diff --git a/CHANGELOG.md b/CHANGELOG.md index f10412d..f87cb04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## Unreleased +- Fix: `graphify export obsidian` no longer crashes in `to_canvas` on a dangling community member (#1236 follow-up, thanks @swells808). The original #1236 fix guarded `to_obsidian` but not `to_canvas`, so a community member id with no backing node in the graph still raised `KeyError` while writing `graph.canvas` — after the notes had exported, leaving a partial mirror. `to_canvas` now applies the same dangling-member filter (`m in G and m in node_filenames`) in both the box-sizing and card-layout loops. + - Feat: TS/JS member calls on a local `new` binding or a type-annotated parameter now resolve (#1630, thanks @DanielC000). `const s = new Svc(); s.doThing()` and a call on a typed param — including inside a returned closure (`(svc: Svc) => () => svc.doThing()`) — now emit `calls` edges to the receiver type's method, so `affected` no longer silently under-reports. Extends the #1316 `this.field` resolver: the per-file type table now also learns local `new` bindings and bare-typed parameters, and `walk_calls` descends into inline/returned closures (attributing their calls to the enclosing function) instead of stopping at the arrow boundary. Resolution keeps the single-definition guard; an untyped or non-bare-typed (array/union/generic) receiver produces no edge. - Fix: the `query` reference doc's inline vocab/fallback snippets now read and write files with `encoding="utf-8"` (#1619 A2, thanks @edtrackai). On Windows (default cp1252) the bare `read_text()`/`write_text()` calls crashed on exactly the cross-language corpora the doc demonstrates (e.g. Cyrillic labels like `обработчик`). Fixed across all generated skill variants. diff --git a/graphify/export.py b/graphify/export.py index 176b179..36e4f99 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -1278,7 +1278,10 @@ def to_canvas( group_sizes: dict[int, tuple[int, int]] = {} group_cols: dict[int, int] = {} for cid in sorted_cids: - members = communities[cid] + # Skip dangling community members with no backing node / filename, so box + # sizing matches the cards actually laid out and `G.nodes[m]` never + # KeyErrors below — mirrors the to_obsidian guard (#1236). + members = [m for m in communities[cid] if m in G and m in node_filenames] n = len(members) inner_cols = max(1, math.ceil(math.sqrt(n))) w = max(600, 220 * inner_cols) @@ -1351,6 +1354,9 @@ def to_canvas( # Node cards inside the group - laid out in the same ceil(sqrt(n))-column # grid the box was sized for (group_cols[cid]), so cards fill the box. inner_cols = group_cols[cid] + # Same dangling-member guard as the sizing loop and to_obsidian (#1236): + # a community id absent from G / node_filenames would KeyError the sort. + members = [m for m in members if m in G and m in node_filenames] sorted_members = sorted(members, key=lambda n: G.nodes[n].get("label", n)) for m_idx, node_id in enumerate(sorted_members): col = m_idx % inner_cols diff --git a/tests/test_obsidian_dangling_member.py b/tests/test_obsidian_dangling_member.py index bda3119..2e0844f 100644 --- a/tests/test_obsidian_dangling_member.py +++ b/tests/test_obsidian_dangling_member.py @@ -48,3 +48,23 @@ def test_obsidian_community_of_only_dangling_members(tmp_path): ghost_note = tmp_path / "_COMMUNITY_Community 1.md" assert ghost_note.exists() assert "**Members:** 0 nodes" in ghost_note.read_text(encoding="utf-8") + + +def test_canvas_dangling_community_member_does_not_crash(tmp_path): + """#1236 follow-up: the fix landed in to_obsidian but not to_canvas, so + `graphify export obsidian` (which also writes graph.canvas) still crashed + with KeyError in to_canvas on a dangling member. The same guard now applies + to both the box-sizing loop and the card-layout loop.""" + import json + from graphify.export import to_canvas + + G, comms = _graph_with_dangling_member() + out = tmp_path / "graph.canvas" + to_canvas(G, comms, str(out)) # before the fix: KeyError: 'agents_doc' + assert out.exists() + + canvas = json.loads(out.read_text(encoding="utf-8")) + node_ids = {n.get("id") for n in canvas.get("nodes", [])} + # real members get cards; the dangling id does not + assert "n_n0" in node_ids and "n_n1" in node_ids + assert "n_agents_doc" not in node_ids