test: corpus mutation guard — deletion/rename/injection phantom tripwires at real-repo ambiguity #43
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#43
Loading…
Add table
Add a link
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?
P2 — self-oracling precision guard. Measured 0-defect baseline.
From
_prdoc/records/brainstorm-2026-07-28-oss-corpus-test-system.md§2.2. Depends on #40.Why this is worth having even though it found nothing
The 2026-07-28 spike ran the deletion mutation against 3 real repos and found 0 rebinds out of 1413 previously-resolved cross-file refs (ripgrep
crates/core/flags/parse.rs568 refs, guzzlesrc/Handler/CurlFactory.php787, sinatralib/sinatra/indifferent_hash.rb58). The resolver's honesty held.Its value is therefore regression protection at an ambiguity level the fixtures cannot reach, not discovery.
Concretely: today's
precision_gate.rsdecoys sit in pools of ~1 candidate because the fixtures are 4–14 files — the exact vacuity I033 had to add a structural anti-vacuity guard against. In ts-zod (8489 symbols) or ripgrep (4127) a decoy competes against hundreds of real same-name candidates, deep import graphs, and macro noise. Same oracle cost, far higher discriminating power.Three self-oracling mutation families
write_phantom_decoysfromprecision_gate.rsto corpus scale.All three need no external tooling — ground truth is known by construction.
Method notes from the spike
ATTACHacross the pre- and post-mutation DBs, joining ref sites on(path, start_line, start_col, name)Acceptance
Landed —
crates/indexer/tests/corpus_mutation.rsTwo self-oracling mutation families over all 7 tier-1 repos:
corpus_deletion_never_rebinds— deletes the file whose symbols absorb the most cross-file resolved refs, re-indexes, and requires every ref that resolved into it to become unresolved.corpus_rename_never_binds_elsewhere— renames that file (keeping the extension) and requires each ref to follow it to the new path or decline. Binding to a third file is the phantom.Result
7056 sites across all 7 languages, up from the spike's 1413 across 3. Zero rebinds, zero strays. The resolver's honesty holds at real-repo ambiguity.
Two implementation notes worth keeping
*.renamedmakes the walker stop recognising the language, so the file leaves the index entirely — a deletion wearing a rename's clothes. This was caught by a positive control on the very first run of the sibling suite in #42.Positive controls (#44)
14 per suite: the victim actually absorbs cross-file bindings (an empty site list makes the assertion vacuously true), and the mutation actually landed in the re-index.
Acceptance boxes 1–4 met. Baseline of 0/7056 recorded here so a regression is visible.
Status check — NOT closing. Two of the three mutation families shipped;
injectionwas never built.Acceptance item 1 requires "all three mutation families".
crates/indexer/tests/corpus_mutation.rscovers deletion and rename (corpus_deletion_never_rebinds,corpus_rename_never_binds_elsewhere, both green with the corpus armed, both registering positive controls per #44). The injection family — stage a uniquely-named symbol plus call sites at known coordinates, assert it resolves exactly there and nothing else binds to it — has no implementation in the tree.That third family is the one that generalises
write_phantom_decoysfromprecision_gate.rsto corpus scale, and it is arguably the highest-value of the three: this issue's own rationale is that fixture decoys sit in ~1-candidate pools while a corpus decoy competes against hundreds of real same-name candidates. Deletion and rename prove the resolver doesn't rebind; only injection proves it doesn't over-bind at ambiguity.Recommend one of: build the injection family and then close, or scope this issue down to the two shipped families and re-file injection as its own item so the gap stays visible. Leaving it open as-is risks it reading like a finished suite.
Closing — the injection family landed in
a6b97c0, so all four acceptance items are now met.1. All three families, ≥3 languages incl. ≥2 dynamic —
corpus_injection_binds_only_where_stagedjoins the two existing families. 7/7 tier-1 repos execute, all 7 languages, 4 dynamic. The floor is asserted in code, not just satisfied by luck:so the family cannot silently shrink to one language.
2. Rebind count asserted == 0, offending site printed —
assert!(rebinds.is_empty(), "…PHANTOM rebind(s) after deleting {victim}…"). Demonstrated live by the M1 mutation, which printed real offending sites:3. Positive controls (#44) — 4 per repo, 28 total: the site is dense not a quiet corner; the injected names are unused pre-injection; the injection landed; the call sites landed at their exact coordinates.
4. Baseline recorded — the
0 rebinds / 1413 refsfigure is in the module doc, and every run prints its live equivalent (mutation-delete: N rebind(s) across M previously-resolved cross-file site(s)), so a regression is visible in the log rather than only in a failure.On what makes injection worth the effort — this issue argued it generalises
write_phantom_decoysto corpus scale, and that is what it does. The probe is staged into each repo's densest directory (ripgrep 1315 syms / 370 names, ts-zod 1504 / 1185, guzzle 1326 / 1194), so it competes against hundreds of real same-name candidates. A unique name cannot be reached by name, so the teeth are in the paths that bind without consulting the name — same-dir locality, tier-1Q anchors, tier-1R receivers. The sharp assertion is inertness: 230,508 foreign bindings proved bit-identical before and after across the 7 repos.The mutation proof worth reading — neutering positive-control 4 makes the whole suite pass green having graded nothing (the positive loop iterates over zero targets). That is exactly the near-miss this issue and #44 were filed about, demonstrated rather than asserted, and it is why the control exists.
Honest gaps, recorded in the test's own doc comment rather than here only: the distant import-less call site is UNRESOLVED in all 7 repos and is graded by the negative only (requiring it to resolve would assert recall the resolver does not claim); python-flask and js-express inject into test trees because those genuinely are their densest directories; and this is cold-index only, matching the other two families.