Commit Graph

951 Commits

Author SHA1 Message Date
Colby Mchenry 0571286907 fix(js,ts): link calls through an import whose name contains $ (#2378)
The ES import regex in extractJSImports captured default and namespace
bindings with \w+, which stops at a `$`. So `import items$ from './store'`,
`import $ from './dom'` and `import * as ns$ from './ns'` failed the whole
statement and produced no import mapping, and `items$.getState().inc()`
never reached the store action. The `as` split of named imports, the
`a: b` split of a CommonJS destructuring and the re-export checks had the
same \w-only shape. Some bound a cut-off name instead (`{ a as b$ }` bound
`b`, `{ default as items$ }` bound `items`), and `export { a$ } from` was
dropped from barrel chasing.

Capture every binding with the ASCII identifier class [A-Za-z_$][\w$]*,
keeping the `type`-modifier lookahead: `import type from './x'` still
binds `type`, and `import type $T` / `import type from$` now bind. A
default binding is never followed by `{`, so the `${` of a code
generator's template literal (`import ${x} from '${src}'`, as in qwik's
optimizer) is not taken for a binding.

importShadowedAt and hasParameterBinding bounded the name with \b, which
never fires beside a `$`, so a local `const items$ = ...` or an `items$`
parameter did not shadow the imported store. Named `$` imports already
got those wrong edges on main, and the mapping fix would have extended
them to default and namespace imports. Bound the name with (?<![\w$]) and
(?![\w$]) instead.

