1137 Commits

Author SHA1 Message Date
github-actions[bot] f4ddf50851 docs(changelog): promote [Unreleased] into [1.6.1]
[skip ci] Auto-generated by Release workflow.
v1.6.1
2026-09-29 05:06:45 +00:00
github-actions[bot] 0695bc47ce release: sync package-lock.json to 1.6.1
[skip ci] Auto-generated by Release workflow.
2026-09-29 05:06:33 +00:00
Colby Mchenry 4a8eb6314e release: 1.6.1 (#2090)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:01:56 +00:00
Colby Mchenry 2c4d4eb9e4 release: withhold codegraph ui until the viewer launches; hold its changelog entries (#2089)
* feat(cli): withhold `codegraph ui` until the viewer launches (CODEGRAPH_UI=1 opts in)

The viewer has been exercised on Expo apps only, not on API, Laravel,
Spring, Angular or Swift projects, so it stays out of this release. `ui` and
its alias `web` — also as `help ui` or `ui --help` — are refused before any
startup work, and the command is hidden from `--help`, unless CODEGRAPH_UI=1
is set, which keeps it usable for the people testing it. The viewer assets
still ship, so the opt-in works on a released build.

Reverting this commit is the launch.

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

* docs(site): say the viewer is not in a release yet

The viewer pages have been live on the docs site since they landed, and
1.6.0 answers `codegraph ui` with "unknown command" (#1816, #1900). Until the
viewer launches, the guide, the CLI reference and the next-steps link say so.

Reverting this commit is part of the launch.

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

* docs(changelog): hold the viewer's entries for its launch; new Highlights for the fixes release

53 entries that describe `codegraph ui` and its screens move, verbatim, to
docs/viewer-launch-changelog.md along with the launch Highlights. 12 entries
whose graph and `codegraph_explore` half ships now (the router navigation
edges, the cross-tier hops, explore's `when` conditions, SvelteKit layouts,
wrapped Express handlers, …) are reworded around the graph; their original
wording is kept in the holding file for the launch. A few kept entries lose a
viewer clause. The Highlights are rewritten for this release.

prepare-release.mjs promotes the section and extract-release-notes.mjs reads
it back with no mention of the viewer; the released sections are untouched.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:01:34 +00:00
Colby Mchenry 09b9646b7e perf(context): compute the dominant file once per index state (#1864) (#2086)
* perf(context): compute the dominant file once per index state (#1864)

getDominantFile() backs context ranking's core-directory boost. Its answer
depends only on the graph, but the whole-graph aggregation behind it
(edges joined to both endpoints' nodes, grouped by file) ran on every
generic codegraph_explore - seconds per call on a large index.

Memoize it in QueryBuilder against a database change stamp:
total_changes() (rows this connection wrote) plus PRAGMA data_version
(commits by any other connection or process). Both are O(1), so no write
path has to remember to invalidate, and a sync made by another process is
seen on the next call. Any write forces a recompute; the heuristic's
result is unchanged.

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

* fix(context): never keep a dominant file read inside a transaction (#1864)

total_changes() counts writes that are later rolled back, so a result
computed inside a transaction could survive the ROLLBACK under an unchanged
stamp and describe edges that no longer exist. getDominantFile now skips the
memo while a transaction is open (and on a runtime without isTransaction).
Ported from the maintainer's hardening of this fix in #1919.

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

* fix(db): drop the dominant-file memo when a QueryBuilder is rebound (#1864)

The memo's change stamp (total_changes + data_version) is per connection,
and fresh read-only connections to two different databases report the same
one ("0:2"). A pool worker rebound onto a rebuilt index kept serving the old
database's dominant file until something wrote through the new connection.
A test rebinds onto a second project and fails without the reset.

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

* docs(changelog): credit @danusha2345 for #1864

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>
2026-09-29 04:37:41 +00:00
Colby Mchenry b44550e328 docs(changelog): credit @danusha2345 for the #1902 fix (#2085)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:05:02 +00:00
Colby Mchenry ee97a57a9b fix(directory): a schema-less codegraph.db does not make a project initialized (#1895) (#2083)
* fix(directory): a schema-less codegraph.db does not make a project initialized (#1895)

isInitialized() accepted any existing .codegraph/codegraph.db, so an empty or
table-less file in an ancestor directory (an interrupted init, a stray touch,
a never-populated ~/.codegraph/) captured the upward walk of
findNearestCodeGraphRoot / resolveProjectPath / resolveServerRoot for every
project beneath it: the real index of a sub-project became unreachable, the
sub-project down-scan never ran, and commands either operated on the broken
file or failed opening it instead of showing the not-initialized guidance.

An initialized project is now one whose db carries the schema: the probe is
gated by file size (>= the 100-byte SQLite header), then the header magic,
then a read-only node:sqlite open with a single sqlite_master lookup for the
`nodes` table, memoized per path + mtime + size so hot callers (the prompt
hook, MCP root resolution on every call) never reopen an unchanged db. The
handle is closed immediately so the probe never holds the db on Windows; a
locked/busy db counts as live.

createDirectory() uses the same predicate, so `codegraph init` in the
directory with the broken file repairs it in place (the schema is applied
with CREATE ... IF NOT EXISTS) instead of refusing with "Already
initialized"; the CLI says what it found. Nothing is deleted automatically.

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

* fix(directory): a database we cannot open read-only still counts as initialized

The schema probe failed CLOSED: a real WAL codegraph.db in a directory the
process cannot write (a read-only checkout, a mount, another user's tree)
made the read-only node:sqlite open throw — SQLite must create `-shm` to
open a WAL database — and isInitialized() reported the project as not
initialized, where main's file-exists check had been right.

The probe now fails OPEN. Once the size gate and the `SQLite format 3\0`
header have passed, the file is a SQLite database; only a SUCCESSFUL
sqlite_master query proving the `nodes` table absent may return false. Any
open/prepare error (locked, busy, cantopen, readonly, disk I/O) returns
true, the pre-existing behaviour for a database we cannot inspect. No error
strings are matched. The per path + mtime + size memo is unchanged, and
schema-less/empty files still return false, so `codegraph init` still
repairs them and never touches a real db.

Test: a WAL db copied into a `.codegraph/` made read-only with chmod stays
initialized — skipped on win32 and when running as root (root ignores mode
bits).

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

* fix(init): refuse a codegraph.db that is not SQLite instead of promising a rebuild

Review found that a `.codegraph/codegraph.db` holding bytes that are not
SQLite at all counted as "schema-less", so `init` printed "rebuilding it"
and then failed with SQLite's "file is not a database". Only an empty file
or a SQLite database without the codegraph tables can be rebuilt in place;
`hasSchemalessDb` now means exactly that, and the new `hasForeignDbFile`
names the other case. `init` stops on it with exit 1, says which file it
is and to move or delete it, and never touches it.

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

* fix(directory): ask SQLite what codegraph.db is instead of reading its header ourselves (#1895)

The header check opened and closed a descriptor of our own on
`codegraph.db`. Closing any descriptor on a database file drops every
POSIX lock the process holds on it, including those of a connection it
already has open (sqlite.org/howtocorrupt.html §2.2.1) — and the MCP
server resolves projects through isInitialized on every call while it
holds the index as its writer. mcp-projectpath-lifecycle's takeover tests
caught it: after a real owner exits, the taking-over engine's next read
failed with "disk I/O error" (2 of 21 fail with the header read; 21/21
on main and with this change).

One read-only SQLite probe now answers all three questions: the schema
is there, SQLite opened it and the schema is not (repairable), or SQLite
refused it as not a database (SQLITE_NOTADB). Anything else — locked,
busy, no `-shm` possible — still fails open as initialized. SQLite's own
connections share one lock table per file, so opening and closing a
second one leaves the first one's locks alone. A test pins that
isInitialized and both init helpers never open the file themselves while
this process holds it.

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

* docs(changelog): keep the #1895 entry clear of #1917's, so the carries merge in any order

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>
2026-09-29 03:01:50 +00:00
Colby Mchenry 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
Daniil 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 Mchenry 15a5d1d0cd fix(c): keep an #undef under an undecidable #if unknown; a macro call never binds to a type (#2069, #2070) (#2081)
Two follow-ups to the unreleased macro-visibility work (#1838/#1839):

- #2069: `#undef FLAG` under an `#if` nothing in the source decides made a
  never-seen FLAG certainly undefined, so `#ifdef FLAG` was read as false
  and the functions it guards lost their callers (CPython's
  `Py_GIL_DISABLED`). A name no directive has touched is now unknown; only
  an #undef on a certain line clears it.
- #2070: dropping the macro constant from a call's candidates let the next
  same-named node win, so expat's `PREFIX(scanRef)(...)` instantiated an
  unrelated `struct PREFIX`. When the name is a function-like macro in the
  project, only a function or method is an eligible callee.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:46:41 +00:00
Colby Mchenry fef37765c1 perf(resolution): remove the indexing regressions since v1.6.0 (vscode stall, per-ref rescans) — graph byte-identical (#2072)
* perf(resolution): read JS/TS parameter lists without exponential backtracking

isLocallyBoundJsName (#1759, cd4e65b5) scans a file for a parameter that
binds a bare-called name. Its per-parameter pattern `(type)?(default)?\s*`
let one `name: T = value` text split several ways, so when the name was
absent the search backtracked through every combination. On vscode's
markersModel.test.ts, whose helper takes nine typed, defaulted parameters,
each `suite`/`test` scan took 30-40s — one pool worker held a whole
resolution batch while the rest idled, a 70-400s stall per run.

Each parameter now has exactly one parse: its first non-space character
after the identifier picks the type, default or bare alternative. The
alternatives match exactly the strings the old optional groups did, so the
result is identical for every input (4M generated cases and 5.6k real
vscode file/name pairs agree); the scan is 0.1-0.3ms on that file.

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

* perf(resolution): memoize Python module-file lookups per resolution pass

#2034 (5eaa6fed) sent every Python named import whose path the import
resolver cannot place — stdlib and third-party modules such as
`from unittest import mock` — through findPythonModuleFile, which scans
every `__init__.py` file node for a path suffix. It ran once per ref:
on django that scan alone went from 0.6s to 1.4s of resolution CPU.

The candidates for a module depend only on the module path, so they are
now collected once per resolution context, in the same name-lookup order;
the importing file is still excluded per call, so the first survivor is
the node the scan returned. The memo drops with the other import-resolver
memos. Resolution output on django is hash-identical; single-threaded
resolution CPU there goes from 6.5-6.8s to 4.9s (v1.6.0: 4.5s).

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

* perf(resolution): test sealed-module candidates cheapest-first

isSealedModule (#1719, bffd50e4) masks a candidate file's comments and
strings before asking whether it is a module that exports nothing. Import
refs call it for every same-named candidate across the tree, so on vscode
it masked nearly every TypeScript file once per resolver: 3.3s of masking
in a single-threaded pass, paid again in each pool worker.

The verdict is a pure conjunction, so it now asks the cheap questions
first: the source contains `import`, no node in the file is exported, and
there is no CommonJS export. Masking runs only for files that pass all
three, which in practice are the few that can be sealed. The masker blanks
text and never deletes it, so a file without `import` in its source has
none in its masked code either. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): scan a class for a field's declared type once per field

matchTsThisFieldCall (#1496, cece0720) resolves `this.<field>.<method>()`
by finding the field's declaration in the enclosing class. It re-split the
whole file, stripped comments line by line and compiled three patterns for
every such call, although the answer depends only on the class and the
field: 16.5s of a 148s single-threaded resolution pass on vscode.

The first declaring line — `: typeof X`, `: X` or `= new X`, in that order
per line — is now memoized per (class node, field), and the last few
classes' stripped lines are kept, since calls arrive file by file. Owners
are still tried in the same order and everything after the declaration
lookup is unchanged. Resolution output is hash-identical on vscode and
django.

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

* perf(synthesis): start cross-tier receiver chains only at a word's start

The cross-tier pass (b1f40c57) scans every non-test JS/TS file whose text
has a `.add(`, `.get(` or `.on(` — nearly all of vscode — with receiver-
chain patterns such as `a.b.c.add('job')`. A chain could begin at any
letter of an identifier, so each letter re-read the dotted chain behind
it; on vscode the pass ran 35s on one pool worker and set the synthesis
phase's wall.

A match that starts mid-word implies one from the word's first letter, and
every match of these patterns ends on a non-word character, so a scan
never resumes mid-word: a lookbehind that rejects those starts skips only
attempts bound to fail. Over all 14,135 vscode JS/TS files the five
patterns return identical matches (positions and groups) before and
after, in 8.8s instead of 32.8s; the pass itself drops from ~9.8s to
~5.6s CPU standalone.

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

* perf(resolution): filter same-named candidates in one pass, cheap checks first

Interface members (#1638) and nested const-bound functions (#1669) made
common names — `editor`, `model`, `dispose` — carry thousands of nodes.
matchByExactName ran ten chained filters over that list for every
reference, copying it once per rule before the source-reading checks ran,
and the method-call strategies copied it again to put the call site's file
first before looking at only the class-like or same-file entries.

Each rule judges one candidate on its own, so the exact-name rules now run
as a single pass with the kind and language checks ahead of the lexical,
sealed-module and local-binding checks; the class strategies narrow to
owner kinds before ordering by file (the same nodes in the same order),
and the object-literal strategy, which keeps same-file holders only,
skips the reordering altogether. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): scope language-specific node scans in SQL

Eleven synthesis passes stream every node of a kind and keep only their
language's (Go, Java/Kotlin, XML, native, ArkTS, Nix, Erlang). On a
TypeScript monorepo the Go passes materialized all 113k vscode methods to
find two Go ones — goMethodContains alone went from 0.5s to ~2.9s on the
main thread after interface members (#1638) grew the method count and the
canonical ORDER BY (#1988) landed.

iterateNodesByKindIn adds the language predicate to the same query. Its
ORDER BY (file_path, start_line, id) is a total order, so it yields
exactly the nodes the callers kept, in the same sequence.

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

* perf(synthesis): run the synthesis-site index and source-gate scan beside the passes

Two serial main-thread steps landed with the incremental synthesis refresh
(#1988, #2038). The bulk-edge window rebuilt idx_edges_synthesis_site
before synthesis — its partial predicate runs json_valid over the
metadata of every edge (~1.5M rows, 2.7s on vscode) — and after the merged
insert synthesis re-read every file in the project to record its source
gates (~1.9s). Neither result is read until the index is finished: only
sync consults the index, and the gates depend only on the files.

The window's end now leaves that one index for later, and the resolver
builds it while the pool computes the passes, when the main thread was
only waiting; the source-gate scan runs in the same window. Both still
happen before resolution returns, whatever path synthesis takes, and a
crash in between is healed on the next open as before. On vscode
edge-index-recreate drops from 8.3s to 3.6s of serial time; the graph,
synthesis_inputs and the final index set are identical.

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

* perf(resolution): stop re-reading resolved targets and re-probing missing files

Every resolved reference now re-reads its target node several times —
the kind gate (#1796), the cross-family language gate (#1986), alias
following (#1482) and edge creation — through the query layer's
1,000-entry node cache, while targets recur ~5x (1.46M vscode edges onto
293k nodes). The language gate alone spent ~6s of single-threaded
resolution on vscode fetching whole nodes to read one field, and
fileExists re-probed the same missing candidate paths (every extension of
a `.js` specifier) from every importing file, now under a path.resolve
containment check (#1631).

The resolver keeps its own id → node cache and a memo of filesystem
probes, both in the stable window of its other caches and dropped by
clearCaches; the language gate returns early when the reference's
language has no code family (no target can cross one) and otherwise
memoizes each target's language. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): cheaper sealed-module and receiver-less call checks

isSealedModule read and decoded a candidate file — its source and every
node — to learn whether anything is exported, though nearly every module
does. It now asks one indexed EXISTS probe first and reads the source only
for files that export nothing (the conjunction is unchanged).

isReceiverLessCall (#1759, #1857) compiled a pattern per call and
end-anchored two more over the line's prefix, which V8 scans from the
start. It now matches the name literally with a sticky `\s*[(<]`, and reads
the prefix backwards: the last non-space character decides whether a
receiver precedes the call, and the word run ending there decides the
keyword exception — exactly what `[.\w$\])]\s*$` and
`\b(?:return|...)\s*$` accepted. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): pull passes onto workers heaviest-first; cheaper registry and tier scans

The pooled fan-out handed every pass out at once, so each worker held a
count-based share and a heavy pass finished only with its worker's whole
share (vscode's registry pass ended ~16s in). Workers now pull one pass at
a time, heaviest first; edges still merge by registry index, so the output
is unchanged. vscode synthesis: 14.5s vs 19.6-24.4s.

The registry pass retried its greedy name at every character of every
identifier; both scans now start a name only at an identifier's first
character or a `this.` (a mid-identifier resume after a `.method` dispatch
falls back to the unguarded pattern), and dispatch lines come from a
newline-offset lookup instead of splitting the prefix per match. Checked
match-for-match against the old patterns on every vscode JS/TS file plus
300k fuzzed strings. The tier pass reads a file's nodes only when a site
needs them. Standalone on vscode: registry 5.6s -> 4.5s, tier 5.1s -> 4.4s.

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

* perf(resolution): lean export indexes; no per-name regex compiles in field and local-binding checks

Three per-lookup costs the resolver pool paid once per worker:

- A file's export index decoded every node in the file and read its source,
  for each file an import reached, in every worker. It now reads only the
  exported rows (same `start_line, id` order); the default-export binding
  and names a local `export { a as b }` clause introduces are read on first
  use, from name-targeted rows. findExportedSymbol across vscode's workers:
  12.5s -> 3.7s.
- The `this.<field>` declaration scan compiled three patterns per field of
  every class and walked every class line. Each pattern needs the field as
  a whole `[\w$#]` token, so a class's lines are indexed by token once and
  three sticky patterns, compiled once, are tried at the field's token
  occurrences. Identical on 721k vscode (file, field) pairs; 56.7s -> 1.1s
  standalone.
- isLocallyBoundJsName searched the whole file with four patterns per name.
  Each can only start at a `const`/`let`/`var` or `function`/`class`
  keyword, an `=>`, or the `(` opening a list before the name, so the
  patterns (compiled once per name, sticky) are tried there. Identical on
  198k vscode (file, name) pairs; 68.1s -> 10.7s standalone.

Replaying every vscode (2.19M) and django (270k) reference single-threaded
gives the same result hash before and after.

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

* perf(resolution): prepare kind-filtered edge reads once; memoize the inherited this-member walk

- getOutgoingEdges / getIncomingEdges with a kind filter prepared a fresh
  statement on every call, a third of the read's cost; they are issued per
  node by supertype walks and the member-lookup synthesis passes. Each SQL
  shape is now prepared once per connection (dropped on rebind).
- The post-loop `this.<member>` pass walked the same supertypes for every
  reference, reading each one's contains edges and decoding members one by
  one until a name matched. Nothing is written until the pass ends, so each
  type's supertype edges and each supertype's callable members (by name, in
  edge order) are read once. vscode: 4.1s -> 1.0s.
- Express's mount pass decoded every JS/TS file to look for `.use(`; it now
  checks the raw bytes first (an ASCII needle survives UTF-8 decoding
  unchanged) and decodes only files that have it.

A full vscode init dumps the identical graph before and after.

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

* perf(resolution): fan the next batch out before inserting edges resolution never reads

The batch loop inserted every batch's edges before fanning out the next
one, so the pool sat idle through each insert. That ordering exists for
one reason: later batches walk supertype chains over the `extends` /
`implements` edges earlier batches resolved. Those are the only edges
resolution reads, and the prerequisite phase drains every such ref first,
so after it no batch makes one. A batch without them now fans the next
batch out first and inserts while it resolves: the same writes in the same
order, and nothing the workers read differs. The kinds live in one
constant, shared with the supertype walk.

vscode resolution, three interleaved rounds against the parent commit:
50.3-51.0s vs 56.8-59.1s wall (the pool's wait per batch halves; the
overlapped inserts cost ~7s more CPU). Full vscode and django inits dump
identical graphs before and after, and identical to main.

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

* perf(db): prepare the edge-endpoint existence check once

Every edge insert checks its endpoints with a 500-id IN query, a chunk at
a time, and prepared that statement afresh for each chunk. The batch loop
now runs these inserts on the main thread while the pool resolves, so the
full-size statement is prepared once; only a final partial chunk prepares
ad hoc (as deleteReferencesByRowIds already does). Same SQL, same result.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:57:18 +00:00
Colby Mchenry c3b1348065 fix(indexing): reject indexAll after a failed store instead of spinning (#1773) (#2079)
The in-order commit window waits for the commit cursor to advance before
dispatching more parses. After a store fails, flushOrdered returns at once and
the cursor never moves, so the backpressure loop spun on microtasks forever:
indexAll never settled, a core stayed pinned, and no timer in the process
could fire. It reproduces whenever a store error (a closed database, disk
full, an I/O error) arrives with more files left than the window holds.

In the test suite, which runs from source with a window of one, a test that
timed out mid-index and closed its graph in afterEach wedged the whole worker,
so a loaded Windows run never produced a report.

Stop waiting once a store has failed or the run was aborted, and destroy the
parse pool on that error path so its worker threads do not outlive the index.
The regression test runs the scenario in a child process under a hard timeout.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:14:10 +00:00
Colby Mchenry 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 Mchenry 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 Mchenry 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 Mchenry 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 Mchenry 7506dbff69 test(explore): guard named symbols in a pinned file past the node cap (#2064)
A file named by path is pinned: its first 300 symbols by start line enter
the gather. Past that cap the file splits into a head cluster of pinned
filler and, one cluster down, the named symbols the cap cut off, which
used to render nothing (`protocol.ts serializeAck`: 0 of 3 lines; the
same query unpinned: all of them).

#2062's cross-cluster hold-back fixes this shape without knowing about
pinning; its own fixture is an unpinned file. This fixture pins the
pinned entry path to it: red on tools.ts before #2062 (2 of 6), green on
main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:34:36 +00:00
Colby Mchenry 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 Mchenry 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 Mchenry 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 Mchenry e63fe2ec5b test(wal-heal): await the heal open() started instead of a wall clock (#2056)
`open() itself kicks off the heal` polled the WAL size for 30s and then
asserted it. On a loaded machine the heal was still running when the
deadline passed, so a slow heal failed as a size assertion, reading like a
regression (#1773). Await the pass open() started instead: it still fails
at once if open() stops kicking off the heal, and reports the heal's own
result rather than the clock's.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:55:37 +00:00
Colby Mchenry 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 49607201d6 fix(resolution): bind implements to the interface of a value+interface pair (#2055)
The inheritance target-kind gate (#1796) rejected any extends/implements
resolution whose target cannot be a supertype. VS Code declares every
service twice under one name, `export const IFoo = createDecorator<IFoo>(…)`
beside `export interface IFoo`, and the import resolver takes the first
export of that name, which is the value. The gate then dropped the edge
instead of taking the interface the same file declares.

Found by the pre-release v1.6.0 vs main comparison: 864 `implements` edges
lost on vscode (AccessibilityService -> IAccessibilityService and ~860 like
it), plus the ~5.8k interface-dispatch call edges synthesized from them.
The gate now moves a TypeScript constant/variable target to the one
same-named supertype in its file; with no such sibling it still drops the
edge. vscode: implements 4,139 -> 5,003 (v1.6.0: 5,436, 254 of them on
non-type targets; now 0), node count unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:55:29 +00:00
Colby Mchenry 1cbac0afaa docs(agents): update the Windows test baseline after #2053 (#2054)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:26:35 +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
Colby Mchenry 1d62a67d8a docs(changelog): add entries for the Windows fixes (#2052)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:37:25 +00:00
Colby Mchenry 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
Colby Mchenry 1b6be1c7d3 fix(watcher): ignore Windows access-only notifications (#1451) (#2050)
* fix Windows watcher false edits from NTFS atime

* fix(watcher): ignore Windows access-only notifications (#1451)

Check indexed size and mtime before recording Windows watcher edits.
Preserve scope reconciliation and fail open when metadata cannot be verified.
Add native Windows regression coverage and validate on Windows, macOS, and Linux.

Co-authored-by: JJordan0K <69581081+JJordan0C@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: JJordan0K <69581081+JJordan0C@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:10:46 +00:00
Colby Mchenry 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
Colby Mchenry d3671faf04 fix(installer): guide Windows shell users to PowerShell (#1294) (#2048)
Recognize MINGW, MSYS, and Cygwin and print the exact PowerShell installer command.
Keep Darwin/Linux behavior unchanged and exit non-zero before downloading.
Cover shell dispatch and real Windows ARM64 Git Bash behavior.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:10:20 +00:00
Colby Mchenry 1008b34e69 fix(bundle): ship a Windows Git Bash launcher (#1278) (#2047)
Ship an extensionless shell launcher alongside codegraph.cmd for Windows.
Use bundled node.exe with matching runtime flags and preserve arguments, exit status, and inherited host PID.
Validate on Windows 11, macOS, and Linux, including CLI/hooks and archive contents.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:10:09 +00:00
Colby Mchenry 7af240923d test(vitest): run pool workers with the WASM runtime flags (#1779) (#2046)
A vitest pool worker is the one launch path that never received
`--liftoff-only`, so the suite compiled tree-sitter grammars on the
turboshaft tier. Once ~640 extraction tests have warmed a grammar function
up, its background tier-up job (Turboshaft LoopUnrollingPhase, symbolized
from the native stack) exhausts a compiler Zone and aborts the worker:
`Fatal process out of memory: Zone`, which vitest reports only as "Worker
exited unexpectedly" with the file's remaining tests unrun.

Pass WASM_RUNTIME_FLAGS through poolOptions.forks.execArgv, so the test
process matches the bundled launcher, the CLI re-exec and refresh-launcher.
V8 flags are process-global, so parse worker threads are covered too.

Linux x64, Node 24.15, `__tests__/extraction.test.ts` (655 tests):
without the flag 3 of 4 runs died at the same test (639 passed);
with it 2 of 2 passed, in 19–24s instead of 34–41s. Neither
`--no-wasm-loop-unrolling` nor `--no-wasm-dynamic-tiering` prevents it.


(cherry picked from commit 565ec815e6)

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 15:01:15 +00:00
Colby Mchenry 594bad7ec4 test(writer-lock): hold the lock with a live child, not a pid assumed alive (#2045)
The live-holder case wrote `pid: 1` and leaned on its own comment, "On Linux,
PID 1 is almost always alive". Windows has no PID 1, so tryAcquireWriterLock
read the holder as dead and acquired — the correct behaviour for a dead
holder, and a failing assertion. The platform-specific part was the fixture,
not the lock.

A parked child process is alive on every platform, and not stealing from a
live holder is what the lock actually promises, so the case now tests the
promise rather than a number that happens to name a running process. The child
is reaped in afterEach.

Fixes #1752.

(cherry picked from commit bee140288d)

Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
2026-09-27 15:01:02 +00:00
Colby Mchenry 0a7321954c docs(changelog): add entries for the final wave of September 27 fixes (#2044)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:17:28 +00:00
Colby Mchenry 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
Colby Mchenry 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 Mchenry 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 Mchenry 9be3f1a8a2 fix(resolution): keep unknown Python import members unresolved (#2040)
Bisect identifies 5eaa6fed (#2034, #1820) as the first bad commit: its Python named-import fallback resolves task.delay() to the task function itself, hiding the queue effect in API steps. Require member resolution before returning a named Python import's target for attribute access.

Keep direct calls, callbacks, constructors, and known members working. Add absolute/relative aliased-import regression coverage without changing the existing steps assertions.

Validation: git bisect with preserved steps test, tsc and copy-assets at every step, CODEGRAPH_KERNEL=0 throughout bisect. 338 tests pass across ui-steps-api-servers, function-ref, rebuilt kernel-tsjs-parity, resolution, and ui-effects. Issue #1820 repro and tsc --noEmit pass.
2026-09-27 14:02:38 +00:00
Colby Mchenry 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
Colby Mchenry 71985f2620 fix(db): guard synthesis metadata JSON and keep calls ahead of imports (#2038)
* fix(db): guard synthesis metadata and migrate the expression index

Bisect identifies 2a2f71ec (#2033) as the malformed JSON regression. Guard index expressions, ownership predicates, wiring lookups, and the legacy Go backfill; schema v11 replaces existing v10 indexes. Cover malformed base edges, staged publication, upgrade replay, and indexed range lookup.

* fix(db): preserve call precedence in deterministic edge traversal

Independent bisect identifies 2a2f71ec (#2033): source-first ordering puts importing file IDs ahead of function callers and consumes the default caller limit. Restore kind-first incoming and outgoing traversal with deterministic endpoint/position ties. Keep both original regression tests unchanged and cover filtered, cached, and provenance-filtered queries across reverse insertion order.

Validation: 25 test files passed (564 tests, 3 skipped), including security, MCP caller truncation, sync, migration, graph, and DB/query coverage; npx tsc --noEmit passed. Both bisects rebuilt tsc and copied assets at every step with CODEGRAPH_KERNEL=0 to avoid using the HEAD kernel on historical extraction code. Final validation used the normal kernel setting.

* chore: no changelog entry for an unreleased regression

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:54:58 +00:00
Colby Mchenry f0b4b262f1 docs(changelog): add entries for the September 27 issue fixes (#2037)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:48:53 +00:00
Colby Mchenry 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 Mchenry fa25883ab0 fix(cpp): resolve local constructors and macro visibility (#1839) (#2035)
* fix(cpp): macro invocations are not calls; local objects call their constructor

A function-like macro invocation (`TRACE_POINT(1)`) parses as a call, and
the resolver bound it to a same-named free function in another translation
unit — a caller and a callee that never existed (#1838). And local object
initialization (`Widget w;` / `Widget w(1);` / `Widget w{1};`) reached the
class node instead of a constructor, or nothing at all for the default
form (#1839).

Extraction (wasm walker and the Rust kernel, in parity):
- every `preproc_function_def` becomes a `constant` node whose signature
  is the directive, so the index knows which names are macros;
- a C++ declaration emits one `calls` ref per constructed object, shaped
  `ns::T::T/<arity>`; pointer, reference and function declarators,
  `extern`, and array element braces construct nothing; the #1035
  `instantiates` ref is unchanged;
- constructors carry their parameter list as the signature.

Resolution:
- `cpp-macro-visibility.ts` walks the translation unit (root file plus
  in-repo includes, each file once) over per-file summaries of
  function-like defines, undefs and includes, evaluating what each file
  can decide itself (literals, its own defines, the include-guard idiom).
  A call whose name is definitely a macro at that point, or a name that
  only macros bear, resolves to nothing. A macro that exists in one build
  configuration only (CMSIS's `__DSB()` under the ARM compilers, a
  target's HAL compat header) keeps the call to the function the other
  configuration compiles; a wrapper macro that calls its own name is how
  that function gets called and hides nothing.
- `cpp-constructor.ts` resolves the constructor refs: the type in the
  lexical namespaces of the call site, then the single overload whose
  parameter count admits the arguments; two admitting overloads, an
  initializer_list overload, or an aggregate yield no edge.
- a `#define` constant is never a `calls` target and does not count
  toward the same-name ceiling.

Measured on a betaflight tree (4,017 files, wasm arm): 153 `calls` edges
lost — all fabricated (`MIN`/`MAX` bound to a CLI enum member, macros
misparsed as functions, a cross-vendor `SWAPBYTE`) — and 34 constructor
edges gained; indexing 34s vs 30s.

Fixes #1838
Fixes #1839

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

* fix(cpp): resolve local constructors and macro visibility (#1839)

Local construction lacked constructor calls, while macro invocations could bind unrelated functions.
Resolve constructors using lexical types and overload signatures, preserving separately declared defaults and array element construction.
Evaluate macro directives in translation-unit order, respecting reinclusion, guards and pragma once.
Mirror extraction in Rust and WASM, preserve type dependencies, and distinguish them in CLI output.

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>

---------

Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 13:46:34 +00:00
Colby Mchenry 5eaa6fedd5 fix(extraction): preserve receivers for method values (#1820) (#2034)
* fix(extraction): record callers for methods passed as values (#1820)

pool.submit(obj.method), thread_pool_exec(self.store.fetch), and Go
Submit(c.store.Fetch) produced no callers/impact edge because only
obj.method(...) call expressions were captured. Member values are now
emitted as *.method function-refs and resolve unique-or-drop
(same-file first). Two methods of the same name still produce no edge.

Go go c.store.Fetch(ids) remains a calls edge (it is a call expression).

EXTRACTION_VERSION 27: MCP daemon catch-up rebuilds a stale index on
start so the watcher does not keep serving the hole after upgrade.

* fix(resolution): do not let test mocks veto #1820 method-value callers

A unique production method plus a unit-test double of the same name
was treated as ambiguous, so retrieve_vision_vectors still looked
callback-invisible on a real repo. Prefer the non-test file when
exactly one production target remains.

* fix(resolution): prefer a real override over a NotImplementedError base

#1820 unique-or-drop treated DocStoreConnection.delete_payload_fields
(the ABC stub) and QdrantConnection.delete_payload_fields as ambiguous,
so gc_stale_member_stamps stayed invisible. Skip short not-implemented
stubs when exactly one real implementation remains.

* fix(extraction): preserve receivers for method values (#1820)

Python attributes and Go selectors used as callback values were dropped or reduced to bare method names.
Preserve receiver paths in native and WASM extraction, resolve through type/import scope, and reject ambiguous or noncallable targets.
Cover callback, assignment, collection, and query behavior, with extraction version 27 signaling the required reindex.

Co-authored-by: Josh66 <info@aska-systems.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:42:27 +00:00
Colby Mchenry 2a2f71ec41 fix(sync): refresh synthesized edges after incremental changes (#1988) (#2033)
* fix(sync): refresh synthesized edges after incremental changes (#1988)

Scoped sync skipped synthesis and retained obsolete dispatch edges.
Refresh all synthesis passes after base resolution when dependencies or source patterns change.
Stage and atomically replace owned edges, preserve ordinary edges, and retry interrupted refreshes.
Stabilize traversal order and migrate existing indexes for rebuild convergence.

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

* fix: preserve structural Go containment during synthesis refresh

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:41:49 +00:00
Colby Mchenry 9649ce1bd6 fix(resolution): reject unrelated cross-language matches (#1986) (#2032)
* fix(resolution): stop expression-receiver calls from binding to unrelated methods

A method called on an expression receiver reached the resolver as the bare
method name, and exact-match bound it to any project method of that name:
`(await list()).map(...)` in TypeScript onto a UI adapter class's `map`
(0.9), and `v.iter().map(...)` in Rust onto the same TypeScript method
across languages (0.5).

TS/JS extraction (mirrored in the kernel): look through receiver wrappers
that keep the same object — parentheses, `!`, `as`/`satisfies`, `<T>x`,
`await` (including the grammar's `(a && b)!` parse of `a && b!`) — so
`x!.m()` is `x.m` and `(await f()).m()` is `f().m`; any other receiver with
no static type (`(a ?? b).m()`, `f().list.m()`, `arr[0].m()`) emits nothing
instead of the bare name. `this`/`super` (and chains rooted at them),
`window.*` chains and `new C().m()` keep their existing bare form.

Resolution: a bare-named call may cross a language family only through the
C ABI (a C/C++ symbol, or a free function a C/Objective-C/Swift caller
reaches, e.g. a cgo `//export`). A class member of another family, or a
function/class/variable outside that pair, is rejected as the candidate a
strategy commits to (exact-match single/best and fuzzy), never filtered out
of the pool, so no lone survivor is manufactured.

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

* fix(resolution): reject unrelated cross-language matches (#1986)

Name-based calls, constructors, and inheritance could select unrelated foreign-language symbols.
Apply a shared interoperability policy to selected results without manufacturing replacement matches, including JSX/Vue synthesis.
Preserve framework exports and ABI-backed calls, building on PR #1944's TS/Rust expression-receiver fixes.
Validate with both repro scripts, 529 targeted tests, kernel/wasm parity, and clean TypeScript checks.

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 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:40:31 +00:00
Colby Mchenry 17d09bf506 fix(resolution): resolve object-literal function references safely (#1932) (#2031)
* fix(resolution): resolve same-file calls through object-literal aliases (#1932)

`api.getUser()` where `api` is `const api = { getUser }` (or
`{ getUser: fetchUser }`) and the function is declared outside the literal
resolved to nothing in the literal's own file: the #1573 containment lookup
only finds members defined inside the literal, and the binding-following
alias path existed only on the import side, so same-file callers were
missing from callers / impact / blast radius.

- Extract the member-binding read into `objectLiteralMemberBinding`, which
  splits the literal into its own top-level members on comment- and
  string-blanked source, so a nested object, a member body, a comment or a
  string never donates a binding, and `{ fn: 1 }` names nothing. The import
  path now uses it too.
- In the same-file object-literal receiver strategy, fall back to that
  binding: a symbol of the file, else one of its imports, skipped when a
  parameter or nested declaration shadows the name where the literal is
  written.

Resolution-only change; extraction output (and kernel parity) unchanged.

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

* test(resolution): a literal wrapped in Object.freeze still names its members (#1932)

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

* fix(resolution): resolve object-literal function references safely (#1932)

Same-file object-member calls only found functions defined inside the literal.
Share lexical binding resolution across same-file, import, and alias-forwarding paths.
Respect exact source columns, top-level membership, shadowing, and property overwrites while preserving wrappers and inline members.
Add JavaScript and TypeScript regressions and verify kernel/WASM parity.

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 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:39:18 +00:00
Colby Mchenry 86708bf519 fix(extraction): preserve symbols with colliding same-line IDs (#1349) (#2030)
Same-kind, same-name declarations on one line shared an ID and silently overwrote each other in SQLite.
Use per-extraction collision tracking to append a UTF-16 column only to later distinct declarations.
Apply the same rule across wasm, Rust language walkers, Liquid, and CFML while preserving file IDs and edge endpoints.
Add persistence, call-edge, identity-vector, and Unicode parity regressions.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:35:19 +00:00
Colby Mchenry e27d6da6b2 fix(scala): resolve companion inheritance to type definitions (#1824) (#2029)
* fix(scala): resolve extends/with to the trait, not its companion object

The Scala trait + companion object idiom puts two same-named symbols in
one file. Both were indexed as kind 'class', so an 'extends X' reference
tied between the trait and its companion and the winner was arbitrary.
When the companion won, every subtype was detached from the trait's
inheritance chain, and impact analysis on a widely-used trait stopped at
depth 1.

- classify Scala object_definition as 'module' (a singleton value, not a
  type - 'extends' can never target it)
- add 'module' to the classifyClassNode contract and handle it in the
  extractor dispatch
- in name matching, bias extends/implements references toward actual
  type definitions and penalize 'module' candidates, so the trait wins
  the tie deterministically

Regression test: trait + companion in one file, subclass in another -
the extends edge must land on the trait and impact must reach the
subtype through it.

* fix(scala): resolve companion inheritance to type definitions (#1824)

Scala objects and their companion types could tie during inheritance resolution, disconnecting subtype impact paths.
Classify objects as modules in both extraction engines and reject Scala singleton inheritance targets across resolution strategies.
Preserve object method lookup and inherited receiver methods, with declaration-order, impact, and native/WASM parity regressions.

Co-authored-by: Hubert Tarnacki <hubert.tarnacki@comarch.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Hubert Tarnacki <hubert.tarnacki@comarch.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:32:17 +00:00
Colby Mchenry b65e05b98f fix(resolution): require receiver evidence for built-in method calls (#1987) (#2028)
* fix(resolution): resolve this.#field.method() on the field's type (#1987)

A call through an ES private field fell outside the `this.<field>` shape
(#1496): the extractor accepted only a property_identifier, so
`this.#items.add(x)` was emitted as the bare `add` and exact-matched
whichever project method shared the name, often the calling method itself.
The extractor and its kernel mirror now keep `this.#items.add`, and the
resolver matches that shape and reads the field's type off the class
declaration. The field regexes open with a lookbehind instead of `\b`,
which never matches before `#`. A builtin or external field type
(`new Set()`, a `Map`, an array) now yields no edge.

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

* fix(resolution): require receiver evidence for built-in method calls (#1987)

Unknown JavaScript receivers could match unrelated methods by name or class-name similarity.
Require validated receiver evidence for built-in method names while retaining typed, imported, object-literal and class resolution.
Preserve private-field receivers in TypeScript and Rust extraction and keep public/private field declarations distinct.
Build on PR #1990 with regression coverage across all four JS/TS variants and kernel/WASM parity.

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 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:30:02 +00:00