Commit Graph
2188 Commits
Author SHA1 Message Date
safishamsiandClaude Opus 4.8 5b74d7d749 release: 0.9.82
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.82
2026-10-09 02:51:25 +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
safishamsiandClaude Opus 4.8 622474b8ec release: 0.9.81
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.81
2026-10-08 14:30:33 +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
Aditya Tiwari d719854a51 docs: add guidance for contributors using AI coding agents
Maintainers are seeing more AI-assisted pull requests. The rules for
them already exist across this guide (branch off v8, one concern per
PR, regression tests that fail before the fix, never hand-edit
generated skills, Co-Authored-By disclosure), but an agent or a new
contributor has to piece them together.

This adds a short "Working with AI Coding Agents" section under Pull
Requests that collects those existing rules in one place. It adds no
new policy. It also mentions ContribOS as an optional tool that reads
these rules and checks a branch against them; the maintainer agreed
to a PR for it on X.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d47b311588)
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
wanjinhao1 a28ff39ef0 Fix #3712: document that graphify path defaults to a directed search
(cherry picked from commit ce99295541)
2026-10-08 14:27:06 +01:00
safishamsiandClaude Opus 4.8 6478eb7123 release: 0.9.80 — include #4195, #4198
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.80
2026-10-07 17:55:27 +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
safishamsiandClaude Opus 4.8 406feee4ae release: 0.9.80
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 17:28:54 +01:00
JFWaskinandClaude Opus 4.8 69c528eb37 fix(deps): raise tree-sitter runtime floor to >=0.25 for ABI 15 grammars (#4148)
0.23/0.24 cap at ABI 14 while pinned grammars ship ABI 15, so a resolver could
pair a 0.23 runtime with ABI-15 grammars and fail every parse. Floor at 0.25,
the oldest runtime that loads them (uv.lock already resolves there). Lock
regenerated on 0.9.80.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 17:27:40 +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
safishamsiandClaude Opus 4.8 f765dcb341 release: 0.9.79
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.79
2026-10-06 22:37:19 +01:00
safishamsiandClaude Opus 4.8 c0024cfeab fix(watch): exclude graph.json schema metadata from the topology compare
0.9.78's #4167 added schema_version/graphify_version under graph.json's
"graph" key, but the candidate topology built from node_link_data(G) has no
such metadata, so _canonical_topology_for_compare judged every incremental
update a topology change and needlessly re-clustered and re-rendered. Strip the
two metadata keys before comparing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 22:37:10 +01:00
safishamsiandClaude Opus 4.8 397d025f7b release: 0.9.78
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.78
2026-10-06 20:55:39 +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