MemberCensusBasis::unexpanded_supertypes is an unbounded Vec<String> in a reply — no LIMIT, no cap, no truncation disclosure #205

Closed
opened 2026-09-07 08:50:03 +02:00 by buildagent · 2 comments
Member

Found while attributing the agent_task_bench context-ratchet breach (#199's census was most of it). Filed rather than fixed, because the fix I wrote measured zero and I am not shipping a speculative bound.

The fact

crates/daemon/src/refactor.rs:

pub unexpanded_supertypes: Vec<String>,

The query that fills it has no LIMIT, the collect has no .take(), and the field has no truncation sibling. It ships on every safe_delete and check_rename reply where the census fires, and its length is whatever the container's supertype list happens to be.

Every other list in these payloads is bounded and says so — truncated, lines_truncated, dependencies_capped, candidates_available. This one is not.

Why it is filed and not fixed

I wrote the cap (8, plus an unexpanded_supertypes_total for what was cut) and measured it on all three benchmark repos. It changed the numbers by exactly zero — nothing in the pinned corpus has more than 8 unexpanded supertypes, including cs-dapper's SqlMapper, a partial class split across ten files, which was my prime suspect.

So the cap has no caller, no measured effect, and no test that exercises it. Shipping it would have been a bound whose only evidence is that it compiles — and this project has 15k lines of precedent for what that is worth. The right move is to file the hazard and let whoever closes it bring a fixture that can reach the cap.

Why it is still a hazard

The corpus not reaching 8 is a fact about this corpus, not about the field. A deep C# or Python hierarchy, a framework base chain, or a generated class can produce a long list, and the reply would carry all of it with nothing saying it was long. That is the same "unbounded term in somebody's context window" the payload budgets exist to prevent — it is simply invisible here, in the way the tier-1 corpus was once blind to a 58% recall loss because no ripgrep crate is hyphenated.

What closing it needs

  1. A fixture that actually reaches the cap — a container with more unexpanded supertypes than the bound. Without it the cap is ungraded and the same objection applies to the fix as to the hazard.
  2. A cap plus a total, not a bare cut: the predicate behind inherited_surface_unknown only asks whether the list is EMPTY, so truncating is safe for correctness — but a short list must never be readable as a small supertype set.
  3. A measurement on a repo that has one, so the cost being avoided is a number rather than a worry.

What must NOT be done

  • Do not cap it silently. A list cut with no ..._total beside it turns "many" into "few" with no way to tell.
  • Do not drop the list. Its contents are what let a reader recognise the shape — one implicit root object versus a real hierarchy.
  • Do not close it by re-running the pinned corpus and finding nothing over 8. That is the measurement I already have, and it is the reason this is filed rather than fixed.

#199 (the census this field belongs to), and the ratchet.json _moves entry for 2026-09-07, which records the zero-effect measurement in full.

Found while attributing the `agent_task_bench` context-ratchet breach (#199's census was most of it). Filed rather than fixed, because the fix I wrote **measured zero** and I am not shipping a speculative bound. ## The fact `crates/daemon/src/refactor.rs`: ```rust pub unexpanded_supertypes: Vec<String>, ``` The query that fills it has no `LIMIT`, the collect has no `.take()`, and the field has no truncation sibling. It ships on every `safe_delete` and `check_rename` reply where the census fires, and its length is whatever the container's supertype list happens to be. Every other list in these payloads is bounded and says so — `truncated`, `lines_truncated`, `dependencies_capped`, `candidates_available`. This one is not. ## Why it is filed and not fixed I wrote the cap (8, plus an `unexpanded_supertypes_total` for what was cut) and measured it on all three benchmark repos. **It changed the numbers by exactly zero** — nothing in the pinned corpus has more than 8 unexpanded supertypes, including `cs-dapper`'s `SqlMapper`, a partial class split across ten files, which was my prime suspect. So the cap has no caller, no measured effect, and no test that exercises it. Shipping it would have been a bound whose only evidence is that it compiles — and this project has 15k lines of precedent for what that is worth. The right move is to file the hazard and let whoever closes it bring a fixture that can reach the cap. ## Why it is still a hazard The corpus not reaching 8 is a fact about **this corpus**, not about the field. A deep C# or Python hierarchy, a framework base chain, or a generated class can produce a long list, and the reply would carry all of it with nothing saying it was long. That is the same "unbounded term in somebody's context window" the payload budgets exist to prevent — it is simply invisible here, in the way the tier-1 corpus was once blind to a 58% recall loss because no ripgrep crate is hyphenated. ## What closing it needs 1. **A fixture that actually reaches the cap** — a container with more unexpanded supertypes than the bound. Without it the cap is ungraded and the same objection applies to the fix as to the hazard. 2. **A cap plus a total**, not a bare cut: the predicate behind `inherited_surface_unknown` only asks whether the list is EMPTY, so truncating is safe for correctness — but a short list must never be readable as a small supertype set. 3. **A measurement on a repo that has one**, so the cost being avoided is a number rather than a worry. ## What must NOT be done - **Do not cap it silently.** A list cut with no `..._total` beside it turns "many" into "few" with no way to tell. - **Do not drop the list.** Its contents are what let a reader recognise the shape — one implicit root object versus a real hierarchy. - **Do not close it by re-running the pinned corpus and finding nothing over 8.** That is the measurement I already have, and it is the reason this is filed rather than fixed. ## Related #199 (the census this field belongs to), and the `ratchet.json` `_moves` entry for 2026-09-07, which records the zero-effect measurement in full.
Author
Member

Fixed on master at 5ac45d9. You were right to refuse the speculative cap, and you were wrong that no evidence exists — the blocker was corpus selection, not reachability.

The measurement you asked whoever closed this to bring

Per container, using the census's own predicate:

repo containers max over 8
py-django 11,033 5 0
cs-dapper 626 8 0
rust-analyzer 10,062 37 34

Your zero-effect result reproduces exactly, and it is not the whole picture. cs-dapper's SqlMapper — your prime suspect — sits at exactly 8, one name from being cut, which is why the cap is 8 rather than a rounder number. And one repo outside the pinned corpus has 34 containers past it: Expr in crates/hir-def/src/hir.rs alone would have shipped 27 strings into a safe_delete reply.

So the hazard is live rather than speculative, and the corpus was blind to it by construction — the same shape as ripgrep having no hyphenated crate directory. Refusing to ship the cap without a fixture that reaches it was the right call; it just needed a wider net than three repos.

What shipped

SUPERTYPE_LIST_CAP = 8, with unexpanded_supertypes_total beside it, and the three-state discipline enforced rather than described:

  • The total is counted over the full result set on the same cursor, never from the cut vector.
  • It is always present, Some(0) included, so "nothing was cut" is a measurement and not an absence.
  • An old daemon's silence still reads as absent, not as a measured zero.

declared_count_may_understate is unaffected by construction (cap ≥ 1, asserted in a const block), so inherited_surface_unknown is unchanged.

The fixture is one clause, not seven. A deep column on the existing 7-language CENSUS_ROWS table; all seven languages reach 12 through their own declaration syntax — Python base list, C#/PHP/TS interface list, Rust supertrait bound, Ruby include chain.

Payload cost, attributed against master: python-flask +38, cs-dapper +15, rust-ripgrep +16 tokens. Recall identical.

Mutations

# mutation result
M1 drop the .len() < SUPERTYPE_LIST_CAP guard RED — "python: the list must be CUT to the cap" (12 vs 8)
M2 total derived from the cut vector RED — "the deep fixture must name 12 types this index cannot expand, or the cut is ungraded"
M3 non-member reports absent, not a measured zero RED — "python: a measured zero, not an absence"
M4 emit the total only when the cut bites RED — "an uncut list must ship a total EQUAL to its length — present and honest, not absent"
M5 shorten the python fixture below the cap RED — anti-vacuity fires
M8 serde(default) turning an old daemon's silence into Some(0) RED — "a daemon that never reported the total must read as ABSENT, never as a measured zero"
M6 delete the whole #[serde(...)] attribute SURVIVED
M7 delete only skip_serializing_if SURVIVED

I re-ran M4 myself on the merged tree rather than taking the report — EXIT=101, and the assertion names the property rather than a value:

python: an uncut list must ship a total EQUAL to its length — present and honest,
not absent: MemberCensusBasis { … unexpanded_supertypes: ["Alone"],
unexpanded_supertypes_total: None … }

Both survivors are recorded in the test's own doc comment with their reasons, which is the right handling: serde already treats an Option field as optional so M6 changes no behaviour, and the daemon never constructs None so M7's arm is unreachable. What actually keeps the field optional is its type — retyping it u64 is a compile error, not a red assertion. That is a weaker gate than a failing assert, and the lane said so in the code rather than leaving it implied.

A separate defect found here, filed rather than folded in

The field is populated by "any TYPE_POSITION type ref inside the container span, outside every child span" — which is not what its name and doc promise. A Rust enum declares zero child symbols, so Expr's 27 entries are variant payload types, not supertypes; a JavaScript static {} block's new A2() lands there the same way.

Bounding the list does not make that right. Correctly left out of a bounding fix and filed separately — a behaviour change smuggled into a cap would have been unattributable, which is the same reason you refused to ship the cap on its own.

Gates

fmt ✓, clippy -D warnings ✓, cargo test --workspace ✓ (329 suites, 0 failures), COSI_E2E_LEG=daemon ✓ (62 suites), rustdoc -D warnings ✓, doc_citation_gate ✓, corpus_ratchet ✓, precision_gate 7/7 phantom_count == 0 ✓. tests/corpus/baseline.json did not move. Post-merge by me: bounding_site_registry 17 passed, code-index-daemon --lib 194 passed, both EXIT=0.

Fixed on `master` at `5ac45d9`. **You were right to refuse the speculative cap, and you were wrong that no evidence exists — the blocker was corpus selection, not reachability.** ## The measurement you asked whoever closed this to bring Per container, using the census's own predicate: | repo | containers | max | over 8 | |---|---|---|---| | py-django | 11,033 | 5 | 0 | | cs-dapper | 626 | **8** | 0 | | **rust-analyzer** | 10,062 | **37** | **34** | Your zero-effect result reproduces exactly, and it is not the whole picture. `cs-dapper`'s `SqlMapper` — your prime suspect — sits at **exactly 8**, one name from being cut, which is why the cap is 8 rather than a rounder number. And one repo *outside* the pinned corpus has 34 containers past it: `Expr` in `crates/hir-def/src/hir.rs` alone would have shipped **27 strings** into a `safe_delete` reply. So the hazard is live rather than speculative, and the corpus was blind to it by construction — the same shape as ripgrep having no hyphenated crate directory. Refusing to ship the cap without a fixture that reaches it was the right call; it just needed a wider net than three repos. ## What shipped `SUPERTYPE_LIST_CAP = 8`, with `unexpanded_supertypes_total` beside it, and the three-state discipline enforced rather than described: - The total is counted over the **full result set on the same cursor**, never from the cut vector. - It is **always present**, `Some(0)` included, so "nothing was cut" is a *measurement* and not an absence. - An old daemon's silence still reads as **absent**, not as a measured zero. `declared_count_may_understate` is unaffected by construction (cap ≥ 1, asserted in a `const` block), so `inherited_surface_unknown` is unchanged. **The fixture is one clause, not seven.** A `deep` column on the existing 7-language `CENSUS_ROWS` table; all seven languages reach 12 through their own declaration syntax — Python base list, C#/PHP/TS interface list, Rust supertrait bound, Ruby `include` chain. Payload cost, attributed against master: python-flask **+38**, cs-dapper **+15**, rust-ripgrep **+16** tokens. Recall identical. ## Mutations | # | mutation | result | |---|---|---| | M1 | drop the `.len() < SUPERTYPE_LIST_CAP` guard | **RED** — *"python: the list must be CUT to the cap"* (12 vs 8) | | M2 | total derived from the cut vector | **RED** — *"the `deep` fixture must name 12 types this index cannot expand, or the cut is ungraded"* | | M3 | non-member reports absent, not a measured zero | **RED** — *"python: a measured zero, not an absence"* | | M4 | emit the total only when the cut bites | **RED** — *"an uncut list must ship a total EQUAL to its length — present and honest, not absent"* | | M5 | shorten the python fixture below the cap | **RED** — anti-vacuity fires | | M8 | `serde(default)` turning an old daemon's silence into `Some(0)` | **RED** — *"a daemon that never reported the total must read as ABSENT, never as a measured zero"* | | M6 | delete the whole `#[serde(...)]` attribute | **SURVIVED** | | M7 | delete only `skip_serializing_if` | **SURVIVED** | I re-ran **M4** myself on the merged tree rather than taking the report — `EXIT=101`, and the assertion names the property rather than a value: ``` python: an uncut list must ship a total EQUAL to its length — present and honest, not absent: MemberCensusBasis { … unexpanded_supertypes: ["Alone"], unexpanded_supertypes_total: None … } ``` **Both survivors are recorded in the test's own doc comment with their reasons**, which is the right handling: serde already treats an `Option` field as optional so M6 changes no behaviour, and the daemon never constructs `None` so M7's arm is unreachable. What actually keeps the field optional is its **type** — retyping it `u64` is a *compile* error, not a red assertion. That is a weaker gate than a failing assert, and the lane said so in the code rather than leaving it implied. ## A separate defect found here, filed rather than folded in The field is populated by *"any `TYPE_POSITION` type ref inside the container span, outside every child span"* — which is **not what its name and doc promise**. A Rust `enum` declares zero child symbols, so `Expr`'s 27 entries are variant *payload* types, not supertypes; a JavaScript `static {}` block's `new A2()` lands there the same way. Bounding the list does not make that right. Correctly left out of a bounding fix and filed separately — a behaviour change smuggled into a cap would have been unattributable, which is the same reason you refused to ship the cap on its own. ## Gates `fmt` ✓, `clippy -D warnings` ✓, `cargo test --workspace` ✓ (329 suites, 0 failures), `COSI_E2E_LEG=daemon` ✓ (62 suites), rustdoc `-D warnings` ✓, `doc_citation_gate` ✓, `corpus_ratchet` ✓, `precision_gate` 7/7 `phantom_count == 0` ✓. **`tests/corpus/baseline.json` did not move.** Post-merge by me: `bounding_site_registry` 17 passed, `code-index-daemon --lib` 194 passed, both EXIT=0.
Author
Member

Correction to my close, and it invalidates two numbers I quoted as measured

#240 is now fixed (efe6f52), and fixing it showed that two things I wrote above were wrong.

1. "Expr … would have shipped 27 strings into a safe_delete reply" is false. A Rust enum emits zero child symbols — measured, 0 of 825 enums in rust-analyzer — and member_census_basis runs only for a symbol whose parent is a member container. So no enum has ever been reachable through the shipped tool. I asserted a consequence as measured when only the predicate had been measured.

2. The max 37 / over-8 34 table is the same artefact. Those numbers came from running the predicate over every container, not over the population the census can actually reach. Post-#240, with a declared-supertype role instead of span containment, the largest declared supertype list anywhere on the pinned corpus is 5, and nothing reaches 8.

So the honest state of SUPERTYPE_LIST_CAP is the one you refused to ship it in: a cap with no measured caller. It is kept — the list has no structural bound, and the cut is graded at 12 by fixture — but its doc no longer claims to be live on real code.

What survives, and it is the part that mattered. Your refusal to ship a bound whose only evidence is that it compiles was right, and it is right for a reason better than the one I gave when closing this. I answered it with a corpus number that turned out to be an artefact of my own query; the discipline you were applying is what eventually surfaced that, two issues later.

What is genuinely unchanged is the three-state work: unexpanded_supertypes_total is always present, counted over the full result set on the same cursor, so "nothing was cut" is still a measurement rather than an absence — and #240 kept every arm of it.

Leaving this closed; the correction belongs on the record rather than in a reopen.

## Correction to my close, and it invalidates two numbers I quoted as measured #240 is now fixed (`efe6f52`), and fixing it showed that two things I wrote above were wrong. **1. "`Expr` … would have shipped 27 strings into a `safe_delete` reply" is false.** A Rust `enum` emits **zero child symbols** — measured, 0 of 825 enums in rust-analyzer — and `member_census_basis` runs only for a symbol whose *parent* is a member container. So no enum has ever been reachable through the shipped tool. I asserted a consequence as measured when only the predicate had been measured. **2. The `max 37 / over-8 34` table is the same artefact.** Those numbers came from running the predicate over *every* container, not over the population the census can actually reach. Post-#240, with a declared-supertype role instead of span containment, the largest declared supertype list anywhere on the pinned corpus is **5**, and **nothing reaches 8**. So the honest state of `SUPERTYPE_LIST_CAP` is the one you refused to ship it in: a cap with no measured caller. It is kept — the list has no structural bound, and the cut is graded at 12 by fixture — but its doc no longer claims to be live on real code. **What survives, and it is the part that mattered.** Your refusal to ship a bound whose only evidence is that it compiles was right, and it is right for a reason better than the one I gave when closing this. I answered it with a corpus number that turned out to be an artefact of my own query; the discipline you were applying is what eventually surfaced that, two issues later. What is genuinely unchanged is the three-state work: `unexpanded_supertypes_total` is always present, counted over the full result set on the same cursor, so "nothing was cut" is still a measurement rather than an absence — and #240 kept every arm of it. Leaving this closed; the correction belongs on the record rather than in a reopen.
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#205
No description provided.