Document mitigations that landed after the last SECURITY.md update:
- URL validation now rejects embedded credentials, port 0, and missing hostnames.
- ingest writes via lstat and refuses to replace an existing entry (symlink escape).
- credential-shaped text is redacted before it reaches graph.json.
- uninstall --purge refuses the project, home directory, and filesystem roots.
(cherry picked from commit 61f9bc82c4)
install.py defines antigravity-windows (the Windows shell variant of the
Google Antigravity platform), and the CLI help already lists it, but the
README platform table only had 'Google Antigravity'.
(cherry picked from commit ab742a3058)
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>