diff --git a/graphify/export.py b/graphify/export.py index 50ed388a..7ff216bd 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -163,7 +163,13 @@ _CONFIDENCE_SCORE_DEFAULTS = {"EXTRACTED": 1.0, "INFERRED": 0.5, "AMBIGUOUS": 0. def attach_hyperedges(G: nx.Graph, hyperedges: list) -> None: """Store hyperedges in the graph's metadata dict.""" existing = G.graph.get("hyperedges", []) - seen_ids = {h["id"] for h in existing} + # Skip id-less persisted entries when seeding the dedup set (#2775): the + # semantic extractor emits hyperedges with no `id` and build.py persists them + # verbatim, so a prior graph.json can contain id-less hyperedges. A hard + # `h["id"]` here raised `KeyError: 'id'` on every incremental re-extract, + # symmetric with the `.get("id")` guard the loop below already applies to the + # incoming set. + seen_ids = {h["id"] for h in existing if h.get("id")} for h in hyperedges: if h.get("id") and h["id"] not in seen_ids: existing.append(h) diff --git a/tests/test_hypergraph.py b/tests/test_hypergraph.py index 20e79e67..10a57e9a 100644 --- a/tests/test_hypergraph.py +++ b/tests/test_hypergraph.py @@ -137,6 +137,20 @@ def test_attach_hyperedges_skips_entry_without_id(): assert G.graph.get("hyperedges", []) == [] +def test_attach_hyperedges_tolerates_id_less_persisted(): + # Regression for #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. On the next (incremental) run, attach_hyperedges read that + # persisted set with a hard `h["id"]` and died with `KeyError: 'id'`, writing + # nothing. Reading the persisted set must tolerate missing ids. + G = nx.DiGraph() + G.graph["hyperedges"] = [{"nodes": ["a", "b"], "type": "project", "attributes": {}}] + attach_hyperedges(G, [{"id": "flow_a", "label": "Flow A", "nodes": ["A", "B"]}]) + # No crash; the id-less persisted entry is retained and the new id-bearing + # incoming hyperedge is appended. + assert len(G.graph["hyperedges"]) == 2 + + # --------------------------------------------------------------------------- # 3. to_json includes hyperedges key # ---------------------------------------------------------------------------