feat(skill): Sigma.js viz fallback + batched community-label reconciliation#1
Conversation
…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
…d/mixed-case extensions (Graphify-Labs#1671)
…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>
…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>
…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
|
Pushed a follow-up commit addressing review feedback on the sigma.js viz:
Also merged in the latest |
|
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 example and the real 20,097-node/1083-community production graph — zero console errors, correct rendering, icons/colors/filter panel all working. |
|
Second follow-up round, driven by direct feedback after actually using the rendered graph:
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 |
|
Third follow-up round: "more structure" + sidebar fixes.
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. |
Summary
Adds a Sigma.js + graphology (WebGL) fallback for
graph.htmlonce 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):linlog=True+ anode_sizerepulsion halo + a highscaling_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.@sigma/node-image'sNodePictogramProgram), color per dominant module, node labels on their own background box (no sigma built-in for this; used the documenteddefaultDrawNodeLabeloverride point), edges colored + labeled by their dominant relation bucket.cites/rationale_forare represented too), a clickable module legend, and draggable nodes for manual layout tidying.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
graph.jsonsamples.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.<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.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(+--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)graph.jsonsamples, including forced edge cases (empty-communities fallback,edges-vs-linkskey, synthetic hyperedges)graph_sigma.htmlopened 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