doctor index freshness counts never-indexable files as staleness — 35/35 false positives on this repo #210

Closed
opened 2026-09-07 16:54:43 +02:00 by buildagent · 2 comments
Member

Found by dogfooding v0.27.0-rc (caa62fe). Sibling of #209; same file, same surface, different rule broken.

The symptom

After a full, clean, completed reconcile — resolve_scope: Full, 0 drifted, 0 gone from disk — doctor still says:

[ WARN ] index freshness   782 indexed file(s); 0 drifted, 0 gone from disk,
                           35 on disk with no row: fuzz/abi.dict (no row),
                           crates/plugin-host/tests/fixtures/tree-sitter-json.wasm (no row),
                           crates/plugin-host/tests/fixtures/rustc_guest.wasm (no row),
                           crates/plugin-host/tests/fixtures/tree-sitter-spin.wasm (no row),
                           crates/guest/ruby/Cargo.lock (no row) (+30 more)

This WARN is permanent. Those files will never have a row. It cannot be cleared by waiting, re-indexing, or anything else an operator can do.

The product already knows the right answer, and doctor does not ask

index_coverage on the very first path in that list:

{
  "verdict": "never",
  "reason": "ineligible_extension",
  "stage": "classify",
  "proof": { "detail": "no compiled-in language and no package installed in this project
                        claims this extension, and it is in neither text-only allowlist" },
  "hint": "Permanent, at any size, for this project's plugin set."
}

So two surfaces of the same binary disagree about the same path: one proves it can never be indexed, the other counts it as the index being behind.

The population is 100% false positive, measured

Derived independently of doctor (index rows vs git ls-files, so a different code path from the one under test):

tracked files: 822    indexed rows: 782    tracked with NO ROW: 40
  9 .wasm    7 .expected    5 .h    5 .rbx    3 .lock    2 .gitignore
  2 .digest  1 each: .gitattributes .example .dict .toml .fingerprint .cips .wat

The two derivations reconcile: my 40 minus the 5 dot-files doctor's walk excludes = the 35 it reports.

Not one of the 40 is a genuine staleness case. Every extension in that list is one no compiled-in language and no installed package claims. .h C shims, .wasm grammars, .expected golden files, .rbx fixtures, Cargo.lock.

The cause

crates/cli/src/doctor.rs:1204 in check_index_freshness:

// The other direction: files on disk the walker admits that have no
// row at all. Capped, and the cap is disclosed.
for entry in code_index_indexer::walker::walk(root) {
    ...
    if !indexed.contains(rel.as_str()) {
        unindexed += 1;
        ...
    }
}
let behind = missing + drifted + unindexed;

The comment says "files on disk the walker admits" — and that is exactly the bug. Walker admission is about ignore rules and directory pruning; it says nothing about whether any extractor could ever claim the extension. A .wasm is admitted by the walk and then correctly refused at classify. doctor only consults the first stage, so every file that dies at the second stage is counted as the index being behind.

behind then drives the severity, so a repository with any binary fixture warns forever.

Why this matters beyond the noise

This is the I034 brick shape — a never-indexable file read as "the index is behind" — in the surface an operator actually reads. I034 fixed that class in the freshness barrier by classifying with a walker-PROVED not_indexed. doctor never got the same treatment.

And the practical cost is the one that always follows a permanent warning: real drift is now invisible. A genuinely missing row would appear as 36 on disk with no row and nobody would notice.

What the fix has to do

Split the count by the classifier, not by the walk. Three populations, each said separately:

  • coverable, no row yet — genuine staleness. This is what behind should mean, and the only thing that should drive the WARN.
  • provably never indexable — reported as a measurement, not a defect, with the reason (ineligible_extension, too_large, auto_generated, …). Reuse the same classifier index_coverage uses so the two surfaces cannot disagree again.
  • could not classify — its own state; not silently folded into either.

An empty "genuine staleness" list must remain a measurement, not an absence.

Mutations the fix must run

  • Classify a .wasm as coverable → the WARN-is-OK assertion fails.
  • Drop the never-indexable files from the report entirely (rather than reporting them as a measurement) → the disclosure assertion fails, because silence would be the opposite failure.
  • Add a genuinely coverable file with no row → the staleness assertion must fire; if it does not, the check is vacuous.

The test must assert on a fixture containing BOTH a real missing row and an ineligible file, or it cannot tell the two populations apart — which is the defect itself.

Found by dogfooding v0.27.0-rc (`caa62fe`). Sibling of #209; same file, same surface, different rule broken. ## The symptom After a full, clean, completed reconcile — `resolve_scope: Full`, 0 drifted, 0 gone from disk — `doctor` still says: ``` [ WARN ] index freshness 782 indexed file(s); 0 drifted, 0 gone from disk, 35 on disk with no row: fuzz/abi.dict (no row), crates/plugin-host/tests/fixtures/tree-sitter-json.wasm (no row), crates/plugin-host/tests/fixtures/rustc_guest.wasm (no row), crates/plugin-host/tests/fixtures/tree-sitter-spin.wasm (no row), crates/guest/ruby/Cargo.lock (no row) (+30 more) ``` This WARN is **permanent**. Those files will never have a row. It cannot be cleared by waiting, re-indexing, or anything else an operator can do. ## The product already knows the right answer, and `doctor` does not ask `index_coverage` on the very first path in that list: ```json { "verdict": "never", "reason": "ineligible_extension", "stage": "classify", "proof": { "detail": "no compiled-in language and no package installed in this project claims this extension, and it is in neither text-only allowlist" }, "hint": "Permanent, at any size, for this project's plugin set." } ``` So two surfaces of the same binary disagree about the same path: one proves it can never be indexed, the other counts it as the index being behind. ## The population is 100% false positive, measured Derived independently of `doctor` (index rows vs `git ls-files`, so a different code path from the one under test): ``` tracked files: 822 indexed rows: 782 tracked with NO ROW: 40 9 .wasm 7 .expected 5 .h 5 .rbx 3 .lock 2 .gitignore 2 .digest 1 each: .gitattributes .example .dict .toml .fingerprint .cips .wat ``` The two derivations reconcile: my 40 minus the 5 dot-files `doctor`'s walk excludes = the 35 it reports. **Not one of the 40 is a genuine staleness case.** Every extension in that list is one no compiled-in language and no installed package claims. `.h` C shims, `.wasm` grammars, `.expected` golden files, `.rbx` fixtures, `Cargo.lock`. ## The cause `crates/cli/src/doctor.rs:1204` in `check_index_freshness`: ```rust // The other direction: files on disk the walker admits that have no // row at all. Capped, and the cap is disclosed. for entry in code_index_indexer::walker::walk(root) { ... if !indexed.contains(rel.as_str()) { unindexed += 1; ... } } let behind = missing + drifted + unindexed; ``` The comment says "files on disk the walker admits" — and that is exactly the bug. **Walker admission is about ignore rules and directory pruning; it says nothing about whether any extractor could ever claim the extension.** A `.wasm` is admitted by the walk and then correctly refused at `classify`. `doctor` only consults the first stage, so every file that dies at the second stage is counted as the index being behind. `behind` then drives the severity, so a repository with any binary fixture warns forever. ## Why this matters beyond the noise This is the **I034 brick shape** — a never-indexable file read as "the index is behind" — in the surface an operator actually reads. I034 fixed that class in the freshness barrier by classifying with a walker-PROVED `not_indexed`. `doctor` never got the same treatment. And the practical cost is the one that always follows a permanent warning: **real drift is now invisible.** A genuinely missing row would appear as `36 on disk with no row` and nobody would notice. ## What the fix has to do Split the count by the classifier, not by the walk. Three populations, each said separately: * **coverable, no row yet** — genuine staleness. This is what `behind` should mean, and the only thing that should drive the WARN. * **provably never indexable** — reported as a measurement, not a defect, with the reason (`ineligible_extension`, `too_large`, `auto_generated`, …). Reuse the same classifier `index_coverage` uses so the two surfaces cannot disagree again. * **could not classify** — its own state; not silently folded into either. An empty "genuine staleness" list must remain a measurement, not an absence. ## Mutations the fix must run * Classify a `.wasm` as coverable → the WARN-is-OK assertion fails. * Drop the never-indexable files from the report entirely (rather than reporting them as a measurement) → the disclosure assertion fails, because silence would be the opposite failure. * Add a genuinely coverable file with no row → the staleness assertion must fire; if it does not, the check is vacuous. The test must assert on a fixture containing BOTH a real missing row and an ineligible file, or it cannot tell the two populations apart — which is the defect itself.
Author
Member

Additional evidence: two other surfaces already handle this population correctly, so doctor is the outlier rather than the classifier being unavailable.

list_files(path_glob="crates/plugin-host/tests/fixtures/*") — the same three .wasm files doctor counts as staleness:

{
  "results": [],
  "total": 0,
  "not_indexed": [
    { "path": "crates/plugin-host/tests/fixtures/rustc_guest.wasm",       "reason": "not_walker_excluded" },
    { "path": "crates/plugin-host/tests/fixtures/tree-sitter-json.wasm",  "reason": "not_walker_excluded" },
    { "path": "crates/plugin-host/tests/fixtures/tree-sitter-spin.wasm",  "reason": "not_walker_excluded" }
  ],
  "not_indexed_scan": { "directory": "crates/plugin-host/tests/fixtures",
                        "entries_capped": false, "truncated": false }
}

Three things it does right that doctor does not:

  1. They are a SEPARATE population. not_indexed is its own list; they never enter results and never contribute to a count that means "behind".
  2. It states the limit of its own evidence. not_walker_excluded means the walk alone proved nothing — exactly the distinction doctor misses by treating walker admission as coverability.
  3. It names the authority. Its contract says "index_coverage(path) is the full per-path verdict" — it defers rather than guessing.

So the per-path picture across three surfaces on identical paths:

surface verdict on rustc_guest.wasm correct?
index_coverage never / ineligible_extension / stage: classify, with proof, hint: "Permanent" authoritative
list_files separate not_indexed population, not_walker_excluded, defers to index_coverage honest
doctor freshness counted into behind, drives a permanent WARN wrong

This closes off the "the classifier is not reachable from doctor" objection: list_files reaches it from the daemon, and index_coverage is the same crate's own function. doctor consults code_index_indexer::walker::walk and stops.

It also sharpens the fix direction in the issue. list_files' three-way shape — indexed / provably-never / could-not-classify, each named — is the shape doctor's disk side should have, and reusing that classification rather than re-deriving it is what stops the two from disagreeing again. Note list_files is not itself complete (it reports not_walker_excluded rather than the classify-stage reason), so the fix should push both toward the index_coverage verdict rather than copying list_files as-is.

Additional evidence: **two other surfaces already handle this population correctly**, so `doctor` is the outlier rather than the classifier being unavailable. `list_files(path_glob="crates/plugin-host/tests/fixtures/*")` — the same three `.wasm` files `doctor` counts as staleness: ```json { "results": [], "total": 0, "not_indexed": [ { "path": "crates/plugin-host/tests/fixtures/rustc_guest.wasm", "reason": "not_walker_excluded" }, { "path": "crates/plugin-host/tests/fixtures/tree-sitter-json.wasm", "reason": "not_walker_excluded" }, { "path": "crates/plugin-host/tests/fixtures/tree-sitter-spin.wasm", "reason": "not_walker_excluded" } ], "not_indexed_scan": { "directory": "crates/plugin-host/tests/fixtures", "entries_capped": false, "truncated": false } } ``` Three things it does right that `doctor` does not: 1. **They are a SEPARATE population.** `not_indexed` is its own list; they never enter `results` and never contribute to a count that means "behind". 2. **It states the limit of its own evidence.** `not_walker_excluded` means the walk alone proved nothing — exactly the distinction `doctor` misses by treating walker admission as coverability. 3. **It names the authority.** Its contract says *"`index_coverage(path)` is the full per-path verdict"* — it defers rather than guessing. So the per-path picture across three surfaces on identical paths: | surface | verdict on `rustc_guest.wasm` | correct? | |---|---|---| | `index_coverage` | `never` / `ineligible_extension` / `stage: classify`, with proof, `hint: "Permanent"` | authoritative | | `list_files` | separate `not_indexed` population, `not_walker_excluded`, defers to `index_coverage` | honest | | `doctor` freshness | counted into `behind`, drives a permanent WARN | **wrong** | This closes off the "the classifier is not reachable from `doctor`" objection: `list_files` reaches it from the daemon, and `index_coverage` is the same crate's own function. `doctor` consults `code_index_indexer::walker::walk` and stops. It also sharpens the fix direction in the issue. `list_files`' three-way shape — indexed / provably-never / could-not-classify, each named — is the shape `doctor`'s disk side should have, and reusing that classification rather than re-deriving it is what stops the two from disagreeing again. Note `list_files` is not itself complete (it reports `not_walker_excluded` rather than the classify-stage reason), so the fix should push both toward the `index_coverage` verdict rather than copying `list_files` as-is.
Author
Member

Fixed in c566f87, on master, shipping in v0.27.0.

The freshness check counted never-indexable files as staleness — 35 of 35 on this repository — producing a permanent WARN that hid real drift. It now splits the disk side by the same classifier index_coverage uses, and asks the project's own extractor set, so the two surfaces give the same verdict. Live after the fix:

[OK] index freshness … 0 coverable with no row; 35 provably never indexable
     (ineligible_extension x35) … eligibility_source=project

Closing on merge.

Fixed in `c566f87`, on `master`, shipping in v0.27.0. The freshness check counted never-indexable files as staleness — 35 of 35 on this repository — producing a permanent WARN that hid real drift. It now splits the disk side by the same classifier `index_coverage` uses, and asks the project's own extractor set, so the two surfaces give the same verdict. Live after the fix: ``` [OK] index freshness … 0 coverable with no row; 35 provably never indexable (ineligible_extension x35) … eligibility_source=project ``` Closing on merge.
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#210
No description provided.