Skip to content

chore(sync): merge upstream d2e0389 and release 1.6.2-fmagent.3 - #22

Merged
Dshuishui merged 355 commits into
mainfrom
sync-upstream-d2e0389
Nov 1, 2026
Merged

Dshuishui merged 355 commits into
mainfrom
sync-upstream-d2e0389

Conversation

@Dshuishui

Copy link
Copy Markdown
Collaborator

What this does

Moves the base from upstream 10e316d to d2e0389 (main, 2026-11-01, 354 commits). Upstream has not tagged a release since v1.6.2; what landed on main in this span is mostly call resolution: Rust receiver types, calls on library and built-in values no longer linking to a same-named project method in every language, and JS/TS module exports.

  • The version becomes 1.6.2-fmagent.3 (.2 was the prerelease for Fix C function extraction across conditional signatures #20).
  • Only two conflicts: CHANGELOG.md keeps both sides; c-cpp.ts is covered under patch 5.
  • Release as a prerelease first and promote it once FM-Agent runs cleanly on it. Some function names change, so a project analysed with .1 needs one full run before --incremental.

Patches

Upstream took none of the five patches; all five stay:

Patch Merge Patch tests run on upstream's code
1 Exclude the fm_agent/ work directory No conflict, unchanged line for line Fail
2 Name wrapped functions nothing binds after the wrapper string No conflict, unchanged line for line Fail
3 Effect-TS service member resolution No conflict, unchanged line for line Fail
4 Generator class fields No conflict, unchanged line for line Fail
5 C functions with a conditional signature (#20) Conflict by position only: upstream added blankCUnbalancedConditionalBranches at the same spot, which handles only an else if written across a directive. Both passes stay, #20's first 10 of 14 behaviour cases fail

Also: #20's test is renamed fm-agent-c-conditional-function-headers.test.ts, matching the other patch tests, and FORK.md records the new base and the fifth patch.

Verification

  • Tests: 5 failures, all known. 2 are a machine without zip, failing on upstream too; 3 are daemon-older-version, caused by the fork's version string and not run in CI. The 5 patch test files (44 tests) all pass.
  • codegraph-benchmark: this PR 15/15, upstream 11/15.
  • 13 projects (Python, Go, Java, C, C++, Rust, JS, TS), read through FM-Agent's CodeGraphExtractor:
    • Apart from opencode, functions and call edges match upstream exactly.
    • On opencode, patches 2 and 3 add 463 edges; a sample of 40 is all correct.
    • Patch 1: on a Redis tree holding fm_agent/, upstream indexes 16,310 extra nodes from it; this PR indexes none.
  • Edges that differ from .1, checked against the call site's source:
    • The ones that go are mostly library methods linked to a same-named project method by name.
    • The ones that come are mostly real; ripgrep gains 893, and a sample of 40 is all real.
  • Performance: indexing time is about the same; peak memory is lower on most projects.

Not fixed (upstream)

  • Rust: a call whose receiver comes from an index, an Option or a Box field (e.g. errs[0].io_error()) still does not link.
  • ESLint: this.matches(...) in a class method still links to the same-named function the file destructures from require.

colbymchenry and others added 30 commits October 21, 2026 10:08
…tsconfig paths (colbymchenry#2740)

bigbluebutton's client imports nearly everything from its root,
`import Icon from '/imports/ui/components/common/icon/component'`, which its
tsconfig maps with `"paths": { "/*": ["*"] }`. tsc maps a rooted name through
`paths` (only `./` and `../` count as relative there) and never reads it
from `baseUrl`. ownConfigCandidates refused every specifier starting with
`/`, so all 2,375 of these imports stayed unresolved and every name they
import linked by bare-name matching, often to the wrong one of several
same-named components.

A rooted specifier now goes through the nearest config's `paths` patterns
only; `baseUrl` is still never applied to it, and a protocol-relative
`//host/x` stays out. On bigbluebutton the unresolved refs fall from 26,189
to 22,565 and its rooted imports from 2,375 unresolved to 1 (a module that
isn't in the repository); 13 control repos are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rapped callee is the plain call (colbymchenry#2742)

A TS/JS call whose callee was no name and no member access (an IIFE, a
curried `connect(mapState)(View)`, `handlers[type]()`, `(0, fn)()`, a
tagged template on a call) stored the callee's whole source text as the
reference's name. Nothing can answer such a name, so each one parked as a
failed reference, an IIFE's entire body among them. extractCall now reads
the callee past the wrappers that keep it the same callee (parentheses,
`!`, `as` / `satisfies`, `await`, type arguments) and records nothing when
that is not a name or a dotted chain; the calls inside are visited on
their own. Mirrored in the kernel (tsjs/extractors.rs callee_name_of).

The reference of a wrapped callee sits at its name, where a plain call's
does, and the resolver's bare-call check reads `fn!(`, `(fn)(`,
`(fn as F)(` and `(fn satisfies F)(` as it reads `fn(`: SigNoz's
`(callback as FrameRequestCallback)(0)` and `(callback)(0)` name the
arrow's own parameter, and from the `(` they bound a generated interface's
`callback` property.

tree-sitter-typescript parses a prefix operator over a generic call as it
parses `await f<T>()` (colbymchenry#2586): `!settings.get<boolean>('X')` and
`void apiRequest<T>()` are calls whose callee is `!settings.get` /
`void apiRequest`. The callee peel that handled `await` now peels a prefix
operator too (peelTsPrefixedCallee; the kernel's peel_await_callee), so the
call is to `settings.get`, and `void apiRequest<T>().then()` encodes as
`apiRequest().then`, not `voidapiRequest().then`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…what the initializer declares (colbymchenry#2743)

PRs colbymchenry#693 and colbymchenry#1802 walk a declaration's initializer under the symbol it declares,
so `const routes = { home }` references `home` from `routes`. The
dispatcher's declaration-wide function-as-value scan then took the same
values again for the enclosing file (or class). Flush-time dedup keys on
the source, so both stayed: a second reference to the same function from
the same spot.

Each initializer walk now records what it walked, and the scan skips it.
PR colbymchenry#2738 added that mechanism for component wrappers, and C#, Scala and Dart
already had their own. The newly covered paths:
- TS/JS module-level constants and variables, owned object literals
  included (colbymchenry#2300), and TS/JS class properties;
- Python assignments, Go var/const specs, Rust const/static items;
- Java fields, and Kotlin properties (walkInitializer takes the property's
  id, so their walk is unchanged).
Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin.

References no initializer walk takes are unchanged: a module-level call
argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler`
(the declaration's own value, which the scan still reads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the barrel its re-export (colbymchenry#2744)

Sync and the file watcher now link `import { useInvocation } from './context'`,
and every use of the name, when an edit gives the barrel context/index.ts
that re-export after the importing file was indexed. Before, the import,
its calls and its JSX or template tags stayed unlinked until the importing
file changed or the project was re-indexed.

The barrel declares none of the names it passes on, so sync's retry by name
never reached the importers' failed references. The edit adds no file and
changes no definition, so neither the importer re-open through added files
(PR colbymchenry#2512) nor the barrel walks (PR colbymchenry#2560, PR colbymchenry#2665) run.

importBindingTail now parks the whole name of a plain named import under
its module's key (`module:context`), as PR colbymchenry#2450 does for default, namespace
and aliased bindings and PR colbymchenry#2507 for Python's unaliased imports. It does so
only while the import resolves to a project file that carries that key and
that module doesn't give the name yet; the by-name lookup still finds the
reference by its own name.

Three versions were measured on 18 repos, and all fix the same barrel-edit
cases. This narrowest one parks 2,727 references on a fresh index, against
11,061 for parking every plain named import, and keeps the retries per
changed file within +1% to +3% of main on folo, vben and milo-ui. Fresh
indexes are identical in nodes, edges and files in all 18 repos, and on
vben's gut scenario sync's difference from a fresh index falls from 2,337
lines to 194.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns put in it (colbymchenry#2745)

A Go struct can hold a function in a field and call it through the field.
Prometheus' service discovery runs every provider that way: refresh.Discovery
calls d.refreshf(ctx), NewDiscovery copies refreshf from Options.RefreshF,
and each provider passes its own method (RefreshF: d.refresh). Nothing
answered d.refreshf, so a flow from Discovery.Run stopped at the field; colbymchenry#2430
rightly dropped the interface-impl edges that had stood in for it.

goFuncFieldEdges, a Go-only synthesis pass, links a call x.F(...) through a
func field of a struct to each function or method value put in that field:
by a composite literal (keyed or positional, found through its instantiates
edge), by an assignment x.F = v, or by one level of field-to-field copy
(refreshf: opts.RefreshF). A value is what the resolver already linked it
to, else x's method m for x.m, an interface's method for an interface value.
Receivers are typed through the resolver's own Go scope reader
(goLocalDeclarationAt over goLocalDecl, colbymchenry#2706), or by a local's composite
literal, new(T), type assertion or resolved call result. Channels are keyed
by package, struct and field, so a generic field name never crosses between
unrelated structs; one package's test wiring links no other package's test;
an edge cites a function's production wiring; generated files are not read.

Edges are calls, provenance heuristic, synthesizedBy go-func-field, with
field, via (package-qualified when the copied field is another package's)
and registeredAt; explore, the node trail and ContextBuilder read them as
"func field `Discovery.refreshf` (set from `Options.RefreshF`)". The pass
sits after the Go framework passes, with a pool cost hint, and every Go file
now counts as a synthesis input.

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

An unexported Go method name belongs to the package that declares it.
goImplementsEdges matched method names only, so an interface asking for
an unexported method was linked to every type in any package with a
method of that name: each of prometheus's `refresher { refresh(...) }`
interfaces (one per discovery package) went to about 37 providers in
other packages, such as discovery/aws.EC2Discovery, none of which Go
lets satisfy it.

Method sets now hold an unexported name together with the directory of
the file that declares the method (goMethodKey), on both sides. Where
the method is declared is what counts, not where the type is: etcd's
gRPC servers outside api/etcdserverpb keep their KVServer edges,
because the UnsafeKVServer or UnimplementedKVServer they embed declares
mustEmbedUnimplementedKVServer in the interface's package.

The interface-dispatch bridge uses the same key, so a call through the
interface reaches the method of the interface's package: not the
type's own namesake from another package, and not one Go's selector
would skip at a shallower embedding depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the query names first (colbymchenry#2748)

codegraph_explore's "Dynamic-dispatch links among your symbols" section
deduped its lines by `source name > target name`. Every interface →
implementation link of one method reads the same way (`TrySet →
TrySet`), so an interface method with several implementations printed
one line, and which implementation it kept was edge order, not the
query. On gin, `tryToSetValue setter.TrySet multipartRequest.TrySet
setByForm` printed only `TrySet → TrySet @binding/header.go:35` and hid
formSource's link.

Lines are now deduped by the edge's endpoints (node ids), and the
6-line cap stays. When more links exist than fit:
- a link that reaches a test or example file through an end the query
  didn't name goes last, as explore's answer leaves those files out;
  the predicate is explore's own `isLowValue`, moved to module scope so
  both uses share it;
- otherwise links go by how pointedly the query named their two ends.
  A symbol a token picks out (at most three definitions, the same
  notion as the boundary scan's agent-specific symbols) or a named
  non-callable endpoint weighs 3, a candidate of an ambiguous token 1,
  an unnamed symbol 0. An implementation the query names always comes
  before one it doesn't. The weights also keep an ambiguous token's
  candidates from crowding out a precise one: prometheus's
  `Discovery.refresh` matches a `Discovery` in each discovery package,
  and in `Discovery.refresh fetchApps Decoder.Decode` their links to
  each other filled the cap and hid the one link into `fetchApps`;
- edge order breaks ties.

The section takes the rendered Flow chain itself. It leaves out an edge
between two of the chain's nodes, as before, and also a line that reads
the same as one of the chain's synthesized hops or as a line already
listed. gin declares its `Binding` and `StructValidator` interfaces
once per build tag (binding.go, binding_nomsgpack.go), so the twin
declaration's link printed exactly like the Flow's hop.

A same-name link names each end by its owner, `setter.TrySet →
formSource.TrySet`, the way an agent writes it in a query.

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

An edit that changes which declaration a module exports as its default
(`export default A` becoming `export default B` while A stays, or the
default dropped) left every reference through a default import of it on
the old declaration. A sync moves other files' edges into a changed file
onto the node that replaces each target, and an `export default` statement
is no node, so no definition delta, failed-ref retry or importer walk saw
the change. A fresh index links the new default, or nothing.

- ReferenceResolver.changedDefaultExports: for each modified JS/TS module
  that unchanged files import by default or as a namespace (an imports edge
  into its file node named for the binding), compare what the whole-name
  references through those bindings resolved to with the module's default
  now (moduleDefaultExport, the lookup resolveViaImport makes). No file is
  read; a module forwarding its default samples 8 importers' edges.
- reopenReferencesThroughChangedDefaults (module-importers.ts): when one
  moved, the edges bound before this sync resolved anything go back for
  the sweep: through every binding that reads the default (importersOf
  seeded with `default`: default imports, { default as Y },
  require('./x').default, modules passing it on), the routers'
  `layout:<binding>` references, and into the old default's own members
  (typed receivers, inherited members).

Regression test sync-default-export-change.test.ts: 6 cases comparing the
synced graph with a fresh index of the final tree; all fail on main.
Fresh indexes are byte-identical on 8 repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sh.Discovery.refresh` (colbymchenry#2750)

Explore's token resolution, codegraph_node's lookup and the callers,
callees and impact lookups dropped every Go symbol written the way Go
code names it: `refresh.Discovery.refresh`, `storage.Queryable.Querier`
and `gin.Context.Next` resolved to nothing, and so did `binding.Default`.
Three gaps:

- A qualified name was matched against the qualifiedName, which holds
  the type and not the package, or entirely against the path, which
  holds the package and not the type, never as a package followed by a
  type. A new matcher stage reads `pkg.Type.method` as leading
  qualifiers that name the package by path segment and trailing ones
  that end the node's scope. A Python `pkg.mod.Class.method` and a Rust
  `module::Type::method` read the same way.
- A module's root package has no directory: gin's `context.go` is
  `package gin`. A Go package qualifier now also matches the package the
  file declares, read from its `package` clause (src/graph/go-package.ts).
  That covers versioned `v2/` directories and generated packages like
  prometheus's `io_prometheus_client` as well. The clause is read last
  and only where it can decide, a top-level function or a node whose
  scope already ends with the written type, so a common name's hundreds
  of definitions cost no file reads.
- Explore and codegraph_node took a qualified token's matches from the
  full-text search's top 50 only, and for gin's `binding.Default` those
  were three `binding/default_validator*.go` files. Through one shared
  helper they now fall back to the exact-name index when the search has
  no match; the CLI's lookup already started there.

The new stage runs only after the existing ones fail, and the search
keeps its order, so a name that resolved before keeps its result.

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

Links of the same rank were listed in edge order, so one named symbol's
long fan-out could fill the 6-line list before another named symbol's
first link. prometheus's `refresh.Discovery.refresh` hands its func
field to 28 discovery packages, and in `refresh.Discovery.refresh
hetzner.refresher.refresh hcloudDiscovery.refresh` those links filled
the list before `refresher.refresh` → `robotDiscovery.refresh`.

Each link now has a turn: how many links of its rank the named symbol
it hangs from already has. Links sort by rank as before, then by turn,
then by edge order, so links of the same rank alternate between the
named symbols. A token that names two interfaces gets both: prometheus's
`Appender.Append` matches storage.Appender and chunkenc.Appender, and
storage's implementations no longer wait behind six of chunkenc's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olves another way (colbymchenry#2752)

A named import that a name match linked to a declaration elsewhere while
its module did not export the name kept that link after an edit gave the
module the name. A barrel's `export { EmptyState } from './EmptyState'`
declares nothing, so no definition delta, failed-ref retry or barrel walk
saw it (the "retargets" PR colbymchenry#2744 left). A name match into the module
itself, a class exported only as its default and imported by name, kept
its link after the default was dropped, where a fresh index finds nothing.

reopenNameMatchedImports (module-importers.ts): for each module the sync
modified, its importers' binding edges that resolved some other way than
their import (getImportBindingsResolvedElsewhere, an indexed range per
importer), whose import statement names this module, are resolved again
against the graph as it is (ReferenceResolver.resolveNow, writing
nothing). When the answer differs, the importer's edges through the name
and into the members of what the name match took go back for the sweep.

Regression test sync-name-matched-imports.test.ts: 4 cases comparing the
synced graph with a fresh index of the final tree. Fresh indexes are
byte-identical on 8 repos.

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

Fixes colbymchenry#2485

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

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

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

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

Fixes colbymchenry#2596

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

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

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

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

Fixes colbymchenry#2477

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

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

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

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

Fixes colbymchenry#2545

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

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

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

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

Fixes colbymchenry#2546

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

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

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

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

Fixes colbymchenry#2543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry and others added 27 commits October 31, 2026 14:19
colbymchenry#3147)

Each stripper split a file into an array of one-character strings, blanked
comments in it and joined it back: 8 bytes a character and two passes, for every
file, on every resolver thread, for each of the several memos that read stripped
text. BlankedText keeps the ranges and copies what's between them by the slice.

Stripping 3,772 files of ten repos: 1,142 ms to 359 ms. django subset: CPU 21.6 s
to 20.7 s, peak RSS 1,412 to 1,302 MB.

Output matches the old strippers byte for byte on 48,283 inputs: those files in
their own language, a sample of them in every other language, and 40,000 random
cuts and splices that leave strings and comments open.

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

LRUCache.get refreshed recency by deleting the key and inserting it again, so a
hit was two Map writes, and the deleted slots piled up until the table rehashed.
It was the hottest line of resolving tokio (1.8 s) and rocksdb (1.5 s).

Recency is now a doubly linked list through the entries. The cache evicts the
same entries in the same order: a test drives it against the Map version with
50,000 random operations.

rocksdb subset: self time in LRUCache.get 1.9 s to 0.4 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame could be one (colbymchenry#3149)

jsCodeBindsName and bindsAtSites asked whether a function or a file binds a name
by running large patterns, or walking back to a parenthesis, for every mention.
Nearly all mentions of a name a function doesn't bind are calls and member reads.
jsNameSites reads the characters around each mention first: a declaration's name
has a blank before it, a parameter's has `?`, `:`, `=`, `,` or `)` after it. When
none could be, neither pattern is built.

pythonLineBindings likewise skips its import pattern on a statement that doesn't
hold the word.

excalidraw: CPU 29.1 s to 26.6 s. django subset: 20.7 s to 19.6 s. Interleaved,
three rounds.

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

The peak memory main gained over 1.6.2 is mostly V8's space for new objects in
the resolver workers. Resolving keeps many objects alive for a while, so V8
grows that space as far as it may, and on Node 24 a worker may take 192 MB: on
tokio it went from 320 MB to 640 MB over six workers, with old space at 184 and
299 MB. Node 22's default is 48 MB, and that's the limit here.

Peak RSS, then CPU, with and without, interleaved, three rounds:
tokio 1,444 to 1,053 MB, 23.0 to 22.1 s; excalidraw 1,589 to 1,148 MB, 28.4 to
27.0 s; rocksdb subset 2,511 to 1,919 MB, 83.0 to 82.3 s; django subset 1,247 to
1,073 MB, 20.8 to 21.3 s; jsoup 1,005 to 877 MB, 13.7 to 14.3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… which paths are tests (colbymchenry#3151)

Every resolver worker detected the project's frameworks for itself as it
started, and a detector may read every source file: Spring's reads each `.java`
until it finds an annotation, so in a Java project without Spring, six workers
each read the whole project. A full index has just detected them on the same
files in the main thread, so it now hands the workers the names. A sync, where
the main thread's answer may be older, leaves them to detect as before.

isTestPath runs seven patterns over a path, and resolving asks about the same
paths for every reference and candidate. It now remembers the answer, up to
100,000 paths.

elasticsearch subset, 1,710 files: self time in detection 4.4 s to 2.3 s, in
isTestPath 1.4 s to under 0.1 s, out of about 50 s of CPU.

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

On Windows, module.enableCompileCache() never returns when its folder's drive
doesn't exist: Node's directory creation spins at 100% of a core, on 22.22 and
the bundled 24.16 alike. The default folder is under the temp folder, so a TEMP
on an unplugged disk or an unmounted RAM drive hung every codegraph command at
start, in 1.6.2 too. Writes that fail in other ways return an error and are fine.

src/compile-cache.ts checks that the drive is there, and replaces the four copies
of that block. NODE_COMPILE_CACHE set to such a path hangs Node itself before any
of our code runs, which nothing here can change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dist's JavaScript was 37% comments, and they cost every thread at load.
name-matcher.js goes from 1.63 MB to 1.07 MB. Its comments held the only 1,889
characters past Latin-1, which made V8 keep the source as two bytes a character,
3.2 MB in each of up to 15 threads: without them it's one byte, 1.07 MB.

The type declarations keep their comments: a second, unchecked tsc pass writes
them, which adds about 4 s to a build. Stack traces keep working through the
source maps.

A parse worker loads in 136 ms instead of 152 ms with 14 MB of heap instead of
20 MB. Peak RSS: flask 419 to 359 MB, gin 418 to 371 MB, excalidraw 1,148 to
1,105 MB. CPU doesn't move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der links again (colbymchenry#3155)

PR colbymchenry#1921 made Python imports resolve from the package roots a project has. A
file's own folder counts as a root only when it holds no __init__.py, because in
a real package a bare `from utils import x` must not take a sibling over the true
top-level module. That also lost this layout: rocksdb's
tools/block_cache_analyzer/block_cache_pysim_test.py does `from block_cache_pysim
import (...)` beside block_cache_pysim.py, in a folder that holds an __init__.py.
A script run from there has that folder first on sys.path all the same.

The fallback from PR colbymchenry#3141 now starts at the importer's own folder, still after
every root has answered, so a top-level module a root provides keeps winning. A
package above the importer stays passed over.

Proving the names is not enough beside a file. A trace of every candidate found
that most bare names with a module beside them are the stdlib's, and the proof
passes on django/utils/text.py, whose `from html import escape` would have taken
django/utils/html.py. So two checks now come with it. A name in
sys.stdlib_module_names stays the stdlib's, above a script as well as beside it;
CPython's `test` package isn't on that list, so redis's `from test import
TestCase` is unaffected. And a module that imports its own name wraps the real
one, which is what that name then means. That covers a wrapper's neighbours and
the wrapper itself, so a file is never its own answer.

A sync follows it. An edit re-opens a module's edges only if an import node spells
that module from a folder that can reach it: a file in a package folder, for the
path from that folder, or a file anywhere below a plain folder that isn't a root.
Those spellings are looked up by name, one per folder above the module, so an edit
nobody imports that way re-opens nothing, even for a hub like mypy/types.py, and
costs nothing measurable. It also closes the gap PR colbymchenry#3141 left open, a dotted
module above a script, and a module that is a package.

The [Unreleased] bullet from PR colbymchenry#1921 gains a clause. No new bullet, because the
regression never shipped.

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

A declare module stub in a .d.ts took imports away from the real file, even
from another package, which main must not do against 1.6.2. On react,
packages/eslint-plugin-react-hooks stubs shared/ReactFeatureFlags for its own
tsc, and 94 imports across the reconciler, react-dom and the rest landed on
that stub. Test-only stubs under packages/react/src/__tests__ took 478 imports
from the whole repository. Regression from PR colbymchenry#2718.

Root cause: every script declaration in the repository answered every bare
specifier, ahead of any file, because TypeScript looks at ambient modules
first. That holds inside one program, and a monorepo has one per package.

Fix:
- A block counts for a file in its own package, in a package nested under that
  one (vscode's extensions, the root package's src/vscode-dts), or in a package
  depending on it (astro:assets from astro/client.d.ts). A package.json that
  only says {"type":"module"} makes no package. A test's block counts only for
  tests in its own package.
- A module of a package the repository holds, a workspace member or a path
  dependency, comes before a block, in the stub's own package too: the stub
  describes that module, and the module is what runs. A name only the stub
  declares still resolves to it. A paths target stays behind the block, as the
  checker has it (copilot's vscode shim), and so does a package whose module is
  only a declaration file.
- A name bound to a module reaches a block only if the block counts for the
  file, and a block that doesn't is no candidate in the exact-name pool: its
  bodiless functions, which PR colbymchenry#2718 added and 1.6.2's crowd never held, took
  react's createRef in the ranking and were then dropped by the gate.
- Sync revisits an import's module edge into a declaration file when a real
  module appears.

Validation: A/B against main with both arms on the same commit. react: 711
edges moved, 49 lost, 0 added; REG's 203 are all back on 1.6.2's target,
imports into the test stubs go 609 to 0 and into ReactFeatureFlags.d.ts 94 to 0,
and none of the 49 lost is a name 1.6.2 linked. kit: 146 moved from the
generated types/index.d.ts to the implementations. vue core: 27 moved to the
package entry. astro: 5 moved, 390 links through dependents kept. Byte-identical:
vscode's extensions (324,231 edges, 18,847 into src/vscode-dts), outline, dub,
excalidraw, starlight, taxonomy, and the controls gin and flask. Red to green:
13 of 17 new tests fail on main's source; each of the 12 safeguards fails a test
when it is disabled. On vue core a sync matches a fresh index edge for edge in
both directions. Index cost: CPU -2.1% on core, -0.6% on excalidraw, median of 5,
interleaved.

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

PR colbymchenry#3126 ends every query worker after five minutes idle so a burst's memory
comes back, which fixed issue colbymchenry#3027. It also made the first call after a pause
slower than 1.6.2: 634 to 729 ms on django against 565 to 587, where warm is 165.

Root cause. The replacement worker is already up when that call arrives, so the
500 ms is not worker startup. Reopening the project inside a warm isolate costs
nearly the same, so it isn't compiled code either, and a compile cache for the
worker moved nothing. It is what a connection works out once and remembers: the
dominant file's scan of every edge (230 ms on django), the name indexes (85 ms)
and a git probe (30 ms), then tree-sitter's first parse. A new connection has to
work all of that out again, and a worker's memos can't be handed over, because
their change stamp only means something on the connection that took it.

A call landing as the recycle ran fared worse. With no warm worker it ran on the
main thread, cold: 884 to 1,154 ms, the next call just as slow, and the transport
held for up to 417 ms.

Fix. The pool keeps the worker that served last and retires the rest. The kept
worker closes every project but the last one used, and its connections give up
their page cache and file mapping, which the next reads fill again. Everything
else stays: compiled code, the memos, tree-sitter. No worker stops, so there is
no moment when a call finds none, and a call that lands during the trim waits a
few milliseconds. A worker still holding over 256 MB afterwards is replaced the
old way, so keeping one can't keep hold of what a large query left behind. A
trimmed worker reports about 80 MB, so this has not fired on any repo here.

There is no forced collection: V8 already shrinks an idle isolate's heap by
itself, well before the five minutes are up. A last-resort collection added
3 MB at that point, so it came out.

Options measured on django, one worker (working set / private MB, then the
first call):
- a clean worker, as on main: 106/79, 732 ms
- a clean worker made to compute the dominant file and git probe first: 144/80, 410 ms
- the same plus every memo: 213/153, 319 ms
- a clean worker replaying the last query: 295/258, 189 ms
- a kept worker, trimmed (this): 168/175, 152 ms

Validation, on Windows, through the real MCP server under 1.6.2's node and flags:
- First call after five real idle minutes, three gaps:
  django: 1.6.2 886/676/443, main 744/878/660, this 170/171/200 (warm 214)
  flask: 1.6.2 111/79/82, main 180/172/166, this 43/41/43 (warm 41)
  elasticsearch: 1.6.2 956/641/674, main 2752/2561/1419, this 764/234/243 (warm 256)
  The first gap's extra 500 ms shows only on arms that release the page cache, and
  is gone by the second, which fits its pages being read back once.
- A call at the deadline on django: 142 to 226 ms at every offset from -300 ms to
  +3 s, the next call warm, the transport never held over 19 ms.
- Memory after a burst of four workers and five idle minutes (ws/priv):
  excalidraw: 1.6.2 505/482, main 177/221, this 244/295
  django: 1.6.2 695/486, main 179/223, this 247/295
  elasticsearch: main 221/265, this 298/348
  So about 70 MB more than main, on any index, in exchange for the first call.
- 30 busy-then-quiet cycles, 600 mixed calls, no errors and no creep.
- 240 processes, each with four real workers, shut down after, during and right
  behind a trim: all exited 0. 960 worker exits, no crash.
- The real shared daemon behaves the same: 155 ms at the deadline.
- Red on main: 13 new tests fail, counting worker starts rather than time. Twelve
  mutations undoing each new line are all caught.

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

A bare call between two functions declared in a method of an object behind
`satisfies` or `as const` linked to nothing: 34 calls in react's ESLint rules,
where `const rule = {...} satisfies Rule.RuleModule` holds everything in
`create`, like `reportProblem(...)` from `visitCallExpression`. 1.6.2 linked
them. Regression from f40db4b (PR colbymchenry#2363), which never shipped.

Traced: the extractor makes a literal's direct function members nodes only
when the initializer is the literal itself, so behind `satisfies` the method
`create` has no node, and what it declares is qualified under the constant:
`rule::reportProblem`. objectLiteralOwnerOf took any function qualified under
a holder whose initializer opens with `{` for a member of the literal, and a
member is never reached by a bare name. So the shape is wider than the report
says: a method of a nested object, a computed key, a function a member's value
passes on, and a literal read through `[key]` all lost the same calls, in
Vue and Svelte script blocks too.

Fix (resolver only): a holder's own text is read once for its `{...}` blocks.
A function directly in the literal's braces is a member. One deeper down is a
local of the block it is written in: reachable by name from inside that block
and from nowhere else, which is tighter than 1.6.2, where such a function
answered a bare name from anywhere. No extraction change, so no kernel change.

Validation: object-literal tests red on main (16), green here, on both
engines. A/B against 4499d00: react +35 (34 calls and 1
reference, all in the two ESLint rules), outline +5 (its oxlint rules);
excalidraw, vscode-be, taxonomy, express, vben, flask, gin identical. All 40
read against source. Index CPU: level (see the PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… module's export (colbymchenry#3162)

`const named = require('./m'); named.second()` on a CommonJS module of
`exports.x = ...` assignments landed on the module's first export. 1.6.2 does
the same. `require()` is the module, and so is a default import of a CommonJS
one, but the lookup asked for the default export, found none stated, took the
guess at the first exported function, and linked the member call to it when
the guess had no such member. So mocha's `utils.clean()` reached `escape`, and
`named.missing()`, which the module doesn't export, reached it too. Modules
whose functions the guess can't see, `module.exports = { a, b }` or named
function expressions, linked nothing at all.

Fix (import resolver): a member read through a default binding is first what
the module exports under that name, read the way a namespace import reads it,
through re-exports and `module.exports = require('./lib/m')`. Where the module
states no default that comes before everything else, and a member with no
such export links nothing: the receiver isn't known, so there is nothing to
name. A stated default keeps its own members first: a static, an object
literal's member, an instance's method, then the module's export. A
declaration file's `export =` counts as stated. `module.exports = X` now beats
the guess at a neighbour flagged exported, which sent mocha's `new Mocha()` to
`afterEach`. What `module.exports` says is read once per file.

Validation: tests fail on main and pass here. A/B against 4499d00: express 24 moved, 49 added, 11 false
removed; mocha 125 moved, 10 added, 1 false removed; eslint 21 added; react
19 moved, 127 added; excalidraw 2 added; outline 2 false removed; koa,
vscode-be, taxonomy, flask, gin identical. Every moved and added group read
against the module it lands in. Index CPU: level (see the PR).

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

A Python reference keeps its whole chain, and resolveStaticMember took the
first link after the receiver for the target. So `HttpResponse.content.fset(...)`
called the `content` getter, `Engine.get_default.cache_clear()` called
`get_default`, and `Container.SomeModel.objects.create(...)` read as a new
`SomeModel`. 1.6.2 left these unresolved, so this never shipped.

Fix: a Python chain is walked to its end through nested classes, so
`Foo.Inner.run()` reaches `Inner`'s `run`, and one that leaves the class,
through a method or an attribute the index has no member for, links nothing.

Validation: tests fail on main and pass here. A/B against 4499d00: django 4 removed, exactly the four false
edges; flask, fastapi, requests, gin, express identical. Index CPU: level (see the PR).

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

`bridge.send(...)` on a `bridge: BackendBridge`, with `export type
BackendBridge = Bridge<A, B>`, linked to nothing. The typed lookup found no
class of that name, and a capitalized type no class is named is past guessing,
so the call stopped. 1.6.2 got react's four `bridge.send` in nested functions
only because it couldn't read the outer parameter's type, and guessed `Bridge`
from the receiver's name.

Fix: tsKnownTypeCall follows a type alias to the one class or interface its
value names: `Bridge<A, B>`, `Bridge | null`, Flow's `?Bridge`, an alias of an
alias, each name read the way the alias's own file reads it. A union of two
types, a function type, an array and a type parameter name nothing. Parameters,
locals and fields all go through it. Memoized per file and name.

Validation: tests fail on main and pass here. A/B against 4499d00: react 147 added, nest 31,
vscode-be 17, excalidraw 2, all typed by an alias of a class read against
source; taxonomy, ai-chatbot, outline, flask, gin identical. Index CPU: level (see the PR).

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

`stream: ReadableStream` in react's Flow files stopped linking to `declare
class ReadableStream` in `flow-typed/environments/streams.js`. PR colbymchenry#2736 keeps a
type a module doesn't import to its own file, a declaration file or a `declare
global` block, and a Flow library is none of them: a `.js` script of `declare`
statements. The types a Flow library declares were lost on every file. PR colbymchenry#2736
never shipped.

Fix: a `.js` script with a top-level `declare` statement is a Flow library, its
`declare module.exports` no CommonJS export, and its top level global to the
JavaScript files Flow reads, which see it as they see a `.d.ts`. A TypeScript
file doesn't: its `ReadableStream` is TypeScript's own. A library's own bare
type names are held to the same rule, and a top-level call `declare(...)` is no
statement.

Validation: tests fail on main and pass here. A/B against 4499d00: react 207 added, 3 false removed (a
`Function` to an enum member, an `HTMLElement` to a property); metro 174 added
(`Buffer`, `http$IncomingMessage`, Jest's types), 5 false removed; excalidraw,
vscode-be, express, taxonomy, flask, gin identical. Index CPU: level (see the PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o's own edge (colbymchenry#3166)

Two ways an import of a repository's own package landed on the wrong code.

The side of the wire: the `exports` reader tries `node` first, so react's
`fixtures/flight/src/index.js`, built for the page, took
`react-server-dom-webpack/client` as `client.node.js` and its `encodeReply` from
the Edge client that re-exports. A file that reads `document` or `window` now
takes the entry's `browser` condition; any other file reads it as before.

The install: react's `compiler/` has a package.json with `workspaces` of its
own, so it installs an npm `react` and `@types/jest`. Its files linked to
`packages/react/index.js` by the outer workspace's name, and its bare `expect`
to the outer `scripts/jest/typescript/jest.d.ts`. A folder whose package.json or
pnpm-workspace.yaml declares workspaces is now a root of its own: a bare import
from inside it doesn't reach the outer workspace's packages, and a guess by name
doesn't leave it. A member of the outer workspace is no root, since package
managers don't nest workspaces. The check is on the winner, not the pool:
taking candidates out of the pool left lone ones, which answered 149
`this.setState()` calls in the outer root with a test stub.

Validation: tests fail on main and pass here. A/B against 4499d00: react 19 moved to the browser
variant, 736 removed, all of them from `compiler/` to outside it; excalidraw,
ai-chatbot, pot-desktop, vscode-be, vben, dub, flask, gin identical. Index CPU:
level (see the PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rents and traits (colbymchenry#3168)

Since colbymchenry#1935 a receiver named after a class took what that class inherits,
walked by bare supertype names. Drupal's `$entity->access()` went to the
views plugin `Entity`, in a namespace the calling file never mentions, and
up three classes to `HandlerBase::access`; Laravel's `$comments->pluck()`
took `Support\Collection::pluck` past the `Eloquent\Collection` the class
imports, and `$pivot->getForeignKey()` took `Model`'s past its trait
`AsPivot`. A typed receiver whose method sat on a supertype found nothing in
the first pass, since no `extends` edge exists yet, and went on to the same
guess.

A class is now found the way PHP finds one: through the calling file's
namespace and `use` imports (group imports included), its parents through
the imports of the file declaring it, past any attributes above it, a
trait before the parent.
- A receiver named after a class the file names reaches all it inherits,
  and that class comes before its namesakes. One the file can't name gives
  only its own method, or what it inherits when it is the one class of that
  name: a word picks no namesake.
- A declared type answers through its source ancestry in the first pass.
  Its name no longer leads to a class named after the variable, and a
  namesake's method is asked only past a parent outside the project or a
  `__call`.
- A Laravel facade hands a static call to the class `getFacadeAccessor()`
  names. A local an Eloquent model's static factory gives is that model.
- `parent::x()` above a parent from outside the project is no trait of the
  project.

Validation, base 4499d00 against this change, every class read and a sample
of each against source (numbers in the PR):
- framework 974cd809: 169,744 to 171,080 edges, 3,209 moved, 1,398 added, 62 lost.
  Moved: typed receivers onto the class their file names (271 `$v->fails()`
  from the Validator contract to the `new Validator`, 142 event dispatches off
  `Bus\Dispatcher`), traits (`make`, `pluck`). 48 of the 62 lost are
  `parent::__construct` above Exception reaching an unrelated trait; 7 were true
  only through a namesake (`$user->getKey()` on a `User` the file can't name).
- drupal-core (REG's sparse copy, no .git): 38,099 to 39,053, 381 moved, 1,066
  added, 112 lost. 52 `HandlerBase` edges go. 10 are left: 9 `$url->access()`
  on the one `Url` left when core/lib/Drupal/Core/Url.php is outside the sparse
  checkout (with that file back they reach `Url::access`), and a `$view->access()`
  inside `class View` itself. 834 `parent::` and
  `$this->` calls link through classes with attributes above them; 353 calls off
  unrelated traits (`BlockPluginTrait::__construct`) move or go.
- bookstack 045c6a98: 6 moved, 7 added. koel da0e70f4: 53 moved, 30 added.
- ospos, ci3, invoiceplane, horizon, controls gin and flask: byte-identical.
- Read against source: 125 sampled entries, 0 false added. Index CPU on framework,
  median of 5 interleaved: 47.9 to 48.1 s (+0.5%).
- REG: all 16 PUB entries for framework and drupal-core now hold.

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

Since colbymchenry#1935 a receiver named after a class took what its supertypes declare,
read by bare names: `class Human < ActiveRecord::Base` asked every class
called `Base`, so rails' `human.name` reached Journey's `Node#name`, and
`Topic.create` Digestor's `Node.create`. A value of a known class walked
`extends` edges the same way, so `company.has_attribute?` took the
`has_attribute?` of a `ClassMethods` module. And `class Mapper; include
HttpHelpers` lost `mapper.get`, which 1.6.2 reached by a substring of the
qualified name.

The ancestors are now read as Ruby reads them: superclass and mixins found
by constant lookup, a class's own body only, the module mixed in last asked
first, and for an instance no module it `extend`s.
- A receiver named after a class takes the class constant lookup finds, and
  past that class's own methods a word picks no namesake.
- A call on a class reaches a superclass's `def self.`, what it `extend`s and
  the `ClassMethods` its concerns bring, never a mixin's instance method or
  a mixin's own `def self.`.
- A bare name that is a method of the class around the call, taking no
  arguments and handing back what one class makes (`@jar ||= Jar.build(…)`),
  types the call on it, so `cookies.signed` reaches `ChainedCookieJars`.
- A `?` or `!` method of Ruby's core classes needs a receiver named after the
  owner itself: sinatra's `bad_cookies.empty?` is no `Cookies::Jar#empty?`.

Validation, base 4499d00 against this change (numbers in the PR):
- rails-src 793d744c: 171,738 to 179,247 edges, 2,159 moved, 8,179 added, 670
  lost. Added: Concern class methods (`Topic.find`), inherited instance methods
  (`topic.id`), helpers on bare calls (`concat`). Moved: to the first method in
  Ruby's order, off `Digestor::Node.create`. 86 sampled entries read: 0 false added.
  Lost: 442 `Node::name`-style false edges of the 670; about 30 were true by luck.
- sinatra cb22afd7: 2 lost, both `bad_cookies.empty?`. mastodon (Ruby only)
  b0555670: 1 moved, 34 added, 1 lost. jekyll 97c3bdc8: same edges, 9 guesses
  now typed. Controls gin and flask byte-identical.
- REG: rails-src 3 lost back; `company.has_attribute?` 14 of 14 on the instance
  method; rails-src/moved/24 (`table.foreign_key`, right by a substring) stays:
  it needs call-site flow, and the substring rule took 429 `error.message`.
- Index CPU on rails-src, median of 5 interleaved: 74.9 to 73.2 s.

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

Regression 4 of the comparison with 1.6.2, from cbd9817 (PR 1736). That
commit taught the statement-macro blank to take names in capitals, so that
`ATOMIC_BLOCK(x) {` stops reading as a K&R function. The blank takes the whole
call, name included, so a function no longer named the macro it uses and its
value reference went too: redis lost 10 (`TEST("…") {`, `TEST_CONTEXT`,
`TEST_DESC`) and rocksdb 16 (`XXH_ASSERT(c)`, which xxhash writes with no `;`).
A change to the macro missed those functions.

In a statement of a function body the name now stays, as `name;`, in the
bytes the call took. Anywhere else a `;` would be the error the blank avoids:
in an initializer, a struct or enum body, another macro's arguments, the rest
of an expression, or as the body of an `if`, `for`, `else` or `do`, where it
would part the block from its `else` or `while`. Those go whole, as before.
cStatementLines reads which lines those are from the file's code, once, and
only for a file with such a macro. It is length-preserving and in the
preParse, so the kernel gets the same bytes. The arguments stay blanked: a
macro's can be types, which a call would not parse, and no call or reference
lost against 1.6.2 on redis or rocksdb was in one.

Lowercase iterator macros take the same path, so `nk_foreach` in glfw's
nuklear.h now counts as used too.

Validation against main 4499d00:
- Tests: 7 of 9 fail on main, all pass. 11 mutations each fail a test. Three
  older tests now expect the name to stay.
- redis: the 10 are back, nothing lost or moved. rocksdb: 15 of the 16 are
  back on the same functions. The 16th sat on `XXH3_ACCUMULATE_TEMPLATE(scalar)`,
  a K&R misreading on both arms whose body is `XXH3_scalarScrambleRound`, which
  neither arm has as a node; REG read that entry as neither arm's. Two more
  such phantoms come back with their 4 calls, and 20 locals that had spilled
  to file scope on main are gone.
- glfw +1 (true). jq, leveldb, gin, flask and betaflight: nothing from this
  commit.
- Parse errors over every C file of redis, jq, glfw, betaflight and linux-sub,
  10,220 files: 0 worse, 0 better, error counts identical.
- Kernel parity, 0 diffs: redis 639/796 (the deferrals main has), jq 66/81,
  glfw 69/97, rocksdb db+util 381/463, betaflight src 1177/1465.
- Cost: C preParse over linux-sub 6.1 to 6.6 s, over redis 1.0 to 1.05 s.
  cStatementLines is 42 ms of a redis index's profile.

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

redis-cli.c and redis-benchmark.c include hiredis's sdscompat.h, which has
`#define sds hisds`. Their 24 `sds` members were linked to src/sds.h's
namesake typedef, a false edge new in PR 3144: the parser sees `sds`, and a
macro with no parameters has no node.

matchCppDeclaredType now asks what the name is renamed to where it is
written. Headers are read once per resolver, by a search for `define`, into
an index by macro name, so a name nothing renames costs one lookup. A rename
counts as the preprocessor applies it: the translation unit from the asking
file must have certainly defined the macro by that line
(isCppMacroDefinedAt, the existing include walk following one object-like
macro). So not above the `#include` or the `#define`, not under a flag no
file sets, and not when two arms disagree. A new name that is no project type
leaves the name as written. The scan stops at 16 MB of headers per resolver:
betaflight has 109 MB, nearly all vendor register maps.

The same rule corrects two more that nobody had read:
- redis's core sets REDISMODULE_CORE, so `RedisModuleString` is `robj` in
  module.c, 19 edges. A module that includes the same header keeps the
  typedef, as does the header read on its own.
- jq's `#define YYLTYPE location`: bison's struct is compiled out, 3 edges.

Validation against main 4499d00:
- Tests: 5 of 7 fail on main, all pass. 9 mutations each fail a test.
- redis 43 edges moved, 24 to `hisds` and 19 to `robj`, each checked against
  the includes and the flag; nothing lost. jq 3 moved, right.
- rocksdb, leveldb, glfw, betaflight, gin, flask: nothing from this commit.
- No extraction change. Cost: a resolver's scan is 26 ms on redis and 81 ms
  on rocksdb, once per worker; 49 ms of a redis index's profile in all.

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

PR 2394 removed the false `extends` that linked a struct to its members'
types, and PR 3144 put `references` in its place for C and C++ only. An
Objective-C struct stayed unlinked, so impact on a type missed what holds
one: on GNUstep's libs-base 1.6.2 had 153 such relations and main 0. No
benchmark repo has Objective-C.

Extraction writes the same member and alias references for `objc`, in the
wasm extractor, which is all Objective-C has. Resolution is name matching,
which already knows Objective-C's rules (a type an `.m` defines is its own),
with two limits in the spirit of the C++ lookup: only a type can be named
there, so a function or a variable of the name is no candidate and no other
strategy sees the ref; and a class's node in some other `.m`, an
`@implementation` or a category, yields to a header's, the class's own first.
GNUstep's `NSString` has 14 class nodes, in test files too. Explore treats
these edges as it does C's: no window per member, and out of the rank walk.

A header that reads as C and names an Objective-C type stays unlinked, as on
1.6.2: `id` in ObjFW's ObjFWRT.h, 18 refs over the five repos.

Validation against main 4499d00:
- Tests: all 6 fail on main, and the explore test on the tools.ts line alone;
  all pass. 4 mutations each fail a test.
- libs-base +421, nothing lost or moved. 140 of 1.6.2's 153 hold at the same
  site with the same type: 50 on the same node, 90 on a header's where 1.6.2
  had a category in an `.m`. The other 13 are members of unnamed structs in
  test functions, which main names (`main::vector`), and they link there too.
- ObjFW +114, FLEX +38, SDWebImage +29, hammerspoon +20, nothing lost or moved.
- 74 read against source over the five repos, every one a true relation. A
  framework class lands on a category of it where the project has one, as a
  method's type reference does; GNUstep's own `@interface NSString` is no node
  on either arm.
- `impact pause_event` on the fixture: 2 symbols on 1.6.2, 1 on main, 4 here.
- rocksdb, redis, leveldb, gin, flask: nothing from this commit.
- No kernel change: Objective-C isn't routed to it. The check reads the
  holder's kind from its id, so a method's reference costs no node read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t's one method (colbymchenry#3175)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the base from upstream 10e316d to d2e0389 (main, 2026-11-01, 354
commits). All five patches stay. Patches 1-4 merge back unchanged;
patch 5 (#20) conflicted by position with upstream's new
blankCUnbalancedConditionalBranches in c-cpp.ts, and both passes now run,
#20's first. The #20 test is renamed to match the other patch tests, and
FORK.md records the new base and the fifth patch.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Dshuishui Dshuishui changed the title Sync to upstream d2e0389, and release as 1.6.2-fmagent.3 chore(sync): merge upstream d2e0389 and release 1.6.2-fmagent.3 Nov 1, 2026
@Dshuishui
Dshuishui merged commit 2c42934 into main Nov 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants