Skip to content

Sync upstream main f72c9d2e while preserving Pinable customizations - #8

Merged
PinableAgents merged 383 commits into
mainfrom
sync/upstream-2026-11-08
Nov 8, 2026
Merged

PinableAgents merged 383 commits into
mainfrom
sync/upstream-2026-11-08

Conversation

@PinableAgents

@PinableAgents PinableAgents commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Merge 379 upstream commits from colbymchenry/codegraph main (f72c9d2), preserving both parents and Pinable customization history.
  • Preserve G6 workbench, Chinese UI, theme preferences, multi-project workspace, legacy Pinable schema repairs, and Desktop runtime packaging/launcher contract.
  • Resolve migration/API/lockfile conflicts, move all Pinable UI test projects into Vitest 4 config, and remove obsolete --minWorkers CI flags.
  • Keep bounded cycle details import-only, matching upstream circular-import semantics, with regression coverage; update legacy DB tests for schema v16.

Validation

  • Prior source candidate passed full engine/UI, native kernel, all six native runtime smoke checks and artifact collection.
  • The authorized version bump updates root package, UI package and all lockfile version fields to 1.6.3. Desktop SemVer upgrade ordering from 1.6.2 and CLI --version verified locally.
  • Application/viewer and UI library rebuild passed; runtime producer/publisher Python tests: 32 passed.
  • Latest head 9bbda98 is rerunning complete CI and six-platform runtimes.
  • The initial fork-suffixed version was rejected by the daemon replacement safety contract. Final 1.6.3 is plain stable SemVer, verified to upgrade from 1.6.2 in both Desktop and daemon logic; no safety comparator was relaxed.
  • Latest remote tree matches local tree fc3dbad83bf2648c877f692b029ce21f709a08c0.

Release safety

User authorized the version increase and merge to main. Do not merge until required checks and six-platform runtime verification pass for the latest head. Main is unchanged. Merging into main automatically publishes stable runtime 1.6.3 after checks (GitHub prerelease=false). This version sorts above installed 1.6.2 in the current Desktop updater. No consumer lock or existing runtime assets are changed here.

Merge with Create a merge commit, never squash/rebase, so upstream ancestry is retained.

colbymchenry and others added 30 commits October 22, 2026 01:59
…es it (colbymchenry#2771)

Root cause: the TOML helpers behind Codex's install, refresh and uninstall
found CodeGraph's table with two literal searches, `[mcp_servers.codegraph]`
at the start of the file or right after a newline, and detection used a
plain substring check. Some valid ways of writing that table went unseen:
- an indented header, which formatters that indent sub-tables write;
- spaces inside the brackets or around the dot;
- quoted or escaped keys.
Install then appended a second `[mcp_servers.codegraph]`. A table defined
twice is invalid TOML, so Codex loaded none of its config. Install and
refresh also appended a table after a definition written with dotted keys
or as an inline table, or under an `mcp_servers` that is an inline table or
an array of tables (both invalid too). The literal search also matched
header-shaped text inside a multiline string and spliced into the string.
Uninstall left the user's original table, a `[mcp_servers.codegraph.env]`
sub-table (a server with no command), and dotted or inline definitions in
place. CRLF files came back with mixed line endings.

Fix: a small structure scan of the file finds every statement (table
header, array-of-tables header, key/value). It decodes key paths the way
TOML reads them (bare, "basic" with escapes, 'literal', spaces around
dots), tracks the table each key sits in, and folds the lines of a value
that spans several into the key that opened it. On top of that scan:
- The `[mcp_servers.codegraph]` table is found however its header line is
  written. Install and refresh replace the first one in place and remove
  any further copies, so a file the old bug broke parses again.
  Sub-tables such as `.env` are kept.
- A block ends at its last key, so a comment heading the next table stays
  with that table.
- Uninstall removes the table, every copy, its sub-tables, and
  definitions written with dotted keys or as an inline table, as whole
  statements.
- An entry the user wrote with dotted keys or as an inline table is left as
  written, with a note.
- Where a table can't be added (`mcp_servers` set as an inline table, a
  value or an array of tables), install refuses with a clear error and
  changes nothing.
- An entry inside an inline `mcp_servers` table is reported by uninstall,
  never edited. The uninstaller now prints such notes.
- Everything is written in the file's own line breaks, and the existing
  block is compared regardless of them.

Validation:
- 25 new tests in installer-targets.test.ts:
  - 20 on the TOML helpers: 8 header forms, CRLF, the old bug's duplicate,
    look-alikes (other names, header-shaped text inside strings and
    arrays), sub-tables, comments, 4 dotted/inline/array definitions, 2
    shapes where a table can't be added, an entry inside an inline table;
  - 5 on Codex install, re-install, refresh and uninstall end to end.
  With main's toml.ts, codex.ts and installer index.ts put back, all 25
  fail: 24 on the behavior itself, and one, a header with a trailing
  comment, only on the new detection helper, since main already handled
  that form. The 13 existing TOML tests pass on both.
- The real CLI on this Windows machine against private homes: `install
  --yes`, a second `install --yes`, `install --refresh` and `uninstall
  --yes --keep-cli`, for 23 config.toml shapes, each result parsed with
  Python's strict tomllib.
  - main (0abae11): 46 of 92 results were invalid TOML (15 of 23 right
    after install), the entry was still there after uninstall in 16
    shapes, and 8 results had mixed line endings.
  - The fix: 0 invalid and no mixed line endings. The entry remains after
    uninstall only inside an inline `mcp_servers` table, which uninstall
    reports. Install refuses with exit 1 and an unchanged file for the two
    shapes where a table can't be added.

Fixes colbymchenry#2562

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ymchenry#2773)

In C, a function declared with its return type written as a macro call,
like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or
libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node.
blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call
that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`.
Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside
redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is
often the return type. Blanked together with its argument, it left
`cJSON_Print(...)` and a block, which parse as a call statement. The
function was lost, the calls in its body went to the file, its locals
and parameters became file-level variables, and a header's prototype
read as a call from the header.

C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter
reads it as a macro_type_specifier, when all of these hold:
- it is in column 0;
- its one argument reads as a type;
- only a function declarator follows: a name, its parameter list,
  `__attribute__` or ALL-CAPS attributes, then `{` or `;`;
- the line right above leaves no declaration open. jemalloc writes
  `size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`.
C++ is unchanged.

Validation:
- New __tests__/c-macro-return-types.test.ts (3 tests: the issue's
  reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT,
  REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the
  blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel
  and with CODEGRAPH_KERNEL=0.
- Pre-parse output compared file by file on 12 repos. The only changes
  are macro text kept where main blanked it; no file's parse-error count
  got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1,
  APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq,
  redis (with jemalloc), sqlite and curl, and on the C++ controls fmt,
  glm, json and abseil-cpp.
- A/B on main 6bd217a, graph dumps:
  - cJSON: +92 functions; 7 functions named `(void)` and 100 phantom
    variables and constants gone; edges 2,882 -> 3,750.
  - libgit2: +403 functions; 3 `(void)` functions and 293 phantoms
    gone; 37 calls that went to a phantom parameter gone; edges 81,984
    -> 85,834.
  - APR: +1,551 functions; 7 bogus ones and 1,723 phantoms gone; edges
    16,163 -> 23,278. 39 calls now reach the real APR_DECLARE definition
    instead of its EBCDIC stub. 283 portable calls now see the
    OS/2 and Windows copies beside the Unix one: 278 go to OS/2 at 0.4
    (was Unix at 0.9), and 5 from win32 code go to win32.
  - expat: 22 phantom parameters gone.
- kernel-parity: 0 diffs on both arms; deferred files fall from 33 to
  31 (cJSON), 218 to 161 (APR) and 208 to 128 (libgit2).
- Cost: medians of 3 interleaved init runs: cJSON -1.6%, expat -4.3%,
  libgit2 -6.9%, APR +18.4%. APR's graph is 44% larger. The pre-parse
  itself is unchanged (APR 262 vs 257 ms, libgit2 598 vs 599 ms).

Fixes colbymchenry#2470

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sets line (colbymchenry#2774)

Root cause, two parts:

1. Install and refresh appended `mcp-codegraph` to
   `platform_toolsets.cli` in `~/.hermes/config.yaml`. It was never
   needed. Hermes loads every enabled MCP server on a platform whose list
   names none (hermes_cli/tools_config.py `_merge_mcp_servers`, a rule
   present back in May 2026 when the target was written). The name
   itself is harmful:
   - Hermes up to 0.22.2 rejects it at startup ("Unknown toolsets").
   - Hermes 0.22.4 maps `mcp-<server>` onto the server, so the list
     becomes an MCP allowlist and every other server disappears from CLI
     sessions.
   A user who deleted the line got it back on the next upgrade.
2. When the `mcp_servers.codegraph` block differed from the template, it
   was replaced wholesale. That reverted an absolute command path, the
   timeouts, `enabled: false` and `env`. The line-based YAML code also
   missed some shapes:
   - a codegraph key written on one line or quoted, which got a second
     codegraph key;
   - a 4-space-indented `mcp_servers`, which came out unparseable;
   - a one-line `mcp_servers: {…}`, which got a second top-level key, and
     Hermes's PyYAML reader then dropped the user's servers.
   CRLF files came back LF.

Fix:
- CodeGraph never writes `platform_toolsets`.
- A `- mcp-codegraph` line an earlier install left in
  `platform_toolsets.cli` is removed, or becomes `- codegraph` when the
  list is an allowlist naming other MCP servers (the spelling every Hermes
  version reads as the server). An allowlist that leaves CodeGraph out
  gets a note, not an edit.
- An existing `mcp_servers.codegraph` entry is the user's: nothing in it
  changes. A block entry missing `command` or `args` gets just those, at
  its own indent.
- A new entry goes in at the indent its siblings use, and a codegraph key
  is found however it is written. `mcp_servers: {}` becomes a block.
- A one-line mapping that holds other servers makes install refuse with a
  clear error and change nothing. One that holds codegraph is left as
  written, with a note, and uninstall says it can't remove it.
- The file keeps its line breaks, and is written only when something was
  edited.
- Uninstall also removes a `- codegraph` the refresh may have written.

Hermes references (NousResearch/hermes-agent v0.22.4, 64dbae64):
- hermes_cli/tools_config.py `_get_platform_tools` / `_merge_mcp_servers`
  ("mcp-<server> names the same server as its bare alias: either spelling
  selects only that server").
- hermes_cli/toolset_validation.py `saved_toolset_resolver`.
- website/docs/reference/mcp-config-reference.md (command, args, env,
  enabled, timeout, connect_timeout).
- hermes_cli/tools_config.py at 1b9c539c (2026-05-17) for the May 2026
  rule.

Validation:
- installer-targets.test.ts: 13 new Hermes tests. 3 existing tests that
  asserted the `mcp-codegraph` write are updated; the colbymchenry#456 same-indent
  list case now proves the old line is removed cleanly. With main's
  hermes.ts put back, those 16 fail, all on behavior. The 7 other Hermes
  tests pass on both.
- The real CLI on this Windows machine against a private HERMES_HOME:
  `install --yes`, a second `install --yes`, `install --refresh` and
  `uninstall --yes --keep-cli`, for 14 config.yaml shapes. Each result
  was loaded with PyYAML (the reader Hermes uses), checked for duplicate
  keys, and run through a re-implementation of the cited selection rule.
  Of 42 results:
  - main: 2 unparseable, 10 with duplicate keys, 26 carrying
    `mcp-codegraph`, 10 where Hermes 0.22.4 would load only codegraph in
    CLI sessions, 3 converted CRLF to LF, and the customized entry
    reverted.
  - The fix: 0, 0, 0, 0, 0, the customized entry kept byte-for-byte, and
    1 install refused by design (a one-line `mcp_servers` holding other
    servers).

Fixes colbymchenry#2485

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry#2776)

In Go, a function used as a value, like `apply(run)`, was recorded once
per function and name, at its first use. Since PR colbymchenry#2658 the resolver drops
a use that a local or parameter of that name shadows. When a function's
first use was the shadowed one and a later use, outside the local's scope,
was the package function, that later use had no reference left, and the
function lost its references edge to the package function (an inner-block
`run := 2`, a closure's `func(run int)`, a case clause, an if header or a
range variable, then `apply(run)`). Issue colbymchenry#2596 predicted this.

Fix, in both extractors (function-ref.ts with tree-sitter.ts's
flushFnRefCandidates, and the kernel's go.rs flush_fn_ref_candidates):
- A Go function that binds the name itself records one use per scope
  instead of one per function. Bindings are a func literal's parameters,
  :=, var, const, range and type-switch variables and a select case's
  value. Scopes are Go's: a block, an if/for/switch/select statement's
  implicit block and each case clause. Within one scope a local declared
  before the first use also covers every later use, so the first use
  still speaks for the scope.
- The function's own receiver, parameters and results keep a single
  reference: they cover the whole body.
- A func literal at file scope is its own unit.
- Every other function is unchanged. The scope and enclosing function are
  read from the tree only for names a function uses at least twice, after
  the name gate (reading them for every container made kernel extraction
  of prometheus 2.4x slower).

Validation:
- go-shadowed-function-value.test.ts: 14 cases, kernel and wasm, LF and
  CRLF. On main's sources and kernel 10 fail; with the fix 14 pass.
  Per-scope keys without the binding check, and dropping range
  variables, case clauses or func literal parameters, each fail 3.
- Kernel parity, Go: gin 99/99, etcd 1065/1105 (40 deferred, as main),
  prometheus 746/746, kubernetes 17779/17952 (173 deferred, as main),
  0 files with diffs.
- A/B against 6bd217a (fresh indexes, each arm with its own kernel):
  gin 0f09c3a9, etcd bf9cadf0, prometheus 017fcf2e and kubernetes
  006cdf87 keep identical nodes and edges. The only change is 0, 4, 13
  and 163 more function_ref rows, all failed: per-scope uses of a local
  (all 17 on etcd and prometheus and 36 on kubernetes read by hand).
  express and flask dump byte-identically.
- Cost: kernel extraction of prometheus 2.90 s vs 2.90 s, wasm 6.06 vs
  6.07 s; kubernetes 34.9 vs 34.9 s; prometheus index medians
  12.35 s / 49.1 s CPU vs 12.45 s / 48.3 s over 4 interleaved rounds.

Fixes colbymchenry#2596

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r, even home (colbymchenry#2777)

codegraph upgrade told a global npm install from a local one by whether
the running file sat under the current folder. npm's global folder is
often inside the home folder (Windows %APPDATA%\npm, nvm, fnm,
~/.npm-global), so run from home every such global install was taken
for a local one: npm ran without -g, the global copy stayed on the old
version and a second copy landed in the home folder. The same test sent
a project's own install to npm -g when upgrade ran from one of its
subfolders or from outside it, and installed into the parent folder
instead when it ran from one.

The scope now comes from npm's layout around the outermost node_modules.
A global launcher beside it (<prefix>\codegraph.cmd on Windows,
<prefix>/bin/codegraph next to <prefix>/lib on POSIX) means global. A
project around it (node_modules/.bin/codegraph or a package.json) means
local, and npm then runs in that project, wherever upgrade was run from;
the version npm left there is read back, with a warning when it did not
change. A pnpm, yarn or bun install (a .pnpm layout, or that manager's
lockfile beside node_modules) is named and given that manager's
command, since npm run in its folder would only add a second copy.

Validation: on Windows, the real CLI was run with a private USERPROFILE,
HOME, APPDATA and LOCALAPPDATA (npm config get prefix = the private
AppData\Roaming\npm) against installs laid out the way npm lays them
out, with a fake npm first on PATH that logs its command and folder.
main ran the wrong npm command in 6 of 9 cases (a global install under
%APPDATA%\npm or ~\.npm-global run from home, and project installs run
from a subfolder, from home, from the drive root or from another
project), the fix ran the right one in 9 of 9; an npx cache run and the
bundled runtime are detected the same by both. On main's code 14 of the
91 upgrade tests fail on assertions (12 new ones, and 2 existing ones
that now also expect the project folder); all pass with the fix.

Fixes colbymchenry#2477

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ref]` are indexed (colbymchenry#2778)

The Pascal grammar's parameter rule takes no RTTI attribute, so a
declaration like `procedure WithRef(const [ref] AValue: Integer; ...)` or
DelphiMVCFramework's `function CreatePerson([MVCFromBody] const Person:
TPerson)` broke the parse at the `[`. The method was lost, the parameter
after the attribute became a field of the class, every member declared
after it went missing until the next class, and in an implementation the
routine's locals became unit variables and its calls were credited to the
file node.

The Pascal extractor now has an offset-preserving preParse that blanks an
attribute group at the start of a routine parameter, before or after its
modifier (const/var/out/constref), in any routine header, procedural type
or anonymous method (`operator` counts only after `class`, since Delphi
doesn't reserve it). It is a linear hand-written scanner that skips
strings, comments and directives, reads generic and qualified routine
names, and leaves every other bracket alone (set defaults, array types,
member and method attributes, property index lists, code). A new
LanguageExtractor flag, signatureAsWritten, lets Pascal signatures read
the source as written, so `(const [MVCFromBody] Person: TPerson)` keeps
its attribute instead of showing blank space. Only Pascal sets the flag.

Validation:
- New __tests__/pascal-param-attributes.test.ts (8 tests, the issue's unit
  included): 6 fail on main's wiring, all pass with the fix.
- A/B against main 5a6917b, both arms built from their own sources:
  SynEdit, HeidiSQL and DWScript (2,497 files) are byte-identical, as is
  mORMot2 (its two `[ref] const` signatures keep the attribute).
  DelphiMVCFramework (32 files changed by the preParse, 150 attributes):
  4 nodes recovered (2 methods, a function and a constant), 10 removed,
  all misparses (9 method locals hoisted to unit variables and a
  parameter read as a field), 18 methods get a correct signature, end
  line and return type; edges -39 +90: 29 calls move from the file node
  or the wrong method to the method making them, 60 new edges come from
  bodies and declarations main lost, 10 contains edges went with the
  misparsed nodes. Every changed edge was triaged.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm with
  its own preParse) are byte-identical in dump-graph.
- Cost: the preParse takes about 4 ms per MB (95 ms on mORMot2's 25 MB);
  index time is within noise (3 interleaved rounds, medians: HeidiSQL
  6.53 to 6.47 s, mORMot2 16.7 to 17.2 s, DWScript 16.3 to 16.1 s wall).
- The shipped scanner matches the prototype used for triage on all 4,578
  Pascal files of the five repos, with lengths and newlines kept.

Fixes colbymchenry#2545

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olbymchenry#2781)

The Pascal grammar's for-in rule takes a plain variable, so a loop that
declares its variable inline (Delphi 10.3 and later), like
`for var lItem in AList do lItem.DropIfEmpty;` or
`for var lHandle: THandle in lHandles do`, became an ERROR node. A
paren-less call in its body lost its statement shape and reached no
graph, so the routine was missing from the callee's callers.

The Pascal preParse added for issue colbymchenry#2545 now also blanks the inline
`var` and the loop variable's `: Type`, offsets kept, so the loop parses
as the grammar's `for x in` form. The type runs to the `in` at nesting
depth 0, so generic and dotted types are whole; `for var i := 0 to`,
which the grammar already parses, and `for var` inside comments or
strings are left alone. The resolver still types the loop variable: it
reads a receiver's declaration from the source as written, so
`lHandle.Close` reaches THandle's Close, not a namesake.

Validation:
- New __tests__/pascal-for-in-var.test.ts (6 tests, the issue's unit
  included): 5 fail with main's pascal-preparse.ts, all pass with it.
- Every textual `for var x in` in the corpus is blanked except 9 that sit
  in comments or strings (DelphiMVCFramework 47 of 49, DWScript 89 of
  96), no other text is touched, and no file parses worse.
- A/B against main 929c65c, both arms built from their own sources:
  SynEdit, HeidiSQL and mORMot2 are byte-identical. DelphiMVCFramework:
  errors in the 20 changed files drop from 57 to 10, edges are
  identical (its loop bodies hold parenthesized calls, which main's
  error recovery already kept), +1 unresolved ref for the VCL's own
  GetControls in a loop's iterator. DWScript (2,497 files, mostly
  DWScript-language test scripts): edges -41 +30, every one triaged.
  The two real-library files gain correct calls; one paren-less
  right-hand-side read main kept only through error recovery
  (`var v := FIter.CollectInteger`) is gone until issue colbymchenry#2514's
  paren-less calls land. The rest are script files: 7 new calls inside
  the loops, one call moved from the file node to its routine, one stray
  top-level statement no longer credited to the routine error recovery
  had stretched over it, 21 file-level guesses error recovery made on one
  script's stray top-level statements, and 8 garbage nodes per
  enumerations script whose multi-line names now hold the blanked text.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are
  byte-identical in dump-graph.
- Cost: no measurable change; index time medians of 3 interleaved
  rounds: HeidiSQL 5.50 to 5.49 s, mORMot2 13.96 to 13.94 s, DWScript
  13.98 to 13.94 s wall.

Fixes colbymchenry#2546

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e chain reaches (colbymchenry#2782)

A Go method called on a receiver the extractor keeps no name for, such as
a fluent chain's later link (model.WithContext(0).Where(1).First()), a
call on what a function returns (time.Now().UTC().Format(...)) or a
composite literal (GitOracle{}.Worktree()), reached the resolver as its
bare name at the column where the chain starts. Name matching then took
a project method of that name by proximity, or the only one: the issue's
GORM-style Where and First went to another query type, the standard
library's Format to the one project Format, and a literal's method to a
namesake type.

A new Go resolver step reads the chain at that column and types the
receiver link by link: a call's result, or the one a multi-value
assignment takes; a conversion's or a composite literal's type; a
generic call given its type arguments; an embedded field by its type's
name; a package-level variable from the call it is set to. A result
written with type parameters keeps them as parameters, read from the
declaration, since the indexed signature leaves them out, and the helper
that reads them now includes a one-line function's only line. The method
is the one Go selects on that type. A type from outside the project, or
one that can't be read, links nothing instead of a guess. When a chain
calls one name twice, the bare ref is the later call.

Validation: 8 new tests; 7 fail on main, and the repeated-name case
fails under a first-link rule; the 43 Go test files pass. A/B against
main c326b6b, edges removed/added: gin 80/72, etcd 1726/1327,
prometheus 2249/1265, sonic (gorm/gen) 910/877, kubernetes 88288/84331.
Every removed edge was a name-match guess except 173 in kubernetes: 171
are the same edge now owned by another ref of its chain, and 2 wrong
links of metav1.Now().Time.Unix(). Sampled retargets, drops and
additions read correct; express and flask dumps are byte-identical.
Indexing cost is within noise, median of 3 interleaved runs, wall/CPU
seconds main -> fix: etcd 9.5/28.6 -> 8.6/25.4, prometheus 10.5/42.1 ->
10.5/42.6, kubernetes 121.9/413.7 -> 121.3/416.8.

Fixes colbymchenry#2543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of the file's tail (colbymchenry#2783)

Root cause: an oversize cluster keeps its first-ranked member however big it
is, and the ceiling windows it (CG-30). Ties on importance went to the
shorter span, so in the issue's file, one 200-line function plus the line
that calls it, the one-line `path = build_report()` took that guarantee. The
search matches it through its signature, so it is an entry point too. The
function the query named then no longer fit beside it and was dropped, and
`codegraph_explore build_report` returned the file's last six lines under
the verbatim-source header. A file asked for by path did the same, because
every definition in it ranks below the bare edge lines. Where a body was
windowed, nothing in the block said so. The footer's advice, ask for the
symbol by name, returned the same window, and it suggested `codegraph_node`,
which the default MCP surface does not list.

Fix, all in the explore render path (query time only):
- Rule 1: among cluster members of one importance, a definition the query
  named ranks ahead of one its search only matched, before the shorter span.
- Rule 2: a fragment does not spend the oversize guarantee. When the kept
  members render under MIN_CHARS, the member they priced out is kept and
  windowed like a first-ranked one. That member must be one the query
  named, or a definition in a file pinned by path with nothing named in the
  cluster.
- Rule 3: a body the ceiling window cuts ends with the per-symbol view's
  note, "... lines X-Y of `name` elided - codegraph_explore `file:X-Y`
  returns them" (one shared helper now). Each note is paid for by its own
  part's last lines, never a focus line, the member's first line or a member
  the part shows whole. So no section grows and no other file's budget
  moves.
- Rule 4: the trim footer and the dynamic-boundary note name
  `codegraph_node` only when it is on the served tool surface.

Validation:
- New explore-queried-oversize-symbol.test.ts, 9 tests: 6 fail on main.
  Reverting any one rule fails that rule's own tests (2, 2, 3 and 1).
- All 37 explore suites pass (414 tests).
- Deterministic A/B: one index per repo, a fresh handler per query, five
  arms (main, then each rule added in turn). 820 queries per arm: the 50
  longest functions by name, their files by path, caller/callee pairs,
  call chains, random names and flow questions.
- The queried function's head (def plus 10 lines) renders, main -> fix:
  express 50 -> 50, flask 49 -> 50, excalidraw 33 -> 48, django 43 -> 47,
  airflow 43 -> 48, of 50 each.
- Queried body lines rendered, main -> fix: excalidraw 6,615 -> 9,391,
  django 5,337 -> 5,662, airflow 6,320 -> 6,749.
- Rule 1 changed 32 responses, each hand-triaged as the queried function
  now rendering. Only 4 of the 504 ordinary flow queries are among them.
- Rule 2 changed none.
- Rule 3 wrote 838 notes, all checked against source and index, at 2.8 to
  3.9 source lines each. Bytes a note frees flow to lower files by the
  existing carry-forward.
- Rule 4's 236 diffs are exactly the removed mention.
- No response exceeds the 25,000-char inline cap.

Fixes colbymchenry#2486

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as Go builds them (colbymchenry#2784)

Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.

A Go call whose target is a type is now a references edge. Two files of
one directory under different package clauses see each other's
top-level names only through an import: qualified, or bare after a dot
import and exported. A reference whose definition has build-constrained
twins in its package reaches the one that builds for the most typical
target its own file builds for: linux/amd64, then darwin/arm64,
windows/amd64 and the rest. Constraints are read from the file name's
GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as
go build sets them by default.

Validation: 9 new tests in three files, all failing on main, and each
part switched off fails its own file; the Go tests pass. A/B against
main 0c3e12d, edges changed as conversions / twin retargets: gin 56/0,
etcd 556/89, prometheus 812/5270, sonic 49/0, x/sys 550/3072, gopsutil
31/179, fsnotify 3/7, kubernetes 16039/328. Nodes and refs are
unchanged, and express and flask dumps are byte-identical. Every kind
change sits on a conversion-shaped call, and the twin retargets read
right by their files' build lines (prometheus labels to the default
stringlabels file, x/sys syscalls to syscall_unix_gc.go). Indexing
cost, wall/CPU seconds, median of 3: x/sys 6.7/20.5 -> 7.1/21.7, etcd
9.6/29.9 -> 9.8/30.0, prometheus 12.5/49.2 -> 12.9/50.2; kubernetes, 2
runs each, 108.1/385.8 -> 110.4/390.0.

Fixes colbymchenry#2581

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d files outrank it (colbymchenry#2785)

Root cause: getDominantFile and getTopRouteFile took the top 20 files in
SQL and only then dropped test and generated files in code. Twenty test
files busier than the app's busiest file left nothing, so getDominantFile
returned null and the context builder's core-directory boost never applied.
Nineteen left the dominant file without its runner-up, and nextEdgeCount 0
passes the "3x the next file" test for a file that is not dominant at all.
getTopRouteFile returned null the same way, and measured its 30% share
against the routes of the 20 files in the window, not the project's.

Fix:
- DOMINANT_FILE_SQL ranks every file. The window saved no work: ORDER BY
  needs every group, so the aggregate runs to the end either way, and only
  one row per file comes back.
- computeDominantFile filters the ranking 20 rows at a time until it has
  the dominant file and its runner-up. The generated-file lookup stays a
  bounded probe per batch.
- getTopRouteFile counts every route file, with ties broken by path.

Validation:
- New dominant-file-window.test.ts, 6 tests, all failing on main: the
  issue's layout with 20 and with 19 test files, 25 header-declared
  generated files (past the first batch), and three route shapes.
- The dominant-file cache, plan and allocation suites and the route-file
  detector test pass.
- Seven of the eight real repos give the same dominant file before and
  after (express, flask, excalidraw, django, airflow, grpc-go, etcd). The
  closest to the window was airflow, with the runner-up at rank 16.
- envoyproxy/go-control-plane crosses the window: 254 generated and test
  files outrank its first hand-written file. Main returns null; the fix
  returns xdsmatcher/pkg/matcher/matcher.go (167 edges, runner-up 165).
- getTopRouteFile on main reported django's admin/sites.py at 8 of 16
  routes and airflow's ui router at 65 of 65. Counting every route file
  gives 8 of 32 and 65 of 332, both under 30%, so null as documented.
- Cost, median of 3 interleaved rounds of 5: getDominantFile on airflow
  (8,564 ranked files) 581 -> 587 ms, django 189 -> 181 ms,
  go-control-plane 277 -> 284 ms. getTopRouteFile on airflow 1.3 -> 2.5 ms.
- Explore A/B, main vs fix: 996 queries on express, flask, excalidraw,
  django, airflow and go-control-plane, all byte-identical.

Fixes colbymchenry#2578

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o a project namesake (colbymchenry#2788)

Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`,
bare `'fs'` and `require('node:fs')` bindings linked their calls to
same-named project functions. Main no longer does that for any static
form. PR 2120 (released in 1.6.2) declines a name match for a name the
file imports from outside the repository. PR 2437 made that hold in
projects with a catch-all tsconfig path alias, and PR 2671 extended it to
function values such as `promisify(readFile)`.

What still linked is the same binding written as a dynamic import. The
out-of-repo check reads the file's static import mappings only. The name
matcher deliberately treats `const { x } = await import('./m')` as an
import, not a local shadow, so the call fell through to a same-named
project function. vite's
`const { createServer } = await import('node:http'); return createServer(app)`
linked to vite's own `createServer`.

Fix (name-matcher.ts):
- isOutOfRepoBinding also asks dynamicImportSource for the module a
  dynamic import binds the name from, where the reference sits.
- A per-file scan (FIFO of 64, cleared with the other name-matcher memos)
  finds `const { a, b: c, default: d } = await import('x')` declarations
  and `import('x').then(({ a }) => ...)` callbacks on the raw text. Only
  files with a match are comment- and string-blanked, to drop matches in
  comments and strings and to find where each binding's scope ends: the
  enclosing block (a top-level await binds the whole module) or the
  callback body.
- A binding counts only when the reference sits inside that scope after
  it, and the innermost one wins, also over a static import of the name.
  nitro's `async function createWebSocketProxy()` imports crossws's
  `createWebSocketProxy` inside itself: only that inner call is dropped,
  and the file's other calls still reach the local function.
- jsBlockEnd and jsBodyEnd are the forward forms of jsBlockOpenAtEnd and
  jsBodyOpenAtEnd, which now call them.

Validation:
- __tests__/dynamic-import-binding.test.ts: 5 of 6 tests fail with main's
  name-matcher.ts (the project-module control passes there); all 6 pass on
  the kernel and with CODEGRAPH_KERNEL=0. Four mutations each fail their
  test: no scope end, no callback form, no dynamic source, and no memo
  clearing (the in-process sync test).
- Sync equals a fresh index (CLI) for adding and removing the dynamic
  import, for the namesake file arriving later, and for the nitro shape.
- A/B on 9 TS/JS repos plus 2 controls, both arms with the kernel: 6 edges
  removed, 0 added, 0 retargeted, all 6 wrong. vite has 3 (node:http
  createServer, node:fs/promises readFile, rolldown watch), memfs 2
  (node:util promisify), nitro 1 (the crossws self-loop). pathe, c12,
  unbuild, fs-extra, unenv and nuxt are byte-identical, as are the flask
  and gin controls.
- Cost: fresh index median of 3, interleaved: vite 9.17 s main vs 9.50 s,
  nitro 7.43 s vs 7.12 s (noise on a shared box). The scan itself over
  every vite source file takes 40 ms (24 files reach the blanking pass).
- Kernel parity is unchanged: 772/776 files byte-parity, 4 deferred,
  over nitro/src and vite's src (this is a resolver-only change).

Fixes colbymchenry#2491

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olbymchenry#2787)

A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.

The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.

Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).

Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
  included): all 7 fail with main's sources. Without only the resolver
  rule the typecast test fails; without only the chain-link gate the
  fluent-chain test fails.
