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>
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>
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.
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.
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>
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>
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>
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>
#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>
`_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
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>
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>
#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>
#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>
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>
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>
#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>
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.
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).
#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>
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>
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>
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>
_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.
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>
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>
`@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>
`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>
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>
`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>
`.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>
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.
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>
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).