fix(export): tolerate id-less persisted hyperedges in attach_hyperedges (#2775) - #2776
fix(export): tolerate id-less persisted hyperedges in attach_hyperedges (#2775)#2776ousamabenyounes wants to merge 1 commit into
Conversation
…es (Graphify-Labs#2775) The semantic extractor emits hyperedges with no `id` and build.py persists them verbatim, so a prior graph.json can carry id-less hyperedges. Seeding the dedup set with a hard `h["id"]` raised KeyError: 'id' on every incremental re-extract, symmetric with the .get("id") guard already applied to the incoming set. Skip id-less persisted entries when seeding instead.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
This PR fixes a KeyError: 'id' that occurred in attach_hyperedges (in graphify/export.py) during incremental re-extraction when a prior graph.json contained hyperedges lacking an id field. The dedup-set construction now filters persisted entries to only those with a truthy id, mirroring the existing guard applied to incoming hyperedges. A new regression test (test_attach_hyperedges_tolerates_id_less_persisted) and a CHANGELOG entry are added to cover this scenario. The touched surface is limited to the one-line dedup change, the test file, and documentation.
No blocking issues surfaced.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 653 functions depend on the 260 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_rebuild_code()— 95 callers, 51 callees - new:
build_merge()— 41 callers, 14 callees - new:
to_obsidian()— 29 callers, 12 callees - new:
to_json()— 44 callers, 6 callees - new:
dispatch_command()— 2 callers, 117 callees - new:
_make_graph()— 30 callers, 6 callees - new:
run_pipeline()— 8 callers, 13 callees - new:
to_canvas()— 17 callers, 4 callees - …and 4 more
Verification — 653 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: 510 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in attach\_hyperedges (not a proof).
The verifier ran both versions of attach\_hyperedges on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 12 more finding(s) on lines outside this diff (see the check run).
|
Landed in v0.9.45, just published to PyPI. Cherry-picked onto |
Summary
Fix #2775 — an incremental re-extract died at the merge step with
KeyError: 'id'.attach_hyperedges(graphify/export.py) seeded its dedup set from thepreviously-persisted hyperedges with a hard subscript:
The semantic/LLM extractor routinely emits hyperedges as
{"nodes": [...], "type": "...", "attributes": {...}}with noid, andbuild.pypersists them verbatim. So a priorgraph.jsoncan — and in practicedoes — contain id-less hyperedges. The first
graphify extracton a corpus hasno prior graph to read back and succeeds; every incremental re-extract
afterwards hits that comprehension and raises, writing nothing. Because the
first run works, it looks like flaky extraction; it is deterministic.
The loop directly below already tolerates id-less entries in the incoming set
via
.get("id"). This change makes the read symmetric — it skips id-lesspersisted entries when seeding the dedup set:
Scope is intentionally the crash only: the pre-existing behavior of the loop
(id-bearing incoming hyperedges are deduped and appended; id-less incoming are
skipped) is unchanged, and id-less persisted entries are retained in the graph.
Test verification (RED → GREEN)
New regression test
test_attach_hyperedges_tolerates_id_less_persistedseeds anid-less persisted hyperedge — the exact input the semantic extractor produces.
RED — on the unmodified
v8code (prod fix reverted):GREEN — with the fix:
Full suite is unchanged aside from the new passing test (
pytest tests/→4514 passed, 7 skipped; the 3
test_ollama::test_detect_backend_*failures arepre-existing on
v8and unrelated — backend env-var detection).skillgen --checkis clean; no generated skill files touched.Files changed
graphify/export.pyattach_hyperedges: skip id-less persisted entries when seeding the dedup settests/test_hypergraph.pyCHANGELOG.md