feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1701
feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1701docwilde wants to merge 7 commits into
Conversation
…iation vis-network's client-side forceAtlas2 physics simulation is verified slow/laggy past a few hundred nodes regardless of hardware (apa monorepo, 1083-community aggregated view, 2026-07-03). Step 6 now generates a separate WebGL Sigma.js render with the layout precomputed offline in Python once a view exceeds ~300 nodes, documented in a new shared reference (references/sigma-viz.md) shipped to every split-platform skill variant via tools/skillgen. Step 5's community labeling also gains a batched-subagent flow for 100+ communities (sample top-degree nodes per community, dispatch one subagent per ~15-20-community batch in parallel) plus a required cross-batch label-reconciliation pass, since batches only see their own slice and naturally produce duplicate labels across siblings (338 of 1083 communities, 31%, collided before reconciliation on the same run). Ported from local edits to the installed ~/.claude/skills/graphify/ skill made in a prior session against a production run, which were not yet committed to the repo.
…lter panel Fixes a node-sizing bug in the Sigma.js viz: itemSizesReference: "positions" needs autoRescale: false alongside it, per sigma's own fit-sizes-to-positions example, otherwise the one-time auto-fit repositions the graph without symmetrically adjusting sizes. Node size is now also calibrated against the layout's own median nearest-neighbor distance instead of a fixed constant, so communities stay legible instead of overlapping as node count grows. Adds three things vis-network's graph.html doesn't surface: an icon per community's dominant content kind (code/document/paper/image/rationale/ concept, via @sigma/node-image's NodePictogramProgram), a color per dominant top-level module/directory, and a left-side panel to filter by entity kind and by relation type — grouped into calls/structure/ imports/references/documentation-and-concepts/groups so non-code relations (cites, rationale_for, semantically_similar_to, etc.) are represented alongside code relations, not just calls/imports. The module legend doubles as an isolate-by-module toggle. Also fixes several bugs found in review: a crash when no community meets MIN_COMMUNITY_SIZE, a missing 'links'/'edges' key compatibility guard (present elsewhere in the codebase but missing here), hyperedges never being processed so the "Groups" filter was permanently empty, an unescaped innerHTML injection of LLM-generated labels and directory names, a case-sensitive </script> safety check, search results ignoring active filters, and a camera-pan target using raw graph coordinates instead of sigma's own display-space coordinates.
…-labels' (v8 update) into feature branch # Conflicts: # CHANGELOG.md
|
Pushed a follow-up commit addressing review feedback:
Also merged in the latest |
…esolution)
Found by actually opening a generated graph_sigma.html in a real browser
(Chrome via Playwright), which the prior fixes were not verified against:
- autoRescale: false (added to fix the earlier sizing bug) also disables
sigma's automatic "fit the camera to the graph" behavior on load. With
no replacement, the camera stayed at its default state while every
node lives in the 0-1000 layout range Step 1 produces - the camera was
pointed at empty space, not a badly-framed graph. Fixed by calling
fitViewportToNodes (from @sigma/utils, the same package sigma's own
demo uses) once after construction and again in the reset handler,
replacing camera.animatedReset() which had the identical bug.
- More fundamentally: the @sigma/node-image and @sigma/utils ESM imports
were throwing at module-load time. esm.sh resolves their `sigma` peer
dependency (declared as a version range) to a literal broken module
path instead of the pinned 3.0.3, so `import { NodePictogramProgram }
from "..."` never completed - and since that's the first statement in
the module, the entire script died before the Sigma instance was ever
constructed. This is the more fundamental cause of the black screen;
the camera fix alone was not sufficient. Both @sigma/* imports now
pin the dependency explicitly via esm.sh's `?deps=sigma@3.0.3`.
- An independent second-opinion review (a separate agent instance,
briefed only on the bug history and told to be skeptical of every
camera/coordinate claim) caught a third latent bug before it shipped:
the icon SVGs had no explicit width/height, which @sigma/node-image's
raster-image path needs to determine a data-URI icon's intrinsic size.
- maxCameraRatio raised 3 -> 10 for headroom on narrow/short viewports,
per the same review.
Verified end-to-end in a real browser against both a small synthetic
multi-type example (icons/colors/filters all visibly correct, zero
console errors) and the actual 20,097-node/1083-community production
graph that motivated this feature (342 aggregated communities, zero
console errors, correct meta line/filter panel/module legend).
|
Critical fix, found by actually opening the generated HTML in a real browser — the previous fix reported here made the graph render as a solid black screen with nothing visible. Root causes:
Verified end-to-end in Chrome (via Playwright) against both a small synthetic multi-type example and the real 20,097-node/1083-community production graph (apa) that motivated this feature — zero console errors, correct rendering, icons/module-colors/filter panel/legend all working. |
…le links Usability pass driven by actually using the rendered graph against a real 20,097-node/1083-community production corpus: - Layout spread: the default forceAtlas2 parameters (linear attraction, no repulsion halo) packed 87% of nodes within 15% of the bounding-box center on that real graph - genuinely unreadable, not just suboptimal. linlog=True + a node_size repulsion halo proportional to member count + a much higher scaling_ratio drops that to ~15-20% while keeping structurally related communities near each other, verified by measuring nearest-neighbor spacing across several parameter sets before picking one, not by eyeballing a single result. - Edges are now colored AND labeled by their dominant relation bucket instead of a flat gray. renderEdgeLabels: true draws them; sigma has no independent edge-label zoom threshold, but ties edge-label visibility to node-label visibility, which manages clutter on a 1000+-edge graph automatically rather than needing custom logic. - Nodes are draggable (sigma's own downNode/moveBody/upNode/upStage pattern, verified against sigma's actual official example source). The setCustomBBox freeze is required, not optional: without it sigma recomputes its normalization extent from live positions on every reindex even with autoRescale:false, so dragging one node visibly pans every other one. Reset now restores original positions too, not just filters/highlight. - The click panel lists each community's actual representative source files (capped at 8, true total count for "+N more"), linked via file:// when .graphify_root confirms the graph is being viewed on the machine that generated it. - doc_ref (a newer AST-derived file_type not in the original six-value schema, added by an upstream ADR/RFC-citation feature) now gets an icon instead of silently miscategorizing as code. - Filter panel legibility: entity-kind icons were solid black rendered as raw <img> against a dark panel (meant to be WebGL-tinted, not shown directly) - now forced white via CSS. Relation-type checkboxes get a color swatch matching their edge color. Verified end-to-end in a real browser (Chrome via Playwright): a synthetic multi-type example confirmed drag + connected-edge-following + click-highlight + file-panel + reset all compose correctly, and the same was independently confirmed on the real production graph via direct DOM/data inspection (all 342 nodes carry correct file data with resolved absolute file:// URLs) plus visual confirmation of the layout spread, edge colors/labels, and a real click showing real file paths.
…panel Follow-up round driven by direct feedback after actually using the rendered graph, not just reading the code: - Layout: the first spread fix (gravity=0.5, scaling_ratio=20) measured better on a numeric spread proxy but was still reported as too bunched to read labels without manually dragging nodes apart. A second pass (gravity=0.15, scaling_ratio=80) actually fixes it - verified by reading real community labels at the default fit-all zoom on the 342-community production graph, not by trusting the proxy metric a second time. - Node labels get their own small background box. Sigma's default label renderer is bare fillText with no background option; defaultDrawNodeLabel is the documented override point for a custom renderer. First attempt used a near-black fill that was indistinguishable from the canvas's own background color - fixed to a visibly lighter tone matching the UI panel chrome. - The click panel's file list is no longer capped with "+N more" - Step 1 sends every distinct file, rendered as a filterable, scrollable list. Clicking a file opens a movable dialog (dragged via its header, plain screen-pixel CSS, unrelated to sigma's graph/camera coordinate spaces) showing an embedded content preview - Step 1 reads a bounded preview from disk for the first 8 files per community, since embedding full content for every file would bloat the self-contained HTML far more than paths alone do. - Embedding real file content surfaced a new instance of the script-tag-collision risk: a previewed HTML/JS file routinely contains a literal </script (confirmed on the real corpus - a Vite index.html's <script type="module"> tag). This used to be a check-after-the-fact step; Step 3 now escapes it unconditionally before embedding, since previews make the collision routine rather than a rare edge case. - The entity-properties panel, previously a separate floating box that could get clipped by the viewport independently of the main panel, is now nested inside the main sidebar - same width, same scroll, no separate truncation point. A parallel Sonnet review pass over the accumulated diff (this file grew quickly through many rapid incremental edits) found no functional bugs, just two cosmetic nitpicks (an unused data attribute, a stray character in placeholder text), both fixed. Verified end-to-end in a real browser (Chrome via Playwright): the label-background fix, merged panel layout, scrollable/filterable file list, and draggable preview dialog were all confirmed on a synthetic example (including an exact-coordinate drag verified via getNodeDisplayData, since eyeballed pixel coordinates missed targets repeatedly); the layout re-tune and script-tag auto-escape were confirmed on the real 20,097-node/1083-community production graph.
|
Second follow-up round, driven by direct feedback after actually using the rendered graph:
A parallel Sonnet review pass over the accumulated diff found no functional bugs, just two cosmetic nitpicks (an unused data attribute, a stray character in placeholder text), both fixed. Verified end-to-end in a real browser (Chrome via Playwright): the label-background fix, merged panel layout, scrollable/filterable file list, and draggable preview dialog were all confirmed on a synthetic example (switched to exact-coordinate clicks via |
Summary
graph.htmlonce a view exceeds ~300 nodes, with the layout precomputed offline in Python (nx.forceatlas2_layout), documented in a newreferences/sigma-viz.mdshipped to every split-platform skill variant.itemSizesReference: "positions"needsautoRescale: falsealongside it, per sigma's ownfit-sizes-to-positionsexample — otherwise the one-time auto-fit repositions the graph without symmetrically rescaling sizes. Node size is also now calibrated against the layout's own median nearest-neighbor distance instead of a fixed constant.@sigma/node-image'sNodePictogramProgram), color per dominant top-level module/directory, and a left-side panel to filter by entity kind and by relation type — grouped into calls/structure/imports/references/documentation-and-concepts/groups. The module legend doubles as a click-to-isolate toggle.autoRescale: falsealso disables sigma's automatic camera-fit-on-load, so without an explicitfitViewportToNodescall the camera pointed at empty space and the page rendered as a solid black screen; (2) more fundamentally, the@sigma/node-image/@sigma/utilsESM imports were failing at module-load time because esm.sh resolved theirsigmapeer dependency to a broken path instead of the pinned version; (3) icon SVGs needed explicitwidth/heightfor@sigma/node-image's raster path. Also fixes several correctness/security bugs from an earlier code review: a crash when no community meetsMIN_COMMUNITY_SIZE, a missinglinks/edgeskey compatibility guard, hyperedges never being processed, an unescapedinnerHTMLinjection of LLM-generated labels (stored-XSS risk), a case-sensitive</script>safety check, search results ignoring active filters, and a camera-pan target using raw graph coordinates instead of sigma's own display-space coordinates.Why: Sigma.js instead of vis-network past ~300 nodes
vis-network runs a live, single-threaded JS forceAtlas2 physics simulation on load, rendered on a canvas 2D surface; past a few hundred nodes this is genuinely slow/laggy regardless of hardware — verified in production on a 1083-community aggregated view of a large monorepo graph (apa). The fix is to remove client-side physics entirely: precompute the layout once, offline, in Python, and render only via sigma.js's WebGL renderer. This ships as a separate output file (
graph_sigma.html), not a replacement forgraph.html.Why: batched community labeling + reconciliation at scale
One-at-a-time labeling is O(communities) sequential LLM round-trips, and a naive path-prefix heuristic fallback produces duplicated-segment junk at scale. The batched approach samples each community's ~15 highest-degree member nodes and dispatches one subagent per ~15-20-community batch in parallel. Batching independently introduces label collisions across batches (31% of communities in the same production run), so a reconciliation pass is a required final step, not optional polish.
What changed across this PR's review rounds
graph.jsonsamples.autoRescale: falsesilently broke camera framing, and a separate esm.sh peer-dependency resolution issue broke the icon/utils imports entirely. An independent second-opinion review (briefed to be skeptical of every camera/coordinate claim) caught a third latent bug (icon SVG sizing) before it shipped.Scope note
Only the shared
core.mdfragment (covering all 14 split-platform skills, rendered viatools/skillgen) was updated. Theaideranddevinmonolith skill bodies maintain their own independent Step 5/6 sections and were intentionally left untouched.Test plan
uv run python -m tools.skillgen— regenerated all committed skill artifacts across the 12 split platforms +--bless'd theexpected/snapshotsuv run python -m tools.skillgen --check/--audit-coverage/--schema-singleton/--monolith-roundtrip/--always-on-roundtrip— all passuv run python -m pytest -q— full suite green (2962 passed, 3 skipped, with[all]optional extras installed)significant-communities fallback path, theedges-vs-linkskey compatibility path, and a synthetic hyperedge to confirm the "Groups" filter bucketgraph_sigma.htmlwas opened in a real browser (Chrome via Playwright, served over local HTTP) against both a small synthetic multi-type example and the actual 20,097-node/1083-community production graph (apa) that motivated this feature — zero console errors, correct rendering, icons/module-colors/filter panel/legend/click-highlight all confirmed working