The claude-cli backend defaults to Opus, so community naming can burn
Opus quota silently. Document the env var in the README environment
section and mention it in the label/cluster-only --model help.
(cherry picked from commit f6d8a9b6b0)
graphify watch needs watchdog, but the README extras table omitted the
[watch] extra, so users following the install docs hit 'watchdog not
installed'. Add the row alongside the other extras.
(cherry picked from commit ff015b9a74)
- README: add a compact 'Labeling and rendering limits' section covering
--batch-size vs context window, --missing-only, .graphify_labels.json.sig,
export html --node-limit / GRAPHIFY_VIZ_NODE_LIMIT, and the Leiden->Louvain
fallback.
- CLI help: state the ~16k default sizing for --batch-size and the 5000
default for export html --node-limit.
Docs/help strings only; no behavior change.
(cherry picked from commit 4bbb93789b)
0.9.95 was gated but never shipped, so the follow-on batch belongs in it,
not a separate 0.9.96. pyproject stays at 0.9.95.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers #4578/#4579/#4580/#4581 (Python/Go/C++/Rust aliases + Rust union),
#4591 (#4584, C# overload nodes), and #4572 (#4569, package.json dep link).
Each PR shipped a stale 20->21 bump against an older base; this is the single
reconciled bump on top of HEAD's 24.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4572 (authorship preserved); stale cache-schema bump dropped for one combined bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4591 (authorship preserved); stale cache-schema bump dropped for one combined bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4581 (authorship preserved); stale cache-schema bump dropped for one combined bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4580 (authorship preserved); stale cache-schema bump dropped for one combined bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4579 (authorship preserved); stale cache-schema bump dropped for one combined bump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applied from PR #4578 (authorship preserved); the PR's stale cache-schema
bump is dropped in favour of one combined bump for the alias cluster.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under a deep root, the cache entry
<root>/graphify-out/cache/ast/v<ver>-s<schema>-g<grammars>/<64 hex>.json,
and mkstemp's even longer <hash>.<random>.tmp temp name, overrun Windows
MAX_PATH. save_cached raised FileNotFoundError and `graphify extract`
failed with "AST extraction failed" (e.g. any project under the default
%TEMP% of a long user name).
Move detect._os_path (the \?\ helper from #1655) to paths.os_long_path
so cache.py can share it, and use it for the entry and temp file in
save_cached and load_cached. detect._os_path stays as an alias. Keys are
unaffected: _normalize_path still strips the prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit abf9c9f6ab)
The 0.9.95 YAML structural extractor (#4706) imports PyYAML, but it was
only a transitive dependency, so a fresh `pip install graphifyy` produced
zero nodes from .yaml/.yml files (the extractor degrades gracefully but
silently). manifest_ingest and markdown also use it via guarded imports.
Declare it like tree-sitter-json so the feature works out of the box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bump _AST_CACHE_SCHEMA 23 -> 24: #4705 (ObjC NS_CLOSED_ENUM) and #4677 (SQL
CREATE TABLE ERROR-node recovery) both change extraction output, and neither
PR included the bump, so a warm ObjC/SQL cache would serve stale results.
- #4677: display the recovered table label through _clean_ident (consistent
with the routine/trigger recoveries) while keeping the id keyed off the raw
name so FK edges still resolve to it.
- Add regression tests the #4708/#4709/#4710 security PRs shipped without
(multicast/unspecified SSRF block, Obsidian tag injection, Google shortcut
frontmatter escaping).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rebuild bodies read graphify-out/ and its .graphify_root relative to
the cwd, but git does not always start hooks at the top level. With
GIT_DIR exported and no GIT_WORK_TREE (a nested git call from another
hook or tool), git takes the caller's cwd as the work tree and
`git rev-parse --show-toplevel` echoes that subdirectory. A stray
graphify-out/ left there by an earlier run then roots the rebuild at the
subdirectory; for .claude/worktrees/ that walks every linked worktree.
The #3265 guard rejects a `..` marker, but its fallback is the same cwd.
Add a shared _TOPLEVEL_ANCHOR snippet right after _WORKTREE_GUARD in all
four hooks. In the standard layout it cds to the parent of the absolute
common .git dir; with GIT_WORK_TREE or a separate git dir it keeps git's
own --show-toplevel. GIT_DIR is rewritten to its absolute form so later
git calls still resolve. The block already runs in a subshell, so the cd
does not leak into chained hooks.
(cherry picked from commit e6193ab816)
An ObjC class whose @interface(.h) and @implementation(.m) live in
different repositories surfaces as two same-named candidate type nodes
that _merge_decl_def_classes folds within a repo but cannot across the
boundary (its id embeds the directory, #1556). The cross-repo member-call
pass then tripped the single-definition guard (len(candidates) != 1) and
dropped the call edge even though exactly one candidate DEFINES the
selector.
Scoped to lang == "objc": when more than one same-named candidate
survives, union their members and bind the selector to the sole candidate
that DEFINES it (a method node with definition_file set, or a .m/.mm
source). Two candidates that both define it are two unrelated classes and
stay ambiguous, so the guard still catches the collision it was meant to.
Other languages, the C# resolver path, and the single-candidate path are
unchanged.
Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit a82a375c2d)
`extract --no-cluster` and the clustered path disagreed on where a
framework-style `#import <Kit/ToolEvent.h>` lands. The clustered path
re-pointed the edge at the header's real file node (`kit_event_toolevent`);
the raw path minted an external stub for the header's bare basename
(`toolevent`) and left the edge on it, so the two write paths of one
extractor produced different graphs for the same corpus.
The raw path only had a normalization map. Normalization cannot bridge
`toolevent` and `kit_event_toolevent` - they are not the same string
modulo case and punctuation - so re-pointing was impossible. The
clustered path resolves it through the pre-migration alias index, which
maps a file's old-stem spellings onto its canonical id (#1504/#1509).
Move that index into a shared `_alias_index` helper and use it in both
paths, so the clustered and the raw write path resolve an endpoint the
same way. The raw path now re-points endpoints BEFORE it mints stubs, so
an import that names a real file is no longer frozen onto a stub minted
for its unresolved spelling; only a genuinely external import
(`<Foundation/Foundation.h>`) still mints one (#2873).
An alias is committed only when exactly one node claims it, so two files
sharing a bare stem stay ambiguous and the import keeps its stub rather
than binding to whichever same-stem file was seen first.
Tests: two unit tests over `finalize_raw_graph_endpoints` (an old-stem
alias resolves to the real file node; an ambiguous alias stays
unresolved) and one end-to-end test that `extract --no-cluster` and the
clustered path agree on the import target. The new tests fail without
this change.
(cherry picked from commit acbec5f5c3)
Pins the resolver against the node shapes #2032 and the rationale pass
produce, plus end-to-end runs of the issue's repro and the comment
control that was never broken.
(cherry picked from commit 3b04706956)
A path query matches every node in the file, so resolution ends in
_prefer_file_node. Two of its passes compare the label to the bare
basename, which #2032's disambiguation rewrote to a unique path suffix
(alpha/hooks.py); the third pass trusts L1 uniqueness, which a module
docstring (a rationale node on line 1) or a one-line top-level
definition also occupies.
Accept a label that names its own source_file, using the
_is_file_node_label predicate build/analyze already share. Runs before
the L1 fallback, so extra L1 nodes no longer decide the result.
(cherry picked from commit b80626ed3b)
Three fixes on top of #4706's YAML extractor:
- Multi-document YAML (`---` separators) returned zero nodes because it used
yaml.safe_load, which accepts only one document; k8s manifests, Helm output,
and CI pipelines (the headline use cases) were silently empty. Use
safe_load_all and give each document a distinct node (labelled by kind+name
for k8s) so two docs in one file do not merge their keys.
- Add a _is_config_yaml gate mirroring extract_json's _is_config_json: a
top-level data array, an OpenAPI/Swagger spec, a JSON-schema-in-YAML, and a
flat all-scalar bundle (i18n) are skipped as data instead of minting up to
500 orphan nodes. A mapping with any nested value, a recognized config name,
or a .github/workflows path is still walked.
- Bump _AST_CACHE_SCHEMA 22 -> 23 (the PR reclassifies .yaml/.yml and adds a
new extractor, so cached YAML must re-extract).
- Tests: multi-doc, data/OpenAPI/array skip, the safe_load ACE guard, and caps.
- Clearer missing-PyYAML hint (pip install pyyaml).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rcfile language table maps 'markdown' -> .md; 'md' is not a recognized
alias, so the override was ignored and the watch test never saw a document.
(cherry picked from commit c1890f376a)
- Add tests/test_yaml_extraction.py (classification, key nodes, contains edges,
stable ids, scalar/empty documents).
- Drop .yaml/.yml from the oversized-document slicing parametrization: they are
code now, not documents sent to the semantic pass.
- Point the language-override lifecycle test at .md so it still exercises a
document override (yaml is no longer a document).
(cherry picked from commit b69b32e2f1)
YAML files were classified as documents, so code-only / AST-only runs
(graphify update) produced zero nodes from them. Move .yaml/.yml into the
code path like .json: add a PyYAML-based structural extractor (extract_yaml)
registered in _DISPATCH, add the suffixes to CODE_EXTENSIONS, drop them from
DOC_EXTENSIONS and the splittable-text set.
Deterministic ids derived from the file path; top-level and nested keys become
nodes with contains edges, capped at 500 pairs / depth 6 to avoid exploding on
data-heavy YAML.
(cherry picked from commit 2e5565a684)
Adds a regression test that fails on the pre-fix resolver: a caller that
imports a module for a different symbol and calls a same-named function must
keep the edge INFERRED, never EXTRACTED.
(cherry picked from commit 7d41be8891)
When an ambiguous bare call resolves to a candidate solely because the
caller imported the candidate's source module (not the symbol itself), the
edge was promoted to EXTRACTED, falsely certifying a same-named symbol that
was never actually imported. Keep the resolution (for graph connectivity) but
mark it INFERRED. Symbol-level (from X import Y) imports still promote to
EXTRACTED.
(cherry picked from commit e73ca4e4a7)
The stub rewire skipped dotted labels, but a SQL table is schema.name,
so dotted labels from .sql files now count as definitions. SQL stubs
that share a normalized name and bind to nothing fold into one node
(the smallest id, so file order doesn't matter). .sql joins the
language families, so a SQL read no longer binds to a same-named
definition in another language.
(cherry picked from commit 55bc540bbe)
A table no file defines, read from N files, becomes N per-file stub
nodes, and a schema-qualified read never binds to its definition in
another file. A SQL read of `Orders` also binds to a Python class
Orders. Guards cover same-file binding and different schemas.
(cherry picked from commit 1dfc710497)
GO is a client-tool command, not SQL, and tree-sitter-sql has no rule
for it. Before parsing, each GO [n] line found on the masked text
(comments and strings blanked) becomes `;` plus spaces, so byte
offsets and line numbers don't move. A go line after a comma, or
before a comma or `)`, is a column and stays.
AST cache schema 20 -> 21 so cached .sql extractions re-run.
(cherry picked from commit 2293c31d79)
A GO line with no `;` before it is read as an identifier, so the next
statement lands in an ERROR node and its table is lost. Guards cover
`;`-terminated batches, GO inside a block comment, a column named go,
and GO after a keyword line such as SET NOCOUNT ON.
(cherry picked from commit f00ecc42c9)
tree-sitter-sql has no T-SQL bracket identifiers and no create_procedure
node, so bracket names were mangled and procedures only got a node from
the regex recovery, never an edge. Labels and lookup keys now drop the
brackets (ids are unchanged). Each recovered routine's body, up to the
next CREATE/ALTER, a GO line or EOF, is scanned on the masked text:
FROM/JOIN give reads_from, and INSERT/UPDATE/DELETE/MERGE/SELECT INTO
give writes_to. Alias updates resolve to their table; CTEs, variables
and temp tables are skipped.
AST cache schema 20 -> 21 so cached .sql extractions re-run.
(cherry picked from commit 70f521dba9)
Bracket names like [dbo].[Orders] come out as the label `dbo].[Orders`,
stored procedures get no reads_from edges in any dialect, and
INSERT/UPDATE/DELETE/MERGE make no edge. Guards cover aliases, CTEs,
temp tables, variables, comments, strings and body boundaries.
(cherry picked from commit a2a666c6d7)
The landed docstring listed only the get_node/get_neighbors MCP tools,
but the CLI `explain` command also routes node lookup through this
resolver (cli.py), so a substring-only near-miss now refuses there too.
Name explain in the docstring so the shared surface is accurate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`get_node` / `get_neighbors` share `_resolve_single_node`, which takes the
first node of the winning tier. The substring tier matches a query anywhere
inside a label, so `get_node(label="MailController")` on a codebase with no
`MailController` returned `EmailController` as if it were the exact symbol,
with no hint that the name does not exist.
The resolver now splits the two fuzzy tiers by intent. A prefix match still
resolves, because the caller started typing the real name (`email` ->
`EmailController`). A substring-only match is a search hit, not the symbol
the caller named, so it returns `No exact match for '<query>'` followed by
up to five closest names with their source location, plus a `path::symbol`
retry example. `get_neighbors` inherits the same answer through the shared
resolver, so both tools report one lookup the same way (ADR-0001).
Exact, source-exact and prefix resolution, the ambiguity refusals, the
dotted/Python and `path::symbol` forms and scoring are unchanged. The other
near-miss case in the issue (`get_node(label="edit")` with several `.edit()`
methods) already refused as Ambiguous under ADR-0001; tests cover both.
Tests: the issue's MailController repro, prefix shorthand, exact-over-substring
precedence, the five-candidate cap.
(cherry picked from commit d3875f4f1c)
A cross-repo `calls` edge took its `source_file` from the caller node
(an ObjC method node is minted at its `@interface` in the `.h`) while its
`source_location` is the call-site line, recorded in the `.m`. Pairing the
two pointed `source_file:source_location` into the wrong file — often past
the end of a short header.
The park payload now carries the call-site file alongside the line, and
`link_cross_repo_member_calls` prefers it, falling back to the caller
node's file for pre-#3152 graphs. The parked path is relativized in the
same extract-time pass that relativizes node source_files, so it never
leaks the absolute checkout path into graph.json.
Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit 3281d42492)