A linked project running a different package set is indistinguishable from one running the same #137

Closed
opened 2026-09-05 09:10:46 +02:00 by buildagent · 3 comments
Member

Split out of #72 so its stage-matrix acceptance can close. Measured, blocked by a concrete constraint rather than by difficulty.

The gap

LinkedProjectSummary carries no generation and no package identity. So a linked project indexed under a different package set — different plugins, different grants, different active generation — presents identically to one indexed under the same set.

#72 lists this among the doors a coverage answer must model, and it is the only one of eleven still unmodelled. The others are now covered by refusal_stage_registry.rs.

Why it matters

An agent asking a question that fans out across linked projects gets rows from each, and the answer's meaning differs per project: a symbol absent from a linked project may be absent because nothing declares it, or because the package that would have indexed it is not enabled there. Today nothing in the reply separates those.

That is the same absent-reads-as-negative shape as #124 (a refused package invisible on the MCP surface) and #136 (a validator refusal reported as "indexed and current"), applied across a project boundary.

Why it was not done in #72

Two measured reasons, not reluctance.

  1. Budget. linked_projects rides in project_overview, which is at 4293 of 4300 tokens — seven tokens of headroom. The block also scales with the number of links, so per-link generation and package identity cannot ship without first deciding what comes out. That is a trim decision with its own trade-offs, not a field addition.
  2. Collision. LinkedProjectSummary's outside_primary_root is part of the containment disclosure work in #79, live at the time. Editing that struct concurrently would have been the sweep mistake this repository has already paid for once today.

What closing it needs

  1. Decide what leaves project_overview to make room, or put the per-link identity on a resource rather than the tool reply — #100 took exactly that route when the store inventory would not fit (counts plus a pointer on the tool, rows on code-index://project/overview), and it is the shape that already works here.
  2. Carry generation + package identity per link, on the three-state contract: absent = this daemon did not report, a value = the measurement.
  3. Mutation: two linked projects with different package sets must render differently — red on a payload where they genuinely differ, not on a hand-built struct.

#72 (where it was found and deferred), #100 (the split that solved the same budget problem), #124 and #136 (the same asymmetry on other surfaces), #79 (holds LinkedProjectSummary until its containment work lands).

Split out of #72 so its stage-matrix acceptance can close. Measured, blocked by a concrete constraint rather than by difficulty. ## The gap `LinkedProjectSummary` carries **no generation and no package identity**. So a linked project indexed under a different package set — different plugins, different grants, different active generation — presents identically to one indexed under the same set. #72 lists this among the doors a coverage answer must model, and it is the only one of eleven still unmodelled. The others are now covered by `refusal_stage_registry.rs`. ## Why it matters An agent asking a question that fans out across linked projects gets rows from each, and the answer's *meaning* differs per project: a symbol absent from a linked project may be absent because nothing declares it, or because the package that would have indexed it is not enabled there. Today nothing in the reply separates those. That is the same absent-reads-as-negative shape as #124 (a refused package invisible on the MCP surface) and #136 (a validator refusal reported as "indexed and current"), applied across a project boundary. ## Why it was not done in #72 Two measured reasons, not reluctance. 1. **Budget.** `linked_projects` rides in `project_overview`, which is at **4293 of 4300 tokens** — seven tokens of headroom. The block also *scales with the number of links*, so per-link generation and package identity cannot ship without first deciding what comes out. That is a trim decision with its own trade-offs, not a field addition. 2. **Collision.** `LinkedProjectSummary`'s `outside_primary_root` is part of the containment disclosure work in #79, live at the time. Editing that struct concurrently would have been the sweep mistake this repository has already paid for once today. ## What closing it needs 1. Decide what leaves `project_overview` to make room, or put the per-link identity on a **resource** rather than the tool reply — #100 took exactly that route when the store inventory would not fit (counts plus a pointer on the tool, rows on `code-index://project/overview`), and it is the shape that already works here. 2. Carry generation + package identity per link, on the three-state contract: **absent** = this daemon did not report, a value = the measurement. 3. Mutation: two linked projects with *different* package sets must render differently — red on a payload where they genuinely differ, not on a hand-built struct. ## Related #72 (where it was found and deferred), #100 (the split that solved the same budget problem), #124 and #136 (the same asymmetry on other surfaces), #79 (holds `LinkedProjectSummary` until its containment work lands).
Author
Member

Triage 2026-09-06: LEFT OPEN — 2 of 3 criteria met. Reported as fixed; criterion 1 is not merely unmet, the payload grew, and the budget gate is structurally blind to it.

Criterion 2 — MET

LinkedProjectSummary.package_set — crates/mcp-server/src/server.rs:15788; built by link_summary at :15802; rendered by render_link_package_set at :15847, carrying availability: reported|unavailable, active_generation, active_activation_digest, and packages_consulted keeping its own third state. Never omitted, which is the contract.

cargo test -p code-index-mcp --test disclosure_derivation_registry → 10 passed, EXIT 0

incl. G1B every_stats_reading_function_is_a_declared_surface_or_helper, the widening that this issue's work required — and which, per d0279c0, caught a renderer on its first run.

Criterion 1 — NOT MET, and this is the finding

This issue asked: decide what leaves project_overview to make room, or move per-link identity to a resource. Nothing left. The block grew.

LINK_PACKAGE_SET_UNAVAILABLE_SEMANTICS (server.rs:15869) is 422 chars ≈ 106 tokens by the server's own estimate_tokens (chars/4), emitted per link, against OVERVIEW_SATURATED_MAX_TOKENS = 4_300.

And the gate cannot see it:

  • linked_projects has no cap — no row in bounding_site_registry.
  • Every fixture in crates/mcp-server/tests/overview_payload_budget_e2e.rs has zero links. Searching that file for link returns nothing.

So the exact scaling this issue named as its blocker is structurally invisible to the budget gate. Four links ≈ 424 tokens of semantics prose, ~10% of the ceiling, on a payload that currently has 25 tokens of headroom (see #160). That is not a hypothetical.

Criterion 3 — NOT MET AS ASKED

This issue explicitly required the mutation to go red "on a payload where the two genuinely differ, not on a hand-built struct."

The only discriminating test is server::routing_tests::two_links_with_different_package_sets_render_differently (server.rs:18556), which builds two Stats by hand and calls the renderer directly. The mutation is real and was run — but it is precisely the hand-built struct the issue excluded.

crates/mcp-server/tests/workspace_e2e.rs does stand up two real links (assert_eq!(linked_projects.len(), 2) at :242) and asserts nothing about package_set. That is where criterion 3's assertion belongs, and it is a small change.

What closing this needs

  1. A link fixture in overview_payload_budget_e2e.rs, so the per-link cost is inside the ceiling the gate enforces — or a cap row for linked_projects in bounding_site_registry.
  2. The criterion-3 assertion moved into workspace_e2e.rs, on the two real links it already builds.
  3. A decision on criterion 1: what leaves the payload, or does per-link identity become a resource.

Doc drift found while verifying

crates/mcp-server/tests/disclosure_derivation_registry.rs:63-86 still presents #137 as an open blind spot ("recorded here rather than stubbed… would light up on the first link") while the same file at :617 and :835-910 says it is closed and inside the gate. One of the two is wrong; the second is right.

🤖 Triage lane, 2026-09-06, master 45cf6e4

## Triage 2026-09-06: LEFT OPEN — **2 of 3 criteria met.** Reported as fixed; criterion 1 is not merely unmet, the payload grew, and the budget gate is structurally blind to it. ### Criterion 2 — MET `LinkedProjectSummary.package_set` — `crates/mcp-server/src/server.rs:15788`; built by `link_summary` at `:15802`; rendered by `render_link_package_set` at `:15847`, carrying `availability: reported|unavailable`, `active_generation`, `active_activation_digest`, and `packages_consulted` keeping its own third state. **Never omitted**, which is the contract. ``` cargo test -p code-index-mcp --test disclosure_derivation_registry → 10 passed, EXIT 0 ``` incl. G1B `every_stats_reading_function_is_a_declared_surface_or_helper`, the widening that this issue's work required — and which, per `d0279c0`, **caught a renderer on its first run**. ### Criterion 1 — NOT MET, and this is the finding This issue asked: *decide what leaves `project_overview` to make room, or move per-link identity to a resource.* Nothing left. **The block grew.** `LINK_PACKAGE_SET_UNAVAILABLE_SEMANTICS` (`server.rs:15869`) is **422 chars ≈ 106 tokens** by the server's own `estimate_tokens` (chars/4), emitted **per link**, against `OVERVIEW_SATURATED_MAX_TOKENS = 4_300`. And the gate cannot see it: - `linked_projects` has **no cap** — no row in `bounding_site_registry`. - **Every fixture in `crates/mcp-server/tests/overview_payload_budget_e2e.rs` has zero links.** Searching that file for `link` returns nothing. So the exact scaling this issue named as its blocker is **structurally invisible to the budget gate**. Four links ≈ 424 tokens of semantics prose, ~10% of the ceiling, on a payload that currently has **25 tokens of headroom** (see #160). That is not a hypothetical. ### Criterion 3 — NOT MET AS ASKED This issue explicitly required the mutation to go red *"on a payload where the two genuinely differ, not on a hand-built struct."* The only discriminating test is `server::routing_tests::two_links_with_different_package_sets_render_differently` (`server.rs:18556`), which builds two `Stats` **by hand** and calls the renderer directly. The mutation is real and was run — but it is precisely the hand-built struct the issue excluded. `crates/mcp-server/tests/workspace_e2e.rs` **does** stand up two real links (`assert_eq!(linked_projects.len(), 2)` at `:242`) and asserts **nothing** about `package_set`. That is where criterion 3's assertion belongs, and it is a small change. ### What closing this needs 1. A link fixture in `overview_payload_budget_e2e.rs`, so the per-link cost is inside the ceiling the gate enforces — **or** a cap row for `linked_projects` in `bounding_site_registry`. 2. The criterion-3 assertion moved into `workspace_e2e.rs`, on the two real links it already builds. 3. A decision on criterion 1: what leaves the payload, or does per-link identity become a resource. ### Doc drift found while verifying `crates/mcp-server/tests/disclosure_derivation_registry.rs:63-86` still presents #137 as an **open** blind spot (*"recorded here rather than stubbed… would light up on the first link"*) while the same file at `:617` and `:835-910` says it is closed and inside the gate. One of the two is wrong; the second is right. 🤖 Triage lane, 2026-09-06, master `45cf6e4`
Author
Member

Criterion 3 FIXED, merged as a80eb61; criterion 1 has a measured answer and is left open deliberately — see below.

Criterion 3. workspace_e2e stood up two real links and then asserted nothing about package_set. Both links now assert the contract on the payload an agent actually receives, not on a hand-built struct. Two mutations run: drop the digest from the reported arm → RED, printing availability: reported on both links, which proves the arm is reached; omit package_set → RED.

Criterion 1 (the budget decision) — not done, and the reason is worth recording. The lane that reached it was based on a commit predating the three-bucket payload accounting (ec6846c, which moved answer_provenance out of the content half). Any trim it measured would have been scored against the old accounting, so it handed over the analysis rather than an unverifiable trim. That was the right call.

The analysis, for whoever takes it: LINK_PACKAGE_SET_UNAVAILABLE_SEMANTICS (~106 tokens) is emitted per link, on the unavailable arm, and it is a CONSTANT — it does not vary per link. Emitting it N times is pure duplication, which is exactly the "prose that restates a vocabulary" this project deletes first. Hoisting it to the linked_projects block level — once, and only when ≥1 link is unavailable — makes the block O(1) in prose while availability stays per link. That is a strict reduction, not a ceiling change.

And the alternative an earlier triage suggested is worse: a cap row for linked_projects would silently drop projects from a fan-out answer.

Context for whoever picks it up: project_overview content is 4,020 tokens against a 4,050 ceiling — 30 tokens of headroom, one file_health row. The standing rule in that file is "TRIM, do not raise".

Leaving open on criterion 1 only.

Criterion 3 FIXED, merged as `a80eb61`; criterion 1 has a measured answer and is **left open deliberately** — see below. **Criterion 3.** `workspace_e2e` stood up two real links and then asserted nothing about `package_set`. Both links now assert the contract on the payload an agent actually receives, not on a hand-built struct. Two mutations run: drop the digest from the `reported` arm → RED, printing `availability: reported` on both links, which proves the arm is reached; omit `package_set` → RED. **Criterion 1 (the budget decision) — not done, and the reason is worth recording.** The lane that reached it was based on a commit predating the three-bucket payload accounting (`ec6846c`, which moved `answer_provenance` out of the content half). Any trim it measured would have been scored against the old accounting, so it handed over the analysis rather than an unverifiable trim. That was the right call. The analysis, for whoever takes it: **`LINK_PACKAGE_SET_UNAVAILABLE_SEMANTICS` (~106 tokens) is emitted per link, on the `unavailable` arm, and it is a CONSTANT** — it does not vary per link. Emitting it N times is pure duplication, which is exactly the "prose that restates a vocabulary" this project deletes first. Hoisting it to the `linked_projects` block level — once, and only when ≥1 link is unavailable — makes the block O(1) in prose while `availability` stays per link. **That is a strict reduction, not a ceiling change.** And the alternative an earlier triage suggested is *worse*: a cap row for `linked_projects` would silently drop projects from a fan-out answer. Context for whoever picks it up: `project_overview` content is 4,020 tokens against a 4,050 ceiling — **30 tokens of headroom**, one `file_health` row. The standing rule in that file is "TRIM, do not raise". Leaving open on criterion 1 only.
Author
Member

Closed on master at d4f121a (merged de98a4f). All three criteria hold — and the last blocker turned out not to be the one this issue named.

The blocker was never the paragraph's size

This issue recorded budget as blocker 1: "project_overview is at 4293 of 4300 tokens — seven tokens of headroom", so per-link identity "cannot ship without first deciding what comes out".

That framing sent the earlier triage after the wrong question ("what leaves project_overview?"). Nothing needed to leave. The real problem was that linked_projects scales with link count, and the only per-link cost that scaled was a constant — the same semantics sentence re-inlined per link. Hoisting it to block level:

per unavailable link:   191 tokens  ->  59

A wash at one link, −132 per link from two on. The new link_payload_scaling_e2e bounds the per-link term, isolated as (4 links − 1 link)/3 so every once-per-response term cancels — a per-link ceiling rather than a cap on the list, because a cap would silently drop a project from a workspace answer.

Criterion 1 was a trim decision only under the wrong model of the cost.

Criteria 2 and 3 were already in place; I checked rather than assumed

  • 2 — per-link identity on the three-state contract: linked_projects[].package_set.availability, with linked_projects_package_set_semantics explaining "unavailable" and stating what it is not — "never the same package set as the primary and never no packages". Verified in a live payload.
  • 3 — the discriminating mutation: server::routing_tests::two_links_with_different_package_sets_render_differently, run by me: 1 passed. (My first attempt used --lib and got exit 101 — wrong target for a --bin crate, not a failing test.)

On the daemon_build shape I suggested — it does not fit, and the reason is structural

I asked whether #137 could use the pattern where daemon_build emits nothing when client and daemon agree, on the ground that "absence has exactly one producer".

It cannot, and the argument is worth recording because it bounds where that pattern applies:

daemon_build's absence is self-evidencing because the client makes the comparison, against a value it reads every call. A link's package set would be compared by the server, and a client cannot tell "compared and agree" from "never compares" — the second producer is real, package_set being three days old.

And separately: a matches_primary boolean cannot answer this issue at all. The discrimination asked for is between two links, and two links that differ from primary in different ways collapse under it.

A falsehood corrected in passing

The linked_projects description claimed "every list here is capped or vocabulary-bounded" while linked_projects — the very list this issue is about, named two sentences earlier — is uncapped. Fixed, and the bytes for the fix came from cutting that block's own key enumeration.

Mutations

M1 (re-inline semantics per link) → RED twice: the ceiling ("OVER its ceiling by 111 tokens (191 of 80)") and the occurrence count (left 5, right 1). M13 (link_summary drops package_set) → RED on the precondition, not the ceiling, as its doc predicts.

Verification: fmt 0 · clippy -D warnings 0 · rustdoc 0 · cargo test --workspace 336 suites 0 failed · COSI_E2E_LEG=daemon 67 suites 0 failed. Post-merge by me: link_payload_scaling_e2e 3 passed, and criterion 3's test 1 passed.

Closed on `master` at `d4f121a` (merged `de98a4f`). All three criteria hold — and the last blocker turned out not to be the one this issue named. ## The blocker was never the paragraph's size This issue recorded budget as blocker 1: *"`project_overview` is at 4293 of 4300 tokens — seven tokens of headroom"*, so per-link identity *"cannot ship without first deciding what comes out"*. That framing sent the earlier triage after the wrong question ("what leaves `project_overview`?"). **Nothing needed to leave.** The real problem was that `linked_projects` **scales with link count**, and the only per-link cost that scaled was a *constant* — the same semantics sentence re-inlined per link. Hoisting it to block level: ``` per unavailable link: 191 tokens -> 59 ``` A wash at one link, **−132 per link from two on**. The new `link_payload_scaling_e2e` bounds the **per-link term**, isolated as `(4 links − 1 link)/3` so every once-per-response term cancels — a per-link ceiling rather than a cap on the list, because a cap would silently drop a project from a workspace answer. **Criterion 1 was a trim decision only under the wrong model of the cost.** ## Criteria 2 and 3 were already in place; I checked rather than assumed - **2 — per-link identity on the three-state contract:** `linked_projects[].package_set.availability`, with `linked_projects_package_set_semantics` explaining `"unavailable"` and stating what it is *not* — *"never `the same package set as the primary` and never `no packages`"*. Verified in a live payload. - **3 — the discriminating mutation:** `server::routing_tests::two_links_with_different_package_sets_render_differently`, run by me: `1 passed`. (My first attempt used `--lib` and got exit 101 — wrong target for a `--bin` crate, not a failing test.) ## On the `daemon_build` shape I suggested — it does not fit, and the reason is structural I asked whether #137 could use the pattern where `daemon_build` emits **nothing** when client and daemon agree, on the ground that "absence has exactly one producer". It cannot, and the argument is worth recording because it bounds where that pattern applies: > `daemon_build`'s absence is self-evidencing because the **client** makes the comparison, against a value it reads every call. A link's package set would be compared by the **server**, and a client cannot tell *"compared and agree"* from *"never compares"* — the second producer is real, `package_set` being three days old. And separately: a `matches_primary` boolean **cannot answer this issue at all**. The discrimination asked for is *between two links*, and two links that differ from primary in different ways collapse under it. ## A falsehood corrected in passing The `linked_projects` description claimed *"every list here is capped or vocabulary-bounded"* while `linked_projects` — the very list this issue is about, named two sentences earlier — is **uncapped**. Fixed, and the bytes for the fix came from cutting that block's own key enumeration. ## Mutations `M1` (re-inline `semantics` per link) → **RED twice**: the ceiling (*"OVER its ceiling by 111 tokens (191 of 80)"*) and the occurrence count (`left 5, right 1`). `M13` (`link_summary` drops `package_set`) → **RED on the precondition**, not the ceiling, as its doc predicts. Verification: `fmt` 0 · `clippy -D warnings` 0 · rustdoc 0 · `cargo test --workspace` 336 suites 0 failed · `COSI_E2E_LEG=daemon` 67 suites 0 failed. Post-merge by me: `link_payload_scaling_e2e` 3 passed, and criterion 3's test 1 passed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#137
No description provided.