Skip to content

fix: preserve dependent nodes in incremental graph rebuild (#305)#311

Merged
HumanBean17 merged 2 commits into
masterfrom
bugfix/graph-increment-error
Jun 13, 2026
Merged

fix: preserve dependent nodes in incremental graph rebuild (#305)#311
HumanBean17 merged 2 commits into
masterfrom
bugfix/graph-increment-error

Conversation

@HumanBean17

Copy link
Copy Markdown
Owner

What

Fixes #305java-codebase-rag increment crashed during scoped deletion and fell back to a full rebuild every time:

[increment] error during incremental rebuild: Runtime exception: Node(nodeOffset: ...) has connected edges in table CALLS in the bwd direction, which cannot be deleted. Please delete the edges first or try DETACH DELETE.; falling back to full rebuild

Root cause

incremental_rebuild()_delete_file_scope() deleted Symbol nodes for both changed and dependent files with a plain DELETE. But:

  • source_file on every Symbol→Symbol edge is the caller's file (pinned by test_source_file_value_matches_symbol_filename), so Phase 1's e.source_file IN $filenames deletes only outgoing edges. Incoming edges from out-of-scope callers survive.
  • The orchestrator expands dependents from changed nodes only (changed_node_ids). So a dependent file's node — pulled into scope because it calls a changed node — can still have an incoming CALLS edge from an out-of-scope caller (a caller of the dependent, which is never brought into scope). That edge survives Phase 1, and the plain DELETE on the dependent node trips LadybugDB's "connected edges in the bwd direction" guard → exception → full-rebuild fallback.

A naive fix (DETACH DELETE, or an extra dst.filename incoming-edge pass) silences the crash but permanently drops those out-of-scope edges (the caller is never reprocessed) → silent graph corruption.

Fix

Delete Symbol nodes only for changed_files; preserve dependent-file nodes. Dependent files are reprocessed only to re-resolve their outgoing edges against changed nodes — their node definitions didn't change, and node ids are deterministic (symbol_id), so they re-MERGE in place on the same id. Their incoming edges from out-of-scope callers survive untouched.

  • Phase 1: delete outgoing edges for the whole scope (unchanged semantics).
  • Phase 3: MATCH (s:Symbol) WHERE s.filename IN $changed_files DETACH DELETE s — changed files only; DETACH DELETE as a safety net for the rare phantom/surviving edge.
  • Changed nodes' real incoming edges are already removed in Phase 1 (their callers are dependents, in scope) and re-emitted when those dependents are reprocessed, so nothing real is lost.

Complementary to #310 (which wires increment to actually invoke incremental_rebuild); no file overlap.

Tests (tests/test_incremental_graph.py)

  • New test_incremental_preserves_incoming_edges_to_dependent — end-to-end repro of Increment graph error #305 (C→B→A, change A). Was mode == "full_fallback" before the fix; mode == "incremental" after, with the out-of-scope C→B CALLS edge and the dependent B node preserved.
  • New test_delete_file_scope_preserves_dependent_nodes — direct unit test of the new (changed_files, dependent_files) signature.
  • Updated test_delete_file_scope_removes_only_matching to the new signature.

Validation

  • .venv/bin/ruff check . — clean.
  • .venv/bin/python -m pytest tests/test_incremental_graph.py -v — 24 passed.
  • Full tests suite: the only failures are pre-existing/environmental and live in modules that do not import the changed code (test_mcp_tools, test_mcp_v2, test_lance_optimize, test_cli_progress_stdout_invariant — missing async pytest plugin in this venv, per the Unknown config option: asyncio_mode warning). Verified unrelated to this change.

Notes

  • No schema or ontology change → no re-index required, no env-var change.
  • No propose/ doc (clearly-bounded one-file-plus-tests fix).

🤖 Generated with Claude Code

HumanBean17 and others added 2 commits June 13, 2026 19:16
_delete_file_scope deleted Symbol nodes for both changed and dependent files during incremental rebuild. Dependent nodes can have incoming CALLS edges from out-of-scope callers: the orchestrator expands dependents from changed nodes only, so callers OF dependent nodes are never pulled into scope, and source_file on an edge is the caller's file, so Phase 1's outgoing-edge delete left those incoming edges in place. The plain DELETE then crashed ('Node ... has connected edges in table CALLS in the bwd direction') and the rebuild fell back to full every time.

Delete Symbol nodes only for changed files. Dependent nodes are kept and re-MERGEd in place on their deterministic symbol_id, so their incoming edges from out-of-scope callers survive (no crash, no silent edge loss). Phase 3 uses DETACH DELETE for changed nodes as a safety net. No schema or ontology change, so no re-index burden.

Fixes #305

Co-Authored-By: Claude <noreply@anthropic.com>
Address PR #311 review feedback (non-blocking follow-ups):

- Extract the `_SYMBOL_TO_SYMBOL_EDGE_TYPES` module constant and derive `_find_dependents` from it. The #305 fix's safety rests on `_find_dependents` enumerating EVERY Symbol->Symbol edge type (so every real caller of a changed node enters scope and Phase 1 removes its edge before the node delete). The named constant plus cross-references in the Phase 1 and Phase 3 comments make that invariant enforced/documented by construction, instead of two parallel hand-maintained lists (6 vs 12 types) silently needing to stay in sync.
- Phase 2: comment why deleting UnresolvedCallSite children of preserved dependents is safe (scope files, dependents included, are reprocessed and re-emit them in `_scoped_write`).

No behavior change; tests unchanged and still green.

Co-Authored-By: Claude <noreply@anthropic.com>
@HumanBean17 HumanBean17 merged commit 397a778 into master Jun 13, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Increment graph error

1 participant