1705 Commits
Author SHA1 Message Date
safishamsi 23f2ffaa43 release: 0.9.58 — python/php/bash import resolution, dedup source-path normalization, SQL indexes, rust statics, install/labelling robustness, all-extra psycopg v0.9.58 2026-09-10 23:19:02 +01:00
safishamsi 519f322125 chore(lock): sync uv.lock for the psycopg[binary] all-extra (#3483) 2026-09-10 23:05:15 +01:00
L4XB fc179f3394 fix(packaging): include psycopg[binary] in the all extra
The postgres extra's psycopg[binary] was the sole dep missing from all; a new
union test guards the all-extra against future drift. (#3483)
2026-09-10 23:05:04 +01:00
L4XB ec12e5e341 test: isolate backend-detection tests from the developer's environment
Autouse conftest fixture clears every backend env var detect_backend() reads
(API keys, Azure/AWS/Ollama), with a self-guard test that the cleared list
covers every variable the detector reads. (#3484)
2026-09-10 23:04:40 +01:00
safishamsi 2f9279f108 test(labeling): keep the no-backend test hermetic against the claude-cli fallback
#3475 added a PATH probe for a claude-cli labelling fallback; the no-backend
test must neutralize it so it stays deterministic on machines with claude
installed (follow-up to #3475).
2026-09-10 22:45:31 +01:00
ktsang622 d9e094c2e2 fix: label via an installed claude CLI, and stop pinning a path that expires
Two independent failures, both silent, found while repairing 16 repositories whose
graphs had quietly stopped tracking their code.

1. `graphify label` downgraded graphs while reporting success

`detect_backend()` is key-based, and `claude-cli` is the one backend with no API key
to find — `_get_backend_api_key` can never return one for it, and the fallback loop
excludes it by name. Nothing else looked for it. So on a machine with the Claude Code
CLI installed and no API key, labelling announced "no LLM backend configured",
replaced every real community name with a `Community N` placeholder, and exited 0.
Run over an existing graph, that overwrites good names with worse ones and looks like
success; taking the exit code at face value commits the damage.

The fallback is placed in the labelling path, NOT in `detect_backend()`. Widening
detection itself was the first attempt and it broke
`test_mixed_repo_without_key_errors_and_points_at_code_only`: extraction deliberately
refuses to run without a configured backend and points at `--code-only`, and on any
machine with the CLI present that contract would have silently changed to "shell out
to the CLI instead". That contract is unchanged here, and a test now pins it.

2. The post-commit hook pinned a path with an expiry date

`_pinned_python()` wrote `sys.executable` verbatim. graphify installed from inside a
snap-confined editor lives under `~/snap/<app>/<revision>/`, and snap swaps that
revision on update and prunes the old tree. Every hook pinned to it then dies —
observed across 15 repositories at once when an editor snap moved past revision 259.
Each commit printed "could not locate a Python with graphify installed" and the graphs
stopped following the code, with nothing failing loudly enough to notice.

Two changes: `_pinned_python()` declines to pin a path under a rotating revision
directory, which its docstring already describes as safe degradation; and the
uv-tools probe — which already scans tool environments — additionally scans
snap-confined HOMEs by glob, since an install made inside a snap lands in that snap's
private HOME and the plain `$HOME` roots never see it once the hook runs from an
ordinary shell.

Verified: same 25 pre-existing failures before and after (unrelated: terraform
fixtures, a TS scaling test); passing goes 5110 -> 5119, the nine tests added here.
Each change was mutation-tested — removing it fails a test that the others still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c6065e24ea)
2026-09-10 22:44:40 +01:00
dajiaohuang 496112d6fc fix(install): do not abort the install when always-on registration is unwritable
`install()` copies the skill files first and writes the always-on
registration afterwards, so an unguarded write to `~/.claude/CLAUDE.md`
left a half-completed install plus a traceback whenever that file could
not be written. That is the normal shape for a declaratively managed
dotfile: nix/home-manager symlinks it into a read-only /nix/store, and
chezmoi or stow with read-only sources do the same.

Route the two registrations inside `install()` (CLAUDE.md and
CODEBUDDY.md, which had the identical unguarded pattern) through one
helper that reports the skip on stderr and lets the install finish, so
the skill files that were already laid down stay usable.

Refs #3474

Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit 47041eef5d)
2026-09-10 22:44:40 +01:00
himanshupatro-334 437c9629bd Fix same-file source path collision during merge
(cherry picked from commit fa1284a356)
2026-09-10 22:44:39 +01:00
L4XB ab80fa7413 fix(rust): extract module-level static and const declarations
The Rust extractor gated every non-function item on struct_item,
enum_item and trait_item, so tree-sitter-rust's static_item and
const_item never produced a node: a constant existed in the graph only
through the files that referenced it, never from the Rust that defines
it (0 of 783 names in a jdx/mise snapshot).

Add a branch for both node types: a module-level declaration gets a
node with a file-level contains edge, an associated const inside an
impl is attributed to the impl like a method, and the declared type is
referenced the way a struct field type is.

Fixes #3471

(cherry picked from commit 70ff464064)
2026-09-10 22:44:38 +01:00
safishamsi adf814fd5e docs(readme): replace the header card with the graphify logo (png) 2026-09-10 15:39:24 +01:00
safishamsi b15cc167ee docs(readme): replace the header logo with the animated graphify card 2026-09-10 15:30:39 +01:00
L4XB d2abe4ed0c fix(sql): emit index nodes for CREATE [UNIQUE] INDEX statements
Adds an AST branch for create_index in the SQL extractor, emitting an index
node with an 'indexes' edge to its table (cross-file stub when the table is
defined elsewhere), mirroring the existing trigger handling. (#3470)
2026-09-10 14:51:54 +01:00
L4XB 355eefd391 fix(ts): keep star-export resolution off same-named interface methods
A workspace-barrel import (`import { evaluate } from '@scope/domain'`)
resolves through `export *` by probing each star target for a symbol
node with the imported name. The symbol map normalizes labels by
stripping the leading dot, so an interface/class METHOD `.evaluate()`
in the first star target satisfied the probe before the exported
function in the second one was reached. The import edge and the
cross-file call then bound to the method node, and the real function
lost its inbound `calls` edge (read as dead code).

Track member nodes separately and skip them in the star-export walk: a
module can only re-export top-level bindings. A member never shadows a
same-named top-level symbol in the map anymore either.

Fixes #3436

(cherry picked from commit 52e4a2b131)
2026-09-10 14:03:23 +01:00
xuxiongjian 76de060036 Never infer an empty repo tag for global add
`tag or source.parent.parent.name` is empty whenever the graph is not two levels
below a named directory: `graphify global add /tmp/graph.json`, or any relative
path. Every node is then prefixed `::`, the prune key is `""`, and the manifest
entry cannot be removed because the `remove` subcommand reads an empty tag as a
missing argument.

Infer through `distinct_repo_tags`, the helper `merge-graphs` already uses, so the
two entry points cannot diverge again and the degrade is `"repo"`; absolutize
first so a relative path names the repo dir rather than the cwd placeholder. An
omitted `remove` tag stays a usage error, while an explicitly empty one is now
addressable so stores written by earlier revisions can be cleaned up.

(cherry picked from commit 6a317e6ef2)
2026-09-10 14:03:22 +01:00
xuxiongjian dba63f7c9e Import Iterable in build.py, the annotations already use it
build.py annotates chunks and ast_sources with Iterable but never imports it, so
ruff reports four F821 on v8 and typing.get_type_hints() raises on
_tier_replacement_sources, merge_raw_extraction and build_merge. Nothing breaks at
import because `from __future__ import annotations` keeps the hints as strings.

The added test walks build.py's own functions rather than the package: nx under
if TYPE_CHECKING in cross_repo_calls.py, cross_repo_types.py and prs.py is
unresolvable on purpose, while build.py imports every annotated name at module scope.

(cherry picked from commit c315a15c24)
2026-09-10 14:03:22 +01:00
edwardselby 3dc3f4d78c fix(bash): resolve exec-position script paths built from a variable
`script_invocation` edges (#1756) only ever resolved a path that is a
bare literal -- `./helpers.sh`, or `bash ./helpers.sh`. Both paths into
that branch go through `literal()`, which rejects by design any token
containing `$`, so the single most common way a shell script calls a
sibling is invisible:

    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    "$script_dir/helpers.sh" --flag

`cmd` is None for that command node, the `elif cmd and ...` guard short-
circuits, and no edge is emitted. On a 206-file shell tree that uses the
`script_dir` idiom throughout, `script_invocation` produced *zero* edges
-- the feature was effectively dead for that whole style of codebase.

The `source` branch already solved the same problem for
`source "$DIR/lib/x.sh"` (#2079): strip the leading `${VAR}/` segments
with `_bash_source_suffix` and resolve the literal remainder against the
sourcing file's own directory, because the canonical `script_dir` idiom
makes that variable equal to exactly that. This applies the identical
treatment to the exec position -- read the command-name node's raw text
rather than its `literal()`, strip the same way, and fall through to the
existing `resolved.is_file()` gate.

Nothing new is fabricated. `_bash_source_suffix` still returns None when
the remainder holds another expansion (`"$dir/$name.sh"`) or a `..`
segment, and the on-disk check still means a wrong script-dir guess
emits nothing at all. The three existing negative tests -- missing,
shadowed, and dynamic invocations -- pass unchanged.

Measured on the same 206-file tree: 0 -> 16 edges across 10 distinct
file pairs, every one hand-checked to a real invocation of a real file,
no false positives. Full suite: no change in outcomes (the 25 failures
here are pre-existing and unrelated -- terraform, ollama, skillgen).

Fixes #3416.

(cherry picked from commit 2e112c39fc)
2026-09-09 17:20:57 +01:00
abhay-codes07 34735fa762 fix(detect): probe every out subdirectory under an entry budget, not the first 20
Review follow-up: a wide TS outDir keeping compiled files only under a
late-sorted module dir slipped past the 20-subdirectory cap. The level-two
probe now covers every subdirectory, bounded by total entries scanned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit ff0a8f6d64)
2026-09-09 17:20:57 +01:00
abhay-codes07 ba9b3e3d6c fix(detect): gate the bare out dir on build-output evidence (#3347)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit eca5af36f1)
2026-09-09 17:20:57 +01:00
ayushcodes10 465270fba3 Prefer the alias for a PHP use import edge's target
An aliased use clause (use GuzzleHttp\Client as HttpClient) always
targeted the bare imported name, ignoring the alias entirely. Two
files importing the same external class under different local
aliases, a real pattern for disambiguating two same named classes
from different namespaces, produced two different stub targets for
one class, splitting its identity. The cross file resolver repoints
a stub through the file's own alias to FQN map, keyed by the alias
when the import has one, so a bare name derived stub could never be
found under that key and stayed stuck, unresolved. The alias, when
present, is also what the rest of the file actually references, so
preferring it here keeps this edge's target consistent with those
reference edges too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
(cherry picked from commit 73f9f1667b)
2026-09-09 17:20:56 +01:00
himanshupatro-334 5dd24de1c9 Fix loose sibling module import resolution
(cherry picked from commit d00d3aea14)
2026-09-09 17:20:56 +01:00
flaukowski d4531f3d25 fix(python): resolve submodule imports inside PEP 420 namespace packages
`from . import brain, ledger` in a directory with no __init__.py — a
namespace package, which `python -m pkg.mod` runs without complaint —
emitted nothing: _resolve_python_module_path returns None when there is
no module file to probe, and the #1146 submodule branch sits behind that
None. So every `brain.think()` / `ledger.write()` call in such a repo was
invisible to the #1883 module arm, and the most-called functions carried
in-degree 0 in the graph.

_resolve_python_namespace_dir mirrors the module-path walk (relative
base, scan root, then sys.path-root ancestors per #2072) and returns only
a directory that exists inside the root and has no __init__.py. The fact
collector uses it when the module path resolves to no file; the existing
submodule probe then emits the same imports_from edges a regular package
gets. A name that is not a module file on disk still emits nothing, and
a namespace package binds no symbols of its own.

Measured on a 275-node Python repo laid out this way, same graphify
version with and without the change: calls 201 -> 307, nodes unchanged;
think() 0 -> 15 callers, remember() 0 -> 10, ledger write() 0 -> 20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTNEBgonu6ASph6w8R2Wcp
(cherry picked from commit e2ec796bfd)
2026-09-09 17:20:55 +01:00
himanshupatro-334 c967797f62 Fix Python nested function resolution
(cherry picked from commit 416be1e9c7)
2026-09-09 17:20:54 +01:00
safishamsi 3f82bf7f83 release: 0.9.57 — incremental cross-file merge fix, richer dedup survivor, C# generic call sites, JS this-member forms v0.9.57 2026-09-09 13:00:13 +01:00
himanshupatro-334 32b209ae10 Fix incremental merge of cross-file project stubs
(cherry picked from commit 8286a51511)
2026-09-09 11:30:29 +01:00
abhay-codes07 5e6c2be092 fix(dedup): pick the richer duplicate as survivor and keep the losers' fields (#3372)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit a3ba93b859)
2026-09-09 11:30:29 +01:00
abhay-codes07 1dc0dc9423 fix(js): keep this.X = function members in every enclosing-function form (#3408)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 08487e9a73)
2026-09-09 11:30:28 +01:00
abhay-codes07 5d09dce42c fix(csharp): resolve unqualified generic call sites, Get<int>(...) (#3406)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 6d8086d585)
2026-09-09 11:30:27 +01:00
safishamsi 67f99bd005 release: 0.9.56 — rust trait methods, Windows temp-path bound, hook-skip subshell, @/ + Node subpath imports, undirected MCP query, builtin member calls, worker cleanup, TS normalizer perf, Dart source_location v0.9.56 2026-09-07 20:47:42 +01:00
safishamsi 20a631ea70 test(ts): measure CPU time in the normalizer scaling test to de-flake it
perf_counter counts wall-clock, which includes time the process is preempted
off-CPU during a busy full-suite run, inflating the larger measurement and
flaking the ratio. process_time counts only CPU work, isolating the algorithmic
scaling. Follow-up to #3402.
2026-09-07 20:30:19 +01:00
ayushcodes10 3b9356bae6 Add source_location to Dart extraction
extractors/dart.py hardcoded source_location: None at every node and
edge construction site, so a Dart graph could cite zero lines no
matter how many declarations it had (0% across three real repos,
~25k unciteable nodes in one of them).

Threads a line number through add_node/add_edge from each match's own
offset, anchored on the capture group rather than the whole match: a
pattern like the class declaration's leading ^\s* otherwise reports
the line too early when blank lines precede the declaration.

The comment stripping pass also needed a fix first: it deleted a
comment token outright, so a multi-line comment shifted every later
offset (and therefore every later line number) up by the comment's
own height. Blanking it to the same number of newlines instead keeps
src_clean's line count in step with the original file.

Sourceless reference stub nodes (an inherited class, a mixin, an
import target, and similar names the file only references rather
than defines) keep source_location: None, same as before, since there
is no single line in THIS file for a symbol that lives elsewhere.
Every node this file actually defines, and every edge attributed to a
real declaration site, now carries its real line.

Fixes #3365.

(cherry picked from commit 0dbcefc5ba)
2026-09-07 20:21:35 +01:00
Sagexd08 82b25b9a96 fix(extract): make _TsRangeIndex a real type alias
The alias was written as a quoted string, which makes it a `str` *value*
rather than a type. Type checkers reject it: mypy reports "Variable
_TsRangeIndex is not valid as a type" at both annotation sites, plus two
cascading errors where the tuple loses its type and the unpacking/indexing
in _ts_ranges_containing and _normalize_ts_import_types become untyped.

`typing.get_type_hints` happened to resolve it at runtime because it
evaluates the annotation string and then the alias string again, so nothing
failed in practice -- but that is an accident of double evaluation, not a
working alias.

Unquote the right-hand side. The module already has
`from __future__ import annotations`, so the annotations themselves need no
quoting either. No behavior change.

(cherry picked from commit d902820ccf)
2026-09-07 20:21:35 +01:00
Sagexd08 1dcb1e1f3a fix(extract): remove quadratic scan in TS import-type normalization (#3359)
`_normalize_ts_import_types` filtered each `import(...)` regex match against
every tree-sitter `type_arguments` byte range with a nested linear scan, in
both the pre-mask and post-mask passes:

    matches = [m for m in matches
               if not any(start <= m.start() < end
                          for start, end in original_type_ranges)]

That is O(matches x ranges), and `m.start()` is re-evaluated inside `any()`
for every range — which is why profiles blamed `re.Match.start`/`_sre` and
`sample` appeared to show a pathological regex. The regexes are all linear;
the quadratic caller was the real cost.

Both counts grow with file size in TypeScript that mixes generic calls
(`fn<A, B>(x)` — one `type_arguments` range each) with `import(...)` types,
which is the shape of generated and barrel modules in a large monorepo. On
the filter alone: a 0.8MB/10k-import file took ~21s, 2.1MB/25k ~2.6min, and
4.2MB/50k ~11.8min. With several such files `extract` never finished, and a
worker sat at 100% CPU holding no open files because the work is entirely
in-memory after the read.

Replace both scans with a binary search over start-sorted ranges plus a
prefix-maximum of `end` values, so a lookup touches nesting depth rather
than every range. Type-argument ranges nest but never partially overlap, so
the prefix maximum is a correct stop condition; a *closed* inner sibling
must not terminate the leftward walk, which a naive running-max break gets
wrong. Verified against the old predicate with 500k differential cases,
including non-nested random ranges: zero mismatches.

Normalization output is unchanged — byte-length and line-count preserving,
same masking decisions, and the runtime-vs-type disambiguation from 6baa5e0
still holds.

Also restore the emptiness guard in the second pass: the range list is now
an index tuple, which is always truthy, so it checks the spans it holds.

The new test asserts on scaling (2x input must not ~4x the time) rather than
wall-clock, so it is not machine-dependent. It fails at ~3.4x before this
change and passes after.

(cherry picked from commit b662361d8b)
2026-09-07 20:21:35 +01:00
vldlnr 98d62e231c test(labeling): pin max_concurrency=1 so batch-order assertion is deterministic
The assertion recorded the order batches reached their LLM call inside worker
threads, which is nondeterministic; the product merges by cid on the main
thread and concurrency equivalence is covered by a sibling test. Takes only the
flaky-test fix from #3374 (its SECURITY.md/uv.lock changes are handled
separately).
2026-09-07 19:03:11 +01:00
Julien E 2d54b05db2 fix(js): resolve Node subpath imports via package.json imports
`import X from '#services/x'` — Node's subpath-imports field, and the way
every AdonisJS 6 app reaches its own modules — was never resolved: the JS
resolver only knew tsconfig `paths` and workspace packages. In a real
Adonis app 438/680 alias imports produced no edge and `affected
"ZitadelService"` listed 2 nodes from a stray root script instead of the
7 controllers and the auth middleware that depend on it.

Resolve `#…` specifiers through the nearest package.json `imports` map,
after tsconfig `paths` (explicit aliases keep precedence, #1269) and
before workspace packages. Node semantics: exact key or a single `*`
wildcard, longest literal prefix wins, condition objects go through
_resolve_export_target, and only `./` targets are followed so an external
mapping keeps the ref-namespaced external id (#1638). The map applies to
every file in the package, so a nested tsconfig without the aliases
(e.g. `inertia/tsconfig.json`) no longer shadows it. Cached per start
dir and cleared per extract() run like the tsconfig caches (#2917).

Measured on a 966-file AdonisJS + Inertia repo: alias imports resolved
go from 242/680 to 680/680 in app/ and 17/17 in inertia/.

Claude-Session: https://claude.ai/code/session_011z6vqqu1uujGKjjUE9Dgbg
(cherry picked from commit 691cccc00f)
2026-09-07 19:02:18 +01:00
ayushcodes10 4a7653b609 Kill workers inside the SIGALRM handler too, before it raises
The no-SIGALRM fallback killing workers before os._exit does not
cover the primary path: on POSIX, where SIGALRM is available, a
TimeoutError raised while the main thread is waiting inside the
ProcessPoolExecutor with-block propagates straight through that
block's own __exit__, which calls shutdown(wait=True) and blocks
until every worker exits. Forever, for a worker stuck in a C-level
call the alarm firing does nothing to stop, the exact shape #3341
was. The except TimeoutError handler that would call sys.exit is
never reached, so the configured timeout bounds nothing in that case.

Reproduced directly: a worker in an unconditional loop, a SIGALRM
after 2s, and the process was still running 15s later.

Kills any live multiprocessing.active_children() inside the signal
handler itself, before it raises, so by the time the exception
reaches shutdown(wait=True) there is nothing left to wait for. Moved
the handler from an inline lambda (which cannot contain a statement,
only expressions) to a named function, matching the no-SIGALRM
fallback's own style.

Fixes #3396.

(cherry picked from commit ed948388db)
2026-09-07 18:42:57 +01:00
ayushcodes10 2226fd91b7 Kill orphaned workers before the rebuild watchdog exits
os._exit(1) terminates only the rebuild process itself, not any
ProcessPoolExecutor worker it spawned for a large corpus. It skips
every cleanup path, including the pool's own context-manager
shutdown, so a worker still running at the moment the watchdog fires
is orphaned, reparented to PID 1 on POSIX, and keeps going
unsupervised for as long as whatever it was doing takes. A worker
stuck in catastrophic regex backtracking, the exact shape #3341
fixed, was observed surviving 2.5 days that way on macOS.

extract.py already documents this risk for its own 1 worker special
case, but that only covers max_workers == 1, not a real multi worker
pool.

The watchdog fires on a separate timer thread with no reference to
the pool object, but multiprocessing.active_children() enumerates
every live worker process regardless of which thread asks, so the
fallback kills them before exiting itself. Kill, not terminate: a
worker stuck in a C level call like regex backtracking never gets a
chance to act on a signal a Python level handler would need to catch.

Fixes #3396.

(cherry picked from commit 319869e6c1)
2026-09-07 18:42:57 +01:00
ayushcodes10 462f89a3ed Let builtin named member calls through to cross file resolution
_LANGUAGE_BUILTIN_GLOBALS is one union across every language, right
for a BARE call (String(x) really would become a god node) but wrong
for a MEMBER call. open is a Python builtin and Set a JavaScript one,
so session.open() in Swift or _server.Set() in C# named after another
language's builtin was silently discarded outright: no same file
edge, but also no raw_calls entry, so cross file resolution never got
a chance to try it.

A member call carries a receiver, so it is not the ambiguous case the
union guards against. Lets a builtin named member call reach the
existing tgt_nid = None deferral every receiver typed resolver
already relies on for other cases (a capitalized Python receiver, any
Java member call, and so on), so it can only ever reach an edge
through a guarded, receiver typed resolver, never the unguarded
bare name path a real god node would need.

Verified against real cross file setups for Python (class qualified
call) and Swift (field typed receiver, reproducing the reporter's own
AppleReadyStreamFinder.find -> RemuxPlayback.open example), plus that
a same file member call still cannot bind to an unrelated bare
function sharing the builtin name.

Scoped to the shared engine only. go.py and rust.py carry the same
outer filter, and the reported issue asks for the same change there
too, but tracing where their raw_calls entries actually go shows the
shared cross file resolver skips every is_member_call entry
unconditionally (extract.py, the "obj.log() has no import evidence"
guard) regardless of builtin status, and neither language has a
dedicated receiver typed resolver the way Python, Java, C#, Swift,
TypeScript, C++, and Objective-C do. Making the same change there
would be safe but currently inert, so left out rather than shipping a
change with no verifiable effect; worth revisiting once either
language gets its own receiver typed cross file resolver.

Fixes #3381.

(cherry picked from commit c0f9ff57e0)
2026-09-07 18:42:56 +01:00
Ku Klaud 29b7a21e98 fix(serve): traverse the MCP query_graph undirected so a seed with no outgoing edges reaches its callers
`_load_graph` forces `directed: True` so renderers can recover stored arc
order (#2309), and `_bfs`/`_dfs` expand through `G.neighbors()`, which on a
DiGraph yields successors only. Seeded on a leaf that is only ever called,
imported and contained, the MCP `query_graph` therefore answered with the
seed alone, while the CLI `query` — which loads the same graph.json
undirected on purpose (#2080) — returned the callers, the test and the
neighbouring modules. Same shape as #1174: one read surface missed a fix
the others got.

Add `_traversal_view`: an undirected copy for the traversal with `_src`/
`_tgt` stashed per edge, so `_subgraph_to_text` keeps rendering
caller->callee regardless of the side the traversal reached the edge from.
An undirected input is returned as-is, so the CLI path is unchanged;
`_load_graph` is untouched because `get_neighbors` and `shortest_path`
rely on direction explicitly.

Regression tests seed the same `calls` edge from both endpoints through
`_load_graph`, in both traversal modes, with and without a context filter;
the callee-seeded ones fail on the prior code with `1 nodes found`.

(cherry picked from commit 341aec5286)
2026-09-07 18:42:56 +01:00
safishamsi 6e420a0840 test(rust): cover trait method declarations and decl/impl id distinctness (#3366)
Follow-up to #3367: guards that signature-only trait methods are captured and
that a trait-declared method stays a distinct node from its impl definition.
2026-09-06 16:31:40 +01:00
himanshupatro-334 5aef7f4081 fix: resolve unmapped @/ imports in JS projects
(cherry picked from commit b2725960c1)
2026-09-06 16:17:38 +01:00
abhay-codes07 b09c839eb3 fix(hooks): a graphify skip must not terminate the whole git hook (#2986)
The generated post-commit / post-checkout block is appended to whatever
hook a repo already has, and other tools chain their logic after it. Every
skip condition in the block was a bare `exit 0`, which in a flat sh script
ends the ENTIRE hook process — so anything after graphify's end marker
was silently dropped on every root commit (HEAD~1 does not exist), every
rebase/merge/cherry-pick, every linked worktree, every GRAPHIFY_SKIP_HOOK=1
and whenever no graphify-capable python could be found.

Both bodies now run inside a subshell `( ... )`, so an `exit` ends only
graphify's section: no rewriting of the seventeen skip sites, no
`return`-outside-a-function hazard for the _PYTHON_DETECT fragment that
tests run standalone, the detached rebuild launch unaffected, and the
hook's own exit status 0 as before. The markers stay outside the subshell
so reinstall/uninstall/status keep finding the block.

(cherry picked from commit 6f09919f10)
2026-09-06 16:17:37 +01:00
himanshupatro-334 7f87c3b08a fix(paths): bound atomic temp filename length
(cherry picked from commit 9d17b94aaf)
2026-09-06 16:17:36 +01:00
Santosh Purbey 43de793f87 fix(rust): extract trait method declarations (#3366)
A trait got a node; the methods it declares did not. Across three real Rust
repositories 504 of 654 trait methods (77%) had no node at all, so a trait — the
API contract — appeared in the graph as a bare name with no operations attached,
and "what must an implementor provide?" was unanswerable.

Two gaps, both here:

  - `function_signature_item`, the node type for a bodyless `fn greet(&self);`,
    had no branch anywhere in the package, so a signature-only method had no
    extraction path.
  - the struct/enum/trait branch returns before descending, so nothing walked a
    trait's body — a default-bodied method inside a trait was never reached
    either.

The methods that did appear came from impl blocks that happened to define the
same name, and were anchored at the impl rather than the declaration.

This adds a `function_signature_item` branch mirroring `function_item` (minus the
body walk, since there is none) and descends into the trait body the way
`impl_item` already descends into its own, attributing each method to the trait
node with a `method` edge.

Before / after on the reproducer in #3366:

    src/lonely.rs (a trait with no implementor in the corpus)
      before:  Lonely
      after:   Lonely, .only_signature() L3, .with_default() L4
               + method edges from Lonely

On a 937-file Rust service: 112 trait methods declared, 70 missing -> 0 missing;
9426 -> 9574 nodes, 28638 -> 29177 edges.

Full suite is unchanged: 17 failed / 5296 passed / 93 skipped both with and
without this commit (the 17 are pre-existing on v8, all in test_skillgen.py).

Note for anyone verifying: the AST cache is keyed on file content, not extractor
version, so `graphify-out/cache` must be removed to observe the change — `--force`
alone re-runs extraction but still serves cached per-file results.

(cherry picked from commit e9fbe22503)
2026-09-06 16:17:36 +01:00
safishamsi c9f9901877 release: 0.9.55 — leading-comment docstrings, underscore-id collisions, ghost source_file, owned type-use sources, no-cluster labels, named re-exports, entity dedup, TS runtime imports, json_config edges, claude-cli preamble, watch semantic flag, mcp stdin v0.9.55 2026-09-05 22:15:39 +01:00
zfaustk 6baa5e08fe fix: preserve runtime imports in TS type normalization
(cherry picked from commit ff7cb6d27a)
2026-09-05 21:55:36 +01:00
himanshupatro-334 a361228d09 fix(mcp): prevent PR subprocess stdin hangs
(cherry picked from commit 0bea7e6041)
2026-09-05 21:55:35 +01:00
Satvik Shrivas 59c76c5aff fix(watch): preserve pending semantic update flag
(cherry picked from commit 08301ccd06)
2026-09-05 21:55:35 +01:00
prav 17d9a71c5c fix(claude-cli): tolerate a diagnostic preamble before the JSON envelope
`claude -p --output-format json` shares stdout with the CLI's own
subsystems, so the envelope is not always the first thing on it. An
attached MCP server that advertises no tools makes every invocation emit

    Client.listTools() called but server does not advertise tools
    capability - returning empty list

ahead of the envelope, and `json.loads(stdout)` fails on the whole buffer.

The failure is raised after the model has already answered, so the chunk
is discarded with its tokens spent. On a real corpus this burned 149,582
input and 52,162 output tokens on chunk 1 of 3 and returned nothing, and
the error names the JSON rather than the preamble that caused it, so the
log points at the wrong thing. Anyone with an MCP server configured hits
it on every chunk.

`_envelope_after_preamble` scans for the first `[`/`{` that begins a
valid JSON document. `raw_decode` ignores trailing bytes, so a diagnostic
on either side is tolerated. The fast path is unchanged: clean stdout
still parses on the first `json.loads`, and stdout with no JSON at all
still returns None so the caller raises -- test_raises_on_garbage_envelope
holds.

(cherry picked from commit ede3a9e0a8)
2026-09-05 21:55:34 +01:00
prav 5711e9d268 fix(json_config): stop emitting self-referential dep imports and array-as-extends edges
(cherry picked from commit 33f02c936b)
2026-09-05 21:55:34 +01:00
Vasu Bansal 3993351c60 fix: preserve export cycles and legacy declaration facts
(cherry picked from commit 0a1f06be8b)
2026-09-05 21:55:33 +01:00