These land together because they are interleaved in extract.py/engine.py.
#2584 (PR #2588, thanks @phudayyy): the 0.9.38 dynamic-import dedupe keyed
only on target, so an in-function import() suppressed the file-level edge
affected follows. Dedupe now keys on the importing file, emitting one
file-level dynamic_import edge per file/target while keeping the call-site
edge.
#2586 / #2417 (PR #2586, thanks @EZZEASY): a Python member call on an
untyped receiver (x.get(...)) no longer binds by name to a same-named
module function. walk_calls now defers non-self/cls/super Python member
calls to the evidence-gated resolver; super().method() still resolves.
Known trade: same-file x = Thing(); x.method() loses its evidence-free
edge (precision over recall, per #2553).
#2589/#2591 resolver arm (in _resolve_objc_member_calls): the @protocol
exclusion and the self.field/_ivar receiver resolution (paired with the
objc.py extractor changes committed separately).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2589 (PR #2500): a @protocol declaration (labeled <Name>) is excluded from
the receiver-type index, so it no longer collides with a same-named class.
#2590 (PR #2501): a category/class-extension interface is keyed off the
base stem and folds into the base class instead of minting a duplicate
node.
#2591 (fresh): @property and ivar declarations are captured into a
per-class field-type table, and a message send to a self.field / _ivar
receiver resolves through it (bare field name only, so Foo.shared cannot
fabricate to a FooShared class). All hold the single-definition guard and
emit INFERRED.
Adapts PRs #2500/#2501 (thanks @xiongjianxu); #2591 fresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graphify watch only rebuilt on a code-file event, so deleting only doc
files while watching flagged needs_update but evicted nothing until the
next code change. A batch containing any vanished path now triggers a full
reconcile rebuild (which evicts the deleted source); a surviving modified
doc still writes the needs-LLM flag. The general deleted-file leak was
already fixed in 0.9.10; this closes the live-watcher residual.
Thanks @angmeng.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fuzzy dedup compared same-file labels with prefix-weighted Jaro-Winkler, so
two distinct entities differing by one content word (asset contribution
flow vs asset consumption flow) cleared the threshold and one was lost. A
one-token difference is now judged on the differing tokens (any distinct
content word blocks; stopword/typo variants still merge), with a
same-length Damerau-Levenshtein typo escape. Genuine typo and
whitespace/case/punct variants still collapse. The #2532 collision path is
untouched.
Adapts PR #2587 (thanks @wilyan09007) with two hardening deltas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These changes are interleaved across engine.py/extract.py by function, so
they land as one batch.
#2568 (thanks @imagineers-tyler): the 0.9.37 #2552 callback-body fix
unioned sibling closures' local names under the shared declaration, so a
local in one callback suppressed a real indirect_call in a sibling. Locals
are now scoped per body (keyed by body id, via walk_calls' extra_locals).
Restore-only, never fabricates; #2552 capture preserved.
#2565 (thanks @kskchaitanya1993): Kotlin property initializers — class,
top-level, companion, and `by lazy {}` — now seed call extraction, so
`val repo = createRepo()` produces a calls edge; literal initializers
produce none; FQ calls compose with the #2550 resolver.
#2561 (thanks @fakewaffle): Swift receiver typing now handles
`@Environment(Store.self)` (whitelisted; @Query/keypath/dotted skipped to
avoid a wrong edge) and in-corpus factory bindings via a marked concrete
return type; opaque/array/out-of-corpus returns stay unresolved.
#2577 (thanks @wilyan09007, PR #2579): the SQL extractor no longer emits a
reads_from edge to a CTE name. WITH names are scoped per query (a subquery
CTE no longer suppresses an outer real table of the same name), so a CTE
no longer mints a bare stub that binds to an unrelated same-named symbol.
#2575 (thanks @phudayyy, PR #2574): a dynamic `import('…')` inside a nested
function or at module scope now produces an edge, dynamic_import is
included in affected, and calls inside nested named functions are
collected; a dynamic import already captured as a deferred imports_from is
not double-counted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2543 (adopts PR #2546, thanks @michaelxer): a failed extraction is no
longer stamped in the incremental manifest as up-to-date, so graphify
update retries it instead of skipping it forever; a manifest already
poisoned by the old behavior is healed on the next run; genuinely
unchanged files are not re-processed. Extended to the watch save_manifest
paths too.
#2554 (adopts PR #2555, thanks @annieyii): the claude-cli backend now
inspects the stdout envelope for an is_error result (e.g. a rate limit
returned with exit code 0) and raises it on both the zero and non-zero
exit paths, instead of parsing it as an empty success and bisecting
against a live rate limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TypeScript (#2553, #2552, thanks @Earthfreedom):
- _resolve_typescript_member_calls matched a receiver type by name alone
and emitted EXTRACTED, so a third-party import could bind to an unrelated
local class of the same name. It now requires the matched type to be
same-file or imported by the caller's file, and tiers table-inferred
receivers to INFERRED.
- calls inside a callback passed to another call (const h = wrapper(arrow))
were never walked; the callback body is now walked and attributed to the
declaration, through the same import-gated resolution so it cannot
fabricate edges. The #2553 gate lands with #2552 by design.
Kotlin (#2526, #2550, #2551; adapts PR #2531, thanks @Mustaqeem66;
reports from @spaceBrownie and @thomasrengot-hub):
- match the bundled tree-sitter-kotlin 1.1.0 import node and resolve each
import to the real target node (imports were silently dropped), so
genuine calls promote to EXTRACTED via import evidence;
- a fully-qualified call com.example.Foo.bar() now produces a calls edge;
- a file the grammar cannot fully parse (e.g. one-line class C { val x })
now warns instead of silently extracting nothing, and declarations
recovered inside an error span keep their enclosing class.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Windows skill variant had a PowerShell Step 1 but its later steps came
from the shared core fragment as bash-only shell (cat-piped interpreter
invocations, rm -f, find -delete). The skillgen renderer now translates the
composed core to PowerShell for powershell-shell platforms (here-string
interpreter invocations, Remove-Item cleanup); POSIX skills are unchanged
and step / #2490 parity is enforced by a generator check. Not a regression
from 0.9.35 (skill files were unchanged between 0.9.34 and 0.9.35).
Thanks @tannermosher2015-debug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_collision_rank no longer falls through to raw lexical source_file order.
It now ranks by a lifecycle penalty (active over archived) computed on the
root-relative path, then a reversed-segment tie-break, so the winner of a
collision (e.g. plans/_done/x.md vs plans/in-progress/x.md) no longer
depends on ASCII filename order, absolute-vs-relative path form, or the
checkout directory name.
Active/archived idea from PR #2540 (thanks @michaelxer); the root-relative
and reversed-segment determinism is implemented here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
swift_extensions[].nid is now remapped in the in-process id/sym remap
passes (not just the cache path), so a cross-file extension merges onto
its base type before call resolution; static Foo.bar() and singleton
Foo.shared.bar() calls resolve again. The merge candidate set is gated by
language family + type-def predicate so it never absorbs a same-named
type from another language.
Folds in PR #2539 (thanks @pawelo446).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cluster-only warns when --backend/--model/--batch-size are ignored on the
label-reuse path; the community-label prompt key no longer collides with
the discard sentinel (an echoed key was silently dropped); tree --root
exits non-zero when it matches no source file instead of flattening the
tree; and cluster-only stamps built_at_commit from the analysed graph, not
the shell cwd. Also folds in the cluster-only refused-write guard from
PR #2522 (thanks @aniJani).
Thanks @elecnix for the report.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2508 (thanks @Tomaskobel): callflow loads the graph directed and recovers
direction from _src/_tgt markers (consistent with the path fix), so
caller/callee columns are correct; indirect calls are now counted.
#2507 (thanks @filipechagas): relational-intent verbs (calls/uses/extends/
...) are excluded from the per-term seed guarantee, so a decoy matching
only the verb no longer becomes a traversal root, while a verb that is a
real symbol name can still be seeded on merit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2495 (thanks @alisson-acioli): graphify update now evicts a file newly
added to .graphifyignore/--exclude even though it still exists on disk
(positive ignore-rule evidence), with .gitignore-driven eviction gated on
an explicit full update; unchanged in-corpus files and fail-closed #1795
cases are preserved.
#2504 (thanks @te7ina-honey): the Java type resolver runs before the
unique-label stub rewire and parks imported-but-external types on their
FQN, and cross-file import resolution checks the package, so a local
class and a same-named external annotation (e.g. Spring @Component) no
longer collapse into one node. In-corpus annotation resolution unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2497 (thanks @sortakool): the shrink guard read the post-replace node
count and was skipped under prune_sources, so a broken partial re-extract
could silently destroy nodes. It now diffs the on-disk baseline by node
identity and refuses any loss from a source neither re-extracted nor
pruned this run, and reports replaced-node counts.
#2446 (thanks @AI-invest): prune_sources given absolute paths under a
non-standard layout now derives the scan root by suffix-matching stored
source paths, reports the accurate matched count, and warns instead of
saying 'already clean' when nothing matched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2486 (thanks @adminwat): normalize dict-shaped hyperedge members to ids
(or drop with a warning) so a malformed hyperedge can't abort a completed
merge with a TypeError.
#2484 (thanks @sortakool; approach from @oleksii-tumanov's #1691):
merge-graphs relabels hyperedge member ids and ids with the repo prefix,
unions both inputs' hyperedges instead of clobbering, and writes both
persistence slots.
#2485 (thanks @sortakool): build_from_json reads hyperedges from the
top-level and nested slots; a full validation wipeout is reported loudly.
#2490 (thanks @PapiScholz): the skill Step-5 flow passes curated
community_labels to to_json, so graph.json ships community_name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
graphify path and the MCP shortest_path tool now build a digraph from the
true _src/_tgt directions and respect edge direction by default, so a
returned path no longer traverses edges backwards. --undirected (CLI) /
undirected=true (MCP) opt out; no directed path is reported plainly
instead of silently returning a reversed one.
Thanks @luliaz0601.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Track C# receiver types per lexical declaration scope (byte ranges) and
resolve by the call's position, instead of a method-wide flat table that
poisoned a name on any None-typed binding. A typed static local-function
parameter now keeps resolving even when an out var reuses the name in the
enclosing body. Fixes a regression from #2346; #2299 cross-method
independence and field-conflict poisoning are unchanged.
Thanks @JensD-git for the bisect and repro.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#2411 (thanks @JensD-git): key the C# partial-class merge on assembly
(nearest ancestor .csproj/.fsproj/.vbproj) in addition to namespace and
name, so same-name partial classes in different assemblies stay distinct
while genuine partial halves within one project still merge. Fixes a
0.9.32 regression from #2332.
#2437/#2438 (thanks @aryanbonigala, builds on PR #2439): incremental
rebuilds no longer drop member-call and indirect_call edges from a
changed file into an unchanged target. Re-resolution now sees the
unchanged corpus (nodes, contains/method edges, and _callable markers,
which now persist to graph.json like _origin); edges to a genuinely
removed target are still evicted.
#2444/#2445 (thanks @Baziar, builds on PRs #2461/#2458): a
BrokenProcessPool triggers the sequential fallback instead of being
swallowed per future, a failed worker file is retried sequentially
rather than merged as empty, and a whole-pass AST failure on a fresh
build exits non-zero instead of writing a zero-node graph
(--allow-partial opts into a best-effort partial graph).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Print a one-time note about free early access to app.graphify.com at the
end of the install summary. Not shown on any other command.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
module Foo::Bar and nested module Foo; module Bar are canonicalized to
one fully-qualified label, and include/extend/prepend keep the full
constant path, so include Foo::Bar resolves. Mixin resolution is scoped
and lexical: a qualified external name (extend ActiveSupport::Concern)
no longer binds to a local module named Concern, while a genuine
in-corpus include Foo::Concern still resolves. Nested-declared classes
keep a last-segment index so typed-receiver calls (Processor.new) keep
resolving.
Adapts the approach from #1394 (thanks @FolatheDuckofDuckingburg) to the
post-refactor extractors/engine.py, with an added last-segment guard for
nested-class receiver resolution.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- C# receivers declared inline via out-var / is / case / switch-arm patterns
are now typed into the per-method table, so their member calls resolve (#2346).
- partial class halves across files now merge to one class node (new
_merge_csharp_partial_class_nodes pass, mirroring the Swift-extension merge),
so cross-half member calls resolve instead of splitting the class (#2332).
- Kotlin anonymous-object (object : Foo {}) members now get nodes, contains/
implements edges, and their calls resolve (#2347).
All in-corpus only, never a wrong edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node/edge ownership was keyed on source_file alone, but each file has an AST
tier and a semantic tier. A semantic-only re-extract deleted a doc's AST
headings, and a full _rebuild_code deleted document AST nodes. Add an
_is_ast_tier() predicate (with a source_location legacy fallback for the
unreliable _origin marker, #2334), backfill _origin on load so graphs
self-heal, make build_merge / merge_raw_extraction replace by (source_file,
tier) instead of source_file (coexist policy: an AST re-extract replaces only
AST nodes and keeps the semantic layer, and vice versa), and scope the
full-rebuild AST drop to sources actually regenerated (#2336) so a
semantic-backed doc keeps its AST layer. Deletion prune stays tier-blind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build_merge fix only covers the incremental path reached by
`graphify <path>`. `graphify update` rebuilds through _rebuild_code in
watch.py and never calls build_merge at all, so it still downgraded a
directed graph — which is the symptom named in the issue.
Two sites: the clustered path called build_from_json(result) with no
directed=, taking its directed=False default, and the --no-cluster path
built candidate_graph_data straight from the raw merged extraction, which
never carries a directed key, so the written file lost it entirely. Both
now inherit from existing_graph_data, defaulting to False when there is no
existing graph, matching build_merge's semantics.
_canonical_graph_for_compare normalizes a missing directed key to False so
an older graph.json without the key does not register as changed against a
candidate that now writes it explicitly.
build_merge took graph direction from its caller only, defaulting to
directed=False, and rebuilt with that default at build.py:1327 — discarding
the direction of the graph it had just loaded from graph_path. Since the
only call site (cli.py:3547) passes no directed=, every incremental
`graphify update` through the merge path silently returned an undirected
graph for a directed one.
_load_existing_graph now also returns the on-disk `directed` flag, parsed
from the JSON it already reads, and build_merge's parameter becomes
bool | None: None inherits that flag when a graph exists and falls back to
False when there is none, while an explicit True/False still wins.
deduplicate_entities() rebuilt its remap table by re-scanning the whole
unique_nodes list once per merged union-find component, costing
O(nodes x components) -- roughly 137M membership checks on a 50k-node
corpus with 2.5k merged components, about a third of dedup wall-clock.
Build an id -> (position, node) index once and slice each component out of
it instead: O(members log members) per component.
The index carries the enumeration position, not just the node, because
_pick_winner() selects via min(), which returns the first minimum -- so
ties (equal chunk-suffix status and equal id length) are resolved by list
order. Sorting the group by id instead changed 179 of 2358 survivors on a
tie-heavy corpus. Sorting by position reproduces unique_nodes order
exactly, leaving survivors and edges byte-identical.
Benchmark, median of 5 runs:
nodes before after speedup
5000 2.37s 2.29s 1.03x
20000 10.58s 9.70s 1.09x
50000 31.72s 21.92s 1.45x
Isolated remap loop at 50k nodes / 2.5k components: 6.838s -> 0.019s (370x).
Merge counts and output node counts are identical at every scale.
This does not address the pre-existing arrival-order dependence of the
component structure itself, which originates in the fuzzy-merge loop's
uf.find() short-circuit and is out of scope here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"coverage" was an unconditional _SKIP_DIRS entry and _is_noise_dir matches
directory names at any depth, so a repo where coverage is a legitimate
package name lost the whole package from the graph — no warning, no
skipped_sensitive entry, nothing in the report. The failure is quiet in
the worst way: the package's dependents survive, so queries keep
returning plausible neighbours while the package itself has no nodes.
The entry's comment cites Vitest/Istanbul/nyc HTML reports (#870), but
the pruning is language-agnostic, so it also removes Python/Go/Rust
packages that happen to be called coverage.
detect.py has been fixed twice for this exact shape — #1666 gated a bare
snapshots/ on real .snap evidence, #2058 gated env/.env/*_env on real
virtualenv markers — so this applies the established pattern rather than
a new mechanism. _has_coverage_artifacts() mirrors _has_venv_markers():
same OSError guard, same "cannot verify, keep a possibly-real code dir"
contract when no parent is available. Evidence is a file a coverage tool
actually writes (lcov.info, coverage-final.json, clover.xml,
coverage.xml, cobertura-coverage.xml, jacoco.xml, .coverage, index.html)
or an lcov-report/ / html-report/ subtree, covering lcov, nyc/Istanbul,
coverage.py, JaCoCo and Cobertura.
lcov-report stays unconditional: it has no false-positive class, so
gating it would add filesystem probes for no benefit — the same split
#1666 made between the unambiguous __snapshots__ and the bare snapshots.
Seven tests, all red on 4fe1109 and green with the fix:
- location, parametrized over the clustered and --no-cluster write sites
- the unchanged-topology early return, the third write site; asserts the
"No code-graph topology changes detected" line so the test cannot
silently drift onto a different branch and keep passing
- the invoking project's own manifest survives an update of another
project (the data-loss half)
- a relative target stores relative keys
- the resulting manifest is actually consumable by detect_incremental
- built_at_commit matches the target repo's HEAD, not the caller's
_rebuild_code computed the correct output dir as `out = watch_path /
_GRAPHIFY_OUT` and routed every artifact through it except the three
save_manifest calls, which omitted manifest_path= and fell back to
detect._MANIFEST_PATH — a module-import-time constant built from the
relative out_path("graphify-out/manifest.json"), so it resolved against
the process CWD. `graphify update <target>` therefore wrote the target's
manifest into the invoking project and left the target with none.
save_manifest also read-modify-writes that path and full-scan callers
pass scan_corpus (#1908 pruning), so the invoking project's own rows were
pruned as out-of-corpus and overwritten. Both projects lost their
incremental baseline, not just the target.
Anchor the manifest keys to watch_root at the same three calls. root=
previously followed project_root, which is CWD-derived for a relative
invocation, so `graphify update ../other` stored out-of-root files under
absolute keys and broke #777/#1964 portability. project_root itself is
left alone: the persisted graph deliberately rehomes source_file against
it across invocation styles.
Also pass cwd= to _git_head. Without it `git rev-parse HEAD` inherited
the caller's directory, stamping the invoking repo's commit into the
target's graph.json built_at_commit (cf. #2081).
`graphify query` returned the right nodes but dropped real edges between them.
Both traversals recorded an edge only when it discovered an unvisited
neighbour, so the result was a traversal tree rather than the induced subgraph
over the node set the query reports.
`_bfs` marks every seed visited before the loop starts, so an edge between two
seeds could never be recorded — the reported symptom, where both endpoints
render and the edge between them does not. It drops ordinary cross-edges and
hub-adjacent edges for the same reason.
`_dfs` needed the same completion for a narrower case. It appends on push
rather than on visit, so it already captured seed-to-seed and cross-edges; an
exhaustive search over every connected four- and five-node graph found no miss.
Its one gap is an edge between two non-seed hubs, where the hub guard means
neither endpoint is ever expanded and so neither records it.
The new `_complete_induced_edges` runs at the end of both. It scans only edges
incident to the visited set, so cost tracks the subgraph rather than the whole
graph. A visited hub is rescanned in full, which is unavoidable: a hub-to-hub
edge is exactly the case `_dfs` misses. Placing the pass inside the traversals
rather than at the call site binds it to the context-filtered `traversal_graph`
by construction, so a relation the user filtered out cannot reappear.
Dedup keys on the ordered pair for directed graphs and the unordered pair
otherwise: on a DiGraph `u->v` and `v->u` are distinct edges (mutual recursion,
circular imports) and collapsing them would drop a real one. Parallel edges on
a multigraph collapse to one entry, matching the renderer, which already shows
only the first. Self-loops are skipped, since no traversal ever recorded one
and surfacing recursion edges is a separate output change.
Traversal edges keep their discovery order; completions are appended after.
Closes#2323
A backend can emit {"id": 10} where the schema says {"id": "10"}. Every id
consumer downstream assumes str, so one numeric id aborted the whole run at
the final merge step, after the full extraction had already been paid for.
There are three crash sites, not the one in the report:
- dedup._pick_winner's _CHUNK_SUFFIX.search(n["id"]) raises
"expected string or bytes-like object, got 'int'" (the reported
traceback), but only when the node lands in a duplicate or fuzzy group
- build_from_json's sorted(node_set) raises "'<' not supported between
instances of 'str' and 'int'" for a lone node with nothing to dedup
- the same sort, reached via the fuzzy-group path
A str() cast at the reported line therefore does not fix the common case:
a single numeric-id node never reaches _pick_winner. Coerce at ingest
instead, in _coerce_non_string_ids, called from build() before dedup (which
keys on id) and from build_from_json (the direct entry used by cli.py,
watch.py and diagnostics.py, which never goes through build()). The nested
call on the build() path is an idempotent no-op.
Edge endpoints and hyperedge members are coerced alongside the nodes on
purpose: coercing node ids alone would renumber 10 to "10" and leave every
edge pointing at the vanished 10, trading a loud crash for a silently
disconnected graph. The legacy from/to aliases are included because dedup
reads them directly (#803).
Only bool-free numeric scalars are coerced. A None, list or dict id is left
for validate_extraction to report, since str(None) == "None" would fabricate
an id that no edge references.
Closes#2326
graphify path decided hop direction from the persisted source/target order,
so a link stored in flipped order (pre-#563 graphs, raw dumps, merge-driver
output) printed backwards; explain had the same defect, and the query/merge
load shims clobbered in-file _src/_tgt markers. All now honor _src/_tgt (the
build-side direction truth), falling back to arc order for markerless files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SQL references to a table defined in another file (e.g. prisma m2 -> m1) were
minted as node-less absolute-stem ids under the referencing file, leaking the
scan path and losing the FK. They now mint a sourceless bare-name stub (Go
#1402 pattern) that _rewire_unique_stub_nodes collapses onto the real table.
Also normalize identifiers (strip quotes, lowercase, bare-name alias) on both
table_nids writes and reference lookups, so a quoted definition ("public".
"users") resolves an unquoted reference (public.users) in-file instead of
stubbing — which also fixes pg_introspect's reconstructed-DDL views/FKs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The C# receiver-type table was per-FILE and poisoned a name on any
conflicting/untypable rebind anywhere in the file, so `var item = items[i]`
in one method silently deleted the true calls edge in another method where
the same name was a typed parameter (~2.3% of calls lost, per the reporter).
Ported C# to a per-method table mirroring the Java resolver (per-class field
scope + method params/locals, method-local poisoning only) and retired the
file-wide table. The namespace resolver, ambiguity bail, and inherits-chain
guards are unchanged, so the no-wrong-edge bar holds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_find_node` ranks matches but never reports that a tie was broken, so callers
taking `matches[0]` present one arbitrary file as the answer. Two workspaces
that each define `MetricsPort` put both nodes in the same `exact` tier,
separated only by `G.nodes()` iteration order — reorder the graph and the same
query answers with a different file, equally confidently.
`affected` already declines this via `resolve_seed()` returning None, and
`path` warns when the top two scores are within 10%. Only `explain` (and the
MCP `get_neighbors` tool, which shares the matcher) was silent.
Split `_find_node` into `_find_node_tiers` (logic unchanged, tiers exposed) plus
a flattening wrapper, and add `find_node_ambiguity()`, which returns rivals only
when the winning tier spans multiple source files. Matches within one file (a
file node plus its members) stay ordinary precedence and resolve as before.
`_disambiguate_file_node_labels` (#2032) already relabels colliding *file*
nodes; this covers the symbol case it does not reach.
Review feedback on #2296: the previous revision added Go's predeclared
names to _LANGUAGE_BUILTIN_GLOBALS, which ~11 languages consult through
engine.py and the cross-file pass, and whose check wraps the in-file
EXTRACTED branch as well as raw_calls. Two confirmed regressions:
* Rust normalizes 'Widget::new(3)' to the bare token 'new', so every
in-file 'Type::new()' edge disappeared. Rust keeps its own
_RUST_TRAIT_METHOD_BLOCKLIST, deliberately on the cross-file branch
only — this change follows that language-local pattern.
* Go 'h.append(v)' is a selector_expression call to a real method and
was dropped with it. On the 3x-ui corpus this cost a genuine
'systemMetrics.append(...)' -> '(*metricHistory).append' edge.
The filter now lives in extractors/go.py as _GO_PREDECLARED_FUNCS and
fires only when the callee node is a bare identifier, so selector calls
('h.append(v)', 'pkg.Delete(x)') and every other language are untouched.
Go raw_calls now carry language="go" and the shared pass gates on it,
mirroring the bash gate, as a backstop for Go raw_calls minted
elsewhere. The set is the Go spec's predeclared list in full: being
Go-local and bare-identifier-only makes 'len'/'max'/'min'/'print' safe
to include, and a spec boundary beats a hand-picked subset.
Remeasured on the same 466-file Go corpus:
upstream v8 16904 edges, 334 inbound to 'append'
previous rev 16571 edges, 1 inbound (332 phantom gone, but the
genuine selector call too)
this rev 16572 edges, 2 inbound (332 phantom gone, genuine
call restored)
Tests kept, plus three regressions: builtin 'append' must not bind
in-file (the branch a cross-file-only gate would miss), the Go selector
call must survive, and the in-file Rust 'Type::new()' edge must survive.
The last two fail on the previous revision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
graphify/extractors/go.py already consults _LANGUAGE_BUILTIN_GLOBALS
when resolving a callee, but the table covered JS/TS, Python and Swift
(#726, #2147) and never Go. Because the Go resolver looks the callee up
by bare name, an unexported method sharing a builtin's name absorbs
every builtin call in the repository.
Measured on a real 8.9k-node Go+TS codebase: a
'func (h *metricHistory) append(...)' method collected 330 phantom
inbound calls edges from the project's ordinary append(slice, x) calls,
which in turn invented twelve database-layer -> service-layer edges —
a layering violation absent from the source. After the fix, phantom
cross-file edges to builtin-named nodes drop from 325 to zero; the 8
that remain are all same-file (struct -> method, and real calls to that
method), and the user's method node is untouched.
Builtin types are deliberately not listed. Go conversions are
call-shaped too, but they produced no phantom edges on that corpus and
listing them would suppress genuine constructor-like calls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`_ANTIGRAVITY_WORKFLOW` is shared by the global and project-scoped installs,
which write SKILL.md to different locations (~/.gemini/config/skills vs
.agents/skills). The template named the global path unconditionally, so
`install --project --platform antigravity` (and `antigravity install
--project`) emitted a workflow pointing at a file that scope never writes -
a dangling reference on any machine without a prior global install.
Refer to the skill by name rather than by location, matching what every
block in always_on/ already does. Antigravity resolves it from the
`name: graphify` frontmatter in either scope, so no path interpolation is
needed and neither scope can dangle.
The existing tests only asserted the workflow file exists, which is why this
shipped; add coverage that the generated workflow names no skill path, in
both project and global scope.
mcp 2.0 removed the low-level decorator API the server was built on and
changed several models. _build_server now defines the four handlers as
plain coroutines and binds them at the end in a version-aware way: the
@server.list_tools()/... decorators on 1.x, the on_list_tools=/...
constructor callbacks (with explicit ListToolsResult/CallToolResult/
ListResourcesResult/ReadResourceResult wrapping) on 2.x. Model diffs
handled: Tool.inputSchema is input_schema on 2.x (constructor aliases
work on both, attribute access does not); Resource.uri is a plain str
on 2.x and rejects AnyUrl instances, so resource URIs are passed as
strings (1.x coerces them). The 2.x server also reports graphifyy's own
version in serverInfo. Extras relaxed from mcp>=1,<2 to mcp>=1,<3.
Verified: full stdio handshake (initialize / tools/list / tools/call /
resources/list / resources/read) and the tests in test_serve.py +
test_serve_http.py (145 passed) under BOTH mcp 1.29.0 and mcp 2.0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mcp 2.0 removed the low-level decorator API (Server.list_tools/call_tool/
list_resources/read_resource) that serve.py is built on, so an install
that resolved the unpinned extra to 2.0.0 crashed at server startup
(first at `from mcp.types import AnyUrl`, then at @server.list_tools()).
Pin mcp>=1,<2 in the `mcp` and `all` extras; import AnyUrl with a
pydantic fallback so that piece is already 2.x-ready. Verified with a
full stdio handshake (initialize / tools/list / tools/call) against the
installed graphify-mcp with mcp 1.29.0. Porting to the 2.x API is a
separate work item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
botocore max_attempts counts the initial call, so GRAPHIFY_MAX_RETRIES must
map to _resolve_max_retries() + 1 (a value of 6 -> 7 total attempts; 0 ->
1, i.e. no retry). Also stub botocore.config in the #2288 reasoning-model
test, which broke once #2283 added the botocore.config import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_parse_js_tree parsed .tsx with language_typescript(), so JSX misparsed and
error-recovery floated nested handlers to top level; the symbol-resolution
pass then emitted calls edges whose SOURCE was an absolute-stem id for a
caller that owns no node — a leak the 0.9.29 backstop (learns only from
nodes) can't see. Fix: use language_tsx() for .tsx; never emit a calls
use-edge from an unowned source (reattribute to the file node); and teach
the backstop stem-form prefixes so any node-less absolute-derived endpoint
canonicalizes. No node id or edge endpoint now carries the scan-root slug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>