fix(mcp+resolution): stop conflating same-named symbols across monorepo apps (#764) (#813)

A NestJS-style monorepo has one UserService/UserModule/UserRepository per
app; with no package concept for TS they share one global name scope and
agents visibly warned that CodeGraph was mixing unrelated classes.

Two distinct problems, two fixes:

1. TOOL AGGREGATION. callers/callees returned one merged list across every
   same-named match, and impact merged all their blast radii into a single
   overstated subgraph. Now: matches group into DISTINCT DEFINITIONS
   (filePath + qualifiedName — same-file overloads still merge, that's the
   overload feature) and render one file-labeled section per definition;
   a new `file` argument (path or suffix, like codegraph_node's) narrows
   to one definition, suppressing the stale aggregation note; a
   non-matching `file` falls back to all definitions with a note.
   server-instructions documents the behavior.

2. RESOLUTION WRONG EDGES. Auditing a real monorepo (amplication, 54k
   nodes) found 1,036 cross-package `references` edges into duplicated
   names. Root cause: the React framework resolver ran PascalCase
   component resolution on refs from PLAIN .ts FILES (a GraphQL types
   file's own `Account` type alias lost to an arbitrary same-named CLASS
   in another package — the resolver's blind `components[0]` fallback at
   confidence 0.8 outranked the name-matcher's proximity-correct 0.7).
   Component resolution is now gated to JSX-capable refs (tsx/jsx) and
   never guesses among multiple candidates without a positional signal
   (same-dir / component-dir / unique). Cross-package wrong edges:
   1,036 -> 40 (-96%; the remainder are genuine shared-model imports and
   codegen template scaffolds), with the freed refs re-resolving to the
   correct same-file/same-package targets. excalidraw (a real React repo)
   is a zero-delta control — legitimate component refs all carry
   same-dir/component-dir signals.

Graph-level separation was verified correct on a fixture before any
changes (import + proximity resolution keeps apps apart) — the conflation
was tool-level plus the react-resolver edge class.

Tests: 6-test e2e suite (grouped callers/callees, per-definition impact
radii, file narrowing, fallback note, cross-app edge isolation) + react
resolver unit tests updated to production reality (tsx refs resolve,
plain-ts refs decline). Full suite 1398 passed. EXTRACTION_VERSION
23 -> 24 (re-index to drop the wrong cross-package edges).

Closes #764

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 16:24:22 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent dce61a5f4a
commit 222af6b87c
7 changed files with 365 additions and 60 deletions
+17 -3
View File
@@ -32,8 +32,19 @@ export const reactResolver: FrameworkResolver = {
},
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
// Pattern 1: Component references (PascalCase)
if (isPascalCase(ref.referenceName) && !isBuiltInType(ref.referenceName)) {
// Pattern 1: Component references (PascalCase). Only from JSX-capable
// files — a component is USED in markup, which only parses in .tsx/.jsx.
// Without this gate, every PascalCase TYPE reference in plain .ts files
// went through component resolution: in a monorepo with same-named
// classes per package (#764, amplication), a `.ts` GraphQL-types file's
// own `Account` type alias lost to an arbitrary `Account` CLASS in
// another package (the framework's 0.8 outranked the name-matcher's
// proximity-correct 0.7).
if (
(ref.language === 'tsx' || ref.language === 'jsx') &&
isPascalCase(ref.referenceName) &&
!isBuiltInType(ref.referenceName)
) {
const result = resolveComponent(ref.referenceName, ref.filePath, context);
if (result) {
return {
@@ -305,7 +316,10 @@ function resolveComponent(
);
if (preferred.length > 0) return preferred[0]!.id;
return components[0]!.id;
// No positional signal: only an UNAMBIGUOUS name may resolve. Returning
// components[0] here picked an arbitrary same-named class anywhere in the
// repo (#764) — let the name-matcher's proximity scoring decide instead.
return components.length === 1 ? components[0]!.id : null;
}
/**