From 3d44ee2eaf573ae762463c4f1334f0acd4282abb Mon Sep 17 00:00:00 2001 From: Ymx1ZQ Date: Thu, 20 Aug 2026 16:22:09 +0200 Subject: [PATCH] Report edge loss on rebuild: the node-only shrink guard misses a 12% edge drop to_json's #479 guard refuses to write when the new graph has fewer nodes than the existing one, and does not look at edges. A rebuild that gains nodes and loses relations is written silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges went 3,378 -> 2,978 (-11.8%), and the guard passed. Step 4 now reads the previous graph's node and edge counts before the write and reports an edge drop as a proportion after it. Reported, not fatal: an edge drop can be legitimate, and a guard that aborted here would be switched off during exactly the rebuild it should be watching. The read takes the 'links' key first and falls back to 'edges', because to_json writes node_link_data(G, edges="links") with a TypeError fallback to the networkx default. Reading only 'edges' returns 0 on every graph the primary path writes. Edited in tools/skillgen/fragments/core/{core,aider,devin}.md; the sixteen graphify/skill*.md bodies and tools/skillgen/expected/ are regenerated output. aider and devin are monoliths under --monolith-roundtrip, so this adds one sanctioned change-class predicate, _is_edge_loss_guard_fix_line. Co-Authored-By: Claude Opus 5 (1M context) --- graphify/skill-agents.md | 28 ++++++++++++++ graphify/skill-aider.md | 28 ++++++++++++++ graphify/skill-amp.md | 28 ++++++++++++++ graphify/skill-claw.md | 28 ++++++++++++++ graphify/skill-codex.md | 28 ++++++++++++++ graphify/skill-copilot.md | 28 ++++++++++++++ graphify/skill-devin.md | 28 ++++++++++++++ graphify/skill-droid.md | 28 ++++++++++++++ graphify/skill-kilo.md | 28 ++++++++++++++ graphify/skill-kiro.md | 28 ++++++++++++++ graphify/skill-opencode.md | 28 ++++++++++++++ graphify/skill-pi.md | 28 ++++++++++++++ graphify/skill-trae.md | 28 ++++++++++++++ graphify/skill-vscode.md | 28 ++++++++++++++ graphify/skill-windows.md | 28 ++++++++++++++ graphify/skill.md | 28 ++++++++++++++ .../expected/graphify__skill-agents.md | 28 ++++++++++++++ .../expected/graphify__skill-aider.md | 28 ++++++++++++++ .../skillgen/expected/graphify__skill-amp.md | 28 ++++++++++++++ .../skillgen/expected/graphify__skill-claw.md | 28 ++++++++++++++ .../expected/graphify__skill-codex.md | 28 ++++++++++++++ .../expected/graphify__skill-copilot.md | 28 ++++++++++++++ .../expected/graphify__skill-devin.md | 28 ++++++++++++++ .../expected/graphify__skill-droid.md | 28 ++++++++++++++ .../skillgen/expected/graphify__skill-kilo.md | 28 ++++++++++++++ .../skillgen/expected/graphify__skill-kiro.md | 28 ++++++++++++++ .../expected/graphify__skill-opencode.md | 28 ++++++++++++++ tools/skillgen/expected/graphify__skill-pi.md | 28 ++++++++++++++ .../skillgen/expected/graphify__skill-trae.md | 28 ++++++++++++++ .../expected/graphify__skill-vscode.md | 28 ++++++++++++++ .../expected/graphify__skill-windows.md | 28 ++++++++++++++ tools/skillgen/expected/graphify__skill.md | 28 ++++++++++++++ tools/skillgen/fragments/core/aider.md | 28 ++++++++++++++ tools/skillgen/fragments/core/core.md | 28 ++++++++++++++ tools/skillgen/fragments/core/devin.md | 28 ++++++++++++++ tools/skillgen/gen.py | 37 +++++++++++++++++++ 36 files changed, 1017 insertions(+) diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index 190827d9ac..c01c839c31 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-aider.md b/graphify/skill-aider.md index 4996beb787..27abfa339f 100644 --- a/graphify/skill-aider.md +++ b/graphify/skill-aider.md @@ -422,10 +422,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index 190827d9ac..c01c839c31 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index abd2811d23..ed711a1df2 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index af3f723c78..94825bb5da 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index abd2811d23..ed711a1df2 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-devin.md b/graphify/skill-devin.md index f9be846cbf..93f13f6700 100644 --- a/graphify/skill-devin.md +++ b/graphify/skill-devin.md @@ -487,10 +487,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index fd148d485d..306b6003c2 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index 3e70b050a4..20672bb15c 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index abd2811d23..ed711a1df2 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index 91ced60675..c1f923e089 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -422,11 +422,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index abd2811d23..ed711a1df2 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index 050667bc20..c99749395f 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -428,11 +428,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 20c7c0835c..1075c26422 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -426,11 +426,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index d631821ec3..d16bfbfa2e 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -452,11 +452,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding="utf-8") analysis = { diff --git a/graphify/skill.md b/graphify/skill.md index abd2811d23..ed711a1df2 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index 190827d9ac..c01c839c31 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-aider.md b/tools/skillgen/expected/graphify__skill-aider.md index 4996beb787..27abfa339f 100644 --- a/tools/skillgen/expected/graphify__skill-aider.md +++ b/tools/skillgen/expected/graphify__skill-aider.md @@ -422,10 +422,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index 190827d9ac..c01c839c31 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index abd2811d23..ed711a1df2 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index af3f723c78..94825bb5da 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index abd2811d23..ed711a1df2 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-devin.md b/tools/skillgen/expected/graphify__skill-devin.md index f9be846cbf..93f13f6700 100644 --- a/tools/skillgen/expected/graphify__skill-devin.md +++ b/tools/skillgen/expected/graphify__skill-devin.md @@ -487,10 +487,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index fd148d485d..306b6003c2 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -427,11 +427,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index 3e70b050a4..20672bb15c 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index abd2811d23..ed711a1df2 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index 91ced60675..c1f923e089 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -422,11 +422,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index abd2811d23..ed711a1df2 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index 050667bc20..c99749395f 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -428,11 +428,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 20c7c0835c..1075c26422 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -426,11 +426,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/expected/graphify__skill-windows.md b/tools/skillgen/expected/graphify__skill-windows.md index d631821ec3..d16bfbfa2e 100644 --- a/tools/skillgen/expected/graphify__skill-windows.md +++ b/tools/skillgen/expected/graphify__skill-windows.md @@ -452,11 +452,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding="utf-8") analysis = { diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index abd2811d23..ed711a1df2 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -430,11 +430,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/fragments/core/aider.md b/tools/skillgen/fragments/core/aider.md index 4996beb787..27abfa339f 100644 --- a/tools/skillgen/fragments/core/aider.md +++ b/tools/skillgen/fragments/core/aider.md @@ -422,10 +422,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/tools/skillgen/fragments/core/core.md b/tools/skillgen/fragments/core/core.md index c527a12563..c93a6bf887 100644 --- a/tools/skillgen/fragments/core/core.md +++ b/tools/skillgen/fragments/core/core.md @@ -365,11 +365,39 @@ questions = suggest_questions(G, communities, labels) # nothing) when the new graph is smaller than the existing graph.json. Only write # GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so # they never describe a graph that graph.json doesn't contain (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).') print('If this shrink is intentional (you deleted files), re-run a full build with --force.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") analysis = { diff --git a/tools/skillgen/fragments/core/devin.md b/tools/skillgen/fragments/core/devin.md index f9be846cbf..93f13f6700 100644 --- a/tools/skillgen/fragments/core/devin.md +++ b/tools/skillgen/fragments/core/devin.md @@ -487,10 +487,38 @@ questions = suggest_questions(G, communities, labels) # Persist the graph first and only write the report/analysis if it actually # persisted - to_json refuses to shrink an existing graph.json (#479), and a # report describing a graph we did not write would be a lie (#1392). +# Read the previous graph's counts BEFORE it is overwritten. The #479 guard +# compares NODES only, so a rebuild that gains nodes and loses edges passes it +# silently: measured on a real corpus, 2,100 -> 2,172 nodes (+72) while edges +# went 3,378 -> 2,978 (-12%), and the guard passed. +_prev_n = _prev_e = None +_prev_path = Path('graphify-out/graph.json') +if _prev_path.is_file(): + try: + with _prev_path.open('rb') as _fh: + _prev = json.loads(_fh.read()) + _prev_n = len(_prev.get('nodes', [])) + # to_json writes node_link_data(G, edges='links'); its TypeError fallback + # can emit 'edges' instead, so accept either key. + _prev_e = len(_prev.get('links', _prev.get('edges', []))) + except Exception: + _prev_n = _prev_e = None + wrote = to_json(G, communities, 'graphify-out/graph.json') if not wrote: print('ERROR: refused to shrink graphify-out/graph.json (fewer nodes than the existing graph). Run a full rebuild to be safe.') raise SystemExit(1) +# Edge side of the same guard. Reported, not fatal: an edge drop can be legitimate +# (files deleted, a noisy extractor tightened), and a guard that aborted here would +# be switched off during exactly the rebuild it should be watching. +if _prev_e: + _new_n, _new_e = G.number_of_nodes(), G.number_of_edges() + if _new_e < _prev_e: + _pct = (_prev_e - _new_e) * 100.0 / _prev_e + print(f'EDGE LOSS: {_prev_e} -> {_new_e} edges (-{_pct:.1f}%) while nodes went {_prev_n} -> {_new_n}. The #479 guard compares nodes and did not see this.') + if _new_n >= _prev_n: + print(' Nodes did not shrink, so relations were lost, not content. Node ids that are not stable across passes are the usual cause: cached edges point at nodes the new pass renamed.') + report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) Path('graphify-out/GRAPH_REPORT.md').write_text(report) diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index 09e19ede00..1dc25df342 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -1142,6 +1142,42 @@ def _is_community_label_export_fix_line(line: str) -> bool: ) +def _is_edge_loss_guard_fix_line(line: str) -> bool: + """Whether a line is part of the edge-side companion to the #479 guard. + + ``to_json``'s #479 guard compares NODES only, so a rebuild that gains nodes + and loses edges is written silently. Step 4 now reads the previous graph's + node and edge counts before overwriting it and reports an edge drop as a + proportion. Reported, not fatal: an edge drop can be legitimate, and a guard + that aborted here would be switched off during exactly the rebuild it should + be watching. The ``try:``/``except Exception:`` pair belongs to the + defensive read of the existing graph.json, which must not crash a build when + that file is corrupt or mid-write. + """ + s = line.strip() + return ( + "_prev_n" in line + or "_prev_e" in line + or "_prev_path" in line + or "_prev = json.loads" in line + or "_new_n" in line + or "_new_e" in line + or "_pct" in line + or "EDGE LOSS:" in line + or "Nodes did not shrink" in line + or "Read the previous graph's counts" in line + or "compares NODES only" in line + or "silently: measured on a real corpus" in line + or "went 3,378 -> 2,978" in line + or "to_json writes node_link_data" in line + or "can emit 'edges' instead" in line + or "Edge side of the same guard" in line + or "files deleted, a noisy extractor tightened" in line + or "be switched off during exactly the rebuild" in line + or s == "try:" + or s == "except Exception:" + ) + # Every line that may differ between a rendered monolith and its pristine v8 # baseline. Each predicate documents one sanctioned change-class; a blank line is # allowed because the multi-line fix blocks insert spacing. Anything else failing @@ -1163,6 +1199,7 @@ def _is_community_label_export_fix_line(line: str) -> bool: _is_uv_from_interpreter_fix_line, _is_semantic_cache_scope_fix_line, _is_community_label_export_fix_line, + _is_edge_loss_guard_fix_line, )