1338 Commits
Author SHA1 Message Date
himanshupatro-334 8f029d28b7 fix(csharp): resolve nested types from using static
(cherry picked from commit b66040d902)
2026-10-09 19:48:13 +01:00
nothariharan 0778755e81 fix(cache): namespace AST cache by tree-sitter grammar versions (#4236)
(cherry picked from commit 90fadbd5e1)
2026-10-09 19:46:57 +01:00
YyunozorandClaude Opus 4.8 089939197f fix(extract): bind an unqualified call to the caller's own class in Java/C#/Scala/C++ (#4252)
An unqualified call fell through to a file-wide last-writer-wins label map, so
it bound to the last-declared same-named method. Walk the caller's class chain
(own class + in-file bases nearest-first, then enclosing classes) and bind a
single hit; fail-closed on a tie; free functions/constructors still resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 19:19:15 +01:00
Mahaboob pashaandClaude Opus 4.8 0b94e3767f fix(csharp): resolve calls through member chains, element access and constructors (#4258, #4246)
Resolve a.b.C(), xs[i].M(), and calls inside constructor bodies: a fail-closed
chain/element-type resolver (one declared type per hop, collection element only
for T[]/List<T>, bail on ambiguity) plus a ctor-body walk attributing calls to
the type node. Reuses the existing C# scoping; avoids same-pair method/calls
duplicates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 19:19:15 +01:00
ayushcodes10andClaude Opus 4.8 230bbd2d92 fix(csharp): never let a call bind to a same-named enum member (#4254)
Exclude case_of targets from the per-file label map and apply the cross-file
_member_nids exclusion to all C# calls (not only new X()), so a bare call like
Delegate(t) no longer binds to a same-named enum case. Methods are method/
contains targets, not defines, so real call targets survive; fail-closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 19:19:01 +01:00
ayushcodes10andClaude Opus 4.8 41b2a7f6f9 fix(csharp): give a generic and a non-generic type distinct ids (#4255, #4249)
Fold the type-parameter arity into the C# type node id (Effect`1), so Effect
and Effect<T> no longer collapse to one node and Effect<T> : Effect stops
producing an inherits self-loop. Arity 0 keeps the current id (back-compat);
backtick is collision-free and deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 19:18:16 +01:00
SoloDrex52andClaude Opus 4.8 f949947003 fix: make the test suite pass on Windows (#4260)
Refuse a read-only destination in _atomic_replace on Windows (nt-only guard;
os.replace there raises PermissionError and the fallback would silently clobber
it), plus test-only guards: utf-8 encoding kwargs, posix-shape path assertions,
FIFO/AF_UNIX/symlink skips, and a replace-retry for AV/indexer locks. No POSIX
behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 19:18:06 +01:00
Deepanshu Pal 68e0e9bbac fix(skill): preserve persisted scan options across skill runs
Read existing CLI build-config options in fresh and incremental skill
scan fragments, keeping corpus boundaries consistent without changing
standalone detection API defaults. Regenerate all host artifacts and
snapshots. Add rendered-snippet regressions and explicit-override tests.

Fixes #4240.
AI-Assisted-By: AI coding assistant

(cherry picked from commit 3f68952d80)
2026-10-09 19:17:53 +01:00
Mahaboob Pasha 2250f205b9 fix(build): keep a method edge when a member constructs its own type
The default graph keeps one undirected edge per node pair. A factory
that returns new Report() also has a calls edge back to Report, and
that call was the one that survived. Report then had no method edge
to Fault, so anything that walks a type's members missed it.

A method or contains edge now wins over a calls edge in the opposite
direction. A directed graph still keeps both.

Fixes #4248

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 95546d6784)
2026-10-09 19:17:53 +01:00
何明林 a7040f53db fix(extract): make unresolved re-export IDs portable
AI assistance: implementation drafted with TRAE, reviewed and verified by the author.

(cherry picked from commit e06c5da8b8)
2026-10-09 19:17:53 +01:00
Yyunozor 5743c9a8cf fix(export): strip control characters from the id drawn for an unlabelled node
A node without a label is drawn under its id (`G.nodes[n].get("label", n)`),
a third path from text to graph.svg. The previous commit cleaned the labels
and left that fallback as it was, so an unlabelled node whose id holds a C0
control character still produced a graph.svg that a strict parser rejects.

Run the fallback through the same strip: one expression,
`_strip_xml_illegal(str(G.nodes[n].get("label", n)))`, now covers the label
and the id. A clean id is drawn as before.

Tests: for each control character the module already lists, an unlabelled
node whose id carries it; the file must parse and still show the cleaned id.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 2e7e28ab32)
2026-10-09 02:51:03 +01:00
rohit-jsfreaky ad1dbc1e9b fix(rust): bind an in-file call to the type it names, not to any same-named function
extract_rust bound every in-file call through one bare-name index (last one wins),
so `Matcher::new()` reached `Printer::new()` when the file defined both, `Arc::new(x)`
reached the file's own `fn new`, `self.go()` another type's `go`, `drop(x)` the
`impl Drop` method and `x.total()` a free `fn total`, all EXTRACTED.

`Type::f()` / `Self::f()` / `self.f()` now bind only to that type's method in the file
(type = last path segment without generics, on the call and on the impl; `i32` and
other primitives count as types). With none, `Enum::Variant(x)` still gets its
constructor reference and `self.f()` falls through to the cross-file self pass. A bare
`f()` binds only to a free function or a type, `x.f()` only to a method.

Against rust-analyzer: wrong edges ripgrep 1232 -> 532, bat 138 -> 36, fd 29 -> 9;
correct edges 1852 -> 2236, 602 -> 655, 148 -> 154.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit de9b728ac6)
2026-10-09 02:48:23 +01:00
safishamsiandClaude Opus 4.8 223b376e9a feat(cli): nudge toward Graphify Cloud once after a local graph build
Print a precise, interactive-only CTA after `graphify extract` succeeds:
faster indexing, fewer tokens, cross-repo search, PR review, and an
always-current SDLC map at app.graphify.com. Shown once per project (marker
under graphify-out/), suppressed when stdout is not a TTY (CI, pipes, the
AI-assistant pipeline) and via GRAPHIFY_NO_TIPS/GRAPHIFY_NO_CTA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:48:22 +01:00
xiehuanyi b50e1b3590 fix(python): preserve nested class identities by enclosing class
Co-authored-by: Codex <noreply@openai.com>
(cherry picked from commit e85a748445)
2026-10-08 14:29:19 +01:00
rohit-jsfreaky 86e3c1dc7a feat(python): resolve self/cls/super() calls to a base-class method in another file
The in-file pass walks only bases declared in the same file, so `Flask.__init__` ->
`App.__init__` (flask/app.py -> flask/sansio/app.py) and every other inherited call
across modules had no calls edge.

_resolve_python_member_calls now walks the caller's class up a single-inheritance
chain and binds the first class that owns the method, as the MRO does. It stops with
no edge at multiple inheritance, a base the inherits edges cannot show (marked
`python_opaque_bases` by the extractor: `mod.Base`, `Generic[T]`), a base outside
the corpus or a self-loop, a caller base its file neither defines nor imports, and
`super(Cls, obj)` with arguments. On a changed-files rebuild an unchanged base is
still a name stub, so the walk rewires copies of the inherits edges the way the full
build does; the opaque-base marker is forwarded with the other context metadata.
AST cache schema 6 -> 7.

flask: 66.7% -> 71.6% of jedi's call pairs found; 16 new edges on httpx/click/flask,
16 confirmed by jedi, 0 lost. Changed-files rebuilds match full builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit b241c3bc79)
2026-10-08 14:29:00 +01:00
rohit-jsfreaky ae2f11a0d7 fix(python): find a nested function's file in the typed-receiver visibility check
The typed-receiver arm of _resolve_python_member_calls checks that the receiver's
class is defined in or imported into the caller's file. _file_node returned the
caller's first contains/method parent, which is the file for a top-level function or
method but the enclosing function for a nested def and the outer class for a nested
class's method, so those calls (click's `def callback(ctx: Context, ...): ctx.abort()`)
never got an edge. Climb the parents to the top, which is always the file node.

0.9.80 vs this change: click +10, rich +4 call edges, all 14 confirmed by jedi at the
call site, 0 lost; httpx, flask and typer unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 56634ac22d)
2026-10-08 14:28:54 +01:00
Raphael Malikian c36833d13f fix(elixir): emit calls edges for keyword-form def bodies
One-line definitions (`def a(x), do: b(x)`) have no do_block node: the
body is the value of the `do:` pair in the arguments keywords node. The
extractor only registered do_block bodies for the call-walk pass, so every
call inside a keyword-form body was silently dropped from the graph. When
there is no do_block, take the `do:` pair value as the body and walk it
the same way (Fixes #4207).

AI assistance: implementation drafted with Muse (Anthropic), reviewed and
verified by the author.

Signed-off-by: Raphael Malikian <rtmalikian@gmail.com>
(cherry picked from commit b00d3704db)
2026-10-08 14:28:54 +01:00
Mahaboob pasha e63386fc0c fix(elixir): resolve qualified and aliased remote calls (#4206)
Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit b834ec821f)
2026-10-08 14:28:27 +01:00
rohit-jsfreakyandClaude Opus 4.8 ab3c3553cc fix(python): never resolve an absolute import to the module that contains it (#4225)
A directory-qualified stem fallback could resolve an absolute import
(from werkzeug.wrappers import Request) onto a local wrappers.py, rewiring a
class to inherit from itself. Exclude the importing file's own stems; a
same-named module in another package still resolves by exact/suffix/bare stem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 14:28:20 +01:00
himanshupatro-334 8d3a6f5305 fix(csharp): resolve enclosing namespace types
(cherry picked from commit 6b6579077d)
2026-10-08 14:28:03 +01:00
rohit-jsfreaky 27df0f9a2c fix(ts): pick a same-named receiver type by the caller's import, never bind to a #private method
_resolve_typescript_member_calls required a receiver's type name to be unique in the
corpus before applying the #2553 origin gate, so hono's `c: Context` (a class plus two
interfaces and a type named Context) never resolved, though the caller's
`import type { Context } from '../../context'` names the exact one. Apply the origin
gate to the candidates first and emit only when exactly one is visible; one definition
behaves as before, an undecided import still emits nothing.

_key() also folds `#name` into `name`, so `c.newResponse()` could bind to a private
`#newResponse()`. Skip a match whose leading `#` disagrees with the call.

Against the TypeScript compiler, 0.9.80 vs this change: hono obj.method() 20.3% -> 47.3%
(+23 edges), NestJS +5, TypeORM +3, all 31 confirmed at the call site, 0 lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 7935593eca)
2026-10-08 14:27:33 +01:00
rohit-jsfreaky 18682e8dce feat(ts): type this.field receivers from class field declarations
The TS receiver table behind `this.repo.findById()` held constructor parameter
properties, `new` locals and typed parameters, but no class fields, which is how most
NestJS, Angular and TypeORM classes hold their collaborators (`protected repo: Repo`,
`private logger = new Logger()`, `#root: Node`). Those calls got no edge.

_ts_receiver_type_table now records non-static class fields: `name: T`, `name?: T`,
`T | undefined` / `T | null`, an untyped `new T()` initializer, and `#private` names
keyed as the extractor records the receiver. Arrays, generics and two-class unions stay
untyped; the #2553 origin gate and first-binding-wins are unchanged. JS/TS files bypass
the AST cache, so no schema change.

Against the TypeScript compiler, 0.9.80 vs this change: NestJS this.x.method() 47.9% ->
77.0% (+102 edges), TypeORM 64.5% -> 79.6% (+91), all 193 confirmed at the call site,
0 lost; changed-files rebuild == full build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit e5fb5e3b95)
2026-10-08 14:27:06 +01:00
JunoLee 97f0dc197e fix(cluster,analyze,report): isolate AMBIGUOUS edges from top-N and modularity
AMBIGUOUS-tagged edges (confidence ≈ 0.35, often `conceptually_related_to`
from LLM semantic extraction) could glue otherwise-independent clusters
together through Leiden/Louvain modularity, and the resulting bridge
questions crowded `Suggested Questions` with low-signal entries. The
`AMBIGUOUS` flag was already correct — only downstream consumers
treated it identically to EXTRACTED/INFERRED.

Changes:
- `cluster()` gains `ambiguous_scale: float = 0.5` (overridable via
  `GRAPHIFY_AMBIGUOUS_SCALE`). Multiplier is applied to AMBIGUOUS edge
  `weight` in a graph copy before partitioning so the raw graph (and
  `/graphify path`/`explain` output) stays untouched. 1.0 restores
  legacy behavior.
- `suggest_questions()` tags each ambiguous_edge entry with
  `low_confidence=True`. Interleave + top_n contract from #3849 is
  preserved; the tag is purely metadata.
- `report.py` splits tagged entries into a new "Low-confidence Hints"
  section below the main Suggested Questions.

Tests:
- `test_cluster_ambiguous_scale.py` (new, 7 tests): default scale
  demotes AMBIGUOUS weight, scale=0.0 drops it entirely, scale=1.0
  is no-op, env override + malformed env fail-soft, raw graph not
  mutated, no copy when no AMBIGUOUS edges present.
- `test_suggest_questions_ambiguous.py` (new, 4 tests): tag present
  on every ambiguous_edge entry, never on other types.
- `test_analyze.py::test_suggest_questions_diversity_preserves_limit_and_candidates`
  relaxed to allow the optional `low_confidence` key.

Fixes #4199

---
한국어 요약: AMBIGUOUS edge (LLM 추론, 신뢰도 ~0.35) 가 Leiden 모듈러리티로 독립
클러스터를 묶거나 Suggested Questions top-N 을 잠식하던 문제 수정. cluster() 에
ambiguous_scale 파라미터 (default 0.5, env override) 추가, 원본 그래프는 복사 후
수정하여 /graphify path/explain 영향 없음. suggest_questions 는 low_confidence
플래그만 추가하고 interleave contract (#3849) 유지. report.py 가 분리 렌더링.
regression test 11 개 신규 + 기존 1 개 relax.

(cherry picked from commit ff52a29266)
2026-10-08 14:27:06 +01:00
nothariharan 4cf3dfba06 fix(analyze): exclude contains-only AST declarations from knowledge gap detection (#4205)
(cherry picked from commit bf55b069f8)
2026-10-08 14:27:06 +01:00
Mahaboob pasha b583288e95 fix(php): parse PHP 8.5 (void) casts and casted match expressions (#4202)
Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 43dbd7457c)
2026-10-08 14:27:06 +01:00
rohit-jsfreaky deda9adb64 test: pin graph.json invariance across checkout folder, rebuild and line endings
Seven bugs of one shape (#4152, #4154, #4156, #4164, #4180, #4182, #4188) were each
found by diffing two builds of the same tree by hand: a checkout path leaking into an
id, a rebuild writing a different graph than the first build, CRLF ids differing from
LF, an edge to a node that is never declared. Each fix got its own narrow test, so the
next extractor or write-path change can bring the same class back unseen.

tests/test_graph_invariance.py builds the whole tests/fixtures corpus (every extractor)
through the real CLI, --no-cluster and clustered, and checks: two folders give the same
graph, the checkout path appears nowhere in graph.json, a rebuild of the unchanged tree
changes nothing, LF and CRLF give the same graph, and every raw edge endpoint is declared.
On 0.9.79 the three checks those fixes cover fail; on 0.9.80 all 12 pass in about 20 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 4293a638d4)
2026-10-08 14:27:06 +01:00
rohit-jsfreakyandClaude Opus 4.8 d1687d9d60 feat(python): resolve obj.method() through an annotated/constructor-bound local (#4198)
Resolves a call on a local or parameter whose class is known from an annotation
(c: Client), an Optional/union/string annotation, or a constructor/with binding,
to the owning class's method - fail-closed to a single in-file-or-imported class
that owns the method. Complements the self.<attr> arm from #4176.

Rebased onto the landed #4176: reuses its _emit_call(inferred=True) INFERRED/0.85
path instead of the PR's own confidence= parameter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 17:53:49 +01:00
rohit-jsfreaky fedf15d4f9 perf(extract): resolve and parse each path once in the id remap and call tie-break
extract()'s id remap called Path.resolve() on the same file paths once
per input, once per target_file-stamped edge and once per node: ~24k
calls for ~1k distinct paths on graphify's own repo. Resolve each
distinct path once per extract() call through a local memo; a hit
returns the same Path the call would, and a path that raises is never
stored.

_path_proximity_winner (the #1553 god-node tie-break) re-parsed every
candidate's parent directory with PurePosixPath on every ambiguous call
site (~227k Path objects per update on graphify's repo), although the
same candidate files come back for every site of a common name. Memoize
the parent-directory parts per path string; equal tuples are exactly
equal PurePosixPath parents, so every tier decides as before.

graphify's own repo (18,830 nodes), `graphify update --force`,
alternated: cold 28.1 -> 26.3 s, no-change update 24.8 -> 22.5 s and
20.9 -> 18.0 s. graph.json identical (all attributes) cold and warm;
old vs new tie-break identical on 200,000 random cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d5966eda8a)
2026-10-07 17:52:47 +01:00
rohit-jsfreaky 288a938ca6 fix(build): give --no-cluster graph.json the same endpoint rules as build_from_json
The raw --no-cluster writers skip build_from_json. `update --no-cluster`
only minted the #2873 import stubs, and `extract --no-cluster` did not
even do that, so graph.json kept edges to undeclared nodes. The common
case is a markdown link to a missing document: extract_markdown keeps
it dangling on purpose and builds the target id from the absolute
resolved path, so the checkout folder and OS username reached
graph.json.

finalize_raw_graph_endpoints() now runs on both raw writers: mint the
import stubs, drop links that still lack two declared endpoints (after
the same normalization remap build_from_json does), and drop stubs no
link references any more, so a stub minted on the extract path cannot
outlive its import.

On graphify's own repo: 186 dangling edges -> 0 (149 carried the
checkout path), node set unchanged, and every pair the clustered build
links is still linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 97027ccf04)
2026-10-07 17:26:17 +01:00
rohit-jsfreaky e9597313ef fix(watch): keep extract()'s run-only keys out of --no-cluster graph.json
extract() returns extracted_sources (#3411) and failed_sources (#2543)
next to nodes/edges. They steer the current run and hold the absolute
paths of its inputs. The raw --no-cluster writers dumped the extraction
as graph.json, so the first `graphify update --no-cluster` and every
`graphify extract --no-cluster` wrote the checkout path and OS username
into the graph. A rebuild dropped them again, and also added a
`hyperedges` key the first build lacked, so the first build and a
rebuild of the same tree wrote different files.

Name the run-only keys once (extract.RUN_ONLY_EXTRACTION_KEYS) and leave
them out of both raw writers; the update writer always writes
`hyperedges`, as the extract writer and the clustered path already do.
Nothing reads these keys back from graph.json: build_merge and
merge_raw_extraction take them from the in-memory extraction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 76cfcef556)
2026-10-07 17:26:17 +01:00
rohit-jsfreaky 1965a00841 fix(watch): drop an external import stub once no edge references it
External import stubs (#2873) are minted without an _origin stamp. On
the next update the #2334 tier backfill stamped them by shape, and a
stub has no L<line> location, so it became `semantic`. Reconcile keeps
semantic nodes, so once the last file importing a module stopped
importing it, the stub stayed in graph.json forever with no edge. It
also made a rebuild of an unchanged tree differ from the first build.

A stub is derived from its import edges and minted again on every
write, so it has no tier. The backfill (now one helper shared by
_load_existing_graph and the reconcile path) skips stubs and removes a
stamp an earlier run gave one; every tier decision reads an unstamped
stub exactly as before. Reconcile drops a preserved stub that no fresh
or preserved edge references.

On graphify's own repo a warm rebuild now equals the cold build (125
stub nodes differed), a long-lived graph heals to the cold node set
(8 ghost stubs removed), and the changed-files path equals the full
rebuild for edits and additions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d3fbf6458f)
2026-10-07 17:26:17 +01:00
SrijanSrivandClaude Opus 4.8 00342989e4 fix(detect): drop a previous checkout's absolute manifest keys (#4175)
A re-clone that copies graphify-out/ while the old checkout is still on disk
left absolute manifest keys pointing at the old path, which survived the
exists()/in-root prune and persisted alongside the new relative keys. On a full
scan, drop stale absolute keys that duplicate a freshly-written relative key
under a real shared directory prefix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 17:26:11 +01:00
Yyunozor 1292639344 fix(python): keep TYPE_CHECKING-only imports out of import cycles (#3159)
Imports under `if TYPE_CHECKING:` never execute, but the Python extractor
emitted them as ordinary `imports_from` edges, so the Import Cycles section
listed cycles that cannot happen at runtime.

Stamp them `type_only`, the marker find_import_cycles already skips for
TypeScript `import type` (#3123); the edge stays in the graph. Covers
`TYPE_CHECKING` and `<module>.TYPE_CHECKING`, `import x`, `from x import y`,
relative imports and nesting. The `else:` branch and the body of
`if not TYPE_CHECKING:` stay runtime imports.

build_from_json collapses the import edges of one file pair into one, and
dedupe_edges those of one (source, target, relation), keeping the marker of
either side. Stamping alone would let a guarded import mask a runtime one
(for example `from . import b` plus a guarded `from .b import B`) and lose a
real cycle. The collapsed edge is therefore `type_only` only if every import
folded into it is: an import that runs always wins. The same collapse already
hid a real cycle for TypeScript `import type` next to a value import of one
file. In the default undirected graph, the reverse-direction duplicate that
#1061 drops is the other file's import, so it leaves the surviving edge's
marker alone.

Not covered: `from pkg import submodule` under the guard. That edge comes from
the resolution pass and is not stamped, so its cycle is still reported.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 685b0fffb5)
2026-10-07 17:26:11 +01:00
Mahaboob pasha a061e0b727 fix(python): resolve self.<attr>.<method>() calls through the attribute's constructor type (#2860)
Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 222e01f199)
2026-10-07 17:26:11 +01:00
rohit-jsfreaky 304d71592d fix(cache): treat an AST hit whose import target is gone as a miss
An AST cache entry is keyed by its own file's content, so after
pkg/slice.py is renamed or deleted, the unchanged importer pkg/user.py
is a hit and replays its import edges resolved while slice.py existed:
a target id minted from the absolute path and a target_file stamp to
the old file. extract() maps a stamped target to a portable id only
when it exists, so the edge kept the absolute-path id and the #2873
stub was minted with it: graph.json held the checkout folder and OS
username, and a warm update differed from a cold build (pkg_slice).

save_cached now records the target_file stamps that name an existing
file when an AST entry is written (made portable like the entry's
ids). load_cached returns a miss when any recorded file is gone, so
the importer is extracted again exactly as on a cold build. Stamps
that did not exist at write time are not recorded and never cause a
miss; entries without the list load as before.

A no-change warm rebuild of graphify's own repo has the same 16 cache
misses as before, and its cold build is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 48578d04f1)
2026-10-07 17:26:00 +01:00
Huanyi Xie 7aab2ed7ac fix(extract): preserve cross-drive syntax warnings
Keep warning path formatting from aborting a recovered extraction. AI assistance: Codex drafted the change and the actual extraction regression.

Co-Authored-By: Codex <noreply@openai.com>
(cherry picked from commit 1e91fc2f1b)
2026-10-07 17:26:00 +01:00
rohit-jsfreaky 357e8a1e75 fix(extract): let the Windows console script use the extraction pool
_spawn_cannot_reimport_main (#3669) skips the process pool when spawn
workers cannot re-import __main__, judged by os.path.isfile on
__main__.__file__. The pip/uv console script on Windows is a launcher
exe with a zipped __main__.py, so __file__ is
...\Scripts\graphify.exe\__main__.py, not a file on disk, and every
`graphify update` / `graphify extract` from the normal Windows command
extracted on one core.

The pool works there: __main__ has a module spec, so
multiprocessing.spawn passes the module NAME to the worker, and a spec
named __main__ is skipped in the worker altogether ("zip archives ...
we don't even try"). Return False whenever __main__ has a spec,
mirroring get_preparation_data. stdin, -c and the REPL have no spec, so
#3669 is unchanged for them. The two stdin/REPL tests now also set
__spec__ = None, as a real stdin caller has.

Cold build of graphify's own repo with graphify.exe, alternated:
39.6 s -> 26.5 s median (12 workers), identical graph.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f64594d7e5)
2026-10-07 17:26:00 +01:00
Cintu07 85cfb273a4 fix(resolution): keep re_exports from both files whose ids collide
Two files whose paths normalize to the same id (a-b/x.ts and a/b/x.ts
both give a_b_x) share a source id until _disambiguate_colliding_node_ids
salts them by source_file. _apply_symbol_resolution_facts deduped its
edges on (source, target, relation, context), so when both files
re-exported the same module the second edge was dropped as a copy of the
first, and which file kept it depended on processing order. In zod's
src/, v4/mini/index.ts and v4-mini/index.ts traded the edge with the
file order.

Key the dedup on the emitting file as well, so only true duplicates from
one file collapse.

(cherry picked from commit 075ee2fea5)
2026-10-07 17:26:00 +01:00
Yyunozor fee41a65b5 fix(markdown): resolve links to file names that contain a space
CommonMark writes a link destination with a space either in angle
brackets ([x](<My Note.md>)) or percent-encoded ([x](My%20Note.md)).
The inline and reference-definition regexes stopped the capture at the
first space inside <...>, and the destination was never decoded, so
both forms kept an id derived from a nonexistent path and the
references edge dropped at build.

Capture the whole bracketed destination in both regexes; an unclosed <
falls back to the bare form, so it does not swallow the links after it.
Percent-decode inline and reference destinations, keep a file whose name
literally contains the escape (with or without the .md completion), and
re-run the external-link guard on the decoded form. Wikilinks stay
verbatim.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 49ba8aafc0)
2026-10-07 17:26:00 +01:00
rajatnagda45 9ec89f0735 fix(java): capture methods and calls inside anonymous class bodies
A Java/Groovy anonymous class (`new Runnable() { … }`) keeps its body as a
`class_body` hanging off an `object_creation_expression`. That node is not
a class_type and the function branch never recurses into bodies, so the
anonymous class's methods — and every call made inside them — got no nodes
at all. Anonymous listeners/callbacks are routine in Java, so this silently
dropped a large slice of the call graph (e.g. a listener's `onClick`
calling a sibling method vanished).

Mirror the Kotlin anonymous-object handling (#2347): scan a function body
for anonymous classes (without crossing a nested method/lambda boundary or
descending into a found one), emit an owner node per anonymous class, and
walk its class_body like the class branch so members and their calls flow
through the normal machinery. The base type is recorded only as the node's
label, not an implements/inherits edge — an anonymous class usually
realises a library interface/class whose kind is unknowable from this file,
so a guessed subtype relation is withheld (fail-closed).

(cherry picked from commit 0a6990c0ee)
2026-10-07 17:26:00 +01:00
Yatharth Verma 96c39d6a88 test: skip optional-extra tests when the extra is not installed (#4190)
Grammar and SDK tests for erlang, r, solidity, vbnet, ollama and the sql encoding case failed instead of skipping on a default uv sync. Guard them with the existing _needs_* skipif pattern and point CONTRIBUTING at uv sync --all-extras to match CI.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit d7f443eeb1)
2026-10-07 17:26:00 +01:00
Mahaboob pasha 748de12553 fix(serve): show the graph's build commit in graph_stats
graph.json already records built_at_commit, but node_link_graph drops
top-level keys, so the MCP server never saw it and graph_stats only
reported counts. Stash the commit in _load_graph and append it to the
graph_stats output. At query time, compare it with HEAD of the repo
holding the graph and note when they differ; with no git or no repo the
comparison is skipped. Graphs without a commit render as before.

Closes #3354

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit fe993d3fd5)
2026-10-07 17:26:00 +01:00
rohit-jsfreaky 39f004ea32 perf(serve): build the MCP traversal view and shortest-path graphs once per graph
query_graph copied every edge of the loaded graph into an undirected
traversal view on every call, and shortest_path rebuilt a sorted
DiGraph (or Graph) of every edge on every call. The structures are the
same for a given graph object, so build each once and cache it on
G.graph, exactly like the trigram index: a hot reload swaps in a new G,
so a cached structure never outlives its graph.

Sharing is safe because nothing writes to them: _filter_graph_by_context
builds a new graph when a filter applies, the traversals and the
renderer only read the view, and only nx.shortest_path reads the path
graphs. The path graph is still built from sorted node and edge lists,
so #2074's deterministic route is unchanged. The traversal-view
docstring that chose a fresh copy per query is updated.

graphify's own graph (18,865 nodes / 38,458 edges), the same calls run
interleaved with the cache cleared vs kept, 240 outputs identical:
query_graph 524 -> 316 ms median (filtered "what calls X": 508 -> 360),
shortest_path 211 -> 0.8 ms directed, 109 -> 0.7 ms undirected.
Memory: +8 MB for the view, +24 MB for both path graphs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fbb21791da)
2026-10-07 17:26:00 +01:00
Mahaboob pasha 9d3ccfc99c fix(analyze): keep edge direction in graph_diff
graph_diff keyed undirected edges by their sorted endpoints, so a call
that reversed (a->b became b->a) dropped out of the --update summary.
When both snapshots carry _src/_tgt and they differ, the edge is now
reported as one removed and one new edge, with source/target taken from
the markers. Graphs without the markers diff as before.

The skill's diff step loaded the old graph.json with a plain
node_link_graph call, which drops the stored direction, so it now uses
paths.load_node_link_graph (#4066/#4088). Skills regenerated with
skillgen, and the aider/devin monolith round-trip allows the changed
lines.

Closes #4067

Co-Authored-By: Grok Bot <noreply@x.ai>
(cherry picked from commit 917fab9fbb)
2026-10-06 20:55:02 +01:00
nikhil2004andClaude Opus 4.8 3ba28369fb fix(extract): preserve C++ classes behind export macros (#3661, #3648)
C++ projects using module export macros (class MYLIB_EXPORT Foo {}) lost the
class and its inherits edge because tree-sitter-cpp fails to parse the macro and
error-recovers over the declaration. Blank the ALL-CAPS macro token before
parsing, preserving byte length and newlines so offsets and ids stay stable;
guarded so legitimate all-caps class names, 'final', range-for, and brace-init
are never touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 20:54:55 +01:00
rohit-jsfreakyandClaude Opus 4.8 be528e110e fix(extract): decode UTF-16 and non-UTF-8 source files before parsing (#4146, #4145)
tree-sitter reads UTF-8, but extractors handed it raw file bytes, so a UTF-16
file yielded zero nodes and a cp1252 name like CaféOrder was truncated. Decode
via a shared reader that returns UTF-8 bytes unchanged (ids/offsets/cache keys
for working files are byte-identical) and re-encodes UTF-16/UTF-32/non-UTF-8
using a deterministic cp1252->latin-1 fallback. The semantic whole-file and
slice readers now share the same decoder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 20:54:45 +01:00
rohit-jsfreakyandClaude Opus 4.8 51c05e7beb fix(watch): keep placeholder nodes for never-scanned files on incremental rebuilds (#4161, #4160)
On an incremental reconcile the corpus sweep read a never-scanned referenced
path (unresolved dynamic import target, .sln/.csproj reference) as a deleted
source and evicted the placeholder node a referrer minted, dropping the
unchanged referrer's preserved cross-file edges. Skip eviction for such
referenced-but-unscanned paths (purely additive; never causes a new eviction),
with deletion-evidence guards so genuinely deleted files are still reaped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 20:54:35 +01:00
oleksii-tumanovandClaude Opus 4.8 84d0ec76d9 fix(powershell): resolve member calls by receiver owner (#4163)
Replaces the name-only member-call resolver with receiver-proven binding:
$this.M() binds to the enclosing class only when its name is unique, and
[Type]::M() only to a single unambiguous local class; value/cast/dynamic/
duplicate/external receivers fall through to raw_calls. Fixes the same-name-
method caveat from the 0.9.77 member-call work (#3992).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 20:54:25 +01:00
rohit-jsfreaky 1d38e84a91 perf(watch): compute each stored source path's identity once per update
_reconcile_existing_graph asks _StoredSourcePaths.identity() for every
node, edge and hyperedge, several times per item. On graphify's own repo
that is ~150k calls for under 1k distinct files, each rebuilding Path
objects and an abspath. The answer depends only on the stored path and
roots fixed in __init__, so cache it (and in_watch_root) per instance.

Reconcile step on this repo: 10.3 s -> 2.7 s. graph.json is byte-identical
with and without the change for a full build and for an incremental
update with an edited, a deleted and an added file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 1360728f0f)
2026-10-06 20:54:15 +01:00
rohit-jsfreaky 0083ee78e5 fix(python): keep missing relative-import targets out of the checkout path
A relative import of a module with no file behind it (`from .absent
import x`) built its target from the attempted absolute path, so the id
became e.g. `c_users_<user>_..._pkg_absent_py`. The root-relative id
remap never rewrites it (there is no node to anchor on), so the checkout
location and the OS username ended up in graph.json and every clone got
a different id.

Use the dotted module name relative to the scan root instead
(`pkg_absent`), the id an unresolved absolute import of the same module
already gets, so `from .absent` and `from pkg.absent` now meet on one
node. An import that climbs above the scan root falls back to a `ref`
id. Imports that resolve, and attempted paths that exist, are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 8cdbb0ea4f)
2026-10-06 20:54:15 +01:00