fix: prevent fuzzy dedup from collapsing distinct same-named symbols on --update (#1178)

Three-part fix:

dedup.py: Pass 1 exact-merge now skips nodes with an empty source_file.
Previously all no-source_file nodes with the same label landed in one
bucket and were merged, destroying distinct symbols (third-party deps,
standalone functions) that happened to share a short name.

update.md (skillgen + all 13 host variants): the --update merge now
passes both deleted AND changed files to prune_sources, mirroring what
watch._rebuild_code already does correctly. Old nodes for re-extracted
files are pruned before fresh AST is inserted — no fuzzy reconciliation
needed, no cross-file collapse possible.

export.py: anti-shrink guard message now names fuzzy dedup as a
possible cause (not only "missing chunk files"), and advises a full
rebuild as the safe recovery path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-06-07 14:26:14 +01:00
co-authored by Claude Sonnet 4.6
parent 3744e6e220
commit 12a9b5e812
29 changed files with 173 additions and 31 deletions
+5 -1
View File
@@ -186,7 +186,11 @@ def deduplicate_entities(
for node in group: for node in group:
sf = node.get("source_file") or "" sf = node.get("source_file") or ""
by_file[sf].append(node) by_file[sf].append(node)
for file_group in by_file.values(): for sf, file_group in by_file.items():
if not sf:
# No source_file — cannot prove same symbol; skip to avoid
# collapsing distinct nodes that happen to share a label (#1178).
continue
if len(file_group) > 1: if len(file_group) > 1:
winner = _pick_winner(file_group) winner = _pick_winner(file_group)
for node in file_group: for node in file_group:
+6 -3
View File
@@ -493,9 +493,12 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
import sys as _sys import sys as _sys
print( print(
f"[graphify] WARNING: new graph has {new_n} nodes but existing " f"[graphify] WARNING: new graph has {new_n} nodes but existing "
f"graph.json has {existing_n}. Refusing to overwrite — you may be " f"graph.json has {existing_n} (net -{existing_n - new_n}). "
f"missing chunk files from a previous session. " f"Refusing to overwrite. Possible causes: missing chunk files from "
f"Pass force=True to override.", f"a previous session, or fuzzy dedup collapsed same-named symbols "
f"across files during an --update on an already-current graph. "
f"Run a full rebuild (/graphify .) to be safe, or pass force=True "
f"only if you have verified the reduction is legitimate.",
file=_sys.stderr, file=_sys.stderr,
) )
return False return False
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
+6 -1
View File
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
@@ -93,13 +93,18 @@ from graphify.detect import save_manifest
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
deleted = list(incremental.get('deleted_files', [])) deleted = list(incremental.get('deleted_files', []))
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
# the same file's nodes and can collapse same-named symbols across files (#1178).
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
prune = list(dict.fromkeys(deleted + changed)) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip # Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801). # so edge direction (calls, implements, imports) is always preserved (#801).
G = build_merge( G = build_merge(
[new_extraction], [new_extraction],
graph_path='graphify-out/graph.json', graph_path='graphify-out/graph.json',
prune_sources=deleted or None, prune_sources=prune,
) )
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')