#1152·graphify

--update: ghost nodes survive for symbols deleted from re-extracted (modified) files (grow-only merge never replaces per source_file)

Author: janreviewCreated Jun 6, 2026Updated Sep 16, 2026

Version: graphifyy 0.8.33 · Windows 10 · Python 3.12

Summary

Incremental update (build_merge) leaves ghost nodes for symbols that were deleted from files that still exist. build_merge is documented as "Never replaces - only grows (or prunes deleted-file nodes via prune_sources)" — which means a function removed from a modified file can never leave the graph through the documented --update flow, even though the file was just re-extracted by AST.

Repro

  1. Full graph build. src/database/queries.py contains get_default_pricing() → node database_queries_get_default_pricing exists.
  2. Delete the function from the file (file itself stays). Re-run the incremental flow: detect_incremental correctly lists queries.py as changed; AST re-extracts it (the new extraction has no get_default_pricing node).
  3. build_merge([new_extraction], graph_path=...) → the old node survives (grow-only merge keeps every existing node).

Measured on a real dead-code-removal commit: 12 functions deleted across 8 modified files → all 12 still present as nodes after --update, with their stale calls edges. For a tool whose report advertises dead-code-ish insights (god nodes, isolated nodes), the graph silently disagreeing with the codebase after an update is a trap.

Why this is safe to fix for AST nodes

AST extraction is deterministic and complete per file: when a changed file is re-extracted, the new chunk contains the full current symbol set for that file. So for every source_file present in new_chunks, any existing node with the same source_file (and file_type == "code") that is absent from the new chunk is provably stale and can be dropped before the grow-merge.

Semantic (doc/concept) nodes are trickier (a chunk may intentionally be partial), so replace-by-source-file could apply to AST/code nodes only, or be opt-in:

build_merge(chunks, graph_path=..., replace_sources=[...changed code files...])

Workaround used

Post-merge: collect node ids whose source_file is among the re-extracted code files and whose id is not in the new extraction, drop them + their edges. Verified 0 ghosts afterwards.

Related: filed alongside the prune_sources path-mismatch issue — both surfaced from the same --update run after a dead-code-removal commit.