Resolution-only: the kernel has no import-mapping mirror. A store
exported by `export { items$ }` (the \b in isExportedLater's clause
branch, left out of #2366) is a separate extraction fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:41:06 +00:00
Colby Mchenry 5a11cd7ab7 fix(vbnet): link fields and properties read or written through a value (#2377)
VB.NET linked a member read only through a type or module name (#2355),
so a read or write through an instance -- a parameter `x.Normal = 3`, a
local `h.Normal`, a field `_h.Title`, `Me._h.Normal` -- produced no edge,
and `codegraph callers` on an instance field or property listed almost
none of its uses.

- Extraction (TS only; VB.NET is not kernel-routed): every member read or
  write is sent with its receiver as a path, whatever the receiver's case:
  `x.Normal`, `Me._h.Normal`, `MyBase.Count`, `.Value` in a `With` block
  (nested `With .Inner` included), `.Switch` in an object initializer
  `New BoolParam With {...}`, and `{T}.X` for a value whose type the code
  writes (`DirectCast(o, T).X`, `New T().X`). A `With` block's `.Run()`
  call, which recorded nothing, is sent the same way. The grammar splits
  `New System.Drawing.Size(...)` into members read through `New System`;
  those, anonymous types' `New With {.X = 1}`, and paths that start with
  System, Microsoft or My are not sent.
- Resolution (vbnet-receivers.ts): each link of the path is typed by the
  rules calls use since #2351 -- declarations, casts, `For Each` over a
  typed collection, call results, type-parameter constraints, Module
  variables, namespaces and Imports -- and the read links the member the
  type declares or inherits. A method named without parentheses is called
  (unless AddressOf or NameOf only names it), and `x.Items(0)`, an index
  that VB.NET writes as a call, reads `Items`. An outside, `Object`-typed,
  ambiguous or untyped receiver links nothing; nothing is guessed by name.
- "Color Color": a value named like its type (`theme As Theme`) now reads
  an instance member through the value, so only the member is linked; a
  Shared member, a Const (read from its declaration) or an Enum case still
  links the type as well, as #2355 did for every such read.
- membersNamed looks a type's members up in its parts' files rather than
  with one query per member name. The graph is identical on SCrawler; on
  staxrip, whose AutoCrop tool redeclares `ServerInfo`, 35 reads that
  main typed through the other project's field, and so left unlinked,
  now link (70 edges).

Verification: vbnet-instance-member-refs.test.ts (11 tests) fails 9/11 on
main and passes; one vbnet-shared-member-refs assertion now expects
`OtherSession::SessionId` from `Shadowed(appSession As OtherSession)`;
the VB.NET suites, tsc and the full suite pass (462 files, 5,859 tests;
22 kernel/platform files skipped). The issue's repro on the built CLI
links every read and write, and `codegraph callers` lists `Run` for
`SharedCache`, `Normal` and `Title`.

Validation, main vs this branch: nodes unchanged, self-loops unchanged.
SCrawler 17,437 -> 31,344 edges, 1 removed (same edge, new metadata).
staxrip 32,569 -> 71,362 edges, 585 removed: 318 same edge with new
metadata (Color Color reads of instance members, qualified-name ->
instance-method) and 267 class links of those reads, every one beside an
instance member. About 42k added two-link reads were re-derived from the
source text by a separate check; its 837 disagreements were all its own
blind spots (constraints, Imports aliases, sibling-block locals, call
results, staxrip's global `p`). No added edge crosses between staxrip
and its AutoCrop tool. Cost (5-run medians): CPU 16.1 -> 19.3 s and
20.2 -> 25.4 s, DB 28.1 -> 43.2 MB and 34.9 -> 59.0 MB; the DB growth is
the new edges plus reads through outside types kept as failed refs for
re-resolution (#1240).

Not covered: unqualified reads, reads through an index or a call result
(`list(0).X`), Handles clauses, and calls through a chained receiver
written directly (`g.MainForm.Update()`), which still resolve by name.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:39:26 +00:00
Colby Mchenry b7e05f3ccd fix(dart): link a chained call only through its receiver's declared types (#750) (#2376)
The Dart extractor keeps one receiver level, so the later links of a call
chain reach the resolver by their bare name (`Provider.autoDispose.family(…)`
→ `family`, `events.map(mapper).transform(…)` → `transform`). They then fell
to name matching and bound to whichever project method shared the name: on
felangel/bloc a Stream's `transform` went to angular_bloc's
`BlocPipe::transform`, and on rrousselGit/riverpod every
`X.autoDispose.family(…)` went to one builder's `family` whatever X was.

A chain link now reaches only the member of what the chain before it
evaluates to, typed from evidence: the head (a type, a constructor call, a
string, `this` / `super`, an import prefix, a local, parameter, field or
getter with a declared type) and each link's declared type (a method's or
getter's return type with the method's own type parameters substituted, a
field's or abstract getter's type, a static constant's initializer). A lookup
nothing declared answers for hands back its one type argument
(`context.read<LoginCubit>()`). Without evidence it links nothing.

Along the way: an annotated extension (`@internal extension X on Y`) is
recognized as one, so its members reach member reads and chains; `super.m()`
reaches the superclass's method instead of the caller itself; an extension
on a type parameter of its own applies to the project's types; a spread's
`...foo()` is a receiver-less call.

Validated before/after with the native kernel: bloc 337 name-only chain
edges on main → 123 kept (now typed), 106 retargeted, 106 removed (68 on a
typed receiver lacking the member), +75 `super.` calls; riverpod 1,254 →
171 kept, 383 retargeted, 696 removed (137 provably wrong), +263. Kernel and
wasm graphs are identical with the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:36:56 +00:00
Colby Mchenry 6421644ef2 fix(dart): link calls in top-level and field initializers (#2367)
Both Dart extractors (the TS walker and the native kernel) skipped the
code in initializers. A `final`/`const` declaration minted its constant
and stopped, and a field's or `var`'s initializer only had its types
recorded (#2360). In Riverpod apps, where most wiring lives in provider
closures (`final repoProvider = Provider((ref) =>
Repository(ref.watch(dioProvider)));`), what a provider builds with had
no callers, and impact and explore flows stopped at the provider.

- A `final`/`const` initializer is walked like a body, for its constant:
  calls, instantiations, static and member reads, types, closures with
  block bodies and local functions, function values.
- A field's (instance, `static var`, typed, `late`) or a top-level
  `var`/`late final` initializer is walked for its class or the file,
  since neither mints a node. A local declaration's second variable
  (`for (var i = 0, j = n(); ...)`) stays its function's.
- Each initializer is walked once: a new `walkInitializer` context method
  marks it, and the dispatcher's function-as-value scan skips it. Other
  languages' hooks are unchanged.
- Resolver: a Dart call through a type's static constant
  (`FutureProvider.autoDispose(...)` on `static const autoDispose =
  ...Builder();`) links that constant, not another type's same-named
  method.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:55:25 +00:00
Colby Mchenry 5b73d0fd46 fix(js,ts): find a store exported on a later line when its name contains $ (#2366)
TreeSitterExtractor.isExportedLater put the binding name into its regex
unescaped. A `$` in the name (`items$`, `$store`, `a$b`) became an
end-of-line anchor, so `const items$ = create(...)` + `export default
items$;` never matched and the store's actions (e.g. `function inc`) were
not extracted, while the kernel mirror escapes the name with regex::escape
and extracts them. The same anchor also matched where it should not: a
store named `items$` read as exported when a multi-line `export { ... }`
ended a line with `items`.

The kernel routes TS/TSX/JS/JSX by default, so the wasm path is what files
with parse errors (deferred to wasm), Svelte/Vue/Astro script blocks and
runs without the kernel get. Escape the name the way regex::escape does;
the guard limits names to [A-Za-z0-9_$], so only `$` changes. `\b` stays
as is on both sides: next to a leading or trailing `$` it never matches
(`export { items$ }`), the same in both engines.

Adds a kernel/wasm parity case (ts/tsx/js/jsx) for `items$`, `$store` and
`a$b` plus the false-positive shape; all 16 fail with the name unescaped
and pass with the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:54:08 +00:00
Colby Mchenry fe7542b6f6 fix(vbnet): index names that begin with a keyword (SharedCache, Dimension, Sub NewItem) (#2365)
The vendored grammar lexed the member modifiers as one prec-10 token and
`Sub New` as one prec-100 token. Neither went through keyword extraction,
and both outranked the prec(-1) identifier, so the lexer stopped at the end
of a modifier spelled at the start of a name: `Public SharedCache` indexed a
field `Cache`, `Public Dimension` a field `ension`, `Public Shared Shared1`
was lost to a parse error, and `Sub NewItem()` became a constructor. `Dim`
and `Const` were part of that token, which also kept the local-declaration
`Dim`/`Const` out of keyword extraction, so `ConstVBV.Value = False` and
`ConstructPath()` inside a method parsed as `Const` declarations.

The modifiers and `Sub New` are now plain keywords. The old tokens also
folded a newline into the keyword after it, which two parses relied on by
accident: `Option` lines under a comment banner may now follow newlines, and
upstream's top-level `file_attribute_section` is gone, so attribute lines
above a top-level declaration belong to it. `Protected Friend` and
`Private Protected` are two modifiers now; the extractor reads them as
protected and private.

Rebuilt with tree-sitter-cli 0.25.10 and emscripten 4.0.4. On SCrawler and
staxrip, 3 fields get their whole names, staxrip recovers one method, and
every other graph difference is an `imports` edge or a node start now on its
own line instead of the comment line above.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:51:51 +00:00
Colby Mchenry f40db4b9cc fix(js,ts): index the functions in every named object literal, not only exported ones (#2300) (#2363)
Issue #2300: a function written inside an object literal became a symbol
only when the object was an `export const`. A plain `const api = {...}`,
an object declared inside an IIFE or a function, and a namespace hung on
the page (`window.WS = {...}`, `ns.mod = {...}`) produced no member nodes,
and no node for `WS` either. Calls made inside those members were credited
to the enclosing constant or lost, and calls into them resolved to
nothing. Script-tag JavaScript, written almost entirely this way, was
mostly missing from callers and impact.

Cause: the TS/JS extractor, and the kernel's tsjs mirror of it, minted
members only for exported object-of-functions; any other literal was
walked as one opaque initializer, and an assignment to a member path was
never a declaration. Resolution had no way to reach a member through
`App.init()`, `window.App.init()` or `App.utils.pad()`.

Fix (extraction, TS and kernel byte-identical): a named object literal
owns its function members (method shorthand, `key: function`, `key: () =>`,
generators; static keys only), whether it is declared at module scope,
in a function body or IIFE, assigned to a path (`window.App = {...}`,
`App.utils = {...}`) or assigned to a plain name at module level
(`dw_page = {...}`). Members are `function` nodes qualified under the owner
(`api::load`, `window.App::init`, `App.utils::pad`), the exported case
included; a path owner is named by its last link and qualified by the
path. Calls in a member are the member's; other values are walked where
they were before. CommonJS exports, prototypes, `this.x = {...}`,
call-argument literals, a name reassigned inside a function, and
generated or minified files keep the old shape. Minified files are now
also recognised by content, in both extractors, and the kernel's
`.min.js` pattern matches the TS one.

Fix (resolution): a member is reached through its object only:
`App.init()`, `window.App.init()`, `App.utils.pad()`, a sibling's
`this.render()`, a `const { init } = App` binding; never a bare `init()`.
A same-file holder is chosen by lexical block (an IIFE's own `App` first);
otherwise a global one (`window.App = {...}`, or a classic script's
top-level `App`) when the caller neither imports nor binds the name.
Arrow members keep the `this` of the method around the literal, in the
`this.x` resolvers too. A dotted-path holder is never reached by its last
name alone, and Svelte's `$store` rule now applies only in `.svelte`
components, so the new local holders are not taken for `$n` in plain
scripts. The lookups read each file once (destructuring patterns only
when the raw text has a `} =`) and keep only the last 32 files' scans.

Contributor PR #2310 (@danusha2345): adopted its model (owner-qualified
members exported or not, path owners, IIFE/local owners, static keys,
no bare-name reach, `<script setup>` contains edges, SFC languages, test
scenarios), re-implemented on current main. Not taken: the
EXTRACTION_VERSION bump (already 28), the extraction-time binding oracle
with its ref/edge metadata and kernel ref patching (replaced by
resolution-time lookups), naming the owner `window.WS`, rewriting
`window.X.m()` ref names, flipping exported members' isExported, and
unrelated bare-call changes.

Verification: new js-object-literal-members suite (native + wasm, 18
tests); the issue's shapes fail on main and the guard test fails without
the guards. kernel-tsjs-parity passes with CODEGRAPH_KERNEL_EXPECT=1
(object-literal owners LF/CRLF, minified bundles); kernel parity sweeps
show 0 diffs on DokuWiki, vue-realworld, TodoMVC, excalidraw and this repo.

Validation (kernel loaded, before = origin/main 023fc311):
- DokuWiki: nodes +85, calls +105; 17 removed edges = 13 moved to the
  member, 4 re-resolved from a wrong LinkWizard::init to the right init.
- TodoMVC: nodes +427, calls +148; 306 removed = 226 moved, 33
  re-resolved (14 fixed `this.render()`/`this.save()`/`this.track()`, 19
  wrong-to-wrong fallback guesses), 47 dropped wrong edges (43 to a
  helper local to jQuery Mobile's scrollstart setup).
- excalidraw: nodes +86, calls +75; 34 removed = 33 moved, 1 fuzzy 0.3
  guess dropped.
- vue-realworld: nodes +10, calls +11; 7 calls re-resolved from the
  ApiService constant to its members.
New self-loops are real recursion, bar one lazily redefined method that
calls itself as written. 81 sampled added edges, 80 correct
(the other is main's own window.open guess, re-attributed). Whole-run
index time, median of 3 interleaved: vue-realworld 1.2s -> 1.2s,
DokuWiki 8.6s -> 9.1s, excalidraw 10.8s -> 10.7s, TodoMVC 16.9s -> 16.9s
(within run-to-run noise).

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:45:43 +00:00
Colby Mchenry 023fc3111b fix(axum,actix): link a route to a handler written below its call (#2326) (#2359)
With rustfmt's wrapped form

    .route(
        "/multi",
        get(handlers::multi_line_handler),
    )

the route was never linked to `multi_line_handler`, even when it was the
only function of that name; the single-line form worked. Rustfmt wraps
every `.route(` past the line width, so most routes of a real router were
affected (31 of 364 handler references resolved in the reporter's router).

Cause: the Axum block of the Rust framework extractor recorded the handler
reference at the line of `.route(`, column 0. Every resolved reference then
passes the Rust scope gate (isRustNameInScope), which reads that one line
to see how the name is written (`handlers::f` names the `handlers` module).
On a line holding only `.route(` it found nothing and dropped the edge. The
same position also broke single-line routes whose handler name appears
elsewhere on the line: in `.route("/login", post(handlers::user::login))`
or `delete(handlers::user::delete)` the gate read the path string or the
method router as a bare use of the name. Actix's `web::resource(..)` chains
and App-level `.route("/p", web::get().to(h))` had the same weakness.

Fix (src/resolution/frameworks/rust.ts):
- The handler reference carries the line and column of the handler's own
  name (its last path segment), for Axum and the three Actix forms; the
  route node stays on the `.route(` / `web::resource(` line. The gate is
  unchanged and still reads one line, the one the handler is written on.
- Axum's method routers are the top-level chain of `.route(`'s second
  argument: a `get(` nested inside it is a call in a closure handler's body
  (`post(|h| async move { h.get(CONTENT_LENGTH) })`), not a route. With the
  reference moved, such a call would otherwise gain an edge.
- An Actix resource's method chain also ends at the `)` of the call it is
  an argument of, so in `.service(web::resource("/").to(index))
  .route("/count", web::get().to(get_count))` the resource keeps
  `.to(index)` instead of taking `/count`'s handler.

Adopted from #2329 by @danusha2345 (the fix and the tests are theirs),
without its EXTRACTION_VERSION bump (already 28 on main); added a case for
a single-line route whose handler name the line also writes elsewhere.

Verification: __tests__/rust-route-handler-line.test.ts: 10 of 13 fail on
main in both wasm and kernel modes; 13/13 pass with the fix in both modes.
The issue's repro now prints `GET /multi [references] src/main.rs:7`.

Validation (main -> branch, route nodes and route->handler edges keyed by
route name/file/line and target QN/file):
- rust-lang/crates.io: 1 -> 5 linked (+4 wrapped routes).
- atuinsh/atuin server: 7 -> 12; +5 newly linked and 2 wrong targets
  replaced (`GET /api/v0/me` user::get -> v0::me::get, `GET
  /api/v0/record/next` a TUI `Events::next` method -> v0::record::next).
- launchbadge/realworld-axum-sqlx: 18 -> 19 (+1 wrapped chain link); 11
  same-file edges now carry the handler's line.
- tokio-rs/axum (whole repo): 204 -> 204, 19 edges carry the handler's
  line; 2 bogus `GET /` route nodes from `headers.get(..)` in closure
  handlers in routing tests are gone.
- actix/examples: 116 -> 116; `ANY / -> index` x2 replaces two wrong `GET /`
  edges (websockets/chat took the next routes' handlers; basics/hello-world
  now records its real `.service(web::resource("/").to(index))`).
No other edge changed in any of the five repos.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:00:29 +00:00
Colby Mchenry ed1a89fb0f fix(rust): a variant path references the enum that declares it (#2328) (#2354)
Using an enum only through its variants (`mode::Mode::A` in an expression,
`mode::Mode::A => 1` / `Mode::B => ...` in a match, a `Mode::C(x)` or
`Mode::D { .. }` pattern) recorded no reference to the enum, so code that
builds or matches variants was missing from the enum's callers and impact.
Only a type annotation (`fn takes(_m: Mode)`) produced one.

Cause: Rust was not in the static-member pass, which turns `Enum.value` /
`Type::CONST` reads into references for Java, C#, Kotlin, Swift, Scala,
Dart, PHP and C++, and a path to a variant that is not a call or a struct
literal reaches no other extractor.

Fix:
- Extraction (wasm extractor and native kernel, mirrored): a path whose
  last segment is capitalized emits a `references` ref named by its
  receiver (the segment before it; `Self` read as the impl's type, nothing
  in a trait), positioned at the receiver, so a `mode::` prefix scopes it
  exactly as it scopes a type annotation. Covers value reads, match arms,
  `|` / `if let` patterns, tuple- and struct-variant patterns and a variant
  passed as a value. A call's callee (`Mode::C(1)`, `Foo::new()`), a struct
  literal, a longer path's prefix, a `use` tree and a lowercase receiver or
  member emit nothing.
- Resolution (isRustNameInScope): a reference written `Name::Variant` or
  `Self::Variant` lands only on an enum that declares that variant, after
  the usual Rust scoping (module path, `use`, glob, prelude, std and
  dependency imports). ripgrep's `Match::None` reaches `ignore`'s enum, not
  the matcher's `struct Match`; std variants (`Ordering::Less`,
  `Option::Some`) never reach a same-named project enum; associated consts
  and functions (`Limits::MAX`, `Mode::ALL`, `Foo::new()`) reference no
  type. `Self::X` is read as the impl's type, never through a path written
  elsewhere on its line.

Adapted from #2330 by @danusha2345: the extraction half (both walkers) and
most of the tests are theirs. Changed here:
- The resolver accepts a declared variant only. #2330 also accepted any type
  whose file holds a variable or const of the item's name; impl consts are
  file-level variables in the graph, so that cannot tell a type's associated
  const from another type's or a top-level one.
- The walkers check the node kind before looking at its parent, and the
  body walk hands the parent down: #2330 called `parent()`, which walks down
  from the tree's root, for every node of every body, which nearly doubled
  the kernel's Rust extraction time on tokio (2.1 s -> 3.9 s); now within
  noise of main.
- EXTRACTION_VERSION is not bumped again (already 28 on main).

Verification: __tests__/rust-enum-variant-reference.test.ts (10 tests):
6 fail on main in wasm mode, in kernel mode, and with this TypeScript over
main's kernel binary; 10/10 pass in both modes. The kernel parity case
covers every shape and asserts the refs exist; the kernel-parity sweep over
ripgrep, alacritty and tokio is 1004/1006 files byte-parity, 0 diffs (2
parse-error files defer to wasm by policy).

Validation (native kernel loaded, main -> branch, edges keyed by
QN/kind/file/line): ripgrep +822/-0, alacritty +917/-0, tokio +665/-0, all
`references -> enum`, node counts unchanged. Every added edge's source
reads `Name::Variant` / `Self::Variant` at its column and its target enum
declares that variant; no std enum targets. New symbol->enum pairs:
ripgrep +220, alacritty +253, tokio +254; enums with no referencer before
and one now: 31 / 13 / 31. One wrong target on tokio
(`oneshot::error::TryRecvError::Empty` -> mpsc's `TryRecvError`) comes
from the existing path rule that checks only the last module segment, which
type annotations share. Index time on tokio stays within noise of main
(interleaved runs; resolveOne time 27.7 s vs 31.2 s summed over workers).

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:59:11 +00:00
Colby Mchenry 7210d2fd4c perf(resolution): remove the JS indexing regressions since 1.6.2; the summary time covers the whole run (#2334) (#2362)
Indexing pretix took 2.2x as long since #2163 (issue #2334), and the
bundled minified d3 was what #2344 left of CPython's 1.6.2 slowdown. Two
JS/TS checks that first shipped in 1.6.2 redid whole-file work for every
reference:

- matchDestructuredCallResult (#2163) ran for every bare call in a file
  holding `const {` / `let {` / `var {`: per call it re-tested that
  pattern on the source, then stripped, blanked and brace-walked every
  line above the call and matched the binding regex over it.
- jsFunctionLocalScope (#2226) re-stripped the calling function's lines
  above each reference. In a minified script every function's text runs
  to the end of its one line, so each strip was the whole file.

CPU profiles, main -> this change: matchDestructuredCallResult 82.4 s
-> 1.7 s and jsFunctionLocalScope 12.3 s -> 5.2 s on pretix; 35.4 s ->
0.37 s and 23.3 s -> 1.0 s on CPython's Lib/profiling + Doc/_static.

Now:
- A file's `const { … } = f(…)` bindings are read once per resolution:
  the blanked code, each binding's end, callee and keys, the block it
  sits in and where that block closes, and the spans the blanker read as
  regex literals. A call looks up its own name; only a call through a
  destructured name scans the code between that binding and itself for
  shadowing, and only once the callee is found to return the key (a
  `require(…)` destructuring, for one, needs no scan).
- Blanking the text above a call on its own gives the file's blanked code
  up to the call: the stripper and blanker look past a character only for
  a comment opener's second character and for a regex literal's closing
  `/`. So the prefix's bindings are the file's that end at or before the
  call (a match never reads past its `(`, and one running past the call
  holds no `{`…`(` to start another), and its brace stack follows from
  where each block closes. For a call inside a span the file reads as a
  regex literal (a division, in minified code) the prefix is the code up
  to the `/` plus the short tail blanked on its own; where a binding
  statement could run across that `/` (inside its `{ … }` or `< … >`), and
  for out-of-range positions, the old per-call scan still runs.
- jsFunctionLocalScope strips a function's lines once and cuts at the
  reference's line end; functions that start on the same line share the
  regex answer by (file, first line, reference line, name).
- The new memos, and JS_FN_LOCAL_MEMO (never cleared before), drop with
  the other per-file memos in clearNameMatcherMemos.

The summary line printed orchestrator.indexAll's durationMs, which covers
extraction only, beside node and edge totals recomputed after resolution:
the issue's 19.9 s run printed "in 1.7s". CodeGraph.indexAll and sync now
report the whole run's wall time, which `codegraph init`, `index` and
`sync`, the MCP auto-sync log and the telemetry duration bucket read.

Verification: nodes, edges and unresolved_refs dumps keyed by
file|kind|qualified_name|start_line are byte-identical to origin/main
8998697f (with #2357's new destructuring and Vue template refs) on all
24 runs: pretix, mealie and CPython's Lib/profiling + Doc/_static, 8
each; and to deac7710 (#2358, #2360, #2361) on one more round of each. A differential harness ran old and new matchDestructuredCallResult
on every call position of mealie (10,330) and the CPython assets (9,170,
742 inside misread regex literals) and on 16,000 random token-soup
sources (1.33M positions), and old and new jsFunctionLocalScope on ~500K
(function, line, name) triples: no difference; it reports one for each
deliberate mutation of the new code that is not provably equivalent.

Medians of 4 interleaved runs on a loaded 16-thread Windows box, process
CPU / wall / peak RSS: pretix 160.2 s / 75.9 s / 2.47 GB -> 88.7 s /
43.5 s / 1.93 GB; CPython assets 75.1 s / 37.6 s / 1.23 GB -> 32.1 s /
20.4 s / 0.81 GB; mealie (no bundles, the control) 39.2 s -> 38.6 s CPU.

__tests__/js-resolution-work.test.ts counts strips: a file with a
destructured binding and 40 bare calls is stripped for the check once
(41 on main), a 40-line function once (40 on main); each count fails when
only its own half of the fix is reverted. full-pipeline.test.ts requires
indexAll's and sync's durationMs to cover a linking pass delayed 250 ms
past the orchestrator's time (on main: 218 < 468).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:52:39 +00:00
Colby Mchenry deac7710f2 fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) (#2361)
Calls through a method receiver or parameter of an unexported type
(`func (s *server) Create() { s.service.AddItem() }`) had no edge: the
Go receiver inference only accepted PascalCase types (#2323). And only
the project-root go.mod was read, so a module in a subdirectory, or one
beside other modules as in etcd, treated imports of itself as
third-party (#2322).

Fixing either alone sends calls to the wrong package, because Go type
names are unique only per directory while types and methods were looked
up by name across the project. The receiver pattern alone sent 1,042 of
harbor's 1,998 new calls to another package's same-named type; reading
nested modules alone made harbor's caching wrappers call themselves and
put about 180 of etcd's type references on same-named methods. Both
share one fix:

- A lowercase receiver or parameter type is read only where a parameter
  list puts it (receivers, parameters, multi-line lists, var blocks).
- Receiver inference also reports the type as written. For Go,
  resolveMethodOnType takes the declaring package's directory (the
  caller's for a bare name, the import's for a qualified one) and counts
  only that package's methods, then the types its declaration embeds
  (struct and interface embedding, read from the source) instead of the
  name-based supertype union. The field-chain matcher scopes its struct
  and field types the same way.
- The resolver reads the nearest go.mod of every directory holding
  indexed Go files (no disk walk; a module under testdata/ or a _/.
  directory serves only its own files) and maps an import path to the
  module with the longest module path, the importing file's own module
  breaking a tie. getGoPackageDir replaces getGoModule.
- A bare name written through a package (`job.OPCommand`) resolves
  through the import into that package's directory, and a name-matched
  candidate outside the qualifier's package is rejected; a method reached
  through a variable that shadows an import is exempt.

Verification: new go-unexported-receiver (3) and go-nested-module (11)
tests: 12 fail on main (the 2 controls pass), all 14 pass. The issues'
repros go from 0 to 1 caller; the root-go.mod and `Server` variants are
unchanged.

Validation, main 26e8488a -> this branch, whole repos, Go->Go edges keyed
by source/target qualified name, file, line and column:
- etcd 6bb7e5e: 52,530 -> 66,284 (+13,827 / -73), calls 18,074 -> 25,265
- harbor 721c6d4: 62,423 -> 77,615 (+15,819 / -627), calls 16,308 -> 25,236
- prometheus 770ca8f: 90,970 -> 92,399 (+2,249 / -820), calls 40,050 -> 41,493
Of the 1,520 removed edges, 1,458 are the same call site or reference
now resolved to the right package's symbol; the other 62 were field
reads taken for method values, names whose real target is not indexed
(a `type T = string` alias), and a call to a method the receiver's type
does not have. Indexing time and CPU are unchanged.

Co-authored-by: Diao Shengjia <104132148+GoDiao@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:16:14 +00:00
Colby Mchenry 9f69e47836 fix(dart): link getter reads, enum-extension members and more type positions (#2338, #2327) (#2360)
Issue #2338: a Dart getter read (`x.area`, `s.label`) linked nothing, so
`codegraph callers area` was empty while `x.grow()` worked, and a member
an extension adds to an ENUM (`s.shout()` through `extension ShapeInfo on
Shape`) never resolved. Issue #2327: `callers Report` saw only parameter
and return types; the type an extension is `on`, field types, and types
named in bodies and initializers (`Future<Report?>.value(null)`, riverpod's
`final reportProvider = Family<Report?, String>()`) recorded nothing.

Causes:
- The Dart body walker only emitted calls (a selector with an argument
  list); a member read is a selector without one, so it produced no ref.
- For a typed receiver, resolveMethodOnType looks for `Type::member` and
  the type's supertype edges; an extension's member is `ShapeInfo::shout`.
  On a class receiver the call fell through to name matching, which found
  a unique `twice` by luck; on an enum the "type the project doesn't
  declare" guard (it only counts classes) returned null.
- Type references were taken from function and method signatures only.

Fix (TS extractor and the Rust kernel, mirrored):
- A member read `x.area` / `x?.area` / `Type.getter` whose receiver is a
  plain name, and that is not a call, is sent as a `references` ref named
  `x.area` on the member. The resolver (matchDartMemberRead, ahead of
  every other strategy) types the receiver — a static read through a
  project type, a parameter or local in scope, else a field of the
  enclosing class — and links the getter that type reaches: declared on
  it or a type it extends, mixes in or implements, else added by an
  extension on one of those (an unnamed extension only in its own
  library). The edge is `calls`: a Dart getter is a method node, reading
  it runs it, and `calls` is what callers, impact and explore's flow
  follow (as Ruby's paren-less `x.area` and VB.NET's paren-less calls
  through #2355 already are). A field read, a getter of a type outside the
  project and an untyped receiver link nothing; there is no name-only
  fallback. Kotlin/Swift/C#/TS link no instance property reads today.
- resolveMethodOnType, for Dart, falls back to a member an extension on
  the type (or a supertype) adds when no declaration of the type has it.
- `references` refs for the type an extension is `on`, a field's declared
  type and initializer (from its class — Dart fields mint no nodes), an
  enum field, a top-level variable's type (from the file), a constant's
  initializer (from the constant), and every type a function body names:
  locals, generic arguments, casts, type tests, catch clauses. Skipped: a
  `new`/`const` constructor's class (already instantiates/calls), an
  import prefix (`p` in `p.Foo`), built-ins, and lowercase names, which in
  Dart are built-ins (`num`, `dynamic`) or error-recovery artifacts around
  syntax the grammar predates (dot shorthands). A class whose field holds
  its own type does not get a self-edge.

Verification: __tests__/dart-getter-reads.test.ts (7 tests) and
__tests__/dart-type-positions.test.ts (3 tests) fail 8/10 on main (both
the TS path and main's kernel) and pass on both arms here; the 2 that pass
on main are the controls. New kernel parity fixture TortureReadsTypes.dart
(+CRLF); kernel-dart-parity passes with CODEGRAPH_KERNEL_EXPECT=1, and
scripts/kernel-parity.mjs over bloc/riverpod/flutter-samples is 0 diffs
(2,103 files byte-parity, 300 parse-error files defer as before). tsc and
build clean; the issues' repros now list c/a/b for area/label/shout and
Holder, ReportX, reportProvider and run for Report.

Validation (kernel loaded, main vs this branch): nodes unchanged, 0 edges
removed, no new self-loops.
- felangel/bloc: 16,372 -> 18,276 edges (+550 getter-read calls, +1,354
  type references)
- rrousselGit/riverpod: 76,432 -> 81,909 (+598 getter reads, +13
  extension-method calls, +4,866 type references)
- flutter/samples: 26,133 -> 26,943 (+142 getter reads, +668 type refs)
Spot-checked 16 getter reads and 16 type references against source.
Index time within noise on riverpod (median of 5 interleaved runs); reads
that link nothing are kept for re-resolution like failed calls (riverpod
db 92.9 -> 103.2 MB).

No EXTRACTION_VERSION bump: it already moved to 28 this release.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:12:24 +00:00
Colby Mchenry 1b56cb1992 fix(explore): find COBOL copybooks by name, with every include site (#2342) (#2358)
`codegraph explore MYCOPYBOOK`, the `codegraph_explore` MCP tool and
`codegraph context` answered "No relevant code found" for a COBOL copybook
named in the query, while `query`/`node` found its include at once.

Cause: the COBOL extractor records `COPY X` and `EXEC SQL INCLUDE X` as
`import` nodes, and findRelevantContext filters every channel by
HIGH_VALUE_NODE_KINDS, which leaves imports out on purpose (a JS/TS/Python
import statement is noise in explore). With the include out of reach the
text search fell through to its fuzzy fallback, so a member with a digit in
it (never extracted as a symbol) came back as an unrelated program one edit
away: CVACT01Y -> CBACT01C, CVCUS01Y -> CBCUS01C on CardDemo. Making COBOL
imports "high value" alone would not have helped either: explore's renderer
skips import nodes, and the import-to-definition step drops a COBOL include
because its `imports` edge leaves the enclosing scope, not the import node.

Change:
- src/graph/cobol-copybooks.ts (new, shared derivation): resolves the COBOL
  words of a query to copybooks - every COPY / EXEC SQL INCLUDE of the member
  (case-insensitive) and the indexed copybook file (.cpy, or a file an
  include resolved to; a same-named program is not one). Gated on the
  project having COBOL files at all (one seek on idx_files_language), so no
  other language's query does extra work or changes. Plain lowercase words
  in a multi-word query are English, not member names.
- findRelevantContext: a named copybook's top-level declarations, then its
  include statements, become entry points ahead of the kind-filtered
  channels, like an exact filename (at most half the slots when the query
  matched other things, include sites in those files first). They are not
  walked (BFS from a record layout spent the answer on whichever program
  MOVEs into it), the member is not re-searched as text (the fuzzy decoy),
  and an explicit nodeKinds filter without `import` is honored.
- codegraph_explore: the copybook file is pinned like a path in the query;
  an include statement that is an entry point is grouped and rendered (a
  window around the COPY line) instead of skipped; a new "COBOL copybook"
  section lists every include site (path:line, COPY vs EXEC SQL INCLUDE,
  25 lines then a count) or says the member has no indexed source (DB2
  DCLGEN, compiler-supplied such as SQLCA/DFHAID).
- QueryBuilder: three indexed lookups; CodeGraph.findNamedCopybooks exposes
  the derivation so explore and the context builder share one answer.
No extraction change; no re-index needed.

Verification: __tests__/explore-cobol-copybook.test.ts (8 tests, through
ToolHandler codegraph_explore and buildContext): with src/ reverted to
origin/main 6 fail (the 2 guard cases pass on both), 8/8 pass with the fix.
tsc clean, npm run build OK. Issue repro fixed on the built CLI (explore +
context) and through a real `serve --mcp` tools/call. Full suite on a heavily
loaded box: 5716 passed, 25 failed in 15 files; 12 of those files pass alone,
and the other 3 (function-ref, git-index-currency, worktree-detection) are
5 s / 60 s timeouts that fail on clean main as well in interleaved runs.

Validation (both builds on one index per repo; query-side change only):
- expressjs/express: 14 explore + 4 context queries byte-identical.
- aws-mainframe-modernization-carddemo: 10 non-copybook queries
  byte-identical; 15 copybook queries all now return the copybook (when
  indexed) and every include site - before: 9 "No relevant code found",
  4 unrelated programs, 2 without the copybook.
- cicsdev/cics-genapp: 6 non-copybook queries byte-identical; 6 copybook
  queries fixed (LGCMAREA's 25 sites incl. multi-line EXEC SQL INCLUDE,
  lowercase lgpolicy, SSMAP with no indexed source).
- In-process explore latency on express unchanged within noise.

Thanks @popolusiak for the report.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:39:03 +00:00
Colby Mchenry 8998697f1d fix(vue,ts): link calls in Vue templates and in top-level destructuring (#2340) (#2357)
`codegraph callers <composable>` missed most of the components using it in
a Nuxt app (Issue #2340), through two extraction gaps:

- A destructuring declaration at module or `<script setup>` scope
  (`const { a } = useFoo(1)`, `const [b] = useFoo(1)`) mints no symbol,
  and extractVariable skipped its declarator with `continue` before
  walking the initializer, so the call was never recorded (inside a
  function the body walker reached it). The declarator is now walked the
  way a function body walks it, with the enclosing scope on the stack:
  the file for module code, which a `<script setup>` block folds into its
  component. Defaults in the pattern and calls inside callbacks in the
  initializer count too. Mirrored in the native kernel
  (tsjs/extractors.rs), which extracts TS/JS in released builds.
- The Vue extractor parsed only `<script>`, so calls in the template
  (`{{ useBar(link) }}`, `:to="useBar(link.location)"`, `v-if`, `v-for`
  sources, `@click="save(item)"`) made no reference. The new
  vue-template-calls scanner reads the root `<template>` quote-aware,
  collects interpolations and directive values, and names each callee
  the way a script call is named (`useBar`, `store.fetchUsers`). It skips
  `v-for` aliases and slot props in their element's subtree, arrow and
  function parameters, `$`-helpers, Vue's template globals, strings,
  regex literals, `new X()`, object-method shorthand, `v-pre` subtrees,
  custom blocks and non-HTML templates. Each call is a `calls` reference
  from the component at its own line and column; a bare call to the
  component's own Options API method is linked at extraction, since
  resolution only binds those through `this.`.

Two resolution follow-ons, found validating on elk:

- The vue-handler synthesizer resolved the leading identifier of every
  `@event` value, so `@click="save(item)"` got a second, line-1 edge, and
  `@click="query = ''"`, a slot prop's `hide()` or a `refresh()`
  destructured from useAsyncData were bound to any same-named function in
  another file. It now resolves only a bare handler name (`@click="save"`)
  and keeps its composable-destructure mapping.
- The Nuxt auto-import rule answered a call to a file's own `clearError`
  or `navigateTo` with an edge from the caller to itself. A same-file
  declaration of that name now shadows the auto-import.

Verification: new tests in vue-template-calls.test.ts (scanner shapes and
four indexed projects, incl. the issue's repro), extraction.test.ts
(module-scope destructuring in ts/tsx/js/jsx; script-setup + template
refs, LF and CRLF) and kernel-tsjs-parity.test.ts (destructuring, kernel
vs wasm). On main (src + kernel reverted) the 10 indexed/extraction tests
fail and the 4 parity cases fail with the main kernel; all pass with the
fix (CODEGRAPH_KERNEL_EXPECT=1). Issue repro on the built CLI with the kernel:
callers useFoo 2 -> 5 (A.vue, B.vue, F.vue, G.ts, D.ts), callers useBar
0 -> 2 (H.vue, I.vue). kernel-parity.mjs over elk, nuxt/movies and
fastify: 467/471 files byte-identical, 0 diffs, 4 deferred.

Validation (kernel loaded, main vs branch, edges by natural key):
elk 6282 -> 6633 (+443 / -92), vue-element-admin 2337 -> 2372 (+60 /
-25), nuxt/movies 939 -> 959 (+26 / -6), fastify (plain JS control) no
edge change. Every removed edge is a line-1 vue-handler edge whose pair
is now linked at the handler's line (101), a name-collision guess (8),
or an auto-import self-loop now resolved to the local function or route
(14). Self-loops: elk 44 -> 30, others unchanged.

Builds on #2341 by @drakeo338 (walk the destructuring initializer) and
the template scan in #2073 by @L0garithmic.

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: L0garithmic <L0garithmic@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:17:51 +00:00
Colby Mchenry 6c61dd74ca fix(vbnet): link Shared fields and properties read through their class (#2305) (#2355)
VB.NET emitted a reference for calls only, so a value read or write
through a type name -- `AppSession.SessionId`,
`AppSession.CurrentUser = "demo"`, `Logger.Level` -- linked nothing, and
`codegraph callers` on the field, the property and the class came back
empty. The reporter's central session class showed 6 callers against
~460 referencing files.

- Extraction (TS only; VB.NET is not kernel-routed): the static-member
  pass now covers VB.NET. A value read through a Capitalized name is sent
  as one `references` ref, `Name.Member`, the receiver kept as a call's
  is. Namespace roots (`System`, `Microsoft`, `My`, `Global`) and the
  built-in type keywords (`String.Empty`) are not sent.
- Resolution (vbnet-receivers.ts matchVbMemberRead, ahead of the
  framework, import and name strategies): the receiver is looked up with
  #2351's VB.NET scoping, case-insensitively. A local, parameter, field,
  property or Module member of that name holds a value and links nothing;
  one typed as the type of its own name ("Color Color") reads that type.
  Otherwise the class, Module, Structure, Interface or Enum the
  namespaces, Imports and aliases around the read see is the type: the
  read links the member it declares or inherits (field, property,
  constant, event, Enum value, nested type; Shared before a same-named
  instance member, never the member the read is in) and, from outside
  the type, the type itself. A method named this way is a call made
  without parentheses (`calls`) unless AddressOf / NameOf only names it.
- `AppSession.Items(0)`, an index into a Shared field that VB.NET writes
  as a call, now reads the field and the type instead of linking nothing.

Contributor PR #2321 (@ChrisPrapas) had the right idea -- enable the
static-member pass for VB.NET with a member-level ref -- and its test
scenarios are adopted. Its resolution is replaced: it name-matched the
receiver's bare name and checked a member only against the receiver's
spelling. Ported onto main it passes its own tests, but binds a
parameter `appSession As OtherSession` to AppSession's members, misses
import aliases, and adds 8,652 / 13,554 edges on SCrawler / staxrip,
about 2,000 per repo binding a receiver to another class's same-named
member (a plugin form's local `Dim CONTAINER_MAIN` went to another
form's control field).

Verification: new vbnet-shared-member-refs.test.ts (7 tests) fails 6/7
on main and passes; the other VB.NET suites pass; tsc and build clean;
the issue's repro lists Consumer.Run and Consumer2.Describe for
SessionId, CurrentUser and AppSession.

Validation, main vs this branch: nodes unchanged, 0 edges removed, no
self-loops. SCrawler 14,893 -> 17,437 edges, staxrip 26,647 -> 32,568:
Enum values 803 / 1,448, Shared fields and properties 363 / 1,459, the
type read through 1,374 / 2,879, Shared functions called without
parentheses 3 / 126, AddressOf / NameOf 1 / 9. Spot-checked against
source: per-site SiteSettings, import aliases, escaped `[Date]` /
`[New]`, nested enums, "Color Color" fields; designer `Point.Empty` and
staxrip's AutoCrop duplicates stay unlinked. Index time within noise;
value reads that link nothing are kept for re-resolution like failed
calls (db 22.6 -> 28.9 MB, 28.3 -> 35.8 MB).

Not covered: reads through Me / MyBase, unqualified reads, instance
reads through a typed variable, Module variables as receivers,
namespace-qualified receivers and With blocks.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: ChrisPrapas <129202926+ChrisPrapas@users.noreply.github.com>
2026-10-05 20:05:18 +00:00
Colby Mchenry 69ee3b9a85 fix(ui): fall back past a port Windows refuses with EACCES, and say why (#2299) (#2353)
On Windows, a port another program holds exclusively (SO_EXCLUSIVEADDRUSE)
or one inside an excluded port range fails listen() with EACCES rather than
EADDRINUSE. The `codegraph ui` port fallback only moved on after EADDRINUSE,
so it stopped at the first such port. The error then blamed the POSIX
privileged-port rule ("Ports below 1024 usually need elevated privileges")
for ports like 4747 or 49912. Confirmed on Windows 11: a .NET listener on
0.0.0.0:P with ExclusiveAddressUse makes Node's listen(P, '127.0.0.1') fail
with EACCES.

Fix:
- shouldTryNextPort moves past EADDRINUSE on every platform. It moves past
  EACCES at any port on Windows, which has no privileged ports, and from
  1024 up elsewhere.
- Below 1024 off Windows, EACCES is the privileged-port rule, and the next
  port falls under it too. The walk stops there with the message it always
  gave, rather than trying 20 privileged ports.
- describeBindFailure takes the platform:
  - Windows names the exclusive hold or reserved range and how to list the
    ranges (netsh int ipv4 show excludedportrange protocol=tcp).
  - POSIX keeps the privileged-port text below 1024. Above it, it says the
    system refused permission.
  - A walk that ran out says "in use or reserved" (Windows) or "in use or
    not allowed" when any port was refused rather than in use.
- Both helpers take the platform as a parameter, like browserOpenCommand,
  so the tests check every platform's answer on any host.

This builds on PR #2319 by @sx4im, which advanced on EACCES, used the
Windows wording and added the issue's listen-spy test. That PR advanced on
EACCES on every platform and printed "Windows refused port N ... netsh" on
Linux and macOS too. On POSIX it would also walk 20 privileged ports and
end with "Ports 80-99 are all in use or reserved". This change restricts
both to where they are true. The changelog entry goes in the viewer-launch
file, because the viewer is not released yet.

Verification:
- __tests__/ui-server.test.ts gains three kinds of test:
  - The issue's end-to-end test: port N is taken and N+1 is refused through
    a listen spy, so the server must land past N+1 and serve 200.
  - A pinned-port message test.
  - Unit tests of the decision and the messages for win32, linux and darwin.
- Results on __tests__/ui-server.test.ts:
  - Main source: 9 failed / 40 passed. The walk fails with "Not allowed to
    listen on port 57423. Ports below 1024 usually need elevated privileges".
  - With the fix: 49 passed / 2 skipped (existing POSIX-gated tests).
- Built CLI on Windows, with 0.0.0.0:<port> held exclusively:
  - `codegraph ui` while 4747 is held: main exits 1 with the privileged-port
    message. With the fix it serves on 4748 (GET / 200).
  - `codegraph ui --port <held>`: prints the Windows message.
  - A 3-port walk over held ports reports "in use or reserved".
  - Re-listening on the same server after a real EACCES works on Node 22.22
    and 24.16.
- tsc --noEmit is clean and npm run build succeeds. The 20 viewer test
  files pass (439 passed, 5 skipped).
- Full suite: 5732 passed, 10 failed, 319 skipped. The 10 failures are
  timeouts and EBUSY teardown under load in function-ref,
  git-index-currency, index-daemon-rebuild and rust-self-owner. All four
  files pass when run alone.

Issue #2299 reported by @ijbranch.

Co-authored-by: Saim Shafique <msaimshafique4664@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:03:03 +00:00
Colby Mchenry c9944147bb fix(vbnet): resolve receiver calls by declared type; guard .NET names, bare calls and ties (#2351)
VB.NET's resolver guessed a receiver call's target from the receiver's
name whenever no class matched it, without any of the guards C# has.
Indexing Structure members (#2345) gave those guesses new targets: 23
SCrawler `xxxFile.Delete()` calls on PersonalUtilities' SFile went to a
nested struct's `TempFileConversion::Delete`, and staxrip's main app
sent 90 calls to its AutoCrop tool's duplicate `ColorHSL`.

- Declared types (new src/resolution/vbnet-receivers.ts): a receiver's
  `As` type decides the call (local, parameter, field, property,
  `For Each`, `As New`, ...). The candidates are that type's member, one
  it inherits, or an extension method written for it. A type from
  outside the project gets no link.
- .NET standard method names (`Add`, `Contains`, `Clear`, `Dispose`,
  `ToString`, ...) on an untyped receiver need a receiver named after
  the owner, as in C#. VB.NET matches them case-insensitively.
- A bare call, or one on `Me`, reaches the enclosing class, what it
  inherits, or a Module. A nested type is matched by its bare name only
  from inside its owner. Types resolve through enclosing namespaces,
  `Imports` (aliases included) and the caller's project. A tie between
  equally good guesses goes to the caller's file, then its project, then
  the nearer directory, or gets no link.
- vbReceiverOf reads the name at or after the reference's column, so
  `Me.Size = New System.Drawing.Size(...)` is read through
  `System.Drawing`, not `Me`.

Before/after on main (nodes unchanged): SCrawler 15,092 -> 14,893
edges, staxrip 28,002 -> 26,647. Of the non-containment edges removed,
855 / 2,802 are the same link with new resolver metadata. The rest:

- .NET names on untyped receivers no longer guessed: 196 / 1,378.
  731 of staxrip's are StringBuilder `sb.Append` calls that had gone to
  `LogBuilder::Append`.
- Other receiver guesses dropped: 86 / 103, including the SFile.Delete
  calls.
- Bare calls out of scope: 38 / 834. 590 of staxrip's are
  `New Point(...)` that had gone to a class nested in `ButtonEx`; others
  are WinForms `Refresh()`, `Focus()` and `Activate()`.
- Retargeted: 227 / 532, e.g. 208 bare `Add(...)` calls in encoder
  classes now reach the inherited `CommandLineParams::Add`.

121 / 958 call sites resolve that did not before, e.g. `cms.Add` ->
ContextMenuStripEx and `td.AddButton` -> TaskDialog. Designer -> Size
links fall from 5 / 1 to 0; designer -> Add stays 0.

Known trade-off, shared with C#'s guard: about 31 staxrip calls like
`switches.Join(BR)` that may be the project's own `MiscExtensions`
extension methods are no longer linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:32:53 +00:00
Colby Mchenry 355afc70b1 fix(csharp): walk field and property initializers; a constant never names a type (#2337) (#2348)
* fix(csharp): walk field and property initializers as the member

Field declarators and property `= initializer` values were skipped by
both extractors, so `private readonly ILogger _log =
LogManager.GetLogger(typeof(X));` linked nothing, and a method or class
used only from an initializer looked unused.

- tree-sitter.ts / csharp.rs: walk each C# field declarator whole with
  its field on the stack, and every property `value:` (not only
  `=> expr`) with the property on the stack. Calls, instantiations,
  static reads, lambda bodies and fn-ref candidates (varinit included)
  belong to the member; the fn-ref scan skips the walked subtrees, so
  each candidate is captured once. Attributes stay unwalked.
- A target-typed `new()` that is the initializer instantiates the
  declared type (`List<Foo> _items = new();` -> List). In bodies it
  stays invisible, as before.
- name-matcher.ts: a .NET type position never names a `constant` (the
  kind `const` / `static readonly` fields get). Walking initializers
  exposed `new Version(5, 18)` binding to a `const string Version` and
  `static readonly Meter Meter = new(...)` instantiating itself.

Kernel/wasm parity: 0 diffs on serilog, Newtonsoft.Json and jellyfin,
and full-index dumps byte-identical between the two arms. No
EXTRACTION_VERSION bump: #2345 already moved it to 28 this release.

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

* docs(changelog): credit the report and contributor behind the constant change

The type-position change here is what issue #2337 reports (`new Station
{ … }` unresolved beside a `private const string Station`) and the same
one-line change as @drakeo338's #2339. Verified on the issue's own
three-file repro: the `instantiates` edge to `Demo.Core::Station`, missing
on main, is present with this branch.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:20:15 +00:00
Colby Mchenry 0c7d0f7d42 perf(resolution): remove the Python indexing regressions since 1.6.2 — graph byte-identical (#2332) (#2344)
Indexing CPython took about three times as long on 1.6.2 as at 290e03f
(193 s vs 59 s wall, 8.3 GB vs 3.5 GB peak on a 16-thread Windows box).
A CPU profile put ~290 s of the resolver workers' 412 s on two Python
checks that first shipped in 1.6.2:

- pythonExternalWrites (#2291) read every .py file once per module
  global its own module types. CPython has 2,375 Python files against
  the resolver's 1,000-entry content cache, so every read went to disk
  (184 s).
- isPythonLocallyBound (#2198, #2219) re-stripped the calling file for
  every (function, name), and, asked once per same-named candidate by
  fitsPythonCallShape, re-derived the enclosing function each time:
  3.55M calls for 53K distinct answers (104 s).

Now:
- A global's own module is read first; when its bindings already leave
  the type unknown (319 of 379 globals on CPython), no other module is
  read.
- One pass per resolution indexes which Python files spell a name after
  a dot or a quote, and which write through .__dict__; the write scan
  visits only those files, in the same path order. Comment stripping
  only blanks text, so any write the scan accepts is spelled that way in
  the raw text; a non-ASCII name still reads every file. Index keys are
  flat copies (a regex capture would pin its whole file).
- The repo files an import names are resolved once per mapping, in a
  WeakMap that lives as long as the resolver's import cache keeps it.
- isPythonLocallyBound keeps the current file's stripped lines and
  per-name module-level answer, and answers a ref once.

Verification: nodes, edges and unresolved_refs dumps of CPython and
pretix are byte-identical to 6560052a. Medians of 3 interleaved CPython
runs on a loaded machine: 290e03f 100 s / 271 s CPU / 3.3 GB, 6560052a
322 s / 801 s / 7.9 GB, this change 123 s / 361 s / 5.5 GB. In the CPU
profile pythonExternalWrites drops 184 s -> 16 s and
isPythonLocallyBound 104 s -> 13 s. Most of the remaining gap to
290e03f on CPython is JS checks running on its bundled minified d3
(matchDestructuredCallResult, #2334; jsFunctionLocalScope from #2226).

__tests__/python-resolution-work.test.ts counts the work instead of
timing it: reads of untouched files, strips of the calling file, and
node lookups with 16 same-named candidates (4 / 2 / 8 with the fix,
19 / 18 / 128 on main). Each count fails when only its own part of the
fix is reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:09:30 +00:00
Colby Mchenry dea076fd1e fix(mcp,extraction): exit a daemon whose install changed; never store a grammar-load failure (#2335, #2336) (#2346)
* fix(mcp,extraction): a daemon whose install changed exits; grammar-load failures are never stored (#2335, #2336)

A daemon that outlived an upgrade loaded grammars lazily from an install
that no longer existed, and stored every file its watcher re-indexed as a
zero-node row under the file's new content hash: no hash-based sync ever
revisited those rows, and `codegraph status` called the index up to date.

- The daemon checks its own package.json every 30 s
  (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, 0 turns it off) and exits once the
  file is gone or carries another version, so the next session starts a
  daemon from the current install.
- A result whose grammar failed to load (`parser_error`) is never stored:
  the file keeps its previous data and the next sync or index retries it,
  and a row an older engine stored that way is re-indexed even though its
  hash matches. The tag-based CFML path reports a missing grammar with the
  same code.
- A launcher whose own install was deleted no longer crashes on the spawn
  error of the daemon it tried to start.
- `codegraph status` and `status --json` (filesNeedingReindex,
  filesWithParseErrors) report indexed files whose symbols are missing
  although their content is current (#2336), and `files --json` carries
  each file's recorded errors.

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

* fix(mcp): a session whose own install changed never claims the writer lock (#2335)

A daemon now exits once its install is upgraded or removed, and nobody
holds the writer slot for a successor at that point. Its sessions run the
same replaced code and serve themselves in-process the moment it goes; one
that found the slot free claimed it as its writer even though its engine
could not load (a real npm upgrade: "Cannot find module
'./sqlite-adapter'"), and so kept a daemon from the current install from
starting. Such a session now falls back read-only and says to restart it.

Also adds the CHANGELOG entries for the install check and grammar-load
failures (#2335) and for the status report (#2336).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:08:14 +00:00
Colby Mchenry 511d86e94d fix(mcp): replace a daemon left running by an older install (#2335) (#2343)
A daemon keeps running the code it started with. The 1.6.1 daemon in
#2335 outlived an upgrade, kept loading grammars from the install the
upgrade removed and wrote every file it re-indexed as empty, while every
session from the new install served itself in-process, read-only, because
the old daemon held the writer lock. Daemons from before this change
cannot notice an upgrade, so the launcher acts:

- A launcher that finds a daemon of an older plain X.Y.Z release stops it
  the way `codegraph daemon` does (daemon.pid and the socket hello must
  agree; SIGTERM, SIGKILL only if it still answers) and starts one from
  its own install. It finds the daemon by its hello on the launcher's own
  socket, or by the project lock when it listens elsewhere: on Windows,
  daemons before #2278 named their pipe after the root as typed. A daemon
  of the same, a newer, a prerelease or an unknown version is never
  touched, so two installs cannot take turns stopping each other's.
- The writer slot never falls free on the way. The launcher swaps
  writer.pid to itself (mode `handover`) before the signal, clears the old
  daemon's leftovers while it holds the slot, and the daemon it spawns
  takes the slot over (CODEGRAPH_DAEMON_HANDOVER). Sessions of the old
  daemon that fall back in-process find the slot held and serve reads
  only, instead of claiming it with the removed install's code. A daemon
  that does not exit gets the slot back, and a successor outwaits a racing
  candidate that took daemon.pid first.

Validated on Windows with real npm upgrades from the published 1.6.1 and
1.6.2 to this build packed as 1.6.3, with the old session calling
throughout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:45:10 +00:00
Colby Mchenry 489b4749c7 fix(vbnet,csharp): index every Structure member; walk property bodies and VB initializers (#2345)
VB.NET's grammar tags each member of a `Structure` as its own `body`
field, so extractAggregate's field lookup saw only the first member.
SCrawler's UserMedia indexed none of its members, and a structure that
opened with a nested enum indexed only the enum. Aggregates now resolve
their body the way classes and enums do (resolveBody, which VB maps to
the declaration itself). Objective-C, the only other struct language
with that hook, is unchanged.

Property bodies were never walked: the propertyTypes branch set
skipChildren after extractProperty, so calls, instantiations and reads
in a VB `Get`/`Set` block, `= initializer` or `As New`, and in a C#
accessor body or `=> expr`, were lost.

- propertyBodies() names the parts of a property that run code (VB: the
  whole declaration, as its methods are walked; C#: each accessor's body
  and an arrow value), walked with the property on the stack. The
  candidates-only fn-ref scan skips the walked subtrees by node id, so a
  function value is captured once, from the property. C# `= initializer`s
  stay unwalked, as C# field initializers are.
- The C# kernel mirrors it (property_bodies, scan skip list). The parity
  suite and a new micro pass; a serilog sweep is 209/214 byte-identical
  with 0 diffs (5 deferred for parse errors).
- VB field declarators (`= expr`, `As New T`) and Custom Event
  AddHandler/RemoveHandler/RaiseEvent blocks are walked as their field or
  event.

Before/after (nodes / edges): SCrawler 9,950 -> 10,427 / 14,336 -> 15,092;
staxrip 13,440 -> 13,644 / 24,120 -> 28,002, most of it its
`Property X As New NumParam With {...}` settings; serilog edges
6,642 -> 6,654. All 23 removed edges are the same call sites re-resolved
by VB's name-guessing resolver now that struct members are candidates:
10 wrong edges dropped, 9 correct ones now guessed wrong, 4 wrong either
way. Designer -> Add/Size edge counts are unchanged.

EXTRACTION_VERSION 27 -> 28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 14:41:59 +00:00
Colby Mchenry 217190680d fix(telemetry): recover from the D1 10 GB outage; dashboard starts on today (#2317)
The telemetry database hit D1's 10 GB cap on 2026-08-11 and refused nearly
every write for seven weeks: the dashboard kept showing Aug 9 as the latest
day, and its activation query (a cohort join over raw events, ~55 s per
week) stalled D1's single query lane until every panel failed.

Root cause was the client: usage counters were aggregated per process and
appended on exit, so every `serve` launch, CLI command and prompt hook
uploaded its own `count: 1` line - 30.3M of the 30.8M event rows.

- Client: merge count lines per (day, kind, name, client) on append,
  stale-claim recovery (which also bypassed the size cap) and send.
- Ingest: usage counters ADD into usage_daily, one row per machine x day x
  tool (migration 0003), so storage no longer depends on upload frequency.
- Rollup: reads usage from usage_daily, folds legacy usage rows out of
  events in 50k-row transactions, maintains machine_first_seen.first_index_day
  (0002), catches up on days a failed run missed, purges usage_daily with
  retention. 0004 drops the now-unused events_machine_day index.
- Dashboard: ranges end today; rolled-up series return null past the
  rollup's last day instead of a phantom zero; no panel reads raw events
  (activation reads first_index_day); min/max lookups stay index-only;
  one retry on transient 5xx; banners when ingest stalls or the rollup
  falls behind.
- scripts/backfill-rollup.sh re-runs the rollup over a day range.

Already applied in production: DB 10 GB -> 467 MB, all usage counts kept,
rollups current through 2026-10-02.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 05:58:44 +00:00
Colby Mchenry dbb3d63cb1 fix(mcp): wait for a stopping daemon, and for the proxy's fallback engine (#2311) (#2316)
* fix(mcp): wait for a stopping daemon, and for the proxy's fallback engine (#2311)

Now that QueryPool.destroy() waits up to 15 s for a query worker still
starting up (#2307), two callers gave up too early:

- stopDaemonAt() gave a daemon 3 s after SIGTERM, then re-probed its
  identity and reported it still running once the daemon had closed its
  socket, so a daemon waiting on a starting worker made `codegraph index`
  abort. A daemon that has closed its socket but still holds its lock is
  now given 17 s more to exit; nothing is signalled during that wait.
- The proxy's in-process fallback called engine.stop() and exited on the
  next line. It now exits once its engine, and any engine still being
  retired after a handback to the daemon, has stopped, with a 20 s
  backstop. Once shutdown starts it ignores client input and starts no
  new engine.

Extracted from bompus/codegraph@cc87ec34da,
a fork merge commit that cannot be cherry-picked whole; only these four
files are taken, unchanged.

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

* fix(mcp): a stopping daemon is waited for whether it closed its socket or released its lock (#2311)

Builds on the previous commit:

- stopDaemonAt: one rule after the first 3 s. If the daemon's identity can
  be re-proven (same lock, socket answers), escalate to SIGKILL as before.
  Otherwise, whether it has stopped answering its socket or already let go
  of its lock, it is partway through its own shutdown: signal nothing more
  and wait, bounded, for the PID to exit. The previous commit reported a
  daemon that had released its lock but not yet exited as still running,
  a race at the end of every slow shutdown. The cleanup that follows
  compares the lock with the one signalled, so a successor's is kept. On
  Windows, where SIGTERM is TerminateProcess, the same wait covers a
  termination still settling.
- The grace is WORKER_START_SETTLE_MS + 2 s and the proxy's backstop
  WORKER_START_SETTLE_MS + 5 s, so they move with the settle wait they
  cover. stopDaemonAt takes `shutdownGraceMs` so a test can bound it.
- The proxy says so on stderr when its backstop fires.

Tests: the daemon fixture's setup and cleanup are shared, and the fixture
process is always killed on cleanup (a failing run used to leak it). New:
a daemon that released its lock mid-shutdown is waited for; a daemon that
never finishes stopping is still reported after the grace and never
SIGKILLed; the proxy waits for an engine being retired at shutdown; and
client input arriving once shutdown has begun starts no engine.

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

---------

Co-authored-by: Aaron Queen <bompus@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 05:05:29 +00:00
Colby Mchenry 70fe0ecc52 fix(windows): helper threads never end mid-start or with GC marking in flight (#2309)
On Windows a worker thread that ends - terminate(), its own process.exit(),
or the process exiting - while V8's concurrent marker is marking its heap
crashes the whole process (exit 3221225477 / 0xC0000005, no error, no dump).
Bisected on the Windows 11 ARM64 VM: --no-concurrent-marking alone stops it
(18/480 -> 0/480; no other concurrency flag does), but it costs about a third
of indexing speed, so CodeGraph avoids the dangerous moments instead:

- src/worker-teardown.ts: owners terminate a worker only once it has started
  (its first message, posted after its modules load - mid-load is by far the
  worst moment), and a worker about to exit runs a full collection first,
  which finishes any marking in flight (gc reached at runtime, never global).
- Resolver pool / store writer: the worker collects before exiting on
  'close' (the normal path - main 21/480 crashed on the VM, now 2/1200), and
  the 5 s close fallback never terminates a worker still starting.
- codegraph_status change counts: the worker posts 'loaded' after its heavy
  require and collects before answering; a deadline answers unknown at once
  but terminates only once loaded (3/720 -> 0/1440), and MCPEngine.stop()
  waits for measurements in flight.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:58:28 +00:00
Colby Mchenry 3d86bcc037 fix(windows): the query pool never terminates a worker still starting up (#2307)
Terminating a worker thread while it is still loading its modules can crash
the whole process on Windows (exit 3221225477 / 0xC0000005, no error, no
dump). QueryPool.destroy() terminated every worker at once, and the daemon
destroys its pool whenever it stops - a session that ends within a few
seconds of starting had its eager worker in exactly that state.
MCPEngine.stop() didn't even wait for the pool before the daemon called
process.exit().

- QueryPool tracks each worker's start (settled on 'ready', error or exit);
  destroy() answers outstanding calls first, then terminates each worker once
  it has started, capped at 15 s - the parse pool's #2301 mechanism.
- MCPEngine.stop() awaits the pool.
- #2301's explanation is corrected: on the Windows VM, terminating workers
  mid grammar compile never crashed (0 in 2,161), but terminating them while
  they load CodeGraph's extraction modules did. The parse pool's wait already
  covers that window, so only its comment and changelog wording change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:03:28 +00:00
Colby Mchenry e0f9c95e2f fix(resolution): a busy machine doesn't boot the resolver pool for a small project (#2303)
The resolve loop boots the worker pool mid-run when the remaining refs'
projected sequential settle (observed per-ref rate x refs left) clears 400 ms.
Load inflates the observed rate, so on a loaded 4-core Windows VM a ten-file
project (19 refs) projected 506 ms for its last 11 and booted three workers in
122 of 240 runs under 24 parallel inits (0 of 80 at 8). The pool only fans
out batches of 1,000 refs, so it resolved nothing; synthesis then waited out
its boot (dedupe-merge 6.3 s vs a 6.5 s boot) and its three extra database
connections pushed the VM's free commit to 11 MB.

Adaptive engagement now also needs 20,000 refs left. tokio-class Rust repos
(~55k refs at ~36 us) still engage; a smaller remainder never repays a
seconds-long boot. With the floor, the same load booted the pool in 0 of 240
runs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:20:23 +00:00
Colby Mchenry 91b9bba133 fix(extraction): never terminate a parse worker while it is loading grammars (#2301)
On a loaded Windows machine `codegraph init` / `index` sometimes died right
after the parse phase with 0xC0000005 (STATUS_ACCESS_VIOLATION), no WER report
and no output past the last parsed file. The parse pool's destroy() terminated
every worker at once, and a short index often ends while a late-spawned worker
is still compiling its grammar WASM; terminating a worker with that compile in
flight crashes the process.

Reproduced on a Windows 11 ARM64 VM without CodeGraph's pipeline: starting
three parse workers with preloaded grammar bytes and terminating them mid-load
crashed 5 of 720 child processes, terminating them only after
grammars-loaded crashed 0 of 360. Under the real load (24 parallel
`codegraph init` runs on 4 cores) main crashed after parsing in 8 of 1,200
runs; with this change 0 of 1,080 did.

destroy() now waits for a worker that is still loading to report
grammars-loaded or exit before terminating it, capped at 15 s so a wedged load
can't hold teardown forever. Recycling only ever touches idle workers, and a
hung parse is still killed by the hard timeout.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:48:01 +00:00
Colby Mchenry 273e5cdc4c fix(upgrade): a Windows upgrade no longer breaks the install while CodeGraph is running (#2185) (#2294)
* fix(upgrade): a Windows upgrade no longer breaks the install while CodeGraph is running (#2185)

On Windows, `codegraph upgrade` renamed only node.exe aside and then
Copy-Item'd the new bundle over current\. Any open agent session runs a
CodeGraph MCP server from current\, which keeps the native kernel loaded, so
the copy failed on lib\kernel\codegraph-kernel.node halfway through, leaving
no node.exe and a mix of versions that no codegraph command could repair.
Re-running install.ps1 failed the same way, because it Remove-Items current\.

Both now share one PowerShell function (a test pins the two copies equal).
The bundle is unpacked next to current\ (same volume), then swapped in file
by file: every file being replaced or dropped is first renamed aside (Windows
allows renaming a running exe or a loaded DLL, just not overwriting or
deleting it), then the staged file is moved in. Any failure or interruption
undoes every step. Renamed-aside files, including the node.exe.old-* left by
earlier upgrades, are deleted on the next successful run once nothing holds
them. The upgrade's exit code now tells whether the install was left
unchanged or needs a reinstall, and the message says which.

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

* docs(changelog): Windows upgrade with CodeGraph running (#2185)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:19:41 +00:00
Colby Mchenry 7bbc1fb7f6 fix(tests): the test suite no longer writes to the developer's real home dir (#2275) (#2295)
Running the suite on a machine with Claude Code configured could wire the
CodeGraph prompt hook into the developer's own ~/.claude/settings.json:
every upgrade test whose fake upgrade succeeded ran the real post-upgrade
prompt-hook self-heal, which detected the real global Claude profile and
wrote it. Daemon suites likewise left records in the real
~/.codegraph/daemons. On Windows a HOME-only redirect would not have
helped, since os.homedir() reads USERPROFILE there.

- The prompt-hook writer is now injected through UpgradeDeps
  (wirePromptHook); the CLI passes the real defaultWirePromptHook and the
  upgrade tests a recorder. New tests pin that a successful upgrade calls
  the recorder and that the real writer never touches a configured profile.
- A vitest setupFiles entry gives every engine test file a throwaway home:
  HOME/USERPROFILE (plus HOMEDRIVE/HOMEPATH/APPDATA/LOCALAPPDATA on
  Windows), XDG_CONFIG_HOME, and GIT_CONFIG_GLOBAL (seeded with a dummy
  identity) point into it, and CLAUDE_CONFIG_DIR/CODEX_HOME/COPILOT_HOME/
  HERMES_HOME are cleared. Spawned CLI/MCP children inherit it. A sandbox
  test asserts os.homedir(), the Claude profile, the daemon registry, a
  child process and git's global config all resolve inside it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:18:46 +00:00
Colby Mchenry e7389b22fb fix(mcp): a session serving in-process hands the project back to the shared daemon (#2277) (#2293)
* fix(mcp): a session serving in-process hands the project back to the shared daemon (#2277)

When a proxy lost its daemon (or never reached one) it served the rest of
the session through an in-process engine. That engine takes the project's
writer lock in fallback mode and nothing ever released it or tried the
daemon again, so every later daemon start for the project failed on the
held lock and every other session ran read-only without auto-sync until
that one session ended. Sessions that fell back read-only stayed read-only
for life too.

The degraded proxy now retries the shared daemon with backoff (5 s,
doubling to a 5 min cap; CODEGRAPH_DAEMON_RETRY_MS / _MAX_MS, 0 disables)
through the existing probe/spawn/poll path. A writing engine is stopped
first, after the calls it is serving have answered, so its lock is free
before a daemon is spawned and two writers never overlap; calls arriving
meanwhile are buffered and go to the daemon, or are served in-process
again if none comes up. A read-only engine keeps serving until a daemon
answers and is closed once its calls finish. The client's initialize is
replayed to the new daemon with its reply suppressed, as on first attach.

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

* docs(changelog): a degraded session hands the project back (#2277)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:18:02 +00:00
Colby Mchenry 45a680df07 fix(mcp): one daemon per project however its root is cased on Windows (#2278) (#2285)
* fix(mcp): canonicalize the daemon rendezvous root on Windows

A project spelled d:\work\codegraph or D:\work\codegraph is one directory, but the daemon socket/pipe name and the registry record name hashed a raw path.resolve, which preserves whichever casing the caller passed. A proxy that computed the other casing probed a socket nobody bound, spawned a redundant daemon, and that daemon exited on the lock the running one held, so the session silently served the graph in-process: no shared watcher, no auto-sync, one extra copy of the index per open window.

Both names now come from one shared canonicalProjectRoot (native realpath, lowercased on win32), so every spelling meets the same daemon and codegraph list / stop --all see one record per project.

(cherry picked from commit 1811788018)

* fix(mcp): share one in-process project however its root is cased (#2278)

The engine's project lifecycle and `codegraph stop`'s registry fallback keyed
a project by its realpath, which keeps the caller's casing, so two spellings
of one root opened two graphs and two watchers in one process. Both now use
the same identity key as the daemon socket.

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

* docs(changelog): file the Windows rendezvous fix under Unreleased

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

---------

Co-authored-by: 养心堂主 <zhouzhk@qq.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:16:25 +00:00
Colby Mchenry 7c01910e54 fix(cfml): tag-based calls, wrapped functions and <cfinterface> are indexed (#2091) (#2296)
* fix(cfml): tag-based calls, wrapped functions and <cfinterface> are indexed (#2091)

Tag-based CFML lost almost all of its call edges. The tag walk sent only
<cfscript> and <cfquery> bodies through call extraction, although the cfml
grammar parses the expressions in <cfset>, <cfif>/<cfelseif>, <cfreturn>
and #hash# (output, strings, attributes) with the same rules as cfscript.
The walk now records each such expression with the scope that owns it
(the enclosing <cffunction>, else the component, else the file) and reads
them all through the existing cfscript extraction in one parse per file:
the expressions are copied out on their original lines, each ended with a
`;`, and every reference is handed back to its expression's scope with its
column restored. <cfloop condition="..."> is read the same way.

A <cffunction> nested in a generic container tag at component or template
scope (<cfprocessingdirective> in Application.cfc, <cfsilent>) was skipped
because only direct children were examined; it is now extracted (never
through an ERROR node, where a lost <cfcomponent> would misfile methods as
top-level functions). <cfinterface>, a generic tag to the grammar, now
yields an interface node with its functions as methods and an `extends`
ref per comma-separated parent, so `implements` resolves to it.

The tag-based parse tree is now freed after the walk.

Resolution: the CFML/Ruby/C# bare-call scope filters evaluated the
receiver-on-line check once per same-named candidate though only the ref
decides it. With tag calls extracted, that made Mach-II's resolution ~8x
slower; the check is now memoized per ref (edges byte-identical).

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

* docs(changelog): tag-based CFML calls (#2091)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 05:00:47 +00:00
Colby Mchenry 2ff7a99c0e fix(cpp): bound the macro-visibility include walk (#2127) (#2292)
* fix(cpp): bound the macro-visibility include walk (#2127)

isVisibleCppMacro walks the call site's translation unit to learn whether a
function-like macro is visible there (#1838). A header whose guard the walk
cannot decide -- `#define X_H 1` (the OpenSceneGraph / osgEarth idiom, which
guardsItself does not read as a guard), or any header reached under an
unknown `#if`, where neither its guard nor `#pragma once` settles -- was re-scanned on
every inclusion path, and every re-scan pushed its define events onto the
cached timeline again. On a layered include graph that is 2^depth scans and
2^depth timeline entries: "Resolving refs" died with "Ineffective
mark-compacts near heap limit" at the first ref whose file reached such a
tree, at the same ref whatever the heap size.

The walk now keeps a version that every observable change bumps
(definitions, `#pragma once`, the macro-name set). A re-entry with the same
inherited truth, while the version still equals that of an earlier visit
which itself changed nothing, is skipped: it would start from the same state,
take the same branches and change nothing again, and the events it would push
repeat each macro's current state, already its last event. Include cycles cut
by the recursion stack are recorded per visit and must still be cut for a
skip. A hard budget of 1M directive events per walk bounds what is left;
past it the timeline is unknown and nothing at or after the cut line is
suppressed, so the budget can only fall back to ordinary resolution, never
invent a macro.

Answers are unchanged: 20,000 random include graphs (guards bare / valued /
pragma once / under flags, #if/#elif/#else, #undef, cycles) give identical
isVisibleCppMacro results to the previous walker on 1.25M queries, with
about one re-entry in eight skipped.

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

* docs(changelog): credit the C/C++ macro walk bound

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

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:49:00 +00:00
Colby Mchenry f838c5a02a fix(resolution): resolve method values on untyped Python module globals (#2074) (#2291)
* fix(resolution): resolve method values on untyped Python module globals (#1820)

A method value whose receiver is a module global without a static type
(`thread_pool_exec(settings.conn.fetch, ...)`, where `conn = None` is
rebound by `global conn; conn = Backend()`) resolved to a `variable` node
and produced no edge. The global's type is now the set of classes its
module assigns to it, at module scope or in a function that declares it
`global`. One class resolves to its own method; several bind to the
nearest declaration every candidate inherits, as a base-typed receiver
does.

No edge (a wrong callback edge is worse than none) when:
- any binding of the global in its module is not a whole constructor
  call: a factory, a conditional, a tuple target, `for`/`with`/import,
  a star import, or an annotation the constructor contradicts;
- the caller or an enclosing function binds the name itself: parameter,
  assignment (any target position), loop, comprehension, `as`, `case`
  pattern, lambda parameter;
- the receiver is a deeper chain (`settings.conn.pool.fetch`);
- the importing file rebinds the name, or imports it from two sources.
Statements are read with their continuation lines, and only statement
lines (bracket depth 0) can assign. Not seen: rebinding the global from
another module (`settings.conn = X`) or through `globals()`.

`pkg.mod.Cls` now resolves through `import pkg.mod`, unless two namespace
imports share the last segment.

Python import mappings also read parenthesized from-imports
(`from x import (\n    A,\n    B,\n)`), which previously yielded the
single name `(` and hid every class imported that way from receiver and
inheritance resolution. Comments and docstrings are stripped first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0aab32a18b)

* fix(resolution): account for writes to a Python module global from other files (#2074)

The module-global receiver type was read from the global's own module only,
so `settings.conn = Decoy()` in another module, or `globals()["conn"] = X`,
left a stale type and a wrong edge.

- A production write `<module>.<name> = Cls(...)` from another file joins
  the type set (resolved in the writing file); any other production write
  (another value, a tuple target, `setattr`/`patch.object`) makes the type
  unknown. The module is matched through each file's imports, including
  aliases and relative imports.
- Test files install doubles (`settings.conn = MagicMock()`,
  `monkeypatch.setattr(settings, "conn", ...)`): they do not change the
  production type, but a ref inside such a test resolves nothing.
- `globals()[...] = ...` with the global's name or a computed key, and
  `globals().update(...)`, in the global's module make the type unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 2055711226)

* fix(resolution): match a module global's writers by file, not dotted suffix

Review follow-up to the cross-module write scan.

- A write now lands on the file its import names: relative imports resolve
  exactly, absolute ones by path suffix; when a spelling could name another
  file sharing the tail (`x/settings.py` and `y/settings.py` for
  `import settings`), a write through it makes the type unknown instead of
  feeding or clearing the wrong module.
- `import a.b` binds `a`: the module is spelled `a.b`; the mapping's
  last-segment name is not treated as an alias of it.
- Test doubles are recognised by the narrow test-suite set plus any
  `conftest.py`; examples, fixtures and benchmarks are production writers.
- Writes inside `if __name__ == "__main__":` are script code, not module
  state, in the global's module and elsewhere.
- Namespace-dict writes: `globals()`, `vars()` and `sys.modules[__name__]`
  are read per statement (continuations joined, strings ignored); any use
  other than a literal-key read or `.get`, or a literal-key write of the
  name, makes the type unknown. `<module>.__dict__[...]` / `.update(` from
  another file does too.
- `settings.conn: Store = Store()` and `settings.conn = (Store())` are
  plain constructor writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 7aab29269a)

* fix(resolution): explicit dotted aliases, one-line __main__ guards, scoped vars()

Review follow-up.
- `import pkg.settings as settings` binds `settings` even though it equals
  the last segment; the import mapping cannot tell it from a plain
  `import pkg.settings`, so the source line decides.
- `if __name__ == "__main__": stmt` on one line is script code, and a
  `globals()` write inside a `__main__` block no longer counts.
- `vars()` is the module dict only at module scope; inside a function it
  is the locals, so `return vars()` there no longer makes the type unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0a5fb69797)

* fix(resolution): explicit-alias check matches the exact module, outside strings

Review follow-up: `import other.pkg.settings as settings` or a string
`"import pkg.settings as settings"` no longer marks a plain
`import pkg.settings` as explicitly aliased, which attached a write to the
wrong module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit a5569a07ea)

* test(resolution): give the Python module-global adversarial table its own timeout

It indexes one project per case, which takes about nine seconds on macOS,
past vitest's five-second default.

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

* docs(changelog): Python module globals and multi-line imports (#2074)

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

---------

Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:47:36 +00:00
Colby Mchenry a4bd890ec6 fix(python): a call on self is never resolved through a same-named import (#2290)
The Python extractor records `self.get_ip(request)` by its bare name, so the
import strategy matched it to a function the file imports as `get_ip` and
the call never reached the class's own (or inherited) method. A bare-named
Python call written on `self` now skips import resolution; the method
strategies then find it. A plain `get_ip(request)` still binds to the import.

Across allauth, DRF, netbox and mealie this retargets 16 calls, all from an
imported helper to the class's own method; no edge is added or lost.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:42:23 +00:00
Colby Mchenry a5184ed5d4 fix(pascal): Delphi .inc include files are indexed as Pascal, not PHP (#2279) (#2289)
* fix(pascal): Delphi .inc include files are indexed as Pascal, not PHP (#2279)

`.inc` maps to PHP (Drupal), so a Delphi/Free Pascal include — a
`{$IFDEF}`/`{$DEFINE}` block or a declaration fragment pulled in with
`{$I defs.inc}` — was parsed as PHP (untagged text = inline HTML) and
contributed nothing.

detectLanguage now decides `.inc` by content: a PHP open tag (`<?php`,
`<?=`) anywhere keeps it PHP; otherwise a Pascal-only line shape (a
compiler directive, a `;`-closed routine header, `unit`/`uses`, a
const/var/type section, a begin/end; pair, Lazarus `{%MainUnit}`) makes
it Pascal; anything else keeps the PHP routing so C / asm / POV-Ray /
SourcePawn / Makefile includes are not handed to the Pascal grammar. A
leading BOM is skipped. An explicit codegraph.json `.inc` mapping wins.
The rule is per file, so a full index and a sync always agree.

The grammar preload set adds Pascal whenever an `.inc` is present (an
include-only tree otherwise had no Pascal parser), and the kernel-parity
script re-detects `.inc` like the indexer does.

Checked on 875 real `.inc` files: 0 of 436 non-Pascal ones (Drupal 7,
php-src, LLVM, FreeBSD mk, POV-Ray, SourceMod, edk2) flip; 433 of 439
Pascal ones (FPC, Lazarus, Castle, mORMot2, JCL) are detected — the rest
are statement-only or bare-literal fragments with nothing to extract.

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

* docs(changelog): Pascal include files (#2279)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:28:02 +00:00
Colby Mchenry e28ac71048 fix(sync): same-named symbols keep their own cross-file callers across a re-index (#2276) (#2288)
* fix(sync): same-named symbols keep their own cross-file callers across a re-index (#2276)

Re-storing a file re-attaches the edges other files had into it by moving
each edge to the re-indexed node that replaces its target. That pairing
keyed new nodes by (kind, name) with the last one winning, so in a file
defining a name twice — the same method on two classes, a method's
overloads — any edit, even a body-only one, moved every incoming caller
of those symbols onto one of them. A body-only edit leaves the sync's
definition delta empty, so nothing re-resolved them afterwards.

The re-store now reads the file's old node identities alongside the edge
snapshot and pairs old nodes with new ones by (kind, name), then
qualified name, then signature, then position within a same-sized group
of otherwise identical nodes. A target no tier can pair one-to-one is
resurrected as its original unresolved reference, so resolution rebinds
it instead of the store guessing.

The common in-memory re-store path also now deletes the old rows, stores
the new ones and re-attaches incoming edges in one transaction. Committed
separately, a failure between the steps left the file looking new or
already current, and other files' edges into it were lost until a full
re-index.

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

* docs(changelog): same-named symbols keep their callers across a sync (#2276)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:26:51 +00:00
Colby Mchenry 455175da67 fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110) (#2287)
* fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110)

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:24:34 +00:00
Colby Mchenry 5271c89f08 fix(php): a class written with a namespace in its name resolves to that class (#2256) (#2286)
* fix(php): a class written through a namespace alias resolves to its class (#2256)

`use App\Fields as Field;` aliases a namespace, so `new Field\FirstName()`
names `App\Fields\FirstName`. Expand the first segment through the file's use
map and look the class up by its qualified name, before the pre-filter that
drops names without a `.` or `::`.

(cherry picked from commit 3917de431cbe8d8bbba32d9f4c739458c36e3fed)

* fix(php): resolve every qualified class name the way PHP reads it (#2256)

Extends the namespace-alias fix beyond `new`: extends, implements, static
calls (`Alias\X::make()`), names relative to the current namespace and fully
qualified `\Ns\X` now all resolve by their exact qualified name. A name
with no single project class is outside the project, so it no longer falls
through to a same-named guess; a static method the class only inherits is
left to the strategies that walk supertypes.

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

* docs(changelog): one entry for the PHP qualified class names fix

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

---------

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

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

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

(cherry picked from commit 18d65f8e10)

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

(cherry picked from commit f1b0ec7f56)

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

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

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

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

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

---------

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

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

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

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

* docs(changelog): credit the idle project release

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

---------

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

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

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

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

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

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:13:42 +00:00
Colby Mchenry 96ba880556 fix(prompt-hook): skip Claude Code <agent-message> subagent hand-backs (#2184) (#2281)
* fix(prompt-hook): skip Claude Code <agent-message> subagent hand-backs

Claude Code delivers a subagent's report to the parent session as a
UserPromptSubmit prompt wrapped in <agent-message from="…">…</agent-message>.
The prompt hook treated it as user input and ran a full codegraph_explore
on the whole report, which is slow on a multi-KB prose query (past the
30 s hook timeout in the report) and injects context unrelated to the
user's question.

Add isAgentMessage next to isTaskNotification (#1832) with the same rule:
only a prompt that is entirely the envelope is skipped, so a user question
that mentions the tag still goes through the gate.

Fixes #2184

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

* docs(changelog): credit the prompt-hook hand-back fix

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

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:12:11 +00:00
Colby Mchenry 404e7827bc fix(windows): hide the git calls that flash console windows (#2094) (#2280)
* fix(windows): hide the git calls that flash console windows (#2094)

The two git calls added for exclude-standard scope parity (#1728),
`git config --get core.excludesFile` and
`git ls-files -o -i --exclude-standard --directory`, ran without
windowsHide. Inside the detached, console-less MCP daemon each one got a
fresh visible console, so a window flashed several times on daemon start
and again on every auto-sync. With Windows Terminal as the default
terminal each flash is a full Windows Terminal window. `git config
user.name` in the viewer's trails API had the same gap.

Add windowsHide: true to all three, and replace the per-file regex guards
with an AST guard over every child_process spawn in src/: options passed
through a variable or a function parameter are traced back to the object
literal that defines them, through every call site.

Supersedes #2095 (same fix, rebased on current main). Closes #2094,
closes #2096.

(cherry picked from commit dd2db41f1b)

* docs(changelog): credit the Windows console-window fix

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

---------

Co-authored-by: Suharaz <minesuhara@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:09:56 +00:00
Colby Mchenry 911fcf6033 fix(vapor): a closure route's calls are the route's (#2273)
A Vapor route whose handler is a trailing closure had no handler symbol and
linked to nothing (vapor's own repo: 76 of 80 routes bound to nothing in the
viewer browser pass). The closure is the handler: its body's calls, each once
with its receiver, are referenced from the route, as an Express inline
handler's are.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:52:43 +00:00
Colby Mchenry f416357b93 fix(cobol): a file ending inside the sequence area no longer hangs the parser (#2272)
* fix(cobol): a file ending inside the sequence area no longer hangs the parser

The vendored COBOL grammar's external scanner skips the fixed-format
sequence area (columns 1-6) with `while (get_column() <= 5) advance()`.
At end of input advance() is a no-op, so a file whose last line stops in
that area (`    .`, `X\n    .`, a sentence closed by a short `    .` line)
never reached column 7 and the scanner spun forever inside wasm. Indexing
hit the parse worker's hard timeout three times (~93 s) and stored the
file with zero symbols; the viewer's highlighter froze on the same text.

The loop now also stops at eof(). It is upstream code (present at
e99dbdc3), so the change is a standalone upstreamable fix; inputs that
parsed before are unaffected (412 NIST + ocesql test sources give
byte-identical trees with the old and new wasm, and the upstream corpus
keeps its single pre-existing failure).

Regenerates docs/grammars/tree-sitter-cobol.patch, rebuilds the wasm per
docs/grammars/tree-sitter-cobol.md, and adds an out-of-process regression
test under a hard deadline (a regression is an uninterruptible wasm loop).

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

* test(viewer): force the highlight deadline instead of relying on a grammar bug

The bounded-highlight test used the COBOL `    .` slice as its slice that
never finishes; the grammar fix makes it parse, so the deadline is now
forced through tokenizeBounded's deadline parameter.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:47:52 +00:00
Colby Mchenry 104b8de3e6 fix(laravel): a string controller resolves without a Controller suffix (#2271)
Only `XController@action` was claimed and resolved, and the written
namespace was stripped, so akaunting's 'Common\\Uploads@inline' (and a
Route::resource with a string controller plus options) bound 304 of 313
routes to nothing. Any `Class@action` is claimed; the namespace path under
App\\Http\\Controllers is kept and picks between same-named controllers;
a resource reads its first argument and resolves its controller class in
the Laravel resolver (no `use` brings a string-named class into scope).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:40:38 +00:00
Colby Mchenry e12f5bbb92 fix(nuxt, astro): a file-routed page is bound to its component (#2267)
A Nuxt pages/x.vue or Astro src/pages/x.astro route linked to nothing, so
the viewer's Steps drew a page alone and the routes list never named what
serves it. Each page route now references its file's own component (never a
same-named one — every index.vue is a component named index), an Astro
endpoint its exported verb handlers, and the routing manifest accepts a
component as what serves a route, which also lists Vue Router screens.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:33:41 +00:00
Colby Mchenry 41cfc8356f fix(viewer): highlighting runs in a worker the server can stop (#2266)
The viewer's server classifies a code slice with the engine's tree-sitter
grammars on its one thread. The COBOL grammar's scanner loops forever on a
line like `    .`, which a cobolcraft Flow card's window ends with, so the
whole server froze — /api/stats included — and a parse inside WebAssembly
cannot be interrupted from JavaScript. Slices are now classified in a
worker that is terminated past a 2 s deadline; the slice is served plain
and the next one gets a fresh worker. Request-time parses also carry a
progress-callback budget.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:33:37 +00:00