Updates the chained-call design doc: Pascal moves from "blocked" to covered
(#791) — the earlier "blocked" read was wrong, caused by probing only the
paren-less form. 13 languages now shipped; EXTRACTION_VERSION 16.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
A checked-in design doc for the #645/#608/#750 chained-call mechanism — the
permanent, discoverable record the work previously lacked (it lived only in git
history, the tracking issue, and an untracked scratch handoff). Covers the 3-part
mechanism, the three shared resolvers + receiver styles, the per-language coverage
matrix (12 shipped with A/B results), the conformance pass, and the full 21-language
README classification (incl. why TypeScript + Luau were skipped and Pascal is blocked).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
* 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>
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>
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>
* 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>
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>
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>
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>
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>
The per-word path fix (#745) brought the backend to parity but not above:
the project name still gave the lexically-matching stack a residual dir
match + an FTS class-name match, so a backend query that included the
project name still ranked the frontend at/above the backend.
Derive the project name from go.mod module / package.json name / repo dir,
and treat a query word matching it as non-discriminative: drop it from path
relevance and from codegraph_explore's PascalCase type-disambiguation bias
(reporter's suggestions #1/#2) — unless it's the only query word, so a bare
project-name search still scores.
Narrow by construction: the down-weighting fires ONLY when a query word
matches the derived project name (≥5 chars), so every query that doesn't
name the project is byte-identical. On the reporter's repro the backend
controllers now top a backend question that includes the project name;
queries without it, bare project-name queries, and normal symbol queries
are unchanged. Query-time only (no re-index).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-word PascalCase query token — typically a project name a user
includes (`SuperBizAgent backend routes`) — splits into sub-tokens
(superbizagent / super / biz / agent) that ALL match the same path segment,
so path relevance summed +5 four times for one concept. In a mixed-stack
repo that ~doubled every score of the lexically-matching stack's file,
burying the stack the query was about.
Score path relevance per original query WORD instead: a word matches a path
level if any of its sub-tokens do, and counts once — while still splitting
the word (via extractSearchTerms on the original case) so it matches across
naming conventions (`getUserName` → `get_user_name`). Distinct words each
still contribute.
Partial fix: this removes the dominant path over-counting (backend rises
from absent-in-top-6 to parity on the reporter's repro). The residual lexical
edge from the project name in the FTS class-name match + dir match is a deeper
down-weighting change, tracked separately. No re-index needed (query-time).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A function called only from an anonymous func_literal at package level — a
cobra `RunE: func(){…}` handler, a goroutine literal, a callback closure
stored in a `var` — had its call leak to the FILE node, because the Go
var-initializer walk ran with an empty scope. So `callers`/`impact` showed
the function with a file (or no meaningful) caller, unlike JS/TS where an
arrow-in-const becomes a named node whose calls attribute correctly.
Scope the Go top-level var/const initializer walk to the declared symbol, so
a call nested in any func_literal initializer (struct field, slice/map,
nested closure) attributes to the enclosing var. EXTRACTION_VERSION 3->4
(re-index to pick up the corrected attribution).
Validated on cli/cli (858 Go files): node/edge counts identical, file-level
dependents byte-identical (no regression), and 62 top-level-closure calls
correctly moved from file-attributed to var-attributed.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A .gitignore transparently encrypted in place by corporate DLP / endpoint
software (UTF-16 header + ciphertext), or one containing a pattern the
`ignore` library can't compile to a regex (`\[` -> "Unterminated character
class"), crashed the entire sync/index. The throw is LAZY — it surfaces at
match time (`ig.ignores()`), not `.add()` — so the existing add-time
try/catch never caught it, and the error never named the offending file.
Read .gitignore defensively: skip a file that isn't valid UTF-8 text whole
(NUL byte or fatal UTF-8 decode), drop only the individual uncompilable
patterns from a text one (probe-compile, then per-line fallback), and warn
with the file path. Indexing continues either way. The watcher inherits the
fix via buildDefaultIgnore.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Two environments that share one working tree — most concretely Windows
and WSL — can't safely share a single `.codegraph/`: the daemon lockfile
records a platform-specific pid + socket (named pipe vs Unix socket), and
SQLite locking across the WSL2/Windows filesystem boundary is unreliable,
so two daemons over one index risks corruption.
Add a `CODEGRAPH_DIR` env var (default `.codegraph`) that overrides the
per-project data directory name, so each environment keeps its own index
in the same tree (e.g. `CODEGRAPH_DIR=.codegraph-win` on Windows). The
name is resolved live and validated (rejects separators / `..` / absolute,
falling back to the default with a one-time stderr warning). Indexing and
file-watching now skip ANY `.codegraph-*` sibling so neither side trips
over the other's data.
Routes the previously-hardcoded `.codegraph` literals (db path, lockfile,
error log, watcher ignore, file-scan skip, installer) through the
resolver. No extraction-version bump — index content is unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TypeScript service/RPC contracts written as a tuple of generic types —
`type List = [Service<'query_apply_record', Req, Resp>, …]` — carry their
names only as string-literal type arguments, so static extraction never
indexed them and `codegraph query query_apply_record` returned nothing.
Add a narrow TS/TSX type-alias pass that emits each tuple entry's
string-literal name as a `method` node under the alias (qualifiedName
`List::query_apply_record`), making it searchable. Scope is limited to a
direct literal arg of a generic that is a direct tuple element, with a
valid-identifier filter — so utility types (Pick/Omit/Record), deeper
nested generics, and route paths produce no noise.
Bumps EXTRACTION_VERSION so existing indexes get a re-index hint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes codegraph_node a drop-in faster Read for indexed source files (file-read mode: <n>\t<line> like Read, offset/limit, + blast-radius header; symbolsOnly for the map). Fixes the old file-view dropping imports/line-numbers. #383/#527 preserved. Validated by A/B: explore/node already return source + line numbers, so Read=0 when used. Includes the A/B eval harness scripts. Full suite green (1270).
Corrects the "run non-nested only" conclusion from #734. The codegraph server
is healthy (handshake ~165ms); the flakiness is that on a multi-step
implementation task the agent dives into Read/grep before codegraph finishes
its ~2-3s startup (worse under nested CPU contention), so it runs with no
codegraph. Fix: pre-warm a persistent daemon (high idle timeout) + skip the
startup re-exec (CODEGRAPH_WASM_RELAUNCHED=1) so claude connects before the
agent's first turn. claude's init snapshot can show status:"pending" even when
it then connects — judge by actual codegraph usage, not the init line.
ab-new-vs-baseline.sh now bakes in the pre-warm + skip-re-exec. Validated: a
clean A/B showed the new build's agent used codegraph 2x / 5 Reads vs the
baseline's 0 / 8 on the same fully-implemented task.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running scripts/agent-eval against a `claude -p` spawned from within a Claude
Code session (nested, e.g. from a Bash tool call) makes the codegraph MCP
attach unreliable: the server is healthy (full handshake ~165ms) but the
nested client marks it status:"pending"/0-tools under CPU/timing contention,
so the agent silently runs with no codegraph. NO_DAEMON + `< /dev/null` don't
fix it — it's the nested client, not the server. Documented in CLAUDE.md's
validation methodology.
Adds ab-new-vs-baseline.sh: A/Bs a retrieval/steering change as new-build vs
baseline-build (both codegraph-on, isolating the change — vs run-all.sh's
with-vs-without), on a throwaway copy of an indexed repo. Run it in a real
terminal.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(mcp): steer agents to codegraph during implementation, not just Q&A
Two changes targeting agents that reach for Read during edits instead of codegraph:
1. Reframe the agent-facing steering (server-instructions + codegraph_node/explore
descriptions): drop "consult BEFORE ... not during"; position codegraph_node as
the Read upgrade for a named symbol (verbatim current on-disk source, safe to
Edit from, + caller/callee trail), explore PRIMARY / node SECONDARY, with the
"cached intelligence — better context, fewer tokens" framing.
2. File-view mode: codegraph_node now accepts a `file` with no `symbol` and returns
that file's symbol map + graph role (its dependents), plus verbatim bodies with
includeCode — so it can displace a path-keyed Read, not just a symbol lookup.
Resolves a path or basename; dedups nested members; budget-capped.
To be A/B'd on an implementation task before shipping (per the retrieval doctrine:
steering changes must be measured, not assumed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): note codegraph_node file-view + implementation steering
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Attached to shared daemon" line is benign INFO, but it was written to
stderr — and MCP hosts render all server stderr at error level (and append an
`undefined` data field), so on every session start a healthy attach showed up
as `[error] … undefined`. It is now gated behind CODEGRAPH_MCP_LOG_ATTACH=1:
silent by default, opt-in for debugging daemon attach. Both attach sites
(runProxy + connectWithHello) route through one helper. The daemon integration
tests opt the harness into the log so their attach assertions still observe a
successful attach.
Re-applies the approach from #640 by @mturac.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): resolve symlinks in path validation to block out-of-root reads (#527)
validatePathWithinRoot was purely lexical (path.resolve + startsWith), so an
in-repo symlink whose logical path is inside the project root but whose real
target escapes it passed validation — and both content-serving read sinks
(codegraph_node includeCode, codegraph_explore source) then readFileSync'd it,
leaking out-of-root file contents (e.g. ~/.ssh, /etc) to the agent.
Add a realpath layer: after the lexical check, resolve symlinks on both the
candidate path and the root and re-compare, rejecting anything whose real path
escapes the root. An in-root symlink is still allowed (no over-blocking).
Comparison is case-insensitive on Windows (NTFS + realpath casing). Not-yet-
existing paths (ENOENT) fall back to the lexical result so about-to-be-written
files still validate; other resolution errors reject.
Removes the dead, never-called isPathWithinRoot / isPathWithinRootReal helpers
(the latter a footgun — it returned true on realpath failure). Adds RED->GREEN
tests: in->out file/dir symlinks rejected, in->in allowed, ../ rejected, ENOENT
allowed, plus an end-to-end test proving getCode no longer serves an out-of-root
file reached through a dir symlink.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changelog): note the #527 symlink path-escape fix
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Vendor tree-sitter-c-sharp 0.23.5 (ABI 15) for C#, replacing the bundled ABI-13
build that dropped primary-constructor classes. Adds native primary-ctor
parsing, primary-ctor parameter dependency edges, return-type extraction via the
renamed `returns` field, and a preParse that blanks `#if` directive lines the
new grammar mis-parses inside enum bodies. Validated on MediatR / eShopOnWeb /
Newtonsoft.Json + full suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
When an MCP host (opencode and others) SIGTERM's the shared daemon as a new
session starts, the existing session's proxy used to exit on the dropped socket
— silently losing CodeGraph for that session, and hanging any request in flight
at the drop. The SIGTERM originates in the host's process-tree teardown, not in
CodeGraph (nothing here signals another process), so the fix is proxy
resilience, not chasing the signal.
The local-handshake proxy now treats a daemon disconnect as recoverable rather
than terminal: it falls back to its in-process engine for the rest of the
session (the same path used when no daemon is reachable at startup, and what
CODEGRAPH_NO_DAEMON does) and re-serves any requests that were in flight to the
dead daemon, so the host never hangs. The proxy still exits when the HOST goes
away (stdin close / PPID watchdog) — only daemon loss is now non-fatal.
Also replaces the over-the-wire liveness-sweep test added in #712 — which was
flaky under heavy parallel load (a raced raw-socket connect) — with a
deterministic Daemon.reapDeadClients unit test. The client-hello round-trip is
still exercised by every daemon test (the real proxy now sends it).
Validated with a reproduction (proxy stays alive, in-flight request answered,
post-drop request recovers) and a regression test in mcp-daemon.test.ts.
Confirmed on macOS (full suite green) and a Windows 11 VM.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layer-2 defense-in-depth follow-up to the Windows PPID watchdog fix (#711).
That fix makes an orphaned proxy exit so its socket closes and the daemon
reaps via the refcount + idle timer. This adds two daemon-side safety nets for
the residual case where a socket close is never delivered (a Windows named-pipe
hazard) and a phantom client would otherwise pin the daemon forever:
- Liveness sweep: a proxy now sends an optional client-hello carrying its pid
(+ host pid) right after verifying the daemon hello; the daemon periodically
drops any client whose peer process is dead, re-arming the idle timer.
Fail-safe and version-pinned — a connection that never sends the hello just
falls back to the socket-close lifecycle, and the daemon reads it before the
transport so a non-hello first line is handed through untouched.
- Inactivity backstop: the daemon exits after a generous no-traffic window
(CODEGRAPH_DAEMON_MAX_IDLE_MS, default 30 min) even with clients attached, so
a phantom client that sends nothing can't keep it alive.
Pure helpers (parseClientHelloLine, peerIsDead) are unit-tested; the full
handshake + sweep and the backstop are covered end-to-end in mcp-daemon.test.ts.
Validated on a real Windows 11 VM: the sweep reaps a dead-pid client over a
named pipe and the backstop fires with a client still connected.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Windows the PPID watchdog could never fire: orphans aren't reparented, so
`process.ppid` stays constant after the parent dies (defeating the ppid-change
check), and the standalone bundle pre-bakes `--liftoff-only`, skipping the
relaunch that sets `CODEGRAPH_HOST_PPID` (defeating the host-liveness check).
With neither signal available, an orphaned proxy / direct server ran forever,
the shared daemon never saw the client disconnect, and its idle timer never
armed — node processes accumulated until CPU saturated.
Add a win32-only signal: poll the original parent's liveness directly, since
ppid is stable there. Gated to Windows so POSIX double-fork cases keep relying
on the ppid-change signal (a dead original parent is not proof of orphaning on
POSIX). The decision is extracted into a pure, unit-tested helper shared by all
three watchdog sites (proxy socket, proxy local-handshake, direct mode).
Validated on a real Windows 11 VM: in the exact bundle scenario (direct mode,
no HOST_PPID) an orphaned server now exits within one watchdog poll via the new
path; the POSIX reparent path is unchanged and its integration test still passes.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`codegraph upgrade [version]` detects how the CLI was installed — the standalone
install.sh/install.ps1 bundle, npm-global, npx, or a source checkout — and
updates in place: re-running the canonical install.sh on macOS/Linux, an
in-place rename-and-extract swap on Windows (a running node.exe can't be
deleted, only renamed, so the detached-helper approach is avoided), and
npm/npx/source-specific guidance otherwise. Flags: `--check` (report only),
`--force`, and a positional version to pin.
Each full index is now stamped with the engine's EXTRACTION_VERSION in
project_metadata; `codegraph status` (and `--json`) flags an index built by an
older engine and recommends re-indexing, and `upgrade` prints the same reminder.
Gated on EXTRACTION_VERSION so it never nags on extraction-neutral releases.
Validated end-to-end on macOS (real bundle upgrade), Linux (Docker, real
curl|sh) and Windows (Parallels VM, real in-place swap). 32 new unit tests.
Closes#679
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- Regenerate assets/waitlist.svg as "Join the waitlist" (no exclamation),
same cream/8px/padding styling.
- Add ?v=2 cache-buster to the README image URL so the new button shows
immediately instead of waiting on GitHub's image cache.
- CLAUDE.md house rule: version-tag every README image and bump ?v=N in
the same commit whenever the asset changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the plain oxblood-filled rectangle + system-font text SVG with a
polished button that matches the getcodegraph.com design language:
- Cream (#f7f6f2) rounded-rect background with a soft hairline border, 8px
radius, 52px height
- Logo mark (graph triangle, mirrors favicon.svg) in ink/oxblood at left
- Hairline divider separating mark from label
- "Join the waitlist!" label rendered as vector outlines (Archivo Bold 760,
17.5px) so the brand typeface renders correctly on GitHub, which blocks
@font-face in statically-served SVGs
- Oxblood arrow at right
Adds assets/generate-waitlist.py (requires fonttools + brotli) so the SVG
can be regenerated from the landing-page's Archivo variable font. Updates
the README img height from 44→52 to match the new geometry.
Advertise the upcoming hosted CodeGraph platform at the top of the README with a "Join the waitlist" CTA (early beta access) linking to getcodegraph.com, plus the button SVG asset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vue's extractor parsed only the <script> block, so a component used solely
in another component's <template> (`<MyButton />`) produced no reference —
and thus showed a false 0 callers, even after the barrel-resolution fix in
PR #657. This is the Vue analogue of Svelte's extractTemplateComponents.
extractTemplateComponents() now scans the template (everything outside the
<script>/<style> blocks, which also handles nested <template> tags for
v-if/slots) for component tags:
- PascalCase tags (`<MyButton/>`) — captured as-is.
- kebab-case tags (`<my-button/>`) — converted to PascalCase so they match
the imported component's name. Safe: an unmatched name creates no edge
during resolution, so native custom elements just don't resolve.
- Native HTML elements (lowercase, no hyphen) and Vue built-ins
(Transition, KeepAlive, …) are skipped.
Adds no nodes — only `references` — so node counts stay stable. With this
plus #657, a Vue component re-exported through a barrel and used only in a
template now resolves end-to-end (callers/impact/callees).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Batch of small, localized fixes from an open-issue triage:
- .codegraph/.gitignore now ignores everything but itself, so the database,
daemon.pid, sockets, and logs stop showing up in git status (#492, #484)
- MCP server answers resources/list and prompts/list with empty lists instead
of -32601, clearing scary log lines in opencode/Codex (#621)
- index SAP HANA .xsjs/.xsjslib as JavaScript (#556) and TS .mts/.cts (#366)
- visit anonymous AMD/CommonJS/IIFE wrapper bodies so their inner functions and
calls are indexed instead of coming up empty (#528)
- batch the changed-file lookup so a huge first sync no longer hits
"too many SQL variables" (#540)
- list files with `git ls-files -z` so non-ASCII/CJK paths survive
core.quotepath and are no longer silently skipped (#541)
- attach Go methods on generic receivers (*T[P]) to their type (#583, RC1)
- impact no longer climbs the structural `contains` edge, so a leaf symbol
stops dragging in its sibling methods (#536)
- README: explicit `codegraph install` step, run in a new shell (#631)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
chokidar v4 holds one OS file descriptor per watched file on macOS (libuv's
kqueue backend registers an fd per vnode; fsevents is installed but v4 no
longer uses it). On a large project the `serve --mcp` daemon accumulated tens
of thousands of open REG descriptors and exhausted kern.maxfiles — crashing
unrelated processes system-wide with ENFILE. #276 only trimmed the count by
ignoring directories; the source tree still cost one fd per file.
Replace chokidar with a pure-JS native fs.watch hybrid, keeping codegraph's
zero-native-addon "any OS builds any bundle" invariant:
- macOS / Windows: a single recursive fs.watch (one FSEvents stream /
ReadDirectoryChangesW handle) -> O(1) descriptors regardless of repo size.
- Linux: one inotify watch per directory (O(dirs), dynamic add for new
dirs, capped via CODEGRAPH_MAX_DIR_WATCHES) instead of per-file watches.
Validated empirically: macOS 0 extra fds at 6k and 12k files; Linux 31 inotify
watches at 6k files (per-file would be 6k); Windows recursive catches nested
and new-directory edits. Full test suite green.
Tests drive the watcher through an inertForTests seam (no OS watcher) for
determinism under parallel vitest, with one real-fs end-to-end test exercising
the genuine native path.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures three development session checkpoints: per-symbol adaptive sizing (PR #569), trace relevance + cold-start fix (PR #580), and the explore-overhaul arc (explore as sole primary + Zustand store coverage + budget/render tuning). Documents root causes found, gotchas (stale-daemon foot-gun, Mac sleep corrupting benchmarks), validation methodology, and open threads for each session.