Skip to content

feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1701

Draft
docwilde wants to merge 7 commits into
Graphify-Labs:v8from
docwilde:feat/sigma-viz-batched-community-labels
Draft

feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1701
docwilde wants to merge 7 commits into
Graphify-Labs:v8from
docwilde:feat/sigma-viz-batched-community-labels

Conversation

@docwilde

@docwilde docwilde commented Jul 6, 2026

Copy link
Copy Markdown

Summary

  • Adds a Sigma.js + graphology (WebGL) fallback for graph.html once a view exceeds ~300 nodes, with the layout precomputed offline in Python (nx.forceatlas2_layout), documented in a new references/sigma-viz.md shipped to every split-platform skill variant.
  • Fixes a node-sizing bug in that view: 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 rescaling sizes. Node size is also now calibrated against the layout's own median nearest-neighbor distance instead of a fixed constant.
  • Adds an icon per community's dominant content kind (via @sigma/node-image's NodePictogramProgram), 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.
  • Adds a batched-subagent flow to Step 5's community labeling for 100+ communities, replacing one-at-a-time labeling and a naive path-prefix heuristic, plus a required cross-batch label-reconciliation pass (338 of 1083 communities, 31%, collided before reconciliation on a real production run).
  • Fixes a chain of rendering bugs that only surfaced when actually opening a generated file in a real browser: (1) autoRescale: false also disables sigma's automatic camera-fit-on-load, so without an explicit fitViewportToNodes call the camera pointed at empty space and the page rendered as a solid black screen; (2) more fundamentally, the @sigma/node-image/@sigma/utils ESM imports were failing at module-load time because esm.sh resolved their sigma peer dependency to a broken path instead of the pinned version; (3) icon SVGs needed explicit width/height for @sigma/node-image's raster path. Also fixes several correctness/security bugs from an earlier code review: a crash when no community meets MIN_COMMUNITY_SIZE, a missing links/edges key compatibility guard, hyperedges never being processed, an unescaped innerHTML injection 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 for graph.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

  1. Initial feature (Sigma.js fallback + batched labeling).
  2. Sizing/icon/module/filter-panel follow-up, verified by two independent review agents (code review + sigma.js API fact-check) and by executing the Python precompute step against real graph.json samples.
  3. A black-screen regression, found only by actually opening a generated file in a real browser (Chrome via Playwright) and reading the console — the sizing fix's autoRescale: false silently 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.md fragment (covering all 14 split-platform skills, rendered via tools/skillgen) was updated. The aider and devin monolith 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 the expected/ snapshots
  • uv run python -m tools.skillgen --check / --audit-coverage / --schema-singleton / --monolith-roundtrip / --always-on-roundtrip — all pass
  • uv run python -m pytest -q — full suite green (2962 passed, 3 skipped, with [all] optional extras installed)
  • The Step 1 Python precompute block was extracted verbatim from the committed doc and executed against a real ~1900-node/141-community sample graph, including forcing the empty-significant-communities fallback path, the edges-vs-links key compatibility path, and a synthetic hyperedge to confirm the "Groups" filter bucket
  • The full generated graph_sigma.html was 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

…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.
@docwilde docwilde marked this pull request as ready for review July 6, 2026 11:53
@docwilde docwilde marked this pull request as draft July 6, 2026 12:57
docwilde added 2 commits July 6, 2026 15:45
…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
@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Author

Pushed a follow-up commit addressing review feedback:

  • Fixed the node-sizing bug: autoRescale: false is required alongside itemSizesReference: "positions", per sigma's own fit-sizes-to-positions example — otherwise the one-time auto-fit repositions the graph without symmetrically rescaling 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 into a blob as node count grows.
  • Added a left-side filter panel for entity kind (code/document/paper/image/rationale/concept, shown as icons via @sigma/node-image's NodePictogramProgram) and 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, not just calls/imports.
  • Added module/top-level-directory color-coding with a clickable legend that doubles as an isolate-by-module toggle — a lightweight grouping tool without introducing a new library.
  • Fixed several bugs an independent code review caught: 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 bucket was permanently empty), an unescaped innerHTML injection of LLM-generated community 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 (confirmed against sigma's demo SearchField.tsx).

Also merged in the latest v8 to pick up the 0.9.7 release.

@docwilde docwilde marked this pull request as ready for review July 6, 2026 14:10
@docwilde docwilde marked this pull request as draft July 6, 2026 14:12
…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).
@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Author

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:

  1. autoRescale: false (added for the sizing fix) also disables sigma's automatic camera-fit-to-graph on load. Fixed with fitViewportToNodes from @sigma/utils, called on init and in the reset handler (replacing camera.animatedReset(), which had the identical bug).
  2. More fundamentally: the @sigma/node-image/@sigma/utils ESM imports were throwing at module-load time — esm.sh resolved their sigma peer dependency to a broken literal path instead of the pinned version, so the import never completed and the entire script died before anything else ran. Fixed by pinning explicitly via ?deps=sigma@3.0.3 on both imports.
  3. An independent second-opinion review (a separate agent instance, told to be skeptical of every camera/coordinate claim) caught a third bug before shipping: icon SVGs need explicit width/height for @sigma/node-image's raster path to size them correctly.
  4. maxCameraRatio raised 3 → 10 for headroom on narrow/short viewports, per the same review.

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.

docwilde added 2 commits July 6, 2026 17:01
…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.
@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Author

Second follow-up round, driven by direct feedback after actually using the rendered graph:

  • Layout re-tuned again: the first spread fix (gravity=0.5, scaling_ratio=20) measured better on a numeric proxy but was still reported as too bunched to read labels without manually dragging nodes apart. gravity=0.15, scaling_ratio=80 actually fixes it — verified by reading real labels at default zoom on the 342-community production graph, not by trusting the proxy metric a second time.
  • Node labels get a 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 indistinguishable from the canvas's own background color, fixed to match the UI panel's tone.
  • Edges colored + labeled by their dominant relation bucket, draggable nodes (sigma's downNode/moveBody/upNode/upStage pattern, with the setCustomBBox freeze required to stop one dragged node from panning every other one), an uncapped filterable/scrollable file list (no more "+N more"), and clicking a file now opens a movable dialog with an embedded content preview (bounded to the first 8 files per community — 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.
  • 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.

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 getNodeDisplayData after eyeballed pixel coordinates kept missing targets); the layout re-tune and script-tag auto-escape were confirmed on the real 20,097-node/1083-community production graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant