find_callers: excluded_test_refs conflates resolved and name_fallback, so exclude_tests=true asserts test callers that do not exist #74
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#74
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?
Source: field use on
h-dv/ixt(Rust, ~700 deps, 5185 tests), v0.14.0 (f850c60), Linux. Found because the number was load-bearing for a P1 security severity call, and I reported it to the user before checking it.Summary
find_callers(exclude_tests=true)reportsexcluded_test_refs: Nwith no resolution breakdown, and theconfidenceblock — the thing that makes the count trustworthy — reportspage_name_fallback: 0because it only counts returned rows. So N unresolved same-name matches get presented as N hidden test call sites.The honest signal is lost precisely in the mode an agent uses to decide "is this production code or test-only?", which is the question the parameter exists to answer.
Reproduction
MeshCluster::deploy_module(crates/ixt-core/src/mesh/runtime_mesh.rs:1062), symbol id 176488. A different type in the same repo has its owndeploy_moduletest helper (crates/ixt-core/tests/mission_i144_dynamic_endpoints_tests.rs:67).exclude_tests=true:Reads as: 7 test call sites exist and were hidden. That is what I concluded and stated.
exclude_tests=false, same symbol:All 7 are
name_fallback. Zero resolved. Six are in the file that defines its owndeploy_module, so they are near-certainly calls to that symbol, not this one.Ground truth (established independently by exhaustive grep before I used the tool):
MeshCluster::deploy_modulehas no callers at all outside tests, and plausibly none anywhere.Why this is worse than an ordinary undercount
The two response modes disagree about the same 7 refs, and the mode that hides rows is the one that loses the qualifier:
totalpage_name_fallbackexclude_tests=falseexclude_tests=trueexcluded_test_refs: 7)counts_scope: "returned_page"is documented and technically correct — but withresults: []it makes the block silent exactly whenexcluded_test_refsis the only number on screen. A caveat that evaporates when the rows do is not a disclosure.This also cuts the other way and is the more dangerous direction: a symbol with genuinely resolved test callers and zero collisions produces a byte-identical shape. An agent cannot distinguish "test-only, safe to treat as dead in production" from "name collisions, tells you nothing" — and the first reading is the one that gets acted on.
Suggested fix
Split the counter, so the qualifier travels with the number:
Or keep the scalar and make the confidence block cover excluded rows too (
excluded_resolved/excluded_name_fallback), socounts_scopestops being load-bearing for a mode that returns no rows.Either satisfies the project's own rule from #71 — "a disclosure attached to the number beats one in a schema the agent read 200k tokens ago" — at roughly zero payload cost, since it replaces an integer already being sent.
Worth checking
find_referencesfor the same shape: its docs state identicalexclude_testssemantics, so it likely shares the defect.What worked, for balance
The tool got the underlying question right and beat a hand-rolled search decisively.
search_symbolsdistinguished the two same-nameddeploy_modulemethods by type (ref_count: 0vs18) where grep conflates them — that distinction is exactly how a severity assessment goes wrong. Andresolutionper row, once inspected, settled it in one call. The data is sound; only the summary line misrepresents it.The lesson I took, which may be worth stating in the docs: for a claim you will treat as evidence, read
resolutionper row rather than trusting an aggregate. Right nowexclude_tests=truemakes that impossible, since it removes the rows.Priority note
Filing as Priority/High to match #72, which is the same family — a three-state disclosure where the state nobody worried about turned out to be a promise the code could not keep. Downgrade freely; the fix is small either way. What earns the priority is that this one silently produced a false statement in a security assessment.
Fixed on
masterin8fd2313(I041) and631b735(I042). Not yet released — no version bump or tag.Your report was accurate in every particular, including the part you filed as a suspicion. Thank you for establishing ground truth by hand before reporting; that is what made the diagnosis unambiguous.
Reproduced here, worse
crates/plugins/src/typescript.rsdeclaresparse_withtwice — once inmod tests, once inmod qualifier_tests. 39 bare calls bind to neither:39 claimed hidden test callers, zero known callers, and the two same-named symbols are both test helpers — so the number was wrong in the strongest available sense.
What changed
excluded_test_refsis now an object, not an integer:We took the first of your two suggestions rather than the second, and the type change is the reason: two adjacent scalars can be skimmed past, a field that is no longer a number cannot. Nothing parses it as an integer in code — it is LLM-facing.
Three things beyond what you filed:
find_referencesdoes share the shape — confirmed, and it is worse there. Its fallback arm passes non-call ref kinds through unconditionally, so its excluded scalar mixed resolved refs, unresolved calls, unresolved type refs andmember_accessinto one integer.find_callershad nototal_resolvedat all.find_referencesgained it in #44; this one never did. So itstotalwas a name-fallback-inflated number with no resolved denominator anywhere in the payload, in any mode. It has one now.limit=0reproduced the whole defect. The documented cheap-count path returnedtotal: 39besidepage_resolved: 0, page_name_fallback: 0— your "a caveat that evaporates when the rows do is not a disclosure", firing in a mode nobody had considered. Everyconfidenceblock now carriespage_rows, so a zero can be told apart from an empty page (limit=0, full exclusion, or an offset past the end).Live, after:
Your priority call was right, and it was the same family elsewhere
You filed this Priority/High to match #72. The audit that followed found the same shape in five more places, two of them worse than this one, on the same symbol:
check_rename(parse_with, …)answerededit_sites_total: 1withreasons: ["clean"]for a rename touching 40 sites — the manifest can only list refs that resolved, and nothing reported the ones that did not. It also discardedtext_scan_reliable(presenting a dark FTS channel as a clean one) and wrote the literal string"production"onto a definition living inside#[cfg(test)] mod tests.name_fallback_count: 0andsafe_delete'ssame_name_unresolved: 804described the same symbol. The parent-kind gate admitted onlymethod_callfor a type-family parent, but Rust and PHP emitType::assoc()ascallwith the type inqualifier— so it could not match an associated function at all, and0was vacuous rather than earned while thesearch_symbolsdescription told you it provedref_counttight. That gate now also admits a call whose qualifier names the symbol's own parent.Plus
resolution_gapsnarrowing its whole population underexclude_testswith nothing saying so,change_impact.total_affectedbeing post-filter since I029, and three inherited traps incontext_pack. All fixed in631b735.On your closing lesson
That is the right instinct and we would rather it not be necessary. The split exists so the filtered mode — where the rows are gone and per-row inspection is impossible — carries the same information the rows would have. Where a number genuinely cannot be qualified, it now says so positively: talking to a daemon that predates the split you get
{"total": 39, "breakdown": "unavailable_daemon_predates_split; …"}rather than a bare integer, because absence is not a state.Residuals, stated
name_fallbackbucket is kind-blind onfind_references: a count of 2 can be onecalland onetype. Repo-wide the fallback population is 22kmethod_call+ 12.7kcall+ 7.3ktype, so mixed buckets are the norm. Aby_kindsub-object is one moreSUMin the same pass if it turns out to matter — say so if it does.ref_count + name_fallback_count == find_references.totalinvariant was verified live, but it is the change most worth a second pair of eyes in the field.Closing as fixed. If the split reads wrong in real use — particularly the degraded
breakdownform — please reopen; the payload wording is the part we are least able to evaluate from inside.