Commit Graph
438 Commits
Author SHA1 Message Date
Synvoya 53c769d23d fix(apex): emit extends edges for interface multiple inheritance
interface X extends A, B captured the parent list in iface_re group 2 but the handler only read group 1, so no inheritance edge was emitted. Split the parent list and emit one extends edge per parent (mirroring the class branch).
2026-07-04 11:26:54 +01:00
safishamsiandClaude Opus 4.8 e2ef4ef3d1 fix: harden semantic extraction and kill phantom import edges (#1631, #1638, #1632)
#1631: a malformed LLM chunk (a stray non-dict entry in edges/nodes/hyperedges)
crashed the AST+semantic merge and the semantic-cache write with
`AttributeError: 'list' object has no attribute 'get'`, discarding every
successful chunk and writing no graph.json. `_parse_llm_json` now sanitizes each
fragment at the single parse chokepoint (dict entries only; non-list values
coerced to []), protecting the cache writer, the adaptive-retry merge, and the
CLI merge in one place.

#1638: an unresolved bare npm import (`import colors from "tailwindcss/colors"`)
emitted an imports_from edge to the bare id `colors`, which build.py's
pre-migration alias index then remapped onto an unrelated local file of that
stem (backend/utils/colors.py) - a confident EXTRACTED cross-language phantom
edge, one per importing file. The external-import fallback now namespaces its
target with the `ref` prefix (the J-4 convention), so it can never collapse to a
local node id; the ref target has no node, so build drops it as an external
reference.

#1632: with a parallel LLM backend, extract_corpus_parallel merged chunk results
in completion order, so which network call returned first reordered nodes/edges
run-to-run even when the model returned identical content - churning graph.json.
Chunks are now merged in deterministic submission order after the pool drains
(matching the serial path); the progress callback still fires in completion
order. The model's own content variance is unchanged (irreducible).

Full suite: 2882 passed, 3 skipped. Validated end-to-end via a local wheel build
on a mixed TS+Python corpus: `explain colors.py` shows only the real importer,
and graph.json is byte-identical across repeated runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 03:12:23 +01:00
safishamsiandClaude Opus 4.8 2ba07e84e1 fix(export): guard to_canvas against dangling community members (#1236 follow-up)
The #1236 fix guarded to_obsidian's member loop but not to_canvas, so
`graphify export obsidian` (which also writes graph.canvas) still crashed with
KeyError on a community member id absent from G — after the notes exported,
leaving a partial mirror. Reported on 0.9.5 by @swells808.

Apply the same `m in G and m in node_filenames` filter in both to_canvas loops:
the box-sizing loop (so the group box matches the cards actually laid out) and
the card-layout loop (so the sort/label deref and the node_filenames fallback
never touch a dangling id). Regression test added alongside the to_obsidian one.
Full suite 2872.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:34:41 +01:00
safishamsiandClaude Opus 4.8 4744dfefca feat(extract): TS/JS member calls on local new-binding + typed-param receivers (#1630)
The #1316 resolver handled `this.injectedField.method()`, but a receiver whose
type comes from a local `const x = new Foo()` binding (Pattern A) or a
type-annotated parameter — including inside a returned closure (Pattern B) —
produced no calls edge, so `affected <method>` silently under-reported.

- _ts_receiver_type_table: augment the per-file type table with local
  `new` bindings (name -> constructor type) and bare-typed parameters
  (`(svc: Svc)` -> svc: Svc), merged after the constructor-injection entries
  (which win on a name clash). Only a bare type_identifier is recorded — an
  array/union/generic/qualified/predefined type is skipped (precision).
- walk_calls now descends into an inline/returned JS/TS closure that is not
  separately tracked in function_bodies (e.g. `return () => svc.doThing()`),
  attributing its calls to the enclosing function, instead of stopping at the
  arrow boundary. A tracked-body-id set prevents double-walking const-assigned
  arrows.

The existing _resolve_typescript_member_calls then resolves both via the
receiver type with its single-definition guard. Verified on the real-CLI shape
(absolute paths + graphify-out cache): both patterns resolve, ambiguity binds
to the right class (Svc not Cache), untyped/array-typed receivers emit nothing.
5 tests, full suite 2871.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:03:18 +01:00
oleksii-tumanov 8d8d2b8c0a fix(update): reconcile removed and renamed sources 2026-07-03 10:30:27 +01:00
Mark Butterworth d56ee8384d fix: guarantee per-term BFS seed diversity in query (fixes #1445)
_pick_seeds' gap_ratio cutoff discards any candidate scoring below 20%
of the top score. On a multi-term natural-language query, one term's
incidental EXACT label match on a node that is otherwise unrelated to
the query's intent (e.g. a common word also used as a field name or
identifier elsewhere in the corpus) scores ~1000x higher than any
SUBSTRING match on the query's other, actually-relevant terms
(_EXACT_MATCH_BONUS vs _SUBSTRING_MATCH_BONUS). The cutoff then
silently discards every one of those substring-tier candidates as BFS
seeds, so the traversal only ever explores the neighborhood of the one
unrelated exact match, and `query` returns confidently-wrong results
with no signal that anything went wrong.

This matches #1445's reproduction exactly: a vague query that doesn't
name a target symbol seeds from unrelated "concept-dense" nodes
instead, even though the target node is present in the graph.

_pick_seeds now optionally accepts the graph and the tokenized query
terms; when supplied, it guarantees at least one seed per distinct
term that has any match at all, so one term's collision cannot starve
out the others. Ties within a term are broken by node degree, so an
isolated incidental match doesn't out-rank a real, well-connected hub
for that term. The parameters default to None and existing callers
that don't pass them see byte-identical behavior (see
test_pick_seeds_without_diversity_args_is_unchanged).

Adds a regression test reproducing the exact failure shape from
#1445 and confirms the previously-starved target node is recovered
as a seed once G/terms are supplied.

Full test suite (74 tests) and ruff both pass.
2026-07-03 10:14:57 +01:00
safishamsiandClaude Opus 4.8 cf4b4ef85a fix(build): don't crash when a node's source_file is the scan root (#1618)
A node whose source_file equals the absolute scan root (e.g. a project-level
semantic concept the LLM attributed to the whole repo) relativized to Path('.'),
and _semantic_id_remap fed that into _file_stem, whose path.with_suffix("")
raises `ValueError: '.' has an empty name`. The crash landed in final graph
assembly — AFTER all LLM extraction cost was spent — writing no graph.json at
all, and leaving `cluster-only` to then report "no graph found".

Two guards: _file_stem returns "" for a name-less path (protects every caller,
not just this one), and both _semantic_id_remap passes skip a root-equal
source_file explicitly (it has no per-file identity to remap — id left
untouched). Reported with a minimal LLM-free repro by @sub4biz.

Not a 0.9.5 regression: _semantic_id_remap/_file_stem are byte-identical to
0.9.4; the latent path was only hit when dedup produced a root-source_file node.
4 regression tests (dot-path stem, remap no-crash, build_from_json with a
root-level concept node, normal remap unaffected). Full suite 2849.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:32:24 +01:00
safishamsiandClaude Opus 4.8 eebc406773 feat(extract): C# receiver-typed member-call resolution (#1609)
C# had no member-call resolver (unlike Swift/Python/Ruby/TS/C++/ObjC), so
`recv.Method()` fell back to a bare method-name match against label_to_nid —
which, under ambiguity, silently mis-bound `_server.Save()` to an unrelated
`Cache.Save()`. That's a WRONG edge, not just a missing one, and it left
delegation-heavy C# call graphs (wrappers, service layers) blind across typed
member/param boundaries.

Mirrors the C++ #1547 pattern:
- capture the member_access_expression receiver (simple identifier or `this`)
  into member_receiver and set is_member_call in the C# invocation branch;
- defer ALL C# member calls with a receiver to the resolver (tgt_nid = None) so
  the bare in-file match can't fire, and emit a raw_call tagged lang="csharp";
- _csharp_member_type_table: file-wide name -> Type from fields, properties,
  parameters, and locals (incl. `var v = new T()`), first-binding-wins;
- _resolve_csharp_member_calls: `this` -> enclosing class (EXTRACTED),
  capitalized -> the named type (EXTRACTED), else the receiver's table type
  (INFERRED), each gated by the single-definition guard; no method on the type
  -> no edge. Registered for .cs.

Verified: the ambiguous `_server.Save()` now resolves to Server.Save and NOT
Cache.Save; field/param/local/this/Type.static/cross-file all resolve; dynamic
receiver and absent-method emit nothing; unqualified calls unregressed. 8 new
tests, full suite 2841.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:25:54 +01:00
Paulo PintoandClaude Opus 4.8 35404167d9 fix(extract): emit reference edges for TS/JS decorators
`@Component`, `@Injectable`, `@Input`, `@Inject`, `@Entity`, … produced no
edge — the `decorator` node kind was never walked. This is framework-critical
(Angular, NestJS, Vue class components, TypeORM): the decorators are the
primary signal of what a class is and does.

Decorators occur only on classes, class members, and parameters, so one pass
over each class declaration covers them. `_ts_emit_decorator_edges` emits a
`references` edge (context="decorator") from the decorated entity to the
decorator symbol:

  - class decorators -> the class. Handles both `@Deco class C` (decorator is
    a child of the class) and `@Deco export class C` (decorator sits on the
    wrapping export_statement), plus stacked decorators.
  - method decorators -> the method node. They are siblings preceding the
    `method_definition`; stacked decorators are skipped past to find it.
  - field / accessor decorators -> the class (the field is not a graph node).
  - parameter decorators (`@Inject(T)`) -> the enclosing method/constructor.

The symbol is the head identifier: `@Injectable`, the `function` of
`@Component({...})`, or the `property` of `@ns.Component()`. Targets go
through `ensure_named_node`, so a decorator defined outside the corpus
becomes a sourceless stub, consistent with type references — one per
referencing file, matching the cross-file stub disambiguation introduced
with full-path node IDs in 0.9.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:29 +01:00
Paulo PintoandClaude Opus 4.8 869aaf7502 fix(extract): emit a node for TS namespace / module containers
`namespace Foo {}` parses as `internal_module` and `module Bar {}` (and
ambient `declare module "pkg" {}`) as a named `module` node. Neither kind
was in `class_types`/`function_types` nor handled by an extra-walk, so the
container produced no node — its members were still reached by the default
recurse, but the namespace/module itself was invisible to the graph and its
members lost their namespace context.

Add `_ts_extra_walk`, dispatched for TypeScript after `_js_extra_walk`,
mirroring `_csharp_extra_walk`: it emits a container node + a file→container
`contains` edge and recurses the body, leaving members file-contained as
before. `internal_module` exposes `name`/`body` fields; `module` exposes
none, so name (identifier / nested_identifier / quote-stripped string) and
body (`statement_block`) are found positionally. The `is_named` guard skips
the anonymous `module` keyword token, which shares the `module` type string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:29 +01:00
Paulo PintoandClaude Opus 4.8 09aeb979c8 fix(extract): extract TS/JS generator functions as nodes
Generator functions were invisible to the graph. The declaration form
`function* g()` parses as `generator_function_declaration`, which was
absent from the JS/TS `function_types`, so it produced no node; the
expression form `const h = function*(){}` parses as `generator_function`,
which was absent from the JS function-value types, so it was never captured
when assigned to a module-level const. Generator *methods* (`*gen()` in a
class) were already covered — they parse as `method_definition`.

Add `generator_function_declaration` to the JS and TS `function_types` (so
it emits a node and its body is walked) and to `function_boundary_types`
(so its calls are scoped to it, parity with `function_declaration`); add
`generator_function` to `_JS_FUNCTION_VALUE_TYPES` (so the const-assigned
expression form is captured like `function_expression`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:38:28 +01:00
Tok6Flow0 009a98b6dd Contain symlinked extraction inputs 2026-07-02 22:29:32 +01:00
Paulo PintoandClaude Opus 4.8 9811def1b3 fix(extract): capture the TS import-equals form (import x = require(...))
`import x = require("./m")` produced no edge at all: tree-sitter parses it
as an `import_statement` whose module string sits inside an
`import_require_clause`, not as a direct child of the statement, so the
direct-child string scan in `_import_js` never found it. The file-level
dependency was silently dropped while the equivalent ESM form
(`import * as x from "./m"`) was captured — an invisible hole in the
import graph of TS codebases that interop with CommonJS modules.

Restructure the scan to first locate the module string — a direct `string`
child for ESM imports/re-exports, or the `string` nested inside an
`import_require_clause` for the import-equals form — then emit the
`imports_from` edge from the single shared path. Relative paths, tsconfig
aliases, and bare modules all resolve through the same
`_resolve_js_import_target` as ESM, giving the import-equals form exact
parity with a namespace import: one file-level `imports_from` edge.

Plain JS is unaffected (the grammar has no `import_require_clause`), and
the pure namespace alias form (`import A = B.C`) is out of scope — it has
no module string and models an intra-code alias, not an import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:29:32 +01:00
AshmitandClaude Opus 4.8 1226c34731 Treat .mts/.cts (TypeScript module extensions) as TypeScript
`.mts` (ESM) and `.cts` (CommonJS) are the TypeScript counterparts of the
already-supported `.mjs`/`.cjs`, but graphify handled them in neither the
code-extension detection nor the TypeScript grammar path — so they were either
skipped entirely (classified as non-code) or, once detected, parsed with the plain
JavaScript grammar, which silently drops every `type`/`interface`/type-alias
declaration.

Detection — add `.mts`/`.cts` alongside their siblings in:
- detect.CODE_EXTENSIONS            (the code / non-code gate)
- analyze._LANG_FAMILY              (cross-language edge family → "js")
- extract._JS_RESOLVE_EXTS          (import target resolution)
- extract._JS_CACHE_BYPASS_SUFFIXES
- build.py per-node language map    (→ "js")

TypeScript grammar — route `.mts`/`.cts` to the plain TypeScript grammar (like
`.ts`; neither is JSX), so their TS-only syntax is captured:
- extract_js()                      (grammar selector: .mts/.cts → _TS_CONFIG)
- extract._DISPATCH                 (.mts/.cts → extract_js)
- the use_ts tree-sitter selector
- the typescript_member_calls resolver frozenset

Result: a `.mts`/`.cts` file now extracts identically to the equivalent `.ts`.
Verified on a real 47 KB `.mts` source — 36 nodes for `.mts`/`.cts`, matching `.ts`
exactly (was 22 with the JS grammar; the 14 dropped nodes were TS type/interface
declarations). No new dependency — same tree-sitter-typescript grammar as `.ts`.

Tests: tests/test_typescript_module_extensions.py — membership regression locks for
the extension sets, dispatch routing, and end-to-end parity asserting `.mts`/`.cts`
capture the TS `type`/`interface` nodes and match the `.ts` node set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:29:31 +01:00
Jeisson 32ff6d6fb3 fix(claude-cli): deliver extraction instructions in the user turn
The claude-cli backend passed the extraction schema via --system-prompt with
only the raw file dump in the user turn, assuming a replacement system prompt
is the model's sole authority. Claude Code >= ~2.1 (verified on 2.1.197) does
not honour that: it still layers in the local coding-agent context
(CLAUDE.md/AGENTS.md in cwd, skills, MCP) and, given a user turn that is just a
file with no request, replies conversationally ("I see the file, but there's no
actual request attached"). That prose parses to zero nodes/edges, so
_response_is_hollow flags it as truncation and the adaptive-retry path bisects
the chunk indefinitely (94 -> 47 -> 23 -> ...), never converging and never
writing graph.json.

Move the full extraction schema plus an explicit imperative into the user turn
and drop --system-prompt, so the CLI emits the JSON object directly. The
<untrusted_source> prompt-injection guardrails are carried verbatim; model
override, --add-dir image handling, timeout, and token accounting are untouched.
2026-07-02 22:29:30 +01:00
sanmaxdev 62f49bad73 fix: persist cluster-only analysis sidecar 2026-07-02 22:29:29 +01:00
Joan F Garcia 9e7fbcbd6e feat(serve): optional project_path for multi-project MCP serving
Add an optional project_path to every MCP tool so one server process can
answer against many projects. Omitted -> the server's default graph (fully
backward-compatible); an absolute project_path -> that project's
<GRAPHIFY_OUT>/graph.json, routed per call.

Implementation (all in _build_server):
- _ctx_cache + _load_ctx(): per-graph context cache with mtime+size
  hot-reload, unified across the default graph and every project graph.
  Unlike _load_graph it raises instead of sys.exit on a bad file.
- _resolve_graph_path()/_select_graph(): map project_path to a graph.json
  and rebind G/communities/active_graph_path per call. No hot-path lock:
  select+handler run in one synchronous span of the call_tool coroutine.
- Tolerant startup: serve with no default graph (pure multi-project mode)
  instead of exiting; a bad project_path is a tool error, not a crash.
- project_path injected as an optional (non-required) field on all tools.

Tests: 3 new HTTP-transport tests (optional-on-every-tool, routing,
bad-path-no-crash). Full serve suite green (91 passed).
2026-07-02 12:11:51 +01:00
Paul Young 6e97088493 fix(serve): drop question/filler stopwords from query terms
`graphify query "<question>"` tokenised the whole question and seeded BFS on
every word, so natural-language scaffolding dominated retrieval. "how does the
frontier cache work" seeded on "how"/"the"/"work" — which prefix-match prose
labels like "Working Principles" at the 100x prefix tier — instead of on
"frontier"/"cache", landing in the wrong part of the graph.

Filter a set of English question/filler words from the query terms so content
words drive seeding, with a fallback to the unfiltered terms when a query is all
stopwords ("how does it work"). Applied to query terms only — node text is never
filtered, so a symbol literally named `work` stays findable via explain/path.

Updates the one test that pinned "what" as a kept term and adds coverage for the
new drop + all-stopword fallback. Full suite green (2745 passed, 28 skipped).
2026-07-02 11:50:17 +01:00
JimandClaude Fable 5 1256d65214 perf(hooks): eliminate multi-second foreground stalls before the detached launch
Three foreground costs ran on every commit before the detached rebuild
launcher even started:

1. Interpreter probes imported the full package. Each probe executed
   'import graphify' wholesale — measured 13s per probe cold on a Windows 11
   dev box with AV-scanned site-packages — and up to four probes could run
   synchronously. Probes now use importlib.util.find_spec, which locates the
   package without executing it (interpreter startup cost only). A broken
   install under the selected interpreter still fails loudly in the rebuild
   log, as before.

2. The shebang probe read a binary. Git for Windows' command -v can return
   the launcher path WITHOUT its .exe suffix, so the '*.exe)' guard missed
   and head -1 read a PE binary: the shell warned 'ignored null byte in
   input' on every commit and the garbage always fell through to the slow
   python3/python fallbacks. The Windows pip layout is now resolved directly
   (Scripts/graphify -> sibling ../python.exe, or ./python.exe for venvs),
   and the remaining POSIX shebang read strips NULs first.

3. GIT_DIR was re-derived. git exports GIT_DIR to hooks; the unconditional
   rev-parse added ~1.3s more on machines where every git exec is scanned.
   Now reused from the environment with rev-parse as the manual-run fallback.

Measured on the affected machine: hook foreground drops from 26s+ (cold) to
~1.4s, warnings gone. Behavior is unchanged on healthy POSIX setups — probe
order and fallback semantics are preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:50:12 +01:00
safishamsiandClaude Opus 4.8 44c0a5e33c fix(swift): resolve calls on a singleton cached into a local var (#1604)
`let manager = NetworkManager.shared` followed by `manager.fetchData()` on a
later line produced zero call edges. Two gaps: (1) local let/var bindings inside
method bodies were never typed (only class-level properties and function params
populated the per-file type table), and (2) a static-member initializer
(`Type.shared`, a navigation_expression) wasn't recognized as typing the local
even in the class-property path — only constructor calls (`Type()`) were.

Add _swift_local_var_types (mirrors _cpp_local_var_types): walk each function
body and type a local from a constructor OR a `Type.staticProp` access whose head
is upper-cased. `x.method()` then resolves to the receiver type through the
existing single-definition god-node guard. The class-property path also learns
the Type.shared shape. Reported on a 23k-file iOS corpus where this idiom is the
median call pattern.

Verified: the repro resolves (loadIfNeeded -> fetchData/isLoading); constructor
locals still resolve; a lowercase-head init (`other.child`) does NOT falsely
type; and an ambiguous method name resolves to the receiver-typed class only.
Swift + full suite 2789 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:46:22 +01:00
safishamsiandClaude Opus 4.8 b70a6d7126 fix: allow python@ shebang in skill detection (#1586) + merge-graphs mixed types (#1606)
#1586: the skill's interpreter-detection allowlist rejected any shebath path
with a char outside [a-zA-Z0-9/_.-], so Homebrew's versioned python@3.13 path
was skipped and detection fell through to a bare python3 without graphify.
Allow @ in the skillgen source fragments (core/devin/aider/posix), regenerate
all skill artifacts, and register the change as a sanctioned monolith diff.
Injection chars (; $ ( etc.) are still rejected.

#1606: graphify merge-graphs crashed with an unhandled NetworkXError when
inputs disagreed on directed/multigraph. _to_simple only converted MultiGraph,
leaving a DiGraph to fail compose. Normalize every input to a plain undirected
Graph (the merged cross-repo view is undirected anyway).

Also confirmed on v8: `graphify kilo install` (#1605) works (reporter was on an
older version), and `pip3 install graphifyy` failing on macOS (#1585) is a
broken Homebrew Python 3.14 / pyexpat env, not a graphify issue — pip crashes
in its own vendored distlib before graphify is touched.

skillgen --check / --monolith-roundtrip / --schema-singleton all green; shell
smoke accepts python@3.13 and still rejects `; rm -rf /` and `$(evil)`. Full
suite 2789.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:25:51 +01:00
Synvoya ad7015262b fix(swift): emit references for enum associated-value types
The Swift `enum_entry` handler in `_swift_extra_walk` iterated the entry's
children only for the `simple_identifier` case name (emitting a `case_of`
edge) and never descended into the sibling `enum_type_parameters` node, where
associated-value types live (`enum_type_parameters -> user_type ->
type_identifier`). As a result `case started(Session)` silently dropped the
`Event -> Session` type reference.

Descend into each `enum_type_parameters` child after emitting `case_of`, run
`_swift_collect_type_refs` over its named children, and emit a `references`
edge from the enum node to each collected type (context `type`, or
`generic_arg` for generic roles), guarding target != enum node. Mirrors the
existing Swift property/parameter/return-type emit style.

Fixture: add `case failed(Config)` to `NetworkError` in sample.swift.
Test: assert (`NetworkError`, `Config`) in references(context=type).
2026-07-01 16:36:53 +01:00
Synvoya 21bcb436b5 fix(cpp): emit generic_arg references for base-class template arguments
The C++ base_class_clause handler's `template_type` branch read the base
name (`sub.child_by_field_name("name")`) and emitted the `inherits` edge,
but never descended into the base's `template_argument_list`. As a result
`class Car : public Base<Dep>` emitted `Car -> Base` (inherits) yet dropped
the `Car -> Dep` generic_arg reference entirely.

The Java handler `_emit_java_parent_type` already emits these generic_arg
references for base-class type arguments; C++ was the asymmetric gap.

Fix: after emitting the `inherits` edge, grab the base's `arguments` field
(the `template_argument_list`) and run `_cpp_collect_type_refs` over each
named argument with the generic flag set, emitting a `references` edge
(context "generic_arg") per collected type, guarding target != class node.
`_cpp_collect_type_refs` already handles nested/qualified args, so
`Base<std::vector<Dep>>` is covered too.

Adds a templated base (`Connection<T>`) + derived class
(`PooledClient : public Connection<HttpClient>`) to tests/fixtures/sample.cpp
and a test mirroring the Java generic-parents test.
2026-07-01 16:36:52 +01:00
Synvoya bb5e5192df fix(csharp): emit type references for properties
The C# class-body walker only handled field_declaration, so a
property's type produced no references(field) edge. In idiomatic C#,
auto-properties (`public Widget Main { get; set; }`) — not bare fields
— are the standard way to declare state, so this silently dropped most
of a class's type relationships.

Add a property_declaration branch alongside the field_declaration
handler, guarded the same way (ts_module == tree_sitter_c_sharp,
parent_class_nid set). A property exposes its type on the node directly
(no variable_declaration wrapper), so read it via
child_by_field_name("type") and collect refs with
_csharp_collect_type_refs, mirroring the Java/PHP/Kotlin siblings so
List<Widget> yields both the List field ref and the Widget generic_arg
ref. Only emit when target != parent_class_nid.
2026-07-01 16:36:50 +01:00
Synvoya 51f805e953 fix(php): emit type references for promoted constructor properties
PHP 8 constructor property promotion (`__construct(private Repo $repo)`)
parses the promoted parameter as `property_promotion_parameter`, not
`simple_parameter`. The PHP parameter loop filtered on `simple_parameter`
only, so promoted params were skipped entirely: their type emitted no
`parameter_type` edge on the constructor, and — because a promoted param
is also a real class field — no `field` edge on the class either. A
non-promoted param in the same signature still emitted `parameter_type`,
so the type reference was silently dropped for exactly the promoted case.

The promoted param's type sits in the same direct named-child shape the
loop already reads for `simple_parameter`, so widening the filter to
accept `property_promotion_parameter` makes the existing type extraction
emit the `parameter_type` edge. Additionally, for a promoted param, emit
a `field`-context references edge on the class (mirroring the
`property_declaration` handler), guarded so it only fires when a parent
class is in scope and the target is not the class node itself. Normal
`simple_parameter` behaviour is unchanged.

Adds a promoted-property constructor to tests/fixtures/sample.php and
test_php_constructor_property_promotion_contexts asserting the promoted
type appears as both `field` and `parameter_type`, and that a
non-promoted param does not leak a field edge.
2026-07-01 16:36:49 +01:00
Synvoya cd3a376030 fix(objc): emit implements edge for protocol-to-protocol adoption
`@protocol Derived <Base>` dropped the protocol-adoption (inheritance)
edge. The protocol_declaration handler in extract_objc walked children
for method declarations but ignored the protocol_reference_list child
that holds the adopted protocols, so no implements edge was ever emitted
for protocol-on-protocol adoption.

The extractor already handled `@interface Foo <Proto>` adoption, but that
nests the protocol name under a parameterized_arguments node; protocol-on-
protocol adoption uses a different grammar node (protocol_reference_list)
whose adopted-name is a direct `identifier` child, so it was never
matched. Walk protocol_reference_list and emit an implements edge for each
adopted protocol, mirroring the @interface handling.

Adds a defined Base/Derived protocol pair to the ObjC fixture and a
regression test asserting the Derived->Base implements edge.
2026-07-01 16:36:48 +01:00
Synvoya a129ff2cd6 fix(powershell): emit inherits/implements edges for class base types
The `class_statement` handler read only the first `simple_name` child —
the class name — and never inspected the base type(s) after the `:`
token. As a result `class Dog : Animal` dropped the Dog->Animal
inheritance edge entirely; derived classes appeared as isolated nodes.

Walk the class_statement children, and once the `:` token is seen treat
each following `simple_name` as a base type. Matching the C# convention
(PowerShell has no syntactic base-vs-interface split), the first base is
emitted as `inherits` and the rest as `implements`, resolved via
ensure_named_node.

Adds a Shape/Circle inheritance pair to tests/fixtures/sample.ps1 and a
regression test asserting ("Circle","Shape") in the inherits edges.
2026-07-01 16:36:47 +01:00
Synvoya 67b4525f32 fix(scala): emit field type references for var declarations
The Scala field handler matched only `val_definition`, so a mutable field
(`var b: Repo`), which parses as `var_definition`, had its type reference
silently dropped from the graph. val and var nodes are structurally identical
(both expose a `type` field), so the existing type-collection logic works
unchanged. Widen the guard to accept var_definition.

Adds a var field to the Scala fixture and a regression test.
2026-07-01 16:36:47 +01:00
safishamsiandClaude Opus 4.8 784e9c833e fix(extract): case-sensitive cross-file resolution in case-sensitive languages (#1581)
Cross-file name resolution folded case for every language, so `from pathlib
import Path` resolved to a shell script's `export PATH=...` node — one variable
becoming the corpus's #1 god-node (266 false incoming edges on a real repo),
polluting god-node rankings, affected blast-radius, and clustering. Reported
with a precise diagnosis by @sheik-hiiobd.

Case is semantic in Python/Rust/Go/Java/C#/Kotlin/Swift/Ruby/C/C++/JS/TS: `Path`
(class), `PATH` (env var), `path` (variable) are distinct. Fix gates folding by
language at the two resolution sites the repro exercised:

- global cross-file CALL resolver: index by exact case; a folded index is built
  only for case-insensitive-language nodes (PHP/SQL/Nim) and consulted only when
  the calling file is such a language.
- type-reference STUB rewire (_rewire_unique_stub_nodes): match stubs to real
  defs by exact case, with a folded fallback restricted to case-insensitive-
  language definitions — so a case-sensitive `PATH` can never absorb a `Path`.

For case-sensitive languages this only ever removes false edges. Concept/doc
dedup (dedup.py, guarded to non-code nodes) is intentionally left folding.
Regression tests: Python `Path` no longer hits shell `PATH`; a case-differing
cross-file ref doesn't resolve; exact-case resolution still works; PHP fold
preserved. Full suite 2777.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:09:42 +01:00
Synvoya 297075c3f3 fix(systemverilog): emit field references for qualified class properties
The SystemVerilog class-body field regex in _augment_systemverilog_semantics
matched only unqualified `<type> <name>;` declarations. Its `^\s*` prefix
consumes leading whitespace but not leading class-property qualifiers, so a
qualified field such as `rand Config m_cfg;` (three tokens) failed the
two-token shape and its type reference was silently dropped from the graph.

Consume optional leading qualifiers (rand/randc/local/protected/static/const/
automatic/var) before the type token. Zero qualifiers preserves the existing
behavior; the type and name capture are unchanged.

Adds test_systemverilog_qualified_field_references plus rand- and
protected-qualified fields (and a Config class) to the shared .sv fixture.
2026-07-01 13:45:53 +01:00
Synvoya 7eb847bcf7 fix(rust): emit field type references for tuple structs
extract_rust() only traversed field_declaration_list (named-struct
bodies), so tuple structs -- whose positional fields nest under
ordered_field_declaration_list -- had every field type reference
silently dropped from the graph.

This is the same node shape the enum handler already accounts for
(tuple variants nest their types under ordered_field_declaration_list);
the struct path was simply left behind. Add an additive branch that,
for each type node in a tuple struct's ordered_field_declaration_list,
collects type refs via _rust_collect_type_refs and emits references
edges with the appropriate field / generic_arg context. The
named-struct path is untouched.

For `struct Wrapper(Logger, Config);` with Logger/Config defined
in-file, no field edges were produced before; both are now emitted.

Adds test_rust_tuple_struct_field_references and a tuple struct to the
shared Rust fixture covering plain and generic positional field types.
2026-07-01 13:45:53 +01:00
SynvoyaandClaude Opus 4.8 984a6a8f0a fix(julia): emit imports for qualified, relative, and scoped-selected forms
Only bare-identifier imports (`using Foo`) emitted edges. tree-sitter-julia
wraps qualified paths in `scoped_identifier` (`using Base.Threads`), relative
paths in `import_path` (`using ..Sibling`), and the package of a
`selected_import` may itself be a `scoped_identifier`
(`import Base.Threads: nthreads`). None of those were matched, so qualified and
relative imports were silently dropped, and scoped selected-imports pointed at
the selected symbol instead of the module.

Resolve the module name from identifier / scoped_identifier / import_path in
all three positions. Adds fixture lines + a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:45:52 +01:00
SynvoyaandClaude Opus 4.8 674184d462 fix(rust): emit references edges for enum variant field types
Enum variant payload types were silently dropped — `struct_item` and
`trait_item` had type-reference handlers but `enum_item` had none, so the
variant field types were never traversed.

Add an `enum_item` branch that walks
`enum_variant_list -> enum_variant -> ordered_field_declaration_list`
(tuple variants, `Click(Logger)`) and `field_declaration_list` (struct
variants, `Resize { size: Dim }`), emitting a `references` edge from the enum
to each field type. Reuses the same type collection as the struct path. Adds
an enum to the fixture and a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:58:39 +01:00
SynvoyaandClaude Opus 4.8 b8f41c77eb fix(fortran): emit calls edges for function invocations
Function calls (`y = f(x)`) were silently dropped — only `subroutine_call`
(`call sub(...)`) was handled in walk_calls. tree-sitter-fortran represents a
function invocation as a `call_expression`, which had no branch, so every
function-to-function call produced no edge.

Handle `call_expression`. Because Fortran uses the same `name(...)` syntax for
array indexing, the callee is resolved against procedures defined in the file
(`target_nid in seen_ids`) before emitting — so array accesses like `arr(i)`
cannot fabricate spurious `calls` edges. Adds a function + caller to the
fixture and a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:58:38 +01:00
SynvoyaandClaude Opus 4.8 f2ea6a6087 fix(elixir): expand multi-alias brace form into per-module imports edges
`alias Foo.{Bar, Baz}` (and the same import/require/use brace form) emitted
NO imports edges. tree-sitter-elixir represents it as a `dot` node holding the
base alias plus a trailing `tuple` of member aliases, but the import handler
only matched a bare `alias` child, so every multi-alias import was silently
dropped.

Add `_get_alias_modules`, which expands the brace form to `Foo.Bar`,
`Foo.Baz`, … while leaving the single form (`alias Foo.Bar`) unchanged. Adds a
fixture line + regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:58:38 +01:00
safishamsiandClaude Opus 4.8 8127ff9a9c fix(cluster): detect stale community labels on cluster-only re-cluster
Reported from a real run: after re-scoping a repo (5807->3710 nodes), the
228-community re-cluster kept run-1's 300 saved labels, so cids now covering a
different community wore the wrong (LLM) names — silently. cluster-only reused
.graphify_labels.json wholesale, and the overlap-based cid remap grabs a prior
cid on any overlap, inheriting a stale name.

Fix: write a per-community membership signature (sha256 of sorted member ids)
beside the labels. On reuse, keep a saved label only when the community's
signature is unchanged; a changed community is renamed by its deterministic
hub (correct-by-construction) with a warning to run `graphify label` for fresh
LLM names. For label files predating the signature, fall back to a community-
count check (a differing count means a different clustering -> don't trust cid
labels). Unchanged graphs reuse labels silently — no false warnings.

Verified: stale legacy labels (42) on a 12-community graph -> warned + hub-
renamed all + sig written; rerun on the unchanged graph -> silent reuse, labels
stable. Unit tests for the signature (deterministic, order-independent, changes
on membership change). Full suite 2771.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:52:50 +01:00
safishamsiandClaude Opus 4.8 e34e27c24c fix(extract): cross-file indirect_call survives id relativization (0.9.4 regression)
Local install-testing of 0.9.4 surfaced that `graphify extract .` dropped every
cross-file indirect_call edge — the headline feature, broken on the primary code
path — while the extract() API worked. Root cause: the cross-file callable-target
guard unioned per-file `callable_nids` (pre-remap ids), but extract() rewrites node
ids afterward (id_remap / prefix sym_remap / _disambiguate_colliding_node_ids). When
the scan root relativizes ids (cache_root == project root, which the CLI passes), the
guard set went stale and `tgt not in callable_nids` rejected every remapped target.
In-file indirect edges survived (emitted with consistently-remapped endpoints), which
masked it — only cross-file dropped.

Fix: mark callable defs with a `_callable` attribute on the node dict instead of
exporting an id list. A marker rides through every id remap; callable_nids is rebuilt
from the final (post-remap) nodes right before the pass that uses it, and the marker
is stripped before output (like origin_file). Regression test extracts with
cache_root == project root (the CLI shape) and asserts the cross-file edge survives
and _callable never ships to graph.json. Full suite 2769.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:43:05 +01:00
safishamsiandClaude Opus 4.8 de7d362537 fix(build): relativize source_file across a symlinked root (#1571 follow-up)
Rigorous smoke testing surfaced an edge case the canonical-tmp unit tests
couldn't reach: when the scan root is under a symlink (macOS /var ->
/private/var, a symlinked home or git worktree), the absolute prune path and
the resolved root differ by prefix, so _norm_source_file's lexical
relative_to fails and the prune/replace match silently misses — deleted
files' ghost nodes survive. Latent in the pre-existing #1007 path too, now
that build_merge resolves the root.

Fix: when lexical relative_to fails, retry with both sides fully resolved.
Only the failure path resolves, so the common lexical match stays
filesystem-free (no per-node stat on the hot replace-per-source loop).

Adds a symlinked-root prune regression test (POSIX-only). Full suite 2768,
and the full end-to-end smoke battery (indirect_call all contexts, JS,
Ruby/Groovy inherits, hyperedge preservation, symlinked-root ghost prune,
corrupt-json errors, dedup collision warning) is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:14:48 +01:00
safishamsiandClaude Opus 4.8 6eb7c014f4 test(build): add regression tests for corrupt graph.json (#1537)
#1537 shipped with a manual test checklist only. Add automated tests that
corrupt a graph.json and assert the actionable RuntimeError at all three load
paths (build_merge, affected.load_graph, diagnostics._read_json_file) plus a
happy-path guard. Also record the six merged small fixes in the changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:49:43 +01:00
hibinmandClaude Opus 4.8 64a6093376 fix(groovy): emit inherits/implements edges for extends/implements
tree-sitter-groovy exposes inheritance via the same `superclass` and
`interfaces`/`type_list` fields as tree-sitter-java, but the inheritance-
emitting block in `_extract_generic` was gated on
`ts_module == "tree_sitter_java"`. Groovy was the only class-based JVM
language in the file with no inheritance handler, so every Groovy
`extends`/`implements` was silently dropped (contains/methods/imports/calls
were unaffected).

Widen the gate to include `tree_sitter_groovy`; the existing
`_emit_java_parent_type` path handles the identical node shapes verbatim.
Adds a base class + interface to the Groovy fixture and two regression
tests (extends -> inherits, implements -> implements).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:47:10 +01:00
hibinmandClaude Opus 4.8 a19b9e90ec fix(ruby): emit inherits edge for class superclass
`class Dog < Animal` exposes the base in the `superclass` field, but the
inheritance handler in `_extract_generic` had branches for
java/kotlin/c#/scala/cpp/php/swift/python and none for Ruby, so every Ruby
`inherits` edge was silently dropped (contains/methods/calls unaffected).

Add a Ruby branch that reads the `superclass` field, handling both a bare
`constant` (`< Animal`) and a `scope_resolution` (`< Foo::Bar` -> Bar).
Adds a subclass to the Ruby fixture and a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:47:10 +01:00
Matias Duarte 879c05894d fix(hooks): limit Windows hook rebuild workers 2026-07-01 10:47:10 +01:00
Varun Nuthalapati 5320aa8eb1 fix(dedup): warn on cross-chunk node ID collision to surface silent data loss (#1504)
When two LLM extraction chunks each process a file with the same name in
different directories, they independently generate the same node IDs and
deduplicate_entities() silently drops one node (first-writer-wins). The
data loss had no indication in any log, counter, or output.

Adds a stderr WARNING when a duplicate ID comes from a different
source_file, telling the user which files collided and recommending the
per-subfolder extract + merge-graphs workflow to avoid it.
2026-07-01 10:47:10 +01:00
Sheik Ershad 1aab291caf feat(cluster): deterministic hub community labels (readable without an LLM)
Community labels defaulted to "Community N" whenever no LLM backend was configured,
making the report + suggested questions unreadable ("why does log_action connect
Community 70 to Community 129?"). Add `label_communities_by_hub`: name each community
after its highest-degree member — the structural hub — so the report reads "log_action"
/ "auth" with zero LLM cost. Ties break by node id for run-to-run stability; a community
with no members in the graph keeps the "Community N" placeholder.

Wired as the default base at both label-building sites — the label/standalone path in
__main__ and the watch/detect rebuild in watch.py. A configured LLM naming pass still
runs and overrides these with richer names; its no-backend placeholder fallback is
guarded so it can't clobber the hub labels.
2026-07-01 10:30:47 +01:00
Sheik Ershad 8fdbf50197 feat(extract): capture getattr(obj, "name") indirect_call edges (#1566 slice 3)
Reflective dispatch by string literal — getattr(obj, "handler") — resolves the
attribute name to a callable def and emits it under indirect_call (context
"getattr", INFERRED) at both function and module scope, so `graphify affected
handler` now covers getattr call sites.

The name is a STRING, not an identifier: it names an attribute and is never
shadowed by a param/local, so it resolves without the identifier shadow guard —
the inverse of the #1565/#1566 identifier paths. A dynamic name (a variable,
f-string, concatenation, or any expression) is not statically resolvable and emits
nothing; obj.getattr(...) (a method, not the builtin) and the 1-arg form are ignored.

Refactors the shared resolve-and-emit core out of _emit_indirect_ref into
_emit_indirect_by_name so the getattr path reuses it (callable-target-only,
cross-file deferral, dedup) without duplicating the guard; the identifier wrapper
is behavior-preserving. Full suite green.
2026-07-01 10:30:47 +01:00
safishamsiandClaude Opus 4.8 4f40967e8d fix(build): preserve hyperedges + harden prune root in build_merge (#1574, #1571)
build_merge backs `graphify --update`. Two incremental-update data bugs:

#1574 — it read only nodes+edges from the existing graph.json, never
hyperedges, and build() only sees the new chunks' hyperedges. So every
--update collapsed the graph's hyperedge set (the highest-signal semantic
groupings) down to just the re-extracted files'. Now existing hyperedges are
carried forward via attach_hyperedges (id-dedup): re-extracted files' prior
hyperedges are replaced by their new version (by source_file), deleted files'
are dropped via the prune set, and unchanged/global ones are preserved. This
mirrors what watch.py already did.

#1571 — when a caller omits `root`, absolute prune_sources (from
detect_incremental) never relativized to the stored relative source_file
keys, so deleted files' nodes survived as ghosts and accumulated across runs.
Added _infer_merge_root: fall back to the committed graphify-out/.graphify_root
marker, else the output dir's parent. This root now drives BOTH the prune set
and the replace-per-source normalization, so both work without an explicit
root. The CLI --update path and all shipped runbooks already pass root; this
hardens the library for any other caller.

5 tests: hyperedge preservation (unchanged/global kept, re-extracted replaced,
with and without root), deleted-file hyperedge prune, and root-less prune via
both the grandparent and .graphify_root-marker fallbacks. Full suite 2742.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:12:46 +01:00
Sheik Ershad 311e63a7fc feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)
A function bound to a name (cb = handler) or returned from a factory
(def make(): return handler) is a real reference, but indirect_call only
covered call arguments and dispatch tables, so `affected` still dropped these
callers.

Emit indirect_call (context "assignment"/"return", INFERRED) for the value-side
identifiers of a Python assignment RHS and a return, at function scope (owner =
enclosing function) and module scope (owner = file node). Reuses the shared
_emit_indirect_ref guard. Scans the VALUE side only -- the assignment target is
a new local binding, not a reference -- so the existing param/local shadow guard
still rejects the false edges #1565 fixed.

Negatives covered: param-shadow, local-shadow, non-callable emit nothing.
Full suite green; ruff clean.
2026-06-30 23:26:13 +01:00
safishamsiandClaude Opus 4.8 47033c8b75 fix(cli): make skill-version warning direction-aware (#1568)
_check_skill_version advised "Run 'graphify install' to update" on ANY
version mismatch. But `install` writes the package's OWN bundled skill and
re-stamps .graphify_version, so when the skill on disk is NEWER than the
running package, following that advice silently DOWNGRADES the skill to
silence the warning. The docstring even said "warn if the skill is from an
OLDER version" but the code never checked direction.

Compare versions numerically (new _version_tuple, so 0.10 > 0.9 and a
malformed stamp degrades instead of raising). When the skill is newer than
the package, recommend upgrading the package (uv tool upgrade / pip install
-U) instead of install; the older-skill case is unchanged. Hit in practice
by a stale `uv tool` CLI and by contributors whose dev checkout stamped a
newer skill. Reported by @TPAteeq.

4 tests (numeric ordering, both mismatch directions, silent-when-equal).
Full suite 2730 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:15:52 +01:00
safishamsiandClaude Opus 4.8 6dc1cb0dd7 feat(extract): capture indirect dispatch for JS/TS (#1566 slice 5)
Brings the indirect_call model to JavaScript and TypeScript, the next-
biggest callback-passing ecosystem. Now captured:

- callbacks passed by name as call arguments: `arr.map(fn)`, `setTimeout(fn)`,
  `el.addEventListener("x", fn)` — function-scoped, attributed to the caller.
- module-level callback registration (idiomatic in JS, unlike Python):
  Express routes `app.get("/", handler)`, event wiring `emitter.on("e", h)`,
  timers — attributed to the file.
- object/array dispatch tables: `const ROUTES = { create: handler }`,
  `const HOOKS = [onStart, onStop]`, object shorthand `{ handler }`.

Arrow-const functions (`const cb = () => {}`) are registered as callable
targets (threaded callable_def_nids + local_bound_names through
_js_extra_walk). Guards mirror Python: shadowing by param/local/module
reassignment is rejected (JS module shadow set excludes function-valued
declarators so arrow-consts stay resolvable), object KEYS and non-callable
values are excluded, inline arrows/function expressions are direct defs not
references, single-definition god-node guard cross-file.

Two cross-file fixes the JS import model exposed:
- a name that resolves to an import-surfaced FOREIGN symbol (JS named
  imports map the real node into the importing file's label map) is now
  deferred to the cross-file resolver instead of being mistaken for a local
  non-callable; the global callable_nids guard still rejects imported data.
- indirect_call dedup is now call-aware: a benign `imports` edge from a file
  to the symbol it imports no longer suppresses the indirect_call to it
  (only a real calls/indirect_call edge does).

Shared the resolve-and-emit helper across Python and JS/TS. 9 new tests
(func-arg, module object/array, Express-style registration, inline-arrow
negative, param-shadow, key/data exclusion, shorthand, cross-file import,
TS typed params). Full suite 2726 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:34:48 +01:00
safishamsiandClaude Opus 4.8 8288829613 feat(extract): capture dispatch tables as indirect_call edges (#1566)
Slice 1 of #1566. A function referenced as a VALUE in a dict/list/set/
tuple literal — the registry/route-table idiom (`ROUTES = {"create":
create_user}`, `HOOKS = [on_start, on_stop]`) — is an indirect dependency
that blast-radius must see, but the call-argument capture (#1565) didn't
cover it. Emit an indirect_call edge for each callable value:

- module-level tables attribute to the file node; function-scoped tables
  attribute to the enclosing function. Same-file and cross-file (an
  imported handler in a table routes through the cross-file resolver).
- dict KEYS are excluded (only values are references); non-callable values
  (a number, a string) never resolve; a name shadowed by a param/local or
  rebound at module scope is the local value, not the function.

Refactors the resolve-and-emit logic shared by the argument and table
paths into one guarded helper (_emit_python_indirect_ref) and threads the
edge `context` ("argument" | "collection") through the cross-file pass.
Adds _python_module_bound_names for the module-scope shadow set.

7 new tests (module dict/list, function-scoped, dict-keys-excluded,
non-callable-value, module-reassign shadow, cross-file table). Full suite
2717 passed. Python only; assignment/return refs + other languages remain
on #1566.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:17:53 +01:00