changed_symbols ships a bare ref_count: 0 where search_symbols says the zero was never measured — same symbol, same index, opposite honesty #213

Closed
opened 2026-09-07 17:06:45 +02:00 by buildagent · 2 comments
Member

Found by dogfooding v0.27.0-rc (caa62fe) against this repository, over the live daemon.

The comparison, one symbol, one index, one generation

Symbol id 745588, MEASURED_LOCK_NS_PER_GENERATION_ROW, a pub const in crates/indexer/src/promotion.rs.

search_symbols:

{
  "ref_count": 0,
  "name_fallback_unmeasured": "no_use_reference_channel"
}

with a note spelling out the consequence:

no USE-BEARING reference row bears that name, AND this index records no reference kind that would capture a USE of a symbol of that kind and language (a const, static, module or impl is used as a bare identifier, which the extractors do not record; an import naming it is not a use). The counter had an empty population, so a 0 would have been structural rather than measured. … ref_count: 0 is not evidence of dead code.

changed_symbols, same id, both detailed and concise:

{
  "id": 745588,
  "name": "MEASURED_LOCK_NS_PER_GENERATION_ROW",
  "kind": "const",
  "ref_count": 0,
  "direct_callers": 0
}

No name_fallback_count. No name_fallback_unmeasured. No note.

This is not a missing nicety — it is the tool's own documented contract

changed_symbols' description states the rule:

Rows carry ref_count (RESOLVED refs only) beside name_fallback_count … A 0 is EARNED — emitted only where some USE-BEARING row bears this name … Absent WITH name_fallback_unmeasured means the counter had nothing to count.

Two states are documented: name_fallback_count present (earned zero), or absent with name_fallback_unmeasured (nothing to count). The rows above are in a third, undocumented state: both absent, silently.

And the index is not guessing about this. project_overview.count_basis MEASURES it on this very index:

"kinds_without_use_channel": [ { "lang": "rust", "kind": "const", "symbols": 1916 }, … ]

1,916 rust consts on which a ref_count of 0 was never measured against anything. search_symbols says so. changed_symbols does not.

Why this is the worst place for the collapse

changed_symbols and review_diff are the tools an agent uses to size a change before making it. In that context ref_count: 0, direct_callers: 0 reads as "nothing depends on this, it is safe to change or delete."

For MEASURED_LOCK_NS_PER_GENERATION_ROW that is exactly wrong: it is consumed by lock_estimate, printed to operators by plugin enable as a predicted lock hold, and asserted against by bench_promotion_lock. It is one of the most load-bearing constants in the crate, and the review surface reports it with the same two zeros it would give genuinely dead code.

kinds_without_use_channel on this index also covers rust module (627), impl (441) and static (89), plus php/typescript/ruby/csharp entries — so this is not one unlucky row. Every changed const, static, module and impl in every diff this tool has ever reported carried an unqualified zero.

The fix

changed_symbols (and review_diff, which shares the mapping) must emit the same three-state field discipline search_symbols, get_symbol and find_callers already implement: name_fallback_count when earned, name_fallback_unmeasured with its reason when the population was empty. The machinery exists and is already correct one call away — this is a surface that did not adopt it, not a mechanism that needs inventing.

Prefer the shared mechanism over a patch in changed_symbols. If the counter and its disclosure were computed together in one place and every tool rendered that, the two surfaces could not have drifted apart. That is the fix worth making; making changed_symbols emit one more field is the fix that lets the next surface drift again.

direct_callers: 0 needs the same treatment or an explicit statement of its own basis — for a const there is no call channel at all, so that zero is structural too.

Mutations the fix must run

  • Suppress name_fallback_unmeasured in changed_symbols → a test asserting the const row carries it must go RED. (Today that test does not exist; write it first and watch it fail against unfixed code.)
  • Change the reason string to a different code → the assertion on the specific reason must go RED, so the test pins the reason and not merely presence.
  • Point the test at a fn instead of a const → it must go RED for the opposite cause, proving the fixture actually exercises a kind with no use channel. A test that passes on both a const and a fn is not measuring this.
  • Make the diff contain ONLY kinds that DO have a use channel → the "earned zero" assertion must fire, so both sides of the three-state split are graded.

