Commit Graph
1507 Commits
Author SHA1 Message Date
safishamsiandClaude Opus 4.8 912d832a60 Fix: stop silently dropping source files during discovery via two over-broad filters (#1666)
@krishnateja7 root-caused this precisely: the files were never reaching
extraction, so the 0.9.7 no-cache-on-empty mitigation could not surface them.
Two discovery-layer filters were the cause:

(a) A bare `snapshots/` directory was pruned as a Jest/Vitest artifact, which
killed legitimate code namespaces like a Rails `app/services/snapshots/`. It is
now pruned only when it actually contains `.snap` files or sits directly under a
JS test root (`__tests__`/`__test__`). `__snapshots__` stays unconditionally pruned.

(b) `_is_sensitive` dropped files on a bare name-keyword hit (device_token.rb,
passwords_controller.rb) even when `classify_file` had already resolved them to
source code. A genuine programming-language source file is now exempt from the
weak keyword heuristic, while real secret stores in data/config formats
(credentials.json, secrets.yaml, .env, .pem, ...) are still caught — those route
through the CODE path for manifest parsing but are deliberately not exempted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:00:31 +01:00
safishamsiandClaude Opus 4.8 bbc3be2238 docs(readme): add animated "path lights up" demo to See it in action
An animated SVG that mirrors the real `graphify path` output: a terminal
types the query on the left while the answer draws itself hop by hop across
the knowledge graph on the right, ending on "3 hops. Zero files opened."

Pure SMIL inside a self-contained dark card, so it renders inline on both
GitHub themes with no JS and no external fonts. Palette is brand-only:
muted emerald (sampled from the logo) as the single accent, no neon.
Generated by scripts/gen_demo_path.py (re-run to regenerate; STATIC=1 bakes
a still frame for QA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:25:35 +01:00
adirbuskilaandClaude Opus 4.8 e801196e4e docs: add Hebrew (he-IL) README translation (#1639)
Adds the Hebrew translation under docs/translations/README.he-IL.md and
links it from the language row (30 -> 31 languages). Merged from PR #1639
by @AdirBuskila; the language-row link was adapted to the current README
structure by hand since the English README changed after the PR was opened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:58:08 +01:00
safishamsiandClaude Opus 4.8 21b851b3d2 Fix: salvage truncated labeling replies and account for labeling token cost (#1690, #1694)
Two related fixes in the community-labeling path:

#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.

#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>
2026-07-06 12:46:12 +01:00
safishamsiandClaude Opus 4.8 b78248f22a Fix: cap Ollama client-side retries so a hung request cannot multiply the stall (#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>
2026-07-06 12:44:47 +01:00
safishamsiandClaude Opus 4.8 0ff584f070 Fix: tolerate tiktoken special-token text in token estimation (#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>
2026-07-06 12:42:39 +01:00
safishamsiandClaude Opus 4.8 97a1371478 Fix: exact-match section heading in _replace_or_append_section to prevent CLAUDE.md data loss (#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>
2026-07-06 12:36:12 +01:00
safishamsiandClaude Opus 4.8 31211a0e7c docs(readme): visual overhaul (hero, capability table, query demo, honest 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>
2026-07-06 02:34:28 +01:00
safishamsiandClaude Opus 4.8 b699182a4f release: 0.9.7
17 fixes/features since 0.9.6. Highlights:
- Ruby: mixes_in edges for include/extend/prepend (#1668) and affected <Class>
  reaching method-bound callers (#1669); constant-receiver call resolution
  hardening continues from #1634.
- Extensionless shebang CLIs are now extracted (#1683); JS/TS rationale + ADR/RFC
  doc refs (#1599); Java stdlib types dropped from references noise (#1603);
  pascal optional extra (#1616).
- Incremental/detect correctness: Office source edits re-enter --update (#1649),
  Windows long-path hashing (#1655), word-count caching (#1656), zero-node
  results no longer cached + warned (#1666).
- JS/TS phantom cross-package calls edge killed (#1659); Windows skill name +
  OpenCode plugin separator + doc-corpus report noise (#1635/#1646/#1657);
  case-insensitive suffix dispatch (#1671); postgres URI on Windows (#1672);
  deferred import() not a cycle (#1241).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.7
2026-07-06 01:31:06 +01:00
guyoron1andClaude Opus 4.8 dd8c24cf7b docs: add .claudeignore tip for prompt cache (#1539); changelog for #1603
Applied the .claudeignore troubleshooting entry from #1539 manually (the PR
branched from an older README). Closes #1387.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 01:08:10 +01:00
NydiaChungandClaude Opus 4.8 92edf78005 feat(java): suppress java stdlib types from references edges (#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>
2026-07-06 01:01:53 +01:00
safishamsiandClaude Opus 4.8 f2b81a9980 docs: changelog credit for #1616
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:56:35 +01:00
Vinicius Machado 7d463c98e2 feat: add pascal optional extra for tree-sitter-pascal
extract_pascal() already imports tree-sitter-pascal for AST-quality
extraction and falls back to a regex extractor when it is absent (#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.
2026-07-06 00:55:18 +01:00
safishamsiandClaude Opus 4.8 a4d4533ae3 docs: changelog credit for #1599
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:48:59 +01:00
Nilton Moura 6d3a6f1c6b feat: extract rationale comments + ADR/RFC doc references from JS/TS
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.
2026-07-06 00:45:51 +01:00
safishamsiandClaude Opus 4.8 5cfd7d9891 docs: changelog credit for #1683
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:12:45 +01:00
stasnamcoandClaude Fable 5 2ab086742a fix(extract): route extensionless shebang scripts to their AST extractor
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>
2026-07-06 00:09:11 +01:00
safishamsiandClaude Opus 4.8 d7aafb0d05 docs(readme): collapse platform picker and optional extras into <details>
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>
2026-07-06 00:03:45 +01:00
safishamsiandClaude Opus 4.8 b06c55eea1 docs(readme): add graph.html hero image + benchmark table
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>
2026-07-05 23:57:59 +01:00
safishamsiandClaude Opus 4.8 21b52e195d docs(readme): new Graphify logo (cropped icon + wordmark)
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>
2026-07-05 12:36:54 +01:00
safishamsiandClaude Opus 4.8 6631af7936 feat(ruby/affected): mixes_in edges for include/extend/prepend + method-bound callers in affected (#1668, #1669)
#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 #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.

#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, #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>
2026-07-05 11:41:18 +01:00
safishamsiandClaude Opus 4.8 d9f97b9c01 docs: changelog credit for #1671, #1672, #1241
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 10:48:42 +01:00
raman118 aa1bbdab23 Fix case-sensitive file suffix filtering silently skipping capitalized/mixed-case extensions (#1671) 2026-07-05 10:42:09 +01:00
Synvoya 94392de640 fix(extract): don't report deferred import() as a file cycle (#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 #1241
2026-07-05 10:42:09 +01:00
raman118 5ffa921e27 Fix invalid virtual postgres source_file URI backslashes on Windows (#1672) 2026-07-05 10:42:09 +01:00
safishamsiandClaude Opus 4.8 1288a557e3 fix(extract): don't cache zero-node results; warn on empty source files (#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>
2026-07-05 10:33:07 +01:00
safishamsiandClaude Opus 4.8 3140b2ecce docs(readme): move star-history chart from top to bottom
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:30:36 +01:00
safishamsiandClaude Opus 4.8 9fea1a42e4 docs: add BENCHMARKS.md and link it from the README
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>
2026-07-05 00:24:52 +01:00
safishamsiandClaude Opus 4.8 54825b6a1c fix: windows skill name, opencode plugin separator, doc-corpus report noise (#1635, #1646, #1657)
#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).

#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).

#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>
2026-07-04 22:43:31 +01:00
safishamsiandClaude Opus 4.8 f9174943a2 fix(detect): incremental correctness for Office sources + long paths, cache word counts (#1649, #1655, #1656)
#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 (#1226).

#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.

#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>
2026-07-04 22:12:47 +01:00
safishamsiandClaude Opus 4.8 62b8eb1416 fix(extract): gate JS/TS cross-file calls on import evidence to kill phantom cross-package edges (#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 #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>
2026-07-04 21:22:39 +01:00
safishamsiandClaude Opus 4.8 983da3c15f docs(readme): sync code-extension list with detect.py
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, #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>
2026-07-04 13:01:18 +01:00
safishamsiandClaude Opus 4.8 29b3f9126d release: 0.9.6
19 fixes/features since 0.9.5. Highlights:
- Ruby: module/Struct.new/Class.new/Data.define container nodes (#1640) and
  constant-receiver singleton-call resolution (#1634) — Rails/Zeitwerk graphs
  now get real cross-file edges.
- Kill cross-language phantom imports_from edges from unresolved bare npm
  imports (#1638); harden semantic extraction against malformed LLM chunks
  (#1631); deterministic graph.json node/edge ordering for parallel semantic
  backends (#1632).
- Contributor extractor fixes: Apex interface multiple inheritance (#1645),
  Kotlin `by` delegation (#1644).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.6
2026-07-04 12:55:35 +01:00
safishamsiandClaude Opus 4.8 13e2bddf4d fix(ruby): extract module/Struct/Class.new containers and resolve constant-receiver calls (#1640, #1634)
#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.

#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 (#1640) AND the resolver (#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>
2026-07-04 12:20:07 +01:00
safishamsiandClaude Opus 4.8 5737388f17 docs: changelog credit for #1645 (apex extends) and #1644 (kotlin by delegation)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:32:07 +01:00
Synvoya 9b040224e1 fix(kotlin): emit implements edge for interface delegation (by)
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.
2026-07-04 11:26:55 +01:00
Synvoya 53c769d23d fix(apex): emit extends edges for interface multiple inheritance
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).
2026-07-04 11:26:54 +01:00
safishamsiandClaude Opus 4.8 e2ef4ef3d1 fix: harden semantic extraction and kill phantom import edges (#1631, #1638, #1632)
#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.

#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.

#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>
2026-07-04 03:12:23 +01:00
safishamsiandClaude Opus 4.8 2ba07e84e1 fix(export): guard to_canvas against dangling community members (#1236 follow-up)
The #1236 fix guarded to_obsidian's member loop but not to_canvas, so
`graphify export obsidian` (which also writes graph.canvas) still crashed with
KeyError on a community member id absent from G — after the notes exported,
leaving a partial mirror. Reported on 0.9.5 by @swells808.

Apply the same `m in G and m in node_filenames` filter in both to_canvas loops:
the box-sizing loop (so the group box matches the cards actually laid out) and
the card-layout loop (so the sort/label deref and the node_filenames fallback
never touch a dangling id). Regression test added alongside the to_obsidian one.
Full suite 2872.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:34:41 +01:00
safishamsiandClaude Opus 4.8 4744dfefca feat(extract): TS/JS member calls on local new-binding + typed-param receivers (#1630)
The #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>
2026-07-03 16:03:18 +01:00
safishamsiandClaude Opus 4.8 21294f0d4f fix(skill): utf-8 encoding in query reference snippets (#1619 A2)
The query reference doc's inline vocab-harvest / fallback-search snippets used
bare Path(...).read_text()/write_text(), which on Windows (default cp1252)
crash with UnicodeEncodeError on the cross-language corpora the doc itself
demonstrates (Cyrillic labels like обработчик). Add encoding="utf-8" to all
five sites in the skillgen source fragment and regenerate; blessed expected/,
skillgen --check + --monolith-roundtrip green.

Scoped to the concrete reproduced crash; the larger #1619 findings (the
Windows .exe interpreter-guard rewrite, INPUT_PATH backslash guidance, BOM
handling) are a separate skill-template pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 11:37:22 +01:00
safishamsiandClaude Opus 4.8 53638d4017 docs(changelog): note stale-source reconciliation on update (#1623)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:33:45 +01:00
oleksii-tumanov 8d8d2b8c0a fix(update): reconcile removed and renamed sources 2026-07-03 10:30:27 +01:00
safishamsiandClaude Opus 4.8 412a29d9d0 docs(changelog): note per-term BFS seed diversity (#1596)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:18:50 +01:00
Mark Butterworth d56ee8384d fix: guarantee per-term BFS seed diversity in query (fixes #1445)
_pick_seeds' gap_ratio cutoff discards any candidate scoring below 20%
of the top score. On a multi-term natural-language query, one term's
incidental EXACT label match on a node that is otherwise unrelated to
the query's intent (e.g. a common word also used as a field name or
identifier elsewhere in the corpus) scores ~1000x higher than any
SUBSTRING match on the query's other, actually-relevant terms
(_EXACT_MATCH_BONUS vs _SUBSTRING_MATCH_BONUS). The cutoff then
silently discards every one of those substring-tier candidates as BFS
seeds, so the traversal only ever explores the neighborhood of the one
unrelated exact match, and `query` returns confidently-wrong results
with no signal that anything went wrong.

This matches #1445's reproduction exactly: a vague query that doesn't
name a target symbol seeds from unrelated "concept-dense" nodes
instead, even though the target node is present in the graph.

_pick_seeds now optionally accepts the graph and the tokenized query
terms; when supplied, it guarantees at least one seed per distinct
term that has any match at all, so one term's collision cannot starve
out the others. Ties within a term are broken by node degree, so an
isolated incidental match doesn't out-rank a real, well-connected hub
for that term. The parameters default to None and existing callers
that don't pass them see byte-identical behavior (see
test_pick_seeds_without_diversity_args_is_unchanged).

Adds a regression test reproducing the exact failure shape from
#1445 and confirms the previously-starved target node is recovered
as a seed once G/terms are supplied.

Full test suite (74 tests) and ruff both pass.
2026-07-03 10:14:57 +01:00
safishamsiandClaude Opus 4.8 cf4b4ef85a fix(build): don't crash when a node's source_file is the scan root (#1618)
A node whose source_file equals the absolute scan root (e.g. a project-level
semantic concept the LLM attributed to the whole repo) relativized to Path('.'),
and _semantic_id_remap fed that into _file_stem, whose path.with_suffix("")
raises `ValueError: '.' has an empty name`. The crash landed in final graph
assembly — AFTER all LLM extraction cost was spent — writing no graph.json at
all, and leaving `cluster-only` to then report "no graph found".

Two guards: _file_stem returns "" for a name-less path (protects every caller,
not just this one), and both _semantic_id_remap passes skip a root-equal
source_file explicitly (it has no per-file identity to remap — id left
untouched). Reported with a minimal LLM-free repro by @sub4biz.

Not a 0.9.5 regression: _semantic_id_remap/_file_stem are byte-identical to
0.9.4; the latent path was only hit when dedup produced a root-source_file node.
4 regression tests (dot-path stem, remap no-crash, build_from_json with a
root-level concept node, normal remap unaffected). Full suite 2849.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:32:24 +01:00
safishamsiandClaude Opus 4.8 eebc406773 feat(extract): C# receiver-typed member-call resolution (#1609)
C# had no member-call resolver (unlike Swift/Python/Ruby/TS/C++/ObjC), so
`recv.Method()` fell back to a bare method-name match against label_to_nid —
which, under ambiguity, silently mis-bound `_server.Save()` to an unrelated
`Cache.Save()`. That's a WRONG edge, not just a missing one, and it left
delegation-heavy C# call graphs (wrappers, service layers) blind across typed
member/param boundaries.

Mirrors the C++ #1547 pattern:
- capture the member_access_expression receiver (simple identifier or `this`)
  into member_receiver and set is_member_call in the C# invocation branch;
- defer ALL C# member calls with a receiver to the resolver (tgt_nid = None) so
  the bare in-file match can't fire, and emit a raw_call tagged lang="csharp";
- _csharp_member_type_table: file-wide name -> Type from fields, properties,
  parameters, and locals (incl. `var v = new T()`), first-binding-wins;
- _resolve_csharp_member_calls: `this` -> enclosing class (EXTRACTED),
  capitalized -> the named type (EXTRACTED), else the receiver's table type
  (INFERRED), each gated by the single-definition guard; no method on the type
  -> no edge. Registered for .cs.

Verified: the ambiguous `_server.Save()` now resolves to Server.Save and NOT
Cache.Save; field/param/local/this/Type.static/cross-file all resolve; dynamic
receiver and absent-method emit nothing; unqualified calls unregressed. 8 new
tests, full suite 2841.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:25:54 +01:00
safishamsiandClaude Opus 4.8 41ce87e442 docs(changelog): note the #1617/#1607/#1615/#1613 fixes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:53:06 +01:00
Paulo PintoandClaude Opus 4.8 35404167d9 fix(extract): emit reference edges for TS/JS decorators
`@Component`, `@Injectable`, `@Input`, `@Inject`, `@Entity`, … produced no
edge — the `decorator` node kind was never walked. This is framework-critical
(Angular, NestJS, Vue class components, TypeORM): the decorators are the
primary signal of what a class is and does.

Decorators occur only on classes, class members, and parameters, so one pass
over each class declaration covers them. `_ts_emit_decorator_edges` emits a
`references` edge (context="decorator") from the decorated entity to the
decorator symbol:

  - class decorators -> the class. Handles both `@Deco class C` (decorator is
    a child of the class) and `@Deco export class C` (decorator sits on the
    wrapping export_statement), plus stacked decorators.
  - method decorators -> the method node. They are siblings preceding the
    `method_definition`; stacked decorators are skipped past to find it.
  - field / accessor decorators -> the class (the field is not a graph node).
  - parameter decorators (`@Inject(T)`) -> the enclosing method/constructor.

The symbol is the head identifier: `@Injectable`, the `function` of
`@Component({...})`, or the `property` of `@ns.Component()`. Targets go
through `ensure_named_node`, so a decorator defined outside the corpus
becomes a sourceless stub, consistent with type references — one per
referencing file, matching the cross-file stub disambiguation introduced
with full-path node IDs in 0.9.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:29 +01:00
Paulo PintoandClaude Opus 4.8 869aaf7502 fix(extract): emit a node for TS namespace / module containers
`namespace Foo {}` parses as `internal_module` and `module Bar {}` (and
ambient `declare module "pkg" {}`) as a named `module` node. Neither kind
was in `class_types`/`function_types` nor handled by an extra-walk, so the
container produced no node — its members were still reached by the default
recurse, but the namespace/module itself was invisible to the graph and its
members lost their namespace context.

Add `_ts_extra_walk`, dispatched for TypeScript after `_js_extra_walk`,
mirroring `_csharp_extra_walk`: it emits a container node + a file→container
`contains` edge and recurses the body, leaving members file-contained as
before. `internal_module` exposes `name`/`body` fields; `module` exposes
none, so name (identifier / nested_identifier / quote-stripped string) and
body (`statement_block`) are found positionally. The `is_named` guard skips
the anonymous `module` keyword token, which shares the `module` type string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:29 +01:00