Skip to content

Merge upstream main through 69af19ac (native-kernel reconciliation) - #552

Merged
bompus merged 96 commits into
fork/consolidatedfrom
merge/upstream-69af19ac
Oct 27, 2026
Merged

bompus merged 96 commits into
fork/consolidatedfrom
merge/upstream-69af19ac

Conversation

@bompus

@bompus bompus commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Summary

Reconciles the fork's native Rust extraction/resolution architecture with upstream main through 69af19ac — 94 commits past the prior merge point 553d5d10. Upstream's deleted TypeScript resolver spine stays deleted; its behavior was ported into the kernel (codegraph-kernel/src/resolve/) or surviving modules.

57 conflicts resolved: 46 both-modified, 11 modify/delete, 1 no-base. Golden dumps rebased — every .dump delta traced to an intended upstream port.

Ported into the kernel

  • new X().m receiver resolution (upstream newReceiver arm) into resolve_call_chain + match_new_receiver_call; C# isCsharpTypeVisible.
  • Dart typedef classes / type-alias refs; Java record classes + compact-constructor is_class_type + parameterized supertypes (java/records.rs); Java typeParameters parity in the generic extractor.
  • Ruby singleton-method is_static; Go #2596 scoped fn-ref Cand.node threading; Go type refs demote to references (Go: three smaller call-edge inaccuracies (type conversions, external test packages, build tags) colbymchenry/codegraph#2581).
  • TSJS: this[…] subscript and new-expression receivers, is_unresolved_member_chain, visit_parameter_defaults.
  • Python from X import Y module-file binding; PHP namespace-alias case-insensitive lookups (file scope + resolve_php_qualified_class).
  • ts_class_type_parameter no longer truncates generic heads at { (Shaped<Item extends { id: string }>).
  • Pascal bare function-reference capture kept (upstream's addressOfOnly assumes pascal-scope.ts, which the kernel covers); C macro NAME(a)(b){ shape kept by isCReturnTypeMacro (fix(c): a function whose return type is a macro call is indexed colbymchenry/codegraph#2773).

Fork behavior restored after the merge dropped it

  • cg_folded_contains SQLite registration + defensive nested transaction rollback.
  • searchNodesFTS two-phase ranked query head — the malformed union had silently disabled FTS for all queries.
  • C/C++ blankers (comment stripping, elifdef, expression guards, const/volatile traversal).
  • Nuxt route-group/interior-index normalization + server-route method-suffix handlers.
  • Explore-budget advisory mock (getSizeStats); release-workflow comment rewording.

Adapted rather than restored

  • resolution.test.ts: upstream-only API describes excised — the kernel covers the behavior natively. Upstream's parity suites for the deleted TS resolver and the wasm check-grammar script/test removed.
  • src/resolution/jvm-overloads.ts added carrying the two name-matcher helpers play.ts still needs.

Known remaining gaps (follow-up work, not regressions)

43 tests fail in upstream-NEW suites — these exercise upstream features the kernel doesn't yet implement; they need dedicated porting passes, not merge fixes:

  • CFML (cfml-include, cfml-create-object, cfml-inheritance-resolution): 10
  • Ruby (ruby-namespaced-receiver, ruby-singleton-methods): 19 — includes (wasm) describes that can never pass on the fork (wasm removed); the (kernel) variants show real namespaced-constant gaps
  • js-super-calls: 3 — super.m() edge emission
  • pascal-parenless-calls: 10 — upstream pascal-scope.ts paren-less call linking
  • rust-receiver-chains: 1 — nested self.field.m1().m2 resolves the outer member to the field's type instead of the inner call's return type

Previously verified green (focused runs this session): python-module-resolution 50/50, function-ref 44/44, nuxt-routes 27/27, ts-array-annotated-receivers 14/14, php-namespace-alias-instantiation 9/9, kernel-generic-extractor-tree parity, expo-modules, resolution.test.ts, expression-receiver-calls, cs-twin-class, kernel-typed-styled, esm-externals.

Test plan

  • All 57 merge conflicts resolved; zero conflict markers tree-wide
  • cargo clippy --profile quick --lib clean; TypeScript typecheck clean
  • Golden dumps rebased (UPDATE_GOLDEN=1); each .dump delta explained in the commit message
  • Focused suites: all previously-fixed files pass (list above)
  • 43 upstream-new-suite failures remain — dedicated ports needed (CFML, Ruby namespaced/singleton, JS super calls, Pascal paren-less, 1 Rust chain case)
  • Strict native-kernel speed gate not re-run (known limitation inherited from refactor(kernel): build the kernel as a native Rust library without Node-API #551 — needs npm run build:kernel optimized build + EVAL_REPOS=… npm run eval:precision)

README rows checked

  • Merge-point row updated to 69af19ac (README:55). No user-visible feature/CLI/MCP/language/agent-target changes; no quoted numbers moved.

colbymchenry and others added 30 commits October 21, 2026 16:56
…od (colbymchenry#2755)

A Go call on an indexed element (kubectl's `schemas[ix].ValidateBytes(data)`,
deepcopy-gen's `(*in)[i].DeepCopyInto(&(*out)[i])`, go-to-protobuf's
`m.Items[len(m.Items)-1].Unmarshal(...)`) reaches the resolver by its bare
name, at the column where the receiver starts, and nothing typed the
receiver: a method one type declares was guessed by name, often onto a
namesake, and a common one like `DeepCopyInto` got nothing.

matchGoIndexedCall, an early hook beside matchGoAssertedCall, reads the
receiver chain with goChainLinks and types it as Go does: the head from the
declaration in scope (goLocalDecl; readGoScopes now records where each `:=`
or `var` value starts and which name it is) or a package-level variable;
values (composite literals, make, new, append, conversions, fields,
elements, addresses, range elements); struct fields, promoted ones too; and
the element type of a slice, array, array pointer, map or named slice type,
with `(*p)` dereferenced. The method is the element type's own, its
interface's or one embedding promotes, and a type parameter's comes from its
constraint. An element type from outside the project, a predeclared one, a
type literal or an outside alias links nothing, and so does a method the type
declares in a file of its package the index couldn't read (kubernetes'
core/v1 generated.pb.go), which is read from that file's text. What can't be
typed (a call's result) is left to the other strategies.

On kubernetes 4c05e667 the indexed-element calls go from 760 resolved and
2,284 unresolved to 2,666 resolved and 378 unresolved; every changed site in
kubernetes, prometheus and etcd is such a call, nodes are identical, and the
express and flask controls are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s not its embedded type's (colbymchenry#2757)

kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB
limit, so every API type's own String, Marshal, Unmarshal, Size and
MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on
a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over
embedded types found the method the embedded TypeMeta or ObjectMeta has and
linked that, a method the call never runs.

resolveGoMethodInPackage, which every typed Go receiver goes through
(parameters, locals, fields, assertions, package-level values, indexed
elements), now reads the method declarations of the files of the type's
package that the index couldn't read, from their text, and links nothing
when the type declares the method there. matchMethodCall's typed branch and
matchGoPackageValueCall keep that null instead of falling through to name
matching, which found the same promoted method. A method the type really
gets from an embedded type, like GetName from ObjectMeta, still links. The
check that colbymchenry#2755 added for indexed elements moves into the shared path.

On kubernetes 4c05e667, 63 calls lose that link and nothing else changes;
etcd, prometheus, express and flask are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nts its methods (colbymchenry#2759)

goImplementsEdges (colbymchenry#584) follows declared embeddings since colbymchenry#2402, but an
embedding of a type outside the index resolves to nothing, so it added no
methods: prometheus's TBRun (testing.TB plus Run) went to a struct with only
a Run, QueryLogger (slog.Handler plus io.Closer) had no implementations,
kapacitor's Diagnostic (a third-party Logger plus Log) took every struct with
a Log, and the predeclared `error` was not recorded at all.

- go-stdlib-methods.ts lists the methods of 130 standard-library types, each
  as `name:params/results`, generated from go1.27.3's source and counted the
  way goSignatureArity counts a stored signature.
- Both extractors record an embedded `error`, which stays unresolved. The
  predeclared-type set is unchanged, so alias and defined-type references
  still skip it.
- goImplementsEdges reads each Go type's failed `extends` rows and reads the
  package back off the line (goRefQualification, now exported). A
  standard-library type's methods join the method set, keyed as goMethodKey
  keys the project's (`std:<import path>` for an unexported one), and the
  arity gate holds them to the table's counts on both sides: a `sync.Mutex`
  makes a struct hugo's `Locker`, not etcd's gRPC `LockClient`. Any other
  outside type is met by a type embedding the same one, or by the only type
  with every other method the interface names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Go's source (colbymchenry#2760)

go-stdlib-methods.ts lists the methods of 130 standard-library types with
their parameter and result counts. Its generator lived in a validation kit.
scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's
Go grammar, counts with the engine's own goSignatureArity, and prints (or
--write's) the table for the types it lists, so a later Go release's
methods are one command away. Against go1.27.3 it reproduces the table byte
for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… run the parameterless overload (colbymchenry#2762)

Three kinds of VB.NET member read that colbymchenry#2355 and colbymchenry#2377 left unlinked:

- A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`,
  a constructor's `Me.New` / `MyBase.New`) went to the first overload
  declared, so `Me.Load` reached `Load(path)` instead of
  `Load(Optional force = False)`, and a `Me.New` whose first overload was
  the calling constructor itself linked nothing.
- A read whose first name is a namespace (`Billing.Invoice.MaxLines`,
  `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front,
  a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was
  looked up as a value or a type only.
- A field, property or Module variable named bare before the dot
  (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was
  not counted as read: a bare name is no member access, so only what is read
  through it could link, and nothing when that is .NET's (`Text`).

Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output
unchanged):

- A method a reference runs without parentheses resolves among its type's
  overloads to one that takes no arguments (none, or only Optional and
  ParamArray parameters, read from the declaration), never the member the
  reference is in. AddressOf / NameOf keep the first overload, as before.
- A receiver name that is neither a value nor a type is looked up as a
  namespace the way VB.NET does: nested in a namespace around the read,
  outward to the root namespaces, then in the file's and project's Imports.
  Each further link is a type declared in it, a nested namespace, or a
  member of one of its Modules. The read links the type (`Billing.Invoice`)
  or the member (`Assets.Logo`), as colbymchenry#2355's `Outer.Mode` reads do.
- A bare name before the dot that VB.NET binds to a field, property or
  Module variable (not a local or parameter, which hide it) is linked as a
  read alongside what is read through it. Not for a `With` block's `.Text`,
  a method run through the name (the call is the method's alone, as with
  parentheses), another project's Module, or a member declared with an
  outside type of its own name (`Responser As Responser`), whose Shared
  members and nested types VB.NET reads through the type; a project type of
  its own name follows colbymchenry#2377's Color Color rule.
- membersNamed looked a type's members up in every project's same-named
  type and memoised the list by qualified name alone, so SCrawler's two
  plugin `UserData` classes shared one list ordered by whichever was asked
  first. It now keeps to the type's own project. Alone this leaves main's
  graph unchanged on SCrawler and staxrip; without it, 17 of the new
  bare-name reads in one plugin linked the other plugin's members.

Dropped from the 2026-10-04 test set: a method named through its class
also linking the class (colbymchenry#2355 treats it as a call, and its test pins that);
`AppSession.SessionId` through a property typed OtherSession linking
nothing (colbymchenry#2377 links OtherSession's field, pinned the same way);
`SCrawler.My.Resources...` reads (colbymchenry#2355/colbymchenry#2377 send no path that starts with
`My` or `Global`); and same-named classes picked by namespace, Imports and
project, which main already does once the fixture declares its
RootNamespace.

Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on
main and passes; a mutation that restores main's membersNamed fails the
own-project test. The other VB.NET, C#, extraction, resolution, sync and
framework suites pass; tsc and build clean.

Validation, main vs this branch (indexes from each arm's own build; nodes
unchanged; self-loops unchanged; serilog and gin dumps byte-identical):

- SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet`
  re-pointed from the two-argument overload to `ColorsGet() As DataColor`.
  Added: 4,973 bare-name reads (3,731 own class, 957 Module variables,
  285 inherited), 192 namespace reads, 12 reads through a Module member
  reached by namespace, and that 1 overload.
- staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads
  (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with
  the root namespace in front, and `box.Show` reaching
  `Function Show() As DialogResult` from its Shared overload.
- Checked against source: every bare-name read's name starts the access,
  has no local of that name, and binds to its class, an ancestor (23
  through a qualified `Inherits Instagram.UserData` the graph's own extends
  edge misses) or a Module of its project; every namespace read's path ends
  its target's full name and the target's project is the reader's or one it
  references; no edge crosses between staxrip and its AutoCrop tool; 48
  sampled edges hand-checked, all correct.
- Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall
  5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall
  7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the
  growth being the new edges (unresolved refs fall 47,074 -> 43,607 and
  38,051 -> 36,165).

Not covered: a call through a bare name (`Button1.Focus()`) or a function
named before the dot, reads in a `With Button1` block, and `My.` /
`Global.` paths.

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

  class Report { final int score; const Report(this.score); }
  typedef ReportCallback = void Function(Report r);

`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.

Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.

Validation, kernel path, arms built from dbb6aa9 and from this change,
each with its own kernel:
- felangel/bloc b9be1e25: 17,875 -> 17,884 edges (+9, 0 removed).
- rrousselGit/riverpod 4ded4f01: 84,726 -> 84,761 (+35, 0 removed).
- jonataslaw/getx e3c8fa04: 8,436 -> 8,456 (+20, 0 removed).
- alibaba/flutter-go a41b2124 (9 legacy typedefs, 6 with a return
  type): 7,994 -> 7,968; 39 wrong links removed (34 `Widget` and 5
  `Future` uses that had gone to typedefs misnamed `Widget` and
  `Future`), 13 added (4 from typedefs, 9 fields typed by the renamed
  ones).
- peng8350/flutter_pulltorefresh 0be096c1 (8 legacy, 5 with a return
  type): 3,186 -> 2,976; 221 wrong `Widget` links removed, 11 added.
All 88 added non-contains edges were checked against the source; every
removed edge pointed at a typedef misnamed after its return type. The
other new typedef refs name SDK or Flutter types and stay unresolved.
gin and zustand dumps are byte-identical across the arms, kernel and
wasm dumps are byte-identical on bloc, riverpod and pulltorefresh, and
the parity sweep over the five Dart repos is 2,330/2,633 files at
parity, 0 diffs, 303 deferred on both arms. Index time is unchanged
within noise (median of 5 interleaved runs: riverpod 12.6 s vs 11.9 s,
bloc 5.0 s vs 5.0 s, getx 2.8 s vs 2.8 s), and the DB grows by 8 to
24 KB (pulltorefresh shrinks by 64 KB).

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

A Go type written without a package qualifier was looked up only in the
file's own package. In a file that dot-imports a project package, like
harbor's config tests (`import . ".../controller/config"`, then a
`controller Controller` field), the type isn't there, so the call
`c.controller.AllConfigs()` fell back to name matching. Name matching
took the same-named type of a package the file doesn't import, the one
that sorts first: matchGoFieldChainCall used the struct's own directory
for a bare field type, resolveGoMethodInPackage found no such type there,
and resolveMethodOnType picked the first of every
`Controller::AllConfigs`.

The same gap affected calls through parameters, locals and package-level
variables of such a type (one set by a conversion), through an alias of
it, and through methods promoted from an embedded field. It also affected
method values, type references, and the embedding edge itself where the
namesake was as near.

The field chain, the typed-receiver and method-value package lookup,
embedded types, alias targets, package-level values and Go type positions
now ask goBareTypeDir. It reads a bare name as Go does: the file's own
package first, then a project package the file dot-imports that declares
it. An exported name that only a dot import of a package outside the
project's modules can supply (`. "github.com/onsi/gomega"`) names none
of the project's types. The exception is a type that one of the file's
functions declares, which the index holds no node for: name matching
still handles it, as before. A bare embedding is retargeted only when a
dot import decides, and files without dot imports resolve exactly as
before.

Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel
and wasm backends. 18 tests fail on main and 2 guards pass. All
go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test
files pass (65 files, 1,140 tests). tsc and the build are clean.

Validation compares main bf6f1fa with this commit, each built with a
kernel from its own sources. Each repo was indexed from scratch and
diffed by call site:
- gomega 820ad4d5: 30 calls retargeted from internal.Gomega's Expect to
  types.Gomega's, the type the dot-imported root package's `Gomega`
  alias names. All 30 were checked against source.
- ginkgo 7ff96d7d: 135 calls added and 1,022 confirmed (same target,
  0.7 to 0.9), all through package-level vars typed by the dot-imported
  internal/test_helpers. 9 retargeted (8 composite literals to the root
  package's SemVerConstraints aliases, 1 Status call to the fake
  handler). None removed.
- harbor src/ 4e07c69c and kubernetes d2c4b85b: byte-identical dumps.
  Their dot-imported types' methods have no namesakes, so main already
  linked them.
- Controls gin 0f09c3a9 and zustand d7a5583c (TypeScript): byte-identical.
- Index time and DB size are within noise. On kubernetes, the median of
  3 interleaved rounds was 151.5 s on main and 148.7 s with the fix.

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

Root cause: the write that refreshes CodeGraph's marker-fenced section in
an agent's instructions file (CLAUDE.md, AGENTS.md, GEMINI.md) located the
section with two independent searches: the first start marker anywhere in
the file and the first end marker anywhere in the file. Everything between
them was treated as CodeGraph's and replaced. A stray start marker above
the real section (what is left when part of an older section is deleted by
hand, or the marker quoted in the user's own notes) therefore stretched the
"section" over the user's text, and when that marker was the first line
the file came back as just the block, which is the report. The
`codegraph upgrade` refresh (`codegraph install --refresh`) and plain
`codegraph install` both go through this write, for the Claude Code, Codex,
Gemini and opencode targets. The same pairing made a lone end marker look
like "no section", so every run appended another block, and left a second
block stale. Uninstall (and the npm preuninstall hook) and the Cursor
rules-file cleanup used the same pairing to delete the same span.

Fix: one parser finds the well-formed sections. Each end marker closes the
nearest start marker before it, and a marker counts only when it is alone
on its line, as every block codegraph has written puts it. A stray start
or end marker is left as plain text. The refresh rewrites only the bytes
between the first section's markers, using the line breaks that section
already has, so a CRLF file stays CRLF and a current CRLF block is
unchanged. Further sections are duplicates and are removed, keeping the
text around them. With no section yet the block is appended after the
existing bytes, which are no longer trimmed. Uninstall and the Cursor
cleanup strip only well-formed sections.

Validation:
- 18 new tests (17 in installer-targets.test.ts: exact bytes for a stray
  start marker on line 1, a stray start marker with no section after it, a
  marker quoted in prose, a lone end marker, two sections, CRLF, no final
  newline, appending, a BOM, uninstall; a refresh plus uninstall per
  target for Claude Code, Codex, Gemini and opencode; the Cursor rules
  file. 1 in upgrade.test.ts: an npm upgrade whose refresh runs the built
  CLI against a private home). 16 fail on main's sources (the other 2 are
  guards that hold on both), all pass with the fix.
- The real CLI on this Windows machine against private temp homes:
  `codegraph install --refresh`, 14 file shapes x 4 targets x 2 runs. Main
  had 28 bad results of 112 (the user's text lost in 3 shapes, a block
  added on every run after a lone end marker, a stale second block kept),
  the fix 0. With the fix, the bytes outside the section are identical in
  every single-section shape, and the input stays a byte prefix in every
  append.
- `codegraph install --yes` and `codegraph uninstall --yes --keep-cli` on
  the reported shape: main kept none of the notes (install left only the
  802-byte block, uninstall left 19 bytes); the fix kept them all.

Fixes colbymchenry#2506

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rough, not replaced (colbymchenry#2768)

Root cause: every installer write goes through one helper that writes
`<path>.tmp.<pid>` and renames it onto the path. A rename replaces a
symbolic link itself, not the file it points to. So a `~/.codex/AGENTS.md`
or `~/.claude.json` linked from a dotfiles repo became a detached regular
copy on install: the shared file never got CodeGraph's settings, and later
edits to it no longer reached the agent. This held for every target (Claude
Code, Codex, opencode, Cursor, Gemini, Antigravity, Kiro, Hermes and the
Copilot family) and for install, the refresh that `codegraph upgrade` runs,
and uninstall. Uninstall also deleted a link whose file it had emptied,
leaving CodeGraph's entry behind in the shared file.

Fix:
- The write helper resolves a link to the file at the end of its chain and
  writes and renames there, so the link stays exactly as it was, relative
  or absolute.
- A link to a file that doesn't exist yet gets that file created, but only
  in a folder that already exists, as writing through it in a shell would.
  No folder is ever created where a link points.
- A dangling chain is followed hop by hop from the real folder holding each
  link, so a relative link under a linked folder lands where the OS would
  put it.
- When the linked file can't be written (a read-only file, a folder that
  doesn't exist), the error names the link and its target and nothing
  changes.
- Where uninstall used to delete a user's file it had emptied (an
  instructions file, Codex's config.toml, Copilot CLI's mcp-config.json,
  the legacy project .claude.json), a link is kept and the emptied content
  is written through it.
- The refresh sweep catches a failure per agent, reports it, still
  refreshes the others, and exits non-zero, so the upgrade warns.

Decided: links to CodeGraph's own legacy files (Cursor's codegraph.mdc,
Kiro's steering doc), which install only ever deletes, are still removed.

Validation:
- 28 new tests. 25 are in installer-targets.test.ts:
  - every target and location: install and uninstall keep each of its
    files that is a link (18 cases);
  - the shared files' content through install and uninstall for Claude
    Code, Codex, opencode (two agents sharing one AGENTS.md) and Cursor;
  - relative links under a linked folder;
  - a link into a missing folder;
  - a link to a read-only file;
  - uninstall emptying through links;
  - a failing link in a refresh;
  - a folder link guard.
  The other 3:
  - 1 in upgrade.test.ts, where the refresh runs the built CLI;
  - 2 in a new installer-links-simulated.test.ts, which simulates the
    link with spied lstat/realpath/readlink, so it runs on machines that
    can't create file symlinks.
  With main's sources put back, 27 fail; the folder-link guard passes on
  both. All pass with the fix. The real-link tests are skipped, with the
  reason in their names, where file symlinks can't be created (Windows
  without Developer Mode or admin).
- This Windows machine has Developer Mode on, so the real-link tests ran
  for real.
- The real CLI against a private home (`install --yes`, `install
  --refresh`, `uninstall --yes --keep-cli`), 10 linked files for five
  agents plus relative, dangling and read-only links:
  - on main, all 29 link checks found the link replaced by a copy;
  - with the fix, none did, every link's text was unchanged, the shared
    files got CodeGraph's settings and lost them again on uninstall, and
    the missing-folder and read-only cases exited 1 with the new message
    and changed nothing.

Fixes colbymchenry#2721

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

explore-cross-call-dedup's "never re-sends a line it already sent" case
has failed on every LF checkout (Linux, macOS, CI) since PR colbymchenry#2655. It
passed on Windows only because core.autocrlf writes the fixture with CRLF.

Root cause. Since PR colbymchenry#2655 the payroll fixture's RunResult struct
(cycle.go lines 31-37) references Payslip through a field, so call 1
renders it. Call 2 renders the file's head, and that 7-line held run comes
back inside it: dedupeRange keeps a covered run shorter than
MIN_COVERED_LINES (8) in the emit set on purpose, rule 3 of the range
algebra, which its own unit test pins. The handler case counted any
repeated line as a duplicate. On a CRLF checkout explore's per-file
character budget also counts each line's carriage return, so cycle.go
rendered fewer lines and the case never met the short run.

Fix, test only:
- The case groups the repeated lines into consecutive runs and fails on a
  run of MIN_COVERED_LINES lines or more, read from EXPLORE_DEDUP. A
  shorter run is the algebra's deliberate re-send. What agents receive is
  unchanged.
- The suite gives its copy of the fixture LF line endings, so every
  checkout indexes and renders the same bytes.

Validation on Windows, with the kernel and with CODEGRAPH_KERNEL=0:
- main's test fails with an LF fixture ("call 2 re-sent
  internal/usecase/payroll/cycle.go lines 31,32,33,34,35") and passes with
  the CRLF one. The new test passes 26/26 with both.
- With the normalisation and main's assertion, a CRLF checkout fails the
  way an LF one does (lines 31-37): both checkouts now test one scenario.
- Teeth: an algebra mutated to re-send held runs of 8-23 lines fails the
  new case ("call 2 re-sent internal/usecase/payroll/cycle.go lines 42-51
  (10 lines)").
- tsc on the test file is clean.

Contributor PR colbymchenry#2758 by @danusha2345 reached the same diagnosis. This
change was written separately and also normalises the fixture.

Fixes colbymchenry#2756

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndex or sync (colbymchenry#2769)

Indexing or syncing a C++ project stopped at "Resolving refs" with
"Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/:
Nothing to repeat" as soon as its code called an overloaded operator by
name, as `this->operator++();` inside `operator++(int)` does. Choosing a
method's overload reads the argument list after the name, and
cppParenListIn and cppOverloadParameters wrote the name into that
pattern with only `~` escaped. `operator++` did not compile. `operator()`
matched the name's own empty parentheses as the arguments, `operator[]`
never matched, and `operator+=` matched the next `operator=`. The same
helper serves every language that overloads by arity, so Scala `++`
overloads and Kotlin names in backticks that hold a `(` failed the same
way.

The fix adds one escape helper, escapeRegExp in src/utils.ts, the same
body as the private copies elsewhere. It is used at every place in src/
where a node name, a reference name, a piece of one, or a word read
from source goes into a RegExp unchecked: 114 interpolations in 13
files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and
the 2 overload readers only `~`. An AST pass over all 490 dynamic
RegExp constructions followed each interpolated value to its sources.
The 72 it still flags were each read by hand. They are
identifier-checked first, built from escaped names, constants, or the
CLI/MCP glob converters. The kernel's one dynamic regex already
escapes.

With operator arguments now read, nlohmann/json's `p->operator[](0)`
reached the argument-type scorer and picked `template <typename T>
operator[](T* key)`: a `size_type` parameter scored as an unknown
class, a `0` as a null pointer. `size_type` and `difference_type` now
count as numbers, as `size_t` does.

Validation:
- New __tests__/operator-method-names.test.ts (5 tests: init and sync of
  an operator fixture, operator() and operator+= overload choice, the
  json operator[](0) shape, Scala and Kotlin). With main's sources,
  4 fail (3 throw "Nothing to repeat"); the json shape fails only with
  size_type taken out of the number types. All pass on the kernel and
  with CODEGRAPH_KERNEL=0.
- Census with a preload that logs every throwing RegExp: main threw
  only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in
  all 12 repos below.
- A/B on main efff745, graph dumps:
  - fmt, glm, rocksdb, protobuf: byte-identical.
  - json: 9 calls retargeted, all right (`j.at("name")` to the key
    overload, `v.at(i)` to size_type).
  - abseil-cpp: 3 retargeted, all right (operator() self-call to the
    two-argument overload, assign(3, 10) to the count overload).
  - CTRE and folly crash on main; both index now. CTRE: 10 of 12 calls
    retargeted against main's census run are right; 2 single-header
    calls reach the identical copy in include/. folly: 4 of 6 are right;
    2 small_vector assign(int, int) calls link the count overload where
    C++ takes the unconstrained template.
  - Controls gin (Go), bulletproof-react (TS), ktlint (Kotlin, 86
    backtick names with pattern characters) and cats (Scala symbolic
    methods): byte-identical.
- Cost: none measurable. Medians of 3 interleaved init runs: fmt -1.5%,
  json +2.1%, glm +0.2%, abseil -2.5%, gin -2.1%, bulletproof-react
  -0.3%, rocksdb +0.4% (45.2 s vs 45.3 s).
- kernel-parity on fmt + json (cpp/c): 458/669 at parity, 0 diffs,
  both arms.

Fixes colbymchenry#2457
Fixes colbymchenry#2482
Fixes colbymchenry#2636

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

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

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

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

Fixes colbymchenry#2562

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

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

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

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

Fixes colbymchenry#2470

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

Root cause, two parts:

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

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

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

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

Fixes colbymchenry#2485

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

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

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

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

Fixes colbymchenry#2596

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

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

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

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

Fixes colbymchenry#2477

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

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

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

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

Fixes colbymchenry#2545

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

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

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

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

Fixes colbymchenry#2546

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

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

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

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

Fixes colbymchenry#2543

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

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

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

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

Fixes colbymchenry#2486

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

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

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

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

Fixes colbymchenry#2581

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

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

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

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

Fixes colbymchenry#2578

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

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

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

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

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

Fixes colbymchenry#2491

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

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

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

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

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

Fixes colbymchenry#2547

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

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

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

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

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

Fixes colbymchenry#2462

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

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

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

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

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

Fixes colbymchenry#2455

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

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

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

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

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

Fixes colbymchenry#2577

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

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

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

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

Fixes colbymchenry#2404

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

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

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

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

Fixes colbymchenry#1887

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry and others added 25 commits October 23, 2026 13:30
…stored (colbymchenry#2845)

CodeGraph.indexFiles returned the orchestrator's indexing promise from
inside the try whose finally releases the cross-process write lock. A
returned promise leaves the try at once, so the finally ran as soon as
indexing started, and the lock was gone while the files were still being
extracted and stored. Another process's sync or index of the same
project, such as a git hook, the CLI or an MCP server, could then write to
the index at the same time. indexAll and sync await inside their try and
hold the lock to the end; the in-process mutex already covered the whole
call.

Fix: await the indexing inside the try, so the lock is let go only once
the files are stored, or when indexing fails.

Validation:
- New __tests__/index-files-write-lock.test.ts (2 cases). A second handle
  on the project stands in for another process, and the real indexing
  waits at a gate the test opens. While indexFiles runs, the other
  handle's sync must be refused with LockUnavailableError. With main's
  src/index.ts it ran to completion instead (red); with the fix it is
  refused, and admitted once the files are stored (green). When indexing
  fails, the lock is let go (passes on both).
- The lock and sync test files pass alongside: sync.test.ts,
  cli-sync.test.ts, cli-unlock.test.ts, orphaned-refs-sweep.test.ts
  and sync-rebuild-convergence.test.ts, the last two of which call
  indexFiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ethod calls in it (colbymchenry#2846)

A Rust method call made through more than one field, or on what another
method returns, was recorded by its bare method name in both extractors:
self.services.store.put("a") became `put`, as did self.store().put("m").
Only a single field kept its receiver (self.store.put). The edge's
refName then said nothing of which value the call goes through, and a
call on a method call's value was never typed, so self.store().put("m")
stayed unresolved. PR colbymchenry#2831 had already made the field-chain calls link
by reading the receiver from the source text, but their names stayed
bare.

Both extractors now keep a receiver that is a chain of fields and method
calls on `self` or on a name, a call written `name()` with its arguments
and turbofish dropped, up to 16 links: self.services.store.put,
self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that
already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are
unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer
chain rooted at a path keep the bare name. The kernel's receiver_chain
mirrors the wasm extractor's rustReceiverChain byte for byte.

Resolution reads such a call as the bare-named call was read before
(rustChainedCallByName, at the top of resolveOneIn), so the frameworks,
the strategies and the scope gates see what they saw, and the edge keeps
the call as written. The chain goes along: the written-receiver hook
reads the receiver the chain names where the call is written
(rustChainedReceiver), so a method call's value is typed from that
method's signature, and a later link that calls the same name again is
told apart instead of being left to a guess by the name.

Validation:
- New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's
  workspace verbatim plus self.store().put("m"), chains whose links call
  the same name through self, a tuple field and a parameter, a call on a
  method's std return value, the recorded shapes and the 16-link cap,
  and a sync that adds the missing method. Red on main's sources, 7 of
  10 failing on both the wasm and the kernel path; green with the fix.
  The Rust kernel parity suite gains a chain case. The extraction test
  that pinned the old collapse now pins the chains.
- A/B against main 865ddf4 (same groups against d833827), both arms
  with the native kernel, every changed call edge read. Per repo: edges
  that only gained their receiver in refName / same target now by type /
  wrong removed / right added / wrong to right:
  ripgrep 3fce3b5b: 159 / 45 / 6 / 6 / 15
  axum 86aa6153: 115 / 25 / 0 / 14 / 7
  tokio 8a2408ed: 1039 / 124 / 36 / 29 / 27, and 1 right link lost
  lemmy 92e077b5: 190 / 24 / 3 / 1 / 3, 1 right lost, 1 wrong added
  The two lost links and the wrong one follow rules the typed-receiver
  reading already applies to typed locals on main: a function-local glob
  `use` does not shadow a file-level named `use` (tokio), a call on a
  dependency's type reaches only impls of project traits, and `x.into()`
  takes the type's explicit `impl Into` (lemmy). Controls gin and
  express: byte-identical dumps. Nodes are identical on every repo.
- Kernel parity (Rust), ripgrep, axum, tokio, lemmy: 1752/1752 files at
  byte parity, 0 deferred.
- Index cost, interleaved median of 3 (wall): ripgrep 3.15 s -> 3.15 s,
  axum 3.39 s -> 3.42 s, tokio 10.48 s -> 10.54 s, lemmy 5.14 s ->
  5.15 s; CPU within noise.

Fixes colbymchenry#2836

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and named options

The static guard in child-process-windows-hide.test.ts, which checks that
every spawn under src/ sets windowsHide: true, matched the key by its
source text. A quoted key was a different property to it, so
{ windowsHide: true, 'windowsHide': false } passed as hidden and
{ 'windowsHide': true } was flagged. A computed key written after
windowsHide: true, like [key]: false, was skipped, so it passed too. And
(file, options) with the args array omitted was recognized only when the
options were an object literal, so execFileSync('git', opts) was read as
passing no options and flagged.

A quoted key is now the same property, a computed key makes the options
unprovable from that point on, and with exactly two arguments the second
one is what gets checked, so an args array there is still an offender.

Validation: with main's guard, exactly the 4 new table rows fail and the
other 29 pass; with this change all 33 pass, including the scan of src/,
which flags nothing on either side. Test-only, so no CHANGELOG entry.

Original PR colbymchenry#2371 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CODEGRAPH_KERNEL=0 sends every language through wasm, and running the
suite with it is how the wasm path is exercised on a machine that has the
kernel built. Three kernel-only blocks failed in that run instead of
skipping, because they skipped only when the .node file was missing:
kernel-deep-nesting asserts in beforeAll that some language is routed to
the kernel, kernel-retry-materialize asks for a raw kernel result and gets
null, and the C++ brace-scope kernel parity block in cpp-brace-scopes
gets no kernel result either.

Each of the three describe.skipIf conditions now also skips when
CODEGRAPH_KERNEL is '0', the same outcome as a checkout without a kernel.
The kernel-independent self-heal block in kernel-retry-materialize still
runs.

Validation, the three files: on main with CODEGRAPH_KERNEL=0, 5 tests
fail and kernel-deep-nesting fails at beforeAll; here, 13 pass and 13
skip. With the kernel (CODEGRAPH_KERNEL_EXPECT=1), 26 of 26 pass, nothing
newly skipped. All 75 test files that reference the kernel pass with
CODEGRAPH_KERNEL=0 on this commit (1,832 passed, 0 failed), so these were
the only kernel-only blocks that failed that way. Test-only, so no
CHANGELOG entry.

Original PR colbymchenry#2372 by @danusha2345.

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

The routing manifest has a row per (route, symbol it reaches), and the
routes list behind Entry points and the Steps picker folds those rows to
one per route. To have rows to fold, it fetched limit * 4 + 1 of them,
so an inline handler, which has a row per call in its body, could use
up the window by itself, and the routes after it were never fetched. On
an Express app with one 24-call inline handler before two routes,
limit=3 listed only GET /dense.

getRoutingManifest takes a perRoute flag that applies its limit to
routes, in the same file and line order, and returns every row of those
routes. Without the flag the query and its result are unchanged.

Added on top of the contributor's change: the window is still
over-fetched, four routes per route shown, because some routes are
dropped only after their rows come back (a test or generated handler,
an inline route written in a test). A window of exactly limit + 1
routes could hold only those. On hono, whose routes are nearly all
registered in test files, Entry points then listed 0 routes instead of
main's 28, and the routes list 21 instead of 32. With the over-fetch,
every route main lists is still listed at each limit tried (3, 12, 60,
200 and 300), and hono's Entry points list 32. truncated also keeps
main's check that the window came back full.

Validation: the new limit=3 test fails on main (only GET /dense) and
passes here. A project whose test-file routes sort first fails with the
PR as first written (it reads as unrouted) and passes here and on main.
Routes listed on Wiki.js (132 routes) and realworld-express are
identical to main, and on hono they differ only by the additions above.
The ui suites and the other route consumers: 34 files, 723 passed, 0
failed, 3 skipped (environment-gated). The changelog line is in
docs/viewer-launch-changelog.md, since the viewer is not released yet.

Original PR colbymchenry#2370 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHP reads namespace names without regard to case, so after
use App\Fields as Field; the name field\FirstName is the same class as
Field\FirstName. The qualified-name resolver compared the alias as
written, found no import, and read the name relative to the file's own
namespace instead, so new field\FirstName() stayed unlinked. The alias
is now matched without regard to case.

This lands the first half of the contributor's PR. Its second half
refused an alias that two namespace { } blocks of one file bind to
different namespaces. That also dropped the first block's correct edge,
because the use map is file-wide, and the case doesn't occur in real
code: across 8 PHP repos (33,820 files), 185 files declare more than
one namespace, and none binds one alias twice outside strings. The fix
for those files is to scope each use statement to its namespace block,
which needs block positions the index doesn't keep yet: a braced
namespace gets no namespace node, and neither does a second unbraced
one, so it is left for its own change.

Validation: the new test fails on main and passes here, with the
native kernel and with CODEGRAPH_KERNEL=0. Graphs of php-parser,
composer, psalm, monica, laravel and phpunit are byte-identical to
main, as are the controls express and flask. Index CPU on laravel,
median of 3 interleaved: 53.3 s on main, 52.9 s here. The PHP and
Laravel suites pass in both engines (10 files, 67 tests).

Original PR colbymchenry#2369 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s type

A TypeScript or JavaScript method called on an element of a class field,
like this.handlers[type].handle() or this.items[0].save(), kept the bare
method name: a member chain rooted at this collapses to it so the resolver
can read the owner off the enclosing class. A subscript in the chain leaves
the resolver nothing to read, so the bare name exact-matched any project
method of that name, often another class's (this.listeners[type].push(h)
reached an unrelated UndoStack.push).

Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and
this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain,
or under window, emits nothing, as arr[0].run() already did. The resolver
reads the element type the field's declaration writes (X[], Array<X>,
ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and
resolves the method on it or leaves the call unresolved: a type parameter
of the declaring class, a union with another type, an array of arrays or a
non-plain element hold no one type. The class type-parameter reader now
reads from the keyword to the list's own >, over several lines and through
braces in constraints.

Our changes: kept escapeRegExp from PR colbymchenry#2769 in the conflict; the
type-parameter reader also accepts the type keyword, since the T['p'] path
passes type-alias owners and the PR as written read State<Item>['current']
as the project class Item (new case in ts-array-annotated-receivers);
CHANGELOG covers JavaScript and asks for a re-index.

Validation: the element tests fail 10 of 11 on main with both engines; the
type-alias case fails on the PR as written and its brace half on main. A/B
against main, kernel built per arm: flask and gin byte-identical. TS:
pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9
wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a
merged mixin member, an index-signature type, an any field); typeorm 3
wrong removed, 1 right lost, 2 retargeted to the declared interface;
excalidraw 1 wrong removed; vscode's base and editor 19 added, 24
retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by
name: a getter, an index-signature field, deep chains, a T[] parameter, a
union alias). JS: three.js, playcanvas and pdf.js
lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM
calls on project classes). Kernel parity holds on every repo with
deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s,
three.js 64.7 s to 67.9 s, medians of 3 interleaved).

Original PR colbymchenry#2766 by @inth3shadows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot the standard library

A Go reference written through a package qualifier was dropped as a
standard-library reference whenever the qualifier matched a name in
the built-in list (errors, log, types, parser, encoding, template, ...).
The check looked at the name alone, never at what the file imports
under that name, so a project with its own package of such a name lost
nearly every call into it: harbor's errors.New(...) through its
lib/errors, prometheus's parser.Inspect(...) through promql/parser.

What the file imports now decides before the name does. A qualifier
the file imports, by its last path segment, the name goimports assumes
for an unaliased import, or an alias, from a package of one of the
project's own modules is a project package, and the reference goes on
to normal resolution. A file that imports the standard library's
package of that name, and a qualifier the file doesn't import at all,
are skipped as before.

Validation: the new test file has 7 cases, 4 of which fail on main;
all pass here, with the native kernel and with CODEGRAPH_KERNEL=0.
A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889
edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both,
nodes unchanged. Every added edge was checked against its source: the
line spells qualifier.Name and the file imports the target's package
directory under that qualifier from inside the module (3,343 of
3,343), and a sample of 10 was read by eye. gin and cobra, and the
controls express and flask, are byte-identical. Index CPU on harbor,
median of 3 interleaved: 40.0 s on main, 39.9 s here.

Original PR colbymchenry#2368 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.

Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.

Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR colbymchenry#2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR colbymchenry#2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.

Validation: the new tests fail 4 of 6 on main 449eefa with both engines
and pass here. A/B against main, kernel per arm: flask, gin and vite
byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2
retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and
test, Array fill, Set add) and 1 right one lost to the grammar's parse of
new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder,
TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted
and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted
and 68 typed, all right, 76 wrong removed (Array keys and fill, typed
arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to
Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy)
and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which
three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right
only by name (Loader methods inherited through a base imported from the
three package). No default-imported class under another name lost an edge.
Kernel parity holds on all six TS/JS repos and this repository, 0 diffs.
Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js
72.0 s to 71.2 s (medians of 3, interleaved).

Original PR colbymchenry#1945 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from a package

When a JavaScript or TypeScript file imports a name from outside the
repository, like import { resolve } from 'node:path', matchByExactName
returns no edge for a bare call of that name (the early return main added
for issue 2101). A call to a function declared inside the calling function
under that name, which shadows the import, lost its edge with it.

The candidates for such a call are now narrowed to the calling file's
declarations nested in a function or method (innerScopeShadows), instead
of giving up; a top-level one is the import's own binding. Lexical
reachability, applied later, keeps only the nested declarations visible
where the call is.

Our changes: tests for a sibling function's nested declaration and one
nested a level below the call, which neither may take the call (both hold
through the reachability rule), and for a call from a function nested in
the declaring one; the CHANGELOG bullet moved next to the JS/TS import
bullets, with credit.

Validation: the tests fail 2 of 6 on main and pass here. A/B against main
on 8 repos, kernel per arm: flask, gin, excalidraw, codegraph, pixijs,
pdf.js and three.js byte-identical; vite gains 1 right link, scanImports'
call to its nested scan() beside import { scan } from
'rolldown/experimental'. Index CPU
time on codegraph, test files full of vitest imports, within noise:
81.7 s to 81.9 s (median of 3, interleaved).

Original PR colbymchenry#1715 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts Store<String>

A Java class implementing a parameterized interface (implements
Store<String>), one extending a generic base (extends Base<Integer>), a
Java interface's extends Store<T>, and a TypeScript class's implements
Store<string> got no implements or extends edge. The grammars wrap the
type in a generic_type node, and the supertype reader took its whole
text, which no node is named. Without the edge the interface-impl
synthesizer had nothing to walk, so a call on the interface stopped there.

Both engines (extractInheritance on the wasm path, the kernel's java.rs
and tsjs) now name a generic_type by its first child that is not
type_arguments; any other supertype node is its own text, as before.

Validation: the new tests fail 3 of 3 on main with both engines and pass
here. A/B against main 0d46dba, kernel per arm: flask and Slim (PHP)
byte-identical. mybatis-3 gains 107 supertype edges (extends
BaseTypeHandler<T> 58 times, TypeHandler<T>, ResultHandler<T>,
BaseMapper<T>...) with 31 interface-impl and inherited calls, 2 calls
retargeted from another package's Mapper to the BaseMapper the interface
extends, and 17 inherited calls typed. pixijs gains 43 implements
(System<T>, RenderPipe<T>, InstructionPipe<T>...) and 11 interface-impl
calls; excalidraw 2 implements DeltaContainer<T> and 4 interface-impl
calls; vscode's base and editor 142 implements (IListRenderer<T>,
IListVirtualDelegate<T>, ISpliceable<T>...) and 183 interface-impl calls.
Nothing removed. Kernel parity holds on mybatis-3 (1,396 Java files),
pixijs, excalidraw and vscode's base and editor, 0 diffs. Index CPU time
within noise: mybatis-3 29.9 s to 30.1 s, pixijs 37.3 s to 38.1 s over 6
interleaved rounds in both orders (spread 35.8 to 38.5 s).

The older PR colbymchenry#729 by @paiyahooJay made the same fix in the wasm extractor;
it is credited in the CHANGELOG.

Original PR colbymchenry#2705 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Go method was indexed as unexported, whatever the case of its
name, in both engines: extractMethod never set isExported, and the
kernel's go.rs mirrored the omission. Plain functions carried the flag,
so dead-code results listed exported methods like Close as dead.

A method that is a top-level declaration (methodsAreTopLevel, only Go)
now gets the extractor's isExported, as a function does, and go.rs sets
is_exported from the same name-case rule in extract_method.

Added on top of the contributor's change: findGoPackageMember, which
resolves pkg.Name through a project import, matched any exported node
of that name in the package's directory, and methods stayed out only
because none was exported. With the flag set, a package-level call
like harbor's log.Errorf(...) went to the method Logger::Errorf
declared beside the function, and prometheus's embedded
storage.Appender resolved to a method named Appender, which dropped
its implements edges. A method is no member of its package, so it is
now skipped there. Two comments in name-matcher.ts that said the flag
is unset on every Go method now say why the name's case is still read.

Validation: the contributor's test fails on main and passes here, and
the new package-call test fails with the PR as first written and
passes here and on main, in both engines. A/B against main on harbor
296f304, prometheus a2fda0e, gin 0f09c3a and cobra adbc881: nodes
differ only in is_exported 0 -> 1 on uppercase Go methods (5,704,
3,880, 391 and 141). Edges are identical on prometheus and cobra; 22
retargets remain, all name-only guesses between an interface's method
declaration and the struct method implementing it, which now win: gin's
engine().GET and its siblings reach RouterGroup's methods, and harbor's
jobservice routes reach DefaultHandler's. The PR as first written had
retargeted 1,384 harbor calls and removed 60 prometheus implements and
dispatch sites. The dead-code report lists 3 Go symbols on harbor
instead of 167, and 0 on prometheus instead of 11. Kernel parity: 2,493
of 2,493 Go files byte-identical. The controls express and flask are
byte-identical. Index CPU on harbor, median of 5 interleaved: 40.8 s on
main, 39.8 s here. EXTRACTION_VERSION stays: main already moved it to
28 after 1.6.2, so 1.6.2 indexes get the re-index hint anyway.

Original PR colbymchenry#1734 by @danusha2345.

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

Since PR colbymchenry#1615 made arity part of an Erlang function's identity, arity
is counted from the argument list's named children. tree-sitter reports
a comment as a named child, so a comment at the top level of a
parameter or argument list raised the count by one. A call written
plain( % note followed by two arguments was looked up as plain/3 and
linked to nothing, a definition with a comment between its parameters
was indexed one arity too high, and the MFA list of spawn(M, f, [A, B])
with a comment between A and B gave f/3. A comment nested deeper,
inside a tuple, a record or a list argument, was never affected.

The three counts now go through one namedArgCount helper that leaves
comments out: a clause's and a spec's arity, a call site's arity, and
the MFA list's.

Conflict resolution: the import block of tree-sitter.ts moved on main
since the PR; main's imports are kept, with namedArgCount added.

Validation: the new test fails on main (its fixture gets no call edge
at all) and passes here. A/B against main: rebar3 97e9367 gains 2 edges
(rebar_compiler.erl:403 and rebar_release_SUITE.erl:247, both calls
whose argument lists hold a comment, both checked against the source),
0 removed, nodes unchanged; cowboy 6475600 and ranch 7ec436d are
byte-identical, as are the controls express and flask. Erlang has no
kernel extractor, so there is no parity to run. Index CPU on rebar3,
median of 3 interleaved: 9.9 s on main, 9.3 s here.

Original PR colbymchenry#1619 by @Dshuishui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r with its name (colbymchenry#2858)

The scope matcher that the indexer, sync and the file watcher share also
takes in the folders `git ls-files -o -i --exclude-standard --directory`
reports as ignored (PR colbymchenry#1754, for issue colbymchenry#1728). They were added to it as
gitignore patterns. git reports them relative to the root, but a pattern of
one segment, like `src/`, has no anchor in gitignore syntax and matches at
any depth. So an ignored root `src/`, holding only a gitignored jib agent,
hid every module's `src/` in a Maven monorepo, and the index came up empty.
As patterns, folder names were also read as syntax: `[draft]` as a
character class, `a[b` as a regex that throws on the first match (the git
listing then fell back to a filesystem walk, and the watcher threw on every
event), `#tmp` as a comment and `!keep` as a negation.

The reported folders now stay root-relative paths in a set, and a path is
out of scope when it is one of them or lies inside one. Nothing is parsed,
so there is nothing to anchor or escape. Escaping was not an option: in
ignore 7.0.5 an escaped `?` never matches a literal `?`, an escaped
backslash before a `.` turns the dot into a wildcard, and every match is
case-insensitive. git prints these paths relative to its cwd, so for a
project in a subfolder of a repo they are relative to that subfolder, the
root the matcher is asked about.

The CLI hint for gitignored nested repos wrote their paths into
codegraph.json `includeIgnored` the same unanchored way, so an opt-in for
`child/` also took in any later `archive/child/`. It now writes a
root-anchored pattern with `*` and `[` escaped.

Validation:
- New git-ignored-dirs-scope tests (9): all fail on main and pass with the
  fix. They cover the issue's layout (scan, index, scoped sync), the lib/,
  build/ and dist/ variants, a `[draft]` folder, the watcher with a
  `scratch[1` folder, a project in a subfolder of its repo with `#tmp` and
  `!keep` folders, and the CLI hint end to end.
- A/B with the kernel in both arms, on real repos after writing the root
  folders a local build writes, against git's own view (ls-files --cached
  --others --exclude-standard, less codegraph's defaults). Files indexed of
  those in scope, main then fix: stencil 1015 and 1707 of 1707, Sentinel
  with the reporter's jib layout 104 and 1423 of 1423, ant-design 2851 and
  3138 of 3138, wger 767 and 770 of 770. The fix misses no file in scope
  and adds none outside it.
- For Sentinel, ant-design and wger, the fix's graph on the built tree is
  byte-identical to main's on a clean checkout. ant-design's 53 changed
  call sites move `useLocale` from the docs site's hook to
  components/locale/useLocale.ts. In stencil, every edge the fix adds on
  the built tree is also in main's graph on a clean checkout, and the 62
  name matches it no longer makes are unresolved there as well.
- Controls gin and flask: byte-identical graphs.
- Index time, median of 3 interleaved: gin 2572 and 2545 ms, flask 2400
  and 2378 ms; the repos above take longer only by their restored files.
  The scope step with 486 and 599 reported entries (a `.DS_Store` in
  every folder): main 206 and 416 ms, fix 105 and 155 ms.

Fixes colbymchenry#2849

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

getTypeHierarchy() walked a type's ancestors and then its descendants
with one visited set. The ancestor walk marks the focal node first, so
the descendant walk returned at its visited guard before reading a
single incoming edge: no type ever had subtypes or implementors, in any
language. Each direction now keeps its own visited set.

The same walk feeds the hierarchy step of findRelevantContext, which
codegraph_explore and codegraph context build on. That step promised
subclasses within a budget of a quarter of maxNodes, but checked the
budget only between entry points, which was harmless while the walk
returned ancestors alone. With descendants coming back, one widely
implemented interface brought its whole tree: vscode's IDisposable, in
src/vs/base and src/vs/editor, has 613 types in its hierarchy, and
codegraph context IDisposable then traded the interface's own dispose
for an arbitrary implementor. Added on top of the contributor's fix:
the budget now holds per node, and only edges between kept nodes are
added.

Validation: graph.test.ts gains a test that getTypeHierarchy() returns
a base class's subclass and an interface's implementor; it fails on
main. context.test.ts gains one that the hierarchy step adds an
interface's implementors within its budget (12 nodes, no traversal:
1 to 3); main gives 0, and the traversal fix without the cap gives 10.
On vscode's base and editor subtree, codegraph context and
codegraph_explore keep main's size and speed (0.8 to 1.3 s), and the
hierarchy they show is now the queried type's own: StandaloneCodeEditor
and EmbeddedCodeEditorWidget for "ICodeEditor CodeEditorWidget". On
prometheus, 3 of 4 explore answers are identical, and the fourth names
more of storage.Appender's implementors. The contributor's codegraph
implementations command and its test-code helper are not part of this
change. This is the getTypeHierarchy() half of issue colbymchenry#2833.

Original PR colbymchenry#2839 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mports, not a namesake (colbymchenry#2864)

PR colbymchenry#2854 sends Java's and C#'s no-argument new Foo().bar() calls, which
the chain conformance pass retries, to the constructed-receiver resolver.
That links them (mybatis-3 +265, Newtonsoft.Json, serilog and jellyfin
+36, all right), but the hint for which Foo it passed was a file path,
which the resolver reads for these languages as a dotted package name,
and C#'s using names namespaces, not classes. So of two classes named
Foo the first indexed one won, whatever the file imported: import b.Foo;
new Foo().bar() linked a.Foo::bar at 0.9, and so did a call the language
itself cannot resolve, after import a.*; import b.*;.

When several project classes share the name, Java and C# now pick as
the language does: Java the written package (new a.Foo()), the file's
single-type import, else the caller's own package; C# the one type the
file's namespaces and usings can see. Only that class's own method links,
otherwise nothing. A name only one class has resolves as before,
inherited methods included.

Validation: the new test fails 4 of 4 on main 44adef8 with the kernel
and with CODEGRAPH_KERNEL=0 (import b.Foo linked a.Foo::bar, and the
ambiguous calls linked a.Foo and A.Foo), and passes here with both;
new-expression-receiver and kernel-tsjs-parity pass. A/B against main,
kernel per arm: flask, mybatis-3, Newtonsoft.Json, serilog and jellyfin
byte-identical, so the 301 links PR colbymchenry#2854 added there stay, mybatis-3's
2 calls on a class with a same-named copy in another package among them,
which resolve to the caller's own package as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uess no target

A member call on a nested receiver, like holder?.values.get(readKey()),
has failed both ways. Before PR colbymchenry#1790, each such call was linked by its
bare method name, to any project method called get. PR colbymchenry#1790 stopped
that by dropping every call on a chain rooted at a name, which also
dropped the call sites effects are drawn from (issue colbymchenry#1794). PR colbymchenry#1862
brought the dotted chains back as unresolved references; shapes its
chain pattern rejects, like holder[0].get(), are still dropped.

The test indexes eight receiver shapes in .ts, .tsx, .js and .jsx:
optional access, computed and numeric keys, a quoted key, a comment
inside the receiver, a non-ASCII name, an imported root and a namespace
import. It checks that the readKey() argument call survives, that
nothing links to Collision.get or to the imported object, and that the
three dotted chains (holder.values.get, imported.values.get and
data.holder.values.get) are still in the index, unresolved.

That last check is ours. As first written, the test passed on PR
1790's commit de5adba, so it could not see the drop issue 1794 is
about. With it, the test fails 4 of 4 on de5adba (the chains are
gone) and 4 of 4 on its parent 72c1ff1 (every shape linked
Collision::get), with the kernel and with CODEGRAPH_KERNEL=0, and
passes 4 of 4 on main d02a88d with both. On main's code, putting the
drop back fails it, as it fails release-main-regressions' retention
test; letting the shapes the chain pattern rejects fall back to the
bare method name fails only this test. The non-ASCII name is written
with U+00F6 and U+00E9, Latin-1 letters, not homoglyphs. Test-only, so
no CHANGELOG entry.

Original PR colbymchenry#1844 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ail file is a link

validatePathWithinRoot returned the lexical path whenever the path did
not exist yet (ENOENT). With .codegraph/ui a link to a folder outside
the project and no trails folder yet, the trails folder passed the
check, mkdir followed the link, and the trail was written outside the
project. The save still answered 403, but only because the list it
sends back re-checked the folder that now existed. The trails list also
joined each file name onto the checked folder and read it with statSync
and readFileSync, which follow links, so a trail file linking to a
trail-shaped file outside the project was listed and served.

validatePathWithinRoot now judges a missing path by its nearest
existing ancestor, and returns null when the root itself is missing.
The list reads every file through the same per-file check as a single
read, opens it with O_NOFOLLOW | O_NONBLOCK where the platform has
them, and checks the open file with fstat. Windows has neither flag;
there the per-file check is what refuses a planted link. A save
re-checks the folder after mkdir and writes its temp file under a
random name with flag wx and mode 0600. The trail author is cached per
project instead of per process, and a trail file that declares an
unknown format version is skipped.

Added on top of the contributor's change: the link tests also run on
Windows (a junction and a directory symlink at .codegraph/ui, a file
symlink in the trails folder), each test removes its own links before
deleting its folder, the helper gets a junction case and three
controls (an in-root link above a missing leaf, allowSymlinkEscape
above a missing leaf, a missing root), its doc comment names the new
missing-path rules, and the viewer changelog lines are reworded and
moved next to the other saved-trails line.

Validation: 8 tests fail on main and pass here (the helper below a
symlink and below a junction, a missing root, a save below a junction
and below a symlink, a linked trail file, the per-project author, the
format version). Through the real CLI (codegraph ui), main wrote the
trail outside the project through a junction and through a directory
symlink, and listed a linked outside file with its note; this branch
refused all three and wrote nothing outside. Graph dumps of express,
zod, excalidraw, flask and gin are byte-identical to main's, and 55 CLI
read commands and 45 MCP tool calls answer the same (codegraph_status
differs only by its last-indexed time). Index time is unchanged (gin
over 9 interleaved rounds: 2645 ms on main, 2687 ms here). Listing 200
saved trails, the list's cap, costs 108 ms on main and 236 ms here on
Windows; 10 trails cost 6 ms and 14 ms.

Original PR colbymchenry#1667 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and accessors

Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.

The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.

An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.

Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.

Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
  main; the sealed-permits and name-only-guess cases are each red without
  their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
  sweeps at full parity with deferrals unchanged (jextract 218/218,
  spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
  spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main d02a88d, every bucket triaged by sampling (edge sites
  added/removed): jextract +207/-1, spring-modulith +375/-1, playframework
  +80/-2, Minestom +17,963/-287, spring-ai +8,806/-471, kestra
  +10,077/-497; most retargets are a constructor function becoming its
  record. The accessor rule drops only name-only results (spring-ai 1,063,
  kestra 765, Minestom 408 sites at 0.65-0.8 confidence); no typed call,
  method reference or qualified call moves. click and gin byte-identical.
- Cost (process CPU, median of 3 interleaved indexes): Minestom
  47.2s -> 50.3s (+6.6%, for 17% more nodes and 23% more edges),
  spring-ai 93.5s -> 94.8s (+1.4%).

Original PR colbymchenry#1946 by @danusha2345.

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

The handler keeps one idle timer, armed for the oldest open project a
trim can release, and it skips a project whose catch-up is still
running. scheduleIdleRelease returned early while a timer was armed.
So when a project's catch-up settled after a newer project had armed
the timer, the trim found the older project not yet idle and kept the
newer one's deadline. The older project stayed open, writer lock
included, until then: up to nearly twice the idle timeout. With the
10-minute default, a project last used at 0:00 whose catch-up settles
at 9:50, while another was used at 8:20, went at 18:20, not 10:00.

Each trim that gets as far as scheduling now replaces the timer,
re-armed for the oldest releasable project. Trims skip scheduling
while calls are in flight, and the last call to finish trims again, so
a settled catch-up still gets its own deadline. There is still one
timer and no polling.

Validation: the new fake-timer test drives that order through
execute(). On main a565f21 nothing is released at 3.6 s (expected []
to deeply equal [ 'a' ]); with the fix A goes at 3.5 s and B at 4.5 s.
Its afterEach restores both CODEGRAPH_* variables the test sets, also
when the test fails: checked with the test failing on main's code,
with the variables preset and with them unset.

Follow-up to issue 2087, whose idle release (PR colbymchenry#2283) shipped in 1.6.2.

Original PR colbymchenry#2306 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codegraph_explore's maxFiles parameter said "(default: 12)" and its
schema carried default: 12, but the handler picks 4, 5 or 8 files by
project size (clamped to 1 to 20), and nothing applies 12. A client that
fills in schema defaults sent maxFiles 12 and overrode the size-based
default. The description now gives the real default and says total
output is also capped in characters by project size, and the schema
default is gone. The query description no longer tells the agent it
needs no prior codegraph_search, a tool not on the default list.

explore-output-budget's small-tier test asserted that the output does
not contain "Complete source code is included above", text the
completeness note no longer has, so it could not fail. On this fixture
the note reads "Complete source for 1 file is included above", which
the PR's replacement ("files is included above") misses too. It now
checks "is included above", and the medium-tier test checks the note is
there, so a rewording can't leave the absence check behind again. With
the small tier's completeness note switched on, main's assertion and
the PR's both still pass, and this one fails. The interface comment
names the note by its current wording.

Left out of PR colbymchenry#1972: its rewrite of the server instructions' "Trust
codegraph's results" line, which is agent steering and needs an A/B
first; and its codegraph_node hint and footer changes, which main
already has (PRs colbymchenry#2783, colbymchenry#2077).

Original PR colbymchenry#1972 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matchByExactName scored a winner among several same-named candidates by
directory proximity alone: 0.7 when the files share two folders, else
0.4. findBestMatch already ranks the definition in the reference's own
file first, so the target was right, but a shallow file's own function
read as an uncertain guess (below 0.6), and because the resolver keeps
the most confident strategy, the same-file definition lost to a
framework's naming guess in another file (ASP.NET's PascalCase and
Service rules at 0.7 and 0.85).

isDecisiveSameFileMatch now scores it 0.9, as a lone candidate, when it
is the only candidate in the file, is not the caller itself, and, as a
member of a type, module or namespace, belongs to one the caller is in.
The PR checked that last rule for methods only. We extended it to every
member kind whose owner the file declares, other than a function, so a
sibling enum's entry or a sibling class's field is not raised.

Validation, main vs this commit, 21 repos in 11 languages (flask, click,
fastapi, gin, cobra, etcd, ripgrep, jq, Slim, serilog, eShopOnWeb,
nowinandroid, okhttp, http4s, playframework, spring-modulith, express,
zod, bulletproof-react, excalidraw, vben):
- no edge added or removed;
- 9 targets moved, all in eShopOnWeb and all right: C# properties and
  fields named after their type that the code reads (Color Color), which
  ASP.NET's naming rule had linked to the class in another project;
- 6,927 edges keep their target and gain confidence, 535 of them from
  0.4; in 874 a framework rule's 0.7-0.85 now loses to the same target by
  exact name; 30 end in another file, Go type positions the type gate
  moves to the package's own type;
- as written, the PR also raised 77 edges onto another type's member to
  0.9, about 48 of them wrong (okhttp's InterceptorOverridesTest enum
  entries, playframework's sibling providers' `get`); they keep main's
  confidence.
Index CPU, median of interleaved runs: okhttp 32.3 vs 32.4 s, etcd 33.4
vs 33.1 s. Tests: 3 of 7 fail on main; the PR as written fails the
sibling-member case. js-class-expressions pinned the old 0.4 for
`this.run()` reaching the class's own `run` in a shallow file; it now
expects 0.9.

Original PR colbymchenry#1938 by @danusha2345.

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

The Play resolver claims every Class.method reference in a Play project, in
routes files and in ordinary Java and Scala code alike, and took the first
method of that name anywhere in the class's file. In a file holding several
classes (Java nested classes, a Scala class with its companion and
siblings) that was usually another class's method: MediaType.parse(...)
went to Cookie.parse, a route controllers.Application.index to a nested
Helper.index, and Scala RequestAuthority.parse(...) to AuthorityPort.parse.

A method now counts only when its qualified name is the named type's own,
cls::method. A Scala object is an owner too, so a companion's own members
are found through it, and for a call in code so are a Java interface's and
enum's static methods. For a call written in code, the types of the
caller's language are tried first: Play declares RequestAuthority both as a
Scala case class and as a Java record, and each language's callers mean
their own; the other language's types still follow, since Play's Java API
calls its Scala classes. Among same-named types the caller's own file comes
first (Scala names carry no package), an overloaded member gives the
overload the call's arguments fit, and a Java file's import of the name
from outside the project leaves the call to the generic resolver. A route
keeps the plain order and takes a class or object. An inherited member is
left to the generic resolver, which finds it through declared supertypes.

Validation:
- frameworks-integration.test.ts: the PR's case (nested Http.Cookie /
  MediaType, Scala sibling objects, a route beside a nested Helper.index),
  red on main; ours, Scala and Java callers of a RequestAuthority declared
  in both languages (red on main and with the PR's own resolver) and an
  inherited route handler; and a guard for overloads, same-named objects
  in two packages and a Java interface's static method.
- A/B against main cde4cd7: playframework +113 / -257, 510 retargeted;
  CMAK 47 retargeted, all first-in-file picks corrected; play-samples -2;
  click and gin identical. The 50 Scala parse calls that records sent to
  the Java record reach their Scala type again.
- Cost: playframework process CPU, median of 3 interleaved fresh indexes,
  63.3 s on main and 62.8 s here.

Known costs, left as follow-ups:
- 177 right edges main reached only through its first-in-file rule are
  lost: 171 Assets.resourceNameAt calls in AssetsSpec through a local
  val Assets = new AssetsBuilder(...), which the generic resolver drops
  because it reads Scala's private[controllers] as file-private, and 6
  Writeable.writeableOf_* calls inherited by object Writeable extends
  DefaultWriteables, an object's supertypes the generic resolver does
  not follow.
- 8 calls in Scala files that import Play's Java EventSource, Comet,
  Environment or (in a nested scope) Accumulator reach the same-named
  Scala object: no Scala import is read.

Original PR colbymchenry#1950 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g itself over

build:ui and build:lib ran npm run build --workspace ui. Under bun, a
package script's npm run becomes bun run, and bun ignores --workspace,
so build:ui ran the ROOT build again, which ran build:ui again. bun run
build:ui on main nests without end: with bun 1.3.8 here, 13 bun
processes deep after 62 s, when a watchdog killed the tree. npm is
unaffected, which is why it went unnoticed.

The scripts now name the workspace by directory: cd ui && npm run build
(and cd ui && npm run build:lib), which runs the same ui script under
either tool.

Validation: with the fix, bun run build, build:ui and build:lib finish
by themselves in 16 s, 6 s and 6 s, and check-ui-build and
check-ui-package pass. npm run build plus npm run build:lib give the
same output before and after, under npm's default script shell
(cmd.exe here) and under bash (npm --script-shell bash): dist/ (1,260
files) and ui/dist (171) are identical by SHA-256 in all four runs,
built in one checkout from clean outputs, and bun's build gives that
same tree. check-ui-build and check-ui-package pass in every run.
scripts/pack-npm.sh still runs npm run build:lib --workspace ui; that
runs only under npm, in the release workflow, so it is left as is.

Original PR colbymchenry#1764 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reconcile the fork's native Rust extraction/resolution architecture with
upstream through 69af19a (94 commits
past the prior merge point 553d5d1). All 57 conflicts resolved:
upstream's deleted TypeScript resolver spine stays deleted and its
behavior was ported into the kernel or surviving modules.

Kernel ports and fixes:
- new X().m receiver resolution (upstream newReceiver arm) into
  resolve_call_chain + match_new_receiver_call; C# isCsharpTypeVisible.
- Dart typedef classes and type-alias refs into dart/ walkers.
- Java record classes/components (java/records.rs), parameterized
  supertypes, is_class_type for compact constructors; Java
  typeParameters parity in the generic extractor.
- Ruby singleton-method is_static flags; ruby qualified-name fixes.
- Go colbymchenry#2596 scoped fn-ref candidate threading (Cand.node) + flush,
  StrRef/uses; Go type refs demote to `references` (colbymchenry#2581).
- TSJS call extraction: this[...] subscript receivers, new-expression
  receivers, is_unresolved_member_chain, window exclusion removed to
  keep fork window.Api.start emit; visit_parameter_defaults +
  visit_function_body alias.
- Python `from X import Y` module-file binding in the import resolver.
- PHP namespace-alias case-insensitive lookups in file scope and
  resolve_php_qualified_class.
- ts_class_type_parameter: stop truncating generic heads at `{`
  (Shaped<Item extends { id: string }>).
- Pascal bare function-reference capture retained (upstream's
  addressOfOnly assumed pascal-scope.ts, which the kernel covers).
- C macro NAME(a)(b){ shape kept by isCReturnTypeMacro (colbymchenry#2773 shape).

Restored fork behavior the merge had dropped:
- cg_folded_contains SQLite registration + defensive nested
  transaction rollback in the adapter.
- searchNodesFTS two-phase ranked query head (malformed union had
  silently disabled FTS).
- C/C++ blankers: comment stripping, elifdef, expression guards,
  const/volatile traversal in followsDeclaredName.
- Nuxt route-group/interior-index normalization and server-route
  method-suffix handler extraction.
- Explore-budget advisory mock (getSizeStats), release-workflow
  comment rewording.

Adapted instead of restored:
- resolution.test.ts: upstream-only API describes excised (kernel
  covers the behavior); upstream parity suites for the deleted TS
  resolver were removed, as was the wasm check-grammar script/test.
- jvm-overloads.ts carries the two name-matcher helpers play.ts
  still needs.

Golden dumps rebased (UPDATE_GOLDEN=1): all deltas traced to
intended upstream ports — Ruby is_static, Java record classes, Dart
typedef classes, Go references demotion, JS duplicate-node fix,
Pascal read-refs.

Known remaining gaps (upstream-new suite coverage, documented for
follow-up): rust nested `self.field.method().outer` chains resolve
the outer member to the field's type instead of the inner call's
return type (1 test); a set of upstream-new language suites still
fails (CFML include/create-object/inheritance, Ruby namespaced
receivers/singletons, JS super calls, Pascal paren-less calls) and
needs dedicated porting passes.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bompus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

…lution to the kernel

The merge at 69af19a left 43 upstream-new tests failing where the
kernel resolver lacked the behavior upstream added to its TypeScript
resolver. All are ported semantically into Rust; every suite is green.

- Ruby namespaced constants and singleton methods (colbymchenry#2503, colbymchenry#2827,
  colbymchenry#2834): resolve/ruby_paths.rs carries matchRubyConstantPathRef —
  `Billing::ChargeCard`, `::Foo.call` resolve to project constants by
  lexical-then-top-level lookup, calls through the owner's Ruby
  ancestry, and an unknown path resolves to nothing instead of a
  same-name guess. rubyMethodsOfKindFirst/`CallerKind` split
  `def self.m` from `def m` for class receivers, instance receivers
  typed by `.new`, and bare calls inside a method of the same kind.
- Pascal calls without parentheses and the colbymchenry#2803/colbymchenry#2810/colbymchenry#2816 scope
  rules (colbymchenry#2514): resolve/pascal_scope.rs ports pascal-scope.ts —
  read/ref/recv modes, unit and used-unit scope, include files,
  nested routines, system intrinsics, overloads and declared types —
  and the extraction's surviving function-value capture is flipped to
  address-of only, as its comment anticipated.
- CFML include, createObject and suite-mirrored inheritance (colbymchenry#2650,
  colbymchenry#2651, colbymchenry#2652): includes resolve their template by path then by file
  name (never the includer), `createObject("component", path)` and
  calls chained onto a construction resolve through the component
  path resolver, which now skips test suites for production callers,
  breaks their ties toward the component, and ranks a one-folder
  mapping onto the project root.
- JS `super.m()` calls: matchJsSuperCall resolves through the
  enclosing class's extends/implements ancestry in
  js_objects_upstream, and the fork's super-self gate now reads the
  receiver at the ref's position so `this.m()` beside `super.m()` on
  one line keeps its own edge.
- Rust `self.field.m1().m2`: `()`-suffixed refs anchor to the chain's
  last call inside rust_written_call, tuple-struct fields type through
  rust_field_written, and named roots walk standalone calls in
  rust_chain_member.

Golden dumps re-baselined: the pascal torture fixture's paren-less
refs resolve, and a Ruby call upstream scores 0.9 now scores 0.9 in
the kernel instead of 0.4.
@bompus
bompus merged commit ccda2b7 into fork/consolidated Oct 27, 2026
8 checks passed
@bompus
bompus deleted the merge/upstream-69af19ac branch October 27, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants