Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: a lazy CommonJS `require(...)` inside a function body (the idiom for breaking circular dependencies) now emits the same `imports_from`/`imports` dependency edges as a top-level require, attributed to the enclosing function, instead of being silently dropped; a dynamic `require(variable)` is still skipped (#2700, thanks @rajanpanth).
- Fix: a JS/TS identifier bound by an import whose target resolves outside the scanned corpus (e.g. a `lucide-react` icon) is now shadowed, so using it as a value no longer fabricates an INFERRED `indirect_call` onto an unrelated same-named callable elsewhere in the corpus; a relative/in-corpus import still resolves to its real target (#2757, thanks @phudayyy).
- Fix: an OCaml qualified call `M.f` to an external module (one not defined in the same file, e.g. Hardcaml's `Reg_spec.create`) no longer binds to a same-named local `let f` — which produced a false `calls` edge and, when the caller was that local `f`, a `f -> f` self-loop. External qualified calls are kept as a distinct target labelled by the full path; unqualified calls and calls into a locally-defined module still resolve locally, and cross-file `Geo.area` still collapses onto another file's `area`.
- Fix: an incremental re-extract no longer dies at the merge step with `KeyError: 'id'` when a prior `graph.json` carries id-less hyperedges (which the semantic/LLM extractor routinely emits and `build.py` persists verbatim); `attach_hyperedges` now seeds its dedup set only from persisted entries that actually have an `id`, symmetric with the guard already applied to the incoming set (#2775, thanks @otterswimware). The first run always worked, so this surfaced as flaky extraction rather than a deterministic crash.

## 0.9.43 (2026-08-14)

Expand Down
8 changes: 7 additions & 1 deletion graphify/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ def _yaml_str(s: str) -> str:
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)
Expand Down
14 changes: 14 additions & 0 deletions tests/test_hypergraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
Loading