Skip to content

bug: incremental rebuild triggered by an unrelated file loses 10 receiver-dispatch edges to WasmWorkerPool #2138

Description

@carlos-alm

Found during dogfooding v3.16.0

Severity: Medium
Command: codegraph build (incremental)

Reproduction

codegraph build .                                    # full baseline build
sqlite3 .codegraph/graph.db "SELECT n1.file,n1.name,n1.line,e.kind,n2.file,n2.name,n2.line \
  FROM edges e JOIN nodes n1 ON e.source_id=n1.id JOIN nodes n2 ON e.target_id=n2.id \
  ORDER BY 1,2,3,4,5,6,7;" | sort > /tmp/edges_baseline.txt

# Edit a COMPLETELY UNRELATED file — src/domain/graph/builder/pipeline.ts,
# which has no direct relationship to src/domain/parser.ts or wasm-worker-pool.ts:
echo "" >> src/domain/graph/builder/pipeline.ts
codegraph build .                                     # incremental: "1 changed, 0 removed"

# Revert the edit back to the exact original bytes:
git show HEAD:src/domain/graph/builder/pipeline.ts > src/domain/graph/builder/pipeline.ts
codegraph build .                                     # incremental again: "1 changed, 0 removed"

sqlite3 .codegraph/graph.db "SELECT ... same query ..." | sort > /tmp/edges_after.txt
comm -23 /tmp/edges_baseline.txt /tmp/edges_after.txt

Expected behavior

Editing (and then reverting to byte-identical content) a file with no relationship to parser.ts/wasm-worker-pool.ts should leave every edge involving those two files completely unchanged after incremental rebuild. Edge count and edge set should match a full --no-incremental rebuild of the same tree exactly.

Actual behavior

The graph permanently loses exactly 10 edges (43195 → 43185, confirmed via full edge-set diff, not just count) and does not recover them even after reverting the triggering file back to its original content:

src/domain/parser.ts|backfillTypeMap|1049|calls|src/domain/wasm-worker-pool.ts|WasmWorkerPool.parse|183
src/domain/parser.ts|backfillTypeMap|1049|receiver|src/domain/wasm-worker-pool.ts|WasmWorkerPool|158
src/domain/parser.ts|backfillTypeMapBatch|1142|calls|src/domain/wasm-worker-pool.ts|WasmWorkerPool.parse|183
src/domain/parser.ts|backfillTypeMapBatch|1142|receiver|src/domain/wasm-worker-pool.ts|WasmWorkerPool|158
src/domain/parser.ts|ensureWasmTrees|369|calls|src/domain/wasm-worker-pool.ts|WasmWorkerPool.parse|183
src/domain/parser.ts|ensureWasmTrees|369|receiver|src/domain/wasm-worker-pool.ts|WasmWorkerPool|158
src/domain/parser.ts|parseFileAuto|1112|calls|src/domain/wasm-worker-pool.ts|WasmWorkerPool.parse|183
src/domain/parser.ts|parseFileAuto|1112|receiver|src/domain/wasm-worker-pool.ts|WasmWorkerPool|158
src/domain/parser.ts|parseFilesWasm|1181|calls|src/domain/wasm-worker-pool.ts|WasmWorkerPool.parse|183
src/domain/parser.ts|parseFilesWasm|1181|receiver|src/domain/wasm-worker-pool.ts|WasmWorkerPool|158

All 10 are calls -> WasmWorkerPool.parse (method call) or receiver -> WasmWorkerPool (constructor/receiver-type binding) edges from 5 caller functions in parser.ts, all targeting the WasmWorkerPool class in wasm-worker-pool.ts. Notably, the sibling calls -> getWasmWorkerPool edge from each of those same 5 callers survives untouched — only the receiver-typed dispatch to the WasmWorkerPool class itself is lost.

The --verbose log for the triggering incremental build shows:

[codegraph DEBUG] runPostNativeCha: Gate A (hierarchy) fired — running full scan

suggesting the incremental CHA/receiver-dispatch re-resolution ("Gate A" full scan) doesn't reproduce the same WasmWorkerPool receiver-dispatch edges that a genuine --no-incremental build computes, even though it's not scoped to only the touched file.

Once lost, a further no-op incremental build does not recover them — only --no-incremental restores the correct 43195-edge graph.

Root cause

Likely in the native incremental orchestrator's CHA/receiver-dispatch re-resolution path (runPostNativeCha / "Gate A" hierarchy full-scan, per the debug log), which appears to run a resolution pass over class-hierarchy/receiver-typed dispatch edges but produces a different (smaller) result set than the equivalent full-build pass for the WasmWorkerPool class specifically.

Suggested fix

Compare the "Gate A full scan" receiver-dispatch resolution logic against the full-build equivalent for WasmWorkerPool-style singleton/class receiver patterns (getWasmWorkerPool() returning a WasmWorkerPool instance, .parse() called on it) and find where the incremental path fails to emit or fails to preserve the receiver/calls edges targeting the class's own methods.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdogfoodFound during dogfooding

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions