Report edge loss on rebuild: the node-only shrink guard misses a 12% edge drop - #2898
Report edge loss on rebuild: the node-only shrink guard misses a 12% edge drop#2898Ymx1ZQ wants to merge 1 commit into
Conversation
…edge drop to_json's Graphify-Labs#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) <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.
Graphify review — findings
Adds an edge-loss check to the graph-persistence step across all skill-*.md variants (agents, aider, amp, claw, codex, devin, windows, copilot, etc.) and their expected/fragment fixtures. Reads the previous graph.json node/edge counts before overwrite, tolerating both links and edges keys, and prints a non-fatal EDGE LOSS warning when edges drop while nodes hold or grow — closing the gap where the #479 node-only shrink guard passes silently.
Worth a look
- Overly-broad sanctioned-diff predicate matches unrelated try/except lines —
tools/skillgen/gen.py:1178· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Unqualified
try:/except Exception:predicate over-broadly sanctions any bare try/except —tools/skillgen/gen.py:1178· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
- Edge-loss allowlist accepts unrelated try/except lines —
tools/skillgen/gen.py:1186· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1096 functions depend on the 1032 functions this change touches.
Health — this change adds coupling hotspots:
- new:
render()— 13 callers, 5 callees - new:
audit_coverage()— 8 callers, 6 callees - new:
main()— 3 callers, 11 callees - new:
monolith_roundtrip()— 3 callers, 5 callees - new:
test_audit_catches_a_dropped_non_allowlisted_heading()— 0 callers, 6 callees
Verification — 1096 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1096 function(s) in the blast radius were not formally verified this run
· 5 more finding(s) on lines outside this diff (see the check run).
The node-only shrink guard passes a rebuild that loses 12% of its edges
to_json's #479 guard refuses to write when the new graph has fewer nodesthan the existing one. It does not look at edges, so a rebuild that gains nodes
and loses relations is written silently.
Measured on a real corpus:
The guard passed. The graph still answers, but every traversal query returns a
shorter path or none, and nothing in the output says why.
What this changes
Step 4 reads the previous graph's node and edge counts before the write, and
after the write reports an edge drop as a proportion, alongside the node delta.
Reported, not fatal, and that is deliberate. An edge drop can be legitimate:
files deleted, a noisy extractor tightened. A guard that aborted here would be
switched off during exactly the rebuild it should be watching. It prints at the
point of writing rather than leaving it to a later step, so the number is in
front of whoever is looking at the rebuild.
When nodes did not shrink, it adds the diagnosis, because that case has one
common cause worth naming: node ids that are not stable across passes, so cached
edges point at nodes the new pass renamed.
Which key it reads
to_jsonwritesnode_link_data(G, edges="links"), with aTypeErrorfallbackto the networkx default. The read takes
linksfirst and falls back toedges,so it works against both shapes. Reading only
edgesreturns 0 on every graphthe primary path writes, and the guard would never fire.
Where the change lives
In
tools/skillgen/fragments/core/{core,aider,devin}.md, so all sixteencommitted skill bodies get it from one edit.
graphify/skill*.mdandtools/skillgen/expected/in this diff are regenerated output(
python -m tools.skillgenthen--bless).aideranddevinare monoliths under--monolith-roundtrip, which requiresevery added line to match a documented change-class, so this adds one predicate,
_is_edge_loss_guard_fix_line, alongside the existing ones. If you would ratherthe monoliths stay frozen, dropping that predicate and reverting those two
fragments leaves the other fourteen platforms covered.
Verified locally:
--check,--audit-coverage,--schema-singleton,--monolith-roundtripand--always-on-roundtripall pass, the sixteenrendered Python bodies compile, and the guard was exercised against a stubbed
graph for six cases — the measured regression under both key shapes, an edge
gain, a legitimate corpus shrink, a first build, and a corrupt
graph.json(which must not crash the build).
Why it is in the skill body rather than in
to_jsonFollowing the existing #479 guard, which lives there too. If you would rather
have it inside
build.to_jsonas a returned warning, say which shape you preferand I will re-cut the PR.
🤖 Generated with Claude Code