2521b49a9a427aff88a2d833b77a7f81292a1072
170
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
222af6b87c |
fix(mcp+resolution): stop conflating same-named symbols across monorepo apps (#764) (#813)
A NestJS-style monorepo has one UserService/UserModule/UserRepository per app; with no package concept for TS they share one global name scope and agents visibly warned that CodeGraph was mixing unrelated classes. Two distinct problems, two fixes: 1. TOOL AGGREGATION. callers/callees returned one merged list across every same-named match, and impact merged all their blast radii into a single overstated subgraph. Now: matches group into DISTINCT DEFINITIONS (filePath + qualifiedName — same-file overloads still merge, that's the overload feature) and render one file-labeled section per definition; a new `file` argument (path or suffix, like codegraph_node's) narrows to one definition, suppressing the stale aggregation note; a non-matching `file` falls back to all definitions with a note. server-instructions documents the behavior. 2. RESOLUTION WRONG EDGES. Auditing a real monorepo (amplication, 54k nodes) found 1,036 cross-package `references` edges into duplicated names. Root cause: the React framework resolver ran PascalCase component resolution on refs from PLAIN .ts FILES (a GraphQL types file's own `Account` type alias lost to an arbitrary same-named CLASS in another package — the resolver's blind `components[0]` fallback at confidence 0.8 outranked the name-matcher's proximity-correct 0.7). Component resolution is now gated to JSX-capable refs (tsx/jsx) and never guesses among multiple candidates without a positional signal (same-dir / component-dir / unique). Cross-package wrong edges: 1,036 -> 40 (-96%; the remainder are genuine shared-model imports and codegen template scaffolds), with the freed refs re-resolving to the correct same-file/same-package targets. excalidraw (a real React repo) is a zero-delta control — legitimate component refs all carry same-dir/component-dir signals. Graph-level separation was verified correct on a fixture before any changes (import + proximity resolution keeps apps apart) — the conflation was tool-level plus the react-resolver edge class. Tests: 6-test e2e suite (grouped callers/callees, per-definition impact radii, file narrowing, fallback note, cross-app edge isolation) + react resolver unit tests updated to production reality (tsx refs resolve, plain-ts refs decline). Full suite 1398 passed. EXTRACTION_VERSION 23 -> 24 (re-index to drop the wrong cross-package edges). Closes #764 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1f15f93feb |
feat(extraction): PHP string/array callables + Ruby lifecycle-hook symbols (#811)
The last two deferred callback-registration shapes from #756, each scoped to positions where the reference is trustworthy: PHP — a string is a callable ONLY in a known callable position: - string args of core HOFs (usort, array_map, array_filter, call_user_func*, preg_replace_callback, spl_autoload_register, set_error_handler, … — PHP_CALLABLE_HOFS): ungated (PHP globals are referenced cross-file without imports) + resolution unique-or-drop, function-kind only ('Cls::m' strings resolve qualified) - array callables anywhere in call args: [$this, 'method'] routes through the class-scoped this. resolver (parents included); [Foo::class, 'method'] resolves qualified - strings to arbitrary functions: deliberately nothing Ruby — hook-DSL symbols name a method of the enclosing class: (skip_)?(before|after|around)_* / validate / set_callback / helper_method / rescue_from(with:) symbols → class-scoped this.<sym>, riding the supertype pass so `before_action :authenticate` in a controller resolves to ApplicationController's method. `validates` (plural) excluded — its symbols name ATTRIBUTES. Class-body-level hooks attribute to the CLASS node (the scoped resolvers now accept class-like from-nodes). Also hardened while validating: the this.X supertype pass is now NODE-anchored — file-anchored class node → implements/extends edge targets → contains-anchored member lookup — replacing the name-keyed getSupertypes walk, which unioned every same-named class's parents (rails has a dozen `Engine`s) and produced a cross-class wrong edge. A/B vs main: WordPress +556 (14/14 sampled genuine — [$this,'m'] wiring, array_map('absint',…), sodium polyfill call_user_func_array dispatch); rails/rails +385 after the node-anchored fix (16/16 sampled genuine, incl. inherited hooks across real extends edges); controls byte-stable (excalidraw 0-delta, redis identical, typeorm keeps its +4 inherited getters). The only calls-edge deltas anywhere are pre-existing minified-bundle resolution jitter (wp-tinymce.js single-letter symbols). Full suite 1391 passed. EXTRACTION_VERSION 21 → 22 (re-index to benefit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
2f50473aaa |
fix(go): attach cross-file methods to their receiver type (#583) (#716)
Add a resolution-phase pass (goCrossFileMethodContainsEdges) that links a Go method to its same-named receiver type within the same package (= directory), so a method declared in a different file from its `type` is no longer orphaned from the struct. Runs before goImplementsEdges so cross-file methods also count toward interface satisfaction (#584). Adds a regression test + CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8d35931c3b |
fix(python): resolve call edges through imported modules (#578) (#715)
Give resolvePythonModuleMember the same absolute-dotted-path fallback that resolveModuleImportToFile already uses, so a `module.func()` call after `from pkg import module` / `import pkg.module as module` records its `calls` edge. Adds a regression test and a CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
bdfd55e69c |
fix(resolution): resolve Svelte/Vue component barrels & workspace imports (#629) (#657)
Component barrels (`export { default as X } from './X.svelte'`) and
monorepo workspace imports (`@scope/ui/widgets`) left the consumer↔component
edge uncreated, so live components showed a false `0 callers` — the canonical
dead-code signal — risking deletion of live code.
The Svelte default-barrel case broke at FOUR layers, each of which alone left
it unresolved:
- findExportedSymbol matched only function/class for a default export, never
`component` (Svelte/Vue SFCs are kind 'component').
- extractImportMappings had no svelte/vue branch, so SFC consumers produced
zero import mappings and resolveViaImport never ran.
- EXTENSION_RESOLUTION had no svelte/vue entry, so relative imports from an
SFC (`./lib` -> `/index.ts`) resolved to nothing.
- getReExports parsed the barrel in the CONSUMER's threaded language, so a
.svelte consumer made extractReExports bail on a .ts index barrel.
Workspace package-subpath barrels get a new workspace-packages module
(mirrors go-module/path-aliases): reads package.json `workspaces`
(npm/yarn/bun) + pnpm-workspace.yaml, maps member name->dir, resolves
`@scope/ui/widgets` -> `packages/ui/widgets`. Gated behind the workspaces
field so single-package repos are unaffected.
Bare `./`/`.` directory imports already resolved; covered with a regression
test. Verified both directions (callers/impact AND callees) for Svelte; Vue
script-level imports also resolve. 4 new tests; full suite green (1126).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2a22f9f55a |
fix(resolution): stream node-kind scans in synthesis to fix OOM on dense files (#610) (#653)
The callback/observer synthesizers loaded every function and method node into
memory at once (getNodesByKind('function'/'method')) before scanning them down
to a tiny matched subset. On a symbol-dense project that array is gigabytes, so
indexing spiked the JS heap and aborted with "JavaScript heap out of memory".
Add QueryBuilder.iterateNodesByKind (a lazy node:sqlite cursor) and stream the
synthesizer scans instead of materializing them. Parsing and reference
resolution were already bounded; only the synthesis enumeration wasn't.
Measured on 80 files x 14k functions (~1.1M nodes): peak RSS 3717 MB -> 1318 MB,
no OOM. Full suite green; synthesized edges unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3a1ddf41cd |
feat(mcp): trace relevance + closure-collection + god-file rendering + cold-start handshake (#580)
Trace endpoint relevance (overloaded names resolve to the real implementation instead of an empty protocol/delegate stub), Swift closure-collection synthesizer, multi-phase god-file explore rendering, and serve --mcp cold-start handshake sped ~811ms→~90ms (proxy answers initialize/tools-list locally). Full suite green (1090 pass). |
||
|
|
f58de8a391 |
feat(resolution): gin middleware-chain synthesizer + Opus 4.8 benchmark refresh (#547)
* fix(agent-eval): detect idle by content-stability, not spinner absence Opus 4.8's extended-thinking TUI shows no spinner / interrupt hint / timer while it streams its final answer — those appear only during the thinking and tool-use phases. The old detector treated ~5s of not-busy + prompt-present as done, so it killed interactive runs mid-answer, silently truncating both arms of the tmux A/B (low tool counts; the final assistant message left as a mid-investigation preamble). Now a run is done only when the captured pane stops changing for ~8s; while streaming, the pane changes every poll so stability never accrues. BUSY_RE stays as the immediate busy-reset for the thinking/tool/live-timer phase. Content-stability is model-agnostic — it survives future spinner re-wordings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(readme): refresh VS Code benchmark on v0.9.7 + Opus 4.8 Re-ran the VS Code A/B (headless median-of-4) on the current build and model. Cost savings held at 26% ($0.66->$0.89), but token/time/tool-call savings narrowed (78->63%, 52->20%, 85->69%) because Opus 4.8's without-CodeGraph arm explores far more efficiently than 4.7's did (16 tool calls vs 55, no Explore-subagent fan-out); the WITH arm is unchanged at 5 calls / 0 reads. Recomputed the average row and noted that the VS Code row is now a different model/version epoch than the other six. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(resolution): synthesize gin middleware-chain edges (Next -> registered handlers) Gin runs its entire handler chain through one dynamic line in (*Context).Next -- c.handlers[c.index](c), a slice-index dispatch tree-sitter can't resolve. So callees(Next) dead-ended at the len() helper and the flow ServeHTTP -> handleHTTPRequest -> Next stopped at the exact symbol a 'how does the middleware chain work' question is about, sending the agent to re-query and Read/grep (a measured gin WITH-arm rabbit-hole: 2/4 headless runs spiraled to ~5min, one mis-firing the opt-in Workflow orchestration tool). Find the chain dispatcher (a Go method invoking a handlers slice by index) and link it -> every HandlerFunc registered via .Use/.GET/.../.Handle, so callees(Next) and trace(ServeHTTP, handler) connect end-to-end. Gated on the dispatcher existing (inert on non-gin Go repos), named handlers only (inline closures skipped), capped; provenance heuristic / synthesizedBy gin-middleware-chain, registeredAt = the registration site. Validated: gin callees(Next) now surfaces Logger/Recovery/ErrorLogger + handlers (node count stable at 2,544; 5 precise edges); agent A/B (headless median-of-4, Opus 4.8) flipped gin from -58% cost / -129% time to +7% cost / +35% tokens / +8% time / 38% tool calls, all 4 WITH runs clean (0 Read/Grep/Bash). 167/167 unit tests pass incl. the new gin-middleware-chain test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(readme): publish uniform Opus 4.8 benchmark + per-repo breakdown accordion Refresh all 7 benchmark rows to the v0.9.7 / Opus 4.8 headless median-of-4 (was a mix of the 4.8 VS Code row + six 4.7 rows). New average 18% cheaper / 51% fewer tokens / 16% faster / 57% fewer tool calls; headline + methodology note updated 4.7->4.8. The gap is smaller than the prior 4.7 numbers because Opus 4.8's native grep/read is more efficient (the without-arm no longer fans out into large Explore-subagent sweeps) -- not a codegraph regression; CodeGraph still cuts tool calls and tokens on all 7 repos, with cost marginal/negative only on django + okhttp. Adds a top-level 'Per-repo breakdown' accordion (per-metric Time/Reads/Grep-Bash/Tool calls/Tokens/Cost, WITH vs WITHOUT, per repo) directly below the condensed summary; methodology/queries/why-wins move to a second accordion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note Gin middleware-chain synthesizer under [Unreleased] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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).
|
||
|
|
48eebe1e3e |
feat(resolution): add C/C++ include path resolution (#453)
* feat(resolution): add C/C++ include path resolution Add full import resolution pipeline for C and C++ #include directives, connecting extracted import nodes to actual header files in the project. - Add C/C++ extension resolution (.h, .hpp, .hxx, .cpp, .cc, .cxx) - Add system header filtering with ~80 C and ~80 C++ stdlib headers - Add extractCppImports() for #include import mapping extraction - Add compile_commands.json parsing for -I/-isystem include directories - Add heuristic include dir discovery (include/, src/, lib/, api/) - Add resolveCppIncludePath() for include directory search - Add C/C++ built-in symbol filtering (printf, malloc, std::*, etc.) - Wire getCppIncludeDirs into ResolutionContext - Add 13 new tests for C/C++ import resolution and extraction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * review: wire #include resolution into pipeline + fix builtin filter The PR landed the include-dir scan logic (loadCppIncludeDirs + resolveCppIncludePath) but the indexer never reached it: imports references with referenceName='X.h' fell into resolveViaImport's symbol-lookup branch (matched extractCppImports' basename-without-ext localName via .startsWith, then tried to find a symbol named like the extension and failed). End result on bitcoin-core: 0 new file→file imports vs main, despite the include-dir scan resolving paths correctly when probed directly. resolveViaImport now has a C/C++ imports branch that resolves the include path to the actual file node and returns that — skipping the irrelevant symbol scan. Measured on bitcoin-core: +2,059 newly resolved file→file imports (6,027 → 8,086, +34%). The unconditional CPP_BUILT_INS / C_BUILT_INS filter also misfired: C/C++ codebases routinely shadow stdlib names (bitcoin's mp::move, custom allocators with free/malloc, stream classes with read/write/ close/open, logging libs wrapping printf). Filtering those names killed legitimate edges — 1,179 → 0 for move(), 33 → 0 for free(), 149 → 7 for write() on bitcoin. The filter now defers to hasAnyPossibleMatch: only filter when no user-defined symbol with the name exists. std:: prefix stays unconditional (never user-shadowed in practice). After: printf/free/open/close/read/write/swap all preserved at main's counts; the std::move-binds-to-mp::move false-positives still drop (correctly: −2,154 C/C++ calls). Also: drop the duplicate 'FILE' in C_BUILT_INS; add an end-to-end test that asserts `#include "X.h"` produces a file→file imports edge in the real indexing pipeline (not just direct resolver probes); add a test documenting the cross-language `.h` heuristic claim (Obj-C dirs are intentionally allowed as C/C++ include dirs); add CHANGELOG entry under [Unreleased] with measured numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> |
||
|
|
8c69001289 |
fix(resolution): Java/Kotlin imports disambiguate same-name classes (#314) (#472)
A Maven multi-module project where `dao/converter/FooConverter` and `service/converter/FooConverter` both expose a `convert` method used to resolve by file-path proximity — picking whichever class was closer to the caller, which is wrong any time the caller lives in an equidistant cross-cutting module. `extractImportMappings` had no Java branch at all, so the FQN signal Java imports carry — `import com.example.dao.converter.FooConverter;` — was thrown away. - `extractJavaImports` parses regular and `import static` directives; wildcard imports (`*`) are intentionally skipped. - `resolveViaImport` has a new Java/Kotlin cross-file branch that converts the imported FQN to a file-path suffix (`com/example/dao/converter/FooConverter.java`, or `.kt`) and resolves the symbol against the file whose path matches by suffix. - For the field-receiver pattern (`@Autowired private FooConverter fooConverter; fooConverter.convert(...)`), `matchMethodCall` now looks up the receiver's inferred type in the caller file's imports and threads the resulting FQN through to `resolveMethodOnType`. When two `FooConverter::convert` candidates exist, the import — not iteration order — picks the right one. Validated with a synthetic 3-module repro: swapping only the import line on the caller swaps the resolved target between dao and service. spring-petclinic (47 .java files): +15 newly import-resolved edges, +2 references, no regression elsewhere. Closes #314. |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
028d25f3af |
Revert "fix(resolution): filter stale-target edges so watch sync survives FK violations (#455) (#463)"
This reverts commit
|
||
|
|
1dfaf30a8b |
fix(resolution): filter stale-target edges so watch sync survives FK violations (#455) (#463)
PR #62 plugged this FK violation at the extraction-layer insertEdges site (empty-named nodes whose containment edges had no target), but the same violation kept reappearing on v0.9.5 during the daemon's *watch sync* once an agent's daemon had been running long enough. The resolution-layer insertEdges (and the callback-synthesizer pass) wasn't guarded the same way: a per-resolver name cache or a framework resolver's WeakMap-keyed lookup map could hand back a Node whose row had been removed by a recent file rewrite, and the FK check then aborted the entire resolution batch, leaving the daemon log filling with `Watch sync failed { error: 'FOREIGN KEY constraint failed' }`. The resolution layer now mirrors the #62 defense — one cache-aware getNodesByIds per pass drops any edge whose source or target is no longer in the nodes table, so the rest of the resolved batch still lands. Regression test seeds the resolver's nameCache with a stale Node and calls resolveAndPersist directly; verified to throw FOREIGN KEY constraint failed without the fix and pass with it. Full suite: 984/984 pass. Closes #455. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8876defbc1 |
fix(nestjs): propagate RouterModule.register prefixes to controller routes (#459) (#460)
NestJS's RouterModule lets apps compose modular route prefixes across files
(`RouterModule.register([{ path: 'admin', module: AdminModule, children: [...] }])`
in `app.module.ts` sets the prefix for controllers declared in another file's
`@Controller()`). The per-file `extract()` only sees one file at a time, so a
`UsersController` indexed in isolation showed up as `GET /` instead of
`GET /admin/users`.
Add an optional cross-file `postExtract(context)` hook to FrameworkResolver,
called by the orchestrator once after each `indexAll` and after every
incremental `sync` that touched files. The nestjs implementation:
* walks every `*.module.{ts,js}` for `RouterModule.{register,forRoot,forChild}([...])`
and recursively resolves `children` into `Module → /full/prefix`,
* walks `@Module({ controllers: [...] })` for `Controller → Module`,
* matches each route node against its controller's class line range
(multi-controller files keep getting attributed correctly), and
* rewrites `name` while preserving `id` (route→handler edges intact) and
`qualifiedName` (still encodes the *original* in-file path, which keeps
the pass idempotent on a re-sync — `app.module.ts` edits propagate to
controllers in unchanged files without double-prefixing).
End-to-end validated against the exact reproduction in #459 (admin children
users) — all four routes (`GET /admin`, `GET /admin/users`,
`GET /admin/users/:id`, `POST /admin/users`) resolve correctly, edits to the
RouterModule tree re-propagate on the next sync, and route→handler edges in
`codegraph context` are preserved.
Closes #459
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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)
|
||
|
|
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 |
||
|
|
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>
|
||
|
|
7340892290 |
fix: bound resolver caches, validate MCP input sizes, add integration tests (#213)
Replace the 7 unbounded ReferenceResolver Map caches with a bounded LRU (env-tunable via CODEGRAPH_RESOLVER_CACHE_SIZE) so memory stays flat on large codebases, and add length caps on MCP tool string inputs (query/task/symbol + projectPath/path/pattern) to prevent oversized-payload DoS. Includes LRU, MCP-input-limit, and full-pipeline integration tests. Closes #213 |
||
|
|
5b71a89574 |
feat(frameworks): add Drupal 8/9/10/11 support (#271)
Detects Drupal projects via composer.json drupal/* deps; extracts routes from *.routing.yml (route nodes + references edges to controllers/forms/entity handlers) and Drupal hook implementations from .module/.install/.theme/.inc. Adds yaml/twig as file-level languages and excludes core/contrib by default. Resolves #268. |
||
|
|
948b287536 |
feat(frameworks): add NestJS support (#220) (#225)
Detect NestJS projects and emit `route` nodes (each linked by a `references` edge to its handler method) across all four transport layers: - HTTP controllers: @Controller prefix joined with @Get/@Post/@Put/@Patch/@Delete/@Head/@Options/@All - GraphQL resolvers: @Query/@Mutation/@Subscription - Microservices: @MessagePattern/@EventPattern - WebSocket gateways: @SubscribeMessage (prefixed with gateway namespace) Detected from any @nestjs/* dependency in package.json (falls back to scanning *.controller.ts/*.resolver.ts/*.gateway.ts). Handles class+method path joining with empty @Controller()/@Get(), a string-aware balanced-paren arg reader so GraphQL type thunks (@Query(() => [User])) aren't truncated, stacked decorators (@UseGuards) when locating the handler, and disambiguates the @Query() GraphQL method decorator from the REST @Query() param decorator (GraphQL only counts inside @Resolver classes). Also resolves injected *Service/*Controller refs to their classes by Nest file-naming convention. Adds 18 framework tests; updates the README framework table and CHANGELOG. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6ac2066a7a |
test+feat: add cargo workspace crate resolution for rust resolver (#151)
* test+feat: add cargo workspace crate resolution for rust resolver Agent-Logs-Url: https://github.com/miketheman/codegraph/sessions/0101633b-8b63-4951-a6ca-03efe7fafe0b Co-authored-by: miketheman <529516+miketheman@users.noreply.github.com> * perf: cache cargo workspace map during rust resolution Agent-Logs-Url: https://github.com/miketheman/codegraph/sessions/0101633b-8b63-4951-a6ca-03efe7fafe0b Co-authored-by: miketheman <529516+miketheman@users.noreply.github.com> * feat(rust): expand cargo workspace member globs and trust workspace hits - Parse glob entries in `[workspace].members` (e.g. `crates/*`, `helix-*`) via picomatch against a new optional `ResolutionContext.listDirectories` so workspaces that don't enumerate every member are covered. Implementation walks the static-prefix subtree with a depth cap and skips `target`, `node_modules`, `.git`, etc. - Bump Pattern 4's confidence to 0.95 when the workspace map produces a hit. The cargo manifest gives an unambiguous crate-name -> crate-root mapping, so workspace-driven module resolution should beat name-matcher's self-file matches (otherwise every file with `use foo::...` self-resolves at 0.7 and the cross-crate edge never materializes). - Validated against astral-sh/uv (`members = ["crates/*"]`, 67 crates, 567 .rs files): 1,969 cross-crate `imports` edges reaching 60 distinct member lib.rs files, up from 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
74327814ee |
feat: wire up framework route extraction (#89)
* docs: add framework extract wiring plan
* feat(resolution): replace extractNodes with extract() returning nodes and references
* feat(resolution): add getApplicableFrameworks helper for per-language dispatch
* feat(django): emit route nodes and route->view references in extract()
* feat(flask,fastapi): emit route nodes and route->handler references
* feat(express): emit route nodes and route->handler references
* feat(laravel): emit route nodes and route->handler references
* feat(rails): emit route nodes and route->handler references
* feat(spring): emit route nodes and route->handler references
* feat(go): emit route nodes and route->handler references
* feat(rust): emit route nodes and route->handler references
* feat(aspnet): emit route nodes and route->handler references
* feat(swift,vapor): emit route nodes and route->handler references
* chore(react,svelte): migrate resolvers to extract() interface
* feat(extraction): run framework extractors after tree-sitter parse
* docs: document framework route extraction
* feat(strip-comments): add per-language comment stripper for framework extractors
Replaces comment characters and string-literal contents with spaces (not
removal) so source offsets stay valid for downstream regex match index ->
line number conversion. Handles Python triple-quoted docstrings, Ruby
=begin/=end, Rust nested block comments, and the standard //, #, /* */
forms across the supported languages.
This is consumed by framework extract() methods in a follow-up commit so
that commented-out / docstring routing examples don't surface as phantom
route nodes in the graph.
* feat(frameworks): strip comments before regex extraction (prevents phantom routes)
Pipes the per-language stripCommentsForRegex helper into every framework
extract() that scans raw source: django/flask/fastapi (python.ts),
express, laravel, rails, spring, go, rust, aspnet, vapor, plus
swiftui/uikit struct extraction in swift.ts.
Without this, examples like:
# path('/admin/', AdminPanel.as_view())
""" path('/users/', UserListView.as_view()) """
urlpatterns = [path('/real/', RealView.as_view())]
produced 3 phantom route nodes. Now only the real one is extracted.
Each framework gets a regression test in __tests__/frameworks.test.ts
asserting that line-, block-, docstring- and (where relevant)
heredoc-style commented-out routes do not surface as nodes.
---------
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5ab81746e8 |
feat: add Vue support (#66)
Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d151c0f922 |
feat(resolution): tsconfig path aliases + re-export chain following (#130)
* feat(resolution): tsconfig path aliases + re-export chain following
Two related correctness improvements that unlock accurate import
resolution on modern JS/TS codebases.
1) tsconfig/jsconfig path aliases.
The resolver previously had a hard-coded list of common aliases
(@/, ~/, src/, app/) and ignored any project-defined paths from
tsconfig.json compilerOptions.paths — which means every import
through @components/Foo, @lib/utils, etc. on Vite/Next/Nuxt/Nest
projects silently failed to resolve. Adds src/resolution/path-
aliases.ts that reads tsconfig.json (and falls back to jsconfig.json),
honours baseUrl, supports the * wildcard, and respects the priority
order of multiple replacement targets per alias. JSONC tolerant
(strips comments + trailing commas, common in the wild). The new
ResolutionContext.getProjectAliases() lazily loads + caches the
result; resolveAliasedImport consults it before the legacy fallback
list.
Verified live on a synthetic project with @utils/* and @lib custom
aliases: both resolved to the correct files and produced edges,
unresolved_refs empty.
2) Re-export chain following.
`import { Foo } from './barrel'` where barrel.ts only re-exports
(`export { Foo } from './real'` or `export * from './real'`) used
to fail because the resolver only looked for declarations IN the
resolved file — it never followed the export chain to the actual
definition. Adds extractReExports() (named + wildcard + as-rename
forms), a per-file getReExports() context method, and a recursive
findExportedSymbol() helper with depth cap (8) and visited-set
cycle protection. resolveViaImport now uses it whenever the symbol
isn't directly declared in the imported file.
Verified live on a synthetic 3-hop chain (main → all.ts wildcard →
index.ts named → auth.ts declaration): signIn resolved correctly,
unresolved_refs empty.
Full test suite: 380 passed, 0 failed.
* fix(resolution): address reviewer findings — isExternalImport bypass, JSONC strings, comment stripping, optional context method
Five fixes from independent semantic review:
- isExternalImport now consults context.getProjectAliases() before
the bare-specifier heuristic. Without this, custom prefixes like
'@components/*' from tsconfig.paths were classified as npm and
resolveAliasedImport never even ran. Adds a context parameter
(optional, for backward compat with mock contexts).
- stripJsonc rewritten as a string-aware state machine. The previous
regex-only version corrupted any URL embedded in a JSON string
value ('https://cdn.example.com' lost everything after '//').
- extractReExports now strips JS line+block comments from content
before applying the regex, so a commented-out 'export { x } from
...' no longer creates a phantom re-export edge. New
stripJsComments helper preserves string literals (single, double,
template) so '//' inside a string stays intact.
- ResolutionContext.getProjectAliases() made optional so existing
mock contexts in __tests__/resolution.test.ts (which TypeScript
doesn't type-check because tsconfig excludes __tests__) don't
throw at runtime when resolveAliasedImport hits them. Caller
uses ?.
- Two new integration tests in __tests__/resolution.test.ts:
* Path-alias resolution with name-collision: two pickMe() in
different dirs, only the @utils-aliased one should be the
call target. Asserts via getCallers on each candidate node.
* No-tsconfig fallback: relative import still produces the call
edge.
Full test suite: 832 passed (was 380; the increase is from the
biomarkers + LLM hooks that ship via parent branches).
* fix(resolution): allow re-export rename chains past the pre-filter
The fast pre-filter in resolveOne() bails when no symbol with the
reference name exists project-wide, which is incompatible with the
new chain-following code: a renamed re-export (`import { login }
from './barrel'` where the barrel does `export { signIn as login }
from './auth'`) intentionally calls a name that has no project-wide
declaration. The chain finds the renamed upstream symbol — but only
if resolution is allowed to run.
Add an import-mapping escape so the pre-filter only bails when the
ref also doesn't match any local import. Adds two tests covering the
3-hop wildcard chain and the named-rename branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8eed24327c |
feat(extraction): instantiates + decorates graph edges (#134)
* feat(extraction): instantiates + decorates graph edges
Two new structural edges that fill gaps in the call graph for
modern JS/TS / Java / C# / Python / Kotlin codebases.
1) `instantiates` edges from `new Foo(...)`:
The bulk-extraction and visitFunctionBody dispatchers only
recognised `call_expression`; `new_expression` (and the equivalent
`object_creation_expression` / `instance_creation_expression` in
other grammars) was silently ignored. Adds INSTANTIATION_KINDS,
extractInstantiation(), and dispatch from BOTH the top-level
visitNode and the per-function-body walker. Children are still
descended so nested calls inside constructor args (`new Foo(bar())`)
get their own `calls` refs.
Output: a `bootstrap` function that does `new UserService(); new
UserController(svc)` now produces two `instantiates` edges to those
class nodes — previously zero edges.
2) `decorates` edges from `@Decorator` annotations:
Tree-sitter places decorator nodes BEFORE the symbol they apply to
in the AST, so the original walk-time dispatch saw the wrong
nodeStack head (file/class instead of class/method). Replaced with
extractDecoratorsFor(declNode, decoratedId) that runs from inside
extractClass / extractFunction / extractMethod after the symbol's
node id is known.
Looks for decorator nodes in two places:
- Direct named children of the declaration (method/property style)
- Preceding siblings in the parent (TypeScript class style:
@Foo class X {} parses as parent { decorator, class_decl })
Sibling check uses startIndex comparison rather than reference
identity — tree-sitter web bindings return fresh JS wrappers from
parent/namedChild navigation, so `===` is unreliable. Took a debug
session to spot this; flagging in the comment so the next reader
doesn't re-introduce the bug.
Output: a `@Controller` class decorator + `@Get` method decorator
on a NestJS-style controller now produce two `decorates` edges
(class→Controller, method→Get) with the correct source nodes.
Verified live on a synthetic NestJS-shape fixture; all 380
existing tests pass.
* fix(extraction): address reviewer findings — decorator boundary, generic constructors, property/field decorators, marker_annotation, tests
Five fixes from independent semantic review:
- extractDecoratorsFor sibling walk now iterates BACKWARD from the
declaration and stops at the first non-decorator/annotation
separator. Previous version walked forward up to declStart and
consumed every decorator-typed sibling — so two adjacent
decorated classes (`@A class Foo {} @B class Bar {}`) had `@A`
spuriously attributed to `Bar`.
- extractInstantiation strips the type-argument suffix from the
constructor field text. `new Map<K, V>()` was producing
referenceName 'Map<K, V>' (the constructor field is a generic_type
node) and resolution always failed.
- extractProperty and extractField now call extractDecoratorsFor
after their createNode calls. NestJS-style `@Inject() private
svc: Foo` and Java field annotations were being silently dropped.
- consider() in extractDecoratorsFor recognises 'marker_annotation'
in addition to 'decorator'/'annotation'. Java's tree-sitter grammar
emits marker_annotation for arg-less annotations like @Override
and @Deprecated; without this every Java marker annotation was
silently skipped.
- 6 new extraction tests covering: instantiates ref for new Foo(),
generic-type stripping (`new Container<string>()` -> 'Container'),
qualified-new keeps trailing identifier (`new ns.Foo()` -> 'Foo'),
decorates ref for @Foo class X {}, regression for adjacent
decorated classes (each gets its OWN decorator), decorates ref
for @Foo method().
Full test suite: 386 passed (was 380, +6 new extraction tests).
* feat(resolution): kind-aware scoring + Python instantiation promotion
Two follow-ups to the new instantiates/decorates ref kinds, surfaced
during review:
1) name-matcher previously only had a kind bonus for `calls`
(preferring function/method). When a class and a function share a
name across modules, an `instantiates` ref would tie or pick the
wrong candidate. Adds:
- `instantiates` → +25 for class/struct/interface
- `decorates` → +25 for function/method, +15 for class
(Python class decorators, Java annotation interfaces)
2) Python (and Ruby) have no `new` keyword — `Foo()` is the standard
instantiation syntax, indistinguishable from a function call at
extraction time. Resolution can tell the difference once the
target is known: when a `calls` ref resolves to a class/struct,
promote it to `instantiates`. Mirrors the existing extends→
implements promotion in createEdges.
Verified: 386 → 389 passing (+3 tests covering the kind biases and
the Python promotion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f402ab8363 |
feat: Add complete PHP language support with trait handling and property extraction
Addresses PHP traits extracted as classes, missing class properties, skipped constants, and invisible trait usage. Adds classifyClassNode to distinguish traits from classes, fixes property extraction for PHP's property_element AST structure (added 4,366 field nodes), and adds visitNode hook for class constants and trait use declarations (increased trait edges from 636 to 1,514). Also improves Liquid schema name handling and file path reference resolution. Verified against Laravel codebase. |
||
|
|
07d899b735 |
feat: Promote "extends" to "implements" for class-to-interface relationships in edge creation
Addresses semantic accuracy in inheritance relationships where classes use "extends" syntax to implement interfaces. Adds target node inspection to detect interface/protocol targets and promotes the edge kind from "extends" to "implements" when the source is a concrete class or struct, ensuring proper representation of implementation vs inheritance relationships in the code graph. |