fix(watch): report net edge delta in rebuild log#1220
Conversation
The watch log printed `+N edges` for every rebuild, where N was the count of edges re-inserted during the rebuild — not the net delta. A comment-only edit to a 10-edge file reported `+10 edges` even though the DB total did not move at all (purge removed 10, rebuild re-inserted the same 10). The companion `nodes` field has always used a signed delta (nodesAdded - nodesRemoved); the asymmetry was the source of confusion. This change: - Tracks `edgesRemoved` in `rebuildFile` by counting the file's edges (and the outgoing edges of every reverse-dep) before purge. - Threads `edgesRemoved` through `RebuildResult` to the watcher. - Formats the edges field in the watcher log as a signed delta (`edgesAdded - edgesRemoved`), matching the nodes field. The `change-journal.ts` field name `edges.added` keeps its existing "count of insertions" semantics — only the user-facing watch log is adjusted. Closes #1219 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @carlos-alm's task —— View job PR Review Complete ⭐⭐⭐Reviewing PR #1220: fix(watch): report net edge delta in rebuild log Root Cause AnalysisProblem Being Solved: The Why This Approach: The fix correctly addresses the root cause by:
Risk Assessment: LOW - This is a display/logging fix that doesn't change core graph building logic. The change-journal semantics are intentionally preserved. Backlog Compliance
Critical ConcernsNone. This is a well-targeted fix with appropriate test coverage. Code Quality AssessmentStrengths:
Technical Implementation:
Minor Observations:
Final Recommendation
Conditions for approval:
|
Greptile SummaryThis PR fixes the
Confidence Score: 4/5Safe to merge; the graph data itself is unaffected — only the watcher log display changes, and the SQL accounting logic is correct for all normal paths. The deduplicating SQL in The early-return branch in Important Files Changed
Sequence DiagramsequenceDiagram
participant W as watcher.ts
participant R as rebuildFile
participant DB as SQLite DB
W->>R: rebuildFile(filePath)
R->>DB: findReverseDeps(relPath)
DB-->>R: reverseDeps[]
loop for each reverseDep
R->>R: parseReverseDep(dep) [pre-parse, before purge]
end
R->>DB: countEdgesRemovedOnRebuild(relPath, deletableDeps)
Note over R,DB: Single DISTINCT union query
DB-->>R: edgesRemoved
R->>DB: purgeFileData(relPath)
R->>DB: insertFileNodes + buildEdges
loop for each deletable dep
R->>DB: deleteOutgoingEdges(dep)
R->>DB: rebuildReverseDepEdges(dep)
end
R-->>W: "{edgesAdded, edgesRemoved, ...}"
W->>W: formatDelta(edgesAdded - edgesRemoved)
W->>W: log Updated: file (+N nodes, +M edges)
Reviews (6): Last reviewed commit: "Merge branch 'main' into fix/watch-edges..." | Re-trigger Greptile |
| let edgesRemoved = countEdgesTouchingFile(db, relPath); | ||
| for (const depRelPath of reverseDeps) { | ||
| edgesRemoved += countOutgoingEdges(db, depRelPath); | ||
| } |
There was a problem hiding this comment.
Double-counting of reverse-dep→file edges inflates
edgesRemoved
countEdgesTouchingFile(relPath) counts edges where target_id IN (nodes of relPath) — this already includes edges that run FROM each reverse dep TO relPath. Then for each reverse dep, countOutgoingEdges(depRelPath) counts ALL outgoing edges from that dep, which includes those same dep→relPath edges a second time. This means edgesRemoved is overcounted by the total number of cross-file edges pointing into the rebuilt file.
purgeFileData (in build-stmts.ts) confirms it with its edge-delete SQL: OR target_id IN (SELECT id FROM nodes WHERE file = @f), which removes the dep→relPath edges before deleteOutgoingEdges runs on each dep. At that point those edges are already gone, so countOutgoingEdges(dep) reports a count that includes edges no longer present at deletion time.
For a comment-only edit to a file with reverse deps, edgesAdded correctly equals the re-inserted count, but the overcounted edgesRemoved makes edgesAdded - edgesRemoved go negative — e.g. "-3 edges" instead of "+0 edges". A corrected formula would use countEdgesTouchingFile(relPath) for the primary file and then add only the outgoing-from-dep edges that do NOT target relPath, i.e. countOutgoingEdges(dep) − countEdgesFromDepToFile(dep, relPath) per reverse dep.
There was a problem hiding this comment.
Fixed in 82becaa. Replaced the two-step touching + Σ outgoing(dep) accumulation with a single union-based COUNT that mirrors the actual purgeFileData + deleteOutgoingEdges delete semantics. dep → relPath edges are now counted exactly once via the DISTINCT-by-construction source-file IN-list, regardless of whether they overlap with the per-dep outgoing set.
| /* ignore */ | ||
| } | ||
| }); | ||
|
|
||
| it('reports edgesRemoved ≈ edgesAdded for comment-only edits (net delta ≈ 0)', async () => { | ||
| const filePath = path.join(tmpDir, 'a.js'); | ||
| fs.appendFileSync(filePath, '\n// pure comment, no graph effect\n'); | ||
|
|
||
| const db = openDb(dbPath); | ||
| initSchema(db); | ||
| const stmts = makeStmts(db); | ||
| const result = await rebuildFile(db, tmpDir, filePath, stmts, { engine: 'auto' }, null); | ||
| db.close(); |
There was a problem hiding this comment.
Test only covers the no-reverse-dep case; the double-counting bug goes undetected
a.js is the sole file in tmpDir, so findReverseDeps always returns an empty array and the for (const depRelPath of reverseDeps) loop in rebuildFile never executes. The double-counting issue in edgesRemoved only triggers when at least one reverse dep exists. Adding a second file that imports from a.js (e.g. import { foo } from './a.js') would put b.js into the reverse-dep list and exercise the cross-file path that the current test misses.
There was a problem hiding this comment.
Fixed in 82becaa. Added a second describe block (rebuildFile edges accounting with reverse-deps) that creates a.js + b.js (with b.js importing from a.js), runs a comment-only edit on a.js, and asserts edgesAdded === edgesRemoved. This exercises the reverse-dep cascade path that the original single-file test missed and would fail against the old accounting.
Codegraph Impact Analysis6 functions changed → 6 callers affected across 2 files
|
Greptile flagged that the original `edgesRemoved` calculation
double-counted edges from reverse deps that point into the rebuilt
file: `countEdgesTouchingFile(relPath)` already captures every
incoming `dep → relPath` edge, and then `countOutgoingEdges(dep)`
re-counts the same edges on the per-dep pass.
For comment-only edits to a file with importers, `edgesAdded`
correctly equals the re-inserted count, but the overcounted
`edgesRemoved` would push the signed delta negative — e.g. "-3 edges"
instead of "+0 edges".
Replace the two-step `touching + Σ outgoing(dep)` accumulation with a
single DISTINCT-by-construction query: count edges whose source file is
in {relPath} ∪ reverseDeps OR whose target file is `relPath`. This
mirrors the actual delete semantics of `purgeFileData(relPath)` +
`deleteOutgoingEdges(dep)` and naturally deduplicates `dep → relPath`
edges.
Add a regression test covering the two-file reverse-dep scenario that
the original single-file test missed.
|
Addressed Greptile feedback:
Fix in 82becaa. Note on the Pre-publish benchmark gate failure: that gate is failing because |
- Fix §1 cross-reference from "Phase 4b" to "§4" (no Phase 4b section exists). - Add a note to §5 explaining the node/edge count gap vs the header (engine-comparison run captured a slightly earlier repo state than the build in §1). - Rewrite the contradictory "No-op rebuild well under the 10 ms target on neither engine" line to correctly state the target was exceeded. - Correct §9 and §13 attribution: the watcher edge-delta fix landed in #1220, not "this PR".
countEdgesRemovedOnRebuild previously included ALL outgoing edges of every reverse dep, but deleteOutgoingEdges(dep) only runs for deps that parseReverseDep returns non-null for. When a dep failed to parse (file deleted, unreadable, or unparseable), its outgoing edges to files other than relPath stayed in the DB yet were still counted in edgesRemoved. This made (edgesAdded - edgesRemoved) go negative in the watch log even though no edges were lost. Pre-parse reverse-deps up front, filter to the parseable set, and compute edgesRemoved from that subset so the displayed delta matches actual DB deletion semantics. The cascade loop is reorganized to consume the pre-parsed map directly. Adds a regression test that introduces b.js → a.js + b.js → c.js, deletes b.js, then rebuilds a.js. The b.js → c.js edges must survive the rebuild and must not appear in edgesRemoved.
|
Addressed Greptile P1 follow-up in b249ea4: Root cause: Fix: Pre-parse reverse-deps up front, filter to the parseable set ( Regression test: Added |
Summary
The
codegraph watchlog printed+N edgesfor every file rebuild where N was the count of edges re-inserted, not the net delta. A comment-only edit to a 10-edge file showed+10 edgeseven though the DB total moved by 0. The companionnodesfield has always used a signed delta (nodesAdded - nodesRemoved), so the asymmetry confused users.This PR tracks
edgesRemovedinrebuildFile(count edges touching the file before purge, plus outgoing edges of each reverse-dep), threads it throughRebuildResult, and renders the watcher's edges field as a signed delta(edgesAdded - edgesRemoved)so it matches how nodes are displayed.The
change-journal.tsedges.addedfield intentionally keeps its existing "insert count" semantics — only the user-facing watch log is adjusted.Found during
Dogfooding v3.11.0 — see #1219 and
generated/dogfood/DOGFOOD_REPORT_v3.11.0.md.Before / After
Test plan
tests/integration/watcher-edges-delta.test.ts— new test asserting comment-only edits reportedgesAdded == edgesRemoved, and added symbols with calls reportedgesAdded > edgesRemovedtests/unit/watcher-flush,tests/incremental/watcher-incremental,tests/integration/watcher-rebuild,tests/integration/watcher-fk-embeddings) still passchange-journaltests still pass (journal field semantics preserved)codegraph watchon the codegraph repo correctly reports net delta for both comment-only and symbol-adding editsCloses #1219