fix(export): guard to_canvas against dangling community members (#1236 follow-up)
The #1236 fix guarded to_obsidian's member loop but not to_canvas, so `graphify export obsidian` (which also writes graph.canvas) still crashed with KeyError on a community member id absent from G — after the notes exported, leaving a partial mirror. Reported on 0.9.5 by @swells808. Apply the same `m in G and m in node_filenames` filter in both to_canvas loops: the box-sizing loop (so the group box matches the cards actually laid out) and the card-layout loop (so the sort/label deref and the node_filenames fallback never touch a dangling id). Regression test added alongside the to_obsidian one. Full suite 2872. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4744dfefca
commit
2ba07e84e1
@@ -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.
|
||||
|
||||
+7
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user