fix(watch): align incremental call resolver with full build#1261
Conversation
The watcher's rebuildFile reverse-dep cascade had its own call resolver that diverged from the authoritative full-build resolver in stages/build-edges.ts. A comment-only watch rebuild of a widely-imported file inflated `calls` edges by ~700 (verified on the codegraph repo: a touch of src/shared/kinds.ts grew the graph from 10178 to 10780 distinct calls edges plus 97 duplicates). Three divergences are fixed so the cascade matches a clean full build: - gate the global-name fallback to receiver-less/this/self/super calls and filter computeConfidence >= 0.5, instead of an unconditional all-matches fan-out to every same-named symbol in the graph - dedup edges within a file rebuild via a seenCallEdges set (insertEdge is a plain INSERT with no OR IGNORE) - follow barrel re-exports in the import-scoped branch so calls to symbols imported through an index/barrel file resolve to the defining file Adds a regression test driving rebuildFile directly (the watch path) — the existing parity tests use buildGraph's native-orchestrator incremental path and never exercised the buggy JS cascade. Closes #1259
|
Claude finished @carlos-alm's task —— View job PR Review: fix(watch): align incremental call resolver with full build
Phase 1: Root Cause Analysis ✅The root cause is correctly identified and the fix is surgical:
The approach is correct: fix the divergence at the source rather than papering over symptoms. Phase 2: Configuration Change Detection ✅No linting rules relaxed, no timeouts increased, no coverage thresholds lowered, no build or TypeScript config touched. Clean pass. Phase 3: Test Integrity ✅The new test file (
No assertions were weakened. No existing tests were modified. Phase 4: Code QualityPositives:
Issues found: Issue 1 — Minor divergence: The full-build // incremental.ts (current)
for (const imp of symbols.imports) {
const resolvedPath = resolveImportPath(...)
for (const name of imp.names) {
importedNames.set(name.replace(...), resolvedPath);
}
}vs. full-build: // Two passes: dynamic first (lower priority), static second (wins)
for (const imp of symbols.imports) { if (!imp.dynamicImport) continue; ... }
for (const imp of symbols.imports) { if (imp.dynamicImport) continue; ... }This is a pre-existing gap (not introduced by this PR), but this PR is explicitly aligning incremental with full-build and the PR description mentions "slightly over-counts Issue 2 — The full-build version at const typed = (ctx.nodesByName.get(qualifiedName) || []).filter((n) => n.kind === 'method');The incremental version at const typed = stmts.findNodeByName.all(qualified) as Array<{ id: number; file: string }>;The SQL in Issue 3 — Global-fallback targets are not sorted by confidence ( The full-build path sorts Issue 4 — Module-level mutable statement caches ( Lazily-cached prepared statements are keyed on Phase 5: Backlog Compliance
Final AssessmentRoot Cause AnalysisProblem Being Solved: The watcher's incremental call resolver had three concrete divergences from the full-build resolver, causing edge inflation (~700 extra Why This Approach: The correct fix is semantic alignment at the source — replicate the same gates (receiver guard, confidence threshold, barrel follow-through, dedup set) rather than adding compensating workarounds. The PR does exactly this. Risk Assessment: Low. The fix only changes what edges are inserted during Backlog Compliance
Minor Concerns (non-blocking)
Final Recommendation
|
Greptile SummaryThis PR fixes a divergence between the watcher's incremental call-resolver (
Confidence Score: 5/5Safe to merge. The changes are tightly scoped to the watch-mode call resolver, the fix is well-verified by a new integration test that directly exercises the previously-untested JS cascade, and end-to-end parity on the codegraph repo is confirmed. All three root causes of the edge inflation are addressed with matching logic from the authoritative full-build path. The remaining divergences (dynamic-import priority ordering in buildImportedNamesMap, receiver/extends/dynamic-import edge under-rebuild) are pre-existing, acknowledged, and tracked separately as #1260 — none are introduced or worsened by this PR. The new test is specifically designed to catch regressions on the watch path that the existing parity tests missed. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[resolveCallTargets] --> B{importedFrom in importedNames?}
B -- yes --> C[findNodeInFile call.name, importedFrom]
C --> D{targets empty AND importedFrom is barrel?}
D -- yes --> E[resolveBarrelTarget → actualSource]
E --> F[findNodeInFile call.name, actualSource]
D -- no --> G[targets resolved]
F --> G
B -- no --> H[findNodeInFile call.name, relPath]
H --> I{targets empty?}
I -- yes --> J[resolveByMethodOrGlobal]
J --> K{call.receiver set?}
K -- yes, non-this/super --> L[typeMap lookup → qualified name search, filter kind==method]
K -- no or this/self/super --> M[findNodeByName call.name, filter confidence >= 0.5]
L --> N{typed results found?}
N -- yes --> O[return typed]
N -- no --> P[return empty - no global fan-out for unknown receivers]
M --> O
I -- no --> G
G --> Q[sort by confidence desc]
Q --> R[buildCallEdges: dedup via seenCallEdges, insertEdge]
Reviews (9): Last reviewed commit: "Merge branch 'main' into fix/dogfood-inc..." | Re-trigger Greptile |
| relPath: string, | ||
| typeMap: Map<string, unknown>, | ||
| ): Array<{ id: number; file: string }> { | ||
| if (call.receiver && typeMap) { |
There was a problem hiding this comment.
The
&& typeMap guard is always truthy here: typeMap is typed Map<string, unknown> and is always initialised to at least new Map() in buildCallEdges (the only call site). The dead condition also appears in the full-build's resolveByMethodOrGlobal, so this is a copy-paste carry-over, but removing it makes the guard clearer.
| if (call.receiver && typeMap) { | |
| if (call.receiver) { |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed — removed the dead && typeMap guard. typeMap is always initialized to at least new Map() in buildCallEdges, so the condition was always truthy. The check is now just if (call.receiver) as suggested.
Codegraph Impact Analysis7 functions changed → 11 callers affected across 3 files
|
…y confidence (#1261) - Drop the always-truthy `&& typeMap` condition in `resolveByMethodOrGlobal`; `typeMap` is always initialised to `new Map()` so the guard is dead code. - Sort multi-target results by `computeConfidence` descending before returning from `resolveCallTargets`, aligning with the full-build path in `stages/build-edges.ts` which performs the same sort when `targets.length > 1`.
|
Addressed Greptile's review feedback:
|
…fidence (#1261) Two incremental resolver gaps remain versus the full-build path: 1. `resolveByMethodOrGlobal` type-aware lookup did not filter results to `kind === 'method'`. The full-build (`build-edges.ts:624`) explicitly does `.filter((n) => n.kind === 'method')`, so languages storing receiver methods as `function` kind with qualified names could produce spurious edges the full build never adds. Fix: add `kind` to `findNodeByName` SQL and apply the same filter in JS. 2. `buildImportedNamesMap` stored the barrel file (`lib/barrel.js`) as `importedFrom` instead of the defining file (`lib/impl.js`). When `computeConfidence` later compared `importedFrom === targetFile` the check failed, producing confidence 0.3 instead of 1.0 for barrel-resolved calls. Fix: mirror the full-build's barrel follow-through in `buildImportedNamesMap` so callers use the actual defining file as `importedFrom`. Expose the second gap in the parity test by adding `confidence` and `dynamic` to the `readEdges` query — the `toEqual` oracle now catches confidence drift.
|
Addressed Greptile's body review issues:
|
|
Claude finished @carlos-alm's task —— View job PR Review: fix(watch): align incremental call resolver with full build (follow-up)
Review of latest commits (addressing Greptile's feedback)Three changes landed since the last review:
Issue 1:
|
Summary
The watcher's
rebuildFilereverse-dependency cascade had its own callresolver (
resolveCallTargets/buildCallEdgesinincremental.ts) that haddrifted from the authoritative full-build resolver in
stages/build-edges.ts. On the codegraph repo, a comment-only watchrebuild of a widely-imported file (
src/shared/kinds.ts) inflatedcallsedges by ~700 — the graph grew from 10178 distinct call edges to 10780
distinct + 97 duplicate rows, and
build --no-incrementalreset it back to10178. This is the underlying cause of the net-edge-delta noise behind
#1220/#1245 and the regression-guard exemptions in #1248/#1255.
Three divergences are fixed so the cascade matches a clean full build exactly:
findNodeByName(call.name)fan-out (every same-named symbol in the graph) is replaced by the
full-build semantics: type-aware lookup, then a global match gated to
receiver-less /
this/self/supercalls and filtered tocomputeConfidence >= 0.5.buildCallEdgesnow dedups within a file rebuild via aseenCallEdgesset (insertEdgeis a plain INSERT with no OR IGNORE).re-exports (
isBarrelFile/resolveBarrelTarget) so calls to symbolsimported through an index/barrel file resolve to the defining file instead
of being dropped.
After the fix, a comment-only watch rebuild leaves
callsat exact parity(10178 total / 10178 distinct, zero duplicates).
Found during
Dogfooding v3.11.1 — see #1259.
Verification
tests/integration/issue-1259-watch-call-resolution.test.tsdrives
rebuildFiledirectly (the watch path) on a fixture that exercisesall three failure modes. It fails on the pre-fix code (extra
commonNamefan-out edge + duplicateleafValedge) and passes after.The existing parity tests use
buildGraph's native-orchestrator incrementalpath and never exercised the buggy JS cascade — which is why they were green.
guard included).
watch+ comment-only edit ofsrc/shared/kinds.tsnow reports net-zerocallschange; DB distinct/totalcalls stay at 10178.
Follow-up
The cascade still under-rebuilds
receiver/extends/dynamic-importsedges(and slightly over-counts
imports) vs the full build — filed separately as#1260. This PR is scoped to the
calls-edge inflation (#1259).Test plan
npx vitest run tests/integration/issue-1259-watch-call-resolution.test.tsnpx vitest run tests/integration/watcher-rebuild.test.ts tests/integration/incremental-edge-parity.test.tsbuilda repo,watch, append a comment to a high-fan-in file, confirm net-zerocallsdelta