export.py: to_json now accepts community_labels and writes community_name onto
each node. Previously cluster-only wrote labels only to GRAPH_REPORT.md,
graph.html, and .graphify_labels.json — graph.json stored only the numeric cid,
so query/MCP showed blank or numeric community values (#1305).
__main__.py: pass community_labels=labels to to_json in cluster-only path.
explain command now prefers community_name over raw numeric community field.
serve.py: query and get_node read paths prefer community_name over community,
with fallback so old graphs without the field still work. Adds --graph flag as
an alias for the positional argument in graphify-mcp/_main(), fixing
"unrecognized arguments: --graph" for users following the documented pattern
shared by every other graphify subcommand (#1304).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
extract.py: clamp ProcessPoolExecutor max_workers to 61 on Windows (issue #1298).
Python's ProcessPoolExecutor hard-caps at 61 on Windows via WaitForMultipleObjects;
>61-core machines crashed on AST extraction. Clamp applied after all input paths
(auto-compute, GRAPHIFY_MAX_WORKERS, --max-workers) to cover all three.
build.py: skip ghost-merge when two AST nodes share (basename, label) key (issue #1257).
When same-named symbols appear in same-named files across directories (e.g. two
render() in two index.ts), last-writer-wins produced an arbitrary canonical node
and mis-pointed all edges. Now tracked in _loc_collisions; ambiguous keys are
skipped in Pass 2, leaving the ghost intact rather than merging into the wrong node.
__main__.py: ignore OSError on unreadable .graphify_version probes (issue #1299).
On restricted-permission installs or network mounts, .exists()/.read_text() raised
PermissionError and crashed every graphify query/explain/path call at startup.
All three FS probes now wrapped in try/except OSError: return.
prs.py: resolve claude.cmd on Windows in prs.py claude-cli backend (issue #1288).
The _call_llm and _call_claude_cli paths were already fixed; prs.py had the same
bare ["claude", ...] call that fails on Windows npm installs with WinError 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stat+MD5 across all files now fans out over a thread pool instead of running sequentially — I/O-bound work releases the GIL so this scales with available disk concurrency.
Eliminates O(depth) generator-frame chain overhead. Each leaf's value previously propagated through 26+ yield-from frames; now a single stack loop handles the full traversal.
Replaced O(n) linear scan `next((n for n in candidates if n["id"] == neighbor_id))` with O(1) dict lookup via pre-built `candidates_by_id`. Also pre-caches `_norm()` results in `norm_cache` to avoid recomputing per inner iteration.
For a 36k-file codebase (~100k high-entropy candidates) this reduces the pass-2 loop from O(n^2*B) (~30–100B iterations) to O(n*B), eliminating the multi-minute CPU hang after AST extraction completes.
Adds complete Persian translation (README.fa-IR.md) with RTL formatting and updates main README language selector to include Persian between Arabic and Italian.
JS/TS symbol resolution only handled named imports. A default-exported
symbol (`export default class Foo`) imported as `import Foo from './foo'`
produced only a file→file `imports_from` edge; the class node received no
incoming symbol edge. On codebases that default-export most classes
(NestJS services/helpers/models, etc.) those symbols looked like isolated
leaf nodes, and `graphify affected "<Class>"` / `explain` reported no
callers.
Record default imports with imported_name="default", register a "default"
export for `export default <class|function|identifier>`, and let the
existing exported-origin resolver wire the `imports` edge — which also
resolves calls made through the local binding, even when it is renamed
(`import Bar from './foo'; new Bar()`). `export { X as default }` was
already handled via the export-clause path; anonymous defaults
(`export default class {}`) have no resolvable symbol and stay file-level.
Adds regression tests for default-export class/function/identifier import
resolution and renamed-binding call resolution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single `!` rule in .graphifyignore set a blanket `has_negation` flag that
disabled directory-level pruning for EVERY ignored directory during the
os.walk in detect(). One unrelated `!docs/**` therefore made the walk descend
bin/, obj/, wwwroot/, generated/, … on large repos — a pathological slowdown.
Output stayed correct (the per-file `_is_ignored` filter still excluded those
files), but the walk visited the entire tree.
The bypass was unnecessary: `_is_ignored` already honours negations correctly —
last-match-wins lets `!dir/` un-ignore a directory (so it is not pruned), and
the gitignore parent-exclusion rule means a `!` cannot rescue a file beneath an
excluded directory, so descending an ignored dir to find a re-included file is
never needed. Prune purely on `_is_noise_dir` + `_is_ignored`.
Adds a regression test that tracks os.walk and asserts the ignored dir is never
descended while the negation still re-includes its target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes#1264
Also fixes a narrow watch-mode path-filtering bug found while running the required local
full-suite gate for this PR. Hidden directories inside the watched corpus are still ignored;
hidden ancestors outside the watched root no longer suppress valid file events.
_read_tsconfig_aliases joined alias targets onto the tsconfig's own
directory and ignored compilerOptions.baseUrl. In the common monorepo /
NestJS layout (baseUrl "./src" with "@services/*": ["services/*"]), the
alias resolved to <dir>/services instead of <dir>/src/services, so every
aliased import failed to resolve and the import edge was silently dropped
— leaving cross-file caller graphs nearly empty on alias-heavy TS repos.
Resolve `paths` relative to `baseUrl` (TypeScript's actual semantics),
defaulting to "." so configs without baseUrl keep their current behavior.
Add a regression test covering a subdirectory baseUrl; the existing alias
test only exercised baseUrl ".", which is why this slipped through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two Windows fixes for the claude-cli backend:
1. _call_llm (the label/community-naming path) spawned a bare ["claude", ...],
which CreateProcess cannot resolve to the npm claude.cmd shim (PATHEXT does
not apply) — every labeling batch failed with WinError 2. Mirror the
extraction path's resolution: prefer shutil.which("claude.cmd") and pass
the resolved path. Regression test included.
2. Both claude -p spawn sites now pass CREATE_NO_WINDOW on Windows. Without
it, each per-batch claude.cmd spawn allocates a console — with Windows
Terminal as the default terminal, a labeling run pops one visible window
per batch on the user's desktop for the duration of the model call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Callable nodes are labeled with a trailing "()" (e.g. classifyProperty()),
so a bare-name query like "classifyProperty" falls through the exact-label
pass and then ties with any prefix sibling (classifyPropertySafe()) in the
contains pass, returning "No unique node match" even though exactly one
callable with that name exists.
Add a bare-name pass between exact-label and exact-source matching that
compares query and label with the trailing "()" stripped from each. Bare
queries now resolve decorated labels and vice versa; genuine duplicates
still return None.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
llm.py: add explicit edge direction rule to extraction system prompt —
source = actor (caller/importer/subclass), target = acted-upon (callee/
imported/base). LLM was systematically emitting callee->caller for calls
edges because the schema never stated direction semantics.
build.py: extend ghost-node merge to catch LLM nodes that populate
source_location (bypassing the old None check). Now uses _origin=="ast"
as the canonical signal — AST nodes always win; any non-AST node sharing
(basename, label) with an AST node is collapsed into the AST canonical.
Fixes LLM bare-stem IDs (bpe_get_pairs) surviving alongside AST
parent-qualified IDs (mingpt_bpe_get_pairs) and carrying reversed edges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>