Commit Graph
1352 Commits
Author SHA1 Message Date
safishamsiandClaude Opus 4.8 22c41c1b23 fix(swift): cross-file extension no longer drops static/singleton calls (#2538)
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>
2026-08-07 22:49:25 +01:00
safishamsiandClaude Opus 4.8 6ba0868228 fix(cli): surface four silent success-exit failures (#2534)
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>
2026-08-07 22:49:25 +01:00
safishamsiandClaude Opus 4.8 9f25a3aaa1 fix(callflow,query): direction-aware callflow; drop relational-verb seed pollution; bump to 0.9.35
#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>
v0.9.35
2026-08-06 17:29:18 +01:00
safishamsiandClaude Opus 4.8 2320f2adc1 fix(update,java): prune newly-ignored files; stop external-annotation/local-class conflation (#2495, #2504)
#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>
2026-08-06 17:29:18 +01:00
safishamsiandClaude Opus 4.8 b8d609732c fix(build_merge): revive the #479 shrink guard and fix absolute-path prune (#2497, #2446)
#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>
2026-08-06 17:29:18 +01:00
safishamsiandClaude Opus 4.8 07b9143d4b fix(hyperedge,skill): merge/load hyperedge integrity + community labels; bump to 0.9.34
#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>
v0.9.34
2026-08-05 22:08:29 +01:00
safishamsiandClaude Opus 4.8 94ebee18b7 fix(path): respect edge direction by default in path and shortest_path (#2487)
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>
2026-08-05 22:08:29 +01:00
safishamsiandClaude Opus 4.8 e300587439 fix(csharp): scope receiver types per declaration so an untypeable rebind can't drop a true call (#2472)
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>
2026-08-05 22:08:29 +01:00
safishamsiandClaude Opus 4.8 4e7e6b1f7e fix(extract,watch): C# assembly-aware partial merge, incremental edge preservation, extract crash recovery; bump to 0.9.33
#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>
v0.9.33
2026-08-05 01:41:12 +01:00
safishamsiandClaude Opus 4.8 a426d63414 feat(install): point users to the hosted platform after install
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>
2026-08-05 01:41:12 +01:00
safishamsiandClaude Opus 4.8 00efd6e796 fix(ruby): resolve compact/nested mixin targets, prevent phantom concern hub (#2302)
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>
v0.9.32
2026-08-01 15:36:17 +01:00
safishamsiandClaude Opus 4.8 7db00fcc1a chore: bump to 0.9.32; changelog for #2333/#2334/#2336/#2342/#2326/#2323/#2316/#2339/#2273/#2346/#2332/#2347/#2328
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 13:07:07 +01:00
safishamsiandClaude Opus 4.8 4a9613f726 fix(extract): C# inline-declared + partial-class receivers, Kotlin anonymous-object members (#2346, #2332, #2347)
- 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>
2026-08-01 12:21:04 +01:00
safishamsiandClaude Opus 4.8 97656fae2d fix(build,watch): tier-aware merge so re-extract keeps the other layer (#2333, #2334, #2336)
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>
2026-08-01 12:21:04 +01:00
Rishet Mehra c262478792 fix(watch): inherit the directed flag on the graphify update rebuild path (#2342)
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.
2026-08-01 11:36:30 +01:00
Rishet Mehra e8436b411e fix(build): inherit the on-disk directed flag in build_merge (#2342)
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.
2026-08-01 11:36:30 +01:00
stupidprogrammer4andClaude Opus 5 0c01da1932 perf(dedup): remove O(nodes x components) scan from remap construction
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>
2026-08-01 11:36:30 +01:00
oleksii-tumanov 1b93c08697 fix(detect): prune only configured output path 2026-08-01 11:36:30 +01:00
Manoj21k 1cdbf48c57 fix(detect): gate coverage/ pruning on report artefacts (#2339)
"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.
2026-08-01 11:36:30 +01:00
Rishet Mehra 1a7be303f5 test(watch): cover manifest location, anchor and commit provenance (#2316)
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
2026-08-01 11:36:30 +01:00
Rishet Mehra 144a3ff8fb fix(watch): write manifest.json to the target's graphify-out (#2316)
_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).
2026-08-01 11:36:30 +01:00
Rishet Mehra 875c7df8f6 fix(query): render every edge between visited nodes, not just traversal edges
`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
2026-08-01 11:36:30 +01:00
Rishet Mehra a2dc3d9ce2 fix(build): coerce non-string node ids so a numeric id cannot abort the build
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
2026-08-01 11:36:30 +01:00
safishamsiandClaude Opus 4.8 3b243d644e docs(readme): early-access CTA at the top + refresh the waitlist line to app.graphify.com
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 01:52:47 +01:00
safishamsiandClaude Opus 4.8 4fe11092cc chore(release): 0.9.31
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.31
2026-07-30 19:15:26 +01:00
safishamsiandClaude Opus 4.8 5d1d1031f2 chore: bump to 0.9.31; changelog for #2308/#2299/#2324/#2309/#2266/#2313/#2233/#2319
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 18:44:09 +01:00
safishamsiandClaude Opus 4.8 50be9dcd36 fix(cli): recover edge direction from _src/_tgt in path and explain (#2309)
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>
2026-07-30 18:38:58 +01:00
safishamsiandClaude Opus 4.8 c8163d8338 fix(sql): sourceless stubs + identifier normalization for cross-file table references (#2324)
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>
2026-07-30 18:38:58 +01:00
safishamsiandClaude Opus 4.8 f99f8d70c8 fix(csharp): per-method receiver typing so cross-method name reuse stops dropping true calls edges (#2299)
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>
2026-07-30 18:38:58 +01:00
0bLoM 2ca565ac1a fix: explain no longer resolves an ambiguous name to an arbitrary file
`_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.
2026-07-30 17:13:48 +01:00
PathGaoandClaude Opus 5 3868434942 fix(go): scope the predeclared-function filter to Go bare identifiers
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>
2026-07-30 17:13:48 +01:00
PathGaoandClaude Fable 5 773250ad31 fix(go): filter Go predeclared functions from call-target resolution
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>
2026-07-30 17:13:48 +01:00
oleksii-tumanov d7cb5b1b1c fix(extract): emit nodes for exported scalar bindings 2026-07-30 17:13:48 +01:00
MalikHaroonKhokhar bdf62ce275 fix(install): stop hardcoding the global skill path in the Antigravity workflow (#2319)
`_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.
2026-07-30 17:13:48 +01:00
Nicola AvanciniandClaude Fable 5 b1613b3083 chore: re-lock uv.lock for the mcp>=1,<3 extra specifier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 17:13:48 +01:00
Nicola AvanciniandClaude Fable 5 576d5cbb61 feat(serve): dual-compat with MCP SDK 1.x and 2.x
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>
2026-07-30 17:13:48 +01:00
Nicola AvanciniandClaude Fable 5 50008cdf36 fix(serve): cap the mcp extra below 2.0; AnyUrl fallback for the 2.x SDK
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>
2026-07-30 17:13:48 +01:00
safishamsiandClaude Opus 4.8 ecfcd160d5 chore(release): 0.9.30
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.30
2026-07-29 17:34:24 +01:00
safishamsiandClaude Opus 4.8 1c6b3dbc58 chore: fold the batch into a single 0.9.30 release; changelog for #2262/#2257/#2287/#2284/#2261/#2268
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 17:27:40 +01:00
safishamsiandClaude Opus 4.8 ffc6dc0207 fix(llm): correct bedrock max_attempts semantics + stub botocore.config in the reasoning test (follow-up to #2283/#2288)
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>
2026-07-29 17:25:08 +01:00
safishamsiandClaude Opus 4.8 3c332ba72a fix(extract): parse .tsx with the TSX grammar so symbol-resolution stops leaking absolute-slug ids (#2262)
_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>
2026-07-29 17:25:08 +01:00
Kartik Gupta b4865ffcbf fix(serve): bound multi-project graph contexts (#2268) 2026-07-29 17:02:00 +01:00
himanshupatro-334 6d6b674b20 fix: preserve edge direction in merge-graphs 2026-07-29 17:02:00 +01:00
Zhi Yan Liu a428378ad3 fix(llm): honor GRAPHIFY_API_TIMEOUT in the bedrock backend
The two bedrock-runtime clients (primary extraction in _call_bedrock and
the secondary dispatch path in _call_llm) were built with no botocore
config, so Converse used botocore's 60s default read timeout and ignored
GRAPHIFY_API_TIMEOUT / --api-timeout entirely. A long opus-class
generation then died with "Read timeout on endpoint URL" no matter how
high the timeout was set.

Both client constructions now pass a botocore.config.Config wiring
read_timeout to _resolve_api_timeout() (default 600s), a 10s
connect_timeout, and retries from _resolve_max_retries() in adaptive
mode. This mirrors the fixes that closed the same gap for the claude-cli
subprocess (#1112/#1111) and the secondary LLM dispatch path (#1442) --
bedrock was the last cloud backend still ignoring the knob.

Also updates the README env-var row, which listed the timeout as
applying to HTTP/claude-cli/Anthropic only, and the _fake_boto3 test
fixture to register botocore.config and capture the client config so the
new coverage can assert the timeout is wired.
2026-07-29 17:01:49 +01:00
Zhi Yan Liu d47f3ea152 fix(llm): read the first text block of a bedrock Converse response
Converse returns output.message.content as a list of blocks and does not
promise a text block is first. Reasoning-capable models emit a
reasoningContent block ahead of the answer, and toolUse or future block
types can precede it too, but both bedrock call sites indexed position 0:

    content", [{}])[0].get("text", "{}")

For those models the default was returned on every call, so _parse_llm_json
saw an empty object, _response_is_hollow reported a hollow result,
finish_reason was rewritten to "length", and the adaptive retry bisected the
chunk. Splitting could not converge because the position assumption fails
identically at every chunk size, and raising GRAPHIFY_MAX_OUTPUT_TOKENS did
nothing because output length was never the constraint. stopReason on those
responses was end_turn, i.e. the model had answered correctly.

Selection now keys on the block's shape rather than its position, at both
_call_bedrock and the bedrock branch of _call_llm. A response whose first
block is already text -- every non-reasoning model today -- is unaffected.

On a 48-document corpus the hollow warnings and the bisection to the
recursion cap disappear, the 17 files previously reported as producing no
nodes are extracted, and output tokens drop from 217,538 to 53,274 as the
wasted retries stop.

Fixes #2287
2026-07-29 17:00:15 +01:00
Kaushik Samadder c5d432710f fix(cache): re-anchor cached ids so a warm hit can't replay another root (#2257)
Extractors mint node ids from the path STRING they are handed
(_make_id(str(path)), _file_node_id(path)), so an AST cache entry written
under root A embeds A's slug in every id and edge endpoint. save_cached
relativized only source_file, never the ids. Because extract()'s
id-remap / final-canonicalization passes key their rewrites off the
CURRENT run's paths, an A-derived id matches no key on a warm hit under
root B (a clone, a moved checkout, a second mount) and the stale
machine slug survives into graph.json. Distinct from #2231/#2243, which
fix producers on a cold run, and from #2199 (stat-index portability).

Entries are now stored root-anchored and re-anchored on read, the same
store-portable/re-anchor-on-load contract source_file (#777) and the
stat index (#2199) already use: _relativize_ids_in replaces the root's
contribution with a $graphify-root$ marker on write, _absolutize_ids_in
restores what the current run's extractor would mint on read. That is
the pre-remap form every downstream pass in extract() expects, so a
replay reproduces a cold run exactly and no other pass changed.

The anchor is derived per entry rather than assumed equal to the scan
root (normalize_id distributes over path joins), so a symlinked root or
relative inputs decompose exactly; only absolute root spellings may
anchor, or a relative root ("src") would rewrite an already-canonical
src_utils_foo into an absolute-derived id on the semantic path. The
walk covers the whole payload rather than a bucket list, since the id
form is self-identifying: that also reaches raw_calls[].caller_nid,
swift_extensions[].nid, edges[].target_file, bash_sources[].source_file
and *_type_table.path, the last three being resolution inputs that
would otherwise still point at root A. save_cached's deepcopy is now
unconditional; the old truthiness gate skipped it for a payload whose
only content lives outside nodes/edges, which would have let the
transform mutate the caller's dict and break cold-run remapping.

Pre-fix entries carry no marker and their content hash never changes,
so they cannot self-heal; they are swept when the release bumps the
version, since AST entries live under cache/ast/v{version}/.

Tests: extract a python/C/bash/markdown corpus under root A, copy the
tree and graphify-out to root B, extract under B on the warm cache, and
assert the run is genuinely warm (zero extractor calls), that no node id
or edge endpoint carries A's slug, that the on-disk entries hold neither
A's slug nor an absolute path, that a cold run still yields canonical
ids, and that warm and cold match exactly. The fixture avoids JS/TS on
purpose: those suffixes bypass the cache, which would make the warm
assertions vacuous.
2026-07-29 17:00:15 +01:00
safishamsiandClaude Opus 4.8 6d81a8290f fix(deps): cap mcp<2 so fresh installs of the MCP server work (#2277, #2279, #2291)
mcp 2.0.0 dropped mcp.types.AnyUrl and the Server decorator-registration
API graphify/serve.py relies on, so an unpinned resolve broke graphify-mcp
on every fresh graphifyy[mcp]/[all] install. Cap mcp>=1,<2 (resolves 1.29.0)
and starlette>=1.3.1,<2 in both extras; re-lock. Bump to 0.9.30. Porting to
the mcp 2.x API is a tracked follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 16:59:16 +01:00
safishamsiandClaude Opus 4.8 0b2bd938c4 chore(release): 0.9.29
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.29
2026-07-28 10:52:23 +01:00
safishamsiandClaude Opus 4.8 0411dede19 chore: bump to 0.9.29; changelog for #2231/#2243/#2251/#2241/#2253/#2206/#2184/#2205/#2052
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 10:21:25 +01:00
safishamsiandClaude Opus 4.8 829224acd0 fix(export)+test: all-dots Obsidian label falls back to unnamed; .env.example regression test (#2205, #2184)
Follow-ups on the cherry-picked #2242/#2232: an all-dots label ('...') no
longer produces an empty 'dot-' Obsidian stem (falls back to 'unnamed'),
and the .env.example carve-out gets the regression test it shipped without
(templates graphable, real .env still sensitive, secrets/.env.example still dropped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 10:18:01 +01:00