Commit Graph
100 Commits
Author SHA1 Message Date
2f96f58cbb fix(scala): resolve chained static-factory/apply calls Foo.create().bar() (#750) (#761)
Ports the #645 (C++) / #608 (PHP) chained-receiver mechanism to Scala. A call
whose receiver is itself a call — `Foo.create().bar()` (companion factory),
`Builder(cfg).bar()` (case-class apply), or a fluent chain — used to drop the
receiver to a bare `bar`, which name-matched a same-named method on an unrelated
type. The most common wrong edge was a stdlib `Option`/`Iterator` `.map`/`.flatMap`/
`.foreach` mis-attributed onto the project's own same-named class.

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:10:01 -04:00
75ae1e8bd9 fix(search): down-weight the project name in ranking — completes #720 (#748)
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>
2026-06-08 22:43:54 -04:00
afec1282e1 fix(search): score path relevance per query word, not per sub-token (#720) (#745)
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>
2026-06-08 22:23:42 -04:00
5b3f5e36db fix(go): attribute calls inside top-level closures to the var, not the file (#693) (#744)
A function called only from an anonymous func_literal at package level — a
cobra `RunE: func(){…}` handler, a goroutine literal, a callback closure
stored in a `var` — had its call leak to the FILE node, because the Go
var-initializer walk ran with an empty scope. So `callers`/`impact` showed
the function with a file (or no meaningful) caller, unlike JS/TS where an
arrow-in-const becomes a named node whose calls attribute correctly.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:05:36 -04:00
Colby MchenryandGitHub 1983590533 feat(mcp): codegraph_node reads files like the Read tool — offset/limit, byte-parity (#738)
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).
2026-06-08 13:48:42 -04:00
0e2789ab71 docs(agent-eval): nested MCP attach is startup-latency, not a hard block (#735)
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>
2026-06-08 10:25:40 -04:00
28c5268ee3 docs(agent-eval): evals must run non-nested + add ab-new-vs-baseline.sh (#734)
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>
2026-06-08 09:59:07 -04:00
7175dc456c feat(mcp): steer agents to codegraph during implementation + file-view node mode (#733)
* 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>
2026-06-08 09:47:23 -04:00
10defecc4b fix(mcp): silence the daemon-attach log by default (#618) (#725)
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>
2026-06-08 01:25:21 -04:00
7fd8b4c185 fix(security): resolve symlinks in path validation to block out-of-root reads (#527) (#724)
* 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>
2026-06-08 01:02:34 -04:00
112e278b5c fix(security): index config files by key only, never surface values (#383) (#722)
Spring `application.{properties,yml}` keys (and Shopify Liquid `{% schema %}`
blocks) were storing the config VALUE in the node docstring, and
`codegraph_explore`'s source section re-read the raw `key = value` line off
disk — so a secret committed to a config file (DB password, API key, JDBC URL
with embedded credentials) could be pushed into an agent's context via
explore/node output without the agent ever opening the file.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:50:15 -04:00
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>
2026-06-07 03:10:09 -04:00
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>
2026-06-07 02:11:06 -04:00
471084dd6e fix(daemon): keep a session alive when its daemon is restarted under it (#662) (#713)
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>
2026-06-06 16:43:02 -04:00
80358a84d9 fix(daemon): reap dead-peer clients + inactivity backstop so a daemon can't leak (#692) (#712)
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>
2026-06-06 16:23:48 -04:00
565eb20e26 fix(windows): reap orphaned MCP processes when their parent exits (#692, #576, #680) (#711)
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>
2026-06-06 15:05:35 -04:00
4e5cf2de56 feat(cli): add codegraph upgrade self-update + stale-index re-index hint (#710)
`codegraph upgrade [version]` detects how the CLI was installed — the standalone
install.sh/install.ps1 bundle, npm-global, npx, or a source checkout — and
updates in place: re-running the canonical install.sh on macOS/Linux, an
in-place rename-and-extract swap on Windows (a running node.exe can't be
deleted, only renamed, so the detached-helper approach is avoided), and
npm/npx/source-specific guidance otherwise. Flags: `--check` (report only),
`--force`, and a positional version to pin.

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

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

Closes #679

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:02:59 -04:00
Colby McHenryandClaude Opus 4.8 bfa84d32b8 docs(readme): drop "!" from waitlist button, version-tag README images
- 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>
2026-06-05 00:54:10 -04:00
Colby McHenry 2a7b34d5a3 docs(assets): redesign waitlist SVG button with outlined Archivo typeface and brand palette
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.
2026-06-05 00:38:24 -04:00
Colby McHenryandClaude Opus 4.8 c95ead5c4e docs(readme): add getcodegraph.com waitlist banner
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>
2026-06-04 19:19:27 -04:00
629d8472b1 fix(extraction): index Vue <template> component usages (#629 follow-up) (#659)
Vue's extractor parsed only the <script> block, so a component used solely
in another component's <template> (`<MyButton />`) produced no reference —
and thus showed a false 0 callers, even after the barrel-resolution fix in
PR #657. This is the Vue analogue of Svelte's extractTemplateComponents.

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

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:44:27 -05:00
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>
2026-06-02 21:37:56 -05:00
7b62356f53 feat(cli): add version, indexPath, lastIndexed to status --json (#329)
Adds `version`, `indexPath`, and an ISO `lastIndexed` to `codegraph status --json`, plus a `CodeGraph.getLastIndexedAt()` library method. `agentCount` dropped (no clear consumer). Reworked from contributor PRs #333 and #480.

Co-Authored-By: Javier Gómez <199902626+12122J@users.noreply.github.com>
Co-Authored-By: Ran <8403607+eddieran@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:50:33 -05:00
ddb1a8f72d fix: issue-triage quick wins (extraction, MCP probes, gitignore, CJK, impact) (#654)
Batch of small, localized fixes from an open-issue triage:

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:49:15 -05:00
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>
2026-06-02 14:23:33 -05:00
c9559d9991 fix(watcher): bound fd/watch cost with a native fs.watch hybrid (#644, #496, #555, #628, #579) (#650)
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>
2026-06-02 14:21:29 -05:00
Colby McHenry 434be1da58 Merge remote-tracking branch 'origin/main' 2026-06-02 11:36:48 -05:00
Colby McHenry 0bc13231f7 docs(claude): add handoff notes for explore-overhaul arc and related sessions
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.
2026-06-02 10:37:28 -05:00
Colby McHenryandClaude Opus 4.8 0cc7ba902f chore(release): bump version to 0.9.9
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:31:47 -05:00
Colby MchenryandGitHub 68eaf0dbd8 feat(mcp): codegraph_explore as the sole primary tool + store coverage + overload disambiguation (#647)
## Summary

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

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

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

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

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

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-02 10:15:27 -05:00
081a6fcaca chore(release): bump version to 0.9.8 (#604)
Cuts the 0.9.8 release, which carries the restored embedded/programmatic
SDK API (#354) along with the rest of the [Unreleased] changelog. The
Release workflow promotes [Unreleased] -> [0.9.8] and appends the link
reference; only the version fields are bumped here.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:19:11 -05:00
89d4d37a29 feat(npm): restore programmatic/embedded SDK API (#354) (#603)
The 0.9.x thin-installer turned @colbymchenry/codegraph into a bin-only
shim: require("@colbymchenry/codegraph") threw MODULE_NOT_FOUND and no
types shipped, breaking embedded library consumers (e.g. Electron apps)
upgrading from 0.8.0.

Restore programmatic use without re-bloating the thin shim or duplicating
the ~49 MB of grammars the per-platform bundle already carries:

- main -> npm-sdk.js re-exports the installed per-platform bundle's compiled
  library (lib/dist/index.js) at runtime, reusing that bundle's own deps; it
  falls back to a self-healed cache bundle, else throws an actionable error.
- types -> ship the .d.ts tree only (~590 KB) in the main package, built from
  the same release so it can never skew from the runtime it re-exports.
- exports map resolves the `types` condition (nodenext) and the default entry.
- DatabaseConnection + QueryBuilder are now top-level exports, so embedded
  callers get the building blocks from the package entry instead of deep
  dist/ imports (which the shim no longer ships).

The CLI/MCP `bin` keeps execing the bundled Node; only library consumers run
on their own runtime, which must be Node 22.5+ for the built-in node:sqlite.

Validated end-to-end: built a real darwin-arm64 bundle, packed the npm
packages, installed them into a throwaway consumer, and confirmed require()
plus a full init/indexAll/searchNodes round-trip and the low-level
DatabaseConnection/QueryBuilder path all work on the host Node; types resolve
under both nodenext and classic node resolution; and the CLI shim still
launches. New hermetic tests cover npm-sdk resolution, cache fallback, and the
missing-bundle error.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:05:40 -05:00
Colby MchenryandGitHub 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).
2026-05-31 18:41:41 -05:00
b026e64b41 feat(mcp): per-symbol adaptive codegraph_explore sizing (#569)
Sizes codegraph_explore to the answer, not the file count: shows the mechanism +
the exact methods you named in full (even buried in a large file) while collapsing
redundant interchangeable implementations to signatures. Adds uniqueness-aware
spare, per-symbol focused rendering of family files, all-tier test-file exclusion,
and named-method cluster survival in non-sibling god-files.

Validated A/B (Opus 4.8, 7-repo sweep): avg 25%% cheaper / 57%% fewer tokens / 23%%
faster / 62%% fewer tool calls. Django 9->23%% cheaper (0 reads), OkHttp 4->11%%
cheaper; gains across small/medium/large, inert repos unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 23:06:12 -05:00
f1b14f021b feat(mcp): adaptive codegraph_explore sizing — skeletonize redundant polymorphic siblings (#564)
codegraph_explore now skeletonizes off-spine, redundant members of a polymorphic
family (OkHttp's interceptor chain, Django's SQLCompiler family) to signatures
instead of shipping every full body, while keeping the dispatch mechanism, the
orchestrator/base, and any method the agent named in full. Sizes the response to
the answer rather than the budget cap, so interface-heavy flows stop costing more
than plain grep/read. Default on; CODEGRAPH_ADAPTIVE_EXPLORE=0 disables.

Gate: off-spine + >=3-impl sibling + not-spared, where spared = the agent named a
callable in the file UNLESS the file defines the family's supertype (a huge
base+subclasses file is Read-anyway, so skeletonizing frees explore budget).

Validated headless A/B (Opus 4.8): both former README cost outliers flipped —
OkHttp and Django went from costlier-than-native to cheaper; full 7-repo average
22%% cheaper / 47%% fewer tokens / 20%% faster / 50%% fewer tool calls, every repo
cost-positive, inert repos unchanged. 7-case regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:42:00 -05:00
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>
2026-05-28 23:41:11 -05:00
7a75c82dd9 feat(cli): codegraph init builds the initial index by default (#483) (#546)
`codegraph init` now runs the initial index automatically. The -i/--index
flag is kept but is now a no-op, accepted for backward compatibility so
existing muscle memory and scripts don't break. Addresses #483, where a user
asked why -i wasn't implicit.

README and site/ docs are intentionally NOT updated in this commit — they
describe the currently-released 0.9.7 behavior (where -i is still required).
Update them at the 0.9.8 release so users on 0.9.7 aren't misled.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:25:07 -05:00
5fbb9f6787 docs(readme): clarify what init -i does vs plain init (#483) (#545)
A user asked what the `-i` flag does and why it isn't the default. Add a short
note under the Initialize Projects quickstart: plain `init` only scaffolds the
`.codegraph/` dir, `-i`/`--index` also runs the first index, and without it you
run `codegraph index` afterwards.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:15:34 -05:00
cdbf451440 fix(extraction): count all file-level-tracked langs (incl .properties) as indexed (#544)
Completes #357. The no-symbol file-level class is yaml/twig/properties, but the
count fix only covered yaml/twig — so a .properties-only project still printed
"No files found to index" even though the files were stored. Introduce a single
isFileLevelOnlyLanguage predicate (the canonical set behind the tree-sitter
no-symbol branch, xml excluded since its MyBatis extractor emits a file node)
and use it at both count sites and the extraction dispatch so the list can't
drift. Adds .properties regression coverage for indexAll() and indexFiles().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:47:33 -05:00
Colby McHenryandClaude Opus 4.8 54caceae31 docs(claude): codify friendly New Features / Fixes changelog format
Update the 'Writing changelog entries' guidance to match the rewritten CHANGELOG:
friendly New Features / Fixes sections (Breaking Changes / Security only when
present), one plain sentence per bullet, strip internal paths/symbols/benchmarks,
keep #PR refs + contributor thanks. Notes why multi-word headings are safe on the
normal release path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:55:44 -05:00
Colby McHenryandClaude Opus 4.8 2e19234578 docs(changelog): rewrite all release notes into friendly New Features / Fixes format
Distill every release's engineer-facing entry into plain-language, user-readable
notes (New Features / Fixes, with Breaking Changes / Security surfaced where they
apply). Also reconcile the mislabeled [0.7.8] block to [0.7.9] to match the
published GitHub release tag and fix its dead link reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:48:23 -05:00
Colby McHenryandClaude Opus 4.8 15dbcdbac0 chore(release): bump version to 0.9.7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:24:38 -05:00
a9c9e76d8c fix(installer): stop duplicating agent instructions; MCP server is the single source of truth (#529) (#538)
The installer wrote a `## CodeGraph` usage block into each agent's
instructions file (CLAUDE.md / AGENTS.md / GEMINI.md / .cursor/rules /
Kiro steering) that duplicated, almost verbatim, the guidance the MCP
server already emits in its `initialize` response — so agents that
surface MCP instructions (Claude Code) read the same playbook twice
every turn.

All 6 instruction-writing targets (claude, cursor, codex, opencode,
gemini, kiro) now stop writing the block. install self-heals by
stripping a block a previous version wrote (uninstall already did), so
the next `codegraph install`/`uninstall` cleans up existing installs;
upgrading the package alone does not (the leftover block is harmless).
server-instructions.ts is now the single source of truth — the two
steers unique to the old template ("trust codegraph, don't re-verify
with grep" and the not-initialized -> `init -i` hint) are ported there.

Removes the now-dead INSTRUCTIONS_TEMPLATE / CLAUDE_MD_TEMPLATE,
claude-md-template.ts, writeClaudeMd / hasClaudeMdSection, and the
Cursor-only wireProjectSurfaces bootstrap. The install log learned a
"Removed" verb. Tests rewritten to the new contract + self-heal
coverage (140/140 installer tests pass).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:13:23 -05:00
71935e37c2 feat(mcp): multi-module Go trace-quality + small-repo retrieval tuning (#494)
* feat(go): generated-file down-rank + gRPC stub-impl bridge + trace-failure inlining

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

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

Changes:

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

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

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

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

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

Tests:

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

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

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

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

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

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

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

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

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

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

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

Tests: 1076/1076 still pass.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Tests: 1076/1076 still pass.

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

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

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

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

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

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

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

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

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

Tests: 1076/1076 still pass.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:38:03 -05:00
978ddba4ef fix(release): use RELEASE_PAT for git pushes so promote+sync land on main (#482)
The Release workflow's auto-promote ([Unreleased] → [<version>] in
CHANGELOG.md) and auto-sync (package-lock.json on version drift) steps
both `git push origin HEAD:main` using the default GITHUB_TOKEN. That
fails against the "Require PR approval for main branch" ruleset:

    remote: error: GH013: Repository rule violations found for refs/heads/main.
    remote: - Changes must be made through a pull request.

The ruleset's bypass_actors only contains the Admin repo role. The
obvious fix — adding the GitHub Actions integration to bypass_actors —
is rejected by GitHub on user-owned (non-org) repos:

    Validation Failed: Actor GitHub Actions integration must be part
    of the ruleset source or owner organization.

So instead, authenticate the checkout (and therefore all downstream git
operations) as the maintainer via a fine-grained PAT. The PAT owner is
admin → bypasses the ruleset → push lands. Setup is one-time: create a
fine-grained PAT scoped to contents:write on this repo, add it as the
RELEASE_PAT secret. After that, future releases auto-promote cleanly.

Hidden the same way previously: 0.9.5's CHANGELOG was hand-promoted
before triggering Release, so the workflow's promote step short-
circuited on `git diff --quiet -- CHANGELOG.md` and never tried the
push. 0.9.5 also exposed the same bug in the lock-sync step — patched
manually after the fact in #440. 0.9.6 is the first release to actually
hit the bug.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:16:12 -05:00
Colby McHenry 2f93af5d89 Update README.md badge labels to remove "CLI" and "IDE" suffixes 2026-05-26 18:39:37 -05:00
Colby MchenryandGitHub a3763e237f Update project description in README.md 2026-05-26 18:37:29 -05:00
Colby MchenryandGitHub ee80d38d2b Update README.md (#476) 2026-05-26 18:36:58 -05:00
Colby McHenry b9ede1bc66 chore: ignore .antigravitycli/ directory 2026-05-26 18:35:08 -05:00
Colby McHenryandClaude Opus 4.7 6e4949838a Bump version from 0.9.5 to 0.9.6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:33:06 -05:00
110e24fea7 fix(installer): tell Kiro IDE users to enable MCP in Settings (#475)
PR #473 emitted only "Restart Kiro for MCP changes to take effect."
That note is incomplete for Kiro IDE users: the IDE ships with MCP
support disabled by default, so a freshly-written
~/.kiro/settings/mcp.json is ignored until the user opens Settings,
searches "MCP", and flips the "Kiro Agent: Configure MCP" dropdown to
"Enabled". The agent then reports "No MCP powers installed" and falls
back to grep/Read — which looks like an installer wiring bug but isn't.

Kiro CLI doesn't gate on this flag — it reads the same file without
any toggle — so the second note calls out which audience needs the
extra step.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:26:36 -05:00
b9dc4a0fa5 docs: add Kiro to README + site docs (and fill in Gemini/Antigravity gaps) (#474)
PR #473 added the Kiro installer target but missed updating the README
and site documentation. This sweeps both:

- README: hero H3, badges row, installer subtitle, auto-detect list,
  restart line, Supported Agents bullet list, footer tagline.
- site/: integrations.md supported-agents list, installation.md
  auto-detect + restart lines, quickstart.md installer subtitle,
  introduction.md tagline, guides/indexing.md auto-sync paragraph,
  pages/index.astro MCP feature card.

The Supported Agents section in README and integrations.md were also
already stale from PR #399 — they didn't list Gemini CLI or Antigravity
IDE. Fixed those alongside.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:14:18 -05:00
6558b585ed feat(installer): add Kiro CLI/IDE target (#385) (#473)
`codegraph install` now detects and configures Kiro alongside the
existing seven agents. Writes `mcpServers.codegraph` to
`~/.kiro/settings/mcp.json` (global) or `./.kiro/settings/mcp.json`
(local), plus a dedicated `~/.kiro/steering/codegraph.md` /
`./.kiro/steering/codegraph.md` instruction file — Kiro's steering
system loads every `*.md` file in `steering/` as agent context, so a
dedicated file is the natural surface (no marker-based merging needed).

Sibling MCP servers in `mcp.json` and unrelated steering files
(`product.md`, `tech.md`, etc.) are preserved across install and
uninstall. Validated end-to-end on macOS, Linux (Docker node:22-bookworm
arm64), and Windows 11 (Parallels VM, Node 24): full installer-targets
suite passes (132 tests) on all three platforms, and live install /
idempotent re-run / uninstall round-trip works as expected.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:09:51 -05:00
Colby MchenryandGitHub 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.
2026-05-26 17:42:14 -05:00
Colby MchenryandGitHub 186632fa88 fix(extraction): TS type-alias object members are first-class nodes (#359) (#471)
A call site `recorder.stop()` where `recorder: RecorderHandle` and
`type RecorderHandle = { stop: () => Promise<void> }` used to attach
its edge to an unrelated `class Foo { stop() {} }` in a sibling
directory — there was no `RecorderHandle::stop` node, so the existing
camelCase/path-proximity scoring picked the only `stop` method in the
graph (which happened to be wrong). False-positive `calls` edges
silently widened `codegraph_impact` blast radius.

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

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

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

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

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

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

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

Two root causes:

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

Fix:

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

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

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

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

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

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

Closes #388.
2026-05-26 17:14:35 -05:00
2543ae565a feat(java): trace Spring/MyBatis enterprise flow end-to-end (#389) (#468)
Closes three gaps that broke `trace(controller, mapper-xml)` on real Spring +
MyBatis projects:

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

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

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

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

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

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:34:30 -05:00
Colby McHenryandClaude Opus 4.7 55839edd8f chore: gitignore .claude/scheduled_tasks.lock
A Claude Code harness artifact that was showing up as untracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:49:12 -05:00
e1eb13cf9b fix(mcp): normalize root-ish path filters in codegraph_files (#426) (#466)
The agent (opencode/Gemini Flash on Windows) called codegraph_files with
path="/" and got "No files found matching the criteria.", which pushed it
straight back to Read/Glob. Indexed file paths are stored as
project-relative POSIX (e.g. "src/foo.py"), and the old startsWith filter
matched nothing for any of the root-ish or platform-flavored shapes an
agent might guess: "/", ".", "./", "", "\\", leading-slash and
leading-./ subpaths, or Windows backslash subpaths.

Normalize the filter (strip leading "/", "./", "\", bare "."; convert
"\" to "/"; trim trailing "/"), then match by exact equal or "<filter>/"
boundary — which also kills a sibling-prefix bleed where filter "src"
used to match "src-utils/...".

Validated on macOS + Linux (Docker) + Windows (Parallels) with 13 new
unit tests plus the existing mcp-input-limits/concurrent-locking
suites, and end-to-end through opencode in tmux (Big Pickle/OpenCode
Zen): codegraph_files [path=/] now returns the project tree and the
agent answers directly instead of falling back to Read.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:39:48 -05:00
6015e4fdd2 docs(changelog): add Unreleased entry for #455 / #462 FK fix (#464)
The #462 fix (orphaned-edge filter inside QueryBuilder.insertEdges)
landed without a CHANGELOG entry, so add the user-facing description
under [Unreleased] now.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:10:17 -05:00
Colby McHenry 028d25f3af Revert "fix(resolution): filter stale-target edges so watch sync survives FK violations (#455) (#463)"
This reverts commit 1dfaf30a8b.

Switching to #462's approach — a single, lower-layer filter inside
QueryBuilder.insertEdges itself instead of three filters spread across
the resolution layer. The DB-layer filter protects every caller (current
and future) automatically and doesn't depend on the queries-layer
nodeCache invalidation staying perfect. See #455 for the bug.

The CHANGELOG entry for the user-facing fix is re-added on top of #462.
2026-05-26 13:08:56 -05:00
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>
2026-05-26 13:03:32 -05:00
e76cc547b0 fix(hermes): preserve YAML list-at-same-indent style on install (#456) (#461)
Hermes Agent writes ~/.hermes/config.yaml with PyYAML's default block
style, which puts list items at the SAME indent as the parent key:

    platform_toolsets:
      cli:
      - hermes-cli      # indent 2, same as `cli:`
      - browser

The previous line-based YAML patcher used `^  \S` to find the end of
the `cli:` block, which mistook that first `  - hermes-cli` line for
the next sibling key, truncated the block, and spliced
`    - mcp-codegraph` at indent 4 BEFORE the existing items. The
result was unparseable YAML: every subsequent item (`- browser`,
`- clarify`, …) and every sibling platform (`telegram:`, `discord:`)
appeared at the `platform_toolsets:` level. Hermes silently fell back
to the default config, dropping every user override.

The new `listChildBlock` helper recognizes `  - ` as a list-item
continuation (not a sibling key), finds the real end of the block at
the next sibling mapping key, and detects the existing item indent so
the new entry matches it. Two regression tests cover the PyYAML-default
style; the existing 4-space-nested test still passes.

End-to-end verified against a real `hermes-agent` install on the exact
bug-triggering config: `hermes mcp list` shows codegraph as enabled,
`hermes tools --summary` lists both `mcp-codegraph` and `codegraph` in
the CLI toolset, and `hermes mcp test codegraph` connects in 264ms and
discovers all 10 codegraph tools. Re-running `codegraph install`
reports `Unchanged` and the file still has exactly one entry. Closes #456.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:20:58 -05:00
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>
2026-05-26 11:55:18 -05:00
180ba785ce feat(installer): add Gemini CLI + Antigravity IDE targets (#399) (#458)
`codegraph install` now detects and configures two more agents:

- Gemini CLI / Antigravity CLI — `~/.gemini/settings.json` (or
  `./.gemini/settings.json`) + `~/.gemini/GEMINI.md` (or project-root
  `./GEMINI.md`). Preserves pre-existing top-level settings like
  `security.auth` and sibling MCP servers.

- Antigravity IDE — writes to Antigravity's unified MCP config at
  `~/.gemini/config/mcp_config.json` (post-migration, detected via
  the `.migrated` marker Antigravity drops). Falls back to the
  legacy `~/.gemini/antigravity/mcp_config.json` on pre-migration
  builds; install migrates a stale legacy entry, uninstall sweeps
  both. Antigravity-managed sibling fields (e.g. the `disabled` flag
  added when users disable a server through the UI) survive re-install.

  Two Antigravity-specific quirks the target handles:
  1. Entries with `type: "stdio"` are silently rejected by
     Antigravity's MCP scanner; we omit the field for this target.
  2. macOS GUI apps launched from Dock/Finder get a stripped PATH
     that excludes nvm — a bare `codegraph` command name fails to
     spawn even when `which codegraph` works in the user's shell.
     The target resolves `codegraph` to its absolute path at install
     time on macOS. Linux + Windows are unaffected.

End-to-end validated:
- macOS: real Gemini CLI v0.43 via tmux — `/mcp` shows codegraph with
  all 10 tools, `codegraph_status` executes and returns real index
  state. Real Antigravity IDE shows codegraph under Customizations
  after restart.
- Linux (Docker node:22-bookworm) + Windows (Parallels Win11): 116
  installer tests pass; CLI install + uninstall round-trip verified.

Test coverage: the new targets inherit the existing parameterized
contract (idempotent install, sibling preservation, install/uninstall
round-trip). Plus 14 target-specific tests covering migration-marker
detection, legacy→unified entry migration, `disabled` flag
preservation, the `type` field omission, gemini+antigravity
coexistence in the same `~/.gemini/`, and macOS-only path resolution.
Full suite: 972 passing.

Closes #399.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:29:18 -05:00
7479c5e82b docs: explain auto-syncing (no manual sync needed) in site + README (#457)
Originated from issue #438 ("Will newly created files be missing from
query results if sync is not manually run?"). Real users are second-
guessing whether their agent's freshly-created files are getting
indexed. They shouldn't have to test for themselves to find out.

## site/src/content/docs/guides/indexing.md

Expanded the existing 2-sentence "Stay fresh automatically" section
into the full three-layer explanation:

  1. File watcher with debounced auto-sync (default 2000ms, tunable
     via CODEGRAPH_WATCH_DEBOUNCE_MS, clamp [100ms, 60s]).
  2. Per-file staleness banner (#403) — covers the debounce window.
     Quoted the actual banner format + the verified Claude Code
     follow-up Read behaviour.
  3. Connect-time catch-up (#414) — covers gaps when the MCP server
     wasn't running.

Plus: how to verify state via codegraph_status (### Pending sync:),
when manual codegraph sync DOES make sense (watcher disabled / CI
scripting), and a link out to the v0.9.5 release notes.

## README.md

Added a <details><summary> collapsible right under the Key Features
table — primed by the existing 'Always Fresh' row in that table.
Condensed to ~10 lines covering the same three layers + a code-block
flow diagram + the verify command, with a deep link to the full guide.
GitHub renders <details> blocks natively, so the section is collapsed
by default and doesn't make the README scroll-length grow visibly.

Heading kept as 'Stay fresh automatically' (single-word slug) so the
README's deep-link anchor is predictable; the longer tagline lives on
its own line below.

940/942 tests still pass; no code changes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:17:03 -05:00
c972102726 chore: sync package-lock.json to 0.9.5 (#440)
The 0.9.5 release bumped package.json but not package-lock.json — the
EUSAGE-on-npm-ci drift that #439's auto-sync workflow step now prevents
going forward. Fixing main retroactively so contributors and any
non-Release CI path see a consistent state.

Impact: zero on the published 0.9.5 release. The npm tarball ships
only npm-shim.js + package.json + README.md (no lock file), so end-user
installs are unaffected. The GitHub Release archives are platform-
bundled-Node tars from build-bundle.sh — also no lock file. Only fresh
git clones of main running 'npm ci' would have hit EUSAGE; this commit
fixes that.

Two-line diff: package-lock.json's top-level `version` and
`packages[''].version` both 0.9.4 → 0.9.5. No dependency changes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:31:05 -05:00
9eb73ac675 feat(release): auto-sync package-lock.json on version drift + CLAUDE.md: don't bump version unless asked (#439)
Paired with the maintainer-preference clarification that Claude
shouldn't proactively bump versions, and that version bumps are often
made via the GitHub web UI (single-file edit to package.json only).

**Workflow change** (`.github/workflows/release.yml`):

  Adds a 'Sync package-lock.json if version drifted' step BEFORE the
  existing `npm ci` step. It:
    1. Reads the version field from both package.json and package-lock.json.
    2. If they match, no-ops.
    3. Otherwise runs `npm install --package-lock-only --ignore-scripts`
       which rewrites just the lock file's version fields (top-level +
       packages."") without touching node_modules — ~100ms locally.
    4. Auto-commits + pushes the lock-file change back to main with
       `[skip ci]`, same pattern as the prepare-release auto-promote step.

  Effect: a maintainer can now edit ONLY package.json (e.g. via the
  GitHub web UI) and trigger the workflow. The previously-fatal
  `npm ci` mismatch is detected, fixed, and committed before the
  build proceeds. Editing both files locally still works — the sync
  step just no-ops in that case.

  Verified the `npm install --package-lock-only --ignore-scripts`
  mechanic against a synthetic drifted lock file locally: both the
  top-level `version` and `packages."".version` get rewritten to
  match package.json in one command.

**CLAUDE.md change** (§ Release flow):

  Adds an explicit 'Claude does NOT bump the version unless explicitly
  asked' rule. Documents that the maintainer typically bumps
  package.json via the GitHub web UI (single-file edit). Explains the
  new sync step and lists the workflow's 5-step internals (sync lock →
  promote CHANGELOG → bundles → release → npm) for future Claude
  sessions to understand.

940/942 existing tests still pass; no new tests needed (the sync step
is a thin wrapper around an npm CLI invocation; the verification was
the local synthetic-drift smoke test in the commit-message above).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:28:54 -05:00
f6fabe9b5a docs(claude): rewrite release section for auto-promote workflow + link-ref on promote (#437)
Two paired updates:

1. **`CLAUDE.md` § Releases** — rewritten to match the actual workflow now
   that #436's auto-promote step lands the entries automatically.

   The old text told Claude to 'Add a new `## [X.Y.Z] - YYYY-MM-DD`
   block at the top of CHANGELOG.md' as the first step. That instruction
   is the exact pattern that caused the v0.9.5 sparse-release-notes
   incident — a hand-added sparse `[X.Y.Z]` block (one early fix
   pre-staged) is what the extractor picked, ignoring everything under
   `[Unreleased]` above it.

   New default: write entries under `## [Unreleased]` during normal
   work. The Release workflow promotes them at release time. The
   formatting rules (sub-section grouping, user-perspective wording,
   issue/PR refs) are preserved. The link-reference rule moves to 'don't
   add it yourself' since `prepare-release.mjs` now appends it.

2. **`scripts/prepare-release.mjs`** — extended to also append a
   `[X.Y.Z]: https://github.com/colbymchenry/codegraph/releases/tag/vX.Y.Z`
   link reference at the end of CHANGELOG.md when promoting (idempotent
   — no-op if one already exists, regardless of where in the file it
   sits). This is what makes the `## [X.Y.Z]` heading text auto-link
   to its release tag in GitHub's renderer; without it the heading still
   renders, just unlinked. 3 new tests cover Case A append, Case B
   append-when-merging, and no-double-add.

940/942 existing tests still pass (2 pre-existing skips); +3 new tests.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:25:06 -05:00
b77af782c5 feat(release): auto-promote [Unreleased] into [<version>] on release workflow run (#436)
Fixes the silent-sparse-release-notes failure mode that surfaced on
v0.9.5: the Release workflow used to do a literal
`extract-release-notes.mjs <version>` lookup with an `[Unreleased]`
fallback. The fallback only triggered when the `[<version>]` block
DIDN'T exist at all — and in practice maintainers sometimes had a
sparse `[<version>]` block pre-populated (e.g. one early fix
documented before the rest of the work landed). The workflow then
extracted that sparse block, ignoring the much-larger `[Unreleased]`
section above it. Result: the published v0.9.5 release notes were
missing the shared MCP daemon, the per-file staleness banner, the
Objective-C indexing, AND the Mixed iOS/RN/Expo bridging.

The fix is a new `scripts/prepare-release.mjs` step that runs at the
start of the workflow:

  Case A — `[<version>]` does not yet exist:
    Rename `[Unreleased]` → `[<version>] - <today>`. Add a fresh
    empty `[Unreleased]` above. The common path.

  Case B — `[<version>]` exists AND `[Unreleased]` has content:
    Merge `[Unreleased]`'s sub-sections (### Added / ### Fixed /
    ### Changed / ### Removed / ### Deprecated / ### Security) into
    the corresponding sub-sections of `[<version>]`. Unmatched
    sub-sections are appended. Then empty `[Unreleased]`.

  Case C — `[Unreleased]` is empty:
    No-op. Re-runs of the workflow are safe.

After the script runs, the workflow auto-commits + pushes the
CHANGELOG.md change back to main (with a `[skip ci]` tag in the
commit body) so future runs and human eyes both see the same
on-disk truth.

9 unit tests (`__tests__/prepare-release.test.ts`) cover all three
cases, idempotency, version-source precedence, and an
extract-release-notes.mjs integration check.

Workflow comment header rewritten to reflect the new flow.

Trigger reminder going forward: bump package.json. CHANGELOG entries
can live under `[Unreleased]` — the workflow takes care of moving
them.

937/939 existing tests pass (2 pre-existing skips); +9 new tests.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:21:06 -05:00
5a4fcd56b7 docs(changelog): move 0.9.5 features from [Unreleased] into [0.9.5] (#435)
The 0.9.5 release tag included all of:
- Shared MCP daemon (#411)
- Per-file staleness banner (#403)
- Worktree-borrow detection (#312)
- Watcher inotify-budget fix (#276)
- Objective-C indexing (#165)
- Mixed iOS / React Native / Expo cross-language bridging (#430)

But the [0.9.5] block in CHANGELOG.md only had two Fixed entries (the
fs-based change detection and default-ignore set), because the major
feature entries were still sitting under [Unreleased] when 0.9.5 was
tagged. release.yml extracts release notes from the matching version
block, so the published v0.9.5 release notes are missing the bulk of
what shipped.

Move all the [Unreleased] entries that pre-date 0.9.5's tag (commit
318cda1) into [0.9.5], and reset [Unreleased] to empty. The GitHub
Release notes for v0.9.5 get updated separately via gh release edit.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:15:28 -05:00
Colby MchenryandGitHub 318cda18d1 Bump version from 0.9.4 to 0.9.5 2026-05-26 02:57:41 -05:00
Colby MchenryandGitHub 22bc542d34 test: eliminate chokidar/FSEvents race in watcher + staleness-banner tests (#434)
Mocks chokidar at the module level for `__tests__/watcher.test.ts` and
`__tests__/mcp-staleness-banner.test.ts` so the pending-file-tracking and
staleness-banner tests no longer depend on OS-level file-watcher delivery
latency. Reduces full-suite failure rate from 3/10 to 0/10.

- `__tests__/__helpers__/chokidar-mock.ts` (new) — controllable
  EventEmitter; `chokidarMockModule` for `vi.mock('chokidar', ...)` plus
  `triggerFileEvent(root, event, relPath)` helper. `watch()` returns an
  EventEmitter that fires `ready` on the next microtask.
- `__tests__/watcher.test.ts` — refactors every event-driving test to
  use `triggerFileEvent` instead of `fs.writeFileSync` for the trigger.
  Pending-file tests assert state synchronously. Filtering tests still
  verify FileWatcher's own filter chain.
- `__tests__/mcp-staleness-banner.test.ts` — same vi.mock + same
  `triggerFileEvent` pattern; tests keep `fs.writeFileSync` for on-disk
  content (`cg.sync()` needs the bytes) and add the synthesized event
  on top.

The watcher's debounce timer (real `setTimeout`) is left untouched — that's
the unit under test.

Total test count unchanged (928 passing + 2 pre-existing skips). Wall-clock
runtime improved (no more 8000ms waitFor polls against real chokidar).

One disclosed tradeoff: the previous node_modules filtering test
incidentally exercised chokidar's `ignored` callback at the OS level;
with chokidar mocked, that property of chokidar itself isn't covered
here. Commented inline.
2026-05-26 02:38:09 -05:00
Colby MchenryandGitHub 4d1a2b3c4d feat(resolution): mixed iOS / React Native / Expo cross-language bridging (#430)
Implements the design from `docs/design/mixed-ios-and-react-native-bridging.md`.
Closes the cross-language flow gap so `trace` / `callers` / `callees` / `impact` connect end-to-end across language boundaries in real iOS, React Native, and Expo codebases.

## Bridges shipped

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

## Bug fixes surfaced along the way

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

## Regression check on 5 control repos

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

## Tests

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

## Documentation

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-26 02:14:00 -05:00
1821038e4b docs(changelog): add Objective-C indexing entry under [Unreleased] (#429)
Covers #165: tree-sitter-objc extractor for .m / .mm / content-sniffed
.h, with full multi-part selectors, @protocol nodes, @property, message
expression call edges, extends/implements edges. Validated on
AFNetworking / RestKit / Texture. Disclosed limitations match the
README's 'Partial support' note (categories produce duplicate class
nodes per category file; .mm ObjC++ parses incompletely under the ObjC
grammar; mixed Swift/ObjC bridging out of scope, tracked separately).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:35:35 -05:00
b48170e69f feat(mcp): per-file staleness banner + tunable watcher debounce (#403) (#428)
Two coupled changes addressing the issue's underlying ask — "how does the
agent know when the index lags" — without resorting to a static wait.

Per-file staleness banner
-------------------------
FileWatcher now tracks per-path `pendingFiles` (path, firstSeenMs,
lastSeenMs, indexing) — events since the last successful sync, cleared
only after a sync whose `syncStartedMs >= lastSeenMs` commits. Chokidar
initial-scan events are gated behind a `ready` flag (with `waitUntilReady()`
exposed so tests can deterministically wait through it) so a fresh startup
doesn't falsely flag every existing file as pending.

ToolHandler now wraps every code-returning response (search, context,
callers, callees, impact, trace, explore, node, files) with
`withStalenessNotice`: intersects "files referenced in the response" with
`getPendingFiles()` and emits a hybrid signal —

  * banner at the top for files referenced AND pending (with edit age +
    indexing/pending-sync state, telling the agent to Read those specific
    files directly; the rest of the response stays fresh and codegraph
    stays authoritative for it),
  * compact footer for pending files elsewhere in the project not
    referenced above (capped at 5).

Cost is one boolean check + N substring matches when pending; zero
allocation when idle. `codegraph_status` surfaces the same data as a
first-class `### Pending sync:` section so the agent can ask "is the index
caught up?" in one call.

Cross-project quirk: when an agent passes `projectPath` matching the
default session's project, the staleness wrapper switches from the cached
cross-project CodeGraph (no watcher) to the default one (with watcher) so
the signal still fires. Same fix applied to `handleStatus`.

CODEGRAPH_WATCH_DEBOUNCE_MS
---------------------------
MCP `serve --mcp` now reads `CODEGRAPH_WATCH_DEBOUNCE_MS` and forwards it
to `cg.watch({ debounceMs })`. Clamped to [100ms, 60s]; out-of-range or
non-numeric values fall back to the FileWatcher default (2000ms). Active
value is logged to stderr on watcher startup so it's discoverable. The
docs in `server-instructions.ts`, `installer/instructions-template.ts`,
and `.cursor/rules/codegraph.mdc` no longer claim "~500ms"; they now
describe the banner mechanism instead — since per-file staleness replaces
the "wait N ms" guidance entirely, the docs become accurate at any
debounce value.

Validation
----------
* 847 unit/integration tests pass (added 15 new ones — pending-file
  tracking, banner/footer routing, status section, env-var parsing).
* Direct MCP probe through a real `codegraph serve --mcp` process: edit a
  file, query within the debounce window, banner fires naming the
  edited file with edit-age.
* Real Claude TUI session via `scripts/agent-eval/itrun.sh` with
  `CODEGRAPH_WATCH_DEBOUNCE_MS=10000`: agent edits `math.ts`, calls
  `codegraph_explore`, reads the banner, **and discloses it unprompted in
  its final reply**: "note: symbol index is mid-sync for the new `divide`,
  but the source it returned is verbatim from disk."

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:48:10 -05:00
Colby McHenryandClaude Opus 4.7 8edd6cfafd docs(claude): document cross-platform validation (macOS host, Linux via Docker, Windows VM)
Rename the Windows section to "Cross-platform validation": macOS is the dev
machine / default npm test target; on Linux use Docker (node:22-bookworm,
`docker run --rm --init` so process-lifecycle tests don't false-fail on
un-reaped zombies; count inotify via /proc/<pid>/fdinfo); Windows VM kept.
Also record the pre-existing mcp-initialize/mcp-roots EPERM failures on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:36:54 -05:00
2721165604 docs: clarify zero-config + built-in default-ignores; drop stale config wording (#418)
Refresh the README and docs-site Configuration pages to (a) state plainly there are no codegraph config files, (b) describe the new built-in default-ignores (#407) and the .gitignore-negation opt-in, and (c) remove the now-inaccurate note that committed vendor/dist dirs are indexed. Also fixes a stray 'excluded by config patterns' phrase in README troubleshooting.

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:36:09 -05:00
1be8e7830f fix(site): large left-aligned feature icons to match the mockup (#379)
Lay out each landing feature as icon | divider | text — a 64px icon on the
left, a full-height vertical rule, then heading + description — instead of a
small icon stacked on top. Pure CSS grid (divider via ::before), with
smaller icon/gaps on mobile so the text stays readable.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:47:03 -05:00
18a99f5f0f fix(site): center docs content on wide screens (#378)
Starlight right-aligns the content against the TOC (margin-inline: auto 0)
on pages with both a sidebar and a TOC, dumping all the empty space on the
left edge — very lopsided on wide monitors. Center the content in its pane.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:38:33 -05:00
6a2098b199 feat(site): add Docs & Languages links to the docs header (#377)
Bring the Starlight docs header to parity with the landing nav — now
Docs · Languages · GitHub · star pill, in that order. Added via the
SocialIcons slot (no full header rebuild, so search/theme/mobile keep
working); the text links and star pill are hidden on mobile, where the
sidebar already covers navigation.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:27:45 -05:00
7f30c4f5c8 docs(readme): link to the website & docs site (#376)
Add a prominent link to https://colbymchenry.github.io/codegraph/ at the
top of the README so visitors landing on the repo can reach the site.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:25:24 -05:00
4509b45dd5 Add landing page + Starlight docs site (#375)
* udpated matrix

* feat(site): add landing page + Starlight docs site

Astro + Starlight site in site/ — a flat/paper editorial landing page
plus 18 docs pages seeded from the README. Monochrome theme, hairline
rules, square corners, live GitHub star count, light default + dark
toggle. Deploys to GitHub Pages via .github/workflows/deploy-site.yml.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 13:21:25 -05:00