change_impact returns an empty, confident answer where find_callers finds 5 call sites — and nothing in its payload says why #122
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#122
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Dogfood finding from #51. Measured while hand-verifying benchmark answers, on real code.
The measurement
Same symbol, same index, same session:
change_impactansweredtest_count: 0at depth 20 for a symbol with five same-name call sites thatfind_callersreturns.The benchmark's "which tests cover W" question came back empty for 7 of 7 hand-verified files across two C# symbols.
The cause is legitimate; the silence is not
change_impacttraverses resolved edges only.find_callersadditionally reports a name-fallback channel. Both behaviours are defensible and deliberate — a reverse closure over unresolved same-name edges would fabricate impact, which is exactly what this project refuses.The defect is that
change_impact's payload contains nothing pointing at the five calls that exist.affected: []withconsulted_files: 1reads as a measured, confident "nothing depends on this". An agent that has learned to trust our disclosure vocabulary — which is what we ask of it — will conclude the symbol has no test coverage and no blast radius.search_symbolssolves this exact problem withname_fallback_count, described as "an UPPER BOUND on how muchref_countundercounts".change_impacthas no equivalent, so its zero cannot be distinguished from an earned one.Why this is a high-value fix
change_impactis the "what breaks if I change Y" tool. An empty answer is the most consequential answer it can give, and it is currently the least qualified one. It is the same asymmetry as #99 — a fact the system knows, disclosed on one surface and silent on its neighbour — but on the tool whose empty result most directly invites a destructive decision.safe_deleteandcheck_renamealready got the stronger treatment in #101: when evidence is incomplete they remove the absence verdict rather than annotating it.change_impactis in the same family and did not.Shape of the fix
Reuse the existing vocabulary rather than inventing a second one. When the seeds have same-name unresolved refs that the resolved-edge traversal did not follow, say so in the payload, per the standing rule that disclosures ride with the data:
Absent when there are none, so a bare
affected: []keeps meaning "measured, and nothing depends on it".Check the neighbours in the same pass rather than patching one tool:
explain_dependencyandrepo_mapseed from the same graph and may carry the same silence. #97 covered all tools by construction for exactly this reason, and #101's grader sits above the router for the same reason.Related
#99 (structural zero, disclosed on one surface), #101 (the downgrade contract for absence verdicts), #118 (a falsely earned zero from
search_symbols). Found by #51's benchmark, which now grades this question class and will keep grading it.Fixed — riding the existing grader, gated on a structural fact rather than a tool list.
Where it lives
It rides
annotate_evidence_gaps, in the sameget_symbolloop #99 already runs (probe_unmeasured_population→probe_seed_symbols), readingname_fallback_countoff the same rowsearch_symbolspublishes it from. No second producer, no second placement — soread_resourcegets it too, by construction, and a future tool 25 is covered without opting in.The gate, and why the first version was wrong
The first attempt was conditional on
name_fallback_count > 0but rode on every id-taking tool. A parallel lane's benchmark caught it immediately: a near-constant +226…+235 tokens on everyfind_callers-shaped answer, four of four growing by the same amount — the signature of an unconditional block, and exactly what this issue must not become.The gate is now one clause on a fact the server already publishes: the block ships only on a reply whose
graph_semanticsbeginsresolved_dependencies. Measured across every id-taking tool, that string cleanly separates:change_impact,explain_dependency,context_pack→ gets the block;find_callers,find_references,find_callees,safe_delete,check_rename→ does not, because they already say in the same sentence that unresolved uses may be absent, and carry the channel row by row.A tool list would have needed maintaining; a structural clause does not.
Measured A/B, old binary vs new
find_callers/find_references/find_calleessafe_delete/check_rename/get_symbolchange_impact/explain_dependency/context_pack, with a findingProse cut 950 → 359 bytes.
Disclosure, not downgrade — and the reason is specific
change_impactrenders no verdict string; there is noreasonsarray to remove from. Andsafe_delete/check_renamealready publishsame_name_unresolved, so downgrading them here would double-count a channel they already report. That is the narrowing #101's lane had to make on evidence, applied up front rather than after a guard fired onWidget.Two corrections to this issue's own assumptions
explain_dependencywas ungraded, becauseseed_symbol_idsread onlysymbol_id/symbol_idsand it takesfrom/to. Now driven by the sharedSYMBOL_ID_ARGSconst that #121's registry pins, so the two halves cannot drift.repo_mapcannot be covered and does not need to be.seed_symbol_ids' doc claimed it takesfocus_ids; it takesfocus, a list of symbol names. The comment was simply wrong, and this issue repeated it.Mutations — five, all run, all red
name_fallback_countarm →change_impact+explain_dependencyred on the reproducing payload, positive control stays green;.filter(|c| *c > 0)→ positive control red, primary green (the pair that proves the block is conditional);find_callersgrows the block, red — the coordinator's regression, now a test;from/tofromSYMBOL_ID_ARGS→ red in two files;SYMBOL_LOCATOR_ARGS→read_code/get_dependenciesred.The fixture reproduces the measurement rather than asserting it:
Telemetry::record(ref_count0,name_fallback_count2,find_callers→ 2 sites allname_fallback) againstQuiet::ping, whose zero is earned because a resolvedpingref exists elsewhere.The residual, itemised and accepted
The benchmark is green on flask (2695 vs 2700, under) and ripgrep (3659 vs 3653, within ceiling). cs-dapper moved 3489 → 3746, and I have accepted and re-recorded it with the reasoning in
tests/bench/ratchet.json's new_movesfield:The entire +257 is two questions, both
which_tests— i.e. bothchange_impact:AsList220→348,ResetTypeHandlers293→427. Everywho_callsquestion is unchanged.Those two are exactly the questions the benchmark scores as MISSED — they return empty while 7 hand-verified test files exist — and the run's own footer says it: "
change_impacthas no such channel, so a question answered this way byfind_callerscomes back EMPTY from the impact tool." The disclosure fires on 2 of 33 corpus questions, and only where the tool would otherwise report a confident zero that invites a destructive decision.~128 tokens is the price of not telling an agent that nothing depends on a symbol when five things do. The available trim (~35 tokens/question, by deleting the "traversing them would fabricate impact" sentence) was declined by the implementing lane as tuning honesty to a threshold, and that judgement is upheld. Re-verified after re-recording: 5 passed, 0 failed, including
the_benchmark_never_writes_its_own_expectations.Reproduced in this repo before anything changed
CodeIndexServer::record(server.rs:499) answeredaffected: [], test_count: 0, consulted_files: 1whilefind_callersreturned its call site at line 17985 markedname_fallback. This issue's C# measurement, reproduced in Rust, in our own tree.