Commit Graph
196 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 455175da67 fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110) (#2287)
* fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110)

An explicit projectPath resolves to the nearest .codegraph/ walking up,
and that walk crosses git boundaries. Inside an independent repository
nested under an indexed parent that excludes it (typically gitignored),
every call silently answered from the parent's code.

After resolving the index root, ToolHandler now asks whether the path
sits in a different git repository strictly below that root (git
toplevel + common dir, memoized per path and root) and, if so, whether
the index holds any file under it (one primary-key range probe per
call). None means the success-shaped "isn't indexed" guidance. Nested
repos the parent does index (submodules, gitlinks, embedded clones),
ordinary subdirectories, linked worktrees of the same repository (the
when git can't run, behavior is unchanged.

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

* docs(changelog): nested repositories the index leaves out (#2110)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:24:34 +00:00
fdc7754a72 fix(mcp): return status and stale refusals as text only (#2088) (#2284)
* fix(mcp): return status and stale refusals as text only (#2088)

Claude Code shows the model a tool result's structuredContent in place
of its content text, under every protocol version. codegraph_status and
explore's stale-index refusal both set structuredContent.freshness, so
the model saw a JSON object instead of the refusal, its retry guidance
or the status report. The text already carries every field, and nothing
in the codebase reads structuredContent, so both results drop it.

The tests now assert that the field is absent and that the text keeps
the refusal line and the stale file list.

(cherry picked from commit 18d65f8e10)

* fix(mcp): list every unverified file in the stale refusal (#2088)

(cherry picked from commit f1b0ec7f56)

* fix(mcp): keep the stale refusal's unchecked-file list bounded

The text-only refusal now names the first 20 files it could not verify and
counts the rest, as it did before; an impact answer over a hub symbol can
leave thousands unchecked, and every one of them would land in the agent's
context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c4513a66c9ae6bb10ede80d102d4122ed7f0508d)

* docs(changelog): one entry for the text-only results fix

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

---------

Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:16:48 +00:00
637fb0fac8 fix(mcp): release an idle explicit project and its writer lock (#2087) (#2283)
* fix(mcp): release an idle explicit project and its writer lock (#2087)

A daemon that answered a `projectPath` query for another indexed project
opened it in-process and, with no owner there, took its writer lock. The
project cache only evicted over its LRU bound or on shutdown, so the lock
stayed held for the daemon's whole life: the project's own daemon could not
become its writer and `codegraph index` there was refused.

Release a cached explicit project once it has gone unused for 10 minutes
(CODEGRAPH_PROJECT_IDLE_TIMEOUT_MS; 0 never releases), through the same
trim path as LRU eviction, so an active call or a running catch-up still
defers it. The next call reopens the project and catches it up (#1835).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 092d49d3db)

* docs(changelog): credit the idle project release

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

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:15:24 +00:00
49248e25eb fix(mcp): answer server/discover locally in the stdio proxy (#2084) (#2282)
* fix(mcp): answer server/discover locally in the stdio proxy (#2084)

Clients that probe `server/discover` before the legacy `initialize`
(Antigravity 2.5) wait for its answer. The proxy forwarded it to the shared
daemon, so no reply was written until the daemon connected: a client that
closed stdin first got nothing, and with the daemon unavailable the reply
came ~6s later as -32603 "daemon unavailable". Answer it in the proxy with
-32601 Method not found, which is what sends such a client on to
`initialize`. The direct and daemon sessions already answer unknown methods
with -32601; the new test pins that too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 77db6c659c)

* docs(changelog): credit the server/discover fix

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

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:13:42 +00:00
04ec5f554d fix(extraction): never read an oversize file; an MPEG-TS video named .ts is not TypeScript (#1910) (#2082)
* fix(extraction): an MPEG-TS video named .ts is not TypeScript (#1910)

`.ts` mapped to TypeScript by extension alone, so MPEG transport-stream
fixtures (`testdata/*.ts`) were parsed by tree-sitter: ~28 s of CPU for a
900 KB clip, minutes for a directory of them, for zero symbols.

Recognise the stream from the head of the file — the 0x47 sync byte at
offsets 0, 188, 376 and 564 plus a NUL byte, which every stream carries in
its first packets and UTF-8 source never does — and drop it at discovery:
not indexed, not parsed, not counted, not tallied as an unsupported
language. The check reads under 1 KB and only for `.ts` files; the batch
reader sniffs the bytes it already read, and the single-file path (sync,
watcher) does the same head read, so a clip handed in by name is skipped
too. The skip happens before parse dispatch, so no kernel mirror is needed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(extraction): a video .ts is never pending on the git path or a scoped sync

Review found two places that decide "is this a source file" without the
MPEG-TS check the scan applies: the git fast path's candidate loop in
getChangedFiles and the scoped-sync path filter (watcher events). An
untracked clip in a git repo was reported as `added`, skipped by sync
without a record, and reported as `added` again on every status; a tracked
.ts that became a clip stayed `modified` with its stale nodes. Both now
treat the clip as non-source: removed when tracked, ignored otherwise.

Regression tests fail without this change and pass with it, on both arms.

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

* fix(extraction): require a binary head before calling a .ts file video (#1910)

Review on #1915: the sniff took four aligned 0x47 sync bytes plus any NUL
as proof of a transport stream, so TypeScript with a `G` at four 188-byte
strides and one raw NUL in a comment was dropped unindexed. The sniff now
needs the sync byte on sixteen consecutive packets (3 KB of head) and at
least 1/64 of the head to be control bytes. Compressed payload carries
8-20% of them (checked on ffmpeg-made H.264/AAC, MPEG-2 and MP2 streams);
source text carries none. A shorter clip is cheap to parse anyway.

Also drops the benchmark figures from the changelog entry.

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

* fix(extraction): a file over the size limit is never read (#1910)

The size gate ran after `readFile`, so a committed video or blob fixture was
decoded in full — 3.4 GB of RSS for a 400 MB file, `Invalid string length`
past ~512 MB — only to be stored as skipped. It was then read again by every
pass that scans files by name: the framework detectors' `readFile` and the
resolver's cached reader, three full passes on the reporter's fixtures.

Stat first, everywhere a source file is read by path: the batch reader and
`extractFile` store an oversize file with a size stamp in place of its
content, change detection hashes the same stamp (so a same-size rewrite of a
file nothing is indexed from is not a change, while crossing the limit in
either direction is), and both by-name readers return null above the limit.
The limit moves to `src/file-limits.ts` so the three sites share one number.

strace on a sparse 400 MB `.ts`: 153 603 reads before, 0 after; indexing it
next to a real file costs no measurable RSS. Stacked on #1914, which shares
the batch-reader hunk.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(extraction): one-purpose size-stamp reader, comments in operation order

`readSourceOrStamp` took a `stats` argument no caller passed and returned
stats no caller read; it now returns the text or stamp change detection
hashes. The batch reader's two comments are back in the order its code runs:
the size gate, then the MPEG-TS head check. No behaviour change.

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

* fix(extraction): bound the read itself, not only the stat before it (#1910)

A file can grow between the stat that gates the size limit and the read
that follows (a log, a download, a build output being written). Every
source read now goes through readBoundedSource/readBoundedSourceSync: the
open descriptor is re-checked, and the read stops one byte past the limit,
so an oversize file is still stamped rather than decoded. The extraction
batch reader, single-file extraction, framework detectors and the resolver's
file cache all use it. Ported from the maintainer's hardening of this fix
in #1919.

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

* fix(extraction): bound the parse-retry reads too (#1910)

The retry passes for files whose worker crashed or timed out re-read the
source with an unbounded fsp.readFile. The file can have grown since the
first, bounded read; an oversize file is now skipped by the retry instead
of being decoded.

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

* fix(extraction): check an oversize file against its size stamp everywhere (#1910)

Review on #1915:

- The viewer (readFileShape, hasDriftedOnDisk, the source endpoint) and
  MCP's drift gate hashed a file's real bytes, but the index stores the
  size stamp for a file over the limit, so every unchanged 1-8 MB file read
  as "changed on disk". oversizeStamp moves to file-limits.ts with an
  indexedHashInput helper, and all four checks hash what the index stored.
  An oversize file is now compared without being read.
- git-index-currency's "keeps a committed path pending when sync cannot
  read it" injected its failure only into readFileSync, which the bounded
  reader no longer calls, so it failed. The failure now reaches openSync
  too.
- The changelog entry drops its benchmark figures.

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

* fix(mcp): check an oversize answer file against its size stamp (#1910)

#2043's answer-freshness check hashes each contributing file whole and
compares it with the stored contentHash. For a file over the index's size
limit the stored hash is the size stamp, so an unchanged oversize file
would read as stale. Compare it by its stamp, without reading it, like
the viewer and the MCP drift gate do.

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

* perf(extraction): judge a .ts clip from the bytes it is read for, not at discovery (#1910)

Discovery sniffed the head of every `.ts` file on every full scan — an
open + 3 KB read + close per file, before anything asked for its content.
On a 10k-file TypeScript tree that took a warm-cache scan from ~105 ms to
~243 ms; on a slow disk or a network share it is a random read per file
per scan, the access pattern #1231 and the SMB reports (#1014, #448) are
about.

The bytes are now judged where they are read anyway, so the check costs no
I/O and runs only for a file that is new or changed: the batch reader (as
before), `indexFile`, the sync reconcile (an untracked clip is skipped, a
tracked file that became one is removed through the same removal path as
a deletion), and both `getChangedFiles` paths. The scan lists `.ts` files
by name again; a test pins that discovery opens none of them on the walk
or the git path. Scan time is back to main's (~98 ms), and a full index is
unchanged within noise.

Also collapses the four #1910 changelog bullets (#1914 and #1915 each added
both) to two, without the benchmark figures.

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

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-29 03:01:46 +00:00
e5a3d85b0c fix(watcher): follow a rebuilt index instead of syncing into the replaced file (#1902) (#1917)
* fix(watcher): follow a rebuilt index instead of syncing into the replaced file (#1902)

`codegraph index` rebuilds through `CodeGraph.recreate()`, which unlinks
`.codegraph/codegraph.db` and creates a new file at the same path. A running
MCP daemon kept its handle on the unlinked inode and its watcher kept
"auto-syncing" into it, so no edit reached the on-disk index. The #925
self-heal only ran on the tool-call path and ran no catch-up after reopening.

- `sync()` now checks, once it holds the index mutex and the write lock,
  whether the database was replaced on disk (one stat, the same inode check
  `reopenIfReplaced` uses). If so it reopens the live file in place and widens
  a scoped sync to a full reconcile. A failed reopen (rebuild mid-way) returns
  the lock-busy shape so the watcher keeps its pending files and retries.
- `reopenIfReplaced()` refuses while an index/sync holds the mutex, so the
  tool-call path never closes the handle an in-flight sync writes through.
- When a tool call's `freshen()` does reopen, the engine runs its existing
  catch-up sync (serialized on the same mutex), but only for its own watched
  instance, so an engine that is not the project's writer never writes.

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

* fix(sync): step aside for a rebuild that has recreated the file but not locked it yet

`codegraph index` recreates the database, then takes the write lock inside
indexAll. With the watcher now following a replaced file, a sync landing in
that gap reopened the empty file and started a full reconcile, holding the
lock the rebuild was about to request — `codegraph index` could then fail
with "another process may be indexing". Review flagged the gap; this closes
it. After following a replaced file, a sync that finds no index_state yet
(the rebuild has not started indexing) and a file written within the last
two minutes returns the lock-busy shape so the watcher retries; the first
sync after that reconciles the whole tree once, then syncs go back to scoped.
The time bound keeps a rebuild that died before indexing from parking the
watcher forever.

New test: fails without the guard, passes with it; 136 related tests pass.

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

* fix(sync): keep the full catch-up until a full sync completes (#1902)

Two gaps in following a replaced database:
- Only a reopen done by sync() itself set pendingFullReconcile. The tool-call
  self-heal (reopenIfReplaced) swapped the handle without it, so the next
  scoped watcher sync skipped whatever the old handle had absorbed. The flag
  is now set wherever the database is reopened.
- The flag was cleared before the sync ran, so a sync that threw lost the
  catch-up. It is now cleared only after a full sync completes.
Ported from the maintainer's hardening of this fix in #1919.

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

* fix(sync): report a rebuild in progress as lock contention, not an empty sync (#1902)

Since #2014 the watcher tells lock contention apart from success only by
LockUnavailableError; it no longer recognises the all-zero result. Following
a rebuilt index still returned that shape when the reopen failed or when the
recreated file had not been indexed yet, so the watcher counted the step-aside
as a clean sync and dropped the pending edits. Both paths now throw
LockUnavailableError. A watcher test proves the edit stays pending through the
rebuild gap and is reconciled once the rebuild finishes.

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

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-29 03:01:38 +00:00
Colby MchenryandClaude Opus 5.5 c405ac5e6c fix(windows): make load-sensitive tests wait on real signals; longer pid-file retry (#1773) (#2078)
* test: make Windows load-sensitive tests wait on the tree, not the clock (#1773)

Each failure from the loaded Windows run was traced to its mechanism and
reproduced deterministically before changing it. No assertion is weakened.

- daemon-lock-refresh: the PowerShell holder slept 200ms against a 375ms
  retry budget. Windows refuses a replace-rename while any handle to the
  target is open, even a Node read handle, so hold one in-process and close
  it inside the rename mock after the first real failure. Also asserts the
  first rename really failed with a retryable code.
- mcp-projectpath-lifecycle: the owner daemon armed a 500ms idle timer at
  start and exited (code 0) before a starved engine reached it. It now starts
  with idle exit off and is armed over IPC once the engines hold a session.
  The file waits for catch-up instead of the gate's 3s serve-anyway deadline,
  and its index-heavy hooks and eviction case get explicit timeouts.
- mcp-daemon / mcp-writer-lock teardown: racing launchers spawn detached
  daemon candidates whose cwd is the fixture; a loser still starting at
  teardown pinned it (EBUSY) or took over after the winner was stopped. A
  --require preload records candidate pids, and teardown waits for the
  losers before stopping the winner.
- mcp-writer-lock: writer.pid appears before the daemon binds and rewrites
  daemon.pid, so cleanup could probe a daemon mid-start ('unverified'); wait
  for both proxies to attach. Removal uses fs.promises.rm, because fs.rmSync
  gives up on the first Windows access-denied error despite maxRetries.
- mcp-daemon #1963: connect only after the proxy attached (lock before bind).
- sync-rebuild-convergence / multi-repo-workspace: explicit timeouts scoped
  to the cases that build real indexes; the global timeout is unchanged.

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

* fix(daemon): give the Windows pid-file replace a longer retry budget (#1773)

Windows refuses to replace a file while any handle to it is open, including a
plain Node read handle, so an antivirus scan, an indexer, or another session
reading daemon.pid can each block the daemon's startup refresh for a moment,
and for longer on a loaded machine. The retry waited 375ms in all; it now backs
off 25, 50, 100, 200, then 400ms three times, about 1.6s over eight attempts,
still well inside a launcher's ~6s connect window. Permanent failures and
non-Windows errors still abort at once, and an ownership change still stops
the retry.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:13:35 +00:00
Colby MchenryandClaude Opus 5.5 003305f1e7 fix(explore): claim complete source only for sections that are complete (#2077)
* fix(explore): claim complete source only for sections that are complete

On the tiers with a completeness signal (>= 500 indexed files) every
codegraph_explore response ended with "Complete source for N files is
included above — do NOT re-read them", whatever the render had cut. A
windowed spine method, a shrunk or dropped cluster and the per-symbol view
all went out under that line; the oversize-spine window never even set the
trim flag the small tiers key off, which is how a 62-line slice of vscode's
968-line rpcProtocol.ts was called complete. The line also said "Reserve
Read for a single specific line range", which explore output must never
say.

Completeness is now measured at emission from the ranges actually sent
(plus back-referenced spans) against the symbols each section set out to
deliver. A trimmed response keeps the verbatim / already-Read guarantee,
names the trimmed files and the most relevant elided symbols, and steers to
another codegraph_explore. The note is offered from most to least specific
and fitted to the leftover room (CG-26); a trimmed note yields the pointer
list's first entry. The lost-pointer, cut and truncation notes drop
"complete" the same way, at no greater length, so the epilogue floor is
unchanged. The small tiers' trimmed note now also fires for the cuts the
flag missed.

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

* fix(explore): offer an elided method as Owner.member in the completeness note

The note offered `as_sql` for django's elided SQLCompiler.as_sql — one of
110 definitions — so agents appended a path and line to disambiguate it,
and that query shape returns none of the body. `SQLCompiler.as_sql` alone
returns most of it. Methods are now offered as `Owner.member` from the
indexed qualified name; everything else keeps its bare name.

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

* refactor(explore): fit the completeness note and pointer list in one tested function

A trimmed note's optional detail (the files, the elided names) never costs a
pointer entry the least specific note would have left. The fit moves out of
handleExplore into fitExploreEpilogue so both rules are pinned by tests; the
second-try branch it had could never fire and is gone.

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

* test(explore): completeness note wording after #2068 flags the spine window

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

* docs(explore): record the completeness-note replay and agent A/B

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

* fix(explore): address review of the completeness note

- Judge completeness AFTER the dedup fold: a remainder too small to fence
  is sent in neither call, so a section with one is trimmed, not complete.
  The whole-file render now declares its symbols too (clamped to the lines
  it prints; the file node is not one), so a fold there registers.
- Label trimmed files unique among every path the response can name, the
  pointer list included.
- "1 file", and no "0 files" when every section was held from an earlier call.
- The lost-pointer, cut and truncation notes are one exported table,
  EXPLORE_FALLBACK_NOTES, with the length rule tested; the lost note picks
  its wording from trimmedShown instead of re-joining the response.
- The design doc's follow-up list marks the overclaim fixed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:54:00 +00:00
Colby MchenryandClaude Opus 5.5 290e03f7bc fix(explore): pin only the same-named file that defines the symbols a query names (#2071)
A bare basename several directories share (`editorOptions.ts`, `worker.rs`,
`compiler.py`) pinned every match, and the matches split the pinned
reservation: on vscode `editorOptions.ts clampedInt cursorStyleToString
cursorStyleFromString` gave the file holding all three 8,626 chars, against
15,181 without the path, and spent the rest on a workbench helper defining
none of them.

Path extraction now takes an injected `symbolFiles` lookup (the module stays
DB-free, like `existsOnDisk`). When a span matches several files, the matches
defining a precise symbol token the query names (camelCase, PascalCase,
snake_case, qualified — the seeder's NL-stopword shape test) are pinned and the
rest set aside; the summary line names them. When no match defines one, or
every match does, the span resolves exactly as before. The same test resolves
a basename shared past the ambiguity budget (django's `models.py`) when it
narrows into budget, and a line anchor binds once the span narrows to one file.

Replay of 196 queries over 8 indexed repos, main vs this branch: every
unambiguous pin is byte-identical (38 unique basenames, 13 dir/basename, 8
file:line, 38 earlier-sweep path queries), as are 26 path-free queries, 13
where every match defines a named symbol and 8 naming no symbol. All 52
changed responses are shared basenames — named FULL 207 -> 231 (11 up, 0 down), named lines 6,045 -> 7,577,
none over 25K.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:03:11 +00:00
Colby MchenryandClaude Opus 5.5 724b5dee0c fix(explore): return named bodies from merged spine clusters; let named files use spare budget (#2068)
* fix(explore): return named bodies from merged spine clusters; let named files use spare budget

Re-applied onto main after #2062 and #2063 landed in the same render path
(originally e31cc9fc on claude/inspiring-grothendieck-8a7b3e):

- the merged-spine hold-back and its top-up are dropped: #2062's hold-back
  (every cluster leaves room for the protected members ranked below it)
  subsumes them, and the past-the-pin-cap test passes on it;
- #2063's "don't window a cluster holding an exact target" becomes the
  member rule's "don't window a spine method that is or holds one";
- the shrink orders exact targets ahead of spine members, and the exact
  branch prices member windows (sectionRangesOf) rather than whole bodies;
- the final ceiling fit shortens the file header back to its estimate
  before it cuts source. With an exact target's overshoot (#2063) spending
  a named file to its funded line, a lower file's header running past its
  estimate cost it 109 chars of source (922 of 1,031; 995 on main). The
  valve test now asserts funding in full and delivery to the line, since
  #2062's exact rendering stops at whole lines.

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

* docs(explore): record the agent A/B for the named-concentration fix

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

* docs(explore): record landing named concentration on main after #2062/#2063

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:21:01 +00:00
Colby MchenryandClaude Opus 5.5 47840a6ed4 fix(explore): return a qualified or line-anchored method's body instead of its signature (#2063)
`SQLCompiler.as_sql pre_sql_setup get_select` on django returned get_select and
pre_sql_setup in full and SQLCompiler.as_sql -- qualified, step 1 of the Flow,
226 lines -- as one signature line. compiler.py renders as a per-symbol
focused view, which picked bodies in source order within a tier, so the
unnamed bridge get_qualify_sql and get_select (both above as_sql in the file)
took the cap. The follow-ups agents issue (`compiler.py:776`,
`compiler.py lines 900-1003`) pinned the file and dropped the line numbers.
Every agent run ended in a Read of compiler.py.

- query-paths: keep `:776` / `:12-40` / `#L88-L120` suffixes as line anchors
  and bind prose ranges (`lines 900-1003`, `L900-L1003`, `lines 900 to 1003`)
  to the nearest single-file path.
- EXACT targets: a qualified name resolving to <=3 callables, or the callable
  enclosing a single-line anchor. Multi-line anchors render as that span.
- Focused view: tiers exact > named spine step > unnamed bridge > unique named >
  family co-named, spine in call order. A tier 0-1 body that does not fit is
  windowed (head + calls into the question's other symbols) with the explore
  range that returns each hole, never reduced to a signature. Priced in
  rendered chars net of the signature lines, so the section keeps its promise.
- Cluster path: exact clusters rank first with the spine's bounded overshoot,
  shrink by exact rendered size, and hold back what lower-ranked exact
  clusters still owe; files holding one are funded like spine files.
- Blast radius leads with exact targets.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:20:54 +00:00
Colby MchenryandClaude Opus 5.5 adad8407ff fix(explore): pay named members before incidental ones across clusters (#2062)
Clusters of equal max importance rank by density, and density counts
every member, so a named function with unrelated helpers merged around
it outranked a cluster holding a named function alone. Taken whole, the
higher clusters spent the file's reservation on the helpers and the
isolated named function rendered nothing (lib/response.ts repro:
sendBody 0 of 36 lines). Reordering alone only moves the victim.

Two rules, both scoped to incidental members (not named, not an entry
point, not on the call path):
- a cluster's incidental members may only use what is left once every
  lower-ranked cluster's protected members are paid; its own protected
  members are never held back, so named-vs-named stays rank's call;
- in a cluster holding a protected member, incidental members are never
  kept past the render's ceiling in rendered size, so the source-order
  window back to the ceiling no longer cuts a named body's tail (gin's
  handleHTTPRequest) on selection or on any re-render.

47 deterministic queries over the 7 README repos plus express vs main:
named bodies complete 198 -> 234 (17 responses up, 0 down), named lines
+10%, responses over the 25K cap 5 -> 1. One new final-cut truncation
(django-bag) comes from owedPayableBelow's MIN_CHARS threshold, recorded
in the design doc. On main + the named-concentration branch: 235 -> 279.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:04:10 +00:00
Colby MchenryandClaude Opus 5.5 c13bd0890f fix(wsl): keep WSL off Windows' index on /mnt drives, and explain the I/O error (#995) (#2061)
When Windows-native and WSL CodeGraph both open the same
.codegraph/codegraph.db under /mnt/<drive>/, SQLite's locking and -shm
shared memory don't hold across the 9p/DrvFs bridge and WSL fails with a
bare "disk I/O error".

- A SQLite I/O error on the default index of a WSL /mnt/<drive> project is
  rewritten (in the SQLite adapter, so every open and query is covered) into
  the CODEGRAPH_DIR=.codegraph-wsl instruction. MCP answers it success-shaped,
  and a failed default-project open is reported instead of "no project loaded".
- On WSL + /mnt/<drive> with CODEGRAPH_DIR unset, the data dir is resolved per
  root: an existing .codegraph-wsl/ wins, then an existing .codegraph/ index
  (kept, no silent re-index), else a fresh index goes in .codegraph-wsl/.
  `codegraph init` says where it went. Every other host is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:22:18 +00:00
Colby MchenryandClaude Opus 5.5 66aebd17e2 fix(explore): three retrieval regressions found against v1.6.0 (#2057)
The pre-release agent A/B (main vs v1.6.0, 7 README repos, 3 runs each)
had main returning 15% less source on vscode for the same queries, with
Reads in 2 of 3 runs where v1.6.0 had none. Replaying every query both
builds' agents issued traced it to three causes:

1. Named gaps (#1711) displaced source. Every cluster fit measured the gap
   names, which on a long path run to hundreds of chars, so a class shrunk
   into its file's room overran it by the names alone and was dropped
   whole: rpcProtocol.ts emitted 222 of 5,496 funded chars (v1.6.0: 5,194),
   and the test fixture came back as an empty fence. Fits now measure bare
   gaps; names are added at assembly from what the file's budget has left.

2. Interface members (#1638) corroborated English words. The NL-stopword
   guard lets a bare word seed a definition when another query word names a
   symbol in the same file; `readonly host` in an options interface made
   "main" seed `main()` in agentHostServerMain.ts, which took the named-first
   tier from the answer files. Members declared inside an interface no
   longer count as corroboration.

3. The interface-dispatch note announced dispatch through any common base,
   declared or not: "`extension` -> runtime dispatch to 2706 types
   implementing Disposable" opened 9 of 31 vscode answers. A supertype now
   has to declare the member (it, an ancestor, or a same-named Swift
   extension). When absence can't be judged, because a protocol's
   requirements aren't indexed, the note stays as before.

Replayed queries, source lines vs v1.6.0 (main -> fixed): vscode 10,744 ->
12,577 (12,617), excalidraw 13,065 -> 13,698 (13,764), django 8,420 ->
8,803 (8,954), tokio 13,618 -> 14,151 (14,600), okhttp 6,511 -> 6,883
(7,090), alamofire 7,019 -> 7,249 (7,120); no query below 60% of v1.6.0.
Real dispatch notes (Alamofire adapt/retry/asURLRequest, django, okhttp)
are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:55:33 +00:00
Colby Mchenry 0a9f3de72c fix: make the full test suite pass on Windows (#2053)
* test: close resolution graphs before removing Windows fixtures

Close ArkTS and JVM graph connections in afterEach, including failed assertions. Verified both files on Windows 11 and macOS (32 tests).

* test: compare explore source lines across LF and CRLF checkouts

Normalize only line endings when matching emitted source against checked-out fixtures. Preserve the closure coverage and whole-line assertions. Both files pass on Windows 11 and macOS (15 tests).

* test: exercise old Git fallback without a POSIX shell shim

Inject only the unsupported ls-files invocation and run every other Git command against real repositories. Assert that each rejected call retries, use portable graph paths, and clean up fixtures. Passes on Windows 11 and macOS.

* test: normalize Python fixture before generating CRLF cases

The tests added in #2004 produced CRCRLF from a Windows checkout. Normalize the input to LF before constructing either variant. All 28 native and WASM cases pass on Windows 11 and macOS.

* test: allow time for eviction fixture indexes on Windows

The #2036 eviction test builds and reconciles nine real indexes and consistently exceeds its default five-second timeout on the VM. Allow 15 seconds for this test alone, preserving every assertion. The full file passes on Windows 11 and macOS (21 tests).

* test: finish MCP processes before resetting Windows fixtures

Launch actual servers with their runtime flags and await termination before directory removal. Use asynchronous removal retries for daemon fixtures. Establish a ready daemon and stop its first proxy before simulating PID reuse, then make source stale without opening a competing SQLite writer. Preserve lock and database byte-equality assertions. All touched files validated on Windows 11 and macOS.

* test: allow Windows named-pipe fallback polling to finish

The VM takes up to 25.7 seconds to complete the nominal six-second retry loop. Allow 30 seconds for the three fallback waits and 40 seconds for the two shorter enclosing tests, retaining their lock-preservation and read-only assertions. The full daemon file is validated on Windows 11 and macOS.

* fix(daemon): retry transient Windows PID-file sharing violations

A transient EPERM replacing daemon.pid aborted startup, observed in the quiet-client test and reproduced with a real locked handle at ba3c21e5. Retry only Windows sharing errors with five bounded delays, rechecking ownership each time and cleaning temporary files. Keep publication synchronous before client processing. Cover transient/permanent failures, changed ownership, non-retryable errors, and a real Windows handle; require a confirmed daemon round-trip before measuring a quiet session. Validated on Windows 11 and macOS.
2026-09-27 16:26:10 +00:00
1926ec39a2 fix(index): coordinate rebuilds with running MCP daemons (#1325) (#2051)
* fix: safely stop daemon before reindex

* fix(index): coordinate rebuilds with running MCP daemons (#1325)

Stop verified project daemons and confirm termination before recreating SQLite.
Fence daemon and proxy reopening throughout rebuilds while preserving foreign and newer locks.
Add Windows lifecycle coverage and cross-platform shutdown regressions.

Co-authored-by: Harry B ✦ <drewavesx0@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): fence project opens during index rebuilds after rebase

Carry the #1325 rebuild fence through the upstream explicit-project lifecycle, read-only engine opens, synchronous initialization retries, and direct writer acquisition. Preserve shared project leases, read-only fallback behavior, freshness handling, and direct-mode query pools.

Cover watched, unwatched, and read-only engines blocking during a foreign rebuild and recovering after the fence is released.

* fix(mcp): answer a running index rebuild as guidance, not a tool error (#1325)

A projectPath call during `codegraph index` hit assertNoRebuild's plain Error
and returned isError; it is an expected, temporary condition, so it now uses a
RebuildInProgressError that the tool handler answers success-shaped.

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

---------

Co-authored-by: Harry B ✦ <drewavesx0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:26:49 +00:00
346d3e3702 fix(mcp): terminate wedged local proxies with watchdog (#943) (#2049)
* fix(mcp): arm liveness watchdog in local proxy

* fix(mcp): terminate wedged local proxies with watchdog (#943)

Install the independent liveness watchdog in the local handshake proxy and stop it during shutdown.
Build on PR #979 while preserving existing daemon assertions.
Add process-termination, slow fallback, and watchdog cleanup coverage.
Validate the repro and targeted tests on Windows, macOS, and Linux.

Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:10:32 +00:00
2268a09b24 fix(sync): recover index freshness after lock contention (#1959) (#2043)
* fix(lock): keep a live writer lock regardless of file age (#1959)

* fix(watcher): re-arm after lock contention and reconcile before trusting index (#1959)

* feat(mcp): report exact index freshness without blocking the daemon (#1959)

* fix(mcp): refuse drifted explore source after watcher degradation (#1959)

* fix(mcp): refuse graph answers that name a drifted file after watcher degradation (#1959)

Explore already refuses to render source from a file that changed while
auto-sync was off. search, callers, callees and impact answer from the same
frozen graph but kept replying, naming the changed file as if its edges were
current. They now take the same refusal: the indexed files an answer names
as locations are hashed against the index, and a drifted one is named
instead of the answer. codegraph_node keeps its own drift gate, which serves
the changed file's current bytes.

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

* fix(sync): recover index freshness after lock contention (#1959)

Keep a live writer's lock regardless of its age.
Re-arm lock-degraded watchers with throttling and retain stale state until full reconciliation succeeds.
Validate contributing files through structured paths and hashes, refusing incomplete checks without tool errors.
Report bounded freshness measurements and cover Git fallback exclusions.

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:16:48 +00:00
8bde4a1500 fix(mcp): stop pending connections and serve read-only fallbacks (#1963) (#2042)
* fix(mcp): close pending sockets and allow read-only fallback (#1963)

* fix(mcp): log when a fallback serves reads without auto-sync (#1963)

A read-only fallback starts no watcher and holds no writer lock, so a
session that quietly stopped syncing looked the same as a healthy one in
the logs. Name the holder on stderr when the fallback goes read-only.
Suggested by @bompus in #1979.

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

* fix(mcp): stop pending connections and serve read-only fallbacks (#1963)

Pending handshakes could block daemon shutdown or register phantom sessions after disconnect.
Track accepted sockets and reject late hello continuations.
Use explicit read-only engines and SQLite connections for blocked fallbacks, suppressing synchronization, watching, migrations, and repairs.
Preserve writer ownership and the existing project lifecycle, with regression coverage for #1963 and #1356.

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:15:29 +00:00
Colby MchenryandClaude Opus 5.5 794d4a2d72 fix(mcp): retire failed-startup query workers (#1357) (#2041)
Workers whose project open failed were enrolled as idle and returned errors indefinitely.
Route failed readiness through the existing idempotent retirement and replacement path.
Ignore retired-worker messages and duplicate readiness handshakes while preserving the crash budget and startup limits.
Cover mixed pools, repeated failures, lifecycle duplicates, and real SQLite recovery.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:14:58 +00:00
Colby MchenryandEric Minish 68085f3997 fix(mcp): cap watchdog and startup-handshake timeouts at the largest timer delay (#1966) (#2039)
Node runs a setTimeout delay above 2^31-1 ms after 1 ms instead, so a huge
CODEGRAPH_WATCHDOG_TIMEOUT_MS SIGKILLed a healthy server at once and a huge
CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS abandoned the launch within
milliseconds. Both parsers now cap at 2147483647.

(cherry picked from commit a242f579ce)

Co-authored-by: Eric Minish <eric.minish@gmail.com>
2026-09-27 13:58:22 +00:00
585f37066c fix(mcp): keep explicit projects synchronized (#1835) (#2036)
* fix(mcp): keep an explicit projectPath project in sync (#1835)

A project opened through a tool call's `projectPath` (a repository other
than the server's default — e.g. an indexed child of an un-indexed
workspace) was opened read-only: no catch-up sync on open and no file
watcher, so its answers went stale until someone ran `codegraph sync`.

The engine now owns the lifecycle of every project the ToolHandler opens
for an explicit path, mirroring the default project:
- the project's writer lock is acquired (#1740 single-writer rule); if
  another live process holds it, that process keeps syncing and we only
  read;
- a catch-up `sync()` runs on open and the first call against that
  project awaits it, time-boxed like the default gate (#905);
- a file watcher runs for as long as the project stays cached.

Bounds: the cache is keyed by the canonical (realpath) root, so a
symlinked spelling shares one connection and one watcher; it is LRU with
`MAX_CACHED_PROJECTS = 8` — opening a ninth closes the least recently
used (watcher stopped, writer lock released, DB closed); `stop()` closes
them all. Un-indexed paths still get the success-shaped guidance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(mcp): keep explicit projects synchronized (#1835)

Explicit projectPath connections bypassed catch-up and synchronization ownership.
Share canonical project leases and catch-up gates, retain daemon sessions, and retry ownership after a writer exits.
Advertise direct-writer readiness and drain active work before eviction or shutdown.
Add exact SQLite regressions for concurrent sessions, aliases, no-watch behavior, and teardown.

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): retry catch-up quietly on sync lock contention

Handle LockUnavailableError without marking catch-up complete or logging an alarming failure, and verify timer-driven recovery against a real indexing lock.

Make the eviction regression deterministic by applying its short timeout only to the held catch-up. Other projects must finish reconciling before the test can assert that the oldest eligible graph was closed.

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 13:46:58 +00:00
Colby MchenryandClaude Opus 5.5 6f31afd499 fix(mcp): resolve the pool's project root only when the pool serves the call (#2027)
The #1465 change read the default project root on every tool call, before
checking whether a query worker would serve it. Resolve it only for a pooled
call; in-process dispatch no longer depends on it (restores the oversized-output
truncation test).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:29:07 +00:00
Colby MchenryandClaude Opus 5.5 9ad6ee9c9a fix(explore): explain empty results with bounded lexical diagnostics (#1904) (#2026)
Empty explore results returned no explanation or retry guidance.
Check FTS and live name segments with two bounded SQL queries to distinguish matched and unmatched words and suggest indexed names.
Preserve success-shaped responses, unresolved-path notes, ranking, and non-empty output; cap added diagnostics below 1.5K characters.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:25:42 +00:00
Colby MchenryandClaude Opus 5.5 73f7f07d9e fix(mcp): enable query workers in direct mode (#1465) (#2022)
* fix(mcp): enable query workers in direct mode (#1465)

Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.

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

* fix(mcp): enable query workers in direct mode (#1465)

Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.

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

* fix(mcp): enable query workers in direct mode (#1465)

Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:23:38 +00:00
e5fb5e390e fix(mcp): deduplicate equivalent project paths (#1057) (#2020)
* fix(mcp): share one connection across two spellings of one root (#1057)

A symlinked checkout, or a case-variant of a root on a case-insensitive
mount (macOS, NTFS, WSL DrvFs), resolved to two path strings and opened two
connections to the same .codegraph/codegraph.db.

The connection cache stays keyed by path, so a root recreated at the same
path still heals in place (#925). On a miss, an open root that is the same
index NOW (both data directories stat to one dev:ino, read as bigints) is
reused and the new spelling filed as its alias. Comparing live identities
means a deleted root never matches, even if a new directory reuses its
inode. Windows has no usable inode, so it compares the case-folded
realpath.native instead. statInode moves to directory.ts so the DB layer
and the cache share one reading, and closeAll closes an aliased instance
once.

* fix(mcp): keep root aliases out of the connection cache (#1057)

A second spelling of an open root was filed in projectCache under its own key,
so one CodeGraph sat under two keys. Anything that drops one key and closes its
connection (a bounded cache's eviction, as proposed in #1929) then left the
other key holding a closed handle: every query on that spelling, and through
the alias scan on the original spelling too, failed with "database is not
open" until restart.

Aliases now live in their own map (spelling -> projectCache key). projectCache
is back to one key per instance, and an alias whose entry is gone is dropped
and re-resolved instead of served.

* fix(mcp): deduplicate equivalent project paths (#1057)

Equivalent project paths could open duplicate cached connections.
Compare current filesystem identities using bigint inodes and native Windows paths.
Revalidate aliases and pin cached connection owners to symlink targets.
Preserve recreation recovery and single-owner cleanup.

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:12:42 +00:00
87924a978c fix(mcp): match complete paths in stale banners (#1968) (#1997)
* fix(mcp): match pending paths as whole paths in the stale-file banner (#1968)

withStalenessNotice counted a pending file as referenced whenever its path
was a substring of the response, so a pending src/app.ts was named in the
banner for a response that only showed src/app.tsx, and a pending root
app.tsx for one that showed src/app.tsx. A match now has to stand as a
whole path.

* fix(mcp): match complete paths in stale banners (#1968)

Substring matching incorrectly classified pending files as referenced inside longer paths.
Build on PR #1971 with response delimiters that reject Unicode and punctuation continuations while preserving exact paths and line references.
Add real-index regression coverage for truthful warnings and the elsewhere footer.

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:16:23 +00:00
94ae04d8d3 fix(explore): detect runtime import templates and concatenations (#1967) (#1996)
* fix(explore): flag dynamic imports built from a template or concatenation (#1967)

The JS dynamic-import form skipped any argument that started with a quote,
so import(`./locales/${lang}.js`) and import('./x/' + y) were treated as
literal imports and the boundary note never appeared. The call is now
dropped only when its argument is exactly one complete string literal.

* fix(explore): detect runtime import templates and concatenations (#1967)

The detector skipped every import argument beginning with a quote or backtick.
Build on PR #1970 to suppress only complete static literals and recognize interpolation with escape-aware scanning.
Add scanner regressions and real-file/SQLite explore coverage.

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Eric Minish <eric.minish@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:16:13 +00:00
Max HsuandClaude Opus 5 ec44e773de fix(explore): report extension-less paths the index doesn't hold (#1830) (#1837)
A query span like `scripts/deploy` was neither pinned nor reported.
`isClearlyPathShaped` demands a dot-extension on the last segment, so a
dotless slashed span that resolved to no indexed file fell through every
arm: it was not consumed, never entered `unresolvedPathSpans`, and its
fragments (`scripts`, `deploy`) went on to feed FTS. The agent got a pile
of unrelated source with no hint that the file it named was never read.
Both caveat sinks — the empty-subgraph note and the summary line — were
already in place; they were simply never handed the span.

Shape alone cannot decide this: `and/or`, `input/output` and
`client/server` have exactly the same shape, and loosening the shape test
to "has a slash" strips that prose out of every query and mints a false
"No indexed file uniquely matches `and/or`" caveat (the existing
`leaves slash-bearing non-paths alone` test pins this). The file's
existence on disk is the discriminator instead — and since query-paths.ts
is deliberately pure (no DB, no fs, safe in the query-pool workers), the
check is injected by the caller as an `existsOnDisk` predicate. The call
site in tools.ts owns the project root and runs it through
`validatePathWithinRoot`, so a `../` span cannot probe outside the
project, and only a regular file counts — a directory span keeps flowing
to the normal matching pipeline.

Retrieval effect (AGENTS.md "do not regress"), measured as a deterministic
A/B on the built dist over an indexed repro, baseline = upstream/main:
- query that is only the dotless path — before: 998 chars of unrelated
  source, no caveat; after: `No relevant code found ... (no indexed file
  uniquely matches `scripts/deploy`)`.
- same path inside a real symbol query — output byte-identical except the
  added caveat sentence (+51 chars); the same 4 symbols across 2 files.
- query with no path span — byte-identical between arms.


Claude-Session: https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 12:11:04 +00:00
Max HsuandClaude Opus 5 31391b9751 fix(node): accept a line-numbered file reference in file-view (#1831) (#1836)
`codegraph node "src/app.ts:42-80"` answered `No indexed file matches`
for a file that IS indexed, worded byte-for-byte like a genuine miss, so
there was no way to tell "wrong path" from "right path, wrong spelling".
The file-view normalizer treated the `:42-80` as part of the filename.

explore has stripped exactly these shapes all along
(src/search/query-paths.ts) — file-view now does too, and the range it
strips becomes the read window.

- The literal spelling is resolved FIRST; only when that finds nothing is
  the suffix stripped, so a file genuinely named `foo:12` still wins.
- Supports `:<a>`, `:<a>-<b>`, `#L<n>`, `#L<a>-L<b>` (and `#L<a>-<b>`).
- A stripped range maps to `offset=a, limit=b-a+1`; a bare `:<a>` is a
  start-here pointer (Read given only an offset). An explicit
  offset/limit from the caller always wins over the suffix.
- `#L<n>` was not mentioned in the issue but was broken the same way.

Tests: 8 new cases across __tests__/node-file-view.test.ts and
__tests__/cli-node-command.test.ts, including a vacuity guard (the same
path without the suffix resolves) and a genuine-miss case. Red-armed:
with the fix reverted, 6 of them fail with the exact
`No indexed file matches` string.


Claude-Session: https://claude.ai/code/session_01XDR1wm73oH8J8cRWnKyv9m

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 11:57:16 +00:00
Christopher BeaulieuandColby Mchenry 1e46123758 fix(daemon): preserve live legacy locks (#1834) (#1850)
Co-authored-by: Colby Mchenry <me@colbymchenry.com>
2026-09-15 16:36:00 +00:00
Colby MchenryandColby McHenry aed046e5c6 fix: stop silent fuzzy symbol substitution (#1473) (#1809)
Adapt @uvmplus's PR #1481 (3ecf7479) to the shared symbol lookup and
named-symbol flow resolver on current main. Missing names return not found
with suggestions, and exact matches with no callers stay empty.

Preserve #1512 definition grouping and --file narrowing, #173 qualified
misses, and codegraph_node's intentional fuzzy file lookup. Port the
upstream regression suite and cover the moved shared lookup paths.

Validation on Linux with Node 22: project build, 74 requested tests,
47 related flow tests, and 16 same-fixture CLI/MCP checks pass. Baseline
captured 12 failing tests and 13 failing fixture checks.

Fixes #1473.
Supersedes #1481.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 19:40:48 -05:00
Colby MchenryandColby McHenry e79d5df448 fix(mcp): make explore guidance advisory (#1504) (#1805)
Apply upstream commit 1572d90d71 from
PR #1570 by @rongbc.

Clarify that suggested explore call counts are advisory and extra calls
remain available. Sync the MCP initialization guidance and eval probe,
retain the upstream regression tests, close the footer's bold formatting,
and credit the fix under Unreleased in the changelog.

Validation: npm run build; focused Vitest (3 files, 53 tests); source and
dist wording greps; git diff --check.

Fixes #1504

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 18:28:36 -05:00
9181dd1ef3 fix(extraction): land upstream declaration initializer walks (#1511) (#1802)
Squash danusha2345's PR #1511 at d282f9e8 onto main 8c9c4761,
preserving its nine non-merge commits and main's existing Unreleased notes.
Calls in Kotlin, Java, TS/JS, Scala, Rust and Python declaration initializers
now retain the owner established by the upstream regression expectations.
Include the upstream CFML, dynamic-dispatch summary and viewer follow-ups.

Linux fail-to-pass validation (Node 22.19.0, rebuilt dist and native kernel):
- Before: TS load belonged to file:app.ts; Python/Kotlin/Scala/Rust calls
  vanished; Java lost the field-lambda, anonymous override and eager calls.
- After: all six languages PASS; 12 native/WASM LF/CRLF parity checks PASS.
- Focused initializer regressions: 10 passed with CODEGRAPH_KERNEL=0 and
  10 passed with the kernel enabled; Kotlin's grammar fallback is recorded.
- Related regression suites: 879 passed, 1 skipped across 15 test files.
- Evidence: /workspace/cg-1510-repro/before and /workspace/cg-1510-repro/after
  (combined test output: after/vitest.log).

Fixes #1510
Supersedes #1511

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 17:33:45 -05:00
8c9c4761b0 fix(cli): group callers/callees/impact by definition (#1512) (#1801)
* fix(cli): port upstream symbol lookup consistency (#1656, #1512)

Port ferrine/fix/symbol-lookup-consistency at
c0ccbacd3f onto current main.
Qualified CLI queries use the shared matcher and ambiguous names disclose
their targets. Keep total/limit/truncated and the human truncation notice
from #1674, and share the matcher with main's named-symbol-flow module.

Refs #1512. Upstream PR: #1656.

Co-authored-by: ferres <justferres@yandex.ru>

* fix(cli): group traversal results by definition (#1512)

Extend upstream PR #1656, ported in 7038fb4f, so callers/callees/impact
show separate sections for each definition and accept --file using the
same groupDefinitions helper as MCP. Preserve same-file overload groups,
path/suffix matching, and the explicit fallback when no file matches.

JSON definitions carry their roots, own neighbors/affected nodes, and
edges. Retain the legacy top-level lists as an explicitly labeled union
and preserve #1674 total/limit/truncated; each callers/callees definition
also reports its own limit and truncation metadata.

Validation: npm run build; 141 tests across 14 targeted suites, including
32 CLI regression tests. The Linux /workspace/cg1512-repro failure now
passes for all three commands, with and without --file.

Fixes #1512.
Upstream PR: #1656 (ferrine/fix/symbol-lookup-consistency @ c0ccbacd).

---------

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: ferres <justferres@yandex.ru>
2026-09-08 17:17:31 -05:00
Colby Mchenryanddanusha2345 b8d46d13c7 fix(mcp): land #1624 opt-in explore dedup (#1788)
Land the #1624 approach by @danusha2345: re-serve source by default so
subagents and compacted contexts never inherit stale already-sent pointers.
Keep cross-call dedup available through explicit truthy
CODEGRAPH_EXPLORE_DEDUP values for durable contexts.

Preserve current Unreleased entries, credit the contribution, and align
the MCP server guidance with the safe default.

Validation on Linux / Node 22.23.2:
- Reproduced default-on failure before the fix; default-off now passes.
- Focused explore-cross-call-dedup suite: 26 passed.
- TypeScript: npx tsc -p tsconfig.json --noEmit passed.

Fixes #1620.

(cherry picked from commit 63992facab)

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 13:13:03 -05:00
Colby MchenryandColby McHenry ee83636acb fix(extraction): index TypeScript interface members (#1638) (#1780)
Land upstream #1686 (maxmilian + bompus kernel/CG-28 follow-ups)
onto current main. tree-sitter-typescript interface members
(method_signature / property_signature) were never listed in the
TS extractor, so platform .d.ts APIs had no declaration nodes for
call edges. Mirrors on the Rust kernel path; keeps CG-28 damping
for pure-interface declaration files; filters damped files from
the explore RWR seed set.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 11:27:05 -05:00
Colby MchenryandColby McHenry 85550eb2ce fix: report callers/callees/query truncation (#1674) (#1772)
Land #1647 onto current main: callers/callees/query (CLI + MCP) now say
when --limit hid matches, with totals in JSON and a widening hint. Also
covers #1639.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 09:52:56 -05:00
Colby Mchenry b715eb6374 Merge pull request #1697 from bompus/fix/claude-always-load
fix(mcp): keep codegraph_explore loaded in Claude Code and Copilot CLI
2026-09-08 08:58:51 -05:00
Colby MchenryandColby McHenry d983f73484 fix(explore): name symbols elided by a file trim (#1711) (#1765)
When codegraph_explore trimmed a rendered file, holes printed as bare
`... (gap) ...` and the header's `+N more` hid the dropped names — while the
footer asked the model to re-explore with exact names it was never given.

Name elided defs in gap markers as `name (file:line)` (using the full file
index, not just the relevance gather), bias the per-file header toward
symbols the trim cut, and point the trimmed-footer at those names.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 07:31:04 -05:00
Aaron Queen 097cd19ad2 fix(mcp): keep codegraph_explore loaded in Claude Code and Copilot CLI (#1696)
Claude Code defers every MCP tool behind ToolSearch by default, so a fresh session sees only the tool name until the model searches for it. The explore tool now carries `_meta: { "anthropic/alwaysLoad": true }`, which exempts it on existing installs, and the Claude Code installer target writes `alwaysLoad: true` on the server entry (re-running install adds the key to an older entry). Copilot CLI tool search holds MCP tools back the same way once ~30 tools are connected, so its entry carries `deferTools: "never"`.
2026-09-08 03:23:33 -06:00
bffd50e4f1 fix(resolution): a binding in a module that exports nothing is not a cross-file candidate (#1719) (#1746)
* fix(resolution): a binding in a module that exports nothing is not a cross-file candidate

On vitejs/vite, 157 cross-file `imports` refs — every `import { defineConfig }
from 'vite'` in the playground and the create-vite templates — resolved onto
`playground/ssr-html/test-stacktrace.js::vite`, which is `const vite = await
createServer(...)` at module scope in a file with zero exports.

Neither existing guard can see it. `isLexicallyReachable` returns early for any
candidate that is not a `function`, and the bare-import guard correctly declines
because `vite` IS a workspace member, so the specifier really is project-local.
What is wrong is only which node the name lands on.

A JS/TS file that contains an `import` statement and no export of any form
offers nothing to any other file, so none of its bindings is a candidate for a
cross-file name match. Applied in both name-based strategies: declining in
matchByExactName alone just hands the same target to matchFuzzy, which resolves
a unique candidate on its own.

Narrow on three axes, each a class this would otherwise get wrong in the
opposite direction: a classic script is exempt (a top-level binding really is a
reachable global), CommonJS is exempt (`module.exports` and `exports.x` count as
exports), and every non-JS/TS language is exempt. The export test reads source
rather than the node's `isExported` flag, because that flag is set only from an
`export_statement` ancestor and so reads false for `const x = ...; export { x }`.

* fix(resolution): count bracket CommonJS exports and `declare global` as exports

A file writing `exports["x"] = …` exports x, and a file with a `declare
global` block contributes every name in it to every other file whether or not
it exports anything of its own — the extractor emits nodes for the ambient
`var` and `interface` members, so sealing such a file would hide names that
really are reachable everywhere. Neither shape occurs on the vite corpus, so
this changes no measured count; both are now covered by the test.

* test(resolution): bind the #1719 fixture without a bare import

The consumer bound every name from 'some-external-pkg'. A bare specifier
names a package that is not in the graph, so no project node is the right
target for such a reference and #1715 declines it -- which made four of the
five positive assertions depend on a resolution that should not happen, and
they failed the moment this branch was stacked on #1715. Free references
reach the same exact-match path without asserting that.

`strayVar` was not testable at all: a bare identifier read emits no edge, so
that assertion only ever passed through the bare-import binding. The
`declare global` coverage moves to an interface reached through a type
annotation, paired with an identical file whose interface is not in a
`declare global` -- so the assertion turns on that clause rather than
passing whichever way the guard goes.

* docs(changelog): record the sealed-module guard under Unreleased

* fix(resolution): the sealed test rejects fuzzy's survivor, never filters its set

matchFuzzy declines an ambiguous name outright, so filtering sealed
candidates out of its set can leave a lone survivor and manufacture a 0.5
edge from an ambiguity that would have been declined. Testing the single
survivor instead closes that path; matchByExactName keeps the filter,
because it ranks a crowd rather than declining one.

No instance on vitejs/vite either way (row-identical, LOST 0 / GAINED 0
per #1720 review). It also declines one shape the filter form resolved: a
sealed same-language survivor no longer yields to a cross-language
candidate at 0.3.

* fix(resolution): reject invalid fallback targets without retargeting

---------

Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 00:13:49 -05:00
Colby MchenryandColby McHenry 7440d2c475 fix(mcp): fail fast on a second direct-mode writer per project (#1740) (#1744)
Concurrent CODEGRAPH_NO_DAEMON / in-process fallback serve --mcp instances
each started a FileWatcher and contended on codegraph.lock until auto-sync
degraded. Add an exclusive .codegraph/writer.pid lock held by the daemon or
the single direct writer; a second writer exits with actionable guidance.
Daemon mode still multiplexes N proxies onto one writer.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-07 23:56:52 -05:00
Colby McHenryandClaude Fable 5 b1f40c57dd feat(steps): cross-tier channels — a client's fetch onto its own route, queue jobs onto consumers, bus and socket events onto handlers
- resolution/tier-synthesizer.ts: http-client (literal fetch/axios/ky/got/$fetch paths, axios.create baseURL instances, template holes as :params, base-URL holes by a two-segment tail; unique match only), queue-job (BullMQ/Bull add ↔ @Process/@Processor, WorkerHost process, new Worker, queue.process), event-bus (EventEmitter2 emit ↔ @OnEvent with globs; socket emit ↔ @SubscribeMessage / socket.on both ways with tier); channel, tier, callee, registeredAt on every edge; generic transport events never pair; test and generated files never sources; registered before the emitter pass
- steps.ts: crossing() reads tier/channel before languages; an endpoint reached across a tier is a bridge box and a boundary like a screen (through=1 enters it); a channel's call is not also an effect; sites read as written; a Next 'use server' action is a crossing by its directive (when.ts directive); a function-valued constant handler (asyncHandler(...)) is a route root and borrows the file-scope calls and refs within its lines
- express.ts: app.use('/prefix', router) mounts composed onto route names in postExtract (nested, by import or require); chained router.route('/x').get(h).put(h2) extracted, across lines
- frameworks/package-deps.ts: dependencies read from workspace package.json files too (Express, React, Expo Router, NestJS detect)
- routing manifest names constant handlers; e2e/ is a test directory; explore's Flow section labels the new channels
- tests: ui-steps-cross-tier (monorepo fixture: Next client + Express/Nest API), servers test updated for the queue landing
- docs: CHANGELOG, spec §3.13 cross-tier paragraph, CLAUDE.md, callback-edge-synthesis.md, plan P3 built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
2026-08-28 14:31:01 -05:00
Colby McHenry 70fd5fefc2 feat(expo-router): add Expo Router support for screens and navigates
Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps.
2026-08-27 22:52:18 -05:00
Colby McHenry 2a0c6dc58f feat(ui): the type hierarchy — what a type is built on, and what dispatches through it (CG-58)
A vertical tree above the members outline for classes, interfaces, structs,
traits, protocols, enums, unions and type aliases: ancestors above (the whole
chain, not just the direct parent), the focus in accent, subtypes below indented
per level. `extends` draws solid, `implements` dashed; a synthesized edge — Go's
implicit interface satisfaction — draws dashed wider and carries the site it was
wired at, so a relation the resolver inferred never reads like one the source
wrote down. For an interface the fan below IS the set of runtime targets a call
can land on, and a type with eight or more implementers leads with that in a
sentence. Members that redeclare an ancestor's are marked in the outline.

The walk lives in `src/graph/type-hierarchy.ts`, following CG-50/CG-51: shared
computation in `src/graph/`, presentation in the caller. Its `countImplementers`
is now also what `ToolHandler.buildPolymorphicBoundaries` counts with, so "N
types implement X" is the same N whether an agent reads it or a person does.
`/api/node` carries the block as `hierarchy` rather than a second endpoint —
it is part of the Symbol view's first paint, and gated to types, so a function
costs one kind test.

Layout is arithmetic (24px rows, 22px indent, orthogonal connectors computed
from the two): no ResizeObserver, same payload → same picture. The header's
`extends X` / `implemented by …` chips are suppressed while the tree is on
screen — two renderings of one relation in one column is how a reader ends up
trusting neither.

`TypeHierarchy` is exported from `@colbymchenry/codegraph-ui` and takes its data
as a prop, so a host holding a `WireSymbolPayload` renders it without a second
read.
2026-08-27 07:14:55 -05:00
Colby McHenry dc7f1e590e feat(ui): where the graph stops — the Flow strip's dynamic-dispatch end cap (CG-51)
A flow that does not reach what it was asked about now ends in a cap instead
of in silence: the dispatch form that ended it, the line, the static key when
the source spells one out, the candidate runtime targets as clickable rows,
and the name-only matches under 0.6 the search refused to follow. A flow that
does reach its destination never shows one.

The verdict is lifted out of `ToolHandler` into
`src/graph/dynamic-boundary-report.ts` and both callers render it —
`codegraph_explore`'s prose and `/api/flow`'s `WireFlowBoundary` — the same
move `named-symbol-flow.ts` made for the path finder, and for the same reason:
a reader holding the strip and the MCP answer must not be told two different
things. The explore prose is unchanged, byte for byte.

When nothing connects at all and a dispatch site explains why, the strip is
that site: one card opened at the line where the static path ends, plus the
cap. When nothing explains it, no stopping point is invented.
2026-08-27 04:54:37 -05:00
Colby McHenryandClaude Opus 5 62e0a89b0e feat(ui): the Flow strip — how one symbol reaches another, one card per hop (CG-50)
Ask "how does execute reach getFile" in the search box and the viewer draws the
call path between them, left to right, opening every card at the exact line that
makes the next call. Dynamic-dispatch hops are dashed and name the site they
were wired at; "Read as flow" turns a trail walked by hand into the same strip.

The path finder is NOT new. `codegraph_explore` already leads its answers with
the longest call chain among the symbols an agent named, and a viewer that drew
a different path would get the two quoted against each other in a review. So the
search moved out of `ToolHandler` into `src/graph/named-symbol-flow.ts` and both
callers ride it — same tokens, same overload rules, same synthesized edges. What
stayed behind in `tools.ts` is the prose.

A pinned from/to question is the same search with two options changed, because
both ends being named is the evidence explore's one-unnamed-bridge cap stands in
for: it bridges freely, keeps twelve candidates per endpoint instead of six
(the CLI's own `main` sorts seventh of ten), and searches from both ends at once
— identical paths to the one-way walk on twelve measured pairs, 3-6x faster.

`/api/flow` is deliberately the one endpoint with no cache: its cards carry
source read from disk, and a drift verdict changes without the index changing.

Verified on this repo (`execute` to `rowToFileRecord`, 8 hops; `main` to
`resolveOne`, 7) and on a fresh excalidraw index, where `mutateElement` to
`renderStaticScene` crosses callback, react-render and jsx-child hops and lists
exactly the hops `codegraph_explore` prints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 03:19:24 -05:00
Colby McHenryandClaude Opus 5 0196c2e53a feat(ui): serve the viewer from codegraph ui, loopback-only and read-only (CG-41)
Adds the `codegraph ui [path]` command (alias `web`) and `src/ui-server/`, a
`node:http` server with no framework and no new dependency.

The command reads an index that already exists — it never creates one, so a
missing index prints the same friendly guidance the MCP tools give instead of
a stack trace, and a sensitive system directory is refused up front.

Security is the substance here, not the routing. The server binds 127.0.0.1
only, answers GET and HEAD only, and sends no CORS headers ever. The realistic
attack on a process that serves your source code from a local port is DNS
rebinding, so every request must carry a loopback `Host` (on our port) and, if
it carries an `Origin` at all, a loopback one — anything else is 403 before
the filesystem is touched. Every path resolves through the engine's existing
`validatePathWithinRoot` chokepoint, which already handles `../` traversal and
in-tree symlinks pointing out of the root (#527); `..` segments are refused
outright so a traversal attempt gets a 404 rather than the SPA shell.

`PathRefusalError` moves from `mcp/tools.ts` into the dependency-free
`errors.ts` (re-exported from its old home, so class identity and every
`instanceof` check are unchanged) — that is what lets a non-MCP read sink
enforce the same refusal without importing the MCP tool graph.

Assets come from `dist/viewer/` resolved relative to `__dirname`, the way
`db/index.ts` finds `schema.sql`. Hashed assets are cached immutably,
`index.html` never. Port 4747, or the next free one — an explicit `--port`
stays explicit rather than silently moving. `--no-open` skips the browser, and
`CODEGRAPH_BROWSER` picks one (or `none` to suppress it), which is also what
makes "did it open a browser" testable end to end.

`resolveProjectFile` and the `/api/` handler seam are the boundary CG-42's
JSON API plugs into; `/api/*` 404s as JSON so a typo'd endpoint never returns
the app shell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:17:09 -05:00
Colby Mchenry 41c10750e0 fix(erlang): give same-name different-arity functions separate arity-qualified nodes (#1610) (#1615)
Fixes #1610. Also fixes #1358 (the `<<binary>>` arity miscount in behaviour dispatch, reported separately and hit by the same code path).

## Problem

Arity is part of an Erlang function's identity — `f/1` and `f/2` are unrelated top-level definitions — but the extractor merged consecutive same-name `fun_decl`s regardless of arity. Reproduced on main exactly as reported:

- adjacent `f(X) -> …. f(X, Y) -> ….` → **one** node spanning both, with the first definition's signature;
- interleaved `f/1, g/0, f/2` → two nodes with **identical** `qualified_name`;
- `cowboy_req`'s `header(Name, Req) -> header(Name, Req, undefined).` → a **self-loop** `header → header`, with the `-spec` for `/3` swallowed by the merged span;
- `-export([f/1])` marked every arity exported.

## Fix

- **One node per (name, arity).** Clauses of the same name+arity still merge (that part of the old behavior was correct); a different arity starts a new node. `qualifiedName` carries the canonical spelling — `mod::f/1` — while the node **name stays bare** so search and bare-name matching are unchanged.
- **`-export` and `-spec` are per-arity.** `-export([f/1])` exports exactly `f/1`; a spec sitting between two arities attaches to the arity its signature names.
- **Refs carry the call-site arity** wherever it's statically known: local `f/1`, remote `mod::f/2`, `fun f/1` / `fun mod:f/1` values, `gen_server` dispatch (`handle_call/3`, `handle_cast/2`), and spawn/apply MFA lists (`spawn_link(?MODULE, work, [A, B])` → `work/2`).
- **The matcher resolves only to the named arity** — same file first (a local call targets its own module) — and when no definition of that arity exists it resolves to **nothing** rather than a sibling arity: silent beats wrong. An arity-less dynamic-MFA ref resolves only when the module defines exactly one arity of that name.
- **Behaviour dispatch** selects the implementer node of the site's arity, and the arity counter now skips `<<1,2,3>>` binary-literal commas per its own docstring (#1358) — `Mod:decode(<<1,2,3>>, Opts)` counts 2, not 4.
- **`codegraph_explore` / `codegraph_node`** accept the written `mod:fn/3` spelling against the new arity-qualified names (the issue's measured `cowboy_stream_h:request_process/3` shape).

## Validation

Minimal fixtures (all three reported shapes) now index as `gap::f/1` + `gap::f/2`, distinct `inter::f/1`/`inter::f/2`, and a real `deleg::header/2 → deleg::header/3` edge with no self-loop.

Cowboy (fresh `--depth 1` clone, this build vs unmodified main build):

| | main | this PR |
|---|---|---|
| nodes | 3,668 | 3,748 (+80 — the arity splits; no explosion) |
| erlang function nodes | 2,850 | 2,930 |
| behaviour dispatch edges | 38 | **44** |
| `cowboy_req::header` | one node, span 420–425, /3's spec lost | `header/2` (420–421, its own spec) + `header/3` (424–425, its spec) |
| delegation | self-loop | `header/2 → header/3` |

`calls` edges drop 6,059 → 5,656: a sample of every removed pair shows the false-positive class the issue predicted — out-of-repo/BIF calls (`length/1`, `error/1`, `quicer:*`) that previously name-matched onto unrelated same-named in-repo functions now stay unresolved.

Tests: new arity coverage in extraction + a new arity-resolution integration suite + a #1358 binary-literal behaviour test; updated existing Erlang expectations to the arity-carrying spellings. Full suite: **3,018 passed, 0 failed**.

No migration: an existing Erlang index picks the new shape up on its next re-index (`codegraph sync` / re-`init`).

Erlang is wasm-only (not in the native kernel), so there is no kernel-parity surface.

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

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
2026-08-26 10:39:15 -05:00