Commit Graph
2044 Commits
Author SHA1 Message Date
ayushcodes10 32ad059ebc Add regression tests for the symbolless file warning
Covers a plain data literal getting named and marked as likely data,
a real function staying silent, no overlap with the existing syntax
error warning on a genuinely broken file, and the total size count
across more than one file. Confirmed to fail against pre fix code.
2026-10-02 23:31:10 +05:30
ayushcodes10 3b3ee82b1a Warn when a cleanly parsed code file yields no symbols
A code file that parses with zero errors but still produces nothing
beyond its own bare file node is usually a plain data literal the AST
extractor has nothing to model (a const array, a module export of
plain data), not a code file that legitimately has no symbols. Those
two cases looked identical in the output: the file silently entered
the graph as an isolated, zero edge node, and nothing reported it. A
real corpus measured in the issue had 88 such nodes covering roughly
40 percent of the corpus, with not one edge among them.

Add a complementary check next to the existing syntax error warning,
gated on the opposite condition (no parse_errors and no extractor
error) so the two can never double report the same file: a file
already explained by a genuine parse error or a missing optional
dependency is excluded here.

Fixes #3946.
2026-10-02 23:31:05 +05:30
safishamsi e10df08877 release: 0.9.74 v0.9.74 2026-10-02 17:20:19 +01:00
Ayushraj06-bit f99a21bac4 fix(build): keep a same-id hyperedge from an untouched file on update
build_merge carries forward the hyperedges of files that were not
re-extracted, but it skipped any carried hyperedge whose id also appeared
in the new chunks, whichever file the new one came from. Hyperedge ids
are chosen per extraction, so two documents can emit the same id: a full
build keeps both, yet re-extracting only one of them silently dropped the
other file's hyperedge.

Match a re-emission on (id, source_file) instead of id alone. The new
side's source_file is normalized the same way the carried side already
is, and an empty source_file counts as missing. A re-emission from the
same file, or a source-less hyperedge re-emitted without one, still
replaces the carried copy. merge_raw_extraction already replaces
hyperedges per source with no id-only check, so the two paths now agree.

Refs #3981

(cherry picked from commit 0c72e04c8a)
2026-10-02 17:10:08 +01:00
Alex Tumanov 4315c604d7 feat(extract): include Verilog header files
Register .vh with the existing Verilog detection and extraction paths,
including incremental and watch coverage.

Refs #3749

AI-Assisted-By: OpenAI Codex
(cherry picked from commit 3b74b8ad3d)
2026-10-02 17:10:08 +01:00
Yyunozor 038954a2e7 fix(markdown): resolve wikilinks to note names that contain a dot (#3904)
A wikilink such as [[note.en]], [[v1.2 release]] or [[Mr. Smith]] was
dropped: Path("note.en").suffix is ".en", so the target skipped the
extension-less .md completion and was rejected as a non-document link,
although note.en.md exists and is a node. The docstring already promised
that extension-less wikilink targets resolve to sibling .md.

For wikilinks only, a target whose suffix is not a document extension is
now retried as <name>.md through the same sibling-then-vault resolution,
and kept only when that document exists. A literal target that graphify
indexes itself keeps precedence: [[pic.png]] beside pic.png and
pic.png.md still yields no edge. [[missing.en]] and inline/reference
links keep their current behavior.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 3d281a674a)
2026-10-02 17:10:08 +01:00
Aditya kumar singh 81b0391aa2 fix(resolution): prefer source entry over built dist in workspace packages (#3834)
(cherry picked from commit 1dabb86b38)
2026-10-02 17:10:08 +01:00
shobhitagnihotri69 8e5649fe83 fix(cli): resolve hook-guard output paths instead of substring matching (#3959)
(cherry picked from commit 3fd280e1b2)
2026-10-02 17:10:07 +01:00
berce b676462c1e fix(extract): keep constructor calls that hit a same-file annotation stub
When a class is only referenced in a type annotation (Python) or a generic
argument (C#) earlier in the file, that reference mints a source-less stub
with the class name. A later `Parser()` / `new Widget()` in the same file
found the stub in the local lookup, was recognised as an external stub, and
was dropped: it was neither emitted as a local edge nor queued for
cross-file resolution. Since 0.9.62 these constructor edges were missing.

Queue those calls for cross-file resolution as well. That pass already
refuses source-less targets (#3156), so the stub itself still never becomes
a calls hub.

These calls did not bind to anything before, so a rescued call is marked
and only binds with evidence: an import of the target, or for C# the
namespace / `using` scope via CsharpNameResolver. Without that, a file that
imports `com.lib.Client` or uses `System.Collections.Generic` would bind
`new Client()` / `new List<int>()` to an unrelated same-named class.

For C#, `new X()` also drops `case_of` / `defines` targets from its
candidates, the same exclusion the C# type-definition index uses (#3815),
so it cannot bind to an enum member or property named X.

Fixes #3888

(cherry picked from commit e4a9270f45)
2026-10-02 17:10:07 +01:00
Liam McElhaney 01a687cb78 refactor(rust): keep the builtin-type set language-local
Move _RUST_BUILTIN_TYPES from extractors/base.py next to the Rust extractor,
matching _GO_PREDECLARED_FUNCS and _RUST_TRAIT_METHOD_BLOCKLIST. The shared
_LANGUAGE_BUILTIN_GLOBALS is consulted with no language gate, so a Rust-only
set belongs with the language that reads it; this also keeps base.py's public
surface unchanged.

(cherry picked from commit 2d766780bc)
2026-10-02 17:10:07 +01:00
Liam McElhaney 7f1d45dee2 fix(rust): stop prelude types becoming god nodes and label constructors
Rust's standard-library types were absent from _LANGUAGE_BUILTIN_GLOBALS, so
every Option/Vec/String/Result annotation was extracted as a domain type and
resolved to a few canonical nodes with corpus-wide degree (String led a
1,200-file workspace with ~1,700 edges).

Add a Rust-only _RUST_BUILTIN_TYPES set, kept separate so the same names still
resolve as user types in other languages, and skip those names in the Rust
type-reference and call collectors. The filter is definition-aware: a file that
defines its own `struct Result<T>` keeps its references.

A tuple-struct construction (`ClientId(id)`) targets a data definition, not a
callable, so it is now emitted as `references[constructor]` instead of `calls`
in both the in-file and cross-file paths; a real function call still emits
`calls`.

(cherry picked from commit 88cb2a8cd1)
2026-10-02 17:10:07 +01:00
Yyunozor 4babf7b173 fix(rust): keep a forward reference unresolved when its type id collides with another item (#3782)
Ids are casefolded, so `fn handle`, `const HANDLE`, `struct HANDLE` or an
`impl` for `HANDLE` share the id of `struct Handle`, and walk() keeps
whichever item comes first. The pre-scan registered the type id
regardless, so a reference placed above the declarations was bound to
that shared id and could end on the function or on the other item, where
v8's extractor left a sourceless stub. For the same reason a `HANDLE`
reference (a `type HANDLE` alias, or an imported name) placed above a
local `struct Handle` was bound to the struct.

Only pre-register a type when every file-level item behind its id is
that same type (`#[cfg]` twins count once), and match it by its exact
name. An `impl` block counts as the type it names, since walk() gives it
the node of its type text; methods keep impl-qualified ids and are not
counted. Otherwise the forward reference keeps its stub. A reference
placed after an item that already owns the id still resolves through
seen_ids, unchanged from v8.

Adds regression cases for a type colliding with a function (also in an
`extern` block), a const, a static, another type, an `impl` for another
type and a type alias, and controls where the id is not shared: another
function name, `#[cfg]` twins, an `impl` for the type itself, a method
named like the type, and a generic type only an `impl` names.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit 93fa872e2c)
2026-10-02 17:10:07 +01:00
Yyunozor 5c8df1566b fix(rust): resolve a forward-referenced type to its local declaration (#3782)
ensure_named_node only resolved names already in seen_ids, so a struct,
enum or trait used above its declaration in the same file got a
sourceless stub:

    fn before(s: &Sink) {}
    struct Sink { n: u32 }

The corpus-level rewire folds that stub back onto the real struct only
when the name is unique across the corpus. Once another file declares a
`Sink` as well, the reference stays on the stub, and `affected` or
`explain` on the local struct miss that user.

Pre-scan the struct/enum/trait names the file declares, as the Verilog,
SQL and Bash extractors already pre-scan their definitions, and let
ensure_named_node resolve against them, so a use above a declaration
resolves exactly as a use below it already does. Only these three items
are pre-registered: walk() never makes a node of a `type` alias or
`union`, so registering one would leave its references to be dropped by
the final dangling-edge filter. The scan never descends further than
walk(), so every registered id ends up as a node.

Adds regression coverage for a struct, enum and trait used above their
declarations, a type declared in an inline module, the two-file
ambiguous case (cold and warm cache), and the items that must not
resolve a forward reference (type alias, union, function body, const
and static initializers).

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
(cherry picked from commit d73c4adf59)
2026-10-02 17:10:07 +01:00
rajatnagda45 9e291b2c50 fix(cpp): treat a member-function declaration as a method, not a field
Inside a class body a `virtual double area() = 0;` (pure virtual), a
`virtual void draw();` and a plain prototype `int run();` parse as a
`field_declaration` whose declarator is a `function_declarator`. The field
branch recognised that shape only to skip emitting a type reference — it still
emitted a node for every declarator as a DATA MEMBER (a bare `area` label with
a `defines`/field edge).

So a pure-virtual interface class had no method nodes, only phantom fields; a
header-declared method and its out-of-line/overriding definition did not share
one node (`area` field vs `.area()` method); and a call to such a method
resolved to a field-shaped node.

Fix: in the field-declaration branch, emit a function_declarator with the same
shape a defined method uses — a `.name()` label, a `method` edge, and callable
— so the declaration and its definition collapse to one method node (parity
with the Java/TypeScript/Scala abstract-method contract). A genuine data member
still gets its `defines` field edge.

This corrects the label of every header-declared method, so a few existing
cross-file tests that asserted the old bare `bar`/`run` field label now assert
`.bar()`/`.run()`; the resolved call edges (source, target, confidence) are
unchanged, and the decl/def merge still produces exactly one node carrying the
definition site.

Added tests/test_cpp_method_declarations.py.

(cherry picked from commit 0038af9394)
2026-10-02 17:10:07 +01:00
rajatnagda45 b52c330ee6 fix(scala): extract deferred (abstract) method declarations
A bodyless `def area: Double` parses as `function_declaration`; a `def` WITH a
body is `function_definition`. Only the latter was a Scala function type, so
every deferred (abstract) method was dropped.

In Scala that is the common case, not an edge case: a `trait` used as an
interface is entirely deferred defs, so the whole contract vanished — the trait
had no method nodes, and a concrete method calling a deferred one
(`describe` -> `name()`) had no target to link to. The same held for an
`abstract class`'s abstract members.

Fix: add `function_declaration` to the Scala function types. A bodyless `def`
only occurs inside a `trait`/`abstract class` body, so it is always a method
declaration; its name sits on the same `name` field as `function_definition`.

Added tests/test_scala_abstract_methods.py: deferred methods (with and without
a parameter list) become nodes in both a trait and an abstract class, a pure
interface trait keeps every contract method, and a call to a deferred method
now resolves.

(cherry picked from commit 7c82507b5e)
2026-10-02 17:10:07 +01:00
rajatnagda45 699fab7793 fix(typescript): extract abstract method signatures
An abstract class's `abstract area(): number;` parses as
`abstract_method_signature`, which was not a TypeScript function type, so the
abstract method was dropped. An interface's `method_signature` has long been a
function type, so `interface D { draw(): void }` put `draw` in the graph — but
the identical contract on an `abstract class` did not.

The result: the base of an abstract class kept only its concrete methods, a
polymorphic `shape.area()` had no declaration to resolve to, and the overridden
contract had no node to carry it.

An abstract method is the same kind of contract as an interface method: a
subclass must implement it. Add `abstract_method_signature` to the TS function
types (inherited by the TSX config). Its name sits on the same `name` field as
`method_signature`, so no other change is needed. Abstract fields
(`public_field_definition`) are left as-is, matching concrete fields which are
not nodes either.

Added tests/test_ts_abstract_methods.py: the abstract method becomes a node,
a call through the abstract type resolves to it, and it reaches parity with the
interface method it mirrors.

(cherry picked from commit 2100098042)
2026-10-02 17:10:07 +01:00
rajatnagda45 7340b82812 fix(ruby): capture paren-less self-send calls
A receiver-less Ruby method call written without parentheses — the most
common call form in idiomatic Ruby — was dropped from the call graph:

    def run
      build      # a method call on self
    end

Tree-sitter parses that `build` as a bare `identifier`, structurally
identical to a local-variable read (`x`). The call-walk only inspected
`call` nodes, so `build(1)`, `self.build` and `build arg` all produced a
`calls` edge while the bare `build` produced nothing. A whole module of
mutually-calling helpers could end up with no internal edges at all.

The Ruby resolver already had a dedicated bare-call promotion path, but it
had nothing to promote because extraction never produced the call.

Fix: mirror Ruby's own disambiguation rule — a bare identifier is a send on
`self` unless a local or parameter of that name is in scope. `_ruby_local_names`
collects the bound names of a method body (parameters, assignments, block
params, `for` variables, `rescue` captures); a bare identifier not among them
is routed through the normal callee path as a non-member call, resolving
in-file (EXTRACTED) or becoming a raw_call the resolver can prove.

The collector is deliberately over-inclusive, so a missed binding only
suppresses a candidate call (fail-closed) rather than inventing a wrong edge.
A receiver-position send (`logger.info` where `logger` is itself a send) is
left for a follow-up and stays unresolved rather than guessed.

Added tests/test_ruby_bare_self_send_calls.py covering the self-send, the
cross-module and mixed-in cases, and the negatives (a local, a parameter and
a block parameter that shadow a method name must NOT become calls).

(cherry picked from commit c95b11c8e6)
2026-10-02 17:10:07 +01:00
Cintu07 18b1498009 fix(csharp): resolve null-conditional member calls (#3797)
_window?.Refresh() parses as a conditional_access_expression, not a
member_access_expression, so the invocation branch fell through to the
raw-text fallback. That split "_window?.Refresh" on the dot and recorded
the receiver as "_window?", which no receiver table types, so the call
was dropped with no unresolved entry.

Read both shapes through one helper that returns the name and receiver
nodes, and use it for the call-site generic-argument walk as well, so
recv?.Do<T>() keeps its references edges. The cast and partial-class
receivers from #3797 are separate and not touched here.

(cherry picked from commit e6e39a9aa5)
2026-10-02 16:50:45 +01:00
rajatnagda45 de3289fb74 fix(erlang): record local fun references (fun Name/Arity)
The body walker only visited `call` nodes, so a `fun Name/Arity`
reference — the idiomatic way to pass a local function to lists:map,
spawn, etc. — was dropped from the call graph. It parses as an
internal_fun node (name in the `fun` field, arity in the `arity` field).

Resolve it by exact name and arity and emit an indirect_call edge: the
function is handed off to be invoked, not called on the spot. Remote
references (fun mod:f/2, external_fun) name an explicit module and belong
to the cross-file remote-call boundary; they stay unresolved here.

Fixes #3993

(cherry picked from commit 1748e9bf0a)
2026-10-02 16:50:45 +01:00
rajatnagda45 1e11bd6942 fix(lua): resolve colon-method calls (obj:method())
A colon call `obj:method()` parses as a method_index_expression whose
callee is in the `method` field and whose receiver is in the `table`
field, but _LUA_CONFIG read the absent `name` field, so the callee was
never captured and every colon call was dropped from the call graph.

Point the accessor at the right fields and, when the receiver is `self`,
rewrite the bare method name to its enclosing-table label (Animal:method)
so it resolves like any other colon-method definition. An arbitrary-object
receiver (other:m()) is left unqualified and deferred so it cannot bind to
an unrelated same-named bare function.

Fixes #3991

(cherry picked from commit 000b0775c0)
2026-10-02 16:50:45 +01:00
himanshupatro-334 fde17d04ea fix(kotlin): extract class literal references from annotation arguments (#3835)
(cherry picked from commit 8c4f102894)
2026-10-02 16:50:45 +01:00
safishamsiandClaude Opus 4.8 ef4450d9c2 docs(changelog): fold 11 more fixes into 0.9.73
Solidity free functions (#3906), VB.NET qualified calls (#3909), Astro
frontmatter-only AST pass (#3902), surprise bonus/reason alignment (#3934),
exclude-hubs stranded-neighbour (#3933), graph-DB push index (#3957),
label retry keep-named (#3956), stale-hook status (#3951), virtual-workspace
Cargo.toml skip (#3930), and symlinked-instructions install/uninstall
handling (#3950/#3953).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.73
2026-09-30 20:52:57 +01:00
berce c228df205e fix(install): name the symlink target when writing CLAUDE.md (#3805)
When ~/.claude/CLAUDE.md is a symlink, the registration block is written
into the file it points to, but install only printed the link path, so a
rules file elsewhere changed without any mention. Print `link -> target`
for symlinks on install and on the project-scoped removal path.

(cherry picked from commit f05abb2b25)
2026-09-30 20:41:10 +01:00
berce b30a40a171 fix(install): keep a symlinked instructions file on uninstall
When removing the graphify section leaves an instructions file empty,
uninstall deletes it. If the file is a symlink, only the link was deleted
and the graphify section stayed in the file it points to. Now uninstall
writes through the symlink instead, so the target is cleaned and the
link stays.

Applies to .claude/CLAUDE.md, CLAUDE.md, GEMINI.md, AGENTS.md,
CODEBUDDY.md and .github/copilot-instructions.md.

(cherry picked from commit cf260b5163)
2026-09-30 20:41:10 +01:00
Aditya kumar singh c5c4ee6bfd fix(manifest): treat virtual-workspace-root Cargo.toml as skipped by design (#3910)
(cherry picked from commit 89b4e4e147)
2026-09-30 20:41:10 +01:00
berce 81bb5280c0 fix(hooks): report hooks written by an older release as out of date (#3771)
Upgrading the package never touches hooks already on disk, so repos
initialised before a hook fix (such as the #2253 console-window launcher)
keep running the old script while `graphify hook status` says
"installed". Status now compares the installed graphify block with the
current template, ignoring the two per-install values (the pinned
interpreter and the viz limit export), and tells the user to run
`graphify hook install`, which already rewrites the block in place.

(cherry picked from commit 2e43d53ffa)
2026-09-30 20:41:10 +01:00
VikramLex 18d834f771 fix(label): keep labels already named when a nested retry fails to parse
_label_batch_with_retry retried the ids a truncated reply left out (#3671)
inside the try that guards the batch's own parse. A retry that still would
not parse therefore landed in this batch's except branch, which re-split the
whole batch, asking the backend again for ids it had already named, and when
that failed the same way the error discarded every name of the batch, the
ones already parsed included. Likewise, once a parse failure split a batch,
one half failing at the base case discarded the names the other half got.

Parse the batch's own reply inside the try, then retry the missing ids (all
of them after a parse failure, the plain #1278 split) outside it, one half at
a time: a half that still won't parse is recorded and the other half still
runs. A batch re-raises only when none of it could be named, so
label_communities still skips a batch that yields nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 24055dabfc)
2026-09-30 20:41:10 +01:00
Yi-111-a bab648b0c3 fix(export): index (label, id) before the graph DB push upsert loop
`push_to_falkordb` and `push_to_neo4j` upsert every node with
`MERGE (n:<Label> {id: $id}) SET n += $props`, which matches on (label, id).
Neither path created an index on that pattern first, so the engine had to
scan every node already carrying the label to decide whether the node is
already there. Each upsert then costs O(nodes with that label) and a full push
degrades to O(n^2): on the graph in #3804 (167,428 nodes / 209,215 edges
across 6 labels) throughput collapsed from ~10,000 node upserts/60s to
~100 upserts/300s partway through, with the database at ~97% CPU.

Create one index per distinct label up front, before the upsert loop. The
label set is derived exactly as the push loops derive it, so every label a
MERGE matches against is covered. Neo4j uses IF NOT EXISTS to stay idempotent
across re-pushes; FalkorDB's CREATE INDEX has no such clause, so a re-push
against an already-indexed graph is tolerated rather than failing an
otherwise valid push.

Refs #3804

Co-Authored-By: opencode space-bunny-free <noreply@opencode.ai>
(cherry picked from commit a3275e2c97)
2026-09-30 20:41:10 +01:00
neo1777 480115033d fix(cluster): keep hub-only neighbours with their hub under --exclude-hubs
With exclude_hubs_percentile set, excluded hubs are removed from the
graph that gets partitioned. A node whose only neighbours are hubs (e.g.
a local symbol of a hub file, or a dependency entry of a manifest) is
then isolated in that subgraph and came back as a singleton community,
while the hub itself was reattached by majority vote. Every such node
ended up cut off from the only node it connects to, and the community
count inflated with singletons.

Such nodes are now held out of the partition and placed after the hubs
are reattached, with the same majority vote the hubs use; since all of
their neighbours are hubs and every hub is placed by then, the vote
always has a winner. A self-loop does not count as a neighbour. True
isolates, and nodes whose only edge is a self-loop, are unchanged.

Follow-up to #919, where --exclude-hubs was introduced. The cluster()
docstring now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 39a24a0626)
2026-09-30 20:41:10 +01:00
Kevin f9a6893844 fix(extract): parse only Astro frontmatter and scripts in the AST pass
extract_astro fed the whole .astro file to the JS grammar. The template is
not JS, so every page reported a syntax error (usually at line 1-2) and
symbols tree-sitter could not recover past the template were dropped.

Blank everything outside the frontmatter and JS <script> bodies (keeping
newlines so locations still match, as _vue_mask_non_script does) and parse
with the TS grammar, Astro's default. Scripts with a non-JS type
(application/ld+json, ...) are blanked too. The existing regex import
rescue is unchanged.

Fixes #2788. Covers the unmasked-script half of #3337.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fe07657fe1)
2026-09-30 20:41:10 +01:00
neo1777 a20c272d46 fix(analyze): make the cross-repo/directory surprise bonus match its reason
_top_level_dir() returned the whole path when it had no "/", so two
files at the scan root (a flat folder of notes, or README.md and
setup.py) got different "top-level dirs". Every such cross-file edge
(other than suppressed INFERRED calls/uses) received +2 and the reason
"connects across different repos/directories" in surprising_connections,
which is false.

- A path without "/" now maps to the scan root ("."). "." rather than ""
  keeps root files distinct from an absolute path outside the scan root,
  whose first component is "".
- Backslashes and a leading "./" are normalized first: build_from_json
  converts backslashes but keeps "./", and serve.py's _load_graph() and
  API callers can skip build_from_json altogether.
- The comparison key is (node "repo", top-level dir). merge-graphs and
  global add keep source_file repo-relative and tag nodes with "repo",
  so two repos can share directory names; single-repo graphs have no
  "repo" and are unaffected. On merged graphs a cross-repo edge now gets
  the bonus whenever the repos differ; before, it depended on whether
  the top-level directory names happened to differ.

Tests: the flat case (fails on d6eaa8a), merged graphs, backslash and
"./" paths, plus pins for what must not change (root vs subdirectory,
root vs out-of-root absolute path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 05d6410535)
2026-09-30 20:41:10 +01:00
rajatnagda45 4dc58d17a7 fix(vbnet): resolve type/module-qualified calls to their target method
The invocation resolver only fired for self/unqualified receivers (Me, the
enclosing type, or a bare name). A call qualified by a type or module name —
Helpers.Log(), the idiomatic way to reach a shared Module Sub — matched
nothing and produced no edge at all, so the caller was disconnected from the
method it invoked.

Map each declared type/module's short name to its owner key, and resolve a
qualified call against the receiver's owner (falling back to raw_calls for
cross-file partial-type merging). A call through a value receiver (a local
variable) still has no known owner and is left unresolved rather than
guessed (fail-closed).

(cherry picked from commit 1297c18f50)
2026-09-30 20:41:10 +01:00
rajatnagda45 85390c83a4 fix(solidity): extract file-level free functions and their calls
Free (file-level) functions have been valid Solidity since 0.7, but the
extractor only descended into named type declarations (contract, library,
interface, abstract contract). A function declared at source-unit scope —
and every call made from its body — was dropped entirely, so a file of
free utility functions produced no nodes at all.

Walk source-unit function_definition nodes, emit each as a file-contained
callable node, and resolve an unambiguous same-file free-function call to a
calls edge. Member calls (Lib.f()) are parked as raw_calls for cross-file
resolution; calls that cross into contract scope are left unresolved rather
than guessed (fail-closed).

(cherry picked from commit d2e56e932f)
2026-09-30 20:41:10 +01:00
safishamsiandClaude Opus 4.8 29eaf12174 release: 0.9.73
Rust/Zig/C++/Scala enum members as case_of nodes (#3938/#3940/#3939/#3937),
Java inherited + super call resolution (#3932), and a warn-once on
semantic file truncation (#3923).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-30 20:30:54 +01:00
rajatnagda45 5bb8a61140 fix(cpp): extract enum / enum class specifiers and their enumerators
`enum_specifier` was missing from the C++ class_types, so an `enum` or
`enum class` produced no nodes at all: the enum type and every enumerator
vanished from the graph, even though struct/class specifiers right next to
them were extracted normally.

Add `enum_specifier` to the C++ class_types — its name and body sit on the
same `name`/`body` fields as struct_specifier (type_identifier +
enumerator_list) so the type gets a node and its body is walked — and add
`_cpp_extra_walk` to emit a node plus a `case_of` edge per `enumerator`.
Anonymous enums (`typedef enum { A, B } Flag`) carry no type name and are
skipped by the existing name guard rather than emitting a nameless node.
This is the C++ parity of Java's enum_constant (#1719), Swift, and Scala.

(cherry picked from commit 9cde6c2a7f)
2026-09-30 20:16:06 +01:00
rajatnagda45 35207e9b4b fix(scala): extract Scala 3 enum definitions, their cases and methods
`enum_definition` was missing from the Scala class_types, so a Scala 3
`enum` produced no nodes at all: the enum type, its cases, and any methods
declared in its body all vanished from the graph, and references to the
enum resolved to nothing.

Add `enum_definition` to the Scala class_types and `enum_body` to the
body fallbacks so the type gets a node and its body is walked, and add
`_scala_extra_walk` to emit a node plus a `case_of` edge per case. Cases
nest under an `enum_case_definitions` wrapper as `simple_enum_case`
(`case Red`) or `full_enum_case` (`case Add(x: Int)`); both carry the case
name as an `identifier` child. This is the Scala parity of Java's
enum_constant (#1719), Kotlin's enum_entry (#1738), and Swift.

Case constructor parameter types and per-case `extends` arguments are not
linked; the case node and its case_of edge are the fix for the dropped
members.

(cherry picked from commit 175af52419)
2026-09-30 20:10:34 +01:00
kclamano b507207a1a fix(llm): warn once when file exceeds _FILE_CHAR_CAP in _read_files (#3773)
(cherry picked from commit 5f0ae1f784)
2026-09-30 20:10:34 +01:00
Jan Mohr 03ac9071fd fix(java): resolve calls to inherited methods and super
Member calls were only matched against the receiver's own type, so
this.start(), super.close() or worker.start() got no edge when the method
lives in a superclass. Walk up the extends chain and take the first
declaration. No edge if the chain leaves the project.

Fixes #3931

(cherry picked from commit a2da367fc7)
2026-09-30 20:10:34 +01:00
rajatnagda45 244f777323 fix(zig): emit a node per enum member with a case_of edge
The enum body walk only emitted an enum's methods; its members — the
`container_field` nodes (`red`, `north = 0`) — were dropped, so an
`enum { red, green, blue }` produced a node for the enum with no members at
all, leaving it a leaf.

Emit a node plus a `case_of` edge per enum member. The handler is gated on
the `enum_declaration` parent so struct and union fields, which share the
`container_field` node shape, stay untouched. This brings Zig to parity with
the other languages that have enums: Java's enum_constant (#1719), Swift,
Scala, and Rust.

(cherry picked from commit 1e44311e22)
2026-09-30 20:10:34 +01:00
rajatnagda45 b8ee397721 fix(rust): emit a node per enum variant with a case_of edge
The enum_item handler walked each variant only to collect its payload type
references (`Click(Logger)` -> Logger), but never emitted a node for the
variant itself. So an `enum Shape { Circle, Square, Empty }` produced a node
for Shape with no members at all — the enum was a leaf and its variants were
absent from the graph.

Emit a node plus a `case_of` edge per enum_variant, keyed on the variant's
`identifier` child, alongside the existing payload-type walk. This brings
Rust to parity with every other language that has enums: Java's enum_constant
(#1719), Kotlin's enum_entry (#1738), Swift, and Scala.

(cherry picked from commit 13903a8ac3)
2026-09-30 20:10:34 +01:00
safishamsiandClaude Opus 4.8 1cd9a36c0c docs(changelog): note auto-refresh of stale skills (#3895) in 0.9.72
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.72
2026-09-29 14:59:37 +01:00
berce 8ae9878465 feat(cli): refresh stale skills after a package upgrade (#1805)
After `uv tool upgrade graphifyy` or `pip install -U graphifyy`, the
installed skills stay on the old version. `graphify install` only fixes
the platform it detects, so codex, opencode, gemini and the others keep
running outdated instructions until someone reinstalls each one by hand.

The CLI now checks the version stamp of every user-scope skill on its
first run after an upgrade, `graphify update` included, and re-copies
the ones that are older than the package. This is on by default and can
be turned off with GRAPHIFY_NO_AUTO_REFRESH=1.

It is deliberately conservative about what it touches:

- only skills graphify installed itself (stamp and SKILL.md both present)
- never a skill newer than the package, so nothing gets downgraded (#1568)
- never a directory that two installers write to, such as copilot and
  `graphify vscode install`, or two platform dirs symlinked to the same
  place, because the stamp can't say which variant lives there
- a locally edited SKILL.md is kept as SKILL.md.bak (#3144)

Install, uninstall and the refresh take the same lock per skill
directory, so two processes never write through each other's fixed
.tmp staging names, and an uninstall can't race a refresh that would
put the skill back. It is an OS file lock (flock on POSIX,
msvcrt.locking on Windows). The OS releases it however the holder
exits, so a crash never leaves a directory locked and there is no
staleness timeout to tune. The lock file lives under
~/.cache/graphify/skill-locks/ rather than in the skill directory, so a
project install leaves nothing behind in the repository. Install and
uninstall wait for the lock; the refresh skips a directory another
process holds and reads the stamp again once it has the lock. Where the
file system can't lock, install and uninstall run unlocked as they did
before, and the refresh leaves the skill to the version warning.

Output goes to stderr, one line per refreshed directory, so --json and
the MCP stdio server are unaffected. A failed refresh never breaks the
command the user actually ran: the stamp stays old and the existing
version warning still points to the manual fix.

While here, the version warning now also checks the gemini skill, which
it used to miss because gemini is not in _PLATFORM_CONFIG, and the vscode
skill path moved into _vscode_skill_destination() so install, uninstall
and the refresh share it. The README lists the new variable and mentions
the refresh in the version-mismatch troubleshooting entry.

(cherry picked from commit 50ebe642af)
2026-09-29 14:37:10 +01:00
safishamsiandClaude Opus 4.8 e40f5c44fc docs(changelog): add the 2026-09-29 batch to the unreleased 0.9.72 section
HTML node-info field fix (#3918), Kotlin annotated-inferred-property
crash (#3915/#3899), SQL DDL-before-DO (#3900), Razor @functions (#3908),
Blade @extends (#3907), and imported-module resolution (#3898).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-29 14:34:41 +01:00
harshaygadekarandClaude Opus 4.8 e34c558035 test(kotlin): add #3899's annotated-inferred-property regression test
The code fix landed via #3915 (byte-identical hoist). Keep @harshaygadekar's
independent regression test for #3884 (found simultaneously) for coverage
and contributor credit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-29 13:51:37 +01:00
nothariharan af153f85ea fix(kotlin): keep annotated inferred properties in the graph
An annotation on a class property with no explicit type never assigned
`line`, so the annotation loop raised UnboundLocalError and _safe_extract
dropped the whole file. Set `line` before the type-node check so the file
and its attribute edge both survive.

Fixes #3884

Co-Authored-By: Grok 4.7 <noreply@x.ai>
(cherry picked from commit 03ba8cc9a3)
2026-09-29 13:49:39 +01:00
harshaygadekar 95c498f967 fix(extract): ignore contained symbols when resolving imported modules (#3887)
In _resolve_python_member_calls, candidate imported modules are gathered from imported_by_filenode by checking 	 in contains_children. Following #3410, functions enclosing a nested def also have contains edges to their nested functions. When a caller imports both a module and a function from that module (where the function contains a nested def), both the module and the function matched the candidate filter because the function's _module_stem_key evaluates to the stem of its source file. The resulting len(mods) == 2 triggered the ambiguity guard and dropped all module.func() calls from that file.

Exclude contained symbols by verifying 	 not in file_of_node, which is already populated for all containment targets and excludes top-level file/module nodes.

(cherry picked from commit 542e4869ba)
2026-09-29 13:49:39 +01:00
rajatnagda45 494c08452c fix(blade): link a view to the layout it @extends
The Blade extractor recognised @include, <livewire:> components, and
wire:click bindings, but not @extends. Template inheritance is the primary
structural relationship in a Blade view — most page templates render inside
a layout via @extends('layouts.app') — so the child was left with no edge
to its parent layout in the graph.

Emit an 'extends' edge from the view to the named layout, mirroring the
existing @include handling (dotted view name -> path). Adds the first test
coverage for the Blade extractor.

(cherry picked from commit efeeeefa53)
2026-09-29 13:49:39 +01:00
rajatnagda45 4f6a4efd1f fix(razor): extract methods from @functions blocks, not just @code
@code is the Blazor (.razor) spelling of an embedded C# member block;
@functions is the equivalent Razor Pages / MVC (.cshtml) spelling. Both
compile to class members, and this extractor serves both file types, but
only @code was matched. A .cshtml page that declared its methods inside an
@functions block lost every one of them.

Match @functions alongside @code in the block regex; the existing
brace-balanced body scan and method extraction are reused unchanged.

(cherry picked from commit e3693430b3)
2026-09-29 13:49:39 +01:00
berce 452e7b7da2 fix(sql): walk block nodes so DDL before a DO block is extracted
With a bare `BEGIN;` transaction, a later `DO $$ ... END $$;` makes
tree-sitter-sql read everything from BEGIN up to the DO body's END as a
single block node. The top-level dispatch had no branch for `block`, so
every CREATE TABLE inside it was silently dropped. Walk block nodes the
same way transaction nodes are walked.

Fixes #3886

(cherry picked from commit 8f47001d5b)
2026-09-29 13:49:39 +01:00
himanshupatro-334 3394d10ccf fix: correct node info field names
(cherry picked from commit 390264da80)
2026-09-29 13:49:39 +01:00