Summary
The /graphify <path> --update skill runbook calls build_merge(prune_sources=...) without passing root=, so the abs/rel path normalization added for #1007 is bypassed. prune_sources (absolute paths) never matches the graph's relative source_file values, nothing is pruned, and the graph accumulates duplicate/stale nodes on every incremental update. The native graphify update CLI is unaffected — it passes root.
Version: graphifyy 0.8.38
Root cause
build_merge only relativizes prune_sources when root is provided (build.py):
_root_str = str(Path(root).resolve()) if root is not None else None
prune_set = set()
for p in prune_sources:
prune_set.add(p)
norm = _norm_source_file(p, _root_str) # no-op when _root_str is None
if norm:
prune_set.add(norm)
to_remove = [n for n, d in G.nodes(data=True) if d.get("source_file") in prune_set]
_norm_source_file(p, None) returns p unchanged for absolute paths (the if root and os.path.isabs(p) guard fails). Graph nodes store relative source_file, so d.get("source_file") in prune_set never matches.
The shipped update runbook (references/update.md, ~line 104) calls it without root:
G = build_merge(
[new_extraction],
graph_path='graphify-out/graph.json',
prune_sources=prune,
)
and prune is built from detect_incremental(...)'s new_files / deleted_files, which are absolute paths — so the mismatch is guaranteed.
By contrast the native CLI (__main__.py, incremental branch) does it correctly:
G = _build_merge([merged], graph_path=existing_graph_path,
prune_sources=deleted_files or None,
dedup=True, dedup_llm_backend=dedup_backend, root=target)
Observed
Running /graphify . --update after editing 4 tracked docs:
[graphify] 4 source file(s) deleted since last run — no matching nodes or edges in graph, already clean.
[update] Merged: 737 nodes # ← 4 changed files' OLD nodes survived; 28 new appended
Re-running build_merge with the same prune list relativized against root:
[graphify] Pruned 84 node(s) from 4 deleted source file(s).
[update] Merged: 653 nodes # ← correct
Over repeated --update runs this compounds, inflating the graph with ghost duplicates of every changed file.
Suggested fix
Pass root in the update runbook (and any other skill flow that calls build_merge with prune_sources):
G = build_merge(
[new_extraction],
graph_path='graphify-out/graph.json',
prune_sources=prune,
root=str(Path('.').resolve()), # or the scan root recorded in graphify-out/.graphify_root
)
Optionally, make build_merge defensive: when root is None, default _root_str to graph_path.parent.parent (the project root, since the graph lives at graphify-out/graph.json) so prune still works when a caller forgets root.
Environment
- graphifyy 0.8.38, Python 3.12, Linux
- Repro is platform-independent (absolute-vs-relative
source_file mismatch), but it bites hardest right after a checkout moves between machines/OSes, where the manifest baseline already forces a large changed-file set.
Summary
The
/graphify <path> --updateskill runbook callsbuild_merge(prune_sources=...)without passingroot=, so the abs/rel path normalization added for #1007 is bypassed.prune_sources(absolute paths) never matches the graph's relativesource_filevalues, nothing is pruned, and the graph accumulates duplicate/stale nodes on every incremental update. The nativegraphify updateCLI is unaffected — it passesroot.Version: graphifyy 0.8.38
Root cause
build_mergeonly relativizesprune_sourceswhenrootis provided (build.py):_norm_source_file(p, None)returnspunchanged for absolute paths (theif root and os.path.isabs(p)guard fails). Graph nodes store relativesource_file, sod.get("source_file") in prune_setnever matches.The shipped update runbook (
references/update.md, ~line 104) calls it withoutroot:and
pruneis built fromdetect_incremental(...)'snew_files/deleted_files, which are absolute paths — so the mismatch is guaranteed.By contrast the native CLI (
__main__.py, incremental branch) does it correctly:Observed
Running
/graphify . --updateafter editing 4 tracked docs:Re-running
build_mergewith the same prune list relativized against root:Over repeated
--updateruns this compounds, inflating the graph with ghost duplicates of every changed file.Suggested fix
Pass
rootin the update runbook (and any other skill flow that callsbuild_mergewithprune_sources):Optionally, make
build_mergedefensive: whenroot is None, default_root_strtograph_path.parent.parent(the project root, since the graph lives atgraphify-out/graph.json) so prune still works when a caller forgetsroot.Environment
source_filemismatch), but it bites hardest right after a checkout moves between machines/OSes, where the manifest baseline already forces a large changed-file set.