check_rename answers "clean" for a rename that would not compile — nameof(X) is not an edit site and the text backstop is file-level #171
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#171
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?
Severity: the highest in this batch. A refactoring tool that answers
cleanfor a rename that breaks the build is worse than one that refuses to answer. It is a wrong answer wearing the shape of a confident one, andcheck_rename's whole design premise — recorded in its own docs — is never say safe.Found while authoring hand-verified benchmark questions for #51 on the pinned
cs-dappercorpus (sha72a54c475f75e18cb93cba0809d00a5e6e49efd9). Traced to source.Measured
Dapper/SqlMapper.cs:2798is:Rename the method and that line no longer names an identifier that exists. It is a compile error, and it is also a reflection lookup, so even the intent is load-bearing. The tool reported
clean.The occurrence is visible to another tool in this same server —
search_textwithwhole_word: truereturns, forSqlMapper.cs,matches_in_file.lines: [2171, 2222, 2387, 2770, 2798]. Five occurrences;check_renameproduced edit sites for four of them and was silent about the fifth.Mechanism — two halves, both read at source
Half 1:
nameof(X)is deliberately not a ref.crates/plugins/src/csharp.rs::emit_calldrops it by decision, and the decision is documented and defended in three places — a comment at the site, the testnameof_is_not_a_call_ref, and migrationm0028. That decision is correct forfind_references:nameof(X)is a reflective use, not a call site, and thecs-dapperoracle intests/bench/oracle/cs-dapper.jsonsays so in its ownverifiedfield. Nothing here argues it should become a call ref.Half 2: the text backstop is FILE-LEVEL, so it cannot cover the gap half 1 leaves.
check_rename'stext_occurrence_filesexists precisely to catch occurrences the ref layer does not model. But it reports files, and it cannot flag an uncovered occurrence inside a file that already contributed an edit site.SqlMapper.cscontributed five edit sites, so it was treated as covered — and the one occurrence in it that the ref layer never saw disappeared into that coverage.So the two halves compose into a hole neither owns: the ref layer excludes
nameofon purpose, and the backstop's granularity is exactly one level too coarse to notice.Why existing gates could not see it
nameof_is_not_a_call_refgrades the decision, and the decision is right. It says nothing about what depends on that decision downstream.check_rename's own tests are over hand-built fixtures where an uncovered occurrence lives in a file with no edit sites — the case the file-level backstop can see.precision_gatemeasures phantoms (phantom_count == 0), i.e. wrong rows returned. This is a missing row plus a wrong verdict, which that gate is structurally blind to.corpus_ratchetpins counts,corpus_stagepins resolver rules; neither can see areasonsarray.Repro
then, against the same index,
check_renameonSanitizeParameterValue(Dapper/SqlMapper.cs). Compare itsedit_sitesagainstsearch_text("SanitizeParameterValue", whole_word: true)'smatches_in_file.linesforSqlMapper.cs.Relationship to #74's family — a RECURRENCE with a DIFFERENT CAUSE
#74 (closed) is the
find_callersexcluded_test_refsconflation, and its family (I041/I042) includedcheck_renamesaying "clean" for a 40-site rename. That was fixed. This is not that mechanism — it is a new one, and I am stating that as a distinction I can defend at source (nameofexclusion plus file-level backstop granularity) rather than as an assumption about the earlier fix, which I did not re-read.That is the point worth acting on.
cleanhas now been wrong twice, for two unrelated reasons. Fixing this cause and stopping is the fix-per-finding pattern this project rejects. The structural question is whethercheck_renameshould be able to emitcleanat all, or whether the verdict should be expressed as evidence found and evidence classes not covered, so that a class the ref layer deliberately excludes is a disclosed gap rather than an invisible one.What must NOT be done to make this pass
nameof(X)a call ref. It is not a call, three artifacts say so deliberately, and doing it would put a reflective use into everyfind_callersanswer in C#.nameofincheck_rename. The next language will have its own reflective spelling (__name__,::class, a string in a DI registration) and the same hole reopens.clean. Thecs-dapperoracle's truth entry for:2798was correctly removed forfind_references— anameofis genuinely not a call site — and that removal is exactly why this defect now has no test. A question assertingcheck_renamemust not saycleanhere is the one that should exist; it is named as owed work in the #51 comment and was deliberately not added in that round to keep a token attribution clean.What is inferred rather than measured
That the rename would fail to compile is read from the C# language rule for
nameof, not from running a build of Dapper. The uncovered occurrence itself, the seven edit sites, the singletext_occurrence_filesentry and the fivesearch_textline numbers are all measured.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
CONFIRMED, reproduced on a four-line fixture, and fixed — plus a THIRD live instance of the same shape that this issue did not name.
Reproduced before touching anything
The mechanism is exactly as filed. On a minimal C# fixture (no corpus needed), pre-fix:
candidates_available: 1is the whole story in one number: the one trigram candidate WAS the file holding the uncoverednameof(SanitizeParamValue), and it was excluded before verification because it had contributed edit sites. The two halves compose exactly as described.THE THIRD INSTANCE — same sentence, third unrelated cause, no language quirk required
While building the repro I ran the other channel:
A dark text channel produced
clean. The reply says in its ownsemanticsstring that the channel never ran, and the verdict said clean anyway. Any symbol whose name is under three characters.check_rename_discloses_when_the_text_scan_could_not_runalready existed and assertedtext_scan_reliable: false— it never asserted whatreasonssaid.Three causes now. That settles the structural question, so I will answer it first.
The structural question: should
check_renamebe able to emitcleanat all?Yes — but only as a DERIVED verdict over a published channel roster, and that is a different object from what
cleanwas.1. Removing
cleandoes not remove the judgement; it relocates it somewhere with less information and no gate. An agent asking this tool needs a go/no-go. If the tool refuses to render one, every client re-derives it fromreasons,same_name_unresolved,text_scan_reliable,text_candidate_window.saturatedandtruncated— five fields with non-obvious interactions. Each client will get it slightly wrong, and none of those derivations is gradeable by our tests. The verdict is the one artifact we can gate.2. What was wrong all three times was not the word. It was that
cleanwas the ABSENCE OF POSITIVE EVIDENCE.Three different failures, one shape one level up: silence from a channel and silence from a limit rendered identically, and the verdict was computed from silence. Fixing the third cause and stopping would repeat the pattern this issue is objecting to.
3. So
cleanis now derived, and the derivation ships in the payload. Newevidence_channels, one row per channel withstatus: clear | evidence | dark | capped, and:Three consequences that were not available before:
cleanbeside adarkrow is now a self-contradicting payload, andclean_and_the_roster_can_never_disagreegrades exactly that biconditional — over cases that exercise both arms, with an explicitsaw_clean && saw_dirtyguard so the loop cannot pass vacuously.cleaninto a named status:evidence_channel_darkorevidence_capped. The dark-scan case above now answers["evidence_channel_dark"].roster_unavailable— "this reply'scleanwas not derived from a channel roster" — because a pre-#171cleanis a weaker claim and absence is not a state.4. What makes it safe THIS time in a way that was not true the last two times — and the honest bound.
It is not that I believe the roster is complete. It is that the failure mode has changed shape. Before, an unmodelled evidence class produced
cleansilently, and nothing in the payload could contradict it — that is what happened twice. Now, a new class either becomes a channel (andcleanis conditioned on it), or it does not — in which casecleanis still wrong, but the payload publishes exactly which six channels were consulted, so the omission is enumerable from the reply rather than inferable only by reading the implementation.That is a real reduction and it is not a proof. I will not claim
cleancannot be wrong a third time. Named residues:evidence_incomplete(#101) is applied CLIENT-side and removescleanwithout appearing in the roster, so the biconditional holds strictly daemon-side only.CHECK_RENAME_REASONSregistry grades the vocabulary and the ranking, not the emitting body — that bound is written into the test.The mechanism fix — and it names no reflective spelling
The rule shipped is: an EXCLUDED file is not a COVERED file.
Coverage is now compared per LINE: word-boundary occurrences against the
symbols+refsrows the index holds for that name starting on that line. The residue is reported located, not counted:Line 7 is the
nameofline, and nothing else in the file is flagged.It mentions neither
nameofnor C#, which is the point —__name__,::classand a DI registration string are caught by the same clause. And it is not a new idea:safe_deletealready wrote this principle down in its own comment — "whole-file exclusion is sound only when the index EXPLAINS that file's uses of the name" — and then applied it to exactly ONE file, the defining one. This is that same test generalised to every excluded file, in both tools.Per-line rather than per-file arithmetic is load-bearing, and measured: two calls to the same helper on ONE line still reads
clean(occurrences 2, explained 2), where a file-level count comparison would have been just as blind there as it was here.check_renamealso now takes its exclusion set from SQL (every file holding a resolved ref) rather than from the CAPPEDedit_sitesmanifest —safe_deletefixed the same bug on its own side;check_renamestill had it.cleanis still reachable — measured, because a never-clean tool is a different broken toolcleancleanclean///doc commentuncovered_text_occurrencesat that lineuncovered_text_occurrences,occurrences: 2, explained: 1The last two are cases a rename genuinely has to look at, and each arrives with a line number, so dismissing one costs a glance.
Mutations — every one run, real RED pasted
text_occurrences["clean"]darkcollapses to"clear"in the status closurereasons: ["clean"]withtext_scan_reliable: Some(false)roster_is_clearalways false (never-clean)left: ["evidence_capped"] right: ["clean"]cleanpushed unconditionally, roster ignoredmust be exactlyevery channel clear… roster=[… text_occurrences: dark …]uncovered_text_occurrencesrank armroster_unavailablenormalization armcleanunstampedM5 is a finding about my own test, reported rather than hidden. The registry gate's predicate mutation (
rank < 9→rank <= 9, the #178 vacuity shape) passed GREEN on the first attempt. The separateassert_eq!(reason_rank("bogus"), 9)graded the DEFAULT ARM, not the predicate. Rewritten so one named predicate is applied to both populations and must discriminate; re-run, now RED: "THE PREDICATE ITSELF must discriminate: ifexplicitly_rankedis true for an undeclared code it is true for everything."All restores
cp+ md5-verified +touched. Nogit checkout.A pre-existing gap the new registry caught on its first run
check_rename's reason vocabulary is wire contract and is graded by no registry —reason_code_registry.rscovers the #80 families and not these. That absence is arguably the root cause of this whole class:cleanwas pushed by oneif reasons.is_empty()and nothing enumerated what "empty" was supposed to have ruled out.The new
CHECK_RENAME_REASONS+ rank gate went red immediately on real data:cleanitself had no explicitreason_rankand fell through_ => 9, sorting below every real finding. Harmless today because it is only ever emitted alone — which is exactly why nobody would have noticed. Now ranked explicitly.Gates
cargo fmt --all --check0 ·clippy --workspace --all-targets -D warnings0 ·RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items0 (two intra-doc violations found and fixed as citations per the house rule) ·cargo test -p code-index-daemon --lib0 — 175 passed.Three existing tests changed, and the change is itself the finding:
candidates_examinedused to exclude the defining file, anddefining_file_verifiedexisted as the only field that could explain an output list longer than the window accounted for. Since every excluded candidate is now verified, that unexplainable shape is structurally gone, and the test that documented it now asserts the reconciliation directly (text_occurrence_files.len() <= candidates_examined) instead of asserting the escape hatch.defining_file_verifiedis retained and still graded — it is the out-of-window guarantee for a SATURATED scan, which the in-window pass cannot give.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Follow-up: integrating the roster found a FOURTH and FIFTH instance. Five causes, one shape.
My earlier comment named three. Running the roster against the full suite found two more, both in
check_rename, both the same sentence:Fourth — a SATURATED candidate window produced
clean.disclosure_contract_e2e::refactor_tools_disclose_a_saturated_fts_candidate_windowasserted it:reasonsmust containcleanwhiletext_candidate_window.saturated == true, over 200 of 209 candidate files. Its own comment one line above calls that verdict "the one that reads as permission". I053 added the disclosure and left the verdict alone, so the test pinned the defect it named.cappednow withholdscleanandevidence_cappedsays why.Fifth — a NON-EMPTY
text_occurrence_filesproducedclean.graph_e2e's six language fixtures found it. Measured on the C# fixture:check_renamelisted four files containing the old name and had no reason code for them.text_occurrences_foundexisted inreason_rankat rank 1 the whole time — onlysafe_deleteever pushed it. The reply named its own evidence and the verdict still readclean.That one exposed a gap in my own fix: the roster correctly said
evidence, but nothing inreasonsnamed which channel. Socleanwas withheld and the reader could not tell why. Closed with a second invariant — everyevidencechannel must have a reason naming it — graded byevery_evidence_channel_has_a_reason_naming_it, in both directions (aclearchannel must NOT be named, or the map is satisfied by pushing everything always). Mutation run: delete thetext_occurrences_foundpush → RED,channel `text_occurrences` reports `evidence` and no reason starting `text_occurrences_found` names it — the verdict is withheld and the reader cannot tell WHY: reasons=["evidence_capped"].graph_e2e's assertion wasreasons.contains("clean") == (missed == 0)— a one-channel derivation. It is now the full derivation (cleaniff every roster row isclear), which is strictly stronger and cannot be satisfied by a tool that stops consulting a channel.Five causes, all the same shape, which is the answer to the structural question restated as evidence rather than argument:
cleanNot one of them is a language quirk. All five are "silence from a limit is indistinguishable from silence from a clear channel", and only #2 was filed.
Two more gates caught real things in my own work — reporting both
ref_kind_stance_registryrefused the newexplained_linesuntil it declared which ref kinds its SQL names and whether an import reaches its result. That forced a decision I would otherwise have made implicitly: imports DO count as explained. Animportref is not a use (m0038) but this population is occurrences of a spelling, anduse crate::a::helper;spellshelper. Excluding it would have putuncovered_text_occurrenceson essentially every Rust and PHP rename — a disclosure that fires on everything, which is one bad decision away from the failure mode this fix exists to remove.generation_build's per-file reader floor caught a#[cfg(test)]I added ABOVE the production SQL: the registry scanner cuts each file at the first column-zero#[cfg(test)], sorefactor.rsdropped from 12 detected row-table reads to 1. The gate's own doc says the sum-over-four-files floor exists so a file cannot silently stop being scanned — it did exactly that job. Helper moved into the test module.Final gates on the integrated tree
fmt --all --check0 ·clippy --workspace --all-targets -D warnings0 ·RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items0 ·RUSTFLAGS="-D warnings" cargo check --target x86_64-pc-windows-gnu --workspace --all-targets0 ·COSI_E2E_LEG=daemon cargo test -p code-index-mcp0 — 52 suites, 0 failed ·cargo test --workspacewith the corpus env — 303 suites ok, 2 failed, both the deliberately-unblessed corpus records (corpus_structural_counts_match_the_baseline,corpus_resolver_stages_match_the_baseline) ·precision_gate7/7,phantoms=0andrecall=1.000in every language.Also worth recording:
startup_payload_budget_e2erejected my description prose — the additions put the payload 255 tokens over the 16555 budget, and its message is "TRIM, do not raise: a raise is a bill sent to every session." Trimmed to fit by moving the substance into the field docs and deleting a sentence abouttext_scan_reliable: falsethatevidence_channels'darkstatus now says structurally and for free. The house rule that disclosures belong in the payload landed on this change directly.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
CLOSING — fixed at source, graded by two mutation-named tests, and demonstrated live
Close-out lane. Verified on master
552e3a2; code read with this repo's own tools, not grep.On master
nameof(X)occurrence is not a ref and has no channelEvidenceChannelroster (crates/daemon/src/refactor.rs:~1553) withtext_occurrencesanduncovered_occurrencesas first-class channelsuncovered_occurrencesis per-LINE (UncoveredOccurrence,refactor.rs:279);explained_pathscomes from SQL, not from the capped manifest (:1513)cleananywayroster_is_clear(refactor.rs:351) —cleanand the roster cannot disagreeCHECK_RENAME_REASONSregistry (refactor.rs:~148),ROSTER_UNAVAILABLE(~130)Graded, and the mutations are named
refactor::tests::clean_and_the_roster_can_never_disagreeandrefactor::tests::every_evidence_channel_has_a_reason_naming_it— both ran here as part ofcargo test -p code-index-daemon --lib, EXIT=0, 179 passed / 0 failed.Live, on a different language than the one it was filed for
check_renamethrough the real MCP server, onname_identifier(itself the #172 artifact), withheldcleanand produced the roster:Those six lines are
.and_then(name_identifier)— a function-pointer pass the ref layer never emits. That is exactly the class this issue was filed about, caught in the wild on C# rather than on the filed fixture.The two bounds the lane named are limits, not residual defects
Client-side
evidence_incompletesits outside the roster, and a class that is no channel is still invisible. The first is disclosed in the payload — it appears in the probe above as adowngradedblock — which is the standard this project holds disclosures to. The second is a true statement about any roster and is not something this issue can close.Closing.