Verification that this is a rendering gap and not an index gap

The index holds the right answer: the same id, in the same generation, returns name_fallback_unmeasured: "no_use_reference_channel" through search_symbols and through get_symbol. Only the diff-shaped surfaces drop it.

Related: #209, #210, #212 — all four are the same class, a surface reporting a state it did not measure. This one is distinctive in that the correct answer is already computed and simply not carried across.

Found by dogfooding v0.27.0-rc (`caa62fe`) against this repository, over the live daemon. ## The comparison, one symbol, one index, one generation Symbol id **745588**, `MEASURED_LOCK_NS_PER_GENERATION_ROW`, a `pub const` in `crates/indexer/src/promotion.rs`. `search_symbols`: ```json { "ref_count": 0, "name_fallback_unmeasured": "no_use_reference_channel" } ``` with a `note` spelling out the consequence: > no USE-BEARING reference row bears that name, AND this index records no reference kind that would capture a USE of a symbol of that kind and language (a `const`, `static`, `module` or `impl` is used as a bare identifier, which the extractors do not record; an `import` naming it is not a use). The counter had an empty population, so a `0` would have been structural rather than measured. … **`ref_count: 0` is not evidence of dead code.** `changed_symbols`, same id, both `detailed` and `concise`: ```json { "id": 745588, "name": "MEASURED_LOCK_NS_PER_GENERATION_ROW", "kind": "const", "ref_count": 0, "direct_callers": 0 } ``` No `name_fallback_count`. No `name_fallback_unmeasured`. No note. ## This is not a missing nicety — it is the tool's own documented contract `changed_symbols`' description states the rule: > Rows carry `ref_count` (RESOLVED refs only) beside `name_fallback_count` … A `0` is EARNED — emitted only where some USE-BEARING row bears this name … **Absent WITH `name_fallback_unmeasured` means the counter had nothing to count.** Two states are documented: `name_fallback_count` present (earned zero), or absent **with** `name_fallback_unmeasured` (nothing to count). The rows above are in a third, undocumented state: both absent, silently. And the index is not guessing about this. `project_overview.count_basis` MEASURES it on this very index: ```json "kinds_without_use_channel": [ { "lang": "rust", "kind": "const", "symbols": 1916 }, … ] ``` 1,916 rust consts on which a `ref_count` of 0 was never measured against anything. `search_symbols` says so. `changed_symbols` does not. ## Why this is the worst place for the collapse `changed_symbols` and `review_diff` are the tools an agent uses to **size a change before making it**. In that context `ref_count: 0, direct_callers: 0` reads as *"nothing depends on this, it is safe to change or delete."* For `MEASURED_LOCK_NS_PER_GENERATION_ROW` that is exactly wrong: it is consumed by `lock_estimate`, printed to operators by `plugin enable` as a predicted lock hold, and asserted against by `bench_promotion_lock`. It is one of the most load-bearing constants in the crate, and the review surface reports it with the same two zeros it would give genuinely dead code. `kinds_without_use_channel` on this index also covers rust `module` (627), `impl` (441) and `static` (89), plus php/typescript/ruby/csharp entries — so this is not one unlucky row. Every changed const, static, module and impl in every diff this tool has ever reported carried an unqualified zero. ## The fix `changed_symbols` (and `review_diff`, which shares the mapping) must emit the same three-state field discipline `search_symbols`, `get_symbol` and `find_callers` already implement: `name_fallback_count` when earned, `name_fallback_unmeasured` with its reason when the population was empty. The machinery exists and is already correct one call away — this is a surface that did not adopt it, not a mechanism that needs inventing. **Prefer the shared mechanism over a patch in `changed_symbols`.** If the counter and its disclosure were computed together in one place and every tool rendered that, the two surfaces could not have drifted apart. That is the fix worth making; making `changed_symbols` emit one more field is the fix that lets the next surface drift again. `direct_callers: 0` needs the same treatment or an explicit statement of its own basis — for a `const` there is no call channel at all, so that zero is structural too. ## Mutations the fix must run * Suppress `name_fallback_unmeasured` in `changed_symbols` → a test asserting the const row carries it must go RED. (Today that test does not exist; write it first and watch it fail against unfixed code.) * Change the reason string to a different code → the assertion on the specific reason must go RED, so the test pins the reason and not merely presence. * Point the test at a `fn` instead of a `const` → it must go RED for the opposite cause, proving the fixture actually exercises a kind with no use channel. **A test that passes on both a `const` and a `fn` is not measuring this.** * Make the diff contain ONLY kinds that DO have a use channel → the "earned zero" assertion must fire, so both sides of the three-state split are graded. ## Verification that this is a rendering gap and not an index gap The index holds the right answer: the same id, in the same generation, returns `name_fallback_unmeasured: "no_use_reference_channel"` through `search_symbols` and through `get_symbol`. Only the diff-shaped surfaces drop it. Related: #209, #210, #212 — all four are the same class, a surface reporting a state it did not measure. This one is distinctive in that the correct answer is already computed and simply not carried across.
Author
Member

Additional evidence, same symbol (id 745588), same index, same generation — a third surface, and it makes the case stronger than the original report did.

safe_delete(745588):

{
  "reasons": ["evidence_incomplete", "text_occurrences_found"],
  "resolved_refs": 0,
  "same_name_unresolved": 0,
  "text_occurrence_files": [
    ".forgejo/workflows/ci.yml",
    "_prdoc/records/80-S44-audit.md",
    "crates/indexer/src/collect.rs",
    "crates/indexer/src/confirm.rs",
    "crates/indexer/tests/bench_promotion_lock.rs",
    "crates/indexer/tests/release_gate.rs",
    "crates/indexer/src/promotion.rs"
  ],
  "evidence_gaps": {
    "unmeasured_population": [
      { "symbol_id": 745588, "name": "MEASURED_LOCK_NS_PER_GENERATION_ROW",
        "kind": "const", "reason": "no_use_reference_channel" }
    ],
    "downgraded": {
      "tool": "safe_delete",
      "removed_reasons": [],
      "added_reason": "evidence_incomplete"
    }
  }
}

safe_delete does not merely disclose the empty population — it changes its verdict because of it, and says so: "VERDICT DOWNGRADED: safe_delete removed nothing from reasons and added evidence_incomplete, because its contract is never to report an absence verdict over incomplete evidence."

So the tally on one symbol is now:

tool ref_count discloses the empty population?
search_symbols 0 yes — name_fallback_unmeasured: "no_use_reference_channel" + explanatory note
get_symbol 0 yes — same field
safe_delete 0 yes — unmeasured_population, and downgrades the verdict
changed_symbols 0 no — bare

That closes off the most likely objection to this issue, which is that the disclosure might be expensive or awkward to carry into a diff-shaped reply. Three surfaces already carry it, one of them (safe_delete) computes it per-symbol on exactly this code path and then acts on it. changed_symbols is alone, and it is the surface where a bare 0 does the most damage, because it is read as a go-ahead rather than as a report.

It also sharpens the fix direction argued in the issue: with three correct implementations and one incorrect one, the problem is demonstrably that each surface renders the disclosure itself rather than receiving it alongside the count. The generic fix is to make the counter and its basis travel together as one value; anything else leaves a fifth surface free to drop it again.

Incidental confirmation from the same reply: text_occurrence_files includes .forgejo/workflows/ci.yml, so the CI dot-directory allowlist (#33) is reaching dot-dirs correctly, and text_candidate_window reports candidates_examined: 7, candidates_available: 7, saturated: false — the window cut nothing, so that list is the whole candidate set rather than a prefix.

Additional evidence, same symbol (id 745588), same index, same generation — a **third** surface, and it makes the case stronger than the original report did. `safe_delete(745588)`: ```json { "reasons": ["evidence_incomplete", "text_occurrences_found"], "resolved_refs": 0, "same_name_unresolved": 0, "text_occurrence_files": [ ".forgejo/workflows/ci.yml", "_prdoc/records/80-S44-audit.md", "crates/indexer/src/collect.rs", "crates/indexer/src/confirm.rs", "crates/indexer/tests/bench_promotion_lock.rs", "crates/indexer/tests/release_gate.rs", "crates/indexer/src/promotion.rs" ], "evidence_gaps": { "unmeasured_population": [ { "symbol_id": 745588, "name": "MEASURED_LOCK_NS_PER_GENERATION_ROW", "kind": "const", "reason": "no_use_reference_channel" } ], "downgraded": { "tool": "safe_delete", "removed_reasons": [], "added_reason": "evidence_incomplete" } } } ``` `safe_delete` does not merely disclose the empty population — it **changes its verdict because of it**, and says so: *"VERDICT DOWNGRADED: `safe_delete` removed nothing from `reasons` and added `evidence_incomplete`, because its contract is never to report an absence verdict over incomplete evidence."* So the tally on one symbol is now: | tool | `ref_count` | discloses the empty population? | |---|---|---| | `search_symbols` | 0 | yes — `name_fallback_unmeasured: "no_use_reference_channel"` + explanatory note | | `get_symbol` | 0 | yes — same field | | `safe_delete` | 0 | yes — `unmeasured_population`, **and downgrades the verdict** | | `changed_symbols` | 0 | **no — bare** | That closes off the most likely objection to this issue, which is that the disclosure might be expensive or awkward to carry into a diff-shaped reply. Three surfaces already carry it, one of them (`safe_delete`) computes it per-symbol on exactly this code path and then acts on it. `changed_symbols` is alone, and it is the surface where a bare `0` does the most damage, because it is read as a go-ahead rather than as a report. It also sharpens the fix direction argued in the issue: with three correct implementations and one incorrect one, the problem is demonstrably that each surface renders the disclosure itself rather than receiving it alongside the count. The generic fix is to make the counter and its basis travel together as one value; anything else leaves a fifth surface free to drop it again. Incidental confirmation from the same reply: `text_occurrence_files` includes `.forgejo/workflows/ci.yml`, so the CI dot-directory allowlist (#33) is reaching dot-dirs correctly, and `text_candidate_window` reports `candidates_examined: 7, candidates_available: 7, saturated: false` — the window cut nothing, so that list is the whole candidate set rather than a prefix.
Author
Member

Fixed in 08e4f67 — "a diff row that reports a zero now reports what the zero was measured against" — on master, shipping in v0.27.0.

changed_symbols reported a bare ref_count: 0 for symbol kinds this index measures as having no use channel, where four other surfaces disclosed the basis. The counter and its basis are now a single value, so no surface can render one without the other; direct_callers gained a basis of its own.

The disclosure is carried in a flattened NameFallback { count, unmeasured, shape_excluded } so the copy-back is whole by construction rather than by convention.

Closing on merge.

Fixed in `08e4f67` — *"a diff row that reports a zero now reports what the zero was measured against"* — on `master`, shipping in v0.27.0. `changed_symbols` reported a bare `ref_count: 0` for symbol kinds this index measures as having no use channel, where four other surfaces disclosed the basis. The counter and its basis are now a single value, so no surface can render one without the other; `direct_callers` gained a basis of its own. The disclosure is carried in a flattened `NameFallback { count, unmeasured, shape_excluded }` so the copy-back is whole by construction rather than by convention. 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#213
No description provided.