Skip to content

fix(resolve): gate the exact byQualified path; never bind production code to test symbols#117

Merged
luuuc merged 1 commit into
mainfrom
fix/production-to-test-edge-gate
May 31, 2026
Merged

fix(resolve): gate the exact byQualified path; never bind production code to test symbols#117
luuuc merged 1 commit into
mainfrom
fix/production-to-test-edge-gate

Conversation

@luuuc
Copy link
Copy Markdown
Owner

@luuuc luuuc commented May 31, 2026

Problem

Follow-up to #116. After that fix, a real Rails codebase still showed 603 production→test structural edges (465 calls + 138 references) where production code resolved a call/reference to a symbol defined in a test file. Root cause: the resolver's gates (cross-language, receiver, cross-scope demotion) only ran on the fallback path. Targets that hit byQualified exactly skipped every gate, so:

  • a bare Ruby call like x.count (emitted at ConfidenceUnresolved, receiver unknown) exact-matched a same-named JavaScript constant in a test spec and resolved at 0.5;
  • a constant call like I18n.t exact-matched a JS test-helper I18n.t at 1.0.

598 of the 603 were cross-language (Ruby production → JS test specs); the other 5 were same-language (a helper's url_for binding to FiltersHelperTest#url_for, where the real method is framework-inherited and unindexed).

Summary

Extend the gates the fallback already applies to the exact byQualified path, and add a one-directional production→test gate.

Changes

  • Exact-path cross-language gatecalls/tests/references exact matches now run through filterByLanguage, exempting intentional synthetic cross-language targets (partial:, turbo-*, importmap:, i18n:, route:, ruby-core:). When the exact name matches only gated-away coincidences, the edge drops to unresolved instead of leaf-falling-back into another coincidence.
  • Bare-guess routing — a bare target emitted at ConfidenceUnresolved (receiver unknown) skips the exact shortcut and goes through the gated fallback, which demotes it below blast's floor rather than letting a leaf coincidence resolve at 0.5.
  • filterByTestDirection — a production-source calls/references edge cannot resolve into a test file. Test files define stubs/helpers that shadow real (often framework) names, so a same-named match there is a coincidence. One-directional: test code still references both production and test symbols.
  • SymbolRef.Path — carried through the existing SymbolRefs left-join so the resolver can determine per-file test-ness, mirroring how it already carries Language.

Architecture / scope notes

  • isTestPath is a small local copy of mcpio.IsTestPath. resolve is a low-level package and must not depend on the presentation layer, so the copy is deliberate; hoisting a shared predicate into a base package is a reasonable follow-up, intentionally out of scope here.
  • Temporal production↔test co-change coupling is legitimate and left untouched — the gates apply only to structural calls/tests/references edges.

Verification (real Rails app)

  • production→test calls+references edges ≥0.5: 603 → 0
  • cross-language structural edges: 803 → 0
  • legit edges intact: production→production calls 6,773, test→production edges 6,083, temporal coupling preserved

Test Plan

  • Resolver: cross-language exact-drop, synthetic-target exemption, production→test (exact + fallback), test-source exemption, bare-guess routing (demote + drop)
  • White-box: isTestPath, isSyntheticTarget, filterByTestDirection; adapter carries Path
  • Coverage ≥92% line & per-function on modified files (resolver.go 98.7%, all new functions 100%)
  • make ci fully green (build, test, lint 0 issues)
  • sense binary builds cleanly

@codecov
Copy link
Copy Markdown

codecov Bot commented May 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…code to test symbols

The unqualified-fallback gates (cross-language, receiver, cross-scope
demotion) only ran on the fallback path. Targets that hit byQualified
exactly skipped them entirely, so a bare Ruby call like `x.count` (emitted
at ConfidenceUnresolved with an unknown receiver) or a constant call like
`I18n.t` exact-matched a same-named JavaScript symbol in a test spec and
resolved at full confidence. On a real Rails app this produced 603
production->test structural edges (calls + references), almost all of them
Ruby production code binding to JS test-spec constants.

Three changes, all extending rules that already existed on the fallback:

  - The exact byQualified path now applies the cross-language gate for
    calls/tests/references edges, exempting synthetic cross-language targets
    (partial:, turbo-*, importmap:, i18n:, route:, ruby-core:) which are
    intentional. When the exact name matches only gated-away coincidences the
    edge drops to unresolved rather than leaf-falling-back into another.
  - A bare target emitted at ConfidenceUnresolved (receiver unknown) skips the
    exact shortcut and goes through the gated fallback, which demotes it below
    blast's floor instead of letting a leaf coincidence resolve at 0.5.
  - filterByTestDirection: a production-source calls/references edge cannot
    resolve into a test file. Test files define stubs and helpers that shadow
    real (often framework) names; a same-named match there is a coincidence
    (a helper's `url_for` binding to FiltersHelperTest#url_for). The gate is
    one-directional, so test code still references production and test symbols.

SymbolRef now carries the file Path (already left-joined in the SymbolRefs
query) so the resolver can determine per-file test-ness, mirroring how it
already carries Language.

Verified on a real Rails app: production->test calls/references edges
603 -> 0 and cross-language structural edges 803 -> 0, while same-language
production->production calls (6773) and test->production edges (6083) are
intact and temporal co-change coupling is untouched.
@luuuc luuuc force-pushed the fix/production-to-test-edge-gate branch from 453b949 to 997eeec Compare May 31, 2026 18:25
@luuuc luuuc self-assigned this May 31, 2026
@luuuc luuuc merged commit c38c3ff into main May 31, 2026
6 checks passed
@luuuc luuuc deleted the fix/production-to-test-edge-gate branch May 31, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant