Commit Graph
477 Commits
Author SHA1 Message Date
Artem BambalovandGitHub 34240eb297 feat(jvm): resolve Java/Kotlin imports by fully-qualified name (#412)
Wrap top-level declarations of `.kt` / `.java` files in an implicit `namespace` node carrying the file's `package`, then resolve `import com.example.foo.Bar` through that qualifiedName index — so a Bar in Models.kt resolves correctly regardless of filename, a top-level function import binds to its declaration, Java↔Kotlin interop crosses cleanly, and same-name classes across packages no longer collide. Wildcard imports still go through name-matcher.

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

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

Agent A/B playbook on small (spring-petclinic-kotlin, 38 .kt), medium (mall, 524 .java), large (guava, 3,227 .java) — 3 flow prompts × 2 runs/arm × 2 arms = 36 runs, claude-opus, headless. Spring repos: 0/0 Read/Grep with-arm, −27% wall-clock vs no-codegraph. Guava: 1.8 Read avg with-arm (vs 2.0 without) — improved by the anon-class extraction; residual is a lambda→SAM coverage gap orthogonal to FQN imports (filing follow-up).
2026-05-26 22:06:53 -05:00
Artem BambalovandGitHub 3808b4d0a8 fix(cli): include resolution + synthesizer edges in indexAll report (#413)
The orchestrator's per-file counter only sees extraction-phase edges, so the `X nodes, Y edges` line printed after `codegraph init -i` / `codegraph index` undercounts the graph — often by more than half on repos with heavy cross-file resolution (mall: 20 047 reported vs 45 629 actually in the DB).

Snapshot (nodes, edges) before/after the full pipeline in `indexAll` and write the true delta back to the result. New lightweight `QueryBuilder.getNodeAndEdgeCount()` is one round-trip with no per-kind breakdowns. `indexFiles` (no resolution) and `sync` (uses `nodesUpdated`, not `nodesCreated`) are unaffected.

Regression test added: `__tests__/integration/full-pipeline.test.ts > reports edgesCreated including resolution + synthesizer phases`.
2026-05-26 21:08:59 -05:00
github-actions[bot] 625e5663c4 docs(changelog): promote [Unreleased] into [0.9.6]
[skip ci] Auto-generated by Release workflow.
2026-05-27 01:16:38 +00: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
48eebe1e3e feat(resolution): add C/C++ include path resolution (#453)
* feat(resolution): add C/C++ include path resolution

Add full import resolution pipeline for C and C++ #include directives,
connecting extracted import nodes to actual header files in the project.

- Add C/C++ extension resolution (.h, .hpp, .hxx, .cpp, .cc, .cxx)
- Add system header filtering with ~80 C and ~80 C++ stdlib headers
- Add extractCppImports() for #include import mapping extraction
- Add compile_commands.json parsing for -I/-isystem include directories
- Add heuristic include dir discovery (include/, src/, lib/, api/)
- Add resolveCppIncludePath() for include directory search
- Add C/C++ built-in symbol filtering (printf, malloc, std::*, etc.)
- Wire getCppIncludeDirs into ResolutionContext
- Add 13 new tests for C/C++ import resolution and extraction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* review: wire #include resolution into pipeline + fix builtin filter

The PR landed the include-dir scan logic (loadCppIncludeDirs +
resolveCppIncludePath) but the indexer never reached it: imports
references with referenceName='X.h' fell into resolveViaImport's
symbol-lookup branch (matched extractCppImports' basename-without-ext
localName via .startsWith, then tried to find a symbol named like the
extension and failed). End result on bitcoin-core: 0 new file→file
imports vs main, despite the include-dir scan resolving paths correctly
when probed directly. resolveViaImport now has a C/C++ imports branch
that resolves the include path to the actual file node and returns
that — skipping the irrelevant symbol scan. Measured on bitcoin-core:
+2,059 newly resolved file→file imports (6,027 → 8,086, +34%).

The unconditional CPP_BUILT_INS / C_BUILT_INS filter also misfired:
C/C++ codebases routinely shadow stdlib names (bitcoin's mp::move,
custom allocators with free/malloc, stream classes with read/write/
close/open, logging libs wrapping printf). Filtering those names
killed legitimate edges — 1,179 → 0 for move(), 33 → 0 for free(),
149 → 7 for write() on bitcoin. The filter now defers to
hasAnyPossibleMatch: only filter when no user-defined symbol with the
name exists. std:: prefix stays unconditional (never user-shadowed in
practice). After: printf/free/open/close/read/write/swap all preserved
at main's counts; the std::move-binds-to-mp::move false-positives still
drop (correctly: −2,154 C/C++ calls).

Also: drop the duplicate 'FILE' in C_BUILT_INS; add an end-to-end test
that asserts `#include "X.h"` produces a file→file imports edge in the
real indexing pipeline (not just direct resolver probes); add a test
documenting the cross-language `.h` heuristic claim (Obj-C dirs are
intentionally allowed as C/C++ include dirs); add CHANGELOG entry
under [Unreleased] with measured numbers.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
2026-05-26 19:56:25 -05:00
NandhisandGitHub 893256b88e fix(extraction): capture top-level initializer and inline-object-method calls (#465)
The variable / method-definition extractors never walked top-level
initializer values or inline-object method bodies, so calls like
`const token = getTokenMp()` and `methods: { save() { getTokenMp() } }`
showed up nowhere in `codegraph_callers`. The variable extractor now
walks any non-object initializer value; the method-definition extractor
still skips synthetic nodes for inline-object methods (noise rationale
unchanged) but now walks their bodies for calls. Surfaces in plain
`.ts`/`.js` files as well as Vue SFCs (`<script setup>` initializers +
Options API `methods: {...}` / `setup()`), which is where the bug was
originally reported.

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

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

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

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

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

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

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

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

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

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

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

Closes #445

Co-authored-by: chenyuxuan <458254969@qq.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:17:07 -05:00
72c08c2bef fix(watcher): retain pending files on zero-result sync (#450)
* fix(watcher): retain pending files on zero-result sync

* refactor(watcher): detect lock-unavailable at the wrapper

Replace the heuristic `(filesChanged === 0 && durationMs === 0)` check
inside `FileWatcher.flush()` with a typed `LockUnavailableError` thrown
by `CodeGraph.watch()`'s sync wrapper. The wrapper has access to the
full `SyncResult`, including `filesChecked` — which is **only** zero
when `sync()` failed to acquire the cross-process file lock (a real
empty sync always has `filesChecked > 0` because `scanDirectory` ran).
That eliminates the heuristic's edge case where a fast no-op sync
returns `durationMs === 0` by `Date.now()` rounding and gets mistaken
for a lock failure on tiny projects.

The watcher's `catch` block now distinguishes `LockUnavailableError`
from real errors: it logs at `logDebug` (not `logWarn`) and does NOT
call `onSyncError` — so a long-running external indexer holding the
lock doesn't spam stderr every debounce cycle via the MCP daemon's
`Auto-sync error` handler. The existing post-catch path already
preserves `pendingFiles` and reschedules, so no new control flow is
needed.

A/B validated end-to-end against the built dist on macOS with a
three-scenario repro (lock held, lock released mid-flight, real sync
error):

- main:           lock-held silently clears pendingFiles (BUG);
                  lock-released never recovers (no real sync runs).
- PR-as-is:       lock-held preserves pendingFiles; lock-released
                  drains. Same observable behavior as wrapper-level.
- wrapper-level:  same outcomes; lock-failure goes through the catch
                  path silently (logDebug only, no onSyncError noise);
                  real errors still surface via onSyncError.

Updates the regression test to throw `LockUnavailableError` (the real
contract surfaced to `FileWatcher` by `CodeGraph.watch()`), and
asserts `onSyncError` stays quiet during the lock-held cycle.

Closes #449.

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

---------

Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:47:04 -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
NandhisandGitHub 572b1ede18 fix(db): skip orphaned edges during batch insert (#462) 2026-05-26 13:09:11 -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
0x1306a94andGitHub 61153f96ee feat(extraction): add Objective-C language support (#165)
Adds tree-sitter-objc extractor for `.m`/`.mm` files and `.h` files
that content-sniff as Objective-C (`@interface`/`@implementation`/`@protocol`/`@synthesize`).

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

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

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

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

Known limitations (disclosed in README):
- Categories produce duplicate class nodes (one per category file)
- Chained/nested message sends record only the innermost method
- `[Class alloc]` patterns don't emit `instantiates` edges
- `@protocol Foo <Bar>` refinement lists not yet wired to `implements`
- Heavy C++ in `.mm` files may parse incompletely under the ObjC grammar
2026-05-26 00:31:43 -05:00
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
4a4a37d135 feat(mcp): detect borrowed git worktree index and surface on read tools (#312)
When a worktree is nested inside the main checkout (e.g. agent tools that place
worktrees under .claude/worktrees/<name>/), the nearest-.codegraph walk resolves
UP to the main checkout's index and queries silently return that tree's code —
usually a different branch. Symbols changed only in the worktree are invisible,
and nothing tells the user (#155).

Two layers:

- **Detection** (src/sync/worktree.ts): detectWorktreeIndexMismatch() compares
  the caller's git working-tree root vs the resolved index root via
  'git rev-parse --show-toplevel'. Best-effort; no git / not a repo / monorepo
  subdir / plain-ancestor index → no warning.
- **Surface**: codegraph status (CLI + MCP) embeds a verbose multi-line warning;
  every MCP read tool (search/context/trace/callers/callees/impact/explore/node/
  files) prefixes a compact one-line notice naming the borrowed index and the
  fix (codegraph init -i in the worktree). Detection is cached per session per
  start path, so it costs at most a single pair of 'git rev-parse' spawns per
  project no matter how many tool calls — respects the wall-clock-latency
  invariant.

Real-git tests (no mocking) cover both layers. Validated on macOS / Linux
(Docker) / Windows (Parallels VM); 11/11 worktree tests green on all three.

Closes #155

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:57:20 -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
b09b23cf54 fix(watcher): exclude ignored dirs before watching to prevent inotify exhaustion (#276)
The file watcher registered a recursive watch over the entire project (node_modules, build output, caches included) and filtered only in the callback — exhausting the Linux inotify budget on large repos (#276). It now uses chokidar and excludes the same directories the indexer ignores (built-in default-ignore set + the project .gitignore) BEFORE registering a watch, so the watch count on a 900-dir node_modules drops from ~1200 to ~14 even with no .gitignore. Stacks with the shared daemon (#411): one watcher across agents, now small.

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

Co-Authored-By: Colby McHenry <me@colbymchenry.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:12:44 -05:00
995da54430 feat(mcp): share one serve --mcp per project across MCP clients (#411)
One shared, detached daemon per project root: every `codegraph serve --mcp` is a thin stdio<->socket proxy (Unix socket / Windows named pipe) to it, so N agents in one repo share a single file watcher, SQLite connection, and tree-sitter warm-up instead of N copies. The daemon outlives any single session and reaps via client-refcount + idle timeout; `CODEGRAPH_NO_DAEMON=1` opts out.

Hardened during review: detached-process lifecycle (preserves the #277 watchdog via the proxy; the daemon no longer orphans on host SIGKILL), atomic lockfile + pid-verified stale-clear (no double-daemon on concurrent startup), realpath root canonicalization. Validated on macOS, Linux (Docker - 3x fewer inotify watches for 3 agents), and Windows (named pipes); A/B confirms byte-identical tool output vs direct mode. Closes #411.

Co-Authored-By: Colby McHenry <me@colbymchenry.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:54:56 -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