Skip to content

bug: incremental rebuild silently drops 32 import edges (native) / 37 (WASM) #1174

@carlos-alm

Description

@carlos-alm

Found during dogfooding v3.10.1-dev.80

Severity: High
Command: codegraph build <dir> (incremental)
Engines affected: native (drops 32), WASM (drops 37)

Reproduction

From a clean checkout of the worktree at refactor-1166-dedup-tracer-validation:

codegraph build . --no-incremental --engine native
# → 19139 nodes, 40889 edges
sqlite3 .codegraph/graph.db "SELECT COUNT(*) FROM edges WHERE kind='imports';"
# → 1362

echo "// edit" >> src/cli.ts
codegraph build . --engine native    # incremental
# → 19139 nodes, 40857 edges (-32)
sqlite3 .codegraph/graph.db "SELECT COUNT(*) FROM edges WHERE kind='imports';"
# → 1330  (-32)

# Restore original content
git checkout src/cli.ts
codegraph build . --engine native    # incremental
# → still 40857 edges, imports still 1330

Confirmed identical behavior on a clean state with WASM:

  • Native: 1362 → 1330 (-32 imports edges)
  • WASM: 1362 → 1325 (-37 imports edges)

Expected behavior

Re-parsing a file should restore all its outgoing edges. Whether the file content changed or not, the final edge set should match a clean --no-incremental build. The current behavior leaks edges per incremental rebuild whenever src/cli.ts is touched, and the lost edges never come back without --no-incremental.

Actual behavior

  • Every incremental rebuild of src/cli.ts deterministically drops exactly 32 imports edges on native (37 on WASM).
  • Subsequent incremental rebuilds on the same file stay at 40857 (so it converges, but to a value lower than the full rebuild).
  • Only codegraph build --no-incremental restores the full 40889 edges.
  • All other edge kinds (calls, contains, extends, implements, imports-type, parameter_of, receiver, reexports, dynamic-imports) stay unchanged.

Root cause (hypothesis)

The native engine's incremental edge purge/restore logic (Stage 6b → Stage 7) appears to not fully re-emit imports edges that depend on cross-file resolution. PR #998 (v3.9.5) fixed the duplicate-edge variant of this bug; this is the opposite — edges are dropped — and is likely a regression in the same code path.

The fact that both engines have the bug (native -32, WASM -37) suggests the root cause is in shared logic — either the resolver or the Stage 6b purge scope.

Suggested fix

  1. Audit Stage 6b's scoped DELETE → Stage 7 re-emit cycle in domain/graph/builder/stages/.
  2. Add a regression test: full rebuild → modify one file → incremental → revert → incremental → assert edge count matches the full-rebuild baseline. This test must be parameterized over both engines.
  3. Consider adding a CI gate (similar to the v3.9.6 parity gate from ci(bench): gate release benchmark on engine parity thresholds #1014) that fails when incremental and full rebuilds diverge on edge counts.

The 32-edge drift is not visible in stats --json summary numbers when reading the database for the first time after a single incremental rebuild — only a head-to-head comparison with --no-incremental reveals it. This means the bug almost certainly affects production codegraph hooks that rely on incremental rebuilds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdogfoodFound during dogfooding

    Type

    No type
    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