Skip to content

resolveByGlobal creates a call edge to every same-named candidate instead of the best match #1863

Description

@carlos-alm

Summary

`resolveByGlobal` in `src/domain/graph/resolver/strategy.ts` resolves a receiver-less (or this/self/super-receiver) call by matching every node in the codebase with the same name, filtered only by a loose directory-proximity confidence threshold (`computeConfidence(relPath, t.file, null) >= 0.5`):

```ts
const exact = lookup
.byName(call.name)
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
if (exact.length > 0) return exact;
```

`resolveCallTargets` then creates a `calls` edge to every entry in the returned array (sorted by confidence, but not deduplicated to the best match) rather than picking the single most likely target.

Concrete reproduction (found in this repo's own graph)

`src/db/connection.ts` defines four separate object-literal `close() {}` methods (in `openRepoNative`, `wrapInjectedRepo`, `openRepoSqliteFallback`, `openReadonlyWithNative`). Callers like `triageData`, `dataflowData`, `hotspotsData`, `loadCommunityGraph`, `sequenceData` destructure a handle via `const { repo, close } = openRepo(...)` and later call the bare `close()`. Because `close` has no receiver and its origin isn't resolved via typeMap/return-type propagation, resolution falls through to `resolveByGlobal`, which matches all four `close` nodes (same `src` grandparent directory as each caller → confidence exactly 0.5) and creates four `calls` edges per caller instead of one:

```
sqlite3 .codegraph/graph.db "SELECT s.name, t.line, e.confidence FROM edges e JOIN nodes s ON e.source_id=s.id JOIN nodes t ON e.target_id=t.id WHERE s.name='triageData' AND t.name='close';"
triageData|433|0.5
triageData|461|0.5
triageData|500|0.5
triageData|580|0.5
```

Only one of these is the function's actual call target; the other three are false edges. This inflates fan-in/fan-out counts, codegraph fn-impact/blast-radius results, and dependency graphs for any function with a common name reachable via this fallback.

Suggested fix direction

When `resolveByGlobal`'s exact-name lookup returns multiple candidates, either:

  • pick only the single highest-confidence candidate (ambiguous ties could still fall back to multiple, but the common single-clear-winner case should not fan out), or
  • treat "multiple equally-plausible candidates, no receiver/type info to disambiguate" as unresolved (create no edge) rather than betting on all of them.

How this was found

Discovered while root-causing #1752 (incremental rebuild produces stale blast-radius/fn-impact numbers). This is a separate bug from #1752: #1752 is about stale reverse-dep edge reconnection after an incremental rebuild (a single previously-resolved edge getting reattached to the wrong same-named sibling). This issue is about initial call resolution creating multiple edges in the first place, and reproduces identically on a full, from-scratch build — it is not an incremental-staleness issue.

Native and WASM engines should be checked for parity here too — need to confirm whether the native Rust call resolver has an equivalent multi-candidate fallback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions