The ambiguous-name refusal added in #4517 routed through _resolve_single_node,
whose winning tier can be a prefix or substring match, so a bare search keyword
that merely substring-matched several labels was wrongly refused: `query "client"`
returned EXIT 3 because it is a substring of `AlphaClient`/`BetaClient`. That
narrowed query's keyword-search semantics.
Adds _query_name_refusal, which refuses only when the term EXACTLY names several
nodes (the source_exact/exact tiers) -- #4511's `.nonce()` case -- and otherwise
falls through to scoring. The `query` and `--seed` paths use it; explain/path/
get_node keep _resolve_single_node, whose prefix/substring tiers are correct for a
direct node lookup. Adds a regression test that a substring keyword still searches.
Co-Authored-By: shivampal46011 <shivampal46011@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
query guessed a node when the same name existed in several files, and an ambiguous --seed still exited 0. explain and path already refuse that lookup. query now returns the same candidate list and exits 3, and a --seed that matches nothing exits 4.
#4561 changes extract_markdown's output (vault-relative wikilinks now resolve to
the real page), and markdown extraction is AST-cached, so a .md file cached under
schema 19 would serve stale unresolved links on re-extract until the file itself
changed. Bump the schema so the first build after upgrading re-resolves markdown
links. (The other batch-3 fixes -- cli, serve, PDF-warning, shebang detection --
do not change cached per-file AST output.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_shebang_interpreter returned the interpreter exactly as written, and both
detect's _SHEBANG_CODE_INTERPRETERS and extract's _SHEBANG_DISPATCH list
only the generic names. An extensionless CLI starting with
#!/usr/bin/env python3.12, #!/usr/bin/python2.7 or #!/usr/bin/lua5.4 was
classified as None and silently left out of the scan. Map a version
suffix on a known interpreter back to that interpreter (python3.12 ->
python3, lua5.4 -> lua) in the shared parser, so detection and extractor
dispatch both see it. Unknown names are left as they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
extract_pdf_text wrapped the whole PdfReader loop in except Exception:
return "", so a malformed or encrypted PDF looked exactly like a PDF with
no text and dropped out of the graph with no trace. Keep returning ""
(callers rely on it), but print a warning naming the file and the error
first. Warn once per path, since detect() and the semantic pass both read
the same file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same-file check used the bare source_file, so web/dup.py and ml/dup.py read as one file, with bare line numbers and a hint that could not tell them apart.
A file is now its repo plus source_file. A repo is shown only when the candidates span more than one repo, so a graph with no repo attribute reads as before. When the path is shared across repos, the hint says to use the full node id.
Obsidian writes wikilink paths from the vault root, so scanning a folder
below it meets links like [[Corp/wiki/Fox]] from inside Corp/. The
vault-wide fallback matched them as suffixes of root-relative paths,
which never succeeds, and the references edge was dropped. When nothing
matches, drop the longest leading run of the scan root's own folders
and match the rest exactly from the root.
_run_cli refreshed stale skills and old OpenCode plugins before the
--version/--help early returns, so checking what was installed rewrote
SKILL.md, left a .bak and restamped .graphify_version. Skip both
refreshes for version, help and `<cmd> --help`; the stale-skill warning
still runs, since it only reads. Free-text commands keep refreshing.
Carries the tests that belong with the module-call exact-name fix (#4577): the
fix landed via the PR's follow-up perf commit, which did not include the test
file. Adds the PR author's five cases (exact-name function/class resolution,
aliased import, fold-only fail-closed) plus a negative cross-module case proving a
call never binds to a same-named child of an UNRELATED module -- the one headline
risk of re-bucketing by exact name.
Co-Authored-By: madhusudanpatnaik <madhusudanpatnaik@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #4566. Adds an opaque (non-JWT) `Bearer <token>` shape to the
credential redactor so `Authorization: Bearer <opaque>` is redacted (the JWT form
was already caught by the eyJ pattern); the word "Bearer" is kept, only the token
is replaced. Adds an over-redaction regression test proving a 40-char git SHA, a
sha256 digest and a UUID pass through untouched, so the redactor never mangles the
ordinary high-entropy identifiers that appear in real rationale labels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This batch's Dart typedef (#4582), C++ Metal/CUDA qualifier (#4554, #4550) and
extern-"C" header-routing (#4526) fixes all change extraction output, so a repo
cached under schema 18 would be served stale output on re-extract. The Dart PR's
own 17->18 bump collided with the already-landed 0.9.93 schema-18 note and was
dropped; the C++ and extern-"C" PRs did not bump at all. One global bump to 19
covers the whole batch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Metal (.metal) and CUDA (.cu/.cuh) are C++-derived GPU languages that route
through the C++ extractor, but their qualifiers are not part of the C++ grammar:
- Metal address-space / function qualifiers: `device`, `constant`,
`threadgroup`, `thread`, `kernel`, `vertex`, `fragment`, ...
- CUDA execution-space / memory qualifiers: `__global__`, `__device__`,
`__host__`, `__shared__`, `__constant__`, `__forceinline__`, ...
tree-sitter-cpp parses a leading qualifier as the declaration's
`type_identifier` (the real type is pushed into an ERROR node), and
`_cpp_collect_type_refs` emitted it as a type reference. Each qualifier then
became a sourceless, corpus-shared god node that every shader/kernel
`references` (e.g. `kernel`/`device`/`constant` on sample.metal,
`__global__`/`__device__`/`__host__` on sample.cu, the CUDA ones colliding on
ids `global`/`device`/`host`). That is a provenance violation — a node with no
source_file — and the hubs also pollute god-node rankings in analyze.
Skip a fixed per-extension set of dialect qualifier keywords in the C++
type-ref collector, keyed by file extension so plain `.cpp`/`.cc`/`.h` files
are untouched (there these words are legal identifiers and still resolve as
real user types). Genuine user types in the shader/kernel (the `Vec3` struct)
are preserved.
Documented limitation: `constant Foo* p` buries the real `Foo` type inside the
ERROR node, so `Foo` is not recovered; this change only removes the bogus
qualifier node.
Docstrings and `# WHY:`/`# NOTE:` comments become rationale node labels,
which are persisted verbatim to graph.json, graph.html, GRAPH_REPORT.md and
the AST cache. A token pasted into a comment therefore ended up in the
artifacts the README tells users to commit and share.
Add security.redact_secrets() (shape-based: vendor-prefixed tokens, PEM
private-key headers, JWTs, URL passwords) and apply it in
_shorten_rationale_label(), before truncation so a cut can't leave a partial
token. That helper is shared by the Python and JS/TS rationale extractors.
Related to #2781 (document text sent to LLM backends unredacted); this
covers only the comment/docstring -> graph path.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The typedef step scanned every file twice (the `=` and legacy forms),
and the variable/method passes compiled-cache-looked-up the typedef guard
on every match, so a typedef-free test file ran ~3-10% slower than v8.
One substring test skips both scans when the file never says `typedef`,
and the guard is precompiled: bloc's 616 files now run at v8's speed or
better.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrXx9K7CikYRNMqWg99y8E
Filtering the children that share a folded key on every call made N
calls to N same-key names O(N^2) (2,000 case-variant calls: 1.7 s vs
0.8 s on v8). The exact-name index is built with the folded one, so the
lookup is O(1); pydantic's graph is identical to the scanning version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrXx9K7CikYRNMqWg99y8E
A C-API header guarded by `#ifdef __cplusplus extern "C" {` has no
class/namespace/template/:: marker, so `_is_cpp_header` left it on
extract_c. The C grammar rejects the linkage block and the header
extracts zero symbols (e.g. whisper.cpp's include/whisper.h and
include/parakeet.h), dropping the project's public API from the graph.
Add `extern "C"` to `_CPP_HEADER_MARKERS`. ObjC sniffing keeps priority.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #4500, graph.json carries "multigraph": true whenever it holds
parallel edges (imports and calls between the same pair, say), and
graphify export loads it as a MultiGraph. to_graphml's attribute
coercion indexed edges as H.edges[u, v], which needs a key on a
MultiGraph, so graphify export graphml crashed with "not enough values to
unpack" and wrote nothing. Iterate edges(data=True) instead, which works
for both graph types and keeps every parallel edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gh's statusCheckRollup mixes CheckRun entries (status/conclusion) with
StatusContext entries, the commit statuses external CI such as Jenkins or
CircleCI posts, which carry only a state. _parse_ci never read state, so a
failing external CI showed as NONE, or as SUCCESS beside a passing check
run, and the PR was classified READY instead of CI-FAIL in pr list,
triage and the MCP PR tools. Fold state into the conclusions, with ERROR
as a failure and PENDING/EXPECTED as pending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The git symbolic-ref fallback in _detect_default_branch kept only the last
path segment of refs/remotes/origin/<branch>, so a default branch named
release/2.x came back as 2.x and every PR targeting it was classified
WRONG-BASE. Strip the refs/remotes/origin/ prefix instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Kotlin typealias (#4553), C# delegate (#4552), and Swift macro (#4551) fixes
in this batch change extraction output (new node kinds), so a repo cached under
schema 17 would be served stale, stub-laden output on re-extract. The Rust fix
(#4567) already bumped _AST_CACHE_SCHEMA to 18; the schema is global, so that one
bump invalidates stale entries for the whole batch. This only extends the
schema-18 comment to document the three extractor changes it also covers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `use` whose first path segment is a `mod` declared in the same scope,
such as `mod item; pub use item::Invoice;`, named no node for the stub
rewire. When nothing else lent the rewire a node, the import edge
dangled, build minted an external stub `invoice`, and `affected` and
`explain` tied on it, the same symptom as #4439 for paths rooted at
`crate`, `self` or `super`.
The extractor now also names a `use` whose first segment is a `mod`
declared beside it. The scope is the use's own parent, so an inline
`mod tests { .. }` does not see the file's top-level mods, and a path
that starts with `::` is never named, because it always means a crate.
`use serde::Serialize` therefore still never lands on a same-named
type in the corpus.
The AST cache schema goes from 17 to 18 so cached Rust files are
extracted again instead of keeping the dangling edge.
Swift 5.9 macros were dropped entirely. `macro stringify(...) = #externalMacro(...)`
parses as a `macro_declaration` (freestanding or attached), which was in neither
`function_types` nor `class_types`, so the symbol never entered the graph. A
freestanding call `#stringify(...)` parses as a `macro_invocation`, not a
`call_expression`, so its call edge was lost too.
- Add `macro_declaration` to the Swift `function_types`. It has no `name` field,
but the name is a bare `simple_identifier` child already covered by
`name_fallback_child_types`, so the existing name resolution handles it; the
generic `<T>` type parameter is excluded by the existing type-param scope pass
(no `t` god node).
- Add `macro_invocation` to `call_types` and give it a callee branch in
walk_calls: a freestanding macro is a free top-level symbol (resolved by bare
name, no `self` receiver). Calls inside the macro's arguments
(`#stringify(helper())`) still resolve because walk_calls recurses into the
invocation's children.
Attached-macro *usage* (`@AddCompletion` on a type) is an attribute, not a
`macro_invocation`, so it is not linked to the macro — left unresolved
(fail-closed); the declaration itself is still extracted.
A C# `delegate` is a named type, but `delegate_declaration` was in neither
`class_types` nor `function_types`, so the declaration was dropped from the
graph. A field or parameter typed as the delegate (`public BinOp Op;`,
`int Apply(BinOp f, ...)`) still resolved the name `BinOp` through
`ensure_named_node`, which minted a *sourceless* stub and pointed the
`references` edge at it — even though `BinOp` is declared in the same file.
That violates the provenance invariant: a real source-backed declaration must
not lose to a sourceless god node (and the bare-id stub becomes a cross-file
hub shared by every same-named reference in the corpus).
`delegate_declaration` has a `name` field and no body, so adding it to the C#
`class_types` emits a clean empty type node (no phantom members) that the
in-file references now bind to. Namespaced delegates are scoped and contained
exactly like the sibling class/enum/struct declarations.
Delegate parameter/return type references and delegate-instance invocation
resolution are unchanged (out of scope).
A Kotlin `typealias` is a named type, but `type_alias` was in neither
`class_types` nor `function_types`, so the declaration was dropped from the
graph. A property or parameter typed as the alias (`var onClick: ClickHandler?`,
`fun handler(h: ClickHandler)`) still resolved the name `ClickHandler` through
`ensure_named_node`, which minted a *sourceless* stub and pointed the
`references` edge at it — even though the alias is declared in the same file.
That violates the provenance invariant: a real source-backed declaration must
not lose to a sourceless god node (and a bare-id stub becomes a cross-file hub
shared by every same-named reference in the corpus). Scala and TypeScript
already model type aliases as nodes, so Kotlin was the inconsistent outlier.
`type_alias` has an `identifier` name (covered by `name_fallback_child_types`)
and no body, so adding it to the Kotlin `class_types` emits a clean empty type
node (no phantom members) that the in-file references now bind to.
_fold_confidence normalized the confidence/confidence_score pair on edges
and hyperedges, but nodes passed through the build verbatim, so a model
emitting a lowercase enum value, a bare float, or a drifted word for a
node's confidence landed in graph.json as-is. Fold it in
_fold_node_aliases, which both build_from_json and build_merge already
call, so nodes share one normalization contract with edges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a labeling reply is cut off mid-object, _parse_label_response falls
back to a regex that pulls the complete "<cid>": "<name>" pairs, but it
used the captured string body as-is. Escapes such as \u00e9 and \" then
ended up in the community name as literal backslashes, while the same
name in a complete reply decoded correctly. Decode each salvaged value as
a JSON string body, keeping it unchanged when it is not a valid one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows PowerShell 5.1 (Set-Content -Encoding UTF8) and older Notepad save
UTF-8 with a BOM. load_graphifyrc read the file as plain utf-8, so the BOM
became part of the first key; unknown keys are ignored by design, so that
setting (say language.inc=php) vanished without a warning. Read with
utf-8-sig, as the other Windows-edited files in the repo already are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Twenty-three community PRs across JS/TS/Python/Go/Rust/Solidity call-resolution
and id-collision fixes, graph round-trip / dedup / confidence folding, skill
code-only-update and dispatch fixes, and LLM/Ollama/detection fixes. AST cache
schema bumped to 17.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Pi skill rendered the shared agent-tool-disk dispatch, which tells the
host to use subagent_type="general-purpose". pi-subagents has no agent by
that name and rejects it as unknown, so semantic extraction never started
(#4494). Step B3's re-run hints named the same agent.
Pi now gets its own dispatch fragment that sends one builtin `delegate`
agent per chunk; delegate has read, bash, edit and write. The two B3 hints
are core slots filled per dispatch variant, so every other host renders
byte-identical output.
(cherry picked from commit a0b3a053a5)
`extractors/markdown.py` read the scan root of the extraction in flight with a
function-local `import graphify.extract` + `getattr(_extract,
"_XAML_ACTIVE_EXTRACT_ROOT", None)`. That violates MIGRATION.md invariant #4
("never import from graphify.extract inside this package; direction is
extract.py -> extractors/"), and the `getattr(..., None)` meant a rename of the
module global would silently disable Markdown wikilink (Obsidian vault)
resolution rather than fail loudly.
Move the ambient scan root onto `extractors/base.py` — where the extractors
legitimately live — as `set_active_scan_root()` / `active_scan_root()`.
`_safe_extract_with_xaml_root` now publishes the root there (still saving and
restoring for nested extraction), the two XAML readers and the Markdown vault
lookup read it through the accessor, and `markdown.py` imports it in the correct
direction. The `_XAML_ACTIVE_EXTRACT_ROOT` module global in extract.py is gone;
nothing else referenced it.
Behavior is unchanged — the same process-global root is set around each
extraction and read by the same sites. Tests assert the extractor no longer
imports graphify.extract (fails on the old code), that the set/get/restore
contract nests correctly, and that markdown's accessor IS the base accessor.
The markdown, XAML/dotnet, and language suites stay green; the only failures on
this machine are the pre-existing v8 baseline set (c-include determinism,
wikilink-unicode, two watch cwd-deleted cases), which fail identically on clean
v8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 86721f765d)
Covers every shape the issue reports: the non numeric drifted words,
a numeric looking string that still backfills confidence_score, a
case only variant that gets recased rather than treated as an
unknown word, the three valid spellings staying untouched, and the
new hyperedge fold. A build_from_json test reproduces the issue's
own warning text end to end and checks it no longer prints.
(cherry picked from commit da369c5ea8)
The confidence fold only ever normalized a NUMERIC legacy confidence
(a pre enum graph's bare float) to INFERRED. A free form string the
semantic pass sometimes drifts into instead, high, confirmed,
medium high, medium low, or a lowercase inferred, passed through
untouched, so a graph carrying one re warned on every command
forever, exactly the gap the numeric fold's own docstring already
described but never closed for strings.
Pulls the shared logic into its own function so edges and hyperedges
can both call it, since a hyperedge carries the same confidence and
confidence_score pair and had no fold at all. A case variant of a
real enum value is corrected to its exact spelling, since it already
is the enum value and only needs recasing. Anything else folds like
the numeric case, backfilling confidence_score when the word happens
to parse as a float, and becomes INFERRED either way, since every
confidence reaching this fold came from the LLM semantic pass by
definition. Checked against validate.py's own VALID_CONFIDENCES
rather than a second copy of the enum.
(cherry picked from commit a158b6aaa9)
Covers the exact repro from issue 4492: a node with a source_file and
one without, sharing an id that neither file encodes. Checks both
arrival orders keep the sourced node, matching the sibling order
independent tests already covering the lifecycle and path form
tiebreaks right next to it.
(cherry picked from commit 151becb880)
When neither colliding node's own file encodes the id, both read as
mere references and tie on the first key in collision_rank too. The
remaining keys then fell straight to the reversed path segments,
where an empty path (no source_file at all) sorts before any real
one, so the node with no source_file always won. That node is the
one telling us nothing about where the entity lives; the sourced one
was the real record, and it was the one getting dropped.
Adds a source_file presence check right after the first key, the
same rule pick_winner already applies one level up for this exact
reason (issue 3775/3786). It changes no collision between two
sourced nodes, since both sides of that key already agree there.
(cherry picked from commit 8359922c33)
The 0.26.0 DreamMaker grammar changed the type-path node shape, so
extract_dm() no longer finds type definitions (a `.dm` file yields only
its own file node). uv.lock pins 0.25.1, so dev/CI are unaffected, but a
fresh `uvx --from graphifyy[all]` / `[dm]` resolves tree-sitter-dm from
PyPI with no upper bound and pulls 0.26.0, silently breaking DM
extraction for end users. Cap both the `dm` and `all` extras to
>=0.25,<0.26 (matching the tree-sitter-cpp pin style).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit c36fbc117b)
bash runs the backticked text in Part C's comment when graphify is on PATH, which broke the test in CI. The blocks do not need it. Also narrow three Optional values for pyright. The test checks the same things.
(cherry picked from commit e487672683)
Part A reads .graphify_detect.json and Step 9 deletes it, but the aider
and devin --update sections never wrote it again, so Part A raised
FileNotFoundError after any full build.
The sections now write it from .graphify_incremental.json, as the
generated references/update.md does, with each file's own path
convention.
The test no longer seeds the file; it asserts the update section wrote
it.
(cherry picked from commit ca090b6da5)
On aider and devin nothing wrote .graphify_semantic.json for a code-only
run, so Part C raised FileNotFoundError, and the --update paragraph sent
the agent from Part A to a merge that reads .graphify_extract.json.
Part B now has the fast-path block, the paragraph names Step 3A, the
fast path, Part C and the merge, and aider's only-deletions guard writes
the file its merge reads.
Adds tests/test_code_only_update_all_hosts.py for the two monoliths and
a skillgen audit entry for the new lines; the 14 generated hosts are
left to #4313.
(cherry picked from commit bbe1a7ca9b)
Bash expands backticks inside a double-quoted -c block, so two comments
ran graphify update and a command named changed before Python started.
Drop the backticks and keep the words; regenerate with --bless. The
gen.py monolith audit accepts the new Part C comment text.
(cherry picked from commit b05a09a1d7)
Checks every generated skill file statically, and runs the Part C and
update merge statements under bash with a stub interpreter. This commit
alone fails 58 cases (29 per check); the next commit fixes them.
(cherry picked from commit 0bbdd9d36c)
Preserve user-supplied database URIs in generated export instructions.
Prevent Neo4j and FalkorDB push workflows from replacing custom destinations with localhost.
Document safe URI quoting and credential handling.
Add regression coverage for URI forwarding and export-only behavior.
Co-authored-by: OpenAI Codex <codex@openai.com>
(cherry picked from commit 813b94ff51)
Two guards for the review questions on #4468: an in-file call to
`ignore()`, `e()` or `E()` binds by the name as written and reaches the
salted node (on v8 the calls to `ignore()` and `E()` bind nowhere, since
those functions had no node), and the other types in a Go file that holds
a case-only pair keep the ids they have without the pair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5c49344807)
#3782 left a reference placed above `struct Handle` on a sourceless stub
whenever another file-level item folded to the same id (`fn handle`,
`const HANDLE`, an `impl` for `HANDLE`), because walk() gave that id to
whichever item came first. Those items now get ids of their own, so the
struct owns its id and the reference can resolve to it, as a reference
below the declaration already did. Without this, the stub only folds back
when the type's name is unique across the corpus.
A declared type is now pre-registered unless a non-type item has its
exact name (`fn Foo()` beside `struct Foo {}` still share one node), and a
`type HANDLE` alias still keeps its stub. The #3782 test that pinned the
stub for the case-only shapes now asserts the resolution; the shapes that
still share a node keep the stub assertion.
local_type_names becomes a frozenset, which is what
_rust_collect_type_refs is annotated to take.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f710c8a16e)