Commit Graph
136 Commits
Author SHA1 Message Date
38095aa95b feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
Three callback-registration shapes deferred from #756/#808, one arc:

1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>`
   registration whose member isn't on the enclosing class defers to a second
   pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs,
   runs after implements/extends edges persist, same lifecycle as the #750
   conformance pass) and resolves up the supertype chain, depth-capped BFS,
   validated targets only. `bus.on("submit", this.handleSubmit)` in a
   subclass links to FormBase::handleSubmit; same-named methods on unrelated
   classes never match. this.-prefixed candidates skip the extraction name
   gate (an inherited member can't be in definedHere).

2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` /
   `OtherClass::handle` emit QUALIFIED names resolved by the scoped
   suffix-matcher — cross-file capable, gated on the scope name being a
   same-file type or an imported name (dotted JVM imports now contribute
   their last segment). `this::m` and `super::m` route through the
   class-scoped resolver (super rides the supertype pass). References
   through a VARIABLE (`subscriber::onNext`) deliberately produce nothing —
   receiver type is unknowable; RxJava's baseline bare capture was resolving
   these to same-named same-file methods (a test method "registering" an
   anonymous class's onNext) — the rework drops 18 such wrong edges and
   keeps the 7 genuine Type::method refs RxJava's main tree actually has.

3. SWIFT enclosing-type scoping (implicit self): bare callback names match
   methods only of the from-symbol's own type (extension/nested scopes
   reconciled by suffix), and top-level code never matches methods.
   Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier`
   resolving to same-named methods on unrelated protocols), all verified;
   the same-class param collision (`task`) remains and is documented.

New ResolutionContext.getNodeById lets matchers derive the from-symbol's
class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw
stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges
changed on any of 7 A/B repos; nodes identical everywhere. Kotlin
companion-object members extract unqualified (pre-existing) so
`Type::companionFn` stays silent rather than guessing — documented.

Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:09:01 -05:00
38eb4e688c fix(extraction): classify TS/JS class fields by value — properties, not methods (#808) (#809)
Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).

Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).

With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.

A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).

EXTRACTION_VERSION 19 → 20 (re-index to benefit).

Closes #808

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:48:11 -05:00
8a114ba53c feat(extraction): capture function-as-value — callback registration sites in callers/impact (#756) (#807)
A function name used as a VALUE — passed as an argument
(signal(SIGINT, handler), qsort(..., compare)), assigned to a function
pointer or field (ops->recv_cb = my_cb, OnClick := Handler), or placed in
a struct initializer / handler table ({ .recv_cb = my_cb },
{ "get", getCommand }) — produced no edge in ANY of the 19 tree-sitter
languages, so registered callbacks looked dead and their registration
sites were invisible to callers/impact.

This adds table-driven function-as-value capture across all 19 languages
(plus the wrapper forms: &fn, &Cls::method, Java Class::m, Kotlin ::f,
Swift #selector, ObjC @selector, Ruby method(:sym), Scala eta, Pascal
@Handler), gated at extraction (same-file definitions + imported
bindings; C-family file-scope initializers are constant-expression
contexts and skip the gate, which is how redis-style cross-file command
tables resolve), and resolved by a dedicated strategy: function/method
targets only, same-file first, unique-or-drop cross-file, no fuzzy
fallback ever. Edges persist as kind 'references' with metadata.fnRef,
so getCallers/getImpactRadius surface them with zero graph-layer
changes; MCP callers/callees label them "via callback registration".

Precision rules bought by real-repo false positives (full A/B record in
docs/design/function-ref-capture.md): C++ is &-explicit outside
file-scope tables (fmt's begin/out/size collisions; out-of-line member
defs are function-kind); TS/JS/Python bare ids resolve to functions only
(TS class fields extract as method-kind — pre-existing quirk); Swift
refuses same-file method overload-families; param-forward shapes
(this.x = x, value: value) and destructuring are skipped; minified
bundles (*.min.js) produce no candidates.

Validated on 17 public OSS repos (redis, excalidraw, gin, bytes, okhttp,
okio, Alamofire, flask, sinatra, Newtonsoft.Json, scopt, provider,
busted, Fusion, AFNetworking, PascalCoin, fmt): node counts identical,
zero calls edges lost or gained, references strictly additive
(+3,200 registration edges total), precision spot-checked by reading
sampled source lines (redis 30/30, flask 8/8). Deliberately NOT covered:
indirect-dispatch resolution (o->cb(x) → impl) — that needs data-flow
through struct fields, and a wrong edge is worse than none.

EXTRACTION_VERSION 18 → 19 (re-index to benefit).

Closes #756

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:20:27 -05:00
0df9246752 fix(extraction): capture & clean docstrings across all README languages (#780) (#806)
* fix(extraction): capture docstrings for export/const/decorator-wrapped symbols (#780)

getPrecedingDocstring walked previousNamedSibling from the EMITTED
declaration node, so it only found a leading comment when the comment was
a direct sibling of that node. For a declaration nested under a wrapper —
`export class X` / `export const f = () => {}` (export_statement /
lexical_declaration), a plain const arrow (variable_declarator), or a
decorated Python def/class (decorated_definition) — the comment is a
sibling of the WRAPPER, so the inner node had no preceding comment and
the docstring was stored as NULL.

Climb out through the wrapper node(s) before scanning for the comment.
Each wrapper holds exactly one declaration, so this can't mis-attribute a
comment to a sibling (verified: an uncommented method does NOT inherit its
class's comment). Also strip leading `#` from Python/Ruby/shell line
comments, which the cleanup chain missed (Python docstrings used to keep
their `#`).

Query/extraction-layer change to a parse helper; re-index to pick up
docstrings on already-indexed files. Verified on the reporter's JS/TS and
Python repros (8/8 now captured) plus over-walk controls; +3 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(extraction): clean comment markers across all supported languages (#780)

Validating docstring capture across every README language surfaced that
the marker cleanup only knew C-style `//` and `/* */`, plus the `#` added
earlier this branch. Doc comments in other styles were captured but left
their markers in the stored text:

  - Rust/Swift/Kotlin doc lines `///` and `//!`  -> leading `/` / `!` leaked
  - Lua/Luau `--` and `--[[ ]]`                  -> not stripped
  - Pascal `{ }` and `(* *)`                     -> not stripped

Extract the cleanup into cleanCommentMarkers() and handle every style.
Paired block delimiters are stripped only when the comment OPENS with one,
so a line comment that happens to end with `}` / `*)` / `]]` is never
truncated; per-line markers stay anchored at line start.

Validated end-to-end (extract -> index -> codegraph_node output) across
all 19 tree-sitter code languages plus Svelte/Vue `<script>` blocks: every
one now stores and returns a clean docstring. +1 cross-language test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:38:22 -05:00
dac00e7d44 fix(pascal): attribute a free routine's calls to it, not the file (#795)
A Pascal/Delphi procedure or function defined ONLY in the implementation section
(no interface declaration, not a class method) had no node of its own, so
extractPascalDefProc's caller lookup fell through to the nodeStack top — the file
node. Every call in such a routine's body was lumped under the unit: callers
returned the file, and impact couldn't attribute the call to the routine. (Methods
were fine — they get a node from their class declaration.)

Fix: when extractPascalDefProc finds no existing node for a FREE routine (a name
with no `.`), create a function node for it and attribute the body's calls to it.
Interface-declared free routines already have a node (found via the methodIndex),
so there's no duplicate; methods keep their existing class-declaration node.

PascalCoin A/B: +511 / -145 — the +511 are calls now correctly attributed to their
actual routine (`allocate_new_datablock -> TDisposables::GetMem`), replacing -145
file-level aggregates; +248 new function nodes for the implementation-only
routines. New synthetic test asserts a free routine's call attributes to it
alongside a method caller. EXTRACTION_VERSION 17->18. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:05:08 -04:00
35dce04e1f feat(pascal): extract paren-less method calls (Obj.Free; / TFoo.GetInstance.DoIt;) (#793)
Pascal/Delphi lets a no-arg method or procedure drop its parens, so the call
parses as a bare `exprDot` (not an `exprCall`) and was never recorded as a call —
callers/impact/trace missed all of them (e.g. `Obj.Free`, `List.Clear`, the
paren-less factory chain `TFoo.GetInstance.DoIt`).

extractPascalParenlessCall handles these, wired into visitPascalBlock scoped to
STATEMENT position only: a bare `Obj.Field;` statement is a no-op, so a
statement-level dot expression is a call — but a dot in assignment LHS/RHS or a
condition is left alone, since there it's genuinely ambiguous with a
field/property access. The chained paren-less form reuses the #750 chain encoding
(gated on the Delphi `TFoo`/`IFoo` type convention) and resolves the same way.

PascalCoin A/B: +1131 / -1 — purely additive, and all 1131 new edges resolve to
METHOD nodes (zero field/property false positives, confirming the statement-level
gate). 3 new synthetic tests (paren-less call, paren-less chained factory, and the
property-write/read non-extraction guard). EXTRACTION_VERSION 16->17. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:54:17 -04:00
af56f3539d fix(pascal): resolve chained factory calls TFoo.GetInstance().DoIt() (#750) (#791)
Ports the #645/#608 chained-receiver mechanism to Pascal/Delphi — which I'd
previously mis-scoped as blocked. The paren'd chained form extracts fine; it just
hit the chained-call gap like the others (with a decoy, `TFoo.GetInstance().DoIt()`
mis-resolved to a same-named method on an unrelated class).

- pascal.ts: getReturnType reads the method's `typeref` (a `function GetInstance:
  TBar` returns TBar; an interface return `IFoo` is captured too).
- tree-sitter.ts: extractPascalCall now re-encodes a chained call `TFoo.GetInstance().DoIt`
  (the exprDot's receiver is an exprCall) instead of collapsing it to bare `DoIt`.
  Gated on the Delphi type-naming convention (`TFoo`/`IFoo`) so a capitalized
  VARIABLE chain (Pascal capitalizes locals too — `Curve.X().Y()`, `Self.X().Y()`)
  stays bare and keeps its existing bare-name resolution.
- name-matcher.ts: `pascal` joins the dotted-chain gate + CHAIN_LANGUAGES +
  CONSTRUCTS_VIA_BARE_CALL (a `TFoo(x)` typecast yields a TFoo). When the factory's
  return type wasn't captured (a `constructor Create` has no `: TBar` but returns
  its class), resolve the method on the factory class itself. resolveMethodOnType
  validates, so a wrong inference yields no edge.

Validation: 4 synthetic tests (factory+decoy, constructor chain, typecast chain,
absent-method safety). Real-repo A/B on PascalCoin (772 files): +19 / -18 — 15 of
the -18 are correct class→interface retargets (`GetInstance(): IAsn1OctetString`
resolves `.GetOctets` on the declared interface, not baseline's concrete-class
guess); 3 are negligible drops (0.02%). EXTRACTION_VERSION 15->16. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:37:04 -04:00
d21d2dfa50 fix(objc): resolve chained message-send calls [[Foo create] doIt] (#750) (#786)
Ports the #645/#608 chained-receiver mechanism to Objective-C. A message send
whose receiver is itself a message send — `[[Foo create] doIt]` — used to drop
the receiver, so `doIt` name-matched a same-named method on an unrelated class
(commonly a test helper's `init` or an Apple-SDK method).

- objc.ts: getReturnType reads the method's `method_type`, SKIPPING nullability /
  ARC qualifiers (`nonnull instancetype` must yield instancetype, not `nonnull`).
- tree-sitter.ts: the message_expression branch now re-encodes a chained send
  `[[Foo create] doIt]` as `Foo.create().doIt` when the inner receiver is a
  capitalized class and the outer selector is unary.
- name-matcher.ts: `objc` joins the dotted-chain gate + CHAIN_LANGUAGES. A
  class-message factory returns an instance of the RECEIVER class by convention
  (`instancetype`), so when the factory's own return type isn't recoverable
  (`alloc`/`new`/`shared…` return instancetype, or aren't user nodes), the
  receiver's type is the class itself — this resolves the ubiquitous
  `[[X alloc] init]` and singleton chains. resolveMethodOnType validates against
  the class and its supertypes, so a wrong inference yields no edge.

Validation: 4 synthetic tests (factory+decoy, superclass conformance, absent-method
safety, the nonnull-instancetype singleton). Real-repo A/B on SDWebImage (208 files):
+35 / -75 — all corrections (the -75 are wrong `init` mis-matches to a test helper /
wrong class, retargeted to the right class's init in the +35, plus 2 Apple-SDK chains
on unindexed classes). db stable, no node explosion. EXTRACTION_VERSION 14->15.
Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:35:49 -04:00
16c73e2b0e fix(dart): resolve chained static-factory / constructor calls Foo.create().bar() (#750) (#762)
Ports the #645/#608 chained-receiver mechanism to Dart, plus makes Dart factory
and named constructors first-class so their chains can resolve at all. A call
whose receiver is itself a call — `Foo.create().bar()` (static factory or
factory/named constructor) — used to drop the receiver to a bare `bar`, which
name-matched a same-named method on an unrelated type (commonly a stdlib
`Option`/`Iterator` `.map`/`.where` mis-tied to the project's own class).

- dart.ts: extractBareCall now re-encodes `Foo.create().bar` when the chain
  starts with a capitalized type; getReturnType captures the return type (generic
  `List<Foo>` → `List`); factory (`factory Foo.create()`) and named (`Foo._()`)
  constructors are indexed as `Foo::create` / `Foo::_` with return type = the
  class (via resolveName + getReturnType + constructor_signature in methodTypes).
- The UNNAMED ctor `Foo()` is deliberately NOT extracted (isMisparsedFunction),
  so plain construction stays an `instantiates` edge to the class rather than a
  call to a phantom `Foo::Foo` method.
- dartCtorInfo validates a "constructor" against the enclosing class name, so a
  method tree-sitter MISPARSES as a constructor — `@override (A, B) m()`, where
  the annotation swallows the record return type and `m()` looks like a one-id
  constructor_signature — is still extracted as the method it is (regression
  found on localsend; covered by a new test).
- name-matcher.ts / index.ts: `dart` joins the dotted-chain gate,
  CONSTRUCTS_VIA_BARE_CALL (case construction), and CHAIN_LANGUAGES (conformance
  for superclass/mixin methods). resolveMethodOnType validates, so a wrong
  inference yields no edge.

Validation: 7 synthetic tests (static factory, factory/named ctor, construction,
conformance, absent-method safety, the misparse regression, instantiation-not-
hijacked). Real-repo A/B on localsend (368 Dart files): hand-written +17/-10 — all
corrections (the -10 = 7 wrong stdlib/extension misattributions removed + 3 ctor
source-renames), plus additive factory/named-ctor call resolution. Instantiation
preserved; no node explosion. EXTRACTION_VERSION 13->14. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:53:04 -04:00
2f96f58cbb fix(scala): resolve chained static-factory/apply calls Foo.create().bar() (#750) (#761)
Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call
whose receiver is itself a call — `Foo.create().bar()` (companion factory),
`Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the
receiver to a bare `bar`, which name-matched a same-named method on an unrelated
type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/
`.foreach` mis-attributed onto the project's own same-named class.

- scala.ts: `getReturnType` reads the `return_type` field — generic `List[Foo]`
  → container `List`, qualified `pkg.Foo` → `Foo`, `this.type` left undefined.
- tree-sitter.ts: re-encode `Foo.create().bar` when the inner call's receiver chain
  starts with a capital (companion factory / case-class apply); instance chains
  (`list.map().filter()`) stay bare.
- name-matcher.ts: `scala` joins the dotted-chain gate + CONSTRUCTS_VIA_BARE_CALL
  (case-class `apply` constructs the class); resolveMethodOnType validates, so a
  non-conventional `apply` returning another type yields no edge, not a wrong one.
- index.ts: `scala` joins CHAIN_LANGUAGES so trait-inherited methods resolve via
  the conformance second pass.

Validation: 4 synthetic tests (factory+decoy, case-class apply, trait conformance,
absent-method safety). Real-repo A/B on gatling (750 Scala files): +14 / -59 unique
edges — all corrections. The +14 are retargets (e.g. `HttpProtocolBuilder(cfg).baseUrl`
now resolves to HttpProtocolBuilder::baseUrl, not the same-named private BaseUrlSupport
helper); the -59 are wrong edges removed (stdlib Option/Iterator monad calls
mis-tied to the project's Validation::*, self-loops, decoy collisions) — zero genuine
factory chains dropped (verified: gatling has no real Validation.success().map() chains).
db stable at 40 MB. EXTRACTION_VERSION 12→13. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:09:33 -04:00
ccced9e358 fix(go): resolve chained factory-function calls New().Method() (#750) (#760)
* fix(go): resolve chained factory-function calls New().Method() (#750)

A Go call through a chained factory function — `New().Method()`,
`With(cfg).Build()` — dropped the receiver to a bare method name, which then
attached to a same-named method on an unrelated type (a wrong edge) or didn't
resolve. Ports the #645/#608 mechanism for Go's bare-factory receivers:

- Part 1: capture Go return types; a pointer `*Foo` -> `Foo`, a multi-return
  `(*Foo, error)` -> its first result, qualified `pkg.Foo` -> `Foo`.
- Part 2: encode a bare-factory chain (`New().Method`), gated to an `identifier`
  receiver so instance chains (`obj.Method().Other()`) keep bare-name.
- Part 3: matchDottedCallChain bare-inner Go branch looks up the FUNCTION's
  return type, then resolves+validates the method on it. Wired into the
  conformance pass so a method promoted from an embedded struct (`type Widget
  struct{ Base }` -> the existing `extends` edge) resolves. FALLBACK: when the
  inner isn't a resolvable function (a package-level VARIABLE holding a function
  value, e.g. gin's `engine()`), fall back to bare-name so the edge isn't dropped.

Validated: synthetic decoy + args + multi-return + embedded-conformance + absent
safety tests (4/4); full suite green. Real-repo A/B on gin (99 .go): pre-fallback
-40 = 25 wrong self-loops removed (good) + 15 correct `Engine::ServeHTTP` dropped
(gin's ginS variable-factory `engine()`); the fallback recovers the 15. gin A/B
re-confirm with the fallback is PENDING (local index flakiness, not a code issue).
EXTRACTION_VERSION 11 -> 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(go): stop the chained-call fallback from looping the batched resolver

The Go variable-inner fallback (for chains like `engine().ServeHTTP()` whose
inner is a package-level var, not a factory function) resolved the method via
a synthetic bare-name ref and propagated THAT ref as `.original`. Its
`referenceName` was the bare `ServeHTTP`, not the stored `engine().ServeHTTP`,
so `resolveAndPersistBatched`'s keyed `deleteSpecificResolvedReferences` no-oped,
the offset-0 batch never drained, and the loop re-resolved + re-inserted the
same rows forever — a runaway that grew a 99-file repo (gin) to 5,050,206 edges
/ 1.4 GB before filling the disk.

- name-matcher.ts: tie the bare-name match back to the original `ref` so the
  batch-cleanup delete matches the stored row and the loop drains.
- index.ts: add a non-progress guard to resolveAndPersistBatched — if the
  unresolved_refs table doesn't shrink after a batch, stop instead of growing
  the graph without bound (defense-in-depth for any future keyed-delete mismatch).
- resolution.test.ts: regression test for the variable-inner chain — asserts the
  fallback edge resolves AND the edge count stays bounded (no explosion).

gin A/B (post-fix): db 5.8 MB / 3,699 calls edges; net-zero unique-edge diff vs
main (the fallback recovers the dropped edges, adds no wrong ones). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:31:24 -04:00
5805f01957 fix(rust): resolve chained associated-function calls Foo::new().bar() (#750) (#757)
A Rust call through a chained associated function — `Foo::new().bar()`,
`Foo::with(cfg).build()` — dropped the receiver to a bare method name, which
then attached to a same-named method on an unrelated type (a wrong edge) or
didn't resolve. Ports the #645/#608 mechanism for Rust's `::` receivers:

- Part 1: capture Rust return types; `-> Self` yields the `self` marker (resolved
  to the impl's own type, like PHP), references/generics are unwrapped/reduced.
- Part 2: encode an associated-function chain (`Foo::new().bar`), gated to a
  scoped_identifier receiver so instance chains (`x.foo().bar()`) keep bare-name.
- Part 3: resolve via matchScopedCallChain (PHP's `::` resolver, generalized),
  validated by resolveMethodOnType. Wire Rust into the conformance second pass
  (matchScopedCallChain variant) so a chained method provided by a trait the type
  implements (`impl Trait for Type` → existing implements edges) resolves too.

Validated: synthetic decoy + args + Self + trait-default-conformance + absent
safety tests; full suite green (lone failure is the known-flaky #662 daemon test,
passes in isolation). Real-repo A/B vs main: clap (329 .rs) a net precision win —
**+937 added (96% correct builder methods), 622 wrong->right retargets**
(`Command::new().arg()` was mis-resolving to `ArgGroup::arg`, now `Command::arg`),
+162 net unique edges; the pure-drops are largely wrong bare-name edges the fix
correctly stops emitting. tokio-rs/bytes 0/0 (no regression). Known limit: the
single-hop mechanism re-encodes only the first hop of a chain (deeper hops keep
bare-name) — clap's unusually deep builder chains are partly covered.
EXTRACTION_VERSION 10 -> 11.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:41:59 -04:00
7c7f0dd56f fix(swift): resolve chained static-factory/fluent calls + nested-extension naming (#750) (#755)
Completes Swift in the #750 chained-call series (after Java #751, Kotlin #752,
C# #753, conformance #754). Two parts:

1. Swift chained-call resolution (the #645/#608 mechanism): capture Swift return
   types (positional, member types -> last segment), encode capitalized-receiver
   chains `Foo.make().draw()` / `Foo(args).draw()`, resolve+validate via the
   shared matchDottedCallChain (+ constructor branch). Fixes the decoy wrong-edge
   bug where a chained method dropped to a bare name and attached to a same-named
   method on an unrelated class.

2. Nested-type extension naming fix: `extension KF.Builder: KFOptionSetter` parsed
   as a class_declaration named `KF.Builder` (dot) — inconsistent with the type's
   own declaration `KF::Builder` (name `Builder`) — so the extension's conformances
   and members were invisible to a chained call on the type. A Swift resolveName
   now names a nested-type extension by its last segment (`Builder`), so its
   `implements`/`extends` edges and methods are found by the supertype walk
   (conformance #754) and the simple-name method match.

Validated: synthetic decoy + args + constructor + absent-method tests; full suite
green; nested-extension repro (`KF.url().onSuccess()` resolves via conformance to
the protocol method). Real-repo A/B vs main (conformance) — Alamofire and
Kingfisher both **0 added / 0 removed, node count unchanged**: NEUTRAL and SAFE.
The prior -168 Kingfisher regression (from the naming inconsistency) is eliminated;
Swift's unique-named fluent methods already resolved by bare name, so the chain
path lands the same edges — the value here is decoy-collision correctness, the
nested-extension naming fix, and consistency with the other four languages.
EXTRACTION_VERSION 9 -> 10.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:54:12 -04:00
48d4654e8d feat(resolution): conformance-aware chained-method resolution (#750) (#754)
* feat(resolution): conformance-aware chained-method resolution (#750)

A chained static-factory/fluent call whose method lives on a SUPERTYPE the
receiver conforms to — a protocol-extension method (Swift), an interface default
method, or an inherited superclass method — now resolves. resolveMethodOnType
falls back to walking the return type's implements/extends edges (via the new
context.getSupertypes) when the method isn't a direct member. Because those edges
don't exist during the single-pass resolution, a second pass
(resolveChainedCallsViaConformance) re-resolves the deferred chained refs after
edges are built. Still validated, so a wrong inference yields no edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): conformance-aware chained-method resolution (#750)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:38:37 -04:00
aa07dc59d4 fix(csharp): resolve chained static-factory calls Foo.Create().Bar() (#750) (#753)
A C# method called through a static factory or fluent chain —
`Foo.Create().Bar()`, `JObject.Parse(s).Property(...)`,
`Instant.FromUtc(...).InZone(zone)` — lost the receiver's type, so the chained
method didn't resolve and the call was invisible to callers/impact/trace. Ports
the #645/#608 mechanism to C# (additive, like Java #751):

- Part 1: capture C# return types in the extractor, reading the `returns` field
  (`static Foo Create()` -> `Foo`); predefined/array/generic/nullable/namespaced
  types are normalized or skipped.
- Part 2: encode a chained `member_access_expression` receiver
  (`Foo.Create(args).Bar()`) as `inner().Bar` with normalized empty parens, so
  factory calls that take arguments still split. Non-chained member calls keep
  their existing `recv.Method` text.
- Part 3: resolve via the shared matchDottedCallChain (now Java/Kotlin/C#),
  validated by resolveMethodOnType so a wrong inference yields NO edge.

Known limitation (safe): C# extension-method chains don't resolve, since the
method lives on the extension class, not the receiver's type — no edge, never a
wrong one.

Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on Newtonsoft.Json (945 .cs: +3, 0 lost) and nodatime (488 .cs:
+73, 0 lost) — node count identical (no explosion), 0 edges lost, precision
spot-checked verbatim (Instant.FromUtc().InZone(), Offset.FromHoursAndMinutes().Plus(),
OffsetDateTimePattern.CreateWithInvariantCulture().WithTwoDigitYearMax()).
EXTRACTION_VERSION 7 -> 8.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:30:03 -04:00
3e04650850 fix(kotlin): resolve chained companion-factory calls Foo.getInstance().bar() (#750) (#752)
A Kotlin method called through a companion-object factory, fluent chain, or
constructor — `Foo.getInstance().bar()`, `Config.create(opts).build()`,
`STMTransaction(f).commit()` — dropped the receiver to a BARE method name, which
then name-matched a same-named method on an unrelated class (a wrong edge) or
failed to resolve. Ports the #645/#608 mechanism to Kotlin:

- Part 1: capture Kotlin return types in the extractor. tree-sitter-kotlin
  exposes no field names, so the return type is read positionally (the type node
  after function_value_parameters); inferred/Unit/Nothing returns yield none.
- Part 2: encode a CLASS/companion-factory call-receiver chain as `inner().method`.
  Gated to a capitalized receiver (`Foo.getInstance()` / `Foo(args)`) so instance
  chains (`list.filter{}.map{}`) keep their bare-name behavior — re-encoding those
  would only drop the edge, regressing recall in fluent codebases.
- Part 3: generalize matchJavaCallChain -> matchDottedCallChain (shared by the JVM
  dot-notation languages); resolve the method on the factory's return type, or on
  the constructed class for a Kotlin `Foo(args).method()` receiver. Validated via
  resolveMethodOnType, so a wrong inference yields NO edge.

Validated: synthetic decoy + args + absent-method safety tests; full suite green;
real-repo A/B on arrow-kt/arrow (734 .kt) — node count identical (no explosion),
+49 validated-correct chained edges, and the removed edges are wrong bare-name
guesses the fix correctly stops emitting (419/438 from test/doc files; the 18
from product code are stdlib `.apply{}`, self-loops, and bare-name mismatches) —
a net precision improvement, ~0 correct product edges lost. Java path unchanged
(constructor branch is Kotlin-gated). EXTRACTION_VERSION 6 -> 7.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 00:12:37 -04:00
7f6bdf7ad1 fix(java): resolve chained static-factory calls Foo.getInstance().bar() (#750) (#751)
A Java method called through a static factory or fluent chain — `Foo.getInstance().bar()`,
`Config.create(opts).build()` — lost the receiver's type, so the chained method either
didn't resolve at all or (when a same-named method existed on an unrelated class) attached
to whichever class was indexed first. Ports the #645 (C++) / #608 (PHP) 3-part mechanism:

- Part 1: capture Java return types in the extractor (skip void/primitives/arrays,
  unwrap generics, strip package qualifier).
- Part 2: encode a chained-call receiver as `inner().method` with normalized empty
  parens, so factory calls that take arguments still split.
- Part 3: matchJavaCallChain resolves the chained method on the factory's return type,
  validated via resolveMethodOnType so a wrong inference yields NO edge (never a wrong one).

Validated: synthetic decoy + absent-method safety tests; real-repo A/B on google/guava
(3,227 files) — node count identical (no explosion), 0 edges lost, +1,507 unique chained
edges recovered, precision spot-checked verbatim (Splitter.on().split(),
CacheBuilder.newBuilder().recordStats(), GraphBuilder.directed().build(), nested
MultimapBuilder.linkedHashKeys().arrayListValues()). EXTRACTION_VERSION 5 -> 6.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:43:17 -04:00
eb5960b535 fix(php): resolve chained static-factory calls Cls::for($x)->method() (#608) (#749)
A method called through a PHP fluent static factory — `ApiClient::for($c)->createOrder()`,
the canonical Laravel per-credential/per-tenant client idiom — produced no
`calls` edge: the receiver of `->createOrder` is the `Cls::for(...)` static
call, whose result type was never recovered, so the edge was dropped and
`codegraph_callers` returned nothing.

Same shape as the C++ singleton/factory fix (#645), reusing its return_type
column + the chained-call mechanism:
- Capture PHP return types (getReturnType): `: self` / `: static` / `$this`
  stored as the `self` marker, a concrete `: Type` as its short name,
  primitives/unions dropped.
- Encode the chained scoped-call receiver as `Cls::for().method` so the
  resolver can split it (PHP-gated, in extractCall).
- New matchPhpCallChain: look up the factory's return type (`self` → the
  factory's own class; concrete → that class), then resolve AND validate the
  method on it — a wrong inference yields no edge, never a wrong one.

EXTRACTION_VERSION 4->5 (re-index to populate PHP return types + chained edges).

Validated on koel (1383 PHP files): node count identical (no explosion),
0 edges lost, +80 chained-call edges recovered; synthetic tests cover the
self-factory, concrete-return, namespace, decoy, and absent-method cases.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:10:01 -04:00
5b3f5e36db fix(go): attribute calls inside top-level closures to the var, not the file (#693) (#744)
A function called only from an anonymous func_literal at package level — a
cobra `RunE: func(){…}` handler, a goroutine literal, a callback closure
stored in a `var` — had its call leak to the FILE node, because the Go
var-initializer walk ran with an empty scope. So `callers`/`impact` showed
the function with a file (or no meaningful) caller, unlike JS/TS where an
arrow-in-const becomes a named node whose calls attribute correctly.

Scope the Go top-level var/const initializer walk to the declared symbol, so
a call nested in any func_literal initializer (struct field, slice/map,
nested closure) attributes to the enclosing var. EXTRACTION_VERSION 3->4
(re-index to pick up the corrected attribution).

Validated on cli/cli (858 Go files): node/edge counts identical, file-level
dependents byte-identical (no regression), and 62 top-level-closure calls
correctly moved from file-attributed to var-attributed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:05:53 -04:00
35b44e242c fix(scan): don't abort indexing on a non-UTF-8 or unparseable .gitignore (#682) (#743)
A .gitignore transparently encrypted in place by corporate DLP / endpoint
software (UTF-16 header + ciphertext), or one containing a pattern the
`ignore` library can't compile to a regex (`\[` -> "Unterminated character
class"), crashed the entire sync/index. The throw is LAZY — it surfaces at
match time (`ig.ignores()`), not `.add()` — so the existing add-time
try/catch never caught it, and the error never named the offending file.

Read .gitignore defensively: skip a file that isn't valid UTF-8 text whole
(NUL byte or fatal UTF-8 decode), drop only the individual uncompilable
patterns from a text one (probe-compile, then per-line fallback), and warn
with the file path. Indexing continues either way. The watcher inherits the
fix via buildDefaultIgnore.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:36:44 -04:00
6e2a24d96a fix(extraction): map PHP include/require to file→file dependency edges (#660) (#663)
PHP's importTypes only captured namespace_use_declaration, so
include/require(_once) — the dependency mechanism in procedural and
script-style PHP — never produced edges. callers, impact, and trace
missed the entire file-include graph; only namespace `use` became a
dependency edge.

Capture the four include/require expression types and emit file→file
imports edges, reusing the path-based resolution that C/C++ #include
already goes through. Only static string-literal paths are resolved
(relative to the including file); dynamic forms (include $var,
require __DIR__ . '/x', interpolated strings) are skipped.

Include PATHS are distinguished from namespace `use` symbols by shape: a
path contains '/' or '.', which PHP identifiers and FQNs never do. A
path-shaped include that doesn't resolve to a known project file is left
unresolved and does NOT fall back to the symbol name-matcher, which would
otherwise mis-connect "inc/db.php" to an unrelated db.php elsewhere — a
wrong edge is worse than a missing one.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
2026-06-08 20:31:15 -04:00
fd03f31b2c fix(cpp): resolve calls through singletons/factories/chained getters (#645) (#742)
A C++ method call whose receiver is another call's result — `Foo::instance().bar()`,
`WidgetFactory::create().draw()`, `openSession()->run()`, or the same stored in an
`auto` local first — lost the receiver's type during extraction. The callee degraded
to a bare method name, so when two classes shared a method name the call silently
resolved to whichever was indexed first (or not at all), corrupting callers / impact /
trace with a plausible-but-wrong edge.

Three parts:
- Capture C++ return types (new nodes.return_type column, schema v5): the
  function_definition's `type` field, normalized — smart-pointer pointee unwrapped,
  void/primitives dropped.
- Preserve the inner-call receiver in extraction: a C/C++ field_expression whose
  receiver is itself a call is encoded `inner().method` instead of dropping to the
  bare name. Other languages keep the existing behavior.
- New resolution strategy (matchCppCallChain): infer the receiver's class from the
  inner call's return type, then resolve AND validate the method on it. Handles
  singletons/accessors, factories returning a different type, free-function
  factories, make_unique/make_shared/new/direct construction, single-level member
  chains, and namespace-qualified inner calls. A wrong inference yields no edge,
  never a wrong one.

EXTRACTION_VERSION 2->3 (re-index to populate return types).

Validated on the issue repro + spdlog: node count stable (no explosion),
deterministic, and ~100 pre-existing wrong `.size()`-style edges removed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:18:17 -04:00
a56d9e6941 feat(directory): CODEGRAPH_DIR env var to override the index dir name (#636) (#741)
Two environments that share one working tree — most concretely Windows
and WSL — can't safely share a single `.codegraph/`: the daemon lockfile
records a platform-specific pid + socket (named pipe vs Unix socket), and
SQLite locking across the WSL2/Windows filesystem boundary is unreliable,
so two daemons over one index risks corruption.

Add a `CODEGRAPH_DIR` env var (default `.codegraph`) that overrides the
per-project data directory name, so each environment keeps its own index
in the same tree (e.g. `CODEGRAPH_DIR=.codegraph-win` on Windows). The
name is resolved live and validated (rejects separators / `..` / absolute,
falling back to the default with a one-time stderr warning). Indexing and
file-watching now skip ANY `.codegraph-*` sibling so neither side trips
over the other's data.

Routes the previously-hardcoded `.codegraph` literals (db path, lockfile,
error log, watcher ignore, file-scan skip, installer) through the
resolver. No extraction-version bump — index content is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:31:50 -04:00
636d9fcb7d feat(extraction): index string-literal names in generic tuple type aliases (#634) (#740)
TypeScript service/RPC contracts written as a tuple of generic types —
`type List = [Service<'query_apply_record', Req, Resp>, …]` — carry their
names only as string-literal type arguments, so static extraction never
indexed them and `codegraph query query_apply_record` returned nothing.

Add a narrow TS/TSX type-alias pass that emits each tuple entry's
string-literal name as a `method` node under the alias (qualifiedName
`List::query_apply_record`), making it searchable. Scope is limited to a
direct literal arg of a generic that is a direct tuple element, with a
valid-identifier filter — so utility types (Pick/Omit/Record), deeper
nested generics, and route paths produce no noise.

Bumps EXTRACTION_VERSION so existing indexes get a re-index hint.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:05:36 -04:00
112e278b5c fix(security): index config files by key only, never surface values (#383) (#722)
Spring `application.{properties,yml}` keys (and Shopify Liquid `{% schema %}`
blocks) were storing the config VALUE in the node docstring, and
`codegraph_explore`'s source section re-read the raw `key = value` line off
disk — so a secret committed to a config file (DB password, API key, JDBC URL
with embedded credentials) could be pushed into an agent's context via
explore/node output without the agent ever opening the file.

Config-leaf nodes (`kind: 'constant'` in a config language) now surface the KEY
only, via a shared `isConfigLeafNode` predicate applied at both surfacing
paths: the value is dropped from extraction, `getCode`/`includeCode` returns
the key instead of the file line, and explore excludes config leaves from
source rendering. The predicate can't match real code (real constants are
ts/java/go/…), so `@Value`/`@ConfigurationProperties` resolution and impact are
unaffected. Adds a regression test asserting a planted secret never appears in
`codegraph_explore` / `codegraph_node` output while the keys still resolve.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:10:10 -04:00
80db274e5f feat(csharp): index C# 12 primary constructors via an up-to-date grammar (#237) (#717)
Vendor tree-sitter-c-sharp 0.23.5 (ABI 15) for C#, replacing the bundled ABI-13
build that dropped primary-constructor classes. Adds native primary-ctor
parsing, primary-ctor parameter dependency edges, return-type extraction via the
renamed `returns` field, and a preParse that blanks `#if` directive lines the
new grammar mis-parses inside enum bodies. Validated on MediatR / eShopOnWeb /
Newtonsoft.Json + full suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:50:15 -04:00
4e5cf2de56 feat(cli): add codegraph upgrade self-update + stale-index re-index hint (#710)
`codegraph upgrade [version]` detects how the CLI was installed — the standalone
install.sh/install.ps1 bundle, npm-global, npx, or a source checkout — and
updates in place: re-running the canonical install.sh on macOS/Linux, an
in-place rename-and-extract swap on Windows (a running node.exe can't be
deleted, only renamed, so the detached-helper approach is avoided), and
npm/npx/source-specific guidance otherwise. Flags: `--check` (report only),
`--force`, and a positional version to pin.

Each full index is now stamped with the engine's EXTRACTION_VERSION in
project_metadata; `codegraph status` (and `--json`) flags an index built by an
older engine and recommends re-indexing, and `upgrade` prints the same reminder.
Gated on EXTRACTION_VERSION so it never nags on extraction-neutral releases.

Validated end-to-end on macOS (real bundle upgrade), Linux (Docker, real
curl|sh) and Windows (Parallels VM, real in-place swap). 32 new unit tests.

Closes #679

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:38:38 -04:00
07af3db6c7 feat(impact): cross-language blast-radius coverage (22 languages + 14 frameworks) (#708)
Completes the cross-file dependency graph behind impact / affected / explore across all 22 supported languages and 14 web frameworks, validated on real-world repos (measured fair-coverage table added to the README). Per-language resolution + framework resolvers/synthesizers (Lua/Luau require, Shopify OS 2.0 Liquid sections, Delphi forms, Rust cross-module + Rocket macros, Swift Fluent, SvelteKit/Nuxt loader/component conventions, RN/Expo bridges). 0 cross-family false edges, full suite green (1187 passed). See #708.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:02:59 -04:00
629d8472b1 fix(extraction): index Vue <template> component usages (#629 follow-up) (#659)
Vue's extractor parsed only the <script> block, so a component used solely
in another component's <template> (`<MyButton />`) produced no reference —
and thus showed a false 0 callers, even after the barrel-resolution fix in
PR #657. This is the Vue analogue of Svelte's extractTemplateComponents.

extractTemplateComponents() now scans the template (everything outside the
<script>/<style> blocks, which also handles nested <template> tags for
v-if/slots) for component tags:
- PascalCase tags (`<MyButton/>`) — captured as-is.
- kebab-case tags (`<my-button/>`) — converted to PascalCase so they match
  the imported component's name. Safe: an unmatched name creates no edge
  during resolution, so native custom elements just don't resolve.
- Native HTML elements (lowercase, no hyphen) and Vue built-ins
  (Transition, KeepAlive, …) are skipped.

Adds no nodes — only `references` — so node counts stay stable. With this
plus #657, a Vue component re-exported through a barrel and used only in a
template now resolves end-to-end (callers/impact/callees).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:44:27 -05:00
ddb1a8f72d fix: issue-triage quick wins (extraction, MCP probes, gitignore, CJK, impact) (#654)
Batch of small, localized fixes from an open-issue triage:

- .codegraph/.gitignore now ignores everything but itself, so the database,
  daemon.pid, sockets, and logs stop showing up in git status (#492, #484)
- MCP server answers resources/list and prompts/list with empty lists instead
  of -32601, clearing scary log lines in opencode/Codex (#621)
- index SAP HANA .xsjs/.xsjslib as JavaScript (#556) and TS .mts/.cts (#366)
- visit anonymous AMD/CommonJS/IIFE wrapper bodies so their inner functions and
  calls are indexed instead of coming up empty (#528)
- batch the changed-file lookup so a huge first sync no longer hits
  "too many SQL variables" (#540)
- list files with `git ls-files -z` so non-ASCII/CJK paths survive
  core.quotepath and are no longer silently skipped (#541)
- attach Go methods on generic receivers (*T[P]) to their type (#583, RC1)
- impact no longer climbs the structural `contains` edge, so a leaf symbol
  stops dragging in its sibling methods (#536)
- README: explicit `codegraph install` step, run in a new shell (#631)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:49:15 -05:00
Colby MchenryandGitHub 68eaf0dbd8 feat(mcp): codegraph_explore as the sole primary tool + store coverage + overload disambiguation (#647)
## Summary

Completes the explore-overhaul arc: `codegraph_explore` becomes the single primary tool an agent reaches for, and its coverage + output shape are tuned so flow/architecture questions resolve with near-zero Read/Grep.

### What changed
- **explore is the sole primary tool** — removed `codegraph_context` (the fuzzy-input Read-trigger) and `codegraph_trace` (under-picked by agents); explore already surfaces the call flow among the symbols you name. A plain natural-language question now works as the query.
- **Store/handler coverage** — functions defined inside object literals (Zustand `create((set, get) => ({ … }))`, Redux/Pinia/MobX, exported handler/route maps) are indexed as real symbols, including calls through `useStore.getState().fn()` and destructured `const { fn } = useStore.getState()`. A general AST rule, not a per-lib hack.
- **Overload disambiguation** — explore leads with the *right* definition when a method name is overloaded across types (a PascalCase type token in the query biases to that type's own def); `codegraph_node` returns *every* overload's body in one call, with an optional `file`/`line` selector to pin one.
- **Method-atomic render** — explore never returns half a method; at the size budget it drops whole methods/files (and lists what it dropped) instead of truncating a body mid-method.
- **Native-read-shaped output** — per-call output is capped to ~24K with a 25K hard ceiling and concentrated into ~150–250-line flow windows, mirroring how the agent natively reads; repo size scales the *call* budget, not the per-call size (a larger response just gets externalized to a file the host Reads back).
- **Blast radius** folded into explore (dependents + covering tests, locations only).

### Benchmark (refreshed on this build)
Re-validated the 7-repo A/B on 2026-06-02 (Opus 4.8, effort=high, median of 4). WITH arm re-measured on this build, WITHOUT reused:

**~16% cheaper · 47% fewer tokens · 22% faster · 58% fewer tool calls** — 0 file reads on 6 of 7 repos (Gin ~1).

The arc trades larger, cache-heavy explore responses for guaranteed near-zero reads, so cost/token margins soften vs the prior build (Excalidraw and Tokio land at cost break-even) while time and tool-calls stay clear wins everywhere — consistent with the project's stated optimization target (latency + tool-calls, not token cost).

### Validation
- Full suite green: **1112 passed, 2 skipped**.
- 28/28 plain WITH runs across the 7 README repos completed clean; reads median 0 on 6/7.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-02 10:15:27 -05:00
cdbf451440 fix(extraction): count all file-level-tracked langs (incl .properties) as indexed (#544)
Completes #357. The no-symbol file-level class is yaml/twig/properties, but the
count fix only covered yaml/twig — so a .properties-only project still printed
"No files found to index" even though the files were stored. Introduce a single
isFileLevelOnlyLanguage predicate (the canonical set behind the tree-sitter
no-symbol branch, xml excluded since its MyBatis extractor emits a file node)
and use it at both count sites and the extraction dispatch so the list can't
drift. Adds .properties regression coverage for indexAll() and indexFiles().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:47:33 -05:00
luo jiyinandGitHub 839cf63dcb fix: count file-level tracked yaml and twig as indexed (#357) 2026-05-28 22:43:49 -05:00
csw-chenandGitHub cea78ceb1b fix(windows): suppress console popup on child_process calls (#498)
On Windows, v0.9.5's detached shared daemon (#411) has no inherited console,
so any console-subsystem child it spawns gets a fresh visible console window
unless the spawn passes `windowsHide: true`. The fix adds the flag to all
ten `spawnSync` / `execFileSync` / `execSync` call sites across extraction,
sync, installer, and the WASM-flags relaunch. macOS/Linux ignore the option,
so this is a no-op elsewhere.

Fixes #485, #510, #530.

Co-authored work:
- #498 (csw-chen) — full sweep across extraction, sync, installer, and wasm-runtime. **This is the change being merged.**
- #505 (yushengruohui) — independently identified and fixed the 7 git execFileSync sites. Superseded by #498's broader sweep; same diagnosis.
- #521 (JirA44) — independently identified and fixed the WASM-runtime spawnSync re-exec. Superseded by #498's broader sweep; same diagnosis.

Validated on Windows 11 ARM64 (Parallels): a detached parent's 15 git spawns produce 15 visible black flash-windows without the fix and 0 with it.
2026-05-28 13:12:54 -05:00
71935e37c2 feat(mcp): multi-module Go trace-quality + small-repo retrieval tuning (#494)
* feat(go): generated-file down-rank + gRPC stub-impl bridge + trace-failure inlining

Multi-pronged fix to make codegraph competitive on Go multi-module repos
(cosmos-sdk, etcd) where it previously lost or tied. Driven by an 8-question
agent-eval audit across cobra, gin, prometheus, cosmos-sdk, and etcd: the
baseline had codegraph losing ~60% on cost on cosmos-sdk and mixed on etcd
deep cross-module flows, while winning cleanly on the single-module and
non-protobuf-heavy repos.

Diagnostics ruled OUT `go.work` parsing as the gap (prometheus crushes
without it). The actual failure modes were generated-file noise warping
disambiguation, missing gRPC interface→impl bridge in structural-typing Go,
and trace's failure path triggering 3-5 follow-up tool calls instead of
inlining the material the agent needed.

Changes:

- New `src/extraction/generated-detection.ts` — path-pattern classifier
  for `.pb.go`, `.pulsar.go`, `_grpc.pb.go`, `_mock.go`, `_mocks.go`,
  `mock_*.go`, `.generated.[jt]sx?`, `_pb2(_grpc)?.py`, `.pb.{cc,h}`,
  `.g.dart`, `.freezed.dart`. Applied as a stable sort tiebreaker in
  `findSymbol`, `findAllSymbols`, `codegraph_search` (MCP + CLI),
  `codegraph_explore` file ranking, and context formatter Entry Points /
  Related Symbols / Code blocks. Cosmos's `msgServer.Send` now ranks #3
  instead of #9 on a `Send` search.

- New `goGrpcStubImplEdges` synthesizer in `callback-synthesizer.ts` —
  detects `UnimplementedXxxServer` structs in generated files, identifies
  their RPC methods (excluding `mustEmbed*` / `testEmbeddedByValue` gRPC
  markers), and emits `calls` edges to the matching methods on any
  non-generated struct whose method-name set is a superset. Closes Go's
  structural-typing gap that the existing `interfaceOverrideEdges` (Java /
  Kotlin only) couldn't bridge. 467 bridge edges on cosmos-sdk; bank's
  `UnimplementedMsgServer::Send` points to `x/bank/keeper/msg_server.go`
  only, not to `msgClient` siblings or mock files.

- Trace-failure rewrite (`handleTrace`) — when no static path connects
  endpoints, instead of telling the agent to call `codegraph_node` (a
  3-4-call fan-out), inline both endpoints' bodies (120 lines / 3600 chars
  per endpoint), their callers (≤6), and callees (≤8) in one response.

- Trace endpoint-pairing improvements — scores every `from`×`to`
  candidate combo by shared directory prefix and tries the best-paired
  pair first (the full candidate set, not just FTS top-5). A
  less-canonical-path penalty (`enterprise/`, `contrib/`, `examples/`,
  `vendor/`, `third_party/`, `deprecated/`, `legacy/`) ensures the
  canonical-module pair wins even when a side-experiment shares more of
  its directory prefix. Find-path probe budget capped at 20 pairs.

- Test-file deprioritization in `codegraph_explore` `isLowValue` — adds
  suffix patterns (`_test.go`, `_spec.rb`, `.test.ts`, `.spec.tsx`,
  `Test.java`, `Spec.kt`) alongside the existing directory-style patterns.
  Otherwise etcd's `watchable_store_test.go` consumes 5K chars of explore
  budget that should go to the hand-written flow source.

Tests:

- New `__tests__/generated-detection.test.ts` (4 unit tests) pins the
  suffix patterns.
- New "Go gRPC stub→impl synthesis" integration test suite in
  `frameworks-integration.test.ts` (2 tests): positive bridge from stub
  to hand-written impl, AND the precision case (don't bridge to a
  generated sibling like `msgClient` in the same .pb.go).
- Full suite: 1076/1076 pass.

Empirical (post-fix, n=2 average per question):

| Repo / Q                | WITH       | WITHOUT     | Reads (W/WO) | Time (W/WO)
|-------------------------|------------|-------------|--------------|------------
| cobra (parse cmds)      | $0.27      | $0.27       | 0 / 4        | 39s / 60s
| prometheus (scrape→TSDB)| $0.63      | $0.70       | 0 / 6        | 106s/143s
| cosmos-sdk Q1 (MsgSend) | $0.41      | $0.26       | 1 / 2        | 67s / 64s
| cosmos-sdk Q2 (Delegate)| $0.47      | $0.46       | 0 / 5        | 50s / 73s
| cosmos-sdk Q3 (gov tally)| $0.34     | $0.31       | 1.5 / 3      | 54s / 76s
| etcd Q1 (Put→raft)      | $0.65      | $0.78       | 0 / 4        | 98s / 129s
| etcd Q2 (watch)         | $0.36      | $0.50       | 0 / 4+       | 58s / 89s

Codegraph wins on reads + time on every question. Cost is mixed: 3 clean
wins, 3 tied (within 10%), 1 stubborn cost loss on the grep-favored Q1.
Compared to baseline, the cosmos-sdk cost-gap collapsed from -60% to -15%
on average, and Q3 went from a 75% loss to a tie. Raw run artifacts in
`/tmp/cg-finalv2-*/` and `/tmp/cg-final-*/`.

Memory written at `project_go_multi_module_audit.md` for the methodology
+ before/after numbers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): auto-inline trace in codegraph_context for flow queries

When a codegraph_context task contains a flow keyword ("trace", "from",
"reach", "flow", "propagat", "how does", "how do") AND at least two
distinct PascalCase / camelCase identifiers, internally invoke trace
between the first two extracted symbols and splice the trace body into
the context response. Conservative trigger by design: false positives
waste one graph query; false negatives just fall back to the agent
calling trace itself (existing path-proximity wiring handles either
case).

Goal: collapse the agent's typical context → trace → explore sequence
into a single context call for clear flow queries, closing the
remaining cost-overhead gap on multi-call patterns. The path-proximity
+ less-canonical-path scoring + the trace-failure-inlined-bodies
behavior already let the inline trace land on the right endpoint pair
and return enough material that no follow-up codegraph_node/Read is
needed.

Doesn't fire on:
- cobra's "How does cobra parse commands and flags?" (no PascalCase
  symbols) — verified in regression run, no behavior change ($0.260
  WITH vs $0.257 WITHOUT, basically tied)
- queries where the agent doesn't call codegraph_context at all
  (cosmos Q1 in the audit went search → trace → node → trace → node)

Tests: 1076/1076 still pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): trace failure inlines TO file siblings to displace node fan-out

The cosmos-Q1 audit revealed a static-resolution gap: msgServer.Send's
*real* next hop is `k.Keeper.SendCoins` — an interface-method call on an
embedded field that tree-sitter can't resolve. The static getCallees list
for msgServer.Send is all utility/error functions (StringToBytes, Wrapf,
…). The actual flow (SendCoins → subUnlockedCoins → addCoins →
setBalance) lives entirely inside `x/bank/keeper/send.go`, which is also
where the TO endpoint (setBalance) lives.

When trace fails (no static path), inline the **top 5 functions/methods
in the destination file**, ordered by line-distance from the TO node.
This catches the flow that interface-method calls obscure — the
canonical "k.<Iface>.<Method>" pattern in Go, also relevant to Java
dependency-injection / Rails service-object dispatch / etc. where
interface dispatch hides the real call.

Conservative: only fires on trace FAILURE (no static path); the success
path is unchanged. Per-body cap (40 lines / 1200 chars), top 5 siblings.
Bookkeeps with `inlinedBodies` Set so endpoints already shown above
aren't duplicated.

Result: cosmos-Q1 — historically the most stubborn cost loss (-2.2× to
-39% across the audit) — flipped to a clean WIN: $0.257 WITH vs $0.449
WITHOUT (-43%), 34s vs 79s, 0 Reads vs 2 Reads + 5 Greps, 5 codegraph
calls vs 12. Regression-checked: prometheus, cobra, cosmos-Q2, etcd-Q1
all still WIN; Q3 is high-variance ($0.30-$0.45 range historically) and
fell within that on this run.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: extend coverage to all supported languages, not just Go

PR review feedback: the audit was Go-driven, so the patterns I added
were Go-flavored. Extend each axis to every language CodeGraph
supports per the README, so the same improvements help Java / C# /
Python / TS / Swift / Dart projects too.

**generated-detection.ts** — Added patterns for:
- TS/JS: `.gen.[jt]sx?`, `.pb.[jt]s`, `_pb.[jt]s`, `_grpc_pb.[jt]s`
  (ts-proto, gRPC-web, Apollo / GraphQL codegen, Hasura).
- Python: `_pb2.pyi` (mypy stubs from protobuf).
- C#: `.g.cs` (T4 / Razor codegen), `Grpc.cs` (protoc-gen-csharp).
- Java: `OuterClass.java` (protoc-gen-java), `Grpc.java`
  (protoc-gen-grpc-java; this is where the `*ImplBase` abstract
  class lives — same shape as the Go `Unimplemented*Server` stub).
- Swift: `.pb.swift` (protoc-gen-swift).
- Dart: `.pb.dart`, `.pbgrpc.dart`, `.chopper.dart`.
- Rust: `.generated.rs`.

**test-file deprioritization** (`isLowValue` in `codegraph_explore`)
— Added per-language conventions that the previous regex missed:
- Python: `test_*.py` (pytest discovery) and `*_test.py`.
- Ruby: `*_test.rb` (minitest) — `*_spec.rb` already covered.
- C#: `*Tests.cs`, `*Test.cs`, `*Spec.cs`.
- Swift: `*Tests.swift` (XCTest).
- Dart: `*_test.dart`.

**IFACE_OVERRIDE_LANGS** in `callback-synthesizer.ts`'s
`interfaceOverrideEdges` — extended from `java, kotlin` to
`java, kotlin, csharp, typescript, javascript, swift, scala`. Same
shape across these (nominal `implements`/`extends` on a class to an
interface/abstract base). Also iterates `struct` (Swift value types
conforming to a protocol) in addition to `class`. The existing
matchesSymbol-style logic and `getOutgoingEdges(..., ['implements',
'extends'])` work unchanged.

**CLAUDE.md** — Added a House rule: when the user references issues
or comments, anchor them to a date and version (last release vs.
last main commit vs. current branch tip) BEFORE concluding a fix is
incomplete. Issue #388 comments from May 25-27 were responding to
the released v0.9.5 / merged-PR-469 state — not to this branch's
in-flight work. The new rule walks through the disambiguation:
`grep -m1 '^## \[' CHANGELOG.md` for release version, `git log
--first-parent main -1` for main tip.

Tests: 1076/1076 still pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): tiny-repo tool gating + shorter tool descriptions

Two cumulative changes targeting the small-repo cost gap surfaced by
the cross-language audit:

1. **Tool descriptions trimmed** (~2.1KB total saved across 10 tools).
   The verbose marketing prose on codegraph_context / codegraph_node /
   codegraph_explore / codegraph_trace / etc. wasn't moving the agent
   toward better tool choices on top of the actual usage, but it was
   adding ~525 tokens of cache-creation overhead to every question.
   The trimmed descriptions keep the operational hints (e.g. "Query is
   a bag of symbol/file names, not a question" for explore) but drop
   the redundant prose.

2. **Dynamic tiny-repo tool gating** in `ToolHandler.getTools()`. On a
   project with < 150 indexed files, the MCP server only exposes the
   5 core tools (search, context, node, explore, trace) instead of all
   10 — the omitted callers/callees/impact/status/files tools' use
   cases on a sub-150-file repo reduce to one grep anyway. The MCP
   tool-defs overhead is the #1 source of cost loss on tiny repos
   (~$0.10-0.15 fixed cache-creation per question); cutting 5 tools
   drops that by ~50%.

   Effect on ky (~25 files, the worst pre-fix offender):
     - Before: $0.59 WITH vs $0.42 WITHOUT (+42% loss, n=1)
     - After:  $0.32 WITH vs $0.44 WITHOUT (-26%, **flipped to WIN**)

   Effect on cobra/sinatra/slim (50-80 files): still cost-loss, but
   the gating doesn't regress them — same call-count, same reads.
   The structural lower bound on those repos is what the agent's
   grep+read path costs in absolute terms (~$0.20-0.30).

   Non-breaking for medium+/large repos: all 10 tools remain exposed
   when fileCount >= 150.

Tests: 1076/1076 still pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): combined tiny-tier — smaller explore + tool gating (cobra/ky flip to WIN)

Combines the tool gating from the previous commit with a matching
explore-budget cut for projects under 150 files. The two together close
the cost gap that neither closes alone:

- Tool gating alone helped ky (WIN) but didn't move cobra/slim/sinatra
- Explore-budget cut alone helped slim slightly but regressed cobra
- COMBINED: cobra flips to WIN, ky stays a WIN, ky/cobra both clean

`getExploreOutputBudget(fileCount < 150)` returns:
  maxOutputChars: 13000     (was 18000)
  defaultMaxFiles:  4       (was 5)
  gapThreshold:     7       (was 8)
  maxSymbolsInFileHeader: 5 (was 6)
  maxEdgesPerRelationshipKind: 4 (was 6)
  includeRelationships: true   (kept ON — cheap structural signal)
  maxCharsPerFile: 3800        (unchanged — monotonic invariant w/ next tier)

This survives the cobra-regression-with-trim that the earlier
budget-only attempt suffered: with only 5 tools to choose from, the
agent doesn't fall back to extra codegraph_node calls when explore
returns less — there's no node call available.

Results on the four worst small-repo losses (combined intervention):

| Repo   | Files | WITH (combo)| WITHOUT     | Verdict (pre → post)     |
|--------|-------|-------------|-------------|--------------------------|
| cobra  | ~50   | $0.25       | $0.31       | loss → **WIN** (-19%)    |
| ky     | ~25   | $0.39       | $0.39       | -42% → tied              |
| slim   | ~80   | $0.31       | $0.24       | LOSS 31% → still LOSS    |
| sinatra| ~60   | $0.30       | $0.23       | LOSS 18% → still LOSS    |

sinatra/slim remain a cost-loss because their WITHOUT path is
structurally cheap (~$0.20 — fewer than 4 cheap grep+read calls).
Codegraph can't beat that absolute floor with any meaningful response.
Both still WIN on time + reads + tool-call count.

Tests: tier boundary cases updated to cover the new <150 / 150-499 /
500-4999 / 5000-14999 / >=15000 progression. Off-by-one guard updated
to include the new 149↔150 boundary. All 1076 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(context): trim maxNodes default to 8 on tiny repos

On a <150-file project the entire repo is grep-able in one turn, so the
20-node default `codegraph_context` was paying for a graph subset that
exceeds the agent's actual question. Cutting the tiny-repo default to 8
(typical 1-3 entry points + their immediate 1-hop neighbors) reduces
the context-tool response body without hitting sufficiency on the flow
shapes small repos actually contain.

Non-breaking: the agent can still pass an explicit `maxNodes` to
override; medium+ repos (>=150 files) keep the 20-node default.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mcp): pin the empirical 5-tool gating floor for tiny repos

n=2 audit on cobra/ky/sinatra ruled out cutting below 5 tools (search +
context + node + explore + trace) on the tiny-repo tier. The smaller
3-tool gate (search + context + trace) saved ~$0.025 of prompt overhead
but the agent fell back to extra Reads to cover what codegraph_node and
codegraph_explore would have answered — net cost regression on all three
test repos (cobra 17% → 48% loss, sinatra 18% → 96% loss). Documented
inline so future tuners don't re-try this dead-end.

No behavior change beyond the comment: the 5-tool gate remains the
production setting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(mcp): pin empirical lower bound on tool gating after n=2 micro test

Tested the hypothesis that exposing FEWER tools on micro repos (<50
files) would close the cost gap. Results:

- 1-tool gate (codegraph_search only):
  - ky:    +44% (worse than 5-tool +30%)
  - express: +107% (catastrophic — was -43% WIN with all 10)
  - cobra: +126% (way worse than 5-tool +17%)

The single-tool gate forces the agent to read everything because it
can't navigate the call graph. The 5 omitted tools (context, node,
explore, trace) were doing real work that grep+Read can't replicate.

Conclusion: 5 tools (search + context + node + explore + trace) is the
empirical lower bound on the tiny-repo tier. Cutting below regresses
EVERY tested repo. The remaining ~$0.04-0.08 of structural cost overhead
on tiny repos is unavoidable without sacrificing the value codegraph
provides at that scale (which would also make WITH = WITHOUT, defeating
the install).

Comment documents the dead-ends so future tuners don't relitigate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): iter3/iter4 — raise tool-gate to 500, sufficiency steering in context, hard-exclude low-value files

Three layered changes targeting the sinatra/slim/small-repo cost gap
that iter2's body-shrink failed to close (smaller bodies just pushed
the agent to Read instead):

1. **Tool-gate threshold 150 → 500** (`TINY_REPO_FILE_THRESHOLD`).
   Sinatra (~159 files) and slim (~200 files) have the same structural
   problem as cobra (

* feat(context): iter7 — core-directory boost to surface dominant-file siblings in search ranking

On projects with a single file holding the dense majority of internal
call edges (e.g. sinatra's `lib/sinatra/base.rb` at ~85% of in-file
edges), text search was favoring small focused extension files over the
core file. A small focused file like `multi_route.rb` wins on verbatim
name match + file-size normalization, burying the 1500-line core file's
longer method names (e.g. `route!` vs `route`).

Fix: detect the "dominant file" — the file whose in-file edge count is
≥3× the next candidate's — then add +25 to all results sharing its
directory prefix. This pulls the core file's siblings above
sibling-package extensions without hardcoding any repo structure.

`getDominantFile()` excludes test/spec files and generated files
(e.g. etcd's `rpc.pb.go` has 4× the in-file edges of `server.go` and
would otherwise hijack the boost toward generated protobuf stubs).
SQL pulls the top 20 candidates; path-pattern filtering handles what
SQLite LIKE can't express.

* feat(mcp): iter10+iter12 — routing manifest inline + probe-sweep harness

On small projects (<500 files) with a routing-shaped query, build a
URL→handler manifest directly from the graph (each `route` node joins to
its handler via `references`/`calls` edges) and inline the top handler
file's source. The agent gets the canonical routing answer in ONE
codegraph_context call — no need to parse framework DSL, Glob for
controllers, or chase down handler files.

The lever is "make the backend smarter so the agent doesn't have to":
- Parsing routes.rb / routes/api.php / urls.py DSL is the agent's job
  in the WITHOUT arm. Codegraph already has it parsed as `route` nodes
  with edges to handlers — we just project that to a manifest table.
- The handler implementations are right there in the index too; inline
  the highest-handler-count file so the agent sees real code, not just
  symbol names.

Results on the realworld template repos that were losing badly:
  rails-rw  +89% LOSS → -15% WIN  (agent often answers with 0-1 tool calls)
  laravel-rw  +29% LOSS → +12% (tight gap)
  gin-rw    +30% LOSS → +23% (still loss but smaller)
  flask-mb  +64% LOSS → +25% (smaller gap)

The residual losses are mostly the agent's defensive read behavior on
super-cheap-WITHOUT repos (express-rw still does 4 Reads even with a
19-row manifest + service file inlined). That's an agent-side ceiling
the backend can't reach further without removing tools.

Also lands `scripts/agent-eval/probe-sweep.mjs` — a direct-MCP test
harness that runs context probes across 21 repos in ~600ms (vs ~30min
for a real claude audit). Enables rapid iteration on backend changes:
edit tools.ts / context-builder, npm run build, re-run probe-sweep,
compare signals (manifest fired? handler file inlined? response size?)
before paying for a claude run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): first tool call awaits catch-up sync (no stale rows for deleted files)

`MCPEngine.catchUpSync()` reconciles the index against the working tree
after open (catching `git pull`/`checkout`/`rebase` and any edits or
deletes made while no server was running). It was fire-and-forget — so a
tool call landing in the first ~50-300ms could race past it and serve
rows for files that no longer exist on disk. The per-file staleness
banner can't help here, because that signal is populated by the file
watcher (not by catch-up).

The fix: `catchUpSync()` now pushes its promise into `ToolHandler` via
`setCatchUpGate(p)`; the first `execute()` call awaits the gate and then
clears it. Subsequent calls pay nothing. Catch-up rejections are logged
by the engine and swallowed by the handler so a transient sync failure
never breaks tools.

Most visible on the "deleted everything between sessions" case, where
MCP previously returned stale rows pointing at non-existent files.
Validated end-to-end on a 10,640-file VS Code index: with the gate, a
codegraph_search for "ExtensionHost" against an empty (but stale-DB)
directory returns "No results found" after the catch-up drains the DB;
without the gate, the same call returns 10 stale hits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): cover small-repo retrieval tuning + auto-trace + iface-override expansion

Add entries for work that landed on this branch but wasn't yet in
[Unreleased]: tiny-repo tool gating + sufficiency steering + budget
tier, auto-inline trace in codegraph_context, routing manifest inline,
core-directory ranking boost, JVM-only interfaceOverrideEdges extended
to C#/TS/JS/Swift/Scala, and the shorter tool descriptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:38:03 -05:00
Artem BambalovandGitHub 34240eb297 feat(jvm): resolve Java/Kotlin imports by fully-qualified name (#412)
Wrap top-level declarations of `.kt` / `.java` files in an implicit `namespace` node carrying the file's `package`, then resolve `import com.example.foo.Bar` through that qualifiedName index — so a Bar in Models.kt resolves correctly regardless of filename, a top-level function import binds to its declaration, Java↔Kotlin interop crosses cleanly, and same-name classes across packages no longer collide. Wildcard imports still go through name-matcher.

Also extracts Java/C# anonymous-class overrides (`new T() { ... }`) as first-class class nodes with their override methods. Phase 5.5 interface-impl then bridges T's abstract methods to the anonymous overrides automatically — including the lambda-returned `new T() { ... }` pattern common in guava (Splitter, CacheBuilder).

Concrete impact on macrozheng/mall (524 .java files, multi-module Spring + MyBatis): 524 namespace nodes, 862 imports edges newly resolve to Java symbols, 76 distinct `Criteria` classes preserved across packages with no merge. On google/guava (3,227 .java): 3,608 anonymous classes extracted, +2,534 interface-impl edges reach overrides hidden in `new T() { ... }` blocks.

Agent A/B playbook on small (spring-petclinic-kotlin, 38 .kt), medium (mall, 524 .java), large (guava, 3,227 .java) — 3 flow prompts × 2 runs/arm × 2 arms = 36 runs, claude-opus, headless. Spring repos: 0/0 Read/Grep with-arm, −27% wall-clock vs no-codegraph. Guava: 1.8 Read avg with-arm (vs 2.0 without) — improved by the anon-class extraction; residual is a lambda→SAM coverage gap orthogonal to FQN imports (filing follow-up).
2026-05-26 22:06:53 -05:00
NandhisandGitHub 893256b88e fix(extraction): capture top-level initializer and inline-object-method calls (#465)
The variable / method-definition extractors never walked top-level
initializer values or inline-object method bodies, so calls like
`const token = getTokenMp()` and `methods: { save() { getTokenMp() } }`
showed up nowhere in `codegraph_callers`. The variable extractor now
walks any non-object initializer value; the method-definition extractor
still skips synthetic nodes for inline-object methods (noise rationale
unchanged) but now walks their bodies for calls. Surfaces in plain
`.ts`/`.js` files as well as Vue SFCs (`<script setup>` initializers +
Options API `methods: {...}` / `setup()`), which is where the bug was
originally reported.

Closes #425.
2026-05-26 19:15:32 -05:00
Colby MchenryandGitHub 186632fa88 fix(extraction): TS type-alias object members are first-class nodes (#359) (#471)
A call site `recorder.stop()` where `recorder: RecorderHandle` and
`type RecorderHandle = { stop: () => Promise<void> }` used to attach
its edge to an unrelated `class Foo { stop() {} }` in a sibling
directory — there was no `RecorderHandle::stop` node, so the existing
camelCase/path-proximity scoring picked the only `stop` method in the
graph (which happened to be wrong). False-positive `calls` edges
silently widened `codegraph_impact` blast radius.

`extractTypeAlias` now surfaces object-shape (and intersection-type)
members as first-class graph nodes:

  type X = { foo: T; bar(): T };
  ->  X        (type_alias)
      X::foo   (property)
      X::bar   (method)

Function-typed properties (`stop: () => Promise<void>`) emit as `method`
kind so `obj.stop()` resolves to them at the call site — same node
kind the existing receiver-name/word-overlap heuristic in
`matchMethodCall` already prefers. No new resolver logic needed.

Walk only immediate `object_type` / `intersection_type` operands of the
alias value. Anonymous nested object types inside generic arguments
(`Promise<{ ok: true }>`) intentionally don't produce phantom members.

Validation on excalidraw/excalidraw (314 .ts files):
  +776 new property nodes (alias non-function members)
  +1,008 new method nodes (alias function-typed properties + method_signatures)
  +226 calls edges newly accurate against alias members

User's exact 3-file repro:
  before: finaliseRecording -> StdioMcpClient::stop (wrong, sibling dir)
  after:  finaliseRecording -> RecorderHandle::stop (correct)
  StdioMcpClient::stop callers: voice/ false-positives gone

Closes #359.
2026-05-26 17:35:26 -05:00
Colby MchenryandGitHub 046e03a05f fix(extraction): C# produces references edges for type annotations (#381) (#470)
Indexing any C# project produced zero `references` edges, so
`codegraph_callers SomeDto` returned no hits even when the DTO was used
as a param/return type across the codebase, and `codegraph_callees` on
a service class only saw its `using` imports — the headline structural
query silently degraded to text-search on half of every typical backend
stack.

Two root causes:

1. `csharp.ts` was missing `returnField` (default `'return_type'` doesn't
   exist on C# AST; the field is `'type'`) AND had
   `paramsField:'parameter_list'` (the node TYPE, not the field NAME
   `'parameters'`) — so parameter type extraction silently no-op'd.
2. `extractTypeRefsFromSubtree` only emitted refs for `type_identifier`
   leaves. C# tree-sitter doesn't produce `type_identifier` — it uses
   `identifier`, `predefined_type`, `qualified_name`, `generic_name`,
   `array_type`, `nullable_type`, `tuple_type`, etc.

Fix:

- `csharp.ts`: `paramsField:'parameters'`, `returnField:'type'`.
- Route C# through a dedicated `extractCsharpTypeRefs` +
  `walkCsharpTypePosition`. Descends ONLY into known type fields
  (`parameter.type`, `method.type`, `property.type`,
  `variable_declaration.type`, `tuple_element.type`), so parameter
  NAMES like `request` in `Build(UserDto request)` never leak as type
  refs.
- Hook `extractField` and `extractProperty` to call
  `extractTypeAnnotations` so property/field type refs land in the graph.

Validation on dotnet/eShop (527 .cs files):
  C# `references` edges: 35 -> 925 (+26x)
  No regression in calls/imports/instantiates/extends/implements.

Closes #381.
2026-05-26 17:23:17 -05:00
Colby MchenryandGitHub f1b79eeae1 fix(resolution): Go cross-package qualified calls resolve via go.mod (#388) (#469)
`pkga.FuncX(...)` cross-package calls in Go monorepos were dropping
through the import resolver — `isExternalImport(go)` flagged any
non-`/internal/` import as third-party because the resolver had no idea
what the project's own module path was. Resolution fell back to name
matching with path-proximity scoring, which on a layered codebase picks
one accidental candidate per call site (~<1% recall per #388's
5,303-vs-1 figure).

- `src/resolution/go-module.ts` (new) parses the `module ...` directive
  from project-root `go.mod`, exposed via `getGoModule()` on
  `ResolutionContext`.
- `isExternalImport(go)` treats `<module-path>/...` imports as in-module;
  the existing `/internal/` escape hatch is preserved for repos without
  a parsed go.mod.
- `resolveViaImport` gets a Go cross-package branch that strips the
  module prefix to a project-relative directory, then resolves the
  qualified member via `getNodesByName(member)` filtered to that exact
  directory and `isExported=true`. Sub-packages don't collide with their
  parents; same-name funcs in different packages don't false-merge.
- Go extractor sets `isExported` from the identifier's first character
  (Go's universal uppercase=exported convention). The resolver depends
  on this to filter candidates.

Validation on gRPC-Go (1,031 .go files, layered package tree):
  total `calls` edges:    23,803 -> 34,105 (+43%)
  cross-pkg `calls`:      10,880 -> 19,929 (+83%)
  fmt/strconv/etc. stdlib calls: stay external (no false positives)

Tests cover in-module disambiguation with same-name funcs in two
packages, aliased imports, and stdlib calls not being false-resolved to
in-project nodes.

Closes #388.
2026-05-26 17:14:35 -05:00
TheSunnandGitHub 7e0d9b9ec0 fix(extraction): extract type refs from TS interface property and method signatures (#432)
Types that appeared only in TypeScript interface members — property
signatures like `value?: Partial<IPage>` and method signatures like
`fetchPage(arg: IPage): IOrderField` — were not being captured at
extraction time, so the resolver never built `references` edges for
them. `codegraph_impact`/`codegraph_callers` on the named type missed
every consumer that imported it solely to use it in an interface shape.

Add a `property_signature` / `method_signature` branch in `visitNode`:
when inside a class-like node (which covers interfaces) and the
language supports type annotations, call `extractTypeAnnotations` with
the parent (interface) node ID as the edge source. No property/method
node is created — only unresolved references that the resolver wires
the same way it wires field and parameter type references elsewhere.
2026-05-26 17:01:45 -05:00
2543ae565a feat(java): trace Spring/MyBatis enterprise flow end-to-end (#389) (#468)
Closes three gaps that broke `trace(controller, mapper-xml)` on real Spring +
MyBatis projects:

1. **Field-injected concrete-bean trace.** Java `this.<field>.method()` is
   unwrapped at extraction (was surfaced as `this.<field>.method` and dropped
   through every name-matcher strategy). The receiver name is then looked up
   in the enclosing class's field declarations to get the declared type and
   resolve the method on it. Closes the controller→bean hop when the field
   name doesn't capitalize to the type (`userbo` → `UserBO`). General Java
   fix, not Spring-specific.

2. **MyBatis XML mapper as a first-class language.** New extractor parses
   `<mapper namespace="..."><select|insert|update|delete|sql id="X">` and
   emits method-shaped nodes qualified as `<namespace>::<id>`, plus
   `<include refid="X"/>` references to `<sql>` fragments. Non-mapper XML
   (pom, log4j, web.xml) → file node only. A new synthesizer
   (`mybatisJavaXmlEdges`) joins Java mapper methods to XML statements by
   suffix-matching qualified names. Ambiguous simple-name collisions dropped
   for precision.

3. **Spring `@Value`/`@ConfigurationProperties` → application config.**
   `application.{yml,yaml,properties}` + profile variants parse on the
   framework path; each leaf key becomes a `constant` node qualified by its
   dotted path. `@Value("${k}")` / `@Value("${k:default}")` and
   `@ConfigurationProperties(prefix="X")` emit binding nodes that resolve
   with Spring's relaxed binding (kebab↔camel↔snake).

Validated on macrozheng/mall-tiny: full chain
`UmsRoleController.listResource → UmsRoleService.listResource → impl →
UmsResourceMapper.getResourceListByRoleId → XML <select>` connects across 5
hops via static + synthesized edges. 11/11 @Value annotations resolved
(incl. `@ConfigurationProperties(prefix="secure.ignored")`); 6/6 custom-SQL
mapper methods bridge to XML.

Tests: 4 new integration tests in frameworks-integration.test.ts. Full
suite: 1005 passed.

Docs: CHANGELOG `[Unreleased]` entry + dynamic-dispatch-coverage-playbook
narrative + matrix row.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:34:30 -05:00
c0cf9c1e7d fix(cpp): resolve callers for typed pointer method calls (#445)
Resolves typed member-pointer method calls like `m_cpAlg->Processing()` so `codegraph callers CDetect::Processing` returns the expected callers.

- Extract C/C++ `field_expression` member calls as receiver-qualified references, so `ptr->method()` is preserved as a receiver-aware reference.
- Surface out-of-line C++ method definitions (`int CDetect::Processing() {...}` in `.cpp` with class in `.hpp`) as proper method nodes with the correct qualified identity.
- C++ receiver-type inference: declarator regex requires a terminator after the receiver (rules out matching `return m_cpAlg->...`), handles `Type*x`/`Type *x`/`Type* x` uniformly, and rejects C++ keywords as a final guard.
- `resolveMethodOnType` matches by `Class::method` qualified-name suffix, so out-of-line definitions across files resolve (typical `.hpp`/`.cpp` split).

Validated on bitcoin-core (1306 .cpp files): 38,180 → 40,503 cpp method incoming-call edges (+6.1%), deterministic across re-indexes. Regression test added for the ambiguous-name + `return ptr->m()` / `Type x = ptr->m()` patterns.

Closes #445

Co-authored-by: chenyuxuan <458254969@qq.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:17:07 -05:00
Colby MchenryandGitHub 4d1a2b3c4d feat(resolution): mixed iOS / React Native / Expo cross-language bridging (#430)
Implements the design from `docs/design/mixed-ios-and-react-native-bridging.md`.
Closes the cross-language flow gap so `trace` / `callers` / `callees` / `impact` connect end-to-end across language boundaries in real iOS, React Native, and Expo codebases.

## Bridges shipped

| Boundary | Mechanism | Real-codebase validation |
|---|---|---|
| **Swift ↔ Objective-C** | Resolver applying Apple's @objc auto-bridging name math + Cocoa preposition prefixes | Charts (S, 269) · realm-swift (M, 369) · wikipedia-ios (L, 1734) |
| **React Native legacy bridge** | Resolver parsing `RCT_EXPORT_MODULE` / `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` (ObjC) + `@ReactMethod` (Java/Kotlin) | AsyncStorage (S, ~60) · react-native-svg (M, ~700) · react-native-firebase (L, ~1100) |
| **React Native TurboModules** | Resolver treating `Native<X>.ts` spec interface as ground truth | via RNSvg + RNFirebase subsets |
| **Native → JS events** | Synthesizer matching native `sendEventWithName:`/`emit(...)` to JS `addListener('e', handler)` keyed by literal event name; falls back to enclosing constant/variable for wrapper-API parameter handlers | RNGeolocation (S) · RNFirebase (L) |
| **Expo Modules** | Framework extract synthesizes `method` nodes from Swift/Kotlin `Module { Name("X"); Function("y") { ... } }` DSL | expo-haptics (S, 14) · expo-camera (M, 72) · ExpoSweep (L, 332, 7 packages) |
| **Fabric + legacy Paper view components** | Extract `component` + `property` nodes from Codegen `codegenNativeComponent<Props>('Name', ...)` specs AND legacy `RCT_EXPORT_VIEW_PROPERTY` / `@ReactProp` macros, then synthesize component → native class by name+suffix convention | react-native-segmented-control (S, legacy) · react-native-screens (M, Codegen) · react-native-skia (L, hybrid monorepo) |

## Bug fixes surfaced along the way

- `tree-sitter.ts` message_expression — multi-keyword ObjC call sites now reconstruct `a🅱️` selectors so they resolve to multi-part method definitions (gap discovered post-#165; 0 → 84 call edges to `GET:parameters:...` style methods on AFNetworking).
- `src/index.ts` resolver lifecycle — `indexAll()` now re-initializes the resolver after extraction so framework `detect()` sees the populated index. Pre-existing latent bug that affected UIKit and SwiftUI resolvers too.
- `src/extraction/index.ts` `buildDetectionContext` — added `listDirectories` so framework detect() can probe monorepo subpackages uniformly (fix needed for react-native-skia detection).

## Regression check on 5 control repos

| Repo | Result |
|---|---|
| Express (small JS) |  unchanged — 266 routes, express framework detected |
| Excalidraw (medium TS/React) |  9284 nodes (CLAUDE.md baseline ~9290); canonical `trace(mutateElement, renderStaticScene)` returns the flow |
| Django realworld (Python) |  django framework detected, 16 routes |
| Spring petclinic (Java) |  spring framework detected, 17 routes |
| Texture (pure ObjC, large) |  exactly matches #165 baseline: 4702 methods, 894 classes, 808/808 file coverage, 913 multi-keyword selectors, 55 protocols, 1036 properties |

## Tests

928 passing (+87 net new bridge tests across the 5 channels); 2 pre-existing skips. The mcp-staleness-banner / watcher parallel flakiness is unchanged by this work (different test fails each run, all pass in isolation; pre-existing on main).

## Documentation

- README: new 'Mixed iOS / React Native / Expo bridging' section with the per-boundary table and validation-corpus links.
- CHANGELOG `[Unreleased]`: full entry per bridge with measurements.
- `docs/design/mixed-ios-and-react-native-bridging.md`: the design doc (§8 measurements filled in across §8a-§8g).
- `docs/design/dynamic-dispatch-coverage-playbook.md` §6 coverage matrix: six new rows.
- `.claude/skills/agent-eval/corpus.json`: four new sections covering 15 real GitHub repos for the eval harness.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-26 02:14:00 -05:00
0x1306a94andGitHub 61153f96ee feat(extraction): add Objective-C language support (#165)
Adds tree-sitter-objc extractor for `.m`/`.mm` files and `.h` files
that content-sniff as Objective-C (`@interface`/`@implementation`/`@protocol`/`@synthesize`).

Extraction covers:
- `@interface` / `@implementation` (deduplicated into a single class node)
- `@protocol` (as `protocol` nodes via new `interfaceKind` config)
- Methods with full multi-part selectors (`doThing:with:`, not just `doThing`),
  including `+`/`-` static distinction
- `@property` declarations
- Inheritance (`extends`) and protocol conformance (`implements`)
- C-style `function_definition` and `#import` (both `<system>` and `"local"` forms)
- Call edges from both `call_expression` and `message_expression`,
  with `self`/`super` skipped on qualified callee names

Two new generic hooks on `LanguageExtractor` (`resolveName`,
`extractPropertyName`) handle the cases where the default name walk
doesn't fit; usable by future languages with similar shape.

Import resolver tries `.h`, `.m`, `.mm` for `objc` imports.

Validated on AFNetworking (84 files, 100% file coverage), RestKit
(282 files, 99.6%), and Texture (926 files, 100%, heavy `.mm`
content) — multi-keyword selectors preserved up to 7 parts, no parse
failures on ObjC++.

Known limitations (disclosed in README):
- Categories produce duplicate class nodes (one per category file)
- Chained/nested message sends record only the innermost method
- `[Class alloc]` patterns don't emit `instantiates` edges
- `@protocol Foo <Bar>` refinement lists not yet wired to `implements`
- Heavy C++ in `.mm` files may parse incompletely under the ObjC grammar
2026-05-26 00:31:43 -05:00
b09b23cf54 fix(watcher): exclude ignored dirs before watching to prevent inotify exhaustion (#276)
The file watcher registered a recursive watch over the entire project (node_modules, build output, caches included) and filtered only in the callback — exhausting the Linux inotify budget on large repos (#276). It now uses chokidar and excludes the same directories the indexer ignores (built-in default-ignore set + the project .gitignore) BEFORE registering a watch, so the watch count on a 900-dir node_modules drops from ~1200 to ~14 even with no .gitignore. Stacks with the shared daemon (#411): one watcher across agents, now small.

Also hardens the #411 daemon lockfile against a concurrent-startup race the new watcher timing made reproducible — the lock is now created atomically with its content (temp-write + hard-link), so racing daemons can never both win. Validated on macOS, Linux (Docker), and Windows (chokidar + fs.linkSync on NTFS).

Co-Authored-By: Colby McHenry <me@colbymchenry.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:12:44 -05:00
832f9063b1 feat(index): default-ignore dependency/build/cache dirs (#407) (#417)
node_modules and other dependency/build/cache dirs were indexed whenever a project lacked a .gitignore excluding them (common in non-git projects), flooding context/search with third-party symbols. Added a built-in default-ignore set spanning every supported language/framework (node_modules, vendor, dist, build, target, .venv, __pycache__, Pods, .next, etc.), applied UNIFORMLY in both the git and non-git enumeration paths — including to tracked files, since committing/vendoring a dependency dir doesn't make it project code. The only opt-in is an explicit .gitignore negation (e.g. !vendor/). First-party-prone names (packages, lib, app, bin, src, deps, env) are deliberately excluded from the list.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 18:15:43 -05:00
4a94696e44 fix(sync): filesystem-based change detection (catch git pull & non-git edits) (#414)
* fix(sync): detect changes via filesystem, not git status

Incremental sync detected changes with `git status --porcelain`, which only sees uncommitted working-tree changes — so committed changes from git pull/checkout/merge/rebase (clean tree afterward) were never reconciled, and non-git projects leaned on a slow full rescan. Change detection is now filesystem-based and git-independent: a (size, mtime) stat pre-filter skips unchanged files, then a content hash confirms the rest; removals are checked against the filesystem (git ls-files still lists deleted-but-unstaged files). Also adds a non-blocking catch-up sync on MCP connect so changes made while the server was down (e.g. a terminal git pull) are reconciled on connect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): add 0.9.5 entry for filesystem-based sync fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:36:09 -05:00
025ebc88d6 Release 0.9.4: framework-aware routing + dynamic-dispatch coverage + retrieval improvements (#365)
* feat(resolution): close dynamic-dispatch coverage holes (callback synthesis + django ORM)

Static tree-sitter extraction misses calls whose target is computed or indirect,
so flows through callbacks, observers, and descriptors were absent from the graph.

- callback-synthesizer.ts: whole-graph pass after base resolution. Detects
  registrar/dispatcher channels (field-backed observers + string-keyed
  EventEmitters), correlates registration sites, and synthesizes
  dispatcher->callback `calls` edges (provenance:'heuristic'). Records the
  registration site (registeredAt) in edge metadata. Precision guards: named
  handlers only, registrar-name match, event fan-out cap.
- frameworks/python.ts + resolution/{index,types}.ts: claimsReference hook +
  django ORM resolver (_iterable_class -> ModelIterable.__iter__).
- extraction/tree-sitter.ts: extract named nested functions so inline named
  handlers become linkable nodes.

trace(mutateElement, triggerRender) and trace(_fetch_all, execute_sql) now
connect; node count stable (no explosion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): self-sufficient flow output + fix explore budget regression

- Surface synthesized-edge evidence in trace, the node trail, and context call
  paths: a dynamic-dispatch hop now shows "callback via onUpdate @App.tsx:3148"
  with the registration site inline (and trace inlines each hop's call-site
  source line) -- the exact glue agents previously Read/Grep'd to reconstruct.
- Fix non-monotonic explore output budget: the 500-5000 file tier capped
  maxCharsPerFile at 2500, BELOW the <500 tier's 3800, so on god-file projects
  (excalidraw's 415 KB App.tsx) one explore returned <1% of the file and forced
  a Read. Raised to 6500/file, 28000 total.
- Stop explore from inviting Read: truncation/trim notes said "use Read for
  more"; they now steer to another codegraph_explore and treat returned source
  as already Read.

Measured on excalidraw: best-case flow answer went from 5 reads / 131s to
0 reads / 73s with ~3-4 codegraph calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(agent-eval): coverage probes, block-read hook, and design docs

Dev-only validation harness for the dynamic-dispatch coverage work:
- probe-{trace,node,context,explore}.mjs: drive MCP tools against a built index
  without a full agent run.
- block-read-hook.sh + hook-settings.json: PreToolUse experiment that denies
  source Reads to measure codegraph sufficiency (forced Read-0).
- docs/design/: callback-edge-synthesis + dynamic-dispatch-coverage playbook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): bridge React boundaries — re-render + JSX child synthesis

Closes the two dynamic-dispatch hops that broke "state mutation -> on-screen
render" flows in React apps. Both are call-invisible (React-internal) but the
code between them is fully call-connected, so one synthesized edge each makes the
whole flow trace end-to-end.

- reactRenderEdges: setState(...) re-runs the component's render(). For each
  class with a render method, link sibling methods calling this.setState ->
  render. The setState gate keeps it to React class components.
- reactJsxChildEdges: a component that returns <Child .../> mounts Child. Link
  parent -> each capitalized JSX child, resolved to a component/function/class
  node (the resolution gate drops TS generics like Array<Foo>). File-oriented,
  capped per parent.
- Surface both in synthEdgeNote (trace + node trail) and context call-paths.

Validated on excalidraw: trace(mutateElement, renderStaticScene) now connects in
6 hops across callback -> react-render -> jsx-child; 1 + 46 + 280 synthesized
edges, node count stable (no explosion). Partial coverage is worse than none:
react-render alone raised agent reads (revealed a hop it then drilled); adding
the jsx hop closed the flow and dropped reads to 0-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(claude): retrieval performance contract + coverage validation methodology

Add a "Retrieval performance & dynamic-dispatch coverage" section so future
changes/PRs don't silently regress agent retrieval:
- the explore call+output budget table by repo size, with the monotonic-per-file
  invariant (the bug that started this: <5000 tier's 2500 < <500 tier's 3800).
- the "partial coverage is worse than none" principle.
- the required validation methodology (small/medium/large x >=3 prompts per
  language x framework; deterministic probes + agent A/B; pass bar).
- the Excalidraw worked example (before/after numbers) as the template to
  replicate for every language/framework.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(claude): use full n=4 measured range in Excalidraw worked example

Best run 0 Read/3 cg/76s; typical ~1 Read/~4 cg; occasional over-drill outlier.
Report the range, not a single run — run-to-run variance is large.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): steer flow questions to codegraph_trace first (tightens variance)

codegraph_trace was absent from every steering intent map — all three guidance
files routed "how does X reach Y" to context+explore, never to the trace tool.
So agents used trace only by chance; when one didn't, it floundered
reconstructing the path with search+callers (an 18-call run vs ~6 for trace-users).

Add codegraph_trace to the intent map + a "flow" common chain (trace from->to
FIRST = the whole path in one call, then ONE explore for bodies) across all three
synced files (server-instructions, instructions-template, .cursor rule).

Validated on excalidraw (hard "to the screen" Q, n=4 before/after):
- call count 3-10 -> 3-4 (over-drill outlier gone)
- duration 64-112s -> 51-74s
- trace adoption 3/4 -> 4/4; search+callers path-reconstruction -> 0
- fully-clean runs (0 Read, 0 Grep) 0/4 -> 2/4; best 3 cg / 0 / 0 / 51s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Vue SFC template coverage (events + kebab components)

The .vue extractor only parses <script>, so template usage is invisible —
handlers and kebab child components used only in <template> have no edge. Add a
vueTemplateEdges channel (scoped to the <template> block of .vue files):
- event bindings: @click="onClick" / v-on:submit="save" -> handler method/function
  (skips inline arrows and $emit; resolves same-file first to avoid cross-app
  mis-match in monorepos).
- kebab child components: <el-button> -> ElButton (PascalCase children like
  <VPNav/> are already caught by the JSX channel via the SFC component node).

Surface vue-handler in synthEdgeNote (trace/node trail) + context call-paths.

Validated on vue repos (reindex, no node explosion):
- vue-handler edges: vitepress 15, vben 404, element-plus 603 — all precise
  (code-login @submit -> handleLogin, register @submit -> handleSubmit, ...).
- callers(handleLogin) now includes the login component (was 0); each monorepo
  app's login resolves to its own same-file handler.
- composition: PascalCase + kebab work; element-plus's el-/filename naming
  (el-button -> button.vue) is a known library-prefix limitation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Vue validation in coverage matrix + limits

Vue / Nuxt row →  template events + composition (vitepress S / vben M /
element-plus L); 🔬 reactive→render (vue-core Proxy runtime, deferred).

§7: Vue results + the two real limits — composable-destructure handlers
(@click="closeSidebar" from useSidebarControl, a data-flow frontier) and
prefix-convention kebab (el-button→button.vue). Agent reads dropped in every
size; strongest where handlers are local functions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): resolve Vue composable-destructure template handlers

@click="closeSidebar" where `const { close: closeSidebar } = useSidebarControl()`
previously didn't resolve — the handler is a destructured composable return, not a
local fn node. Now: parse the SFC's `use*()` destructures into alias→{composable,
key}, and for an unresolved template handler follow alias → composable → the
returned member (`close`) defined in the composable's file. Precise-only: no
fallback to the composable itself (the component already has a static useX() call
edge), so we add an edge only when the specific returned fn is found.

Validated: vitepress Layout @click→close / @open-menu→open (in composables/
sidebar.ts); sidebar-flow agent run dropped 6→0 reads (best case). element-plus's
fallback-only matches correctly drop to 0; node counts stable; direct handlers
(vben handleLogin) unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): composable-destructure handlers now resolved (Vue)

@click="closeSidebar" → composable returned fn; vitepress sidebar 6→0 reads.
Remaining Vue limits: prefix-convention kebab + reactive→render frontier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(extraction): extract function-valued properties of exported-const objects

`export const actions = { default: async () => {...} }` (SvelteKit form actions,
and general JS handler/route/reducer maps) left the arrow functions unextracted —
the walker skips object-literal functions (deliberately, to avoid inline-object
noise like `ctx.set({...})`). So an action's body (and its calls) was invisible.

Now: for an EXPORTED const whose initializer is an object literal, extract each
function-valued property (arrow / function expression) as a function named by its
key and walk its body. extractFunction gains a nameOverride so ONLY this explicit
path names pair-arrows — inline-object arrows reached by the general walker still
fall through to the <anonymous> skip, so no noise returns. JS/TS-gated.

Validated: fixtures extract the actions + walk bodies (default→helper, default→
api.post resolve); SvelteKit detection doesn't break it. Blast radius tiny:
excalidraw +1 node, Python (django) +0, Vue repos +0, realworld +11 (the actions).

Known residual: a `$lib`-alias namespace-member call (`api.post`) from an extracted
action node doesn't resolve even though the same alias resolves for `load` — a
deeper resolver interaction, separate from this extraction change. Local/relative
calls from actions connect fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Svelte validation (already well-covered) + actions fix

Svelte/SvelteKit row → already strong (template calls/composition/namespace/load);
+ exported-const object-of-functions extraction. Lesson: measure before assuming
a hole — modern Svelte barely uses on:click={fn}; Svelte needed far less than Vue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): connect Express inline arrow route handlers to their services

The Express resolver created route nodes but linked handlers via a single regex
whose `[^)]+` broke on inline arrows — so `router.post('/x', async (req,res) =>
{...})` (the dominant modern pattern) connected to NOTHING, and the anonymous
handler's body (the actual request→service flow) was lost. The whole inline-handler
API was unreachable: e.g. realworld's `POST /users/login` route → 0 edges.

Now: match the route head, span the full call with a string-aware balanced-paren
scan, and for an inline arrow handler extract its body's calls (string-aware brace
scan) and attribute them to the route node as `calls` edges. A RESERVED denylist
drops res/req/builtin methods (json, next, status, ...) to keep only business calls.
Named-handler routes keep the existing reference behavior.

Validated: realworld POST /users/login → login (auth.service); 19 precise
route→service edges (was 0) — POST /articles→createArticle, .../favorite→
favoriteArticle, etc., no json/next noise. ghost +65 inline-handler edges. No node
explosion (ghost 40767, parse 3394 unchanged). Framework-scoped: zero blast radius
off Express.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Express validation (inline-handler fix)

Express/Koa row → resolver already handled named handlers; the real hole was
inline arrow route handlers (router.post('/x', async (req,res)=>{...})) — fixed:
route→service body calls (realworld 19 / ghost 65 edges, no explosion). Agent A/B
muddied by repo size (realworld tiny) / complexity (ghost layered API). Lesson
inverse of Svelte: Express's dominant pattern WAS the uncovered one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record NestJS validation (already well-covered)

NestJS row → resolver handles @decorator routes; DI controller→service
(this.svc.method) resolves correctly at scale (immich: addUsersToAlbum→addUsers,
etc.). Agent A/B: codegraph eliminated Grep (0 vs 3). No dynamic-dispatch hole.
Surfaced a general hygiene gap (not NestJS): committed dist/ build output gets
indexed (no default build-dir ignore) — narrow (real apps gitignore dist/),
deferred as a core-indexer follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Rails RESTful resources routing → controller#action

The rails resolver only saw explicit `get '/x' => 'c#a'` routes, so apps using
the dominant `resources :articles` / `resource :user` RESTful routing had ZERO
route nodes (realworld + spree: 0 routes despite full routes.rb files). The whole
request→controller flow was disconnected.

Fix (frameworks/ruby.ts):
- extract: expand `resources`/`resource` into their REST actions (only/except
  filters; pluralize the singular `resource :user` → users_controller), emit a
  precise `controller#action` ref per action. Explicit routes now also reference
  `controller#action` instead of a bare ambiguous `action`.
- resolve: new `controller#action` pattern → the action method in
  <ctrl>_controller.rb (file convention + controller-class fallback).
- claimsReference: claim `controller#action` refs so resolveOne's pre-filter
  doesn't drop them before resolve() runs (same hook the django ORM work needed —
  these refs name no declared symbol).

Validated: realworld 0→16, forem 0→635 precise route→action edges (GET /articles→
index, resource :user→users#show, etc.), pluralization correct, no node explosion
(route nodes proportional to resources). Agent A/B (forem, large): with codegraph
1-4 reads / 0 grep / 47-53s vs without 4-5 reads / 2-3 grep / 66-85s. Framework-
scoped (zero blast radius off Rails). Residuals: Rails Engine routing (spree
mounts an engine), ActiveRecord dynamic finders (metaprogramming frontier).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Spring bare + class-prefixed route mappings → controller method

The Spring resolver required a string path in the mapping regex, so BARE method
mappings (`@PostMapping` with the path on the class-level `@RequestMapping`) were
missed — the dominant multi-method-controller pattern. realworld's two-action
ArticleFavoriteApi only linked one method; halo had 28 routes for 2444 files.

Fix (frameworks/java.ts):
- Treat class-level `@RequestMapping` as a PREFIX (not a bogus route) and join it
  onto each method's path.
- Match verb-specific mappings (@GetMapping/@PostMapping/...) BARE or with a path.
- Also handle method-level `@RequestMapping(value=..., method=RequestMethod.X)`
  (older style) — restored after an initial cut dropped it (mall regressed 292→1;
  caught by the regression check).

Validated: realworld 13→19, mall 246 (all precise, class prefix joined:
GET /subject/listAll→listAll, POST /articles/{slug}/favorite→favoriteArticle +
DELETE→unfavoriteArticle), no node explosion. DI controller→service resolves
(article→findBySlug, updateArticle→canWriteArticle). Agent A/B (mall cart flow):
with codegraph 0 reads/0 grep vs without 2/2. Residuals: halo's complex custom
patterns (9/29 resolve); Spring Data JPA derived queries (metaprogramming frontier).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Spring validation (bare-mapping routing fix)

Spring row → bare @GetMapping/@PostMapping + class @RequestMapping prefix join →
route→method (realworld 13→19, mall →246); DI controller→service resolves. A
first cut regressed mall 292→1 (dropped @RequestMapping-on-method), caught by the
route-count regression check. Residuals: halo custom patterns, JPA derived queries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Django DRF router.register → ViewSet

Django's ORM (_iterable_class, prior work) and URL routing (path/url/as_view→view)
were already covered. The remaining hole: DRF `router.register(r'articles',
ArticleViewSet)` — the core CRUD endpoints — wasn't extracted (only path()/url()),
so a DRF API's main resources connected to nothing (realworld's ArticleViewSet:
0 callers).

Fix (frameworks/python.ts): match `.register(r'prefix', XViewSet)` → route→ViewSet
class. The STRING first arg distinguishes DRF router.register from
`admin.site.register(Model, Admin)` (model class first arg); View/ViewSet suffix
keeps it to viewsets. The ViewSet class resolves via the existing View/ViewSet
pattern.

Validated: realworld VIEWSET /articles → ArticleViewSet (was 0). Narrow in corpus
(realworld 1 router; wagtail=path, saleor=GraphQL) but real for DRF-router APIs.
Agent A/B (wagtail Page flow, medium): with codegraph 4-7 reads / 1-4 grep / 58-81s
vs without 7-9 reads / 6 grep / 82-86s. No regression (wagtail/saleor route counts
unchanged — purely additive). Residuals: signals, DRF inherited viewset actions,
GraphQL resolvers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Laravel route → precise Controller@method (not bare action)

extractLaravelHandler discarded the controller: `Route::get([UserController::class,
'index'])` and `'UserController@index'` both emitted a BARE `index` ref. With the
route in routes/api.php (not the controller file), name-matching mis-resolved every
common action to the WRONG controller — realworld's GET user → ArticleController.index
(should be UserController), GET articles/feed → ArticleController (should be
FeedController), etc. The routes existed but pointed at the wrong handler.

Fix (frameworks/laravel.ts): emit precise `Controller@method` (array + string
syntax, namespace-stripped) and `claimsReference` it so resolveOne's pre-filter
doesn't drop it before Pattern-4 resolveControllerMethod runs (the recurring hook,
also needed by django ORM + Rails routing).

Validated: realworld all routes now resolve to the correct controller; bookstack
267/332 precise (GET pages → PageApiController.list, array syntax). No node
explosion. Agent A/B (bookstack page-view, large): with codegraph 2-3 reads / 1-2
grep / 51-60s vs without 4-6 / 3-5 / 60-74s. Residuals: firefly's fluent
->uses()/['uses'=>...] handler format (3/568 resolve), Eloquent dynamic finders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Gin/chi routes on group vars (any receiver, not just r/router)

The route regex matched only `(router|r|mux|app|e).METHOD(...)`, but real Gin/chi
apps route on GROUP variables — `v1.GET`, `PublicGroup.GET`, `userRouter.POST` —
so group-routed apps connected almost nothing: gin-vue-admin had 4 routes for 625
files. Broaden the receiver to ANY identifier; the verb + string-path + handler-arg
gates keep it route-specific (e.g. `http.Get(url)` has no handler arg, so it's
excluded).

Validated: gin-vue-admin 4→259 routes, 257 resolve precisely (POST createInfo→
CreateInfo, GET getInfoList→GetInfoList); realworld stable 24→25 (no regression);
no garbage (257/259 resolve, not false positives), node count proportional. gitness
(chi, custom handlers) is a residual (26/321). Inline `func(c *gin.Context){...}`
handlers still lose their body (anonymous, like Express was) — separate residual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Gin validation (group-var routing fix)

Gin/chi row → routes on ANY group var (v1.GET/PublicGroup.GET), not just r/router
(gin-vue-admin 4→259 routes). Agent A/B: 0 reads/0 grep/26-30s vs 3/3/52-53s —
cleanest backend win yet. Residuals: inline func handlers, gitness chi custom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): ASP.NET feature-folder detection + bare attribute routes

Two holes left ASP.NET apps disconnected:
1. detect() only fired on a /Controllers/ dir, root Program.cs/Startup.cs, or a
   .csproj (which often isn't in the indexed source set). Feature-folder apps
   (realworld: Features/*/FooController.cs, subdir Program.cs) were never detected
   → 0 routes despite a full set of controllers. Broaden: scan Controller/Program/
   Startup .cs source for ASP.NET signatures ([ApiController]/[Route]/[Http*],
   ControllerBase, MapControllers, WebApplication, Microsoft.AspNetCore).
2. The attribute regex required a string path, so BARE [HttpGet] (route on the
   class [Route("[controller]")]) was missed — eShopOnWeb was 24 bare / 2 string.
   Match bare-or-with-path + join the class [Route] prefix (like the Spring fix).

No claimsReference needed: ASP.NET attribute routes are co-located IN the controller
with the action, so the bare method-name ref resolves same-file.

Validated: realworld 0→19 routes (all precise: GET /articles→Get, POST /articles→
Create, class prefix joined), eShopOnWeb 9→33. Route→action correct + co-located.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record ASP.NET validation (detection + bare-attribute fix)

ASP.NET Core row → feature-folder detection (realworld 0→19, was undetected) +
bare [HttpGet] / class [Route] prefix (eShopOnWeb 9→33, jellyfin 362→399). No
claimsReference needed (routes co-located in controller). Agent A/B (eShop): 1-2
reads/0 grep vs 6-7/1-6. Residual: EF Core LINQ.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Flask/FastAPI route holes + Python builtin-name handler guard

Three fixes that connect the request→route→handler flow for Flask and
FastAPI. Validated S/L: fastapi-realworld 12→20, flask-microblog 6→27,
Netflix dispatch 290/290 (100%), redash decorator routes 6/6; canonical
flows trace end-to-end (login→get_user_by_email, create_user→from_dict).

- Flask: the route regex required `def` immediately after `@x.route(...)`,
  so an intervening decorator (@login_required, @cache.cached) or stacked
  @x.route lines (one view bound to several URLs) dropped the route.
  Switch to the findHandler scan (match the decorator, then find the next
  def) like FastAPI — skips intervening decorators.
- FastAPI: the path regex `[^'"]+` rejected the empty path `@router.get("")`
  (router/prefix-root routes, frequently multi-line). Allow empty path +
  guard the route name against a trailing space.
- Python builtin-name guard (src/resolution/index.ts): a handler named
  after a Python builtin method (index/get/update/count…) was filtered by
  isBuiltInOrExternal and lost its route→handler edge. Mirror the
  dotted-method branch's knownNames guard onto the bare branch — a bare
  name a declared symbol owns is a real target, not a builtin call.
  +2 legit edges on realworld, 0 change on the django control (precision held).

Tests: new Flask (intervening/stacked decorator) and FastAPI (empty-path,
multi-line) extractor cases + a Flask end-to-end integration test (a view
named `index` behind @login_required). Also corrects 6 pre-existing stale
Laravel/Rails route-ref assertions surfaced by the suite — they expected
the old bare action name, but the resolvers now emit precise
controller@action / controller#action (from earlier precision commits).
Full suite green (781 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Flask/FastAPI validation (decorator + builtin-name fixes)

Matrix row Python/Flask+FastAPI 🔬 and a §7 note: Flask intervening/
stacked decorators, FastAPI empty-path routes, the Python builtin-name
handler guard, S/L numbers, the login-auth A/B (0–1 read/0 grep with vs
3 read/2 grep without), and residuals (Flask-RESTful class-based
add_resource; redash JS file-route false-positives).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Drupal route-handler resolution (claimsReference, single-colon controllers, contrib detection)

The *.routing.yml extractor and _controller/_form resolver existed but two
gaps left most routes unlinked. Validated S/M/L: admin_toolbar 0→14 (14/14),
webform 144/208, drupal-core 536→731/836 (87%); canonical flow traverses
(getAnnouncements ← /admin/announcements_feed); node count unchanged.

- claimsReference: Drupal handler refs are FQCNs (\Drupal\…\Class::method),
  bare form classes (\…\SettingsForm), or single-colon controller-services
  (\…\Controller:method). Only the ::method shape survived resolveOne's
  pre-filter (its member is a known method name); the bare-FQCN forms and
  single-colon controllers were dropped before resolve() ran. Claim FQCN /
  Class:method / hook_* refs (same pattern as Rails controller#action).
- Single-colon controller match: broaden the controller regex from :: to
  :{1,2} and tighten the _form branch to !name.includes(':').
- Detection: detect() only checked composer `require` for a drupal/* dep, but
  a contrib module often has an empty require and is identified only by
  "name":"drupal/<m>" + "type":"drupal-module" (admin_toolbar → 0 routes).
  Broaden to composer name/type + a *.info.yml fallback.

Remaining unresolved is the entity-annotation handler frontier
(_entity_form: type.op) and OOP #[Hook] attributes (Drupal 11 moved ~all
procedural hooks to attribute methods — out of scope here). Tests: contrib
detection, *.info.yml fallback, claimsReference, single-colon controller.
Full suite green (787 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Drupal validation (claimsReference + contrib detection)

Add the PHP/Drupal matrix row () and a §7 note: the claimsReference
pre-filter fix for FQCN/single-colon handlers, broadened contrib detection,
S/M/L numbers (admin_toolbar 0→14, webform 144/208, core 536→731), the
route→controller A/B (0 read/1 grep with vs 1 read/2 grep+glob without), and
the frontier residuals (entity-annotation handlers, OOP #[Hook] attributes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Axum chained methods + namespaced handlers

The Axum route extractor used a flat regex that captured only the first
method(handler) of a .route() call and only a bare \w+ handler, so two
dominant Axum idioms broke:
- method chains: .route("/user", get(get_current_user).put(update_user))
  emitted no node for the .put arm — half the API was missing.
- namespaced handlers: get(listing::feed_articles) captured `listing`
  (the module), so the route resolved to nothing.

Rewrite with a balanced-paren scan of each .route(...) call, a route node
per chained method, and last-::-segment handler names. realworld-axum
12→19 routes, 19/19 resolved (every chained PUT/DELETE/POST now present,
feed_articles resolves). Rocket needed nothing (550/556, 99%, attribute
macros); crates.io confirms namespaced axum handlers resolve.

Residual frontier: actix runtime routing web::get().to(handler) (the
dominant actix style, unextracted; attribute macros 35/51). Fix is
Axum-scoped — the attribute/actix/Rocket path is untouched. Tests: chained
methods + multi-line namespaced handler. Full suite green (789 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Rust/Axum validation (chained methods + namespaced handlers)

Update the Rust matrix row 🔬 and add a §7 note: the Axum chained-method
+ namespaced-handler fix (realworld-axum 12→19, 19/19), Rocket already 99%,
crates.io (utoipa routes! macro frontier + SvelteKit frontend routes), the
update-user A/B, and the actix runtime-routing frontier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Vapor grouped/RouteCollection routing (was 0 routes on real apps)

The Vapor extractor only matched (app|router|routes).METHOD("path", use:
handler), but real Vapor apps route on a grouped builder inside
RouteCollection.boot(routes:): `let todos = routes.grouped("todos");
todos.get(use: index)` — any var receiver, no path arg (the path is the
group prefix). Every real app tested extracted 0 routes (template,
SteamPress, SwiftPackageIndex-Server, penny-bot, Feather).

Rewrite the extractor:
- any receiver (\w+), not just app/router/routes;
- optional path segments that may be non-string (User.parameter, :id, a
  path constant) — the `use:` keyword discriminates a route from
  Environment.get("X") / req.parameters.get("X");
- a group-prefix map from `let X = Y.grouped("a")` and
  `Y.group("a") { X in }` so a grouped/nested route gets its full path
  (todo.delete(use: delete) -> DELETE /todos/:todoID).

Result: vapor-template 0→3 (3/3, nested path exact), SteamPress 0→27
(27/27), SwiftPackageIndex-Server 0→14 (14/14 handler resolution).
Canonical flow traverses (createPostHandler <- GET /createPost ->
createPostView). Route names now carry a leading slash (GET /users),
consistent with the other frameworks.

Frontier: typed-route enums (SPI's SiteURL.x.pathComponents — handler
resolves, path label only) and closure handlers (app.get("x"){ } —
anonymous). Tests: grouped RouteCollection, self.handler + non-string
segments, use:-discriminator. Full suite green (792 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Vapor validation (grouped RouteCollection routing)

Update the Swift/Vapor matrix row  and add a §7 note: the extractor was
dead on real apps (0 routes everywhere); rewrote for any receiver, optional
non-string paths, .grouped/.group{} prefix tracking, and the use:
discriminator. S/M/L all 100% handler resolution (template 0→3, SteamPress
0→27, SPI 0→14), the create-post A/B (0 read/0 grep with vs 1–4 read
without), and frontiers (typed-route enums, closure handlers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): React Router <Route> JSX route extraction

react.ts extracted components/hooks and Next.js file routes but returned
references: [], so React Router <Route> declarations produced no route
nodes or route→component edges. Add <Route> JSX extraction: scan a window
after each <Route (so the nested > in element={<Comp/>} doesn't truncate
the match), pull path="…" + component={C} (v5) or element={<C/>} (v6) in
any attribute order, emit a route node + component reference (resolved by
the existing PascalCase resolveComponent). The <Routes> container is
excluded via the \b boundary.

react-realworld 0→10 routes, 10/10 resolved (/login→Login,
/editor/:slug→Editor, /@:username→Profile). No regression on excalidraw
(9,290 nodes, 46 react-render synth edges intact, 0 false routes). Tests:
v5 component=, v6 element=, <Routes>-container guard. Suite green (794).

Frontier: object data-router createBrowserRouter([{path,element}]) (modern
v6) is object-based not JSX — not covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record React Router routing (the React row's routing half)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): actix-web builder-API routing (web::resource / .to(handler))

Actix's attribute macros were covered, but the dominant actix style is the
builder API — web::resource("/path").route(web::get().to(handler)),
web::resource("/").to(handler) (all methods), and App .route("/path",
web::get().to(handler)). The handler is in .to(handler), not get(handler),
so the Axum .route scan extracted nothing — actix-examples had 80
web::resource calls all unlinked.

Add an actix block: scan each web::resource("/path") (bounding its method
chain at the next resource) for web::METHOD().to(h) pairs, fall back to a
direct .to(h) (method ANY), plus the App-level .route("/x",
web::METHOD().to(h)) form. actix-examples 51→128 routes, 35→112 resolved
(GET /user/{name}→with_param, POST /user→add_user). No regression on Axum
(realworld-axum still 19/19). Tests: resource+route, resource direct .to,
App-level route. Suite green (797).

Frontier: web::scope("/api") prefixes not prepended; anonymous .to(|req|…)
closures have no named target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record actix builder-API routing validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(extraction): Flutter setState→build synthesis + Dart method body ranges

Two changes that connect Flutter's reactive dispatch:

- Dart method ranges (foundational): Dart models a method body as a SIBLING
  of the method_signature node, so every Dart method node had endLine ==
  startLine (signature only) — body-level analysis (callees, context slices,
  the synthesizer's body scan) saw only `void f() {`. Extend endLine to the
  resolved body in the shared createNode, guarded to only ever extend
  (child-body grammars are a no-op; controls excalidraw 9,290 / django 302
  unchanged).
- Flutter setState→build synthesizer channel (the Dart analog of react-render):
  for each Dart class with a `build` method, link sibling methods whose body
  calls setState( → build. setState re-runs build (Flutter-internal, no static
  edge), so "tap → handler → setState → rebuilt UI" dead-ended at setState.

counter initState→build, books build→BookDetail/BookForm. Widget composition
needs no synthesis — Dart widgets are explicit constructor calls, already
static (compass_app build→ErrorIndicator/HomeButton). Tests: Dart method
spans its body; Flutter handler→build synthesis end-to-end. Suite green (798).

Frontier: MVVM Command/ChangeNotifier dispatch (no setState) + Navigator.push
route-as-widget navigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Dart/Flutter validation (setState→build + method ranges)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Spring Boot Kotlin routing (.kt + fun handlers)

Kotlin had zero framework coverage — no resolver listed kotlin, and the
Spring resolver was languages:['java'] with a .java-only extract gate and a
Java-syntax handler regex (public X name()). Spring Boot Kotlin apps (same
@GetMapping/@RestController annotations, .kt files) extracted 0 routes.

Extend the Spring resolver: languages ['java','kotlin'], accept .kt, and add
a Kotlin `fun name(` alternative to the handler-method regex (Kotlin has no
access modifier; the return type follows the name). Also allow Kotlin class
modifiers (open/data/sealed) in the class @RequestMapping-prefix detection,
and tag route/ref language per file.

spring-petclinic-kotlin 0→18 routes, 18/18 resolved; class @RequestMapping
prefixes join, stacked annotations skipped, DI controller→repo resolves
(showOwner ← GET /owners/{ownerId} → OwnerRepository.findById). Java Spring
unchanged (realworld 19/19 — the Kotlin fun and Java public-X alternatives
are disjoint per language). Jetpack Compose composition already works
(@Composable→child are plain function calls). Tests: Kotlin @GetMapping+fun,
class-prefix + stacked annotation. Suite green (800).

Frontier: Ktor inline-lambda routing, Compose recomposition, coroutines/Flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Kotlin validation (Spring Boot Kotlin + Compose)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Lua/Luau validation (module dispatch already covered)

Measure-first: Neovim/Roblox dispatch is module-heavy (require + cross-file
mod.fn calls), already resolved by general import+name resolution
(telescope.nvim 220 imports + 335 cross-file calls; traces end-to-end). The
matrix's assumed "callback synthesizer" hole isn't real — event-callback
registration (keymap/autocmd/:Connect) is predominantly inline anonymous
closures (corpus ~12 inline vs ~2 named), too rare to synthesize. A/B: 0
read/0 grep with codegraph vs 1 read without. No code change; validated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Play Framework conf/routes → controller routing (Scala/Java)

Play declares routes in an extensionless conf/routes file (GET /computers
controllers.Application.list(p: Int ?= 0)) the file walk never indexed
(isSourceFile requires an extension), so Play apps had 0 route nodes.

- grammars.ts: add isPlayRoutesFile (conf/routes + *.routes), opt it into
  isSourceFile, and map it to the no-grammar (yaml-style) path in
  detectLanguage so the framework resolver extracts it. Narrow match — only
  ADDS Play routes files, never affects other indexing.
- play.ts: a Play resolver — detect (build.sbt/conf), extract (parse each
  METHOD /path Controller.action(args) line, drop package + args), resolve
  (Controller.action → the action method in that controller class),
  claimsReference for the dotted Controller.action handler.

computer-database 0→8 routes, 7/8 resolved (the 1 unresolved is
controllers.Assets.versioned — Play's framework controller, external);
starter 0→4 (3/4). Flow connects request→route→controller→DAO. No-regression
(excalidraw 9,290 / suite unchanged). Tests: routes parse + `->` include
skipped, conf/routes file detection.

Frontier: SIRD programmatic routers (-> include + case GET(p"/x")) + Akka
actor message→handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Scala/Play validation (conf/routes → controller)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(extraction): C++ inheritance (base_class_clause) + virtual-override synthesis

C/C++ direct dispatch already resolves well (redis 29k / leveldb 1.4k
cross-file calls). Two changes close the C++ virtual-dispatch gap:

- extractInheritance handled base_clause (PHP) but not C++'s
  base_class_clause, so C++ `extends` edges were missing/partial. Add the
  C++ branch (emit an extends ref per base type, skipping access
  specifiers) — leveldb extends 219→298.
- cpp-override synthesizer channel (the C++ analog of react-render): for
  each extends edge, link each base method → the subclass override of the
  same name, so trace/callees from a virtual/interface method reach the
  implementation. Gated to C++, capped per class. leveldb 12 precise edges
  (Iterator::Next/Seek/Prev → MergingIterator), 0 on C (redis) and TS
  (excalidraw). Test: base virtual → subclass override bridge.

Frontier: C callback structs (cmd->proc() → 422-way fan-out, too noisy)
and C++ pure-virtual base methods (declarations aren't nodes, so those
overrides can't bridge). Suite green (804).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record C/C++ validation (inheritance fix + override synthesis)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): React Router object data-router + Next.js route precision

- Object data-router (v6.4+): createBrowserRouter([{ path, element: <Comp/> }])
  / { path, Component: Comp } — extract route + component (gated to files using
  the data-router API; requires a component so a stray `path:` field isn't a route).
- Next.js precision: filePathToRoute treated config files (next.config.mjs,
  vite.config.ts) and a `nextjs-pages/` dir (substring of "pages/") as routes.
  Require a real page extension (.tsx/.ts/.jsx/.js), exclude *.config.* and
  _app/_document, and match pages/ + app/ as path SEGMENTS. bulletproof-react
  4 bogus config "routes" → 0.

Frontier: lazy data-router routes (path: paths.x.path + lazy: () => import())
use variable paths + lazily-imported modules — no literal path/named component.
Tests: object-router literal form, config/nextjs-pages exclusion. Suite 806.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Flask-RESTful add_resource + tuple methods + broader detection

Three Flask gaps closed (redash Flask-RESTful 6→77 py routes; flask-realworld 0→19):
- Flask-RESTful: api.add_resource(ResourceClass, '/path') (+ redash's
  add_org_resource) now extracts a route per path referencing the Resource
  class, whose get/post verb methods resolve as the handlers.
- Tuple methods: @x.route('/p', methods=('POST',)) — the method regex only
  accepted a list [...]; now accepts a tuple (...) too, so POST/DELETE routes
  aren't mislabeled GET.
- Detection: detect() only checked root app.py for the literal Flask(__name__);
  broadened to requirements/pyproject/Pipfile/setup.py + any entrypoint file
  (root or subdir, e.g. conduit/app.py) that imports flask and instantiates
  Flask(...). flask-realworld (subdir app-factory) 0→19; django not falsely
  detected.

Tests: tuple methods, add_resource. Suite green (808).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record frontier pass; test(go): gorilla/mux subrouter coverage

Frontier triage after the main sweep — tractable partials closed (React object
data-router, Next.js false-positive fix, Flask-RESTful add_resource, Flask
tuple methods + detection, gorilla/mux confirmed), and the genuinely
hard/low-precision ones (C callback fan-out, metaprogramming finders, reactive
runtimes, Akka, anonymous closures, lazy data-router, C++ pure-virtual) left
documented with rationale. Adds a gorilla/mux subrouter-var HandleFunc test
(confirms the any-receiver handling already covers it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(benchmarks): A/B with/without codegraph across every language (S/M/L)

37-cell matrix (every flow-relevant language × small/medium/large indexed
repos): a headless agent answers one canonical flow question per repo, with the
codegraph MCP vs without any MCP. Fresh re-index per cell so the with-arm
reflects current resolvers.

Result: 75% fewer file reads with codegraph (40 vs 158 across cells), ~70%
fewer greps, never more reads in any cell. Biggest wins on medium/large
backends (excalidraw 0R vs 9R, spring-halo 0R vs 9R+8 Bash, jellyfin 4R vs 13R+
21 Bash + a spawned sub-agent); tie zone on tiny repos where the flow fits in
1-2 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): self-sufficient codegraph_trace + CODEGRAPH_MCP_TOOLS allowlist

codegraph_trace now returns a complete flow dossier in one call: each hop with its full body inlined (not just the call-site line), plus the destination's own outgoing calls — the last mile agents otherwise explore/Read to get. Validated by A/B (arm I, 6 repos x 2): >= baseline on reads/turns/cost with no wall-clock regression, because one richer trace call displaces the explore+node+Read follow-ups. Sufficiency, not steering: complete context is what stops further investigation.

Also adds CODEGRAPH_MCP_TOOLS, an optional comma-separated allowlist that trims the exposed MCP tool surface (inert when unset); used to run the tool-ablation experiment cleanly, and useful for constraining an agent to a minimal surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(benchmarks): call-sequence + tool-ablation analysis; agent-eval arms harness

Records why codegraph read savings (-75%) under-convert to wall-clock (-16%): the bottleneck is round-trips + the synthesis turn, not reads. Ablation (arms A-I) shows explore is 68% of payload but load-bearing, trace is path-scoped but under-adopted, instruction/description steering cannot match an append-prompt's salience (and regresses), and the shippable win is making the trace output sufficient (arm I). Adds harness: seq-matrix, run-arms/arms-*, parse-arms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): line-number codegraph_node + codegraph_trace source output

node's code block and trace's inlined hop/destination bodies now carry cat -n line numbers (reusing numberSourceLines, matching codegraph_explore and Read), so the agent can cite or edit exact lines without re-Reading the file just to get them. Consistency across the code-returning tools + edit-workflow sufficiency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(resolution): Java/Kotlin interface & abstract dispatch synthesis

A call through an injected interface (Spring @Autowired svc.list()) or an abstract base dead-ended at the interface method — no static edge to the implementation — so request->service->impl flows broke at the DI boundary. Adds interfaceOverrideEdges: for each class implementing an interface (or extending an abstract base), synthesize interface/base-method -> same-name override 'calls' edges (JVM-gated, capped per class, overload-aware), with an 'interface-impl' trace label. trace + callees now follow the flow into the implementation.

Validated on spring-mall: 310 synth edges, node count unchanged (edges only); trace(PmsProductController.getList, PmsProductServiceImpl.list) connects in 3 hops (controller -> service interface -> impl) where it previously dead-ended at the interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(playbook): record Java/Kotlin interface-DI synthesizer (probe-validated; agent A/B adoption-gated)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): codegraph_explore surfaces the execution flow from its named symbols

Agents call explore far more than trace and pass a bag of symbol names that spans the flow they're after. explore now resolves those names and surfaces the longest call path AMONG them — riding synthesized dynamic-dispatch edges (callback/react-render/jsx/interface-impl) — leading the output with it, so a flow question answered via explore gets the trace-quality path without switching tools.

Precision: ambiguous tokens disambiguated by CO-NAMING (keep candidates whose qualifiedName SEGMENT matches another named token, so 'list' resolves to PmsProductServiceImpl::list not OmsOrderService::list); BFS anchored at named symbols on both ends with <=1 consecutive unnamed bridge (crosses a missing intermediate, never wanders a god-function's fan-out). Validated by probe: spring-mall getList->service-interface->impl (3 hops); excalidraw mutateElement->triggerUpdate->[callback]->triggerRender->[react-render]->render->[jsx]->StaticCanvas (full re-render chain). No flow section on fuzzy queries (safe). Suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(mcp): explore-flow resolves qualified Class.method query tokens

The agent often passes fully-qualified names to explore (PostEndpoint.publishPost, PmsProductServiceImpl.list) — its most precise input. The tokenizer's file-extension strip mangled Class.method into Class (treating .method as an extension), then the identifier filter dropped anything with a dot, throwing the method away. Now strips only REAL file extensions and keeps qualified tokens, which findAllSymbols resolves exactly; disambiguates ambiguous SIMPLE names by whether their container class is also named (segment match). Validated: 'PmsProductController.getList PmsProductServiceImpl.list' now surfaces getList->interface->impl. (spring-halo's publish flow stays absent — it's reactive/reconciler dispatch with no static edges, a coverage frontier, not an explore-flow gap.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(claude): record the 'adapt the tool to the agent' retrieval principle

The lever that decides whether a retrieval change lands: make a tool the agent already calls do more with the input it already gives; changes that need the agent to behave differently (different tool, query, examples) hit codegraph's low-salience channels and don't land. Captures the validated evidence (sufficiency + explore-flow pass; steering + new-tools + context-fuzzy-flow fail) and points coverage as the remaining lever.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: correct 'cost stays flat' → neutral-to-lower (excalidraw with/without A/B)

Fresh with-vs-without A/B on excalidraw (current build, n=3): 3x faster (49s vs 145s), 15x fewer tool calls, ~0 vs 23 reads, and -40% cost ($0.41 vs $0.68). Cost is neutral-to-lower, not flat — compact codegraph answers cache across turns while the without-arm's read/grep thrash is fresh, poorly-cacheable input. Recorded in call-sequence-analysis.md; corrected the CLAUDE.md optimization-target note (still: don't optimize for cost; target wall-clock + tool-call count).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(benchmarks): current-build A/B on all 7 README repos + fix token-measurement bug

Re-ran the README benchmark on the current build (7 repos reindexed, median of 4): avg 35% cost / 57% tokens / 46% time / 71% tool calls saved — reproduces the published README (35/59/49/70), no regression. Adds bench-readme.sh + parse-bench-readme.mjs harness.

Fixes a token-measurement bug: result.usage is last-turn-only in current Claude Code; must sum per-turn assistant usage for cumulative tokens. Corrects the earlier excalidraw note (its '-34% tokens' was off this bug; real ~90%) and the cost MECHANISM (volume/fewer-turns, not cache-ability — the without-arm's huge token volume is mostly cheap cache-reads, so token savings 57% > cost savings 35%). Cost/time were always correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: finalize 0.9.4 — consolidate CHANGELOG + re-validate README benchmark

Folds the framework sweep + retrieval work into [0.9.4] (2026-05-24). README benchmark table refreshed with current-build medians (avg 35% cost / 57% tokens / 46% time / 71% tool calls) + a v0.9.4 re-validation note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(readme): add codegraph_trace to the MCP Tools table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:41:04 -05:00
fb45959af7 fix(mcp): reap serve --mcp child when parent is SIGKILL'd (#286)
Add a PPID watchdog to the MCP server so a `codegraph serve --mcp` child terminates when its host (Claude Code, opencode, …) is force-killed — OOM killer, `kill -9`, container teardown — and the stdin close handlers don't fire. The child would otherwise linger indefinitely, holding inotify watches, file descriptors, and the SQLite WAL.

Also propagates the host PID across the `--liftoff-only` re-exec (CODEGRAPH_HOST_PPID) so the watchdog reaps the orphan on the from-source path too, not just the bundled launcher. Poll interval is CODEGRAPH_PPID_POLL_MS (default 5000ms, 0 disables).

Resolves #277.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:02:29 -05:00