Skip to content

fix(cli): route callers/callees/impact through one symbol resolver - #1656

Open
ferrine wants to merge 1 commit into
colbymchenry:mainfrom
ferrine:fix/symbol-lookup-consistency
Open

fix(cli): route callers/callees/impact through one symbol resolver#1656
ferrine wants to merge 1 commit into
colbymchenry:mainfrom
ferrine:fix/symbol-lookup-consistency

Conversation

@ferrine

@ferrine ferrine commented Aug 31, 2026

Copy link
Copy Markdown

Branch: ferrine:fix/symbol-lookup-consistencymain
Size: 5 files, +453/−110 · Tests: __tests__/symbol-lookup.test.ts (28 new)

Why

codegraph callers, callees and impact each resolved the symbol name their
own way, and none of them understood a qualified name. Asking for something like
Accounts.Format.group matched nothing, so the command quietly fell back to
whichever symbol full-text search happened to rank first — or reported "not
found" for a symbol that plainly exists.

Two root causes, and the second is the one that made it look intermittent:

  1. The comparison was against the bare name, so a qualified query never
    matched.
  2. The separator model assumed ::, so a language whose module names contain
    dots (Elixir, protobuf FQNs, C# namespaces) could not be addressed at all —
    A.B.C is genuinely ambiguous between "container A.B, member C" and a
    module literally named A.B.C.

Because the fallback was FTS ranking, the wrong answer arrived with no signal
that it was a guess.

What changed

  • New src/graph/symbol-lookup.ts — one "what did the user mean by this name?"
    path shared by all three commands.
  • Lookup consults the exact-name index first (complete and uncapped) and
    uses FTS only as a fallback, so a name that exists is never missed because
    ranking put it past a cap.
  • Matching canonicalises the container so a dotted module name resolves whether
    written A.B.C or A.B::C.
  • When a plain name matches several distinct definitions, the commands now say
    so and print the qualified spelling that narrows it, instead of silently
    merging unrelated symbols into one list.

Notes for review

  • No behaviour change for a name that was already resolving correctly; the tests
    cover the previously-broken shapes.
  • This is the base for two other PRs (protobuf indexing needs dotted-container
    matching, and the multi-definition diversity fix builds on the same path).

`callers`, `callees` and `impact` each carried their own symbol filter,
comparing the user's query against the BARE node name only:

    node.name === symbol || node.name.endsWith('.' + symbol)

That fails in two opposite directions in the same repository.

A bare name OVER-reports. Every same-named definition passes the filter
and their results are unioned under one heading — "Callers of group" can
list callers that belong to an entirely different `group`, with nothing
saying the name was ambiguous. Collisions cluster on short generic names
(`group`, `num`, `parse`), so this bites hardest exactly where the verbs
would otherwise be most useful.

A qualified name UNDER-reports. `Foo.Bar.baz` can never equal a bare
`baz`, so every candidate fails the filter, and the guarded fallback
takes whichever node full-text search ranked first — or reports "not
found" for a symbol that plainly exists. It only ever appeared to work
when FTS happened to return exactly one hit.

All three now resolve through graph/symbol-lookup, which the MCP tools
share, so a verb cannot drift from the matcher again:

- the exact-name index is consulted first and is authoritative. It is
  complete and uncapped, whereas FTS ranks, truncates, and tokenises
  `::` away — so resolution no longer depends on search ranking. FTS
  stays as the fallback for the fuzzy cases it is good at.
- `matchesSymbol` gains a boundary-aligned suffix match under a
  canonical separator. Splitting on every separator assumes no scope
  component contains one, which is false for any language whose module
  names are themselves dotted: the stored `A.B::c` can never equal the
  split-and-rejoined `A::B::c`, so a precise query resolved to nothing.
- an ambiguous bare name still aggregates (an interface method and its
  overrides are usually all wanted) but the union is now disclosed, with
  the matched definitions named and a qualified spelling that narrows
  it. `--json` gains a `targets` array and an `ambiguous` flag.

`matchesSymbol` moves out of mcp/tools.ts unchanged apart from the new
stage; the tool path delegates to it, so its existing coverage applies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Bsi9EH64kMisnik4E1gf7
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