- A/B against main 88ebefa, both arms built from their own sources,
  diffed by natural keys:
  - Every removed edge is a cast changing kind at the same site, with
    the same source and target (SynEdit 491, HeidiSQL 1,262, mORMot2
    4,832, DWScript 3,115, DelphiMVCFramework 2,761), apart from 10
    function references that moved to an earlier use in the same
    routine.
  - New edges: SynEdit 55, HeidiSQL 249, mORMot2 1,196, DWScript 690,
    DelphiMVCFramework 2,727. Mostly casts read as references to their
    type (14 of 14 sampled right) and receiver calls.
  - 178 new call edges sampled: 140 right. instance-method 30/30,
    qualified-name 10/10, bare functions 26/30, bare methods 70/92,
    calls landing on a property, field or variable 0/12. The wrong ones
    follow rules main applies to every call: the TypeInfo intrinsic
    (main already links 1,709 such calls to a project method), calls
    through local procedural variables, and calls now read inside a
    receiver's arguments, named as main names them.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are
  byte-identical in dump-graph.
- Cost within noise: index time medians of 3 interleaved rounds,
  HeidiSQL 7.10 to 7.30 s, mORMot2 18.80 to 18.90 s, DWScript 20.00 to
  19.40 s wall.

Fixes colbymchenry#2547

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, not glossed over (colbymchenry#2789)

Root cause: explore answers whatever the query's words match. When a name
the query spelled is not in the index at all, the answer is built from the
other words and says nothing about the missing ones. In the report, the
tool projects under a .NET repo's root build/ folder are skipped by
default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData
and AddFeatures returned an unrelated Program.Main and a C++ Parser, and
the agent read the wrong code. The miss diagnostics (issue colbymchenry#2026) ran only
when nothing at all matched.

Fix:
- Query tokens written like code names (a camelCase hump, snake_case, or a
  qualified name) that resolve to no node at all are checked with a new
  bounded probe, getUnseenNames. It reports names the index has never seen:
  no node called that in any case, and no name, qualified name or path,
  signature or docstring word starting with it.
- A qualified name is judged by its member. Express's app.handle is indexed
  as handle, so the receiver alone never makes a name missing.
- Names that pass are listed in a note right under the summary line,
  success-shaped. The note points to codegraph status and never to Read.
  It names the .gitignore re-include for a default-skipped folder like
  build/ as the user's choice, since indexing scope is the user's call.
- A single word (Program, Main, How) is never named: it may be English.
- The total-miss answer gains the same note.
- The seeding's query tokenizer moved into one shared function, unchanged.

The skip itself is unchanged here. On main a root build/ folder is still
skipped by default: PRs 2681 and 2687 re-opened build folders below src/
only. The documented opt-in, !/build/ in .gitignore, indexes it, and sync
picks the opt-in up, verified on main and on 1.6.2.

Validation:
- New explore-unindexed-names.test.ts, 8 tests: 6 fail on main.
- All 38 explore suites pass, plus the tool-allowlist and dynamic-boundary
  suites (466 tests).
- The reporter's repo, newrelic/newrelic-dotnet-agent 418a0f2f, now commits
  the !/build/ workaround. With that line removed and the index synced,
  main answers the reporter's query with VersionFileGenerator/Program.cs
  Main and Sicily/Parser.cpp, saying nothing. The fix adds the note naming
  exactly the four missing names.
- Ordinary queries: 996 on express, flask, excalidraw, django, airflow and
  go-control-plane, all byte-identical to main.
- Miss probes: 120 queries mixing real names with invented ones, or only
  invented ones. Main never notes anything. The fix names every invented
  name in all 120 and no real name. The note costs about 5 source lines
  on a full response.

Fixes colbymchenry#2462

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not there (colbymchenry#2790)

Issue 2455: when production code imports a workspace package the resolver
cannot follow (`import { formatLabel } from 'acme-shared'`), its names fall
back to name matching, and a Jest manual mock beside the code
(`__mocks__/acme-shared.ts`) is the nearest namesake. So the call, the
import's names, type references, constructions and `extends` all linked to
the mock, and the real function lost its callers. The visibility gate keeps
test suites from production code, but `__mocks__/` is not one of them.
jsx-render, Vue templates and the event-emitter pass pick by name with
their own lookups, which no gate covers.

A manual mock stands in for a module only while Jest or Vitest runs, and it
re-declares that module's names by design. Rejecting it after the ranking
picked it (what the gate does for `__tests__/`) leaves the call unlinked even
where the real function is the only other candidate. It also leaves sync
behind a fresh index: deleting the mock re-opens nothing, while a fresh
index then links the real function (main has this today for `__tests__/`).

Fix:
- ReferenceResolver.resolveOne resolves a reference from code that is not a
  test (isTestPath) on a view of the graph without manual mocks, when the
  project has any. Every lookup by name, lower-case name, qualified name,
  kind and method answers as if no `__mocks__/` file were indexed. Only
  names a mock declares are filtered, and those answers are cached. The
  view is swapped in as this.context for the call, so every strategy
  (frameworks, imports, name matching, fuzzy, function refs) reads it, and
  clearCaches drops it with the memos keyed by it. Test code keeps the
  whole graph.
- jsxChild, the Vue template resolver and the event-emitter handler lookup
  skip a mock's component or function for a non-test file
  (isManualMockHiddenFrom).
- The synthesized-edge merge drops any edge from non-test code into a node a
  manual mock declares, whatever pass made it.
- An import that names the mock's own path (`import x from
  './__mocks__/uuid'` in a vitest setup file) still links. Jest's
  `moduleNameMapper` and automock are not read: they act only while Jest
  runs, so they never change what production code calls.

Validation:
- __tests__/jest-manual-mocks.test.ts: 5 of 6 tests fail with main's
  sources. Each of five rules fails a test when removed: the view (three
  tests, the in-process sync case among them, which adds and deletes a
  mock), the JSX, Vue and event lookups, and the merge guard.
- Sync equals a fresh index (CLI) when the mock or the real module arrives
  later, when the production file is edited, and when the mock is deleted.
- A/B on 8 TS/JS repos plus 2 controls, both arms with the kernel. Edges
  from production code into mocks: webdriverio 23 to 0, outline 21 to 0,
  social-app 3 to 0, insomnia 19 to 13 (the 13 left are setup-vitest.ts
  importing mocks by path). webdriverio gains 23 right edges and 142
  confidence raises that the mocks had blocked. jest, nuxt, flask and gin
  are byte-identical. On webdriverio, production code's edges equal main's
  on a copy without the mocks, 20,885 of 20,885 with metadata. insomnia
  matches the same way, apart from the 13 path imports.
- Cost: fresh index, median of 3 interleaved: webdriverio 12.67 s main vs
  12.71 s, insomnia 12.84 s vs 12.60 s.

Fixes colbymchenry#2455

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inks its file (colbymchenry#2792)

Issue 2577: `import('@/widgets/Chart/')`, `from 'src/widgets/./Chart'`, a
doubled slash, a `..` after an alias, and a workspace package's
`from '@acme/ui/'` lost the edges the plain spelling gets. The file import
was dropped, the names were matched by name alone, and the lazily loaded
component rendered the importing file's own `lazy(...)` constant.

Root cause: the fallback-alias, workspace, direct-path and own-config
branches of the import resolver join the specifier as written and probe
the result through tryWithExtensions. `fileExists` falls back to the disk,
which accepts any spelling of a file, so the resolver returned
`src/widgets/Chart//index.tsx` or `src/widgets/./Chart/index.tsx`, and every
lookup keyed by the indexed path missed it. The relative and tsconfig-path
branches go through path.resolve and were already fine.

Fix (import-resolver.ts): tryWithExtensions spells each candidate as the
index keys files (indexedSpelling): for the ES module family, a path with a
doubled slash or a `.`/`..` segment is posix-normalized before it is probed
and returned. That is how Node, tsc and the bundlers read a specifier. A
trailing slash still names only a folder, because `Chart/` plus `.tsx` is
no file. Other languages are untouched: a C include's `a/../b.h` follows the
folder on disk, as PR 2610 noted.

Validation:
- __tests__/import-path-spelling.test.ts: 4 of 5 fail on main (the plain
  spelling passes). They cover the issue's spellings, a doubled slash, `..`,
  a trailing slash beside a same-named file, and a workspace member. All
  pass on the kernel and with CODEGRAPH_KERNEL=0. Dropping the
  normalization, or its doubled-slash check, fails 4.
- Sync equals a fresh index (CLI) when the folder arrives after its
  importer, when the importer is re-spelled, when the folder is deleted
  and when the target is edited.
- A/B on 11 TS/JS repos plus 2 controls, both arms with the kernel:
  pulse-sms-web (53 imports like '@/utils/') gains 65 edges and corrects
  7, including 2 imports that had name-matched to same-named API classes;
  react-one gains 11 file imports and corrects 2 bindings. All 85 are right.
  The other nine (jvue-admin, vue-invoice-app, vue3-antdv-admin, v-ui,
  vue-blog-admin, dreammart, vite, outline, social-app) and the flask and
  gin controls are byte-identical.
- Cost: fresh index, median of 3 interleaved: vite 7.59 s main vs 7.49 s,
  outline 16.63 s vs 16.94 s.

Fixes colbymchenry#2577

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… daemon killed (colbymchenry#2793)

Root cause: the daemon arms its liveness watchdog with its own project's
index files (codegraph.db + -wal), and since PR colbymchenry#1231 a silent timeout
defers the kill while those files advance. But the daemon also writes every
other project a session opens with projectPath: the project lifecycle takes
that project's writer lock, starts its watcher and runs the catch-up sync,
all storing on the main thread. A long synchronous stretch of such a sync
left the daemon's own files still, so the watchdog read it as a wedge and
SIGKILLed a healthy daemon mid-sync; the next session reattached the project
and the cycle repeated. A proxy serving in-process, armed with no files at
all, had the same blind spot for its own project.

Fix: addWatchdogProgressPaths() lets a process add files to its watchdog at
runtime. The project lifecycle adds a project's db and -wal the moment this
process becomes the project's writer, and drops them after it closes the
project, so only indexes this process writes count, never one another
process owns. Each change re-arms the watchdog with a new child whose argv
carries the whole list; the old child is stopped after the new one starts.
Added files take effect at once (a sync may block the loop right after);
dropped ones wait a turn, so a server releasing its projects on the way out
re-arms once or not at all. A message over the heartbeat pipe was tried
first and dropped: Node's Writable holds a second write while one is still
in flight, so the -wal line of a db + -wal registration never reached the
child before a block that followed in the same tick (4 of 4 runs).

Validation:
- New __tests__/liveness-watchdog-projects.test.ts (11 cases, real watchdog
  child, lowered timeout): main's source 9 fail / 2 pass (the two guards),
  fix 11 pass. The lifecycle case arms the watchdog for project A, attaches
  B through the project lifecycle and stores into B's index on the main
  thread for 3s: killed at ~1s on main, runs to completion with the fix.
- 8 mutations, each failed at least one case: no lifecycle registration,
  registration before the writer-lock check, no drop on release, no re-arm on
  add, no re-arm on drop, old child not stopped, drops re-armed at once,
  adds deferred.
- Real daemon on Windows (watchdog timeout 5s, daemon keyed on a 2-file
  project, second project excalidraw 1d4dc2c with all 687 TS files edited,
  attached with codegraph_status projectPath), 3 interleaved rounds:
  main killed 3/3 about 13s after attaching B ("Main thread unresponsive
  for ~5s"), leaving B with 27,698 edges and 72,534 pending refs; the fix
  survived 3/3 through 7.8-9.3s main-thread blocks and finished B (61,120
  edges, 0 pending), and 3/3 again on the final build (9.7-9.9s blocks).

Fixes colbymchenry#2404

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…led at every start (colbymchenry#2794)

Root cause: a run killed inside a bulk-load window (a crash, an OOM kill,
the liveness watchdog itself) leaves that window's secondary indexes
dropped, and since PR colbymchenry#1562 every writable open rebuilds them in
DatabaseConnection.open(), one CREATE INDEX after another on the caller's
thread. Each one scans and sorts its whole table. On a large index the sort
writes nothing to the database or its WAL, so on an MCP server the liveness
watchdog saw a silent main thread with no disk progress, and killed the
process before the index committed. Each restart died at the same index,
and the proxy's in-process fallback died the same way on the request it
re-served.

Fix: an open on a watched event loop leaves the rebuild to a worker thread
with its own connection, and the project is served once the rebuild is
done.
- DatabaseConnection.open({ deferIndexRecovery }) records the missing DDL
  instead of running it; recoverIndexesOffThread() runs it on a worker
  (single-flight, rejects on failure so the next call retries; SQLite's
  default temp store, so the sort spills to disk; the worker collects its
  garbage and exits by itself, per worker-teardown.ts).
- CodeGraph.open() (the MCP default project, the CLI) defers and awaits it.
- openSync({ deferIndexRecovery }) plus cg.recoverIndexes() for the MCP's
  synchronous opens: the ToolHandler's projectPath projects and the engine's
  retry path. ToolHandler.execute awaits the rebuild before serving (not
  time-boxed: reads without the indexes scan whole tables), the project
  lifecycle awaits it before its catch-up sync writes, and the retry path
  starts the query pool after it.
- Synchronous opens that ask for nothing keep rebuilding in line, as before.

Validation:
- Reproduced on main b2a8487: a django 79502d0 init killed 1.5s into the
  resolution window left the field report's index set missing (5 ref + 5
  edge indexes, index_state indexing); the in-line rebuild is about 750ms
  of main-thread work there.
- Real daemon on Windows, a 3M-ref / 1M-edge index left the same way
  (781 MB), watchdog 1.5s: main was killed in 4 of 4 starts, stuck at 2 of
  10 indexes, the launcher's in-process fallback killed too, no reply. The
  fix rebuilt all 10 in about 11s with no main-thread block over 506ms,
  answered the call, and the next start opened in about 120ms.
- New __tests__/index-recovery-off-thread.test.ts (9 cases): main's source
  8 fail / 1 pass (the in-line guard), fix 9 pass. Spies show no CREATE
  INDEX on the server thread for CodeGraph.open, a projectPath call
  (watched and not), the retry path and the startup path; a real watchdog
  (500ms) kills main mid-rebuild of a 300k-ref index and leaves the fix
  alone. 6 mutations each failed at least one case.
- Contributor PR colbymchenry#1888 and the stale draft PR colbymchenry#1919 were read for guidance;
  this ports their worker recovery to current main's open paths.

Fixes colbymchenry#1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for minutes (colbymchenry#2795)

Root cause: a launcher that found no answering daemon spawned one and then
polled for it 240 times, 25ms apart. A probe that connects but gets no hello
waits out the 3s hello timeout, so a process that holds the daemon's pipe
and never says hello made the poll take about 240 x 3s, plus the first
probe: about 12 minutes, with the session's tool calls held meanwhile. In
practice that holder is a daemon whose main thread is blocked: the OS still
accepts connections on its pipe, and the daemon the launcher spawns cannot
take its lock, so it exits.

Fix: the poll has a wall-clock budget instead of a probe count (the 6s it
was meant to be), and each probe's hello wait is capped by what is left of
it (pollForDaemon, connectAnyDaemonCandidate, a hello timeout on
connectWithHello). Spent, the session is served in-process exactly as when a
spawned daemon never binds: read-only while the stuck daemon still holds the
writer slot. The proxy's existing retry with backoff then reaches the daemon
again. The liveness watchdog kills a wedged daemon after its 60s window
(longer while its disk advances), and the next retry spawns the replacement
and proxies to it. The bound sits well inside that window, so a session
neither waits on the wedge nor gives up on the daemon.

Validation:
- A real daemon wedged by a preload's busy loop on Windows, then a second
  session's codegraph_status:
  - no watchdog on the wedged daemon: main answered after 733.2s; the fix
    after 9.9s, and later calls in 0.5s while the daemon stayed wedged;
  - the default 60s watchdog (it killed the wedged daemon at about 121s,
    one deferral for its own off-thread writes): main answered after
    125.2s; the fix after 9.9s in-process, then proxied to the replacement
    daemon at 121.1s.
- New __tests__/daemon-silent-holder.test.ts (8 cases): main's source 0
  pass, fix 8 pass. Fake-clock tests of the budget and the per-probe cap; a
  real silent socket honouring the hello timeout; the real launcher against
  a project whose pipe, lock and writer slot are held by a holder that
  never says hello: first call served in-process in about 10s (main: none
  in 45s); and after the holder dies, the session proxies to the daemon
  that replaces it. 3 mutations each failed at least one case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ode version problem (colbymchenry#2796)

Root cause: createDatabase() caught everything the node:sqlite connection
threw and reported it as "CodeGraph requires node:sqlite (Node.js 22.5+)",
though the module had loaded and SQLite itself had failed: a database
another program held open (SQLITE_CANTOPEN on Windows), a folder the user
can't write, an I/O error on a network share. Failures in the connection's
first statements (locked, not a database, a WAL I/O error) came out raw,
without the path. The MCP server then told the agent "No CodeGraph project
is loaded" for a default project whose index exists, or "Tool execution
failed: ... requires node:sqlite" for a projectPath one. Meanwhile a runtime
that really lacks node:sqlite never got the hint: isInitialized's schema
probe loaded the module outside its try, so the CLI and MCP server died with
"Unhandled rejection: No such built-in module: node:sqlite".

Fix:
- src/db/node-sqlite.ts loads node:sqlite in one place;
  NodeSqliteUnavailableError carries the Node 22.5+ hint, now only for a
  module that cannot be loaded. The schema probe treats it as "unknown", so
  the command goes on to the open and reports it from inside its own catch.
- src/db/open-error.ts: DatabaseOpenError names SQLite's code and the path,
  with a hint for BUSY, PERM, READONLY, IOERR (network share), CORRUPT,
  FULL, CANTOPEN (folder, permissions, another program holding the file)
  and NOTADB, keeping code/errcode/errstr and the cause. The Windows/WSL
  shared-index guidance still wins for its case.
- DatabaseConnection.open/initialize wrap the connection's first statements
  as well as the constructor, and close the connection on failure.
- MCP: the default project's open failure is reported to a tool call instead
  of "no project loaded", and a DatabaseOpenError or a missing node:sqlite
  is answered isError with the retry-once note (a malfunction, per
  AGENTS.md), worded as what failed rather than "an internal codegraph
  error". Not-indexed projects keep their success-shaped guidance.

Validation:
- Windows, an indexed project whose codegraph.db another process holds open
  with no sharing (a real SQLITE_CANTOPEN): CLI status, sync and query, an
  MCP default-project call and a projectPath call all said "requires
  node:sqlite" on main, or "No CodeGraph project is loaded" for the default
  project; with the fix all name SQLITE_CANTOPEN, the path and the hint.
  With --no-experimental-sqlite (node:sqlite unloadable), main's CLI and MCP
  server died with an unhandled rejection; the fix prints the Node 22.5+
  explanation and the MCP call answers with it.
- New __tests__/sqlite-open-error.test.ts (21 cases): main's source 18 fail
  / 3 pass, fix 21 pass. Real CANTOPEN (a directory, a file held
  exclusively), NOTADB and BUSY opens, the CLI and the MCP engine, plus
  each SQLite code's message. 5 mutations each failed at least one case.
- The POSIX-only wsl-shared-index case for a real I/O error outside WSL now
  expects the named error (DatabaseOpenError, SQLITE_IOERR, the path) instead
  of the bare "disk I/O error"; it is skipped on Windows, so it was not run
  here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tine (colbymchenry#2797)

Delphi calls a routine that takes no arguments without parentheses
(`Before;`, `X := FProvider.Value + 1`, `if Ready then`), and only a
statement-level `Obj.Method;` was recorded, so most call sites were
missing from callers, callees and impact. Bare names in arguments and
assignments were captured as function values and matched by name alone,
which linked locals and parameters to other classes' methods.

The extractor now records each name written without parentheses with
how it is used (a statement, a value, an event's method, a receiver),
the routine's class, and the declared type of a receiver the routine
declares. It leaves out what the routine declares, data the class and
the unit declare, names the class's code assigns or passes to Inc/Dec,
names Delphi's conventions give to data, and anything in a `with` body
but an event's method. A new resolver links such a name only to a method
or function Delphi's scope rules reach: the class and its ancestors, the
unit, then the units it uses, last first, or a method of a receiver's
declared type. Data of that name links nothing. A function runs (calls);
a procedure read as a value, an event's method, and a routine that needs
arguments written without them are method pointers (references). Where
the tree has errors, the class and declarations come from the source
text, and a routine's own header is no call. `class sealed (TBase)` and
`class abstract (TBase)` heads now name their ancestors. Pascal function
values capture only `@Handler`.

A/B against main 8073637 (SynEdit, HeidiSQL, mORMot2, DWScript,
DelphiMVCFramework): added edges 3011/4626/6501/5092/6776; main's
name-matched function values now linked by scope 2387/2727/948/726/712,
114/281/554/342/586 of them as calls; removed 85/139/802/333/707,
triaged as main's false positives (locals and parameters linked by
name, other classes' methods, routine headers misparsed in broken
trees) and own-class retargets, less deliberate losses (reads in `with`
bodies, lowercase or type-like names read as values). Sampled precision
of added edges: 196/196. leveldb and SDWebImage byte-identical. CPU,
median of 3 interleaved: +15.2%, +16.2%, +8.7%, +12.9%, and +0.8% for
DelphiMVCFramework (one run each).

Fixes colbymchenry#2514

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onger drive the hook (colbymchenry#2798)

Claude Code's desktop app puts <system-reminder> blocks (which local
sessions are linked, and similar status) in front of the prompt it
submits, and UserPromptSubmit hooks receive them inside `prompt`. The
hook gated, picked the project and explored on the whole string, so the
reminder's words drove it: a linked session titled after its work, like
"trace-order-flow", carries structural keywords, so every turn, a bare
"yes" included, got a full explore of the reminder. A reminder in front
of a task notification or a subagent hand-back also defeated the
start-anchored envelope checks, and a message from another session
(a <cross-session-message from=...> envelope) was never skipped at all.

The hook now removes every complete <system-reminder> block before the
envelope checks, the gate, project selection and explore, returns early
when nothing is left, and skips a prompt that is entirely a
<cross-session-message> envelope, as it already did for task
notifications and subagent hand-backs. The scan stops at the last
closing tag and the attribute run stops at `<`, so a pasted prompt full
of unclosed tags stays linear (520 KB in under 1 ms; the `[^>]*` form
took 83 ms on a tenth of that and grows quadratically).

Validation:
- Prompt shapes come from the Claude Code desktop transcripts on this
  machine (314 transcripts): reminders always lead, one to four of
  them, separated by one or two newlines, and cross-session messages
  follow one to four reminders.
- frontload-hook.test.ts: 15 new tests (8 spawn the built hook with
  Claude Code-shaped payloads) fail on main and pass with the fix; the
  file's 48 other tests pass on both.
- Built CLI, private HOME, 13 payloads: on main every prompt behind a
  reminder got a full explore of the reminder (2.1 to 5.0 KB); with the
  fix the 8 that are not user input inject nothing and exit in about
  200 ms instead of 680 ms, and a question behind reminders injects
  byte-for-byte what the same question alone does.

Issue colbymchenry#2635. Contributor PR colbymchenry#2642 (yang0228) was read for guidance.

Fixes colbymchenry#2635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n entries (colbymchenry#2799)

Issue 2502: a pnpm (or npm, Yarn, Bun) workspace member that declares its
entry with `main`, `module`, `types` or `exports`, like
`"main": "./src/index.ts"`, has no index file at its root, so
`import { caseStatus } from '@acme/core'` failed and its calls fell back to
name matching, where a same-named private function nearer the caller took
them. Issue 2456: a sibling package another declares as a path dependency
(`"acme-shared": "file:../common"`), with no root `workspaces`, was not
known as a package at all.

Root cause: workspace-packages.ts mapped a member's name to its folder only
(its header said it did not yet honour `exports` or `main`), and the
resolver probed `<folder>/index.*`. Path dependencies were read only from
ohpm manifests.

Fix:
- workspace-packages.ts reads each package's declared entries: `exports`
  (subpaths, `*` patterns, fallback arrays, nested conditions, read with
  import/require/node/module/default first, other conditions next, types
  last) and the root's source/module/main/types/typings. It reads `file:`,
  `link:` and `portal:` dependencies from every package.json in the
  existing manifest walk: the root manifest's for every file, any other's
  for the files under it, as npm installs them.
- The resolver takes the first declared target, `exports` before the
  main-style fields, that the index holds or that is build output the
  index lacks, read as the source it is built from (dist/, build/, lib/,
  out/, es/, esm/, cjs/ mirrored under src/, then without a format folder).
  Then the package's folder, as before, and a path into its build output
  (`@acme/utils/lib/path`) read as its source. A file a local build left on
  disk never wins over an indexed source, and no mapping links nothing.
- Sync: a full reconcile reads the manifests again and, when their digest
  moved, re-opens every JavaScript-family reference, as a moved Python root
  set does. A file added inside a package re-checks the imports of that
  package that landed elsewhere in it. A full index drops the cached map.

Validation:
- __tests__/workspace-package-entries.test.ts (26 tests): 25 fail on main,
  the control passes; all pass on the kernel and with CODEGRAPH_KERNEL=0.
  Each of 17 rules removed in turn fails at least one test.
- Sync equals a fresh index in 15 CLI scenarios (entries, subpaths, a
  member renamed, added or leaving the globs, file: deps added, removed or
  retargeted, an entry's file appearing). On real repos: two vben manifest
  edits (237 edge rows move) and a vite playground file: edit, synced graph
  identical to a fresh index.
- A/B on 14 TS/JS repos and 2 controls: vben +2,955 edges, insomnia +2,222,
  webdriverio +2,472, jest +1,700, nuxt +706, memfs +470, vite +251,
  t3-turbo +47, social-app +40, vue3-antdv-admin +8, with name-matched
  namesakes corrected; nitro, outline, pathe, c12, flask and gin are
  byte-identical.
- Cost: fresh index within 3% (median of 3, interleaved); a full sync reads
  the manifests, +30 to 160 ms on these monorepos.

Fixes colbymchenry#2502
Fixes colbymchenry#2456

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…colbymchenry#2801)

A call or `new` that a strategy resolved by name or through an import could
target a TypeScript interface or type alias, which nothing can call or make
an instance of. memfs's fs-core `result.ts` declares `interface Ok` beside
`function Ok` (and `Err` beside `Err`), and `Ok(...)` through an import
reached the interface. A `new` of a global that a project only declares a
type for (`new Response()`, `new Worker()`, `new Event()`) was linked by
name to that type, and jest's `write()` parameter call to a `type write`.

Root cause: no rule on what a call may target. Whichever strategy found the
name picked among same-named nodes by order, and nothing stopped it on a
type.

Fix (gateTargetKind): for the JS family, a `calls` or `instantiates` result
whose target is an `interface` or a `type_alias` moves to the value the
target's file declares under the same name (a function, class, component,
constant or variable, first by position): `function Ok` beside
`interface Ok`, a class merged with an interface. With no such value the
result is dropped, and the reference stays unresolved.

Validation:
- __tests__/call-value-targets.test.ts (3 tests): all 3 fail on main and
  pass on the kernel. Each of 5 rules removed in turn fails at least one.
- Sync equals a fresh index (CLI) when the value beside a type is added or
  removed, and when a project type for a global or a parameter appears.
- A/B on 14 TS/JS repos and 2 controls: memfs's 11 Ok/Err calls move
  from the interfaces to the functions; 165 `new` of globals dropped (vite
  27, nuxt 57, webdriverio 26, insomnia 47, jest 7, memfs 1) and jest's
  `write()` call on `type write`. Every dropped `new` names a global
  (Worker, Event, Response, Request, Headers, ReadableStream, Node's Module,
  mocha's Context/Suite) or a local variable. vben, outline, social-app,
  t3-turbo, nitro, vue3-antdv-admin, pathe, c12, flask and gin are
  byte-identical.
- 91 related test files pass (2,178 tests).
- Cost: fresh index within noise (median of 3, interleaved): vite 9.07 s
  to 8.74 s, nuxt 11.71 s to 10.93 s, insomnia 15.26 s to 15.11 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion's call (colbymchenry#2802)

A default runs inside the function each time a call leaves the argument
out, but both extractors pushed the function and walked only its body,
so `function renderWithNote(note = buildNote())` recorded nothing for
the call: no edge and no unresolved reference. Arrows, methods,
constructor parameter properties, class-field arrows, object-literal
methods and destructured parameters lost theirs the same way. Only an
anonymous arrow inside a body was walked whole, params included.

The TS extractor (visitParameterDefaults) and the kernel's TS/JS walker
(visit_parameter_defaults) now hand each default value, and a computed
key in a destructured parameter, to the body walker, with the scope the
body gets on the stack: the function for a named function, method or
component, the enclosing scope for an anonymous function or an
object-literal method. It runs before the body walk at every site that
walks only a body, including a class field's arrow, a forwardRef/memo
render function and a styled .attrs() function. Patterns are recursed
through; the names a parameter binds, its type annotation and a bodiless
signature are not walked. TS/JS only: a Python default runs once, when
the def does, and is left as it was.

Validation: the new test fails 13 of 14 on main 5d81d29 and passes on
both engines, LF and CRLF; the kernel parity fixtures gained a defaults
section. A/B against main on 14 TS/JS repos: 118 edges added, none
removed or retargeted, each a call or `new` written in a default (nuxt
45, mostly kit utilities' `nuxt: Nuxt = useNuxt()`, webdriverio 30,
memfs 17, jest 7, vben 5, social-app 5, vite 4, outline 3, nitro 2;
insomnia, t3-turbo, vue3-antdv-admin, pathe and c12 none). Seven ride
resolution the body already gets: six Vue/Nuxt auto-import self-links
and one member call on memfs's `export default createProcess()`. The
flask and gin control dumps are byte-identical. kernel-parity.mjs over
the 14 repos: 15,408 files byte-parity, 0 with diffs. Index time,
median of 8 interleaved runs on vite, nuxt, webdriverio and insomnia:
+1.6% total, within noise (the last 5 rounds alone: -1.7%). A CLI sync
that adds defaults, adds a file and gives an unchanged file's default
its callee matches a fresh index.

Fixes colbymchenry#2588

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nger link namesakes (colbymchenry#2803)

A Pascal call to a name the compiler or the System unit gives meaning
to (SizeOf, TypeInfo, High, Low, Length, Inc, Dec, Assigned, Ord, Exit,
Include, Move, Assert, typecasts like Pointer(P)) went to name
matching, which linked any project member of that name: mORMot's
TRttiProp.TypeInfo, a record's length or high field, a property named
Pointer. The built-in name check compared case, so length(S) and dec(N)
got through too. And a call that resolved to a field, property or
variable linked whatever data had the name.

Now a bare call to a System name links only to a declaration its scope
reaches: the class and its ancestors, the unit, the units it uses last
first, skipping a unit's stand-in declared under {$IFDEF} or {$IF}. A
call resolved to data stands only when the data's declared type is a
routine type (written out, a project alias to one, generic or on two
lines, or an RTL procedural type by name), retargeted to what the scope
declares. A name the calling routine declares itself links nothing; a
name the scope doesn't declare at all (a with object's member, a
declaration the parse misplaced) links to the one routine-holding field
or variable of that name. Built-in names compare without case; the RTL
namespaces keep their spelling, except system. Scope lookups now see a
unit whose unit header the parser lost, by its file name (its indented,
lost class members aren't taken for its routines), and an include file
shares its unit's scope and uses clauses. A with body's bare call to a
System name is not extracted, since it may be the with object's.

Validation, edges removed/added against origin/main: SynEdit -243/+50,
HeidiSQL -858/+296, mORMot2 -9039/+987, DelphiMVCFramework -2420/+242,
DWScript -705/+222; leveldb and SDWebImage byte-identical. Sampled
precision of calls to data, before and after: 20/50 and 50/50; of calls
to System names: 2/50 and 47/47. Sampled removed edges: 40/40 wrong.

Follow-up to colbymchenry#2514.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a large index (colbymchenry#2804)

The prompt hook hands codegraph_explore the whole prompt, so a pasted
log, document or source file arrives as a query of hundreds of words.
findRelevantContext searched every word several times, and the reads
behind those searches scanned the index each time:

- findNodesByNameSubstring, four times per symbol word: `name LIKE
  '%word%'` with a leading wildcard reads every node of those kinds,
  about 80 ms each on vscode (562,636 nodes);
- the LIKE fallback after an FTS miss (`name LIKE` or `qualified_name
  LIKE '%word%'`), about 120 ms per miss, and most words of a log miss;
- the fuzzy fallback after that re-read `SELECT DISTINCT name` each time
  (about 150 ms), and getAllNodeNames, documented as cached, was not;
- FTS joined nodes on the TEXT id instead of the rowid;
- scorePathRelevance re-split every query word for every scored path,
  and the deprioritize predicate stat'ed codegraph.json per candidate.

On vscode a 1.7k-character log took 80 s in explore, a 10k-character
skill document 258 s, and the hook gave up on both. A per-statement
profile put 65-80% of that in those scans.

The fix:
- A name-substring index answers the substring lookups from memory with
  exactly the rows SQLite returns, in its order: ASCII-only case folding
  and `_` as one character, like LIKE, and ties in length in the order
  the plan reads rows, the kind index's or rowid's. The plan is asked of
  SQLite once per statement shape, since a small index is scanned whole;
  a `%`, a language filter or any other plan goes to SQLite as before.
- A word of letters and digits that no name and no letters-and-digits
  run of a qualified name holds can match nothing, so the LIKE fallback
  returns empty without scanning for it.
- The fuzzy fallback reads the name list once and skips names outside
  its edit-distance length window.
- These reads are memoized per database change stamp, like the dominant
  file (PR colbymchenry#1864): rebuilt after any write from any connection, never
  kept inside a transaction, dropped on rebind.
- FTS joins on rowid, with CROSS JOIN keeping nodes_fts the outer loop
  as before.
- Path relevance prepares each query once and each path is scored once
  per search; the deprioritize patterns are read once per search.
- findRelevantContext searches at most 64 symbol names and 128 search
  terms of a query. Both lists put code-shaped words first, so a long
  paste is searched by its identifiers and its opening words. A
  16-symbol bag (75 terms) stays whole. This is the one intended change
  in output, for long queries only.

Validation (vscode ff0a86dc, median of 3 interleaved, fresh process):
- explore, ordinary queries: 6 of 6 outputs byte-identical, 5.5-7.5 s
  fresh (the dominant-file aggregation is about 4 s of that) and
  0.9-2.1 s warm vs 2.7-3.9 s on main;
- explore, long queries: log 1.7k 80.1 to 10.7 s, markdown 3k 73.3 to
  9.0 s, Java 3k 108.8 to 12.2 s, TS 3k 63.4 to 11.9 s, markdown 10k
  258.3 to 10.8 s, Java 10k 144.7 to 11.1 s, TS 10k 146.7 to 9.6 s;
- the real prompt hook on the issue's prompt shapes: short 7.2 to
  7.0 s, log 84.2 to 12.9 s, markdown 3k 78.1 to 10.2 s, Java 3k 113.1
  to 13.3 s, markdown 10k 261.7 to 11.3 s, Java 10k 146.3 to 11.1 s;
- without the word caps, all 7 long queries gave byte-identical output
  4.3-9.0x faster;
- django 79502d08 and prometheus ed26d727: 11 of 11 queries
  byte-identical, tracebacks included, 1.0-2.9x faster;
- the substring index against the LIKE scan on vscode: 4,830 of 4,830
  lookups identical (805 words, snake_case included);
- explore-long-query.test.ts: 13 tests; 8 fail on main's source.

Issue colbymchenry#2620. Closed contributor PR colbymchenry#2621 (retsamekohC) was read for
guidance; open PR colbymchenry#2260 (danusha2345) overlaps on the name-list memo
and path relevance.

Fixes colbymchenry#2620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as they exit (colbymchenry#2806)

Root cause: every release bundles the Node that scripts/build-bundle.sh
defaults to, since the release workflow passes only the target, and that
default was v24.16.0. On Windows that Node can abort just as the process
exits, after the command's work is done: a V8 worker posts a delayed task
after the platform's delayed-task scheduler has started closing its uv_async
handle, and libuv fails "Assertion failed: !(handle->flags &
UV_HANDLE_CLOSING), file src\win\async.c, line 94" with status 0xC0000409
(nodejs/node issue 56645). Node fixed it in 24.20.0 (nodejs/node PR 61999).

Fix: the default moves to v24.21.0, the newest 24.x LTS release that is at
least two weeks old (2026-09-07). It also carries the Windows 8.3 short-path
file-watcher fix (nodejs/node PR 65118). V8 13.6.233.17, libuv 1.52.1 and the
module ABI (137, N-API 10) are unchanged; SQLite goes from 3.53.0 to 3.53.4.
A comment at the default says why it must stay on 24.x at 24.20.0 or later,
and a test pins that, along with the workflow passing only the target.

Validation, on Windows 11 x64:
- Node's own regression test for the crash
  (test-process-exit-after-fetch-throw.js, from the v24.21.0 tag), 300 runs
  per arm, against the official win-x64 node.exe of each version (zips checked
  against nodejs.org's SHASUMS256.txt): v24.16.0 aborted 300/300 with
  0xC0000409 and that async.c line 94 assertion, and the test as written
  failed 300/300; v24.21.0 passed 300/300. With the launchers' --liftoff-only
  both passed 300/300, since that flag only removes this test's trigger.
- build-bundle.sh win32-x64 builds the bundle on this machine. Its node.exe is
  byte-identical to the official v24.21.0 one. --version through the Git Bash
  launcher, cmd and PowerShell, then init, status, query, callers and sync on
  a small TypeScript and Python project all exit 0, on the wasm path and with
  the native kernel bundled.
- The real CLI (index, status -j, sync) run 300 times per runtime on the
  bundled app: 0 nonzero exits on either.
- The runtime-facing test files (WASM flags, node:sqlite backend and open
  errors, launchers, worker teardown, version check) pass under Node 24.21.0:
  9 files, 65 passed, 3 skipped (POSIX-only).
- The new test fails on main's script (expected 16 to be greater than or
  equal to 20) and passes with the fix.

Fixes colbymchenry#2617

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s process (colbymchenry#2808)

Root cause: scripts/add-lang/check-grammar.mjs ran the parse loop in its own
process and printed "RESULT: PASS" as soon as the loop finished. A grammar
can finish every parse and still take the process down after that. With the
issue's vim grammar (tree-sitter-wasm 2.0.4) on Node 24, V8's optimizing tier
aborts the process from a background compile job ("Fatal process out of
memory: Zone") after PASS is printed, so the verdict and the exit code
disagree. The check also ran without the V8 flag indexing always uses
(--liftoff-only), so it judged grammars in a setup codegraph never runs them
in, and an iteration count of 0 or a word passed without a single parse.

Fix: the script runs the grammar in a child process, the way indexing does,
and prints the verdict only after that child has exited.
- The child gets indexing's V8 flags (WASM_RUNTIME_FLAGS) and loads every
  vendored grammar beside the one under test, like a polyglot index.
- It parses the sample with one reused parser, walks every node of each tree
  and frees it, as extraction does. Every parse of the same sample must give
  the same tree.
- It then parses the sample repeated to 1 MiB, the most indexing reads from
  one file, within the 10 s indexing gives one file. Only a crash or a hang
  counts there.
- The parent fails the grammar when the child dies (any exit code or
  signal), runs past a timeout (CODEGRAPH_CHECK_GRAMMAR_TIMEOUT_MS, 120 s by
  default), or reports ERROR trees or differing trees. An iteration count that
  is not a whole number above 0 is refused (exit 2).
The add-lang skill now says what counts as a FAIL.

Validation:
- New __tests__/add-lang-check-grammar.test.ts. A preload marks the process
  that runs WebAssembly, records its flags, and on request kills it or makes
  it hang at exit, after the parses. With main's script 4 of 5 tests fail:
  PASS printed for a process that then died, a hang until the test's 60 s
  limit, no --liftoff-only in the parsing process, and "0" passing. With the
  fix all 5 pass. The fifth (the tree-sitter-wasms Lua ABI-13 grammar fails
  with ERROR trees) passes on both.
- The issue's vim grammar (sha256 df3906a0, the issue's file) on Node 24.21.0:
  main's script printed PASS and then died with the Zone OOM, exit 3, in 3 of
  3 runs. The new check runs it with --liftoff-only, as indexing does, where
  it parses cleanly, 1 MiB included, and passes in 3 of 3. A copy of the new
  script with the flag withheld fails it in 3 of 3: "the parses finished, but
  the process running them then died (exit code 2147483651, 0x80000003)".
- Old vs new on the 19 vendored grammars with real samples, plus the
  tree-sitter-wasms Lua and the vim grammar, on Node 22.22.0 and 24.21.0
  (84 runs): the same verdict every time, except vim on Node 24, where main's
  PASS came with exit 3. No grammar's parses diverged.

Fixes colbymchenry#2464

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… called (colbymchenry#2809)

Root cause: LanguageExtractor in src/extraction/tree-sitter-types.ts declared
an optional extractVariables hook, documented together with the VariableInfo
type it returned, but nothing ever called it. Variables are named by the
core's own extractVariable, from its per-language branches and the
variableTypes list. A language author who wrote the hook got nothing back,
with no error and no warning; the reporter hit this while adding Nim.

Fix: remove the hook and VariableInfo. No language implemented the hook,
nothing else referred to either one, and the module is not part of the
public API. The variableTypes doc now says how the core finds the declared
names, and that a grammar which nests them deeper gets its own branch in
extractVariable, as the add-lang skill already says. Wiring the hook in was
the alternative, but nothing needs it, so it would be an API no language
exercises; the open Nim PR uses a core branch instead.

Validation:
- New __tests__/language-extractor-hooks.test.ts reads LanguageExtractor's
  members with the TypeScript parser and checks that the extraction core
  reads each one through an extractor receiver. On main it fails, listing
  extractVariables; with the fix both tests pass, and all 59 remaining
  members are read.
- The compiled JavaScript is byte-identical: of 1,164 dist files hashed, only
  tree-sitter-types.d.ts and its map changed.
- expressjs/express (0dee2c52, 214 files) indexed before and after, on the
  native-kernel and the wasm paths: the graph dumps are byte-identical
  (12,450 lines; 1,135 nodes, 2,258 edges).

Fixes colbymchenry#2465

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry and others added 28 commits November 1, 2026 05:11
…t's one method (colbymchenry#3175)

PR 1926 keeps a Python receiver's text, so that a bare name can't land on a
list method. No strategy reads a subscript in that text, so `stats[i].write()`
stopped linking at all, where 1.6.2 reached the project's method by name:
rocksdb lost `cond1_trigger["col-fam-A"][0].get_message()`, entry lost/41 of
the comparison.

An element's call now takes the project's one method of the name, in the
file's own module or one it imports, and not a name a list or a dict has, or
a dunder. The rule is narrower than 1.6.2 on purpose, each limit from reading
what a wider one added:
- no word scoring: netbox's `context['object'].get_absolute_url()` is no
  `ConfigContext`, and `module_bays[1].clean()` no `ModuleTypeImportForm`;
- the module in hand: netbox's `children[0].process_rhs()` is a Django
  lookup's, not a mixin's in a module its file never names;
- nothing past the element: django's `connections[db].settings_dict.get()` is
  a dict's, whatever `MultiValueDict` shares with its name, and
  `tournaments[0].pool_set.all()` a manager's, not `QuerySet`'s. Those stay
  unlinked, as on main.

Validation against main 4499d00:
- Tests: 3 of 6 fail with the rule off, all pass. 5 mutations each fail a test.
- rocksdb +14: the 4 `get_message`, `Policy`, `PolicyStats`, `MissRatioStats`,
  `CostClassEntry`, `BlockAccessTimeline`. django +36. netbox +3. All 53 read,
  all true, and 1.6.2's false `pop` and `values` stay out.
- mealie, flask, redis, gin, leveldb: identical.
- No extraction change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…colbymchenry#3176)

git ls-files lists every tracked path, including the skip-worktree ones a
sparse checkout keeps out of the working tree: 31,837 of the 36,918 indexable
paths in a sparse clone of elasticsearch. a565f21 (the viewer trails fix) made
validatePathWithinRoot judge a missing path by its nearest existing ancestor,
one realpathSync per missing folder. A write needs that, because mkdir follows
a link out of the root. Every read paid for it too: about 3 ms per missing path
on the main thread, in framework detection and again in the batch reader. On
that clone init took 239 s against 84 s on 1.6.2, and died to the 60 s watchdog
when detection ran past it. sync after one edit took 96 s against 29 s: a CPU
profile puts that in the same detection pass, lstat under realpathSync under
validatePathWithinRoot under probe. This regression never shipped.

A read of a missing path finds nothing, wherever the path leads:
- validatePathWithinRoot takes a readOnly option that returns a missing path at
  once. It is opt-in, so anything that may write keeps the walk by default, and
  the walk is unchanged: .codegraph/ui as a link out of the project plus a
  missing trails folder is still refused. A path that exists is judged in full.
- allowSymlinkEscape returns a missing path at once too, because with the root
  in place the walk could only answer yes.
- Framework detection passes readOnly. The batch reader and indexFile already
  read with allowSymlinkEscape.
- Every other caller stays as it was. resolveProjectFile carries the one write
  path (saved trails), and the other strict reads ask about a few paths per
  request and treat null and a missing path alike.

The scan also stops listing what isn't there. ls-files -t marks skip-worktree
entries, and one that isn't on disk is dropped. The bit alone doesn't say the
file is gone, since update-index --skip-worktree keeps it in place, so the disk
decides, at about 10 us per entry. Sparse paths never read as deleted: a sync
removes a row only when its file has gone, as before. They also stop being
recorded as failed on every sync, which on elasticsearch was 3.4 MB of dirty
paths in the metadata. That list is also how status noticed a widened cone, by
accident: 1.6.2 and main report the new files as added after a sync. status now
reads up to date until the next sync picks them up, which main already does
after an init, and every version does for a narrowed cone.

Validation, medians of 3 interleaved rounds on Windows, same Node for all arms,
wall / CPU seconds:
- elasticsearch sparse, init: 1.6.2 84.2 / 203.7, main 239.4 / 330.7 (watchdog
  off; with it on the run died at 124 s), here 47.3 / 137.5.
- elasticsearch sparse, sync after an edit: 29.2 / 28.6, 95.7 / 95.1, 13.6 / 13.8.
- drupal-core sparse, init: 15.8 / 26.1, 34.6 / 41.6, 9.2 / 19.0.
- drupal-core sparse, sync after an edit: 3.3 / 3.1, 3.4 / 3.3, 2.7 / 2.7.
- The same elasticsearch source with no .git: main and this branch within noise.
Graph dumps are byte-identical to main on 7 repos: elasticsearch and drupal-core
sparse, the no-git copy, excalidraw, express, flask and gin, and on elasticsearch
after a sync too. 9 of the 14 new tests fail on main, none of them on timing: they
count realpathSync calls and use real sparse checkouts. Undoing any one piece of
the fix fails at least one test. The trails link tests pass on Windows, junction
and symlink cases included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry#3170)

`new List<CatalogBrand>()` in a Blazor page class `List` linked the page: a
type's namespace wins for a bare name, and the type arguments weren't read.
A name written with them now means a type declaring as many, in the exact
match, the fuzzy fallback and the ASP.NET name heuristic, so `IProbe<T>` also
picks the right one of `IProbe` and `IProbe<T>`.

jellyfin's `int year = 0` and its `year.ToString(…)` reached `Year : BaseItem`:
the receiver patterns read no keyword type, so the receiver's name led to a
class, which took what it inherits. A keyword type is now read: such a
receiver names no class, and an extension method is the only project method
it has. A standard method name a class only inherits isn't taken through a
word either.

0b19444 made a call of a method's own name on another object no guess, which
lost serilog's `sequence.Elements.Select(e => e.LiteralValue())`, real
recursion over a value's children. A C# extension method calling itself on an
element a lambda receives stays one.

A declaration's node starts at its attributes, so both are read past them. A
type's parameters come from its declaration, however many attribute lines
stand above it. A method is an extension method by its first parameter, found
from the method's name, which an attribute line, a wrapped list or
`[NotNullWhen(false)] this string? s` no longer hides.

Validation, base 4499d00 against this change (numbers in the PR):
- nopCommerce 51f116ac: 165,195 to 164,628 edges, 21 moved, 567 lost: 565
  `new List<T>()` links to `.cshtml` views named `List`, 1 string `Equals`.
- jellyfin a46fa455: 72 moved (`IProbe`-style arity picks), 31 added (extension
  methods on strings), 3 lost (`year.ToString`).
- serilog 81af75f3: 25 lost (`value.ToString` on `decimal`, `float`…), 1 added
  (`e.LiteralValue`). eShopOnWeb f21d6233: 12 lost, all `List<T>`.
- modular-monolith 91c8ef24: 36 moved by arity. Controls byte-identical.
- Index CPU on jellyfin, median of 5 interleaved: 38.5 to 37.3 s. The last
  paragraph's read, on the jellyfin subset: 16.0 to 15.9 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ess from elsewhere (colbymchenry#3171)

rails' activestorage bundle writes `Sha256.prototype.update = function…`
inside a closure, where the index holds no node for it, and calls
`buffer.update(data)`. The one method named `update` in the project was turbo's
`History#update` in the guides' assets, so the call went there; 1.6.2 had the
same guess in the unbundled source, and main also in the two bundles, since the
object literal's arrow functions became symbols.

The lone-method guess now passes over a file that assigns a function to
`X.prototype.<name>` itself. A guess from any other file stands.

Validation, base 4499d00 against this change (numbers in the PR):
- rails-src: the 2 `buffer.update` guesses in the bundles go. The third, in the
  unbundled sha256_algorithm.js, is 1.6.2's too: the file writes no such member.
  express, taxonomy, ai-chatbot,
  excalidraw: byte-identical. Controls gin and flask byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olbymchenry#3177)

git says nothing when a sparse checkout changes shape. The files that come and
go are skip-worktree, or were, so git status stays clean and HEAD stays put.
status used to notice a widened checkout by accident, because left-out files
were on record as failed, and colbymchenry#3176 rightly stopped that. No version noticed a
narrowed one.

The index now keeps a stamp of the sparse checkout beside its commit stamp: a
hash of info/sparse-checkout and of the config lines that switch it, because
git sparse-checkout disable leaves the patterns as they were. A full index and
a full sync write it, read before they scan, so a change during the run still
shows. A sync scoped to some paths leaves it alone, so the watcher's path is
as it was. On its git fast path getChangedFiles compares the stamp, and when
it moved, adds the paths where a scan and the index disagree to its candidates.
The existing check of each candidate against the disk then says added or
removed, so a clean tree can't short-circuit this. An index with no stamp
reports nothing, and the next full sync starts one.

How a repo that isn't sparse is told apart: it has no info/sparse-checkout in
its git dir. That costs a failed read, with the git dir found without git: the
nearest .git upward, as a folder or as the gitdir line of a file, which covers
a linked worktree, a submodule's own checkout and a project below its repo's
root. With the file there, the stamp is a hash of up to three small files, still
with no git process. Only a stamp that moved takes a scan, and it keeps doing so
until a full sync takes the new stamp. A submodule's or an embedded repo's own
sparse state seen from the repo around it is out of scope.

Validation:
- 23 new tests on real sparse checkouts: cone mode, a sparse index and hand-written
  patterns, widened right after an index and after a sync, narrowed, widened then
  synced, turned off, a linked worktree, a project below its repo's root, an index
  with no stamp. 20 of them fail on main. Nine mutations, each undoing one
  piece, each fail at least one.
- The real CLI, on a 300-file sparse repo and on a linked worktree with a sparse
  index: Added: 300 after widening, right after an init too, Removed: 300 after
  narrowing, and up to date after each sync.
- No new spawn: the tests compare a status with the stamp against one without it,
  on a repo that isn't sparse and on a sparse one that hasn't moved. On gin the
  git commands status runs are identical on main and here, 10 on the first call
  in a process and 4 after.
- status on django, 7,123 files and not sparse, both arms over the same index,
  interleaved: 0.987 s on main and 0.981 s here (median of 20), getChangedFiles
  161.4 ms and 163.8 ms (median of 96).
- A stamp that moved on a sparse drupal-core clone: status takes 1.0 to 1.2 s
  against 0.75 s until the next sync, after a first run of 2.6 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e 1.6.2 did (colbymchenry#3178)

A sync after an edit took more memory at its peak than 1.6.2 on several repos:
caddy 188 to 264 MB, django 350 to 437, rails-src 358 to 413. Nothing is held.
At the end of caddy's sync the live heap is 33 MB of 178 MB: Node 24 lets the
new space double up to 64 MB a side, and a short run that allocates a little
more than it did lands one doubling higher.

Every way the CLI starts now passes --max-semi-space-size=16: the bundled
launchers, the npm shim and the re-exec. It has to be on the command line, since
setting the flag at run time doesn't reach the main thread's heap. Workers get it
too, which is what resolver workers already asked for by their own limit.

Peak RSS of a sync after an edit, main to this, 3 rounds: caddy 258 to 193 MB,
tokio 196 to 164, excalidraw 223 to 168, with wall and CPU level. 1.6.2 on caddy
is 190.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ss a slow file (colbymchenry#3179)

A small project paid for threads it couldn't use. Eight workers started however
many files there were, each loading its modules and grammars, for a few hundred
ms of parsing: 1.6.2 did that too, and since then loading has grown, by about
half a second of CPU on flask. The pool now starts what the parsing to come is
worth. W workers take parseMs / W, so one more saves parseMs / W^2, and it stops
when that falls below a worker's cost. It takes a typical file's time until it
has measured a few of its own, and grows if the files prove slow.

A parse worker also loaded every framework resolver at start, which is half its
modules, for a project that may use none. They now load on first use.

Parsing could run only 2 files per worker ahead of the ordered commit, so one slow
file at the cursor stopped every worker once the window was out: on rocksdb they
sat idle more than half the time. It is 8 per worker now. Files still commit in
order, so the graph is the same, and what waits is a few MB.

Interleaved, 5 rounds, this against main: gin 4.28 to 2.81 s CPU, 365 to 259 MB;
flask 3.77 to 2.61, 359 to 223; taxonomy 3.27 to 2.19, 405 to 270; ripgrep 5.37 to
4.23, 444 to 298. Wall is level or lower.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry#3180)

Reading a node built an object keyed by column and then a second one by hand.
node:sqlite can hand a row over as an array, about a third cheaper, on Node 22.16
or newer; an older runtime keeps its objects.

The resolver stat'ed a file and then read it, and on Windows that's a second walk
of the path, a quarter of the read. It now opens the file and asks the open file,
with O_NONBLOCK so a FIFO with no writer can't park the open, as the trail store
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce (colbymchenry#3181)

The self time in C++ resolving rose from 93 to 109 s on rocksdb, in code added
since 1.6.2: a flat tail of whole-file scans and of the same questions asked again
for every reference. cppNonCodeLines and cppLiteralsBlanked walked strings a
character at a time and built arrays of them; they now read char codes and work by
slices. The answers about a candidate from one file, about a caller's class, about
a type as written and about an include search are remembered: on rocksdb the
questions asked are 3 to 15 times the distinct ones.

None of what's remembered reads an edge, so it holds through the resolve; what
depends on a reference has the reference's place in its key. The memos of one
file's candidates keep to the last 256 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m one file holds (colbymchenry#3184)

tokio's `use crate::process::unix::GlobalOrphanQueue;` linked nothing on
main, where 1.6.2 linked it. Since PR colbymchenry#3014 a `use` path walks the crate's
modules, and tokio's `mod unix { pub(crate) use super::imp::*; }` globs a
module with a `#[path]` for each cfg: `unix/mod.rs` or `windows.rs`.
rustSubmodule answered null for such a module, so the glob couldn't be
followed and the import had no target.

The fix: rustSubmodule keeps every file the alternatives name
(RustModule.choices), and rustCfgChoiceItems looks a name up in all of them.
What one alternative holds is that one's, whichever the build takes; a name
two of them hold is the build's choice and stays unread, as PR colbymchenry#3064 pinned.
A step further down the path walks each alternative the same way.

Validation:
- rust-path-attribute-modules.test.ts: 2 of its new cases fail on main and
  all 9 pass.
- A/B against main 4499d00: tokio +7 imports, all true (the reported edge,
  `Wait` and `OrphanQueue` through `imp::orphan`, `AtomicU64` through tokio's
  cfg'd `imp`), and 2 calls keep their target by the import instead of by
  name. ripgrep, clap, serde, dioxus, gleam and axum byte-identical (axum's
  24 "moved" are the diff tool's duplicate route nodes, identical by row);
  controls flask and express identical.
- CPU, dioxus, median of 5 interleaved: 33.97 to 33.28 s. Resolution only, so
  no kernel change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s types (colbymchenry#3185)

goImplementsEdges matched method names and, since PR colbymchenry#2447, parameter and
result counts. Go wants the signatures identical, so a type with the right
number of the wrong types passed: caddy's TLS matchers, with
`Match(*tls.ClientHelloInfo) bool`, "implemented" its HTTP `RequestMatcher`,
whose `Match` takes an `*http.Request`, and every HTTP matcher its TLS
`ConnectionMatcher`. Each false entry also sent calls through the interface
to a method they never reach, and took a place under the per-interface cap.

The fix: at the counts that fit, typesFit compares each parameter's and
result's type, read off the stored signature (goItemTypes,
goComparableType). A type compares by its name with the package left off,
since the two declarations may write `Context` and `caddy.Context` from
different packages. Nothing is ruled out by a true alias, by a type
parameter of the type or of what it embeds, by a `func`/`struct`/`interface`
literal type, or by a method a standard-library embedding brings, whose
types the table doesn't hold. A gRPC server keeps crossing to its client
interface at the server's counts.

Validation:
- go-implements-types.test.ts: 4 of 9 cases fail on main, all pass.
  go-promoted-dispatch.test.ts pinned the old behaviour (two series sets
  implementing each other's interface by name) and now says what Go says.
- A/B against main 4499d00, implementers dropped: caddy 24, prometheus 261,
  etcd 36, hugo 169. A separate judge reads each dropped pair's signatures
  through embedded types with qualifiers and names removed: 490 of 490 differ.
  Calls dropped with them: caddy 24, prometheus 636, etcd 36, hugo 98.
  Added: prometheus 8 implementers and 30 moved dispatch edges, real
  implementers the false ones had pushed past the cap (`blockSeriesSet`,
  `seriesSetAdapter`). gin and cobra byte-identical; controls flask and
  express identical.
- REG's 7 false entries on caddy are gone. The 11 real implementers caddy's
  `Provisioner` and `Unmarshaler` still lose against 1.6.2 are the cap's: both
  arms keep 40 real ones, of more than 40, in file order. That isn't changed
  here; raising the cap to 400 adds 239 implementers and 467 edges on caddy.
- CPU, median of 3 interleaved: hugo 22.75 to 22.44 s, etcd 21.19 to 22.00 s
  (wall 7.23 to 7.14 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alue the other (colbymchenry#3186)

Alamofire's `Protected` declares `write(_ closure: (inout Value) throws -> U)`
and `write(_ value: Value)`. Their labels are alike, so retargetSelfOverload
kept whichever the ranking found first: every `didStop.write(true)` reached
the closure overload. A trailing closure wasn't read at all, so
`write { $0 = value }` inside `write(_ value:)` called itself, as did
Kingfisher's `clearCache { … }` inside `clearCache() async`.

The fix: swiftOverloadForClosure reads the call's labels and whether its last
argument is a closure, trailing or in the parentheses, and takes the one
overload whose parameter for that argument agrees. An `@autoclosure` parameter
takes a value, and the `{` after an `if`, `guard`, `while`, `for` or `switch`
condition is the statement's. It only runs for an overload set in which some
overload takes a function, and each overload's parameters are read once.

Validation:
- swift-overload-closure.test.ts: 3 of 6 cases fail on main, all pass. Fixture
  sw1 had reproduced this all along: REG read it as clean because 1.6.2 is
  wrong in the same way there.
- A/B against main 4499d00, Swift calls moved: Alamofire 46, Kingfisher 67,
  vapor 1, SnapKit 0; nothing added or lost. All 114 read against source.
  The 26 `write(value)` sites go to `write(_ value:)`, `adapt(_:using:)` with
  a trailing closure to the overload with that label, `cURLDescription { … }`
  to `cURLDescription(calling:)`, and Kingfisher's async wrappers from
  themselves to the completion-handler overload. Controls flask and express
  identical.
- CPU, Kingfisher, median of 7 interleaved: 5.33 to 5.19 s. Resolution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eter is typed (colbymchenry#3187)

Java overloads were told apart by nothing but the count, and only when one
sibling alone fitted it. With two of a count the first declared won:
jsoup's `super.before(node)`, new on main since 2577601, reached
`before(String)`, and rocksdb's `it.seek(k2)` with a `ByteBuffer` reached
`seek(byte[])`. With several of the right count and a target of the wrong one
the wrong one stayed: every `Jsoup.parse(html)` reached the two-argument `parse`.

The fix: retargetSelfOverload, which every Java call passes through, asks
javaOverloadsFitting. It keeps the overloads of the right count, reads each
argument's static type where its text says (a literal, a cast, a `new`, `this`,
a local or parameter, a field of the class or one it inherits, `x.f` and
`Type.FIELD`, an element of an array, or a call on one of those, by what that
method returns), and scores it against each parameter as Java does: the same
type, widening before boxing, a project type's supertypes, variable arity last
when every argument was read. An overload the compiler would refuse goes, and a
private one goes from another file. A name the project doesn't declare may be a
type parameter, so it refuses nothing. When the arguments don't settle it the
ranking's pick stands, a pick that was a private overload of another file stays
unresolved as it was rather than become a guess between the rest, and a set with
a Kotlin overload goes on to the count check as before.

A lambda's parameter is the other half of REG's list: `tb` in jsoup's
`assertAdoptionRecovery(new HtmlTreeBuilder(), tb -> { tb.process(…); }, …)`
linked by a name guess on 1.6.2 and nothing on main. javaLambdaParameterType
reads the type from the method the lambda is passed to, when that is a
receiver-less call to a method of the same file taking a JDK functional
interface with a type argument of its own: a rule, not the guess back.
`list.forEach(x -> …)` says nothing until `list` is typed, so it stays out.

Validation:
- java-overload-arguments.test.ts: 16 of 22 cases fail on main, all pass.
- A/B against main 4499d00, calls moved to the overload that fits (the wrong
  argument count before / told apart by type / variable arity):
  jsoup 2,002 (1,347 / 636 / 19), rocksdb's java/ 1,136 (987 / 146 / 3),
  Minestom 711 (351 / 349 / 11), spring-ai 881 (141 / 645 / 95),
  kestra 1,235 (498 / 617 / 116), elasticsearch 2,860 (770 / 1,649 / 438),
  okhttp 2.
- Read against source: every call moved by a call's return type or a field on
  jsoup, Minestom and rocksdb (about 180), every call decided by score with an
  argument unread on jsoup, Minestom and rocksdb (130: one still wrong,
  Minestom's `wrap(MemorySegment.ofArray(bytes), …)`, and wrong on main too),
  plus random samples of 70 on jsoup, 50 on spring-ai, 45 on Minestom, 30 on
  rocksdb and 30 on elasticsearch.
- Left open: where an argument can't be read (a chain, a library call) and the
  overloads of its count differ there, the ranking's pick stands, or the first
  of the count when the ranking's had the wrong count. jsoup 28 of the 2,002,
  rocksdb 209 of 1,136, Minestom 70 of 711, spring-ai 24 of 881. Samples of 26 on
  Minestom and 16 on rocksdb found 4 of those on the wrong overload, all of them
  wrong on main as well: `db.newIterator(handles.get(1))`, a `List` element.
- Added, all true: jsoup 7 lambda calls and 5 `remove(String)` calls, which had
  reached a private `remove(int)` in another file and been refused; spring-ai 4
  and kestra 35 lambda calls; elasticsearch 10 lambda calls and 25 of the
  private overload's: 19 `Errors.merge`, 3 `Checkpoint.write`, 2 `shardContainer`
  and 1 `executeFetchPhase`.
- Lost: spring-ai 4 and elasticsearch 2 `super.x(…)` calls into a library class
  (commonmark's `AbstractVisitor`, Lucene's `FieldInfos`) that reached a sibling
  overload of the caller. Those calls leave the project.
- REG's rocksdb/added/10 class: `it.seek(k2)` and every such call in
  WriteBatchWithIndexTest now reach `seek(ByteBuffer)`.
- Controls flask, express and serilog identical; no other language changes.
- CPU: the Java PRs together, elasticsearch, median of 5 interleaved rounds, 151.03 to
  153.81 s, +1.8%, with wall -4.1%; Minestom +0.2%. The cost is reading the
  arguments of the calls that have overloads, which is what moves about 3,000 of
  elasticsearch's calls. Resolution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on a namesake (colbymchenry#3188)

Four false readings of which type a Java name means.

javaAncestorNames walked up the supertypes with one memo for every type,
filled at whatever depth a type was first reached. A type met at the depth
limit was remembered with its own header's names alone, and every shallower
walk through it stopped there, while a walk that found a finished entry reached
past the limit, so what a bare `Reader` could mean turned on which reference ran
first. Each resolver worker keeps its own memo and sees the chunks the pool
hands it, so the graph changed with the pool's size: two cold indexes at one
size agree, as REG saw, but on elasticsearch the default pool and 2 workers
differ in 16 edges and references, and `GetSynonymsAction.Response` lost the
`Reader` it inherits from `Writeable`. Each type now walks to the top for itself
and memoizes only the finished answer. csharpAncestorNames had the same shape
and gets the same walk.

The walk also went through every type of a supertype's name.
`InternalGeoGrid implements GeoGrid` means its own package's interface, but a
record `BlockLoaderFunctionConfig.GeoGrid` shares the name and implements its
owner, so `BlockLoaderFunctionConfig` passed for an ancestor, the record for a
nested type in reach, and with paths too deep to tell them apart the record took
the `implements`. Now a supertype's name means the types its file has in reach,
when it has any.

With every inherited name now found, a name written through a qualifier needs
the qualifier read: `java.io.Reader` is no `Writeable.Reader` the class inherits,
and `IndexFieldData.Builder` is the one nested in `IndexFieldData`, not the
`Builder` the enclosing class declares. What's written directly before this
reference decides, not what the line holds elsewhere: an anonymous class's
`new BucketOrdsEnum() {` below `LongKeyedBucketOrds.BucketOrdsEnum delegate = …`
stays its own file's.

`Template::lifecycle` matched every member of that name, and since records
have accessors a record `Template` nested in another class made two candidates
in other files, so the reference resolved to nothing. Now it means the
`Template` the file can see, as a bare type name does.

Validation:
- java-supertype-reach.test.ts: 6 of 9 cases fail on main, all pass, none by
  timing: `Deep` sorts first and poisons the memo for `Near`.
- Worker order: elasticsearch's default pool, 2 workers and 5 workers now give
  identical dumps of every edge and unresolved reference; on main the default
  pool and 2 workers differ in 8 lines each way.
- elasticsearch, A/B against main 4499d00: `GeoGrid` and 2 `Range` implements
  back on their own package's interfaces, which moves 6 `super.x()` calls to the
  real parent, drops 8 interface-impl edges of the records and adds 3 real ones;
  4 `Reader` and 2 `BucketComparator` references and 2 `Template::lifecycle` back;
  266 references through a type move to the type it names; 6 through a package
  drop (`org.apache.lucene.search.ScoreMode` and the like), all false before.
- spring-ai: 55 method references gained (`Generation::getOutput` and the like),
  each on the type the file imports; 50 references through a type moved to it.
- kestra: 39 references moved, each to what its qualifier names
  (`io.kestra.jdbc.AbstractJdbcRepository`), 6 through a package dropped.
- jsoup 6 and Minestom 1 references through a package dropped
  (`java.util.regex.Matcher`, `java.lang.Class`, `java.util.Comparator`), all
  false before; okhttp and rocksdb's java/ unchanged by this class.
- C#: serilog, eShopOnWeb, modular-monolith and jellyfin identical to main.
  Controls flask and express identical.
- Seen outside this change: pascal-scope's classNamed keeps a depth-limited memo
  too, for a chain of 5 or more aliases; no fixture reached it, so it stays.
- CPU: with the Java overloads PR, which has the numbers. Resolution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn (colbymchenry#3189)

The value-reference pass links a symbol to the same-file constants its body
reads, and it matched identifiers by name alone: elasticsearch's
`RerankRequest.parseStringOrObject` passed for a reader of its own `PARSER`
because it reads `InferenceString.PARSER`.

The fix, in the wasm extractor (isForeignJavaField) and the kernel
(is_foreign_field) alike: an identifier that is the field of a `field_access`
counts only through `this` or through the class that declares the constant.

Validation:
- java-value-reads.test.ts: 1 of 2 cases fails on main without the kernel, all
  pass with and without it.
- Kernel parity, java: jsoup, Minestom and rocksdb's java/ 2,267 of 2,301 files
  byte-identical, 34 deferred to wasm, 0 with diffs; elasticsearch's action and
  cluster packages 1,269 of 1,270, spring-ai and kestra 5,488 of 5,489, 0 with
  diffs.
- A/B against main 4499d00: value references dropped, elasticsearch 226,
  Minestom 72, kestra 19, jsoup 1, spring-ai and okhttp 0. A check over each
  dropped edge's source finds no bare, `this.` or owner-qualified read of the
  constant in any of them (`LeafBucketCollector.NO_OP_COLLECTOR` passes for one
  by its suffix, 3 times: it is a read of another class).
- Kotlin and Swift have the same reader, and it matters little there (2 of 31
  and 3 of 496 on nowinandroid and okhttp), so they stay as they are.
- CPU: with the Java overloads PR, which has the numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bymchenry#3190)

Since 01e0ac3 a Kotlin receiver chain stops at a type the project
doesn't declare, and gets no edge: right for `Regex`, but nowinandroid's
`viewModel.uiState.value.shouldKeepSplashScreen()` goes through
`StateFlow<MainActivityUiState>`, whose `.value` is written in the
declaration. 1.6.2 linked it, by a name guess that happened to be right.

The fix: KOTLIN_VALUE_HOLDERS names the library types whose member is one of
their type arguments (`StateFlow`, `MutableStateFlow`, `State`, `MutableState`,
`LiveData`, `MutableLiveData`, `Lazy`, `Pair`, `Triple`), and the chain reads
on as that argument, from the declaration of the segment before it. Any other
library type still stops the chain, so no guess comes back.

Validation:
- kotlin-value-holder-chains.test.ts: 3 of 4 cases fail on main, all pass.
- A/B against main 4499d00: nowinandroid +1, the reported edge; mihon +2,
  `extensionManager.value` and `downloadManager.value` on `Lazy` properties;
  okhttp no call changed. Nothing lost. Controls flask and express identical.
- CPU, mihon, median of 3 interleaved: 20.02 to 19.86 s. Resolution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…against (colbymchenry#3191)

A Kotlin Multiplatform `expect` and its `actual`s share one qualified name,
and a reference through an import took the nearest declaration, the `expect`
on a tie. That suits common code only. PR colbymchenry#3023 sent Java's calls through this
pick, so `Util.go()` from `app/src/main/java`, which 1.6.2 linked to the
`androidMain` `actual` by a name guess, reached the `commonMain` `expect`, and
so did `extends Timber.Tree`, which 1.6.2 left unresolved. An `expect` isn't in
JVM bytecode, so a Java caller can never reach one. Kotlin in `androidUnitTest`
or a plain Android module went the same way.

The fix: pickJvmDeclaration chooses by what the caller compiles against, for
the owner of an imported reference and for the import itself. Common source
sets (`commonMain`, `commonTest`, okhttp's `commonJvmAndroid`) keep the nearest
declaration. A platform's source sets take that platform's `actual`, so
`androidUnitTest` takes `androidMain`'s, or that of a target they build on
(`iosArm64Main` from `iosMain`), and otherwise the `expect`, as an intermediate
set like `nativeMain` sees. Java and a plain Kotlin module take a JVM target's
`actual`, never `iosMain`'s: the one their path names, else not Android's, so
okhttp's samples reach `jvmMain` and its `android-test` `androidMain`.

This reimplements the idea of PR colbymchenry#3136 by @johnhldiver-beep, who found the
cause. Beyond it: `commonTest` stays on the `expect` (that PR's check named
`commonMain` alone), `androidUnitTest` finds `androidMain`, Java never lands on
a native target's `actual`, and the import edge agrees with the calls.

Validation:
- kotlin-expect-actual-callers.test.ts, 10 cases built from the issue's repro
  and timber's and okhttp's shapes: 8 fail on main, all pass.
- 1.6.2 run first: the Java cases link the `actual` by a 0.65 guess, so this is
  a regression from colbymchenry#3023, as the issue says.
- A/B against main 4499d00: okhttp 9 imports and references move from the
  `expect` in `commonJvmAndroid` to the platform `actual`, each to the target
  its module builds for; nowinandroid, mihon and every Java repo unchanged,
  having no `expect`. Controls flask and express identical.
- CPU, mihon, median of 3 interleaved: 20.02 to 19.86 s. Resolution only.

Fixes colbymchenry#3033

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scans (colbymchenry#3182)

Each resolver worker read the same whole-project facts for itself as it first
needed them: the namespace macros and aliases, and where every include resolves.
The main thread now reads them once and gives them to the workers as they start,
along with what it probed on disk. A worker that gets nothing reads for itself.

That reading is one synchronous stretch of the thread the liveness watchdog
watches (colbymchenry#1091), so it is bounded. Only a full index does it, whose parse has just
read every file: a sync may find them cold, at 5.7 ms a file on redis against 0.3.
And only up to 2,500 C and C++ files: 0.6 to 1.1 ms a file on rocksdb on a busy
machine, includes and all, so 3 s at worst. Past that, each worker reads for
itself off the main thread, as it always did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…usy (colbymchenry#3183)

The pool started 6 workers on any large project, though the main thread saves
every batch itself and keeps about 4 busy. A further worker mostly waited, having
cost a second or two of CPU to load and to compile the hot code in its own isolate,
a share of the others' cache hits, and a few hundred MB. That's a large part of why
rocksdb's CPU rose 31% for a wall rise of 8%: V8's background threads went from 12
to 25 s, mostly compiling, for more code in every isolate. The pool now takes at
most 4, and fewer when a run projects little work.

Workers 6 to 4, 2 rounds: rocksdb 125.9 to 107.3 s CPU, 2780 to 2425 MB, best wall
37.0 to 35.8 s; react 113.4 to 103.2; django 70.0 to 60.0; vscode-be 53.2 to 47.3,
wall 14.4 to 12.2. Dumps of the whole stack are byte-identical on 24 repos.

The rest of the waiting was the batch barrier: 10 chunks over a few workers, and
the next batch held until the slowest was done. Past the prerequisites no batch
makes an edge resolving reads, so the next one now queues behind it. A file's
chunks stay on one worker, which has its cached reads.

The WAL valve parks only at a pool-idle boundary, since a full backfill needs the
readers between statements, so it now waits for a batch with nothing queued ahead,
which the connection recycle guarantees every 8 batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oesn't scan the project (colbymchenry#3192)

The first batch of a run resolved on the main thread while the workers booted. That
batch is the prerequisites, and in C++ the first qualified base class reads every
source file for the project's namespace aliases: one synchronous stretch on the
thread the liveness watchdog watches, 287 ms on rocksdb and 20 s or more on a project
the size of LLVM (colbymchenry#1091). 1.6.2 had no such scan on that path; it never shipped.

A run large enough to start the pool up front, 150k references or more, now waits
for it before the first batch. The wait is bounded at 30 s, and a failure ends it, so
a pool that doesn't come up leaves the run on its own thread, as it would be with no
pool. That needed one more fix: a worker that failed to open the index never failed
ready(), which nobody had waited on before. Any failure does now, and so does a worker
that exits before it's ready. A run under the bar starts nothing up front and doesn't
wait for a pool engaged mid-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es a function (colbymchenry#3193)

Follow-up to colbymchenry#3187. A lambda or a method reference has no type of its own, so
the overload reader left it unread, and where overloads differ at that argument
the ranking's pick stood: Minestom's `.append(x, b -> …)` reached
`append(Argument, Execution)`, `item.withAmount(a -> a * 2)` reached
`withAmount(int)`, and `setDefaultValue(sender -> …)` the bare `T`.

The fix: javaArgumentType reads a lambda, with the count of its parameters, or
a method reference as a function value, and javaFunctionFit scores it against a
parameter. 1 for a JDK functional interface (java.util.function's, `Runnable`,
`Callable`, `Comparator`) whose method takes as many arguments as the lambda
has. 0 for what may be one: another interface, a library type, a type variable.
-1 for what can't take one: a primitive, `String`, `Object`, an array, a project
class or enum, and a JDK interface of another count. So `defaulted(s -> s)` is
the `Function` overload and `defaulted(() -> x)` the `Supplier` one. A string or
a number is no function for any JDK functional interface now, not only the ones
a lambda's parameter is read from. And a number may start at its point:
`spawnPos.sub(.5)` is `sub(double)`; `2d` is a double.

A project's own `@FunctionalInterface` isn't scored. I tried it: elasticsearch's
`parseList(parser, p -> …)` has a marked `CheckedBiFunction` in the index and its
`CheckedFunction` outside, and the marked one took 10 true calls to the wrong
overload. So two project interfaces tie, the ranking's pick stands, and Minestom's
`RegistryTaggedUnion(Registries::x, …)` stays open.

Validation:
- java-overload-arguments.test.ts: 5 of 27 cases fail on main, all pass.
- The 78 Java, JVM and resolution test files, 890 tests, pass. The change doesn't
  reach past Java: every new branch sits in the Java overload reader.
- A/B against main 1548371, calls moved: Minestom 27, spring-ai 137, kestra 45,
  elasticsearch 101. jsoup, rocksdb's java/ and okhttp unchanged, nothing added or
  lost anywhere. Controls serilog and flask identical.
- Read against source: all 27 on Minestom, and every group on spring-ai (14),
  kestra (5) and elasticsearch (37). True, except two classes that were false on
  main as well. spring-ai's 37 `SearchRequest.from(r).filterExpression("…")` sit on
  a wrong class before and after, and move between two of its overloads because
  a string is no `Supplier`. elasticsearch's 6 `stringListSetting(key, value -> …,
  Property.Dynamic)` move from `(String, Property...)` to `(String, List<String>,
  Property...)`, where `Validator` is right: `List` and `Validator` both may be
  functional, so the first of the count stands.
- The class colbymchenry#3187 left open, moved calls whose arguments don't settle the overload,
  recounted: jsoup 28 to 27, rocksdb 209 unchanged (no lambda among them), Minestom
  70 to 61, spring-ai 24 to 81. spring-ai rises because 57
  `FunctionToolCallback.builder("x", (City c) -> …)` calls now move to the right
  overload and stay open between `Function` and `Consumer`, each of one parameter.
  All calls left open, moved or not: jsoup 207 to 206, rocksdb 267 unchanged,
  Minestom 567 to 533, spring-ai 1,167 to 1,109.
- Read: all 61 left open on Minestom. 20 are on a wrong overload, all false on main
  too: 5 `RegistryTaggedUnion`, 4 `register(Key.key(…), …)`, 3 `allocateTrimmedPacket`
  with a `var` packet, 2 `Duration.of(…)`, 2 static-imported constants, 2
  `div(…get())`, 1 `setDefaultValue(Block.AIR)`, 1 on a wrong class.
- CPU, elasticsearch, median of 5 interleaved: 134.17 to 130.89 s, so no cost. Resolution only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot who asked first (colbymchenry#3194)

django's graph changed between runs of one build: 5 runs of main gave two hashes.
isPythonLocallyBound said whether a function had bound a name above the reference,
and kept that yes or no per function and name, so the first reference to ask
answered for all of them. `async_view = login_required(async_view)` hands on the
function, and the `async_view(request)` calls below read the local. When a call
asked first, the argument's edge was lost, and with a pool that depends on which
worker gets which chunk. 1.6.2 has the same memo.

It now keeps the line the binding starts at, and each reference compares its own.

That exposed a second fault the memo had hidden: a line inside a call could read as
an assignment. `Fan, through="Invitation"` matched as the tuple `Fan, through = ...`,
so every `Fan` below it looked local. Only a line that starts a statement can
assign now.

django: one hash over 7 runs on one thread, 2 workers and 4, against two for main.
+2 edges and none lost, both read: the argument of `x = wrap(x)` is the function
defined above it. flask, fastapi, requests and rocksdb are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll sites (colbymchenry#3195)

`table.foreign_key` in rails' `ReferenceDefinition#add_to(table)` says
nothing of `table`, and a guess by the receiver's word took the class
called `Table`. 1.6.2 got this one right by a substring accident, through
a rule that also sent 429 `error.message` to `WrongEncodingError`. But
`add_to` has one caller, and it passes `self`.

A new pass, `parameter-flow.ts` with `ruby-parameter-flow.ts` as its one
adapter and `db/parameter-flow-queries.ts` for its SQL: a call on a
parameter of the calling method, when every call site in the project
passes a value of one known class, is a call on that class. It runs last,
on what resolution left, and moves only the calls a flow answers, so a
method no flow types stays exactly as it was. Any call site it can't type,
two that disagree, or anything in the project that could be a call the
graph never linked gives no answer.

An unseen caller is found by counting: the project's text may write the
name no more often than its declarations and recorded calls account for.
That catches send, public_send, method(:x), &:x, alias, delegate, a string,
and a call in a class body or a top-level block, which the graph records
nowhere. A call on an unknown receiver counts as a caller unless its
arguments can't fit the method: `part.add_to(buffer, hash)` isn't a call
of `add_to(table)`.

Where the class has no such method, the guess goes only on evidence that
it answers the call itself (attr_reader, attr_accessor) or that the class
is from outside the project. Dropping every such guess lost rubocop's
delegated `target_ruby_version`; keeping them all kept 5 false edges.

A pass over rails takes about 0.7 s, on a 5 s sync, so a sync doesn't run
one unless it has to. The pass leaves a record in one metadata key:
what every flow came to, the names each reads, and how often each file
writes the names the moved calls rest on. A sync reads what a changed file
declared and called before and after, and looks again at the methods of
those names and at the flows that read them. Anything it can't settle
from that goes to the pass over the project: a class, a constant or an
ancestor changing, `initialize` defined or undefined, a flow unlike its
record, a missing record, or a run that stopped short.

Validation: rails-src 4 moved, 16 added, 1 false guess gone; mastodon's
Ruby +1; rubocop +4; jekyll 4 false guesses gone; sinatra, gin and flask
identical. Every entry read against source: all right, no true edge lost.
A sync matches a pass over the project, edge by edge, across 790 steps of
randomized edits on rails, rubocop, mastodon's Ruby files, jekyll and
generated projects, and a fresh index across 12 real edits on rubocop and
jekyll. Pool matches sequential on 5 repos. No extraction change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m moves (colbymchenry#3214)

A resolver lives as long as its CodeGraph, so a server syncs on one for days.
Eight memos in name-matcher and three in frameworks outlived clearCaches, and
the Cargo crate map and compile database's include directories were read once
for the process, so an edit was resolved by what the file said before it.

The eight go in clearNameMatcherMemos, and the framework memos renew with the
node lists, whose identity changes with the caches. Cargo manifests and compile
databases join the module-config snapshot: a sync stats them, reads one only
when its stamp moved, and re-opens Rust or C and C++ imports when what
resolution reads from it did. A crate under a glob member is found when files
come or go. importMappingCache and the two clear functions no one called go.

A pool that fails while no one waits on it leaves no rejection: each worker's ready promise rejects when a start fails, and only ready() handled that, so a pool torn down mid-boot left an unhandled rejection when the timing gave it that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PinableAgents
PinableAgents marked this pull request as ready for review November 8, 2026 13:24
@PinableAgents
PinableAgents merged commit 22cfe64 into main Nov 8, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.