`_resolve_typescript_member_calls` binds `this.repo.save()` only when the
receiver's type is declared in the same build. With the type in another
repository the resolver held the receiver type and dropped the call, so
`graph.json` — the only artifact `merge-graphs` and `global add` read — recorded
nothing and no merge-time pass could recover it.
Those calls are now parked on the caller as `metadata.unresolved_calls`, and
`link_cross_repo_member_calls` finishes them after the merge. One
`_LANG_SUFFIXES` key covers all eight TS/JS suffixes: a TS class legitimately
answers a JS call site, and two keys would drop every TS<->JS cross-repo call.
Only a table-typed receiver parks. The uppercase-spelling arm also matches
namespace aliases and default imports (`React.createElement()`), and
`node_modules` is never scanned, so parking those would send npm names to the
merge to be matched by bare name. The park sits strictly upstream of the #2553
origin gate, which only ever vets a name that did match locally.
(cherry picked from commit 5b593c6d00)
`_DISPATCH` routes `.mjs`/`.cjs` to `extract_js` and the cross-file call gate
already lists them, but `typescript_member_calls` claimed only six suffixes, so a
repo written entirely in `.mjs` was the one shape where the resolver never ran and
`const s = new Svc(); s.doThing()` produced no edge.
Both the suffix set and the resolver now read one `_JS_TS_SUFFIXES` constant.
(cherry picked from commit 84fce94ce8)
A receiver typed to a class this build declares nowhere is parked on the
caller (#3152); the merge then binds it to the one declaration in another
repo. PHP was absent from `_LANG_SUFFIXES`, so parked PHP entries were never
language-checked and the edge was dropped. Adding PHP's suffixes lets the
merge finish the call, gated to PHP declarations like every other tier.
Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit e7fee26542)
v8's `_resolve_php_member_calls` (#3872) binds scoped `Class::method()` calls
but leaves instance `$g->method()` calls unresolved. This adds the instance
tier beside it: a per-file receiver type table (`_php_receiver_type_table`
over typed properties, promoted constructor params, typed params and `new`
bindings) names the receiver's class, and the shared resolver takes the single
PHP declaration of that class and emits the `calls` edge, INFERRED.
The declaration index is gated to the PHP interop family: the index is
corpus-wide, so a same-named class in another language would both answer the
receiver and hide that no PHP file declares it. A receiver the table cannot
type to one class — untyped, union, longer chain — stays unresolved.
Co-Authored-By: Claude Code <noreply@anthropic.com>
(cherry picked from commit 199ba1af0d)
test_the_first_binding_of_a_name_wins asserted the flat table's cross-function
shadowing as intended behaviour, which the scoping fix removed; its fixture now
pins that each function's parameter answers only its own calls.
(cherry picked from commit 8a4bfef899)
Parameter names repeat across a Go file, so the file-flat table answered one
function's receiver with a sibling function's parameter type: `Second(g *Shouter)`
calling `g.Greet()` bound to Greeter.Greet. Each function body now carries its own
parameter/local table, consulted before the flat one, which keeps serving fields.
(cherry picked from commit b74abf098a)
A corpus-wide `_callable_class` lookup let a same-named class in another language
answer a Go receiver, and it hid from the parking branch that no Go file declares
the type. The flat per-file table also mistyped a method's own receiver when an
earlier binding of the same name won, so `s.Save()` inside a `Store` method could
reach `Server.Save`.
(cherry picked from commit 86792724ff)
The Go resolver had the receiver's type in hand and dropped the call when nothing
in the corpus declared it, so `graph.json` — the only artifact `merge-graphs` and
`global add` read — recorded nothing and no merge-time pass could recover it. A
merged graph of a Go service and a Go library was missing exactly the edges that
make it a call graph.
Parks the pair on the caller node like the Java/C++/C#/Swift resolvers already do,
and adds `"go": {".go"}` to the merge pass's language guard. `.go` alone: Go and
Java both turn up in one backend merge, and a shared type name across them is a
coincidence, not a namespace.
The `_key` case preservation the merge pass already documents is what makes this
correct for Go — `greet` is a different method from `Greet` and is not reachable
from another package at all.
(cherry picked from commit f7d2a50603)
`g.Greet()` where `Greeter` is declared in another file produced no edge. The
shared cross-file pass skips member calls, and the Go extractor read the
receiver's identifier only to discard it for anything that was not an imported
package — so the one shape Go dependency injection actually takes (a struct
field used from a method) contributed nothing to the call graph.
`extract_go` now exports a per-file `go_type_table` built from the four places a
Go receiver's type is written down: a struct field, a parameter (which covers the
method receiver `func (s *Server)`), `var x T`, and `x := T{}` / `x := &T{}`.
`_resolve_go_member_calls` looks the receiver up there, takes the single
declaration of that type and emits the `calls` edge to its method.
Also stamps `_callable` / `_callable_class` on Go declarations (#2438). Go is the
one extractor outside the tree-sitter engine, so its nodes carried neither, which
left them invisible to every consumer that reads the markers off the node.
Design notes:
- The receiver travels as `member_receiver`, not `receiver`: in this extractor
`receiver` means "imported package", and the Swift/Python/Ruby member-call
resolvers select on `is_member_call` plus `receiver` without checking the
language, so a Go name there binds to their types in a mixed corpus.
- `s.logger.Log()` is read as a call on the `logger` field only when `s` is the
enclosing method's own receiver. The table is flat and file-scoped, so any
other head could be a package or a variable whose type it cannot confirm.
- Names are matched case-sensitively, unlike the sibling resolvers. Go exports by
capitalisation, so `Run` and `run` on one type are two different methods.
- Go does not defer: a member call reaches `raw_calls` only once the bare callee
name misses in the caller's file, so in-file behaviour is byte-identical.
- Two packages declaring the same name stay two type nodes and the
single-definition guard bails — without import evidence neither is the answer.
(cherry picked from commit deca9fa366)
extract_go keys a type on its package directory, so every file declaring a
method on it mints the type again under the same id. Disambiguation then splits
those apart by path, and the type ends up fragmented into several partial nodes,
each owning some of its methods: the declaring file's node owns none of them,
and every single-definition guard downstream reads the set as an ambiguity.
Fold the id-collision before disambiguation, the same point and the same shape
as _merge_decl_def_classes for C/C++/ObjC. Guards: all members must be .go
files in one directory (the id folds in only the directory's name), and a
_test.go member stops the fold, since its package clause may be a separate
external test package. The declaring file wins; no edge re-pointing is needed
because the group already shares one id.
(cherry picked from commit 7f9439ddd5)
Validate both complete argument spans and reject unsupported body children instead of emitting partial enum graphs.
AI-Assistance: OpenAI Codex
(cherry picked from commit 7c09b4d360)
Extract NS_ENUM and NS_OPTIONS typedefs in Objective-C files using isolated enum body parsing and original member locations.
Fixes#4400
AI-Assistance: OpenAI Codex
(cherry picked from commit d6a74da475)
A `struct Stack<Element>` / `func map<R>(...)` binds `Element`/`R` as local
names, not corpus symbols, but `_swift_collect_type_refs` appended the bare
identifiers used in a signature or stored-property type as type references and
ran them through the sourceless-stub path. Every generic type/function in the
corpus then collapsed its `T` onto one bare `t` node — a god node that pollutes
god-node rankings, since single-letter type parameters are not in the
analyzer's builtin-noise filter. C#, Java, TypeScript, Kotlin, Scala, C++, and
Go already exclude theirs.
Mirrors that pattern (and complements the `Self` keyword fix already in this
collector): a new `_swift_type_parameters_in_scope` helper walks up to every
enclosing declaration's `type_parameters` list and takes each `type_parameter`'s
first `type_identifier` as the bound name (a constraint like `T: Equatable` is a
separate `user_type`, left to reference), and `_swift_collect_type_refs`
auto-computes and threads that skip set. Genuine user types and the `Int`/
`String` builtin hubs are unaffected.
(cherry picked from commit 2e8a5c0f97)
A `template<typename T>` binds `T` as a local name, not a corpus symbol, but
`_cpp_collect_type_refs` appended the bare identifiers used in a template
signature or member as type references and ran them through the sourceless-stub
path. Every template in the corpus then collapsed its `T` onto one bare `t`
node — a god node that pollutes god-node rankings, since single-letter type
parameters are not in the analyzer's builtin-noise filter. C#, Java,
TypeScript, Kotlin, Scala, and Go already exclude theirs.
Mirrors that pattern: a new `_cpp_type_parameters_in_scope` helper walks up to
every enclosing `template_declaration`'s `template_parameter_list` and collects
names from its `type_parameter_declaration`/`variadic_type_parameter_declaration`
children (a non-type `int N` parameter is a value and is left alone), and
`_cpp_collect_type_refs` auto-computes and threads that skip set. Genuine user
types and the generic container head still reference.
(cherry picked from commit e3bf52b318)
A `class Box[T]` / `def map[R](...)` binds `T`/`R` as local names, not
corpus symbols, but `_scala_collect_type_refs` appended the bare identifiers
used in a signature as type references and ran them through the sourceless-stub
path. Every generic class/method in the corpus then collapsed its `T` onto one
bare `t` node — a god node that pollutes god-node rankings, since single-letter
type parameters are not in the analyzer's builtin-noise filter. C#, Java,
TypeScript, Kotlin, and Go already exclude theirs.
Mirrors that pattern: a new `_scala_type_parameters_in_scope` helper walks up
to every enclosing declaration's `type_parameters` list (the use site is
`type_arguments`, a distinct node) and collects parameter names from direct
identifiers and variance-annotated params, skipping `context_bound`/
`upper_bound`/`lower_bound` nodes (those name real types). `_scala_collect_type_refs`
auto-computes and threads that skip set. Context bounds (#2046), genuine user
types, and the generic container head still reference.
(cherry picked from commit 585e909535)
A `class Box<T>` / `fun <R> map(...)` binds `T`/`R` as local names, not
corpus symbols, but `_kotlin_collect_type_refs` appended the bare identifiers
used in a signature or property type as type references and ran them through
the sourceless-stub path. Every generic class/function in the corpus then
collapsed its `T` onto one bare `t` node — a god node that pollutes god-node
rankings, since single-letter type parameters are not in the analyzer's
builtin-noise filter. C#, Java, TypeScript, and Go already exclude theirs.
Mirrors the C#/Java pattern: a new `_kotlin_type_parameters_in_scope` helper
walks up to every enclosing declaration's `type_parameters` list (the use site
is `type_arguments`, a distinct node, so an instantiation argument is never
collected), and `_kotlin_collect_type_refs` auto-computes and threads that skip
set. Genuine user types and the generic container head still reference.
(cherry picked from commit a9304a245c)
_dispatch_confidence was computed from the raw implementers set before
the per-implementer language gate ran, so a sourceless/external-reference
implementer, or a cross-language name collision, that never produces an
edge could still dilute the confidence score for the implementer(s) that
do. A single real implementer alongside one gate-failing entry scored
0.75 (the two-implementer tier) instead of 0.85.
Gate first, then count only the implementers that pass it, so the score
reflects how many implementers actually share the dispatch edge.
Addresses review feedback on PR #4396 (Graphify review bot, finding at
interface_dispatch.py:154).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit b01a6fcf10)
resolve_interface_dispatch (shared by the C# and Swift gates) bailed out
entirely whenever an interface/protocol had more than one implementer,
on the reasoning that guessing which one a runtime call reaches would be
wrong. For a static structural graph the question isn't which one is
reached at runtime, it's which ones *can* be reached, and the answer with
N implementers is all N, not none -- so every chain through an injected
dependency with more than one implementation was left permanently cut at
the interface.
Each implementer is still subject to the existing single-owner guard
independently: an implementer that itself declares more than one method
of the same name is skipped on its own, without suppressing its siblings.
Confidence drops as the candidate set grows, using the extraction
rubric's own discrete INFERRED tiers (0.85 / 0.75 / 0.65 floor) instead of
inventing continuous precision.
Fixes#3800 for the dispatches_to half (interface->implementation calls).
The property-access half of that issue (a method edge for `c.Count`
through an interface-typed receiver) is a separate mechanism and is not
addressed here, to keep this PR to one concern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit f340494e07)
Pi has no PreToolUse hook, so the installed hook-guard policy never ran
there. graphify/pi/ ships a Pi package (Node built-ins only, no
node_modules) that runs `graphify hook-guard read|search` before Pi's
read, grep, find and search-style bash calls: strict-mode denials become
tool_call blocks, and guidance arrives as a graphify system reminder.
Only local targets inside the project are checked, the hook uses the first
`graphify` on PATH outside the project, and every call is bounded and
fails open.
`graphify pi guard install|uninstall` delegates registration to Pi's own
`pi install|remove <dir>`; `graphify pi guard path` prints the package
directory. Oh My Pi reads the same `pi` manifest key, so
`omp plugin install "$(graphify pi guard path)"` loads it there too,
where guidance uses OMP's passive tool-call context channel.
(cherry picked from commit a6281363bb)
A grep fed by a pipe filters that command's output, not the code, so its
pattern is not a graph lookup the agent skipped.
(cherry picked from commit f9f3c9ce3a)
The always-on blocks now tell the assistant to pass the rule to every
subagent that explores code (a fresh subagent may never see a nudge before
it starts reading) and to read raw files only for lines the graph or an
earlier read already located, matching the hook's narrowed wording.
(cherry picked from commit 3127ccdcbd)
Sanitise graph text before it reaches the hint (one line, no control
characters or backticks, capped length), skip the graph parse when the call
has nothing to look up, isolate hint failures so they can never cost the
nudge or the strict deny, skip value-taking flags (rg -t/-g, -A/-B/-C...)
and file-name searches (find/fd), drop declaration keywords from terms,
match labels only (no source-path tier, short terms exact-only), resolve a
relative read target against the project root, and align the strict deny's
wording with the narrowed nudge.
(cherry picked from commit f8ba08fe93)
Agents treated the soft nudge as noise: it was the same placeholder text on
every call, and "or to modify/debug specific lines" excused nearly any
targeted read. A firing nudge (and the strict-mode deny) now carries:
- a command built from the call: `graphify explain "<symbol>"` when the
searched word is a node label, `graphify query "<terms>"` otherwise, or
`graphify explain "<file>"` for a read;
- up to five matching graph nodes with file and line, so the nudge shows
what the graph already knows instead of only asking for it.
The lookup scans graph.json labels and paths directly (0.1-0.5 s on 3k-15k
node graphs; the full query stack costs ~1.5 s per hook call), skips graphs
over 32 MiB, and falls back to the plain nudge on any failure or no match.
The escape clause now allows raw reads only once the exact file and lines
are known, and the always-on rule names the moment to query: before the
first grep, search or source-file read.
(cherry picked from commit b51314765c)
Twenty community pull requests: Java/C-C++/Python/Swift/Go/PowerShell/C#
/JS extraction and call-resolution fixes, EJS extractor, dedup and
shrink-guard hardening, and query/graph.html ergonomics. The AST cache
schema is bumped (now 12), so the first build after upgrading
re-extracts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Java inherited-supertype and chained/new/cast receiver typing (#4305,
#4307), C/C++ cross-directory quoted-include provenance (#4392), and
JS external imports anchored to the package rather than the path leaf
(#3092) all change AST extraction output, so warm caches must
re-extract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add to_json() and watch._check_shrink() regression tests that build a real
graph where build_from_json folds a semantic ghost into its AST twin (#4389),
plus a negative control showing a genuine semantic loss is still refused.
Both positive tests fail on v8 and pass with this branch.
- Update _fold_node_aliases / _fold_edge_aliases docstrings and the build()
dedup comment to list the new file/filepath and relationship aliases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 946333b2ab)
The seeded path hands the resolved nodes to _subgraph_to_text as seeds (seeds=start_nodes, the BUG2 fix), so the
given seed is the first NODE line and still shows under a small token budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 67f65c44b0)
- graphify query --seed with no value (trailing --seed or --seed=) now exits with an error instead of running a
keyword query.
- An unresolved seed given twice is listed once.
- The query log records the seeds used.
- Tests: the callable form (name and name()), --seed with --context, both no-value forms, and the logged seeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c46b0e78d0)
An external ranker (embeddings, a hybrid or learned retriever, or an agent
that already knows the file) can choose where graphify query starts instead
of the question's keyword matches; the graph then does the expansion.
Each --seed value resolves exactly as affected resolves its seed
(affected.resolve_seed: node id, unique label, callable name, repo-relative
or absolute file path anchored to the graph's root), so there is one
interpretation of a node reference. Fail closed: a value that does not
resolve to exactly one node is reported and skipped; if none resolve, the
query says so and does not fall back to keyword seeds. Without --seed the
output is unchanged (rendering moved into _render_query as is).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c8af4e1b93)
Follow-up to the review on #4291:
- Hovering anywhere on a legend row except its count (checkbox, dot,
gaps, padding) now shows the truncated name. The tooltip tracks the
area the pointer must leave (the whole row for a name, the count
itself for a count), so moving inside a row doesn't hide or restart it.
- hideSidebarTip() also clears textContent, so a hidden tooltip doesn't
keep the last name in the DOM.
- Tests: the mock elements now have offsetWidth/offsetHeight, so the
positioning math is checked (no more NaNpx). New tests cover the
cursor offset, clamping and flipping near the window edges, and
hovering every part of a legend row on a parent-linked mock DOM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1000de9b46)
The 280px sidebar truncates community names in the legend, search results
and Node Info neighbor links with a CSS ellipsis and gives no way to read
the full text, and the legend count has no label. On graphify's own graph
126 of 283 legend labels (45%) are truncated.
Add one shared tooltip that reuses vis-network's .vis-tooltip style and
100ms tooltipDelay:
- Names show it only when the text is truncated (scrollWidth > clientWidth).
- The legend count always shows it, as "N nodes" / "1 node".
- Text is set with textContent only, keeping the #1838 / #3686 escaping
invariant.
- One delegated listener covers rows rebuilt later.
- No title attributes are added, so the native tooltip does not stack.
Node-driven regression tests cover truncated and non-truncated rows, the
count wording, literal rendering of & " ' < >, and single escaping of the
legend label.
Fixes#4286
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit def404e576)
EJS views sat in CODE_EXTENSIONS with no extractor, so an Express/Node app's templates were counted as code and contributed nothing (the #1689 warning). A .ejs file is HTML with <% %> scriptlets and inline <script> blocks, so feeding the whole file to the JS grammar made it one top-level ERROR.
Add extract_ejs: mask the template down to its JS regions (code-tag and <script> bodies), parse them with the TS grammar, and add the two relationships no JS grammar can see - dynamic import('./X') and the EJS <%- include('partial') %> composition edge, resolved to the real partial file. Wire .ejs into the dispatch, the jsts language family, the JS resolve/cache sets, and the hook source extensions.
(cherry picked from commit 3824cda537)
Python property-accessor decorators and `Self` (#4385, #4386), Swift
`Self` (#4387), Go generic type params (#4388), the Python forward
type-hint file-node fix (#4343), PowerShell dot-source paths (#4369),
and the C# verbatim-interpolated-string recovery (#3612) all change AST
extraction output, so warm caches must re-extract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A semantic fragment sometimes mints the bare id of a file it only
points at: docs/firmware/protocol.md emits `protocol` for
docs/protocol.md. Path-qualifying that id per file renamed it onto the
mentioning file's own page, so the reference became a link from the file
to itself. A collapse that picked the survivor by label length could
instead drop the real document.
Rule: a node is read as a mention when another node of its own file
points at it with a `references` edge from that same file AND its id is
itself a path slice of that file (it names a document). A mention is
not a rename candidate, and it ranks as a reference in _collision_rank,
so the real document outranks it whatever the label lengths are. One
helper, _mentions, decides both places.
The path-slice gate matters: entity ids such as `x_intro` are the target
of ordinary intra-file `references` edges. Without the gate the rename
skips them and two real documents collapse, and the ranking changes the
survivor of unrelated collisions. A test pins the survivor.
Fixes R (reference to another document) and R_labelswap: no self-link,
and no dropped document decided by label length. Two real documents
(D), a reference from a third file (F) and one entity described in two
documents (G) are unchanged.
Limits:
- The evidence is inferred from the edges an LLM chose to emit. A
missing or reversed edge gives no evidence, so both files are renamed
as before.
- A real document whose own node is linked to its file-level id the
same way reads as a mention and collapses as on v8.
- A top-level protocol.md that also mints `protocol` is a separate
collision and still drops one document; pinned by a test.
- If two or more other files also mint the mention's id, it stays on
the bare id and build folds it into its own page.
(cherry picked from commit 2d69f5e541)
Semantic extraction sometimes mints an id from the bare filename stem, so
a/x.md and b/x.md both emit `x`. Dedup then collapsed them into one node
and one file's node was lost. _semantic_id_remap in build.py knows how to
rebuild the full-path id, but it runs after dedup and is keyed by id alone,
so it cannot send one id to two files.
Add _path_qualify_dropped_collisions next to the extension pass. When two
or more files with different full path stems mint the same shorter-stem
id, rename each node to its own full-path id (keyed by id and source
file) and move edges by their source_file. References from other files
cannot be attributed, so they are left unresolved with a warning.
Boundaries: a group is skipped when the file the bare id names is in the
input (a genuine reference); absolute source_files are never qualified,
so checkout paths cannot leak into ids; AST-tier nodes are untouched.
This reverses the collapse pinned in #1504/#4282 for path-dropped ids:
the #1504 test now uses a non-shorter-stem id and the protocol test now
expects both files to be path-qualified.
(cherry picked from commit 58f844e57f)
Two gaps in the PowerShell extractor dropped edges from real-world
scripts:
- Dot-source only read the path when tree-sitter parsed it as
command_name. `. "$PSScriptRoot\lib.ps1"` and `. '.\lib.ps1'` parse as
string_literal, and unquoted `. $PSScriptRoot\lib.ps1` as
path_command_name, so no imports_from edge was emitted. Accept all
three and strip surrounding quotes; the existing basename logic
already handles the rest.
- Calls were collected only from function/method bodies, so commands at
script scope (the main body of most .ps1 scripts) produced no calls
edge or raw_call. Walk the file root too, with the file node as the
caller. walk_calls stops at function_statement/class_statement, so
function bodies are not walked twice (covered by a control test).
`. (Join-Path $PSScriptRoot 'lib.ps1')` stays unresolved: the path is
only known at runtime.
AST cache schema 8 -> 9 so cached results for unchanged .ps1 files are
re-extracted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LcXGsdFdpLjYVHemLogkE
(cherry picked from commit 69e20c70e3)