fix(resolve,blast): demote unverified cross-scope guesses; seed diff-blast by changed line range#116
Merged
Conversation
…blast by changed line range Two root causes behind phantom calls edges and over-broad diff-blast. Resolver: the unqualified-name fallback granted caller-grade confidence to leaf-only matches whose qualifier could not be verified. An instance call on an external gem type (`Stripe::StripeError#message`) missed exact lookup and bound its bare leaf to a coincidental same-named test method at full 0.7; bare receiver-unknown calls (`x.body`) bound at 0.5; and a dispatch-kind contradiction (instance call, singleton-only candidate) failed open instead of demoting. Generalize the existing `::`-only cross-namespace demotion into one rule: a qualified target that resolves only by its leaf is demoted to ConfidenceNameCollision (below blast's floor, still counted for dead-code liveness) unless the qualifier is verifiable — the receiver type is an indexed symbol (so an inherited or reopened method is plausible) and the dispatch kind agrees. Bare targets keep base confidence only when the extractor was confident; a bare emit at ConfidenceUnresolved is a receiver-unknown guess and is demoted. filterByReceiver now reports when every candidate's kind contradicted the call so the resolver can demote rather than fail open. Diff-blast: SymbolsInFiles seeded every symbol in any touched file, so a one-line edit to a routes file with hundreds of helpers seeded them all. Replace with GitDiffHunks (git diff -U0) + SymbolsInChangedLines, which seed only symbols whose line span overlaps a changed hunk. Wire into both the CLI and MCP diff paths; remove the now-unused file-granular helpers. Verified on a real Rails app: external-type and bare phantom edges drop from the >=0.5 tier (production->test calls edges 689 -> 465, with the remainder being framework-inherited and exact test-stub matches outside this fix's scope), and diff-blast on a hub-file commit falls from 939 to 82 modified symbols.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two related defects undermined trust in
callsedges and diff-blast on a real Rails codebase:Phantom calls edges. The resolver's unqualified-name fallback granted caller-grade confidence to leaf-only matches whose qualifier could not be verified. An instance call on an external gem type (
Stripe::StripeError#message) missed exact lookup and bound its bare leaf to a coincidental same-named test method at full 0.7; bare receiver-unknown calls (x.body) bound at 0.5; and a dispatch-kind contradiction (instance call, singleton-only candidate) failed open instead of demoting. These plausible-looking-but-wrong edges meant outboundcallscould not be taken at face value.Over-broad diff-blast. Seeding selected every symbol in any touched file, so a one-line edit to a routes file with hundreds of helpers seeded them all — inflating blast radius by an order of magnitude on hub-file commits.
Summary
Make resolver confidence track verifiability, and seed diff-blast from changed line ranges instead of whole files.
Changes
internal/resolve— generalize the existing::-only cross-namespace demotion into one rule: a qualified target that resolves only by its leaf is demoted toConfidenceNameCollision(below blast's floor, still counted for dead-code liveness) unless the qualifier is verifiable — the receiver type is an indexed symbol (so an inherited/reopened method is plausible) and the dispatch kind agrees. Bare targets keep base confidence only when the extractor was confident; a bare emit atConfidenceUnresolvedis a receiver-unknown guess and is demoted.filterByReceivernow reports when every candidate's kind contradicts the call so the resolver demotes rather than fails open.internal/cli+internal/mcpserver— replace file-granularSymbolsInFileswithGitDiffHunks(git diff -U0) +SymbolsInChangedLines, which seed only symbols whose line span overlaps a changed hunk. Wired into both the CLI and MCP diff paths; removed the now-unused file-granular helpers.Architecture Highlights
Child#m→Parent#mstill resolves. Go/Python top-level bare calls (emitted at 1.0) are untouched.--end-of-options,LookPath, preserved stderr). Pure deletions and/dev/nulltargets contribute no ranges.Verification (real Rails app)
callsedges at confidence ≥0.5: 689 → 465 (the StripeClient phantoms are gone; the remainder are framework-inherited and exact test-stub matches that are out of scope for this fix).Test Plan
/dev/null), overlap selection, chunk boundary, scan/query errorsmake cifully green (build, test, lint 0 issues)