Skip to content

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

Merged
docwilde merged 41 commits into
v8from
feat/sigma-viz-batched-community-labels
Jul 6, 2026
Merged

feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1
docwilde merged 41 commits into
v8from
feat/sigma-viz-batched-community-labels

Conversation

@docwilde

@docwilde docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a Sigma.js + graphology (WebGL) fallback for graph.html once a view exceeds ~300 nodes, plus a batched-subagent flow for community labeling at scale, both validated against a real production run (apa, a 20,097-node/1083-community monorepo) — then hardened across three follow-up review rounds.

Sigma.js viz (graph_sigma.html):

  • Layout precomputed offline in Python (no client-side physics), using linlog=True + a node_size repulsion halo + a high scaling_ratio, tuned twice — a first pass measured better on a numeric spread proxy but was still reported as too bunched to read labels without manual dragging; the second pass actually fixes it, verified by reading real labels at default zoom on the real production graph.
  • Icon per community's dominant content kind (@sigma/node-image's NodePictogramProgram), color per dominant module, node labels on their own background box (no sigma built-in for this; used the documented defaultDrawNodeLabel override point), edges colored + labeled by their dominant relation bucket.
  • Left-side filter panel (entity kind + relation type, grouped so non-code relations like cites/rationale_for are represented too), a clickable module legend, and draggable nodes for manual layout tidying.
  • Click panel lists every one of a community's source files (filterable, scrollable, uncapped), and clicking a file opens a movable dialog with an embedded content preview (bounded to the first 8 files per community, since full content for every file would bloat the self-contained HTML far more than paths alone do), with a file:// link when opened on the machine that generated the graph.

Community labeling (Step 5): a batched-subagent flow for 100+ communities (sample top-degree nodes, dispatch one subagent per ~15-20-community batch in parallel) replacing one-at-a-time labeling and a naive path-prefix heuristic, plus a required cross-batch label-reconciliation pass — batches only see their own slice, so duplicate labels across siblings are the common case at scale (31% of communities collided on the same production run), not an edge case.

Why: Sigma.js instead of vis-network past ~300 nodes

vis-network runs a live, single-threaded JS forceAtlas2 physics simulation on load; past a few hundred nodes this is genuinely slow/laggy regardless of hardware. The fix is removing client-side physics entirely: precompute the layout once, offline, and render only via WebGL. Ships as a separate output file, not a replacement — vis-network is fine below the threshold and has features (a community filter dropdown, confidence-styled edges) this lighter template doesn't replicate.

Review history

  1. Initial feature, landed via two parallel review agents (code review + sigma.js API fact-check) plus execution of the Python precompute step against real graph.json samples.
  2. A black-screen regression, found only by actually opening a generated file in a real browser: autoRescale: false (added for a sizing fix) silently broke camera framing, and a separate esm.sh peer-dependency resolution issue broke the icon/utils imports entirely before any script code ran. An independent second-opinion review caught a third latent bug (icon SVG sizing) before it shipped.
  3. A usability pass after actually using the rendered graph surfaced: layout still too bunched (re-tuned a second time), a near-invisible label background color, file previews needing a script-tag auto-escape (a previewed HTML file's own <script> tag collided with the page's), and a floating info panel getting clipped independently of the main sidebar.

Note: this PR mirrors upstream #1701 against Graphify-Labs/graphify, which continues to receive further hardening rounds (module clustering/hulls/edge decluttering, a post-load window-resize fix) after this PR merged.

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 (+ --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)
  • Step 1's Python precompute block executed against real graph.json samples, including forced edge cases (empty-communities fallback, edges-vs-links key, synthetic hyperedges)
  • The full generated graph_sigma.html opened in a real browser (Chrome via Playwright, served over local HTTP) against a synthetic multi-type example and the real 20,097-node/1083-community production graph — zero console errors, correct rendering, icons/colors/labels/filter panel/legend/click-highlight/dragging/file dialog all confirmed working, layout spread confirmed readable at default zoom without manual dragging

safishamsi and others added 30 commits July 3, 2026 16:03
…receivers (Graphify-Labs#1630)

The Graphify-Labs#1316 resolver handled `this.injectedField.method()`, but a receiver whose
type comes from a local `const x = new Foo()` binding (Pattern A) or a
type-annotated parameter — including inside a returned closure (Pattern B) —
produced no calls edge, so `affected <method>` silently under-reported.

- _ts_receiver_type_table: augment the per-file type table with local
  `new` bindings (name -> constructor type) and bare-typed parameters
  (`(svc: Svc)` -> svc: Svc), merged after the constructor-injection entries
  (which win on a name clash). Only a bare type_identifier is recorded — an
  array/union/generic/qualified/predefined type is skipped (precision).
- walk_calls now descends into an inline/returned JS/TS closure that is not
  separately tracked in function_bodies (e.g. `return () => svc.doThing()`),
  attributing its calls to the enclosing function, instead of stopping at the
  arrow boundary. A tracked-body-id set prevents double-walking const-assigned
  arrows.

The existing _resolve_typescript_member_calls then resolves both via the
receiver type with its single-definition guard. Verified on the real-CLI shape
(absolute paths + graphify-out cache): both patterns resolve, ambiguity binds
to the right class (Svc not Cache), untyped/array-typed receivers emit nothing.
5 tests, full suite 2871.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hify-Labs#1236 follow-up)

The Graphify-Labs#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>
…fy-Labs#1631, Graphify-Labs#1638, Graphify-Labs#1632)

Graphify-Labs#1631: a malformed LLM chunk (a stray non-dict entry in edges/nodes/hyperedges)
crashed the AST+semantic merge and the semantic-cache write with
`AttributeError: 'list' object has no attribute 'get'`, discarding every
successful chunk and writing no graph.json. `_parse_llm_json` now sanitizes each
fragment at the single parse chokepoint (dict entries only; non-list values
coerced to []), protecting the cache writer, the adaptive-retry merge, and the
CLI merge in one place.

Graphify-Labs#1638: an unresolved bare npm import (`import colors from "tailwindcss/colors"`)
emitted an imports_from edge to the bare id `colors`, which build.py's
pre-migration alias index then remapped onto an unrelated local file of that
stem (backend/utils/colors.py) - a confident EXTRACTED cross-language phantom
edge, one per importing file. The external-import fallback now namespaces its
target with the `ref` prefix (the J-4 convention), so it can never collapse to a
local node id; the ref target has no node, so build drops it as an external
reference.

Graphify-Labs#1632: with a parallel LLM backend, extract_corpus_parallel merged chunk results
in completion order, so which network call returned first reordered nodes/edges
run-to-run even when the model returned identical content - churning graph.json.
Chunks are now merged in deterministic submission order after the pool drains
(matching the serial path); the progress callback still fires in completion
order. The model's own content variance is unchanged (irreducible).

Full suite: 2882 passed, 3 skipped. Validated end-to-end via a local wheel build
on a mixed TS+Python corpus: `explain colors.py` shows only the real importer,
and graph.json is byte-identical across repeated runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
interface X extends A, B captured the parent list in iface_re group 2 but the handler only read group 1, so no inheritance edge was emitted. Split the parent list and emit one extends edge per parent (mirroring the class branch).
class Foo : Bar by baz produced no edge because the delegation_specifier loop only handled constructor_invocation and bare user_type children; the by form wraps user_type in an explicit_delegation node. Add that branch so the implements edge (and generic-arg recovery) fires.
…hify-Labs#1644 (kotlin by delegation)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stant-receiver calls (Graphify-Labs#1640, Graphify-Labs#1634)

Graphify-Labs#1640 (node extraction): the extractor only created nodes for `class Foo`, so
plain `module Foo`, `Foo = Struct.new(...) do ... end`, `Foo = Class.new(Super)`
and `Result = Data.define(...)` produced no container node — their methods hung
off the file via `contains` with dot-less labels and no edge could target them.
`module` is now a container type (methods attach via `method`, nested modules
included), and a constant assignment whose RHS is Struct.new/Class.new/Data.define
synthesizes a class node named after the constant, attaches block-defined methods
to it, and emits an `inherits` edge for `Class.new(Super)`. Plain constant
assignments (MAX = 100, X = Foo.new) are untouched.

Graphify-Labs#1634 (resolution): constant-receiver singleton calls (`Service.call`,
`Model.where`, `SomeJob.perform_async`) emitted no edge, so a Zeitwerk-autoloaded
Rails app (no requires) had near-zero cross-file edges. resolve_ruby_member_calls
now handles a capitalized receiver with any callee: bind to the class's owned
singleton/instance method (`def self.call`) when present, else to the class node
itself so inherited/dynamic class methods (ActiveRecord where/find_by) still give
blast-radius. Namespaced receivers resolve by bare class name. The
single-owning-class god-node guard is kept — ambiguous receivers resolve to
nothing, never a wrong edge.

The two compound: PaymentProcessor#process -> TaxCalculator.rate_for needs the
module node (Graphify-Labs#1640) AND the resolver (Graphify-Labs#1634); both now land.

Full suite: 2893 passed, 3 skipped. Adversarial smoke confirms no false class
nodes from plain/multiple assignments and no self-loops on self-class calls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 fixes/features since 0.9.5. Highlights:
- Ruby: module/Struct.new/Class.new/Data.define container nodes (Graphify-Labs#1640) and
  constant-receiver singleton-call resolution (Graphify-Labs#1634) — Rails/Zeitwerk graphs
  now get real cross-file edges.
- Kill cross-language phantom imports_from edges from unresolved bare npm
  imports (Graphify-Labs#1638); harden semantic extraction against malformed LLM chunks
  (Graphify-Labs#1631); deterministic graph.json node/edge ordering for parallel semantic
  backends (Graphify-Labs#1632).
- Contributor extractor fixes: Apex interface multiple inheritance (Graphify-Labs#1645),
  Kotlin `by` delegation (Graphify-Labs#1644).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "What files it handles" code row omitted several extensions that reuse
existing tree-sitter grammars (so the grammar count is unchanged): `.mts`/`.cts`
(TypeScript, Graphify-Labs#1607, new in 0.9.6), `.cc`/`.cxx` (C++), `.kts` (Kotlin), `.psd1`
(PowerShell), `.toc` (Lua). Apex (`.cls`/`.trigger`) and Terraform already have
their own rows. `.r`/`.ejs`/`.ets` are intentionally left out — they are in
CODE_EXTENSIONS but have no registered extractor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…phantom cross-package edges (Graphify-Labs#1659)

When a callee had exactly one same-named definition repo-wide, the cross-file
resolver emitted a `calls` edge at INFERRED/0.8 even with no import path between
caller and callee. On a monorepo this fabricated dependencies: a 14-package repo
showed `platform`/`sidecar` depending on `registry-protocol` purely because it
exported generically-named symbols that unresolved calls collapsed onto.

JS/TS modules have no implicit cross-module scope, so a cross-file call is real
only if the caller imported it. Direct JS/TS cross-file `calls` attribution is
now gated on import evidence and left unresolved otherwise. Scoped to direct
calls: other languages keep the Graphify-Labs#1553 single-candidate resolution (C/C++
headers, Ruby autoload, same-package implicit scope), and the indirect_call path
(already INFERRED + callable-gated) is untouched.

Also hardens caller/candidate -> file mapping to resolve via the node's
`source_file` string (identifying the file node by its basename label) instead
of `relative_to(root.resolve())`, which threw on a path-resolution/symlink
mismatch and fell back to a non-matching absolute id — spuriously failing import
evidence. This both makes the new gate safe and fixes legitimate cross-file
calls being mislabeled INFERRED instead of EXTRACTED.

Full suite: 2898 passed, 3 skipped. Verified via CLI on the reporter's repro
(phantom dropped) and a control (imported call resolves EXTRACTED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… cache word counts (Graphify-Labs#1649, Graphify-Labs#1655, Graphify-Labs#1656)

Graphify-Labs#1649: detect_incremental tracks the converted markdown sidecar, and
convert_office_file early-returned whenever the sidecar existed — so a .docx/
.xlsx edited after its first conversion never updated its sidecar and was
reported "unchanged" forever, freezing the graph. It now re-converts when the
source is newer than the sidecar (bumping the sidecar so the hash check catches
it); an unchanged source still skips the rewrite (Graphify-Labs#1226).

Graphify-Labs#1655: _md5_file/save_manifest/count_words used plain open()/stat(), which the
Windows file APIs reject for absolute paths over 260 chars unless prefixed with
`\\?\`. Deeply-nested files never hashed, their manifest entry never stabilized,
and detect_incremental re-flagged them as changed every run. A new _os_path adds
the extended-length prefix on win32 for change-detection I/O (mirror of
cache._normalize_path, which strips it for keys). No-op elsewhere.

Graphify-Labs#1656: detect() re-parsed every PDF/docx/text file to size the corpus on each
run. Word counts are now memoized in the existing content-hash stat index (keyed
by size + mtime_ns), so an unchanged file is parsed once. file_hash's fastpath is
guarded so a word-count-only entry (no hash) can't KeyError, and both writers
augment a co-located entry in place instead of clobbering the other's field.

Full suite: 2906 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… noise (Graphify-Labs#1635, Graphify-Labs#1646, Graphify-Labs#1657)

Graphify-Labs#1635: the windows skill variant declared `name: graphify-windows`, but
`graphify install --platform windows` writes it to ~/.claude/skills/graphify/
SKILL.md and Claude Code requires the folder name to equal the frontmatter
`name` — the suffix broke discovery. platforms.toml now sets name = "graphify"
(regenerated + re-blessed).

Graphify-Labs#1646: the OpenCode (and Kilo) plugin prepended its reminder with `&&`, which
Windows PowerShell 5.1 rejects as a statement separator, breaking the first
bash command of every session. Switched to `;` (valid in PowerShell 5.1, Bash,
POSIX).

Graphify-Labs#1657: the GRAPH_REPORT.md "Import Cycles" section printed "None detected" on
documents-only corpora where imports don't exist — now gated on code nodes /
import edges being present. The other two items in that issue (mojibake in
manifest/report, stdout encoding) are already handled on current v8: both files
are written UTF-8 and main() reconfigures stdout/stderr to UTF-8.

Full suite: 2909 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a benchmark writeup covering graphify as long-term memory (LOCOMO,
LongMemEval-S vs mem0/supermemory/bm25/dense/hybrid) and as a code-intelligence
layer (ERPNext), run on graphify's own harness with competitors as adapters:
one shared model (Kimi K2.6), identical budgets, shared BGE-m3 embedder where
allowed, and a judge blind-validated against a second judge (90.6% agreement,
kappa 0.81). Numbers are wins-forward but every retained figure is exact; the
supermemory recall comparison is labeled embedder-confounded. README gets a
short Benchmarks section linking to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es (Graphify-Labs#1666)

krishnateja7 reported that on a full-repo run a stable subset of Ruby files
yields zero nodes (not even a file node), each fine in isolation, drop set
byte-stable across runs. Root cause is a transient batch/parallel extraction
that produces an empty result, which then gets cached and persists.

Every extractable file yields at least a file node, so a zero-node result is
anomalous. Both extraction paths (parallel worker and sequential fallback) now
skip the cache write when a non-error result has no nodes, so a rerun re-extracts
and self-heals instead of loading the stale empty. extract() also warns, listing
the files that landed in the graph with zero nodes, so the previously-silent
blindness in affected/explain is visible.

This addresses the persistence and the silent blindness. The underlying trigger
(why a valid file occasionally extracts empty when co-processed with certain
others) was not reproducible with synthetic corpora; the warning now surfaces it
for a concrete report if it recurs.

Full suite: 2912 passed, 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1241)

`_dynamic_import_js` emitted a deferred `import('./x')` as a plain
`imports_from` edge, so `find_import_cycles` counted it as a static import.
A file that statically imports another which dynamically imports it back was
reported as a phantom circular dependency.

Keep the edge as `imports_from` (the dependency stays visible in the graph)
but mark it `deferred`, and skip deferred edges in `find_import_cycles`.

Closes Graphify-Labs#1241
…aphify-Labs#1241

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…od-bound callers in affected (Graphify-Labs#1668, Graphify-Labs#1669)

Graphify-Labs#1668: Ruby `include`/`extend`/`prepend <Const>` in a class/module body now emits
a `mixes_in` edge to the module. The mixin is captured during the node walk and
resolved cross-file by resolve_ruby_member_calls (single-owner guard, reusing
the Graphify-Labs#1640 module nodes as targets). The shared call pass skips these markers so
they are not mislabeled as `calls`. `extend self` and non-constant args are
skipped; ambiguous/undefined modules produce no edge. Rails concern composition
is now visible to affected/explain.

Graphify-Labs#1669: affected <Class> seeds the reverse walk with the root's own member nodes
(one method/contains hop) so callers that bind at method granularity (e.g.
Service.call -> the def self.call node, Graphify-Labs#1634) are reachable from the class.
method/contains stay out of the general relation-filtered walk (no forward
noise), and the seeded member nodes are not reported as hits.

Full suite: 2924 passed, 3 skipped. Verified end-to-end (Rails-shaped repros)
plus edge cases: extend self / undefined / ambiguous mixins emit nothing, mixins
are not emitted as calls, member methods aren't reported, class-level callers
still resolve, and one-hop seeding does not pull in downstream classes' methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the old v4-hosted SVG wordmark with the new brand logo (graph-cube
icon + "Graphify" on the green brand gradient), tightly cropped from the source
export (1384x645, ~2.15:1, even ~90px padding). Served from docs/logo.png on v8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a hero screenshot of the interactive graph (FastAPI codebase, community
coloring) right after the tagline so the value prop is shown, not just told.
Converts the Benchmarks prose into a compact scannable table (LOCOMO recall/QA,
LongMemEval, zero-credit build) while keeping the judge-validation credibility
line and the link to BENCHMARKS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ils>

The 20+ row platform-install table and the optional-extras table (~60 lines
combined) sat between Install and the value content. Wrapping both in collapsible
<details> blocks keeps a skimmer moving to the report/commands sections while each
platform's command stays one click away. No content changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
detect.classify_file already labels extensionless files with a bash/python/
node/... shebang as CODE via _shebang_interpreter, but _get_extractor
dispatched purely on path.suffix — so a CLI entry point like `devctl` or
`manage` was detected as code and then silently contributed zero nodes to
the graph (its doc-referenced symbols stayed dangling stubs).

Resolve extensionless files through the same _shebang_interpreter and a
new _SHEBANG_DISPATCH map. Only interpreters with a real extractor are
mapped (python/bash-family/node/ruby/lua/php/julia); detect's wider set
(perl, fish, tcsh, Rscript) stays unmapped and skipped rather than being
mis-parsed by a wrong grammar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parity with _extract_python_rationale: Python files get rationale nodes
from docstrings and '# NOTE:'-style comments, but JS/TS comments were
discarded entirely. This adds a post-pass to extract_js that:

1. extracts rationale comments ('// NOTE:', '// WHY:', block-comment
   '* NOTE:' variants) as rationale nodes with rationale_for edges,
   matching the Python behavior;
2. first-classes architecture-decision references (ADR-NNNN, RFC NNNN)
   found in comments as doc_ref nodes with 'cites' edges from the file.

The doc_ref pass is the natural join point between code and design docs
in mixed corpora: teams conventionally cite ADR ids in file headers, but
today those citations produce no edges, so code<->ADR connections never
form even when the discipline exists. Spellings are normalized
(ADR-11 / ADR 0011 -> ADR-0011) so references to the same document
collapse to one node, and string literals are excluded (comment-shaped
lines only).

Tested on a real mixed corpus (Flutter/Supabase monorepo): router.ts
alone yields 10 ADR citations that previously produced zero edges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_pascal() already imports tree-sitter-pascal for AST-quality
extraction and falls back to a regex extractor when it is absent (Graphify-Labs#781),
but the grammar was not declared anywhere in the package metadata, so it
was never installed and the AST path never ran out of the box.

Declare a `pascal` extra (and add it to `all`) so users can opt into the
AST extractor with `uv tool install "graphifyy[pascal]"`. tree-sitter-pascal
publishes prebuilt wheels for every platform (win/macOS/Linux), so unlike
the `dm` extra it needs no C toolchain.

On a mid-size Delphi codebase the AST path yields notably more accurate
relationship edges than the regex fallback (calls and inherits both up
~25%). README extras table and uv.lock updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1603)

Adds a _JAVA_BUILTIN_TYPES skip list so ubiquitous java.lang/util/io/time/etc.
type names (String, List, Map, Optional, ...) are not emitted as references
edges (they never resolve to a project node). Mirrors _GO_PREDECLARED_TYPES /
_PYTHON_ANNOTATION_NOISE. Nested user-type generic args still resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
guyoron1 and others added 5 commits July 6, 2026 01:08
…angelog for Graphify-Labs#1603

Applied the .claudeignore troubleshooting entry from Graphify-Labs#1539 manually (the PR
branched from an older README). Closes Graphify-Labs#1387.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 fixes/features since 0.9.6. Highlights:
- Ruby: mixes_in edges for include/extend/prepend (Graphify-Labs#1668) and affected <Class>
  reaching method-bound callers (Graphify-Labs#1669); constant-receiver call resolution
  hardening continues from Graphify-Labs#1634.
- Extensionless shebang CLIs are now extracted (Graphify-Labs#1683); JS/TS rationale + ADR/RFC
  doc refs (Graphify-Labs#1599); Java stdlib types dropped from references noise (Graphify-Labs#1603);
  pascal optional extra (Graphify-Labs#1616).
- Incremental/detect correctness: Office source edits re-enter --update (Graphify-Labs#1649),
  Windows long-path hashing (Graphify-Labs#1655), word-count caching (Graphify-Labs#1656), zero-node
  results no longer cached + warned (Graphify-Labs#1666).
- JS/TS phantom cross-package calls edge killed (Graphify-Labs#1659); Windows skill name +
  OpenCode plugin separator + doc-corpus report noise (Graphify-Labs#1635/Graphify-Labs#1646/Graphify-Labs#1657);
  case-insensitive suffix dispatch (Graphify-Labs#1671); postgres URI on Windows (Graphify-Labs#1672);
  deferred import() not a cycle (Graphify-Labs#1241).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nest benchmarks table)

Rework the top of the README into a self-contained pitch:
- New logo + Trendshift badge; collapse the 31-language row into a toggle so the
  tagline and hero land in the first screen.
- Hero screenshot of the FastAPI graph, tagline with selective bold.
- 'What it does' capability table and a 'See it in action' section with verbatim
  explain/path output (real relations + confidence tags).
- Benchmarks as a scannable table, honest: shows where competitors lead, only
  bolds rows graphify wins or ties, names the tie.
- Collapse platform picker + optional extras into <details>; typography and
  identifier-formatting polish; fix orphaned paragraphs; reword the confidence-tag
  gloss; drop the redundant callflow CTA from the hero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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.
…vent CLAUDE.md data loss (Graphify-Labs#1688)

The updater located its managed block by substring (`marker in content`
and `next(... if marker in line)`), so a heading that merely appeared as
a substring of another line, or a duplicate heading, matched the wrong
offset and the rewrite could truncate or drop unrelated content in
CLAUDE.md / AGENTS.md.

It now matches the section heading exactly (`line.strip() == marker`),
appends when the section is absent, and prefers the last exact match
when several exist. Thanks @bdfinst for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@docwilde docwilde marked this pull request as ready for review July 6, 2026 11:39
safishamsi and others added 6 commits July 6, 2026 12:42
…fy-Labs#1685)

`_TOKENIZER.encode(content)` raises ValueError by default when the text
contains a special token such as `<|endoftext|>`, so a doc or corpus that
merely mentions these strings crashed the entire semantic pass. Both
`encode` sites in `_estimate_file_tokens` now pass `disallowed_special=()`
so such text is tokenized as ordinary bytes. Thanks @Kyzcreig for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the stall (Graphify-Labs#1686)

A stalled local model wedged for `timeout * (max_retries + 1)`, which with
the default 6 retries turned one long stall into a ~21-minute block with no
progress. `_call_openai_compat` now defaults the Ollama backend to zero
client-side retries (a local model that stalls will not un-stall on retry);
set `GRAPHIFY_MAX_RETRIES` to opt back in. Other backends are unchanged.

This bounds the wait; the underlying stall is driven by the model server
and is non-deterministic, so it is not eliminated. Thanks @Kyzcreig for the report.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n cost (Graphify-Labs#1690, Graphify-Labs#1694)

Two related fixes in the community-labeling path:

Graphify-Labs#1690 (thanks @vdgbcrypto): a truncated or slightly malformed reply no
longer discards the whole batch with "Expecting value: line 1 column 6".
`_parse_label_response` now salvages the complete `"id": "name"` pairs from
a reply that failed a strict `json.loads` (e.g. one truncated mid-object),
raising only when no pairs can be recovered. The per-batch token budget was
also raised (256 + 48*n, was 64 + 24*n) so models that prepend a short
preamble have headroom to finish the JSON. The exact provider truncation
could not be reproduced without a live key; the parser and budget address
the mechanism.

Graphify-Labs#1694 (thanks @sub4biz): cluster-only mode reported a hardcoded
`0 input * 0 output` token cost because the labeling LLM calls were never
accounted for. `_call_llm` now accumulates per-response usage into an
optional accumulator threaded through the labeling path and surfaced in
GRAPH_REPORT.md. Backends that do not return usage (the Claude Code CLI)
contribute nothing, which is honest rather than estimated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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
Owner Author

Pushed a follow-up commit addressing review feedback on the sigma.js viz:

  • 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.
  • Added a left-side filter panel for entity kind (code/document/paper/image/rationale/concept, shown as icons via @sigma/node-image) and relation type (grouped into calls/structure/imports/references/documentation-and-concepts/groups — covering non-code relations like cites/rationale_for, not just calls/imports).
  • Added module/directory color-coding with a clickable legend that doubles as an isolate-by-module toggle.
  • Fixed several bugs an independent code review caught: a crash when no community meets MIN_COMMUNITY_SIZE, a missing links/edges key compatibility guard, hyperedges never being processed (so the "Groups" filter was permanently empty), an unescaped innerHTML injection of LLM-generated labels, a case-sensitive </script> safety check, search results ignoring active filters, and a camera-pan target using raw graph coordinates instead of sigma's display-space coordinates.

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

@docwilde docwilde merged commit d763ac1 into v8 Jul 6, 2026
@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Owner 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.

Verified end-to-end in Chrome (via Playwright) against both a small synthetic example and the real 20,097-node/1083-community production graph — zero console errors, correct rendering, icons/colors/filter panel all working.

@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Owner 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.
  • Node labels get a background box (sigma has no built-in option for this; used the documented `defaultDrawNodeLabel` override point) — first attempt used a near-black fill indistinguishable from the canvas background, fixed to match the UI panel's tone.
  • Edges colored + labeled by relation type, draggable nodes, uncapped filterable/scrollable file list, and a movable file-preview dialog with embedded content (bounded to the first 8 files per community).
  • Embedding real file content surfaced a new script-tag-collision case (a previewed HTML file's own `<script>` tag) — now auto-escaped unconditionally rather than checked after the fact.
  • The entity-properties panel is now nested inside the main sidebar (same width, no separate truncation point) instead of floating separately.

A parallel Sonnet review of the accumulated diff found no functional bugs, just two cosmetic nitpicks (fixed). Verified end-to-end in a real browser: label backgrounds, panel merge, file list, and the draggable dialog on a synthetic example (using exact getNodeDisplayData coordinates after eyeballed clicks kept missing); layout spread and the script-tag fix on the real 20k-node production graph.

@docwilde

docwilde commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Third follow-up round: "more structure" + sidebar fixes.

  • Module clustering: same-module edges get a boosted layout-only weight before forceAtlas2 runs, so modules cluster spatially instead of scattering by topology alone. Verified via within/between-module distance ratio: 0.96 → 0.48.
  • Module hulls: a soft translucent region drawn behind each module's cluster (pure-Python convex hull, no scipy dependency), rendered on a canvas layer behind sigma's own, synced on every afterRender.
  • Edge decluttering: edges below a weight percentile hidden by default (toggle to show all; anything touching the highlighted node always shows). This surfaced a real gap — nothing ever called applyReducers() at startup, so the reducers had been inert until first interaction the whole time (harmless before since it was a no-op; not harmless now).
  • Sidebar bug: position: absolute let the panel drift out of sync with the viewport on any page scroll — read as "truncated on my laptop." Fixed to position: fixed + visible custom scrollbar (macOS hides scrollbars by default) + height instead of max-height so it's a proper full-height sidebar.

Verified on the real 20k-node production graph: hulls render/track pan-zoom correctly, the edge toggle visibly changes density, module-isolate gates its hull too, and the sidebar renders correctly full-height at both a 1440×760 laptop viewport and 2560×1440.

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.

9 participants