Commit Graph

999 Commits

Author SHA1 Message Date
safishamsi 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
guyoron1 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
NydiaChung 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
safishamsi 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
safishamsi 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
safishamsi 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
stasnamco 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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
safishamsi 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 Pinto 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 Pinto 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
Paulo Pinto 09aeb979c8 fix(extract): extract TS/JS generator functions as nodes
Generator functions were invisible to the graph. The declaration form
`function* g()` parses as `generator_function_declaration`, which was
absent from the JS/TS `function_types`, so it produced no node; the
expression form `const h = function*(){}` parses as `generator_function`,
which was absent from the JS function-value types, so it was never captured
when assigned to a module-level const. Generator *methods* (`*gen()` in a
class) were already covered — they parse as `method_definition`.

Add `generator_function_declaration` to the JS and TS `function_types` (so
it emits a node and its body is walked) and to `function_boundary_types`
(so its calls are scoped to it, parity with `function_declaration`); add
`generator_function` to `_JS_FUNCTION_VALUE_TYPES` (so the const-assigned
expression form is captured like `function_expression`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:28 +01:00
Tok6Flow0 009a98b6dd Contain symlinked extraction inputs 2026-07-02 22:29:32 +01:00
Paulo Pinto 9811def1b3 fix(extract): capture the TS import-equals form (import x = require(...))
`import x = require("./m")` produced no edge at all: tree-sitter parses it
as an `import_statement` whose module string sits inside an
`import_require_clause`, not as a direct child of the statement, so the
direct-child string scan in `_import_js` never found it. The file-level
dependency was silently dropped while the equivalent ESM form
(`import * as x from "./m"`) was captured — an invisible hole in the
import graph of TS codebases that interop with CommonJS modules.

Restructure the scan to first locate the module string — a direct `string`
child for ESM imports/re-exports, or the `string` nested inside an
`import_require_clause` for the import-equals form — then emit the
`imports_from` edge from the single shared path. Relative paths, tsconfig
aliases, and bare modules all resolve through the same
`_resolve_js_import_target` as ESM, giving the import-equals form exact
parity with a namespace import: one file-level `imports_from` edge.

Plain JS is unaffected (the grammar has no `import_require_clause`), and
the pure namespace alias form (`import A = B.C`) is out of scope — it has
no module string and models an intra-code alias, not an import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:29:32 +01:00
Ashmit 1226c34731 Treat .mts/.cts (TypeScript module extensions) as TypeScript
`.mts` (ESM) and `.cts` (CommonJS) are the TypeScript counterparts of the
already-supported `.mjs`/`.cjs`, but graphify handled them in neither the
code-extension detection nor the TypeScript grammar path — so they were either
skipped entirely (classified as non-code) or, once detected, parsed with the plain
JavaScript grammar, which silently drops every `type`/`interface`/type-alias
declaration.

Detection — add `.mts`/`.cts` alongside their siblings in:
- detect.CODE_EXTENSIONS            (the code / non-code gate)
- analyze._LANG_FAMILY              (cross-language edge family → "js")
- extract._JS_RESOLVE_EXTS          (import target resolution)
- extract._JS_CACHE_BYPASS_SUFFIXES
- build.py per-node language map    (→ "js")

TypeScript grammar — route `.mts`/`.cts` to the plain TypeScript grammar (like
`.ts`; neither is JSX), so their TS-only syntax is captured:
- extract_js()                      (grammar selector: .mts/.cts → _TS_CONFIG)
- extract._DISPATCH                 (.mts/.cts → extract_js)
- the use_ts tree-sitter selector
- the typescript_member_calls resolver frozenset

Result: a `.mts`/`.cts` file now extracts identically to the equivalent `.ts`.
Verified on a real 47 KB `.mts` source — 36 nodes for `.mts`/`.cts`, matching `.ts`
exactly (was 22 with the JS grammar; the 14 dropped nodes were TS type/interface
declarations). No new dependency — same tree-sitter-typescript grammar as `.ts`.

Tests: tests/test_typescript_module_extensions.py — membership regression locks for
the extension sets, dispatch routing, and end-to-end parity asserting `.mts`/`.cts`
capture the TS `type`/`interface` nodes and match the `.ts` node set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:29:31 +01:00
Jeisson 32ff6d6fb3 fix(claude-cli): deliver extraction instructions in the user turn
The claude-cli backend passed the extraction schema via --system-prompt with
only the raw file dump in the user turn, assuming a replacement system prompt
is the model's sole authority. Claude Code >= ~2.1 (verified on 2.1.197) does
not honour that: it still layers in the local coding-agent context
(CLAUDE.md/AGENTS.md in cwd, skills, MCP) and, given a user turn that is just a
file with no request, replies conversationally ("I see the file, but there's no
actual request attached"). That prose parses to zero nodes/edges, so
_response_is_hollow flags it as truncation and the adaptive-retry path bisects
the chunk indefinitely (94 -> 47 -> 23 -> ...), never converging and never
writing graph.json.

Move the full extraction schema plus an explicit imperative into the user turn
and drop --system-prompt, so the CLI emits the JSON object directly. The
<untrusted_source> prompt-injection guardrails are carried verbatim; model
override, --add-dir image handling, timeout, and token accounting are untouched.
2026-07-02 22:29:30 +01:00
sanmaxdev 62f49bad73 fix: persist cluster-only analysis sidecar 2026-07-02 22:29:29 +01:00
safishamsi d89ec68af9 release: 0.9.5
Two 0.9.4 regressions (CLI cross-file indirect_call, stale community labels on
re-cluster), the case-folding god-node fix (#1581), ~15 language extractor
fixes (Ruby/Groovy/Elixir/Fortran/Rust/Julia/SystemVerilog/Scala/PowerShell/
ObjC/PHP/C#/C++/Swift), merge-graphs mixed-type handling (#1606), Swift
singleton-into-local resolution (#1604), Homebrew python@ shebang (#1586),
hooks foreground-stall perf (#1601), serve query stopwords (#1597) and
multi-project MCP serving (#1594), plus JSON-loading hardening, dedup collision
warning, and Windows hook worker limit.

Built wheel validated in a clean venv: CLI reports 0.9.5, import resolves to the
installed package, the case-folding and Swift-singleton fixes verified, and a
real `graphify extract` produces indirect_call + inherits edges end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.5
2026-07-02 13:03:34 +01:00
Joan F Garcia 9e7fbcbd6e feat(serve): optional project_path for multi-project MCP serving
Add an optional project_path to every MCP tool so one server process can
answer against many projects. Omitted -> the server's default graph (fully
backward-compatible); an absolute project_path -> that project's
<GRAPHIFY_OUT>/graph.json, routed per call.

Implementation (all in _build_server):
- _ctx_cache + _load_ctx(): per-graph context cache with mtime+size
  hot-reload, unified across the default graph and every project graph.
  Unlike _load_graph it raises instead of sys.exit on a bad file.
- _resolve_graph_path()/_select_graph(): map project_path to a graph.json
  and rebind G/communities/active_graph_path per call. No hot-path lock:
  select+handler run in one synchronous span of the call_tool coroutine.
- Tolerant startup: serve with no default graph (pure multi-project mode)
  instead of exiting; a bad project_path is a tool error, not a crash.
- project_path injected as an optional (non-required) field on all tools.

Tests: 3 new HTTP-transport tests (optional-on-every-tool, routing,
bad-path-no-crash). Full serve suite green (91 passed).
2026-07-02 12:11:51 +01:00