Response::Unclaimed is the same silent-loss door as #115, still open: three causes collapse into one count that reads as the designed case #123

Closed
opened 2026-09-04 17:20:37 +02:00 by buildagent · 1 comment
Member

Found while fixing #115 (a package worker's refusal silently promoted into the active generation). Code-verified, not driven end to end, and deliberately scoped out of that fix rather than absorbed into it.

The gap

extract_one collapses three distinct causes into Response::Unclaimed:

  1. routes to nobody — the designed case, and the overwhelmingly common one;
  2. the file vanished between walk and extract;
  3. non-UTF-8, or a read failure — an accident.

Only the first is benign. The other two are the same shape as #115: a file that should carry facts silently carries none.

Why it survives promotion

write_queue sets file_id, and the carry at crates/indexer/src/generations.rs:754 excludes work='reparse' AND file_id IS NOT NULL with no state filter. So an unclaimed file's active contribution is left behind in the outgoing generation and nothing replaces it in the incoming one.

The only disclosure is the Ready::unclaimed count — and because cause 1 dominates in any real repo, a non-zero unclaimed reads as "these files route to no plugin", which is exactly what an operator expects to see. Causes 2 and 3 hide inside a number that looks healthy.

Why it is worth its own issue

#115's gate asks a precise question — does promoting this generation delete facts the active one is serving for that file? — and answers it for refusals. An unclaimed file never produced a ParseResult::Error, so it never reaches that predicate. The chokepoint #115 established (build.rs:1386 in finish, the only production transition to ready) is the right place to ask the same question about this population, but the population has to be distinguishable first.

Fixing it inside #115 would have meant widening a verified five-link chain on an unverified sixth. That is the trade this project keeps getting right by refusing.

What closing it needs

  1. Split the three causes so the count is not one bucket. Unclaimed should carry which case it was — routing (a measurement), or an accident (a finding).
  2. Run #115's predicate over the accident cases: if promoting would drop facts the active generation is serving for that path, the same gate_failed applies. The machinery now exists; it needs the population.
  3. Three-state discipline: unclaimed_routing: 0 is a measurement; an absent field means this build did not report; the accident list is the finding, absent when empty.
  4. The mutation that must go red: make a file unreadable mid-build with facts already live for it in the active generation, and promotion must refuse — naming the path, not reporting a count.

Split from #115. Same family as #101 (a truncated extraction disclosed on one surface only) and #99. Note also two adjacent facts recorded in #115 and not fixed there: files.parse_error is not restamped by promotion (m0054 names that omission deliberately), so it can disagree with file_contributions.refusal until the next ordinary pass; and plugin_activation has no MCP surface for refusals at all.

Found while fixing #115 (a package worker's refusal silently promoted into the active generation). Code-verified, **not driven end to end**, and deliberately scoped out of that fix rather than absorbed into it. ## The gap `extract_one` collapses **three** distinct causes into `Response::Unclaimed`: 1. **routes to nobody** — the designed case, and the overwhelmingly common one; 2. **the file vanished** between walk and extract; 3. **non-UTF-8, or a read failure** — an accident. Only the first is benign. The other two are the same shape as #115: a file that *should* carry facts silently carries none. ## Why it survives promotion `write_queue` sets `file_id`, and the carry at `crates/indexer/src/generations.rs:754` excludes `work='reparse' AND file_id IS NOT NULL` **with no state filter**. So an unclaimed file's *active* contribution is left behind in the outgoing generation and nothing replaces it in the incoming one. The only disclosure is the `Ready::unclaimed` **count** — and because cause 1 dominates in any real repo, a non-zero `unclaimed` reads as "these files route to no plugin", which is exactly what an operator expects to see. Causes 2 and 3 hide inside a number that looks healthy. ## Why it is worth its own issue #115's gate asks a precise question — *does promoting this generation delete facts the active one is serving for that file?* — and answers it for **refusals**. An unclaimed file never produced a `ParseResult::Error`, so it never reaches that predicate. The chokepoint #115 established (`build.rs:1386` in `finish`, the only production transition to `ready`) is the right place to ask the same question about this population, but the population has to be distinguishable first. Fixing it inside #115 would have meant widening a verified five-link chain on an unverified sixth. That is the trade this project keeps getting right by refusing. ## What closing it needs 1. **Split the three causes** so the count is not one bucket. `Unclaimed` should carry which case it was — routing (a measurement), or an accident (a finding). 2. **Run #115's predicate over the accident cases**: if promoting would drop facts the active generation is serving for that path, the same `gate_failed` applies. The machinery now exists; it needs the population. 3. **Three-state discipline**: `unclaimed_routing: 0` is a measurement; an absent field means this build did not report; the accident list is the finding, absent when empty. 4. The mutation that must go red: make a file unreadable mid-build with facts already live for it in the active generation, and promotion must refuse — **naming the path**, not reporting a count. ## Related Split from #115. Same family as #101 (a truncated extraction disclosed on one surface only) and #99. Note also two adjacent facts recorded in #115 and not fixed there: `files.parse_error` is **not restamped by promotion** (m0054 names that omission deliberately), so it can disagree with `file_contributions.refusal` until the next ordinary pass; and `plugin_activation` has **no MCP surface** for refusals at all.
Author
Member

Triage 2026-09-06 at f6a878a: CLOSING. All four "What closing it needs" items are met, and the two residuals are reported by the product rather than hidden — which is this issue's own standard.

Verified against master and by re-running the tests myself, not from a lane report. Landed in d0279c0.

Against the four numbered items

1. Split the three causes. crates/indexer/src/build.rs:1474-1525 — Unclaimed is now four variants: Routing { reason }, Vanished { at }, ProducedNothing, Failed { lang, detail }. The collapse is unrepresentable rather than merely avoided, while the old collapsed answer stays available to callers who only need the total.

2. Run #115's predicate over the accident cases. This is the part I like: it does not add a second gate. Unclaimed::refusal() (build.rs:1537-1552) makes only Failed owe a refusal, and build.rs:1352 writes that refusal contribution — which manufactures the population #115's gate 5b already reads. So promotion refuses through the chokepoint #115 established, exactly as this issue proposed, with no new five-link chain to verify.

3. Three-state discipline. Ready carries unclaimed_routing, unclaimed_accident_count, unclaimed_accidents: Vec<(path, cause)> and unclaimed_cause_unobserved (build.rs:393-426), summing to unclaimed by construction. A 0 is a measurement; an absent field is "did not report"; the accident list is the finding.

4. The mutation that must go red. an_unreadable_file_with_live_facts_fails_the_generation — crates/indexer/tests/generation_build.rs:3259. It reports the path, not a count.

$ export CARGO_INCREMENTAL=0
$ cargo test -p code-index-indexer --test generation_build -- \
    the_unclaimed_split_is_reported_and_sums_to_the_durable_total \
    an_unreadable_file_with_live_facts_fails_the_generation \
    an_accident_that_loses_no_facts_is_promoted_and_named
test an_accident_that_loses_no_facts_is_promoted_and_named ... ok
test the_unclaimed_split_is_reported_and_sums_to_the_durable_total ... ok
test an_unreadable_file_with_live_facts_fails_the_generation ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 24 filtered out
EXIT=0

Run as uid 1000 (non-root), so the chmod 000 probe was real rather than degrading to its root fallback arm — worth stating, because that test is vacuous under root.

The two residuals, named because they are the reason this is a close and not a hand-wave

Both are recorded in d0279c0's own message and in the tree:

  1. Unclaimed::ProducedNothing still folds "deleted" and "grew past the size cap" — because index::run_task folds both into one Ok(None) (build.rs:1497-1505). This is a finer split than the three causes this issue named, and it sits inside the benign variant, not inside an accident.
  2. The cause split does not survive a cross-process resume, because m0053's CHECK allows no fourth value. That is precisely what unclaimed_cause_unobserved (build.rs:426) reports rather than hides.

Neither is silent, and silence is what this issue was about. The door is shut.

Relationship to #115, since the title asserts one

A correct split, not a duplicate. #115 is closed; what closed it does not by itself cover this, because an unclaimed file never produced a ParseResult::Error and so never reached #115's predicate. #123's fix works by manufacturing the population for #115's existing gate. Both issues were needed.

Not addressed here, and correctly so — the two adjacent facts this issue's own "Related" section flags as recorded-but-not-fixed (files.parse_error not restamped by promotion; plugin_activation having no MCP surface for refusals) belong to their own work, and the second is substantially answered by #124.

🤖 Triage lane, 2026-09-06, master f6a878a

## Triage 2026-09-06 at `f6a878a`: CLOSING. All four "What closing it needs" items are met, and the two residuals are **reported by the product** rather than hidden — which is this issue's own standard. Verified against master and by re-running the tests myself, not from a lane report. Landed in `d0279c0`. ### Against the four numbered items **1. Split the three causes.** `crates/indexer/src/build.rs:1474-1525` — `Unclaimed` is now four variants: `Routing { reason }`, `Vanished { at }`, `ProducedNothing`, `Failed { lang, detail }`. The collapse is **unrepresentable** rather than merely avoided, while the old collapsed answer stays available to callers who only need the total. **2. Run #115's predicate over the accident cases.** This is the part I like: it does **not** add a second gate. `Unclaimed::refusal()` (`build.rs:1537-1552`) makes only `Failed` owe a refusal, and `build.rs:1352` writes that refusal **contribution** — which manufactures the population #115's gate 5b already reads. So promotion refuses through the chokepoint #115 established, exactly as this issue proposed, with no new five-link chain to verify. **3. Three-state discipline.** `Ready` carries `unclaimed_routing`, `unclaimed_accident_count`, `unclaimed_accidents: Vec<(path, cause)>` and `unclaimed_cause_unobserved` (`build.rs:393-426`), summing to `unclaimed` by construction. A `0` is a measurement; an absent field is "did not report"; the accident list is the finding. **4. The mutation that must go red.** `an_unreadable_file_with_live_facts_fails_the_generation` — `crates/indexer/tests/generation_build.rs:3259`. It reports **the path**, not a count. ``` $ export CARGO_INCREMENTAL=0 $ cargo test -p code-index-indexer --test generation_build -- \ the_unclaimed_split_is_reported_and_sums_to_the_durable_total \ an_unreadable_file_with_live_facts_fails_the_generation \ an_accident_that_loses_no_facts_is_promoted_and_named test an_accident_that_loses_no_facts_is_promoted_and_named ... ok test the_unclaimed_split_is_reported_and_sums_to_the_durable_total ... ok test an_unreadable_file_with_live_facts_fails_the_generation ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 24 filtered out EXIT=0 ``` Run as uid 1000 (non-root), so the `chmod 000` probe was **real** rather than degrading to its root fallback arm — worth stating, because that test is vacuous under root. ### The two residuals, named because they are the reason this is a close and not a hand-wave Both are recorded in `d0279c0`'s own message and in the tree: 1. **`Unclaimed::ProducedNothing` still folds "deleted" and "grew past the size cap"** — because `index::run_task` folds both into one `Ok(None)` (`build.rs:1497-1505`). This is a *finer* split than the three causes this issue named, and it sits inside the benign variant, not inside an accident. 2. **The cause split does not survive a cross-process resume**, because m0053's `CHECK` allows no fourth value. That is precisely what `unclaimed_cause_unobserved` (`build.rs:426`) **reports** rather than hides. Neither is silent, and silence is what this issue was about. The door is shut. ### Relationship to #115, since the title asserts one **A correct split, not a duplicate.** #115 is closed; what closed it does **not** by itself cover this, because an unclaimed file never produced a `ParseResult::Error` and so never reached #115's predicate. #123's fix works by *manufacturing the population* for #115's existing gate. Both issues were needed. Not addressed here, and correctly so — the two adjacent facts this issue's own "Related" section flags as recorded-but-not-fixed (`files.parse_error` not restamped by promotion; `plugin_activation` having no MCP surface for refusals) belong to their own work, and the second is substantially answered by **#124**. 🤖 Triage lane, 2026-09-06, master `f6a878a`
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#123
No description provided.