Commit Graph
1012 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 6500a3782b fix(cfml): a <cfquery> or <cfscript> block that fails to parse no longer flags the file (#2843)
Both delegates in cfml-extractor.ts read a block with its own grammar
through a fresh TreeSitterExtractor and copied its errors onto the file.
That extractor warns "parse produced no symbols (tree has errors) - the
file is indexed but contributes nothing to the graph" when its tree has
errors and it found no symbols: true of the block, not of the file, and a
`<cfquery>` body never declares a symbol. So `codegraph init` printed the
warning and `codegraph status` counted the file among those whose symbols
are missing, though its component, methods and calls were indexed.
extractTagExpressions already drops the warning for the same reason.

Fix: the delegates record that warning as one naming the block and its
line, with code partial_parse, so `codegraph files --json` shows which
block failed while init and status, which key on parse_error, no longer
report the file. Other errors pass through unchanged.

The issue's own example SQL parses cleanly with the bundled cfquery
grammar, so the reporter's trigger was not reproduced; any query body the
grammar can't read (here `EQV` in a <cfif> condition) and any
statement-only <cfscript> block with a syntax error showed the same
warning.

Validation:
- New cfml-embedded-parse-warning.test.ts: the repro DAO keeps its class,
  both methods and the call inside the failing query, and records one
  partial_parse warning naming <cfquery> at line 6; a failing <cfscript>
  block likewise; getIndexHealth lists neither file. All 3 fail on main's
  extractor and pass. status-index-health.test.ts passes.
- On the repro, main prints the whole-file warning during init and status
  says 1 file could not be parsed so its symbols are missing; with the fix
  neither does.
- A/B vs main f406fe00: FW/1, BlogCFC, ColdBox, ContentBox, Preside and
  CFWheels unchanged; in Wheels' vendor/wheels core (indexed as its own
  project) one file changed, tests/html.cfm, whose <cfscript> block at line
  8 fails: its warning now names that block. Nodes, edges and references
  are identical in every repo. express and flask controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 57.1 / 54.5 s, Wheels core 33.2 / 32.8 s. Within noise.

Fixes #2654

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:32:36 +00:00
Colby MchenryandClaude Opus 5.5 f406fe0064 fix(cfml): createObject("component", path) links its component, and .init() on it (#2841)
`new a.b.Batch()` made an instantiates edge, but
`createObject("component", "a.b.Batch")`, the form older CFML uses most,
was extracted as a call to the engine function createObject, which never
resolves. A call chained onto either construction, `createObject(...).init()`
or `new a.b.Batch().init()`, was extracted as a bare `init`, which the
name matcher bound to whichever component's init came first (0.4), or to
none when there are many.

Fix:
- cfscript extraction (tag expressions included): a component
  createObject, with two arguments, one, or named ones, makes an
  instantiates reference under the dotted path as written, not a call.
  Java and other object types, and names built at runtime, are unchanged.
- A call on a construction is extracted as `<component>().<method>`.
- The resolver reads a dotted instantiates path with
  resolveCfmlComponentPath, the rules extends uses, and resolves a chained
  call to the method of that component or one it inherits (matched
  ignoring case; inheritance is wired before calls). A bare component name
  is the one in the calling file's folder, else the only one so named.
  Without both, the call gets no edge rather than a guess by name.
- resolveCfmlComponentPath also accepts a component at the project root
  for a path with one folder segment, its file named like it:
  `wheels.JobWorker` in Wheels' own source, where `wheels` maps onto the
  root. Without it Wheels' core lost calls like
  `new wheels.JobWorker().processNext()` that name matching had found. It
  ranks below any folder match; extends gains the same.

Validation:
- New cfml-create-object.test.ts (5 cases) and a root-component case in
  cfml-inheritance-resolution.test.ts: 5 of the 6 fail on main (the sixth
  pins the variable typing that already worked), all pass.
- A/B vs main d90f6abe on FW/1, BlogCFC, ColdBox, ContentBox, Preside,
  CFWheels and its vendor/wheels core indexed as its own project:
  1,022 edges removed: 563 resolved again to the same target by type,
  444 moved from a method guessed by name to the constructed component's
  own or inherited method, and 15 dropped, all name guesses for
  components outside the index, without the method, or tied. 1,342 added:
  641 instantiates, 38 calls, and in Wheels' core 653 extends and 10
  implements to root components. Oracles over the fix graphs: of the 983
  chained-construction call edges, 965 land in the file named like the
  component and 18 on a method its supertypes declare (each checked); all
  608 dotted instantiates and, in Wheels' core, all 786 dotted
  extends/implements edges target a file named like the path's last
  segment. One site is doubtful: Wheels' core public/docs/core.cfm names
  the application's app.models.Model, which that subtree lacks; one
  `models` folder corroborates a test asset's Model.cfc, as it would for
  extends. express and flask controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 58.7 / 54.5 s, Wheels core 32.2 / 33.0 s. Within noise.

Fixes #2652

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:49:44 +00:00
Colby MchenryandClaude Opus 5.5 d90f6abec7 fix(cfml): <cfinclude template> and cfinclude() link the code to the file they include (#2838)
Only cfscript's `include "x.cfm";`, written outside any function, linked
the including code to the template. CfmlExtractor never read
`<cfinclude template="...">`, which the grammar parses as a
cf_selfclose_tag whose name is only in the text after `<cf`, so the tag
form made nothing anywhere. cfscript's include hook read only the string
form, `cfinclude(template="x.cfm")` went through the call extractor as a
call to `cfinclude`, and the function-body walker never extracts import
statements, so an `include` in a function was dropped. Resolution went
through the generic pipeline: the file-name matcher links a relative path
like `../shared/helpers.cfm` only when no other file shares its name, and
past it the symbol strategies could take another template's import node
of the same path, or the including file itself.

Fix:
- `<cfinclude template="...">` is extracted like the script form: an
  import node in the scope it sits in (function, component, or the
  template's file) and an imports reference from that scope.
  `cfinclude(...)` and `include template="...";` make the same import and
  no call, and an `include` in a function body counts, from the function.
- A path built at runtime (`#dir#/x.cfm`) is no import, in every form.
- A CFML include of a template path resolves as CFML does: relative to the
  including file's directory, or from the project root for a leading `/`
  (less sure, 0.85), to the file at that path, ignoring case when one file
  matches. A path that names no file there goes to the file-name matcher,
  as before, but only ever to a file, and never to the including file.

Validation:
- New cfml-include.test.ts: the tag form at component level (same folder,
  ../, upper case, unquoted), in a function body inside <cfif>, in a
  template and from the web root; the script forms; runtime-built paths,
  cfinclude(template=variables.x) and <cfmodule> (no import); a relative
  path with a namesake elsewhere; an unmatched path that another template
  also includes, and a path whose only namesake is the includer. 5 of 7
  fail on main, all 7 pass.
- A/B vs main 51c4124d (CFML has no kernel path) on FW/1, BlogCFC,
  ColdBox, ContentBox, Preside, CFWheels and its vendor/wheels core as a
  project of its own: 127 new imports edges and 152 import nodes (36
  more resolved again to the same file, more surely); 9 import nodes of
  runtime-built paths gone; 1 edge gone, CFWheels' starter-app
  views/wheels/wheels.cfm including itself. No edge moved to another
  target. Every CFML imports edge in the fix graphs (225) targets a file
  named like the include path, and every relative path that stays inside
  the project reaches exactly that file but one: Wheels' Public.cfc
  including views/routetesterprocess.cfm, no file beside it, matched by
  name to public/views/routetesterprocess.cfm as before. express and flask
  controls byte-identical.
- Index time, median of 4 interleaved runs, CPU with workers, main vs
  fix: Preside 52.6 / 50.8 s, Wheels core 28.2 / 29.5 s. Within noise.

Refs #2651 (part 1; part 2 is a feature request and stays open)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:59:26 +00:00
Colby MchenryandClaude Opus 5.5 cf3e6bc525 fix(ts): a type-only re-export like export type * from './types' now parses (#2837)
TypeScript 5.0's type-only re-export of a whole module, `export type * from
'./x'` and `export type * as ns from './x'`, has no rule in the vendored
tree-sitter-typescript grammar (v0.23.2, the revision the kernel's crate
compiles) or in upstream master: `type` parses as an ERROR inside the export
statement. The graph was already right, since the import resolver reads the
statement from the file (PRs #1955 and #2526), but the error tree meant a
barrel of re-exports got the "parse produced no symbols" warning and a
parse_error row, `codegraph status` listed it among the files it could not
parse, and the native kernel deferred every file with such a line to wasm.

The TypeScript extractor's preParse (.ts and .tsx) now blanks that `type` to
four spaces where a statement starts (a line start, or after `;`, `{`, `}` or
a block comment) when `*` is followed by `from` or `as`, so the line reads as
the `export *` it is to the graph. Offsets are kept. The kernel route applies
the preParse before calling the kernel, so both engines parse the same bytes
and no Rust change is needed. A grammar patch would mean vendoring and
regenerating the grammar for both dialects, two wasm rebuilds and a kernel
off the crates.io crate, for a statement `export *` already models. A
literal pre-check skips other files: 16 ms over strapi's 4,659 TS files.

The only node change is an import node's stored statement text, which now
reads `export      * from './x'`. Import nodes are filtered from search,
explore and the context builder, and nothing in resolution reads it.

status-index-health.test.ts used `export type * from` as its unparseable
file (issue #2336); it now uses a file with merge-conflict markers.

Validation:
- New ts-export-type-star.test.ts, 15 tests: 13 fail with main's
  typescript.ts, 11 with the blank unwired, all pass with the fix (kernel
  and wasm, .ts and .tsx, LF and CRLF).
- A/B, kernel engine, main vs fix, dumps by natural keys: masto.js,
  kysely, TanStack/query, graffle and strapi keep every edge and ref
  (162,421 and 192,248 on strapi). The only changes are 883 import
  signatures, one per `export type *` statement, and 76 cleared warnings
  (masto.js 11, kysely 1, query 4, graffle 28, strapi 32). The Go and
  Python controls gin and requests are byte-identical.
- Kernel parity, typescript and tsx: 0 diffs on every sweep. Deferrals
  drop masto.js 11 to 0, graffle 203 to 13, strapi 105 to 69, query 32
  to 27, kysely 14 to 12; the rest are other grammar gaps.
- Index time, median of 3 interleaved: graffle 12.7 s vs 13.1 s, query
  11.2 s vs 11.4 s, within noise.

Fixes #2474

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:09:05 +00:00
Colby MchenryandClaude Opus 5.5 51c4124dad fix(cfml): a dotted extends finds its component when a test suite mirrors its path (#2835)
A CFML component that extends another by its dotted path, like
extends="contenscms.core.basecore", got no extends edge when a test
suite held a file of the same name in a folder of the same name, such as
a TestBox spec at tests/testbox/tests/specs/unit/core/basecore.cfc.
resolveCfmlComponentPath counts the path's folders that match a
candidate's, from the right. contenscms is a server mapping onto
contens/, so only core matched, for the component and the spec alike,
and a tie gives no edge. The rule the resolver applies elsewhere since
PR #2209, that production code never names a symbol in a test suite,
never ran on this path: it returns before the name matcher's visibility
gate.

Fix:
- A component in a test suite is no candidate for a reference from
  production code (isTestSuitePath, now exported from the name matcher).
  This also keeps application code from extending a test file that
  happens to match more folders.
- A test's tie between a component and a suite file that mirrors its
  path goes to the component outside the test suites. Two production
  components still tie.

Validation:
- cfml-inheritance-resolution.test.ts: 2 new cases (the issue's tree in
  script and tag form, a test extending the component, a mirror that
  matches more folders, a path only a test suite holds, a test naming its
  own suite) fail on main's resolver; all 12 pass with the fix.
- The issue's repro: page and tagpage now extend contens/core/basecore.cfc
  (qualified-name 0.9); on main both stayed unresolved.
- A/B vs main 8fdc8a1e (CFML has no kernel path): FW/1 (305 CFML files),
  BlogCFC (167), ColdBox (668), ContentBox (726), Preside (1,791) and
  CFWheels (the repo, and its vendor/wheels core of 1,528 CFML files as a
  project of its own): no node, edge, reference or file error changed.
  None has a component and a test-suite file that tie on a dotted path.
  express and flask controls: full dumps byte-identical (kernel path).
- Index time, median of 3 interleaved runs, CPU with workers, main vs
  fix: Preside 63.6 / 60.4 s, Wheels core 35.5 / 32.7 s. Within noise
  (main alone ranged 59-67 s and 35-38 s).

Fixes #2650

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:47:14 +00:00
Colby MchenryandClaude Opus 5.5 16a8e77c46 fix(ruby): a call reaches the class method or the instance method its receiver calls (#2834)
A Ruby class method (`def self.build`, `def Widget.build`, or a def inside
`class << self`) was indexed exactly like an instance method: is_static 0,
under the same qualified name as an instance method of that name
(Widget::build twice). So every call took whichever of the two the class
declared first. Widget.build could reach `def build`, and widget.build on an
instance could reach `def self.build`. In the repro for issue 2503,
`card = Billing::ChargeCard.new(...); card.call` linked to `def self.call`
rather than `def call`.

Fix:
- Extraction, both engines: the Ruby isStatic hook (isRubyClassMethod in
  languages/ruby.ts, is_class_method in codegraph-kernel/src/ruby.rs) marks a
  singleton_method, or a method whose parent past one body_statement is a
  singleton_class, as static. Every other def is marked not static, the same
  way on both, so the graph only changes for class methods.
- Resolution, Ruby only, where the call says which kind it is:
  rubyMethodsOfKindFirst orders a call's candidates with the wanted kind
  first. A call on a constant (Strategy 1, its inherited walk, and the
  namespaced path in ruby-constant-paths.ts) wants the class method. A
  receiver typed by `x = Widget.new` (resolveMethodOnType) wants the
  instance method. A bare call splits only a real pair, one qualified name
  among its candidates as both kinds, keeping the caller's kind: a block
  may run on another object, so nothing else moves. A receiver known only
  by its name (Strategies 2 and 3) says nothing of its kind and is left as
  it was: rubocop's `cop.cop_name` runs over cop classes. With only one
  kind present, every path picks what it did before.

Validation:
- New __tests__/ruby-singleton-methods.test.ts (6 cases, each on the
  kernel and on wasm) and a parity case in kernel-ruby-parity.test.ts. On
  main 9 of the 18 tests in the two files fail, every one that checks the
  new behaviour; the guards and the old parity cases pass. All pass with
  the fix.
- A/B against main f7820b58 (kernel path): nodes are unchanged except
  is_static going 0 to 1 on the class methods, 167 on sinatra, 358 on
  rubocop and 266 on mastodon's Ruby tree. Edges: none added or removed.
  20 changed their target or confidence: sinatra 10, rubocop 0, mastodon
  10. 19 are right: 13 moved from the wrong twin to the right one, such as
  InlineRenderer.render to def self.render and sinatra's instance methods'
  force_encoding to def force_encoding, and 6 kept their target with
  higher confidence. One kept a target wrong in both arms: a call inside
  sinatra's define_method block. Controls express and flask are
  byte-identical.
- The A/B narrowed two rules. Preferring instance methods for a receiver
  known only by its name moved rubocop's 19 right cop.cop_name edges, and
  dropping other-kind methods from any bare call moved block-DSL calls on
  sinatra. Both are gone: only a real pair is split.
- kernel-parity.mjs --lang ruby is the same on both arms: sinatra 147/147,
  rubocop 1799/1801 (2 deferred), mastodon 3333/3333, 0 diffs. The fix
  arm's kernel and wasm graphs are byte-identical on sinatra and rubocop.
- Index time, median of 3 interleaved runs: sinatra -3.7%, rubocop +0.5%,
  mastodon +0.7%, controls express -2.7% and flask +1.0%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:22:15 +00:00
Colby MchenryandClaude Opus 5.5 f7820b584f fix(dart): calls to a private name in another part of the same library link again (#2832)
Dart keeps a `_name` to its library: the library file and every file its
`part` directives name. isVisibleAcrossFiles still listed Dart in
PRIVATE_IS_FILE_LOCAL (added with PR #1745 for Kotlin, Java, C#, Swift,
Scala and PHP), and both Dart extractors mark a `_` function, method or
class `private`. So once the name-matching pipeline had picked a private
candidate in another file, the pick was declined and the reference stayed
unresolved. A library split into parts, generated `.g.dart` and
`.freezed.dart` parts included, lost the calls and type uses between its
files.

Fix:
- dartPrivateVisible (dart-libraries.ts) says whether a file belongs to the
  reference's library, read from `part` / `part of` alone. Unlike
  inSameDartLibrary it needs no pubspec.yaml, since a part names its library
  by a path or a library name. When the reference's library can't be found,
  the name stays file-local, as before.
- isVisibleAcrossFiles takes Dart out of PRIVATE_IS_FILE_LOCAL and lets a
  private Dart candidate through when it is in the reference's library.
- matchByExactName drops another library's private Dart candidates before
  the ranking, as it does for C#, PHP, Java and Go names the file can't see.
  Otherwise another library's namesake can rank first, be declined, and take
  the library's own declaration down with it. Without a pubspec nothing else
  filters a top-level `_name`, and a member's class hierarchy is matched by
  type name, which can reach another library's class of the same name (the
  SDK's wasm and VM `_ImmutableLinkedHashMapMixin`).

Validation:
- New __tests__/dart-part-privacy.test.ts, run with and without a pubspec:
  the issue's library plus a receiver call, a generated part's
  `_$UserFromJson` and `_$Counter`, another library that imports the one
  declaring the names, a namesake beside a part, a part named by library
  name, and a part whose library is missing. 6 of 12 fail on main's
  resolver, 12 pass. With the pre-ranking filter disabled, 1 fails (the
  namesake without a pubspec).
- A/B vs main 58eb57a3, each arm with its own dist and kernel; the kernel
  and wasm paths give the same changed-site sets. Re-run after the rebases,
  on 5b47bc49 (kernel) and 8fdc8a1e (wasm), with the same changed sites.
  Edges added: getx (217 Dart files) 0, bloc (616) +49, riverpod (1,300)
  +542, localsend (393) +1, protobuf.dart (225) +143, apidash (954) +342,
  dart-lang/sdk sdk/lib (480, no pubspec) +207. Nothing removed or
  retargeted, and 1,064 edges changed only their confidence. Every newly
  resolved ref has its edge at the same site; nodes and files are
  unchanged.
- Triage of all 1,284 added edges: 1,274 right. An independent
  part/part-of oracle confirms 1,195 (a top-level `_name` unique in its
  library, a member of the call site's own hierarchy, or a member of the
  receiver's declared type); 79 were checked by hand. 10 are wrong, all
  receiver-name guesses (instance-method 0.65-0.8) that pick one
  implementation of an abstract member, or the first of a tie: the guess
  public names already get, which the file-local rule had hidden. The
  1,064 confidence changes are same-file targets that had another
  library's namesake beside them (exact-match 0.7 to 0.9).
- gin and moshi controls byte-identical, kernel and wasm. Kernel and wasm
  full-index dumps byte-identical on every Dart repo. kernel-parity --lang
  dart: 0 diffs on all seven repos.
- Index time, median of 3 interleaved runs on the kernel path, CPU with
  workers, main vs fix: riverpod 55.6 / 50.3 s, sdk/lib 59.4 / 59.6 s,
  apidash 29.5 / 30.8 s, bloc 18.6 / 18.3 s, protobuf.dart 14.7 / 14.3 s.
  Within noise.

Fixes #2466

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 07:18:05 +00:00
Colby MchenryandClaude Opus 5.5 8fdc8a1efb fix(rust): calls on field chains, struct literals and outside-type paths link by type (#2831)
Issue #2614 reported four Rust calls that linked to a same-named project
method. Case 1 (a slice's contains) was fixed on main by the typed-receiver
work. The other three still reproduced on main in both engines:
vis.map.get("a") on a HashMap field went to Ext::get, J::from(i) under
`use serde_json::Value as J` to a local `impl From<io::Error> for Fail`, and
Xyz { x: 1.0 }.to_val() to Stat::to_val.

Root cause. The extractor keeps one receiver level on a call, so a deeper
field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal reaches
the resolver as the bare method name, and the typed-receiver reading only
ran for x.m and self.field.m refs. The name strategies then took the one
project get, or one of the two to_val impls. J::from matched no qualified
name, J names no project type, and a single capital letter is read as a
possible generic parameter, so Strategy 3 took the crate's one from. The
same family: under an unrenamed `use serde_json::Value`, Value::default()
linked by qualified name to a project Value's impl.

Fix.
- matchRustWrittenReceiverCall, an exclusive hook beside the as_mut one:
  for a bare-named call, read the receiver from the text at the ref's
  column. Only a field chain rooted at a local, a parameter or self, or a
  struct literal (alone or in parentheses) is read; it is typed with
  rustValueType and the call resolved with rustTypedMethodCall. Any other
  receiver, an unreadable one, or a type no node names (one a macro
  declares) is left to the name strategies. A provided method of a project
  trait the type derives counts (axum-extra's #[derive(TypedPath)]).
- rustFieldValue reads an element of a tuple-typed field (self.pair.0).
- isRustPathCallReachable in gateRustScope: a call through a type path
  whose type the file's use, or the written path, names from std or a
  dependency reaches only the project's impl of a trait for that type, or
  for a trait's path the project's impls of that trait. A path dependency
  the project holds is the project's.

Validation.
- New __tests__/rust-written-receivers.test.ts (12 tests: the issue's
  crate verbatim, chain/literal/derive/path variants, guards): 9 fail on
  main's sources, all pass with the fix. The 27 Rust test files plus
  resolution and cross-language resolution pass.
- A/B against main 5b47bc49 (kernel on), changed call sites triaged as
  removed wrong / added right / wrong to right / same target now by type:
  ripgrep 30 / 18 / 10 / 37 and tokio 281 / 58 / 54 / 141 (every site
  read), axum 18 / 7 / 0 / 7 (every site read), rust-analyzer without
  its parser test data (951 files) 737 / 186 / 434 / 843 (every path
  removal checked by its use; every group sampled). No right link lost.
  Two wrong links added on rust-analyzer: a block-scoped glob
  (`const _: () = { use syntax::ast::*; }`) is read file-wide by the
  existing glob lookup, so a `String` field there reads as ast::String;
  typed locals already misread it the same way on main. Nodes identical
  on every repo; controls gin and express byte-identical.
- Kernel parity (Rust): 1226/1226 files on ripgrep, tokio and axum.
  Resolver-only change, so the kernel needs no mirror.
- Cost: ripgrep median 6.7 s to 6.5 s, tokio 20.2 s to 20.8 s (interleaved,
  median of 3, noisy box); a CPU profile puts about 0.3 s of tokio's 57 s
  busy CPU in the new code and the typed reading it calls.

Fixes #2614

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:25:23 +00:00
Colby MchenryandClaude Opus 5.5 dcb985f1d2 fix(ruby): a call on a namespaced class links its caller to the class and its method (#2827)
A Ruby call on a constant written with its namespace, like
Billing::ChargeCard.call(...) or ::Foo.call, recorded no edge. Both
extractors (the kernel's ruby.rs and the wasm one) emitted the receiver's
references ref only for a bare `constant` receiver, so a scope_resolution
receiver got none. The call ref itself, Billing::ChargeCard.call, matched
no resolver strategy either: the method-call patterns reject a `::` in the
receiver. A class used only through such calls showed no callers and no
dependents.

Fix:
- Both engines treat a constant path (a constant, or a scope_resolution of
  constants, a leading `::` included) as a constant receiver and emit the
  references ref under the receiver as written. A path through a value
  (obj::Thing, self.class::LIMIT) still emits none, and .new stays an
  instantiation.
- A new resolver step, src/resolution/ruby-constant-paths.ts, resolves a
  Ruby references ref written as such a path, and a calls ref on one, the
  way Ruby looks the constant up: the class or module around the
  reference, then each namespace outside it, then the top level; a
  `::`-rooted path at the top level only. A call reaches the class's or
  module's own method, else the first one its ancestry declares
  (rubyAncestry, now exported). The step runs before the pre-filter,
  which dropped `::`-rooted names, and it is exclusive: a path the project
  doesn't declare (ActiveRecord::Base) or a class without the method links
  nothing, never a namesake by its last name, and production code never
  reaches a class only a test declares.

Validation:
- New __tests__/ruby-namespaced-receiver.test.ts (8 cases, each on the
  kernel and on wasm, plus a sync case) and a parity case in
  kernel-ruby-parity.test.ts. On main 16 of the 22 tests in the two files
  fail (the rest are guards and the old parity cases); all pass with the
  fix. Reverting only the resolver step, or only the wasm extractor,
  turns 13 and 10 of them red.
- A/B against main (kernel path), every edge triaged: sinatra +23 edges,
  rubocop +168, mastodon's Ruby tree (4,363 files) +676; 0 removed, nodes
  identical. 866 of 867 are right: references by the path as written or
  under an enclosing namespace (each such case checked against the call
  site's own nesting), calls to a class method, a module_function method,
  or an inherited class method. 1 is wrong: Paperclip::Transcoder.make
  reaches the instance method `make`, since a singleton method and an
  instance method share one qualified name today; a bare Transcoder.make
  does the same on main. Controls express and flask are byte-identical.
- kernel-parity.mjs --lang ruby: sinatra 147/147, rubocop 1799/1801 (the
  same 2 deferred on main), mastodon 3333/3333, 0 diffs. The fix arm's
  kernel and wasm graphs are byte-identical on sinatra and rubocop.
- Index time, median of 3 interleaved runs: +0.4% to +1.7%, the same as
  the controls (+0.8%, +1.3%).

Fixes #2503

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:20:21 +00:00
Colby MchenryandClaude Opus 5.5 5b47bc4963 fix(js,ts): .tsx and .jsx classes link their overrides as .ts and .js classes do (#2828)
The interface-impl pass links a base class's or an interface's method
to each subclass's override, so a call through the base reaches the
method that runs. It took a class's methods, and its supertype, by
language: typescript and javascript were in its language set, and tsx
and jsx were not. A class in a .tsx or .jsx file got no override
edges, and neither did a .ts class whose base or interface was in a
.tsx file. The pass's own gate, the static/instance split of #2761
and the other JS-family lists already count tsx and jsx, and the
set's history (#494, #708) gives no reason for leaving them out.

tsx and jsx are now in the set, so these classes get the edges that
.ts and .js classes get, statics paired with statics.

Validation: the new test fails 4 of 4 on main 013b91c1 and passes 4
of 4 on the fix, and dropping tsx or jsx alone from the set fails the
tests for it. A/B against main on 11 repos: outline gains 262 override
edges in 42 base-subclass pairs (26 emails overriding BaseEmail's
subject, render and preview; editor nodes, marks and extensions
overriding Node, Mark and Extension) and lexical 27 in 12, grouped by
pair and sampled against the source, none pairing a static with an
instance method. Nodes and all other edges are unchanged. excalidraw,
social-app, insomnia, two .jsx apps and the controls (three, webpack,
flask, gin) are byte-identical: their .tsx classes extend React's or
other packages' classes. explore on outline now leads with the flow
BaseEmail.send -> render -> WelcomeEmail.render, where main finds
none. A sync that adds an interface method, adds an override and
drops a subclass's base matches a fresh index. The pass takes 65-108
ms on both arms, and index time, median of 3 interleaved runs on
lexical, outline, excalidraw and social-app, is +1.7% in total, noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:41:00 +00:00
Colby MchenryandClaude Opus 5.5 58eb57a324 fix(nuxt): navigateTo('/x') links to the page it opens, as router.push does (#2826)
Nuxt's navigateTo was in the Vue resolver's list of Nuxt auto-imports,
which answers each with a sink: an edge from the calling node to itself
at confidence 1. The Vue resolver runs before Vue Router's, so the call's
destination was never read. Every navigateTo, in pages and route
middleware alike, became a self-pointing calls edge, and no navigates
edge was drawn; the sink also kept sync from revisiting the call when a
page appeared (a navigation waits as a failed reference).

Fix:
- navigateTo leaves the auto-import list, so Vue Router reads it.
- A new optional hook, FrameworkResolver.ownsReference: a reference a
  framework answers alone stops at the frameworks' answer, and no import
  or name strategy guesses past it. nuxtResolver owns a bare navigateTo
  that no declaration or import in the file binds. One whose destination
  is computed, names no page or is an external URL stays unresolved,
  rather than linked to a project function that only shares the name
  (elk declares a navigateTo of its own inside a plugin). A file that
  declares or imports its own navigateTo calls that one, as before.
- Vue Router's call reader keeps only destinations that start with "/",
  as its markup reader does: navigateTo(url, { external: true }) leaves
  the app, and the shared reader had matched an absolute URL's path.
- framework-coverage.md: the Vue Router / Nuxt row and a new trap 21.

Validation:
- nuxt-navigate-to.test.ts (6 tests) and a Nuxt case in
  sync-navigation-retry.test.ts (the page appears later; sync equals a
  fresh index). All 7 fail on main. Without the ownership check the
  unresolved case fails (a composable's call reaches a namesake); without
  the "/" rule the external URL navigates to "/".
- A/B against main 9ee6718a (and before that 628f69e2, with the same
  deltas), kernel on, natural-key dumps and a site-keyed edge diff. Every
  changed edge is a navigateTo call site; no node changed. Across
  atidone, elk, nuxt.com, mealie and nuxt/ui, 54 navigateTo calls
  (elk's plugin with its own navigateTo aside, unchanged) lost their
  self-loop; 41 now navigate (42 edges, each checked against its source
  line, all correct) and 13 are unresolved: 6 computed, an API endpoint
  opened with external: true, 4 pages of a Nuxt layer (its own route
  table), and 2 pages the matcher can't read yet (a catch-all and an
  optional parameter).
- Byte-identical: nuxt/movies; vue-vben-admin and vue-element-plus-admin,
  plain Vue apps that declare their own navigateTo; the controls
  svelte-realworld (Svelte) and gin (Go).
- Index time unchanged within noise, medians of interleaved runs, main
  then fix: nuxt.com 5.21 s and 5.24 s (5 runs), mealie 11.52 s and
  11.60 s, elk 3.38 s and 3.55 s (3 runs).

Fixes #2634

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:01:39 +00:00
Colby MchenryandClaude Opus 5.5 9ee6718ad1 fix(vue): a <script lang="tsx"> block is read with the TSX grammar (#2824)
A Vue component's <script lang="tsx"> went to the JavaScript grammar:
the Vue extractor knew only lang="ts" and "typescript". That grammar
stops at the first type. A script holding only
withDefaults(defineProps<{ title?: string }>(), ...) was reported as
"parse produced no symbols", and a script with any other code failed the
same way without a warning, because the warning fires only when nothing
survives: its interfaces and type aliases were missing, a typed arrow
function was indexed as a constant, error recovery made nodes named after
a type or a keyword (a constant "CSSProperties", a function "if"), and
calls were credited to the component instead of the function they are in.

Fix:
- sfcScriptLanguage (sfc-script.ts) reads a block's grammar from its
  attributes: ts or typescript is TypeScript, a Vue tsx block is TSX, and
  anything else JavaScript, whose grammar reads lang="jsx" already. The
  Vue and Svelte extractors and the viewer's syntax regions all ask it, so
  the viewer highlights a component with the grammar it was indexed with.
- A Vue project preloads the TSX grammar (expandGrammarLanguages). It came
  along only with a .tsx or .js file in the project; without it a
  lang="tsx" file is never parsed, and nothing of it is stored.

Validation:
- vue-tsx-script.test.ts indexes a project of .vue files only. On main 4
  of its 5 tests fail; with only the preload reverted 3 fail (two because
  the files are not stored at all), with only the extractor reverted 2,
  with only the viewer reverted 1. 19 related test files, 879 tests, pass.
- A/B against main 628f69e2, kernel on, natural-key dumps and a site-keyed
  edge diff. Byte-identical: vue-vben-admin (2,014 files, no tsx blocks),
  elk and nuxt/movies, and the controls svelte-realworld and shadcn-svelte's
  registry (Svelte, which shares the new helper) and gin (Go).
- Changed only in lang="tsx" files, in node and edge counts added/removed:
  vue-element-plus-admin (3 such files) 0/1 and 1/3; a Vue and TS only
  folder of ant-design-x-vue's docs (91; TSX loads there only through the
  preload) 112/15 and 151/7; ant-design-x-vue (122) 143/27 and 293/26;
  a tiny-vue workspace subset (39) 26/59 and 119/93; element-plus (10)
  43/16 and 55/13. Every removal and retarget was read, the additions
  sampled: new nodes are the scripts' types, interfaces, typed constants
  and functions; removed ones are error-recovery debris, duplicates of
  Options API methods among them; moved calls now start in the function
  they are written in. ant-design-x-vue's src/bubble/loading.vue loses its
  "no symbols" warning.
- Index time unchanged within noise, medians of 3 interleaved runs, main
  then fix: element-plus 14.8 s and 13.5 s, ant-design-x-vue 2.94 s and
  2.95 s, the docs folder that now preloads TSX 1.83 s and 1.82 s.

Fixes #2570

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:37:25 +00:00
Colby MchenryandClaude Opus 5.5 013b91c1be fix(js,ts): a named class expression is a class with its own members (#2825)
A named class expression, like register(class Widget extends Base {})
or module.exports = class Router {}, was not indexed as a class, as
issue #2499 found. Its methods became free functions, its fields and
supertypes were dropped, and calls in it had no class to start from,
so its this-calls, super calls and own static calls went unresolved or
to a by-name guess, and calls elsewhere guessed into its methods.

Both engines now index a named class expression as a class, with its
methods, properties, extends and implements. An anonymous one is still
passed over. Its head (class Widget extends Base) is its signature,
which marks it as an expression, and it is never exported by itself.

JavaScript binds the name only inside the expression, and the resolver
now scopes it the same way. A reference written outside every class
expression resolves in a view of the graph that hides class
expressions and their members, so new Widget() elsewhere links
nothing, and a same-named class, function or binding keeps its links.
A name only class expressions carry is unknown there. A reference
inside one sees the whole graph, unless it names another class
expression. A class a CommonJS module exports, module.exports = class
Router {} or exports.Store = class Store {}, is the module's value,
which require hands to every importer, so it is not scoped: it reads
as a declaration does, and a require of it reaches its static methods
and its instances' methods. The reader of CommonJS named exports takes
exports.X = class Y {} too. Super calls in a class expression now
resolve through the super-call matcher.

Validation: the new test fails 11 of 13 on main d5fc1fbb and passes 13
of 13 on the fix, and a mutation of each of 7 parts (the view, the
inside check, the name rule, the expression marker, the class node,
the CommonJS export rule, the named-export reader) fails a test.
A/B against main d5fc1fbb on 22 repos (20 TS/JS, flask and gin): 193
classes are added in 10 repos with their members, and 801 edge sites
change in 7 repos, each bucketed and sampled by hand. 563 are jest's
calls to its global test(), 0.4 guesses that move from one wrong
namesake to another: the old one, a class expression's test() in an
e2e fixture, is now a method. Of the rest, 85 are new extends,
implements and type references of class expressions; 74 calls, news
and references move to the member they are written in; 65 calls and
news gain or change a target, inside class expressions (this-calls,
own static calls) and through require (static calls, typed instance
calls, new); and 14 guesses are removed, among them 3 super calls
whose parent the index can't name. 12 repos' dumps, the controls among
them, are byte-identical. Kernel parity holds on 22 repos. Three sync
scenarios match a fresh index. Index time, median of 5 interleaved
runs: lit +0.8%, webpack +2.7%, jest +0.6%; insomnia +12.8%, though it
indexes byte-identically, holds no class expression, and its resolver
time is the same on both arms, so that gap is other jobs' load.

Fixes #2499

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:16:07 +00:00
Colby MchenryandClaude Opus 5.5 23a80a847a fix(python): a from-import of a module with no dot in its name links the module's file (#2823)
In Python, `from loader import X` with loader.py at a package root left
the module reference `loader` unresolved, so the importing file had no
imports edge to loader.py. `import loader` and `from pkg.loader import X`
both link their module's file. The from-import's names (X) linked, but
`codegraph affected` and impact could miss the file's dependency on the
module. v1.6.2 behaves the same.

Root cause: resolvePythonAbsoluteModule resolves only dotted module refs.
It leaves bare names to resolveModuleImportToFile, because a bare
imported name like `authentication` in `from app.api.routes import
authentication` is ambiguous on its own. But resolveModuleImportToFile
matches an import by its local name, which for `from loader import X` is
X, never `loader`. So the module of an undotted from-import was never
looked up by its path. When the file also bound the module's name some
other way, the generic import loop resolved it through that binding:
next to `from django.core import mail`, the module of `from mail import
...` became django/core/mail/__init__.py. Django's tests/mail/__init__.py
is the module meant, and the statement's own names already resolve
there.

Fix: resolvePythonAbsoluteModule also takes the module a from-import
names (isPythonFromImportModule), dotted or not. It resolves it from the
package roots, as `import loader` is resolved. Imported names keep their
path, and a module from outside the project still links nothing.

Validation:
- New __tests__/python-from-import-module-file.test.ts, 7 tests: a
  root-level module, a top-level package, a src layout, a module named on
  a continuation line, a submodule imported by name, sync after the module
  file is added (matching a fresh index), and an outside module with a
  same-named nested file (links nothing). With main's import-resolver.ts
  the 6 positive tests fail and the guard passes. Seven assertions in
  python-module-resolution.test.ts listed a file's import edges exactly
  while the module edge was missing; they now include the module's file.
  What they test is unchanged: where the imported name binds, and no
  cycle.
- A/B against main d5fc1fbb. Both arms were built from their own sources
  with the kernel. Rebased onto 628f69e2 (PR #2822, Kotlin only), the fix
  arm's graphs are byte-identical to before the rebase:
  - requests: +2 edges;
  - sqlalchemy: +3,189;
  - django's 2,938 Python files: +103 edges and 2 retargeted;
  - every change is a from-import's module linking its file, and nothing
    else changed;
  - express and gin (controls): byte-identical.
  Triage of all 3,296 added edges:
  - 3,284 point at the file, or the package folder, where the same
    statement's imported names already resolve;
  - the other 12 were read by hand: multi-line statements, re-exported
    constants the name resolver doesn't follow, and a deliberately broken
    name in a Django test fixture, each on the right module file;
  - the 2 django retargets are the mail case above.
- Cost: index CPU medians of 3 interleaved runs, main vs fix: sqlalchemy
  100.7 s vs 100.3 s, django 98.4 s vs 97.8 s.

Found while verifying another Python issue in the sweep; no issue was
filed for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 02:38:35 +00:00
Colby MchenryandClaude Opus 5.5 628f69e275 fix(kotlin): repository().pending() links the method of the type repository() declares (#2822)
Issue #2805: in a Kotlin test class with `private fun repository():
RealRepo`, `repository().pending(1)` went to a test double's
`FakeRepo.pending` in another package, and `val repo = repository();
repo.pending(1)` to the interface `Repo.pending`. The extractor records a
call on an unqualified call's result by the outer method's bare name (a
capitalized inner, `Foo().bar()`, is kept as a chain), so the name matcher
took whichever `pending` came first; and the local's type was read only
from a constructor or an annotation, so the receiver's name guessed it.

The name matcher now reads such a call where it is written
(`repository().pending(1)`, `this.repository()…`, `nullable()?.pending()`)
and finds the inner function the way Kotlin does: a method of the calling
class, of a class it inherits from or one around it, read from their
declarations, else a top-level function the file can name. The call is
resolved on the type that function's signature declares, including what
the type inherits and extensions on it, or links nothing when the type
lacks the method: the #750 rule, as for `Foo.factory().bar()`. A local set
from such a call alone (`val repo = repository()`) takes that type for
the existing typed-local path. Edge cases the A/B surfaced:
- a nested type is the one the function names, `Request.Builder` or a
  plain `Builder` written in `Request`, matched by whole names
  (`MockResponse.Builder` is no `Response.Builder`);
- an extension is the one the calling file can name (Exposed declares
  `Expression<T>.year()` in three modules);
- a type from outside the project, a call inside an extension on one,
  and an extension the index keeps unattached (on a nullable receiver, or
  `fun Dns.Call.x()` filed under `Dns`) keep the name match as before.

Validation:
- New kotlin-call-result-receiver test (9 cases): 7 fail on main, the two
  that guard unchanged behavior pass on both; all pass with the fix on the
  kernel and on the wasm path. Mutation checks confirm the owner-path,
  scoped-nested-type, whole-segment and extension-visibility rules are
  each exercised.
- A/B on main e32021f6, calls by call site, nodes identical:
  okhttp c317930: 82 retargeted, 176 gained, 3 dropped, 516 confidence
  only; JetBrains/Exposed 09f814c: 152 retargeted, 138 gained, 1 dropped,
  1353 confidence only; nowinandroid 2982e63: 1 confidence only. Controls
  gin (Go), nest (TypeScript) and mybatis-3 (Java): byte-identical.
- Triage: retargets go from name guesses to the declared type
  (`request().post()` → `Request.Builder.post`, `varchar(…).index()` →
  `Column.index`, `rowNumber().over()` → `RowNumber.over`,
  `newReader().readInt()` → `Hpack.Reader.readInt`); gains are locals now
  typed (`val connection = connect(peer)`); the 4 drops are Kotlin's
  `apply` scope function, which went to a test helper's or a Gradle
  plugin's `apply`. Every retarget group was checked against the source.
- Cost: whole index, median of 3 interleaved: okhttp 10.5 s -> 10.9 s,
  Exposed 17.7 s -> 16.8 s, within noise.

Fixes #2805

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 02:12:42 +00:00
Colby MchenryandClaude Opus 5.5 d5fc1fbb21 fix(python): calls like reg.get() on a typed local or parameter reach the class's method (#2821)
In Python, a call of a method named like a builtin dict, list, string or
file method (get, update, items, pop, close and the rest of the
builtin-method list) was dropped by the builtin filter before any
strategy could read its receiver. Only a receiver whose capitalized name
is a project class, or an imported project module, got past it. So
`reg = SystemPackRegistry(); reg.get(k)`, `registry = load_registry()`
with `def load_registry() -> SystemPackRegistry`, and a parameter
`reg: SystemPackRegistry` gave no edge, while `self.reg.get()` linked
through the attribute reader of PR #1926.

A call the filter drops now gets one way back, by evidence only:
matchPythonTypedLocalCall. It reads the def that binds the receiver
where the call reads it (the calling def, or one it is nested in). It
types the name only when every binding there gives it the same project
class:
- a parameter gives what its annotation names ("T", Optional[T],
  T | None);
- an assignment gives its own annotation, or what its whole value
  makes: a constructor, or a factory typed by its return annotation or
  by its returns (the attribute reader's pythonValueEvidence and
  pythonFactoryType);
- `x = None` gives nothing either way;
- each class is the one the file's imports name (pythonClassNamed), so a
  class imported from outside the project is none of the project's;
- the method must be the class's own or a declared base's.

Everything else stays dropped: any other binding of the name (a loop,
with, except or comprehension target, a tuple or augmented assignment, a
walrus, an import, del, a nested def or class of the name, a nonlocal
write from a nested def), a value that names no class ({}, json.load(p),
Foo().data(), a union), a rebound callee, a module-level name. These are
the shapes the reporter's follow-up found making wrong edges with a
looser reader. Like an attribute call, the edge records the other files
its answer read (typeFrom), so a sync that edits the factory or a base
re-opens it.

Validation:
- New __tests__/python-typed-builtin-method-calls.test.ts, 20 tests:
  - the issue's three calls; annotated, quoted and Optional parameters;
    items, pop and close; a module-qualified constructor; typed, untyped
    and awaited factories; a None placeholder; a local annotation; a
    closure; typeFrom; two same-named classes; an inherited method;
  - the reporter's trap shapes, which stay unlinked;
  - three sync cases, each matching a fresh index.
  With main's source the 13 positive tests fail and the 7 trap tests
  pass.
- A/B against main 3f58e343. Both arms were built from their own sources
  with the kernel. Rebased onto e32021f6 (PR #2820, outside Python),
  the fix arm's graphs are byte-identical to before the rebase:
  - requests: +15 edges;
  - sqlalchemy: +52 edges;
  - django's 2,938 Python files: +73 edges;
  - 0 removed and no other change in any of them;
  - express and gin (controls): byte-identical.
  All 140 added edges were read at their source: each is a constructed
  local, a factory-typed local or an annotated parameter calling its own
  class's method, or one a declared base has. The issue's fixtures link
  all three calls, and the verification kit's nine trap call sites stay
  unresolved, kernel and wasm alike.
- Cost: a CPU profile puts the new reader at about 1.0 s on django and
  1.2 s on sqlalchemy, around 1% of index CPU. Index CPU medians of 3
  interleaved runs, main vs fix: sqlalchemy 98.6 s vs 98.5 s, django 97.9 s
  vs 99.1 s (single runs vary by several seconds on this shared
  machine).
- kernel-parity.mjs: 295/295 Python files at parity.

Fixes #2583

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 01:50:54 +00:00
Colby MchenryandClaude Opus 5.5 e32021f6fb fix(java,kotlin,csharp,scala): an overload links only the overrides with its parameters (#2820)
The override pass (interface-impl) grouped a class's methods by name and
linked every overload of an interface or base-class method to every
same-named method of the class. Issue #2663's ExampleService got four edges
for two overloads: putTairValue(String) also reached
putTairValue(List<String>, double), and the other way round, so flows,
callers and impact walked into overloads a call never runs. Java, Kotlin,
C# and Scala make an override repeat the parameter list of what it
overrides, and the stored Java and Kotlin signatures already told the
overloads apart; nothing compared them.

A new override-parameters module picks, of a class's same-named methods,
the ones that can override a base overload. The pass asks it only when a
name is overloaded on either side, so a name declared once on each side
links as before. A pair with another parameter count never links. Of those
with the base's count, the ones whose parameter types are written alike
win: qualifiers, annotations, final, default values and C#'s BCL names for
its keywords aside, varargs as arrays. When none is, the ones whose types
can't be told apart are kept: a type parameter of the base method or of a
type around it matches any type in its place (save(T) implemented as
save(String), List<T> as List<String>), a raw or nullable spelling of the
same type may be it, and a method written in another language than the
base is compared by count alone. An override's own type parameter matches
no concrete type of the base, as these languages rule. Java, Kotlin and
Scala lists come from the stored signatures; C# stores none, so its list
is read from the declaration past attributes, comments and string
literals, as the resolver's call-site overload choice already reads one.
Extraction is unchanged, so both engines give the same input.

Validation:
- New override-overload-pairing test (12 cases, Java, Kotlin, C#, Scala):
  11 fail on main, the single-method case passes on both; all pass with
  the fix on the kernel and on the wasm path.
- A/B on main df1cd1e1, override edges main -> fix; nodes and every other
  edge identical in each: hadoop-common-project (hadoop 2c01c36f)
  4651 -> 3762 (-1045 +156), guava 5765 -> 4360 (-1427 +22), mybatis-3
  1126 -> 898 (-278 +50), Exposed 999 -> 884 (-115), okhttp 709 -> 665
  (-44), Newtonsoft.Json 527 -> 404 (-125 +2), serilog 98 -> 91 (-7),
  twitter/util 838 -> 699 (-139). Controls gin (Go) and nest (TypeScript):
  graph dumps byte-identical.
- Triage: of the 3180 removed edges, 1761 pair methods with different
  parameter counts and 1419 methods whose concrete parameter types differ;
  every one of the 230 added edges is a same-parameter override the
  per-class cap of 40 had cut off. An independent re-read finds no
  same-typed override missing next to any removed pair, and each group of
  base methods that lost every link into a class was checked against the
  source (the class overrides other overloads only, or the method is final
  or static).
- Cost: the pass alone, median of 9 alternating runs, +3 to +96 ms per
  index (guava 715 -> 811 ms, hadoop-common 501 -> 555 ms); whole index,
  median of 3 interleaved: Newtonsoft.Json 12.3 s -> 11.9 s, okhttp
  12.3 s -> 12.9 s, within noise.

Fixes #2663

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:35:03 +00:00
Colby MchenryandClaude Opus 5.5 3f58e34383 fix(cli): index no longer blocks on a daemon lock naming a process that isn't CodeGraph (#2819)
codegraph index told you to run `codegraph daemon stop`, which the CLI
rejects ("too many arguments"), whenever it could not verify the process
named in .codegraph/daemon.pid. In that state `codegraph daemon` lists
nothing, because it lists only daemons that answer their socket. And
`codegraph unlock` kept an old-format lock whose process was alive (the
rule from issue #1834), so nothing the CLI offered got past it. The
writer-lock message gave the same wrong advice.

Root cause: a live pid in the lock was the only evidence, and a pid
proves nothing once the OS gives it to another program. Every CodeGraph
daemon since the first one (v0.9.5) runs
`<runtime> ... codegraph.js serve --mcp --path <root>`, so the holder's
command line settles the question.

Fix:
- New src/mcp/process-identity.ts reads a process's command line and
  executable name from the OS: Win32_Process through PowerShell (tasklist
  as fallback) on Windows, /proc on Linux, ps elsewhere. Each lookup is
  capped at 5 s and never throws. A holder is a CodeGraph MCP server,
  provably something else, or unknown, and unknown is never evidence.
- stopDaemonAt treats an unverified live holder that is provably not a
  CodeGraph server like a dead one. Its lock is cleared, under the same
  writer-slot claim, and the process is never signalled.
  clearStaleDaemonArtifacts (codegraph unlock) does the same for an
  old-format lock. A holder that may be a daemon keeps its lock, as
  before (issues #1553 and #1834).
- daemonStopFailureMessage builds index's error from what remains: the
  pid, what it runs, and a command that clears it (taskkill /PID N /F or
  kill N), or which lock file to delete when only that will do.
- writerLockHeldMessage names `codegraph daemon` instead of the missing
  `codegraph daemon stop`.

Validation (Windows 11):
- New daemon-lock-holder.test.ts (11 tests) and a daemon-registry test.
  With main's daemon-registry.ts, writer-lock.ts and codegraph.ts put
  back, 11 tests fail, including all 8 CLI cases and the writer-lock
  wording; with the fix all pass. Four existing tests used generic
  processes to stand in for a daemon that doesn't answer. Those stand-ins
  now carry a CodeGraph server's command line, and pass on main and here.
- Real CLI with a private HOME, 4 cases: an old-format lock naming a live
  PING.EXE; an old-format lock naming a live Node process that isn't
  CodeGraph; a current-format lock naming that kind of Node process with
  a pipe nobody listens on; an old-format lock naming a Node process whose
  command line reads `codegraph.js serve --mcp`. On main all 4 index runs
  exit 1 with the old advice, `daemon stop` is rejected, `daemon` lists
  nothing, and unlock clears only the third. With the fix the first 3
  rebuild (exit 0), each holder stays alive, and unlock clears the first
  too. The 4th exits 1, naming the pid and `taskkill /PID <pid> /F`;
  after that command, index exits 0.

Contributor PR #2373 (@danusha2345) changes the two strings only; I read
it for guidance.

Fixes #2770

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:32:31 +00:00
Colby MchenryandClaude Opus 5.5 b8c3c032be fix(bundle): bundle Node 24.22.1, the October security release, and verify the download (#2817)
Release bundles were set to ship Node 24.21.0. The release workflow runs
build-bundle.sh with no version, so the script's default decides the Node
every bundle carries, and that default predates Node's October 2026
security release. v24.22.1 (2026-10-20) fixes 8 CVEs, 4 rated High (TLS,
DNS, Brotli), and ships OpenSSL 3.5.9. It keeps V8 13.6.233.17, libuv
1.52.1 and module ABI 137, and it carries 24.20.0's fix for the Windows
abort at exit (issue #2617). The script also never checked the Node
download against the SHA-256 sums nodejs.org publishes.

Fix:
- The default and the usage example move to v24.22.1. The comment and a
  new test hold the default at 24.22.1 or later, on 24.x.
- Step 1 downloads the release's SHASUMS256.txt and refuses the archive,
  before unpacking anything, unless its SHA-256 matches the listed one.
  The zip and tar.gz downloads now share that one code path (sha256sum,
  or shasum on macOS).
- The unreleased CHANGELOG bullet for issue #2617 now names 24.22.1 and
  the check. BUNDLING.md describes the check.

Validation (Windows 11 x64):
- bundle-launcher.test.ts: 4 new tests (the version floor; a matching
  download is unpacked; a mismatched or unlisted one is refused before
  unpacking). With main's build-bundle.sh all 4 fail; with the fix the
  file has 7 passed and 3 POSIX-only skipped.
- build-bundle.sh win32-x64 with the new default: the check passed
  (sha256 d2046d28...8b95fa5, as listed). The bundled node.exe is
  byte-identical to the official v24.22.1 one and prints v24.22.1
  (OpenSSL 3.5.9). Its launchers from Git Bash, cmd and PowerShell, with
  no system Node on PATH, ran --version, init, status, query, callers and
  sync on a small TypeScript + Python fixture: all exit 0.
- Node's own reproducer for the exit abort, 300 runs per mode, 4 at a
  time: the bundled v24.22.1 exited cleanly 300/300 in all three modes.
  The official v24.16.0 control aborted 299/300 with 0xC0000409 when the
  child ran directly.
- 13 runtime-facing test files (sqlite, wasm flags, worker and stdin
  teardown, watcher, launcher, shim) run on the bundled node.exe: 136
  passed, 22 skipped, 0 failed.

Fixes #2780

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:32:10 +00:00
Colby MchenryandClaude Opus 5.5 df1cd1e185 fix(pascal): Delphi units with re-published properties and similar syntax parse whole (#2816)
SynEdit's main unit parsed into one ERROR node on main. A re-published
property, `property OwnsObjects;` at line 303, is one of several Delphi
constructs tree-sitter-pascal has no rule for. Each breaks the parse
locally, and enough of them cost a whole unit: routines after a break
lost their calls or had them credited to the file, their locals showed
up as unit variables, and class members after an unnamed record went
missing.

The Pascal pre-parse now fixes each construct in place, offsets and
newlines kept (signatures still read the source as written):
- a re-published property (`property Cursor default crIBeam;`) is blanked;
- a conditional right after a routine's header that holds no whole body
  (`{$IFDEF HASINLINE}inline;{$ENDIF}`) has its directives blanked, and
  so does one nested at the start of a conditional that holds no block;
- branches that open the same `begin` keep only the first one, and an
  assembler version that opens a body beside a Pascal one is blanked;
- a re-raise is blanked, or written as `;` right before an `end`;
- an unnamed record type (`fIn: record … end`) is written as `string`
  with its fields blanked, so the declaration keeps its name;
- a label right before `end` is blanked;
- alternative statements after a case label, or after then/else/do
  without a `;`, read as one block: the directives become begin, `;`
  and end, so every branch keeps its calls.

The last fix also undoes a regression the header fix caused alone:
in HeidiSQL's SynRegExpr.pas a case statement full of `{$IFDEF}`
alternatives broke the unit once main's accidental resync points were
gone (659 references lost). With all fixes it has 1,712 against
main's 1,681.

Also fixed on the way: a routine nested in another credited the
constants its parent's body reads to itself, when it was the last one
before the body (the value-ref scan pulled in the next sibling, which
is the outer routine's body).

Validation: pascal-delphi-syntax (17 tests, 13 fail on main) and a new
pascal-nested-routines case (fails on main); 14 mutations, one per rule
and three guards, each fail a test. Two pascal-system-calls fixtures
broke the parse on purpose with constructs this now reads; they use an
old-style `begin … end.` initialization and an anonymous record holding
a method instead, which still break it, and still fail when the guard
they cover is removed. Parse census, main -> fix, files
without a unit node: SynEdit 4 -> 1, HeidiSQL 6 -> 1, DelphiMVCFramework
15 -> 1, mORMot2 34 -> 6, DWScript 2100 -> 2086 (its test scripts have
no unit header). Indexed A/B, edges removed/added: SynEdit 1889/2150,
HeidiSQL 2571/3305, DelphiMVCFramework 4915/6227, mORMot2 15298/27195,
DWScript 3758/4570; leveldb and SDWebImage identical. No file loses
more call sites than it gains outside DelphiMVCFramework, where the
six that do lose only wrong edges. Index time within noise (HeidiSQL
+3% wall, +8% CPU for the code now read); the pre-parse itself takes
223 ms over mORMot2's 25 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:42:08 +00:00
Colby MchenryandClaude Opus 5.5 ab92c90a54 fix(js,ts): a super.method() call links to the method of the class's parent (#2818)
A TS/JS call written super.run() is recorded under its bare name, as
issue #2497 found. Inside an override of run every strategy found the
calling method itself, and the self-edge gate declined it, so the call
was unlinked; elsewhere it went to whatever method had the name, often
the class's own or a sibling class's in the same file, and, for a
parent outside the project like HTMLElement, Set or Promise, to a
project namesake.

A call written on super where it starts is now resolved through the
class's resolved extends edges, which the resolver persists before it
resolves calls: the nearest ancestor's method of that name, a static
one for a call in a static member, or nothing when no ancestor in the
project has it. The call may sit in a method, a class field, or an
arrow function inside either. One the index can't place in a class (a
class expression's method, an object literal's) keeps the old path, as
does one after a class keyword in the member, outside comments and
strings. The self-edge gate now reads a TS/JS call at its own column,
so a this.run() on the line of a super.run() keeps its edge.

The issue's first part, a method call on the receiver of an aliased
constructor, already resolves on main since PR #2661 and is unchanged.

Validation: the new test fails 3 of 4 on main 68fcf2f1 and passes 4 of
4 on the fix; the fourth pins the class-expression guard, and a
mutation of each guard fails a test. A/B against main on 22 repos (20
TS/JS, flask and gin): 660 call sites change, all super calls, checked
against each class's extends chain and sampled by hand. 643 gain an
edge to an ancestor's method (304 in three.js, like super.copy(source)
reaching Object3D.copy; 210 in webpack, 85 in lit); 2 move off the
class's own method to the parent's; 4 keep their target, now by the
parent; 11 lose a guess, where the parent is a builtin or a library's
class, like webpack's SortableSet extending Set, or a mixin's
parameter. Nodes and all other edges are
unchanged; 14 repos' dumps, the controls among them, are byte-identical.
A sync that moves a parent, swaps a parent or adds an override to an
intermediate class matches a fresh index. Index time, median of 3
interleaved runs on three.js, webpack, jest and insomnia: +1.7% total,
noise; 7 interleaved rounds on three.js alone: -1.5%.

Fixes #2497

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:11:29 +00:00
Colby MchenryandClaude Opus 5.5 53f72fec90 fix(liquid): variables declared with {% capture %} are indexed, as assigned ones are (#2814)
The Liquid extractor indexed a variable declared with {% assign %} as a
variable node, in both spellings (braced, and as a line of a
{% liquid %} tag) and skipping comment and raw regions, but it never
read {% capture %}, Liquid's other way to declare a variable. A
captured variable could not be found: search missed it, and
codegraph_explore reported its name as not in the index.

Fix:
- extractAssignments reads capture tags through the same
  findTagOccurrences scanner as assign, so both spellings and the
  comment/raw rule apply. The name is word characters, as for assign,
  optionally in single or double quotes (Liquid reads 'title' as
  title), and must end at whitespace or the tag's end (-%} included). A
  name that goes on with "-" or ".", which the assign rule doesn't read
  either, gets no node rather than a cut-short one.
- Captures are read after assigns. Node ids are keyed by file, kind,
  name and line, so an assign keeps its id when a capture of the same
  name shares its line.
- One helper now builds the variable node and its contains edge for
  both. It computes the declaration's column once, where the assign
  code computed it three times, each a split of the whole file.

Validation:
- 13 new tests in __tests__/extraction.test.ts. 11 fail with main's
  extractor and pass with the fix: 8 declaration forms (braced,
  whitespace-trimmed, tight -%}, single- and double-quoted, {% liquid %}
  lines with LF and CRLF, a quoted {% liquid %} line), an assign and two
  captures of one name on one line (distinct ids, the assign keeps
  its), tags inside a capture body, and an index-level test that search
  finds captured variables in a non-theme folder. 2 guards pass on
  both: no capture from comment, raw, inline comments, prose, a
  nameless tag or a tag inside a string, and no cut-short hyphenated
  or dotted name. Red/green on the committed tree: 42 passed and 11
  failed with main's extractor, 53 passed with the fix.
- A/B on efb091ec, each arm built from its own checkout: 8 Shopify
  theme repos (Shopify/dawn, Shopify/horizon, Shopify/skeleton-theme,
  ChumsInc/chums-impulse-dev, agencecinq/cinquante-et-un,
  noclocks/shopify-devstore-theme, panoply/syncify, Shopify/theme-tools)
  and 2 controls (express, requests). The controls and skeleton-theme,
  which has no capture, are byte-identical. Every other theme gains
  only variable nodes, each with one contains edge: dawn 36, horizon
  192, chums-impulse-dev 90, cinquante-et-un 25, shopify-devstore-theme
  27, syncify 25, theme-tools 49, 444 in all (383 braced, 61 in
  {% liquid %} tags). That is exactly the count of capture tags a
  separate scan finds outside comment and raw regions, and a script
  checked each new node against the source text: it sits on a capture
  tag naming it, with no edge but its contains. 444 right, 0 wrong.
  Nothing is removed or moved, no unresolved ref changes, and no node
  present on both arms changed its id. Every capture and assign name in
  these themes is a plain word.
- Cost: the extractor is faster with the fix. A microbenchmark over
  1,386 Liquid files (8 MB), 15 runs in each of 3 interleaved rounds,
  gave per-round medians of 443-465 ms on main and 316-387 ms with the
  fix, because whole-file line-start scans fall from 29,807 to 20,671.
  Index wall time, median of 3 interleaved runs, main vs fix: horizon
  2975 vs 2935 ms, chums-impulse-dev 2404 vs 2311, syncify 4329 vs
  3867, theme-tools 8405 vs 8108, and the byte-identical controls
  express 2427 vs 2417 and requests 2006 vs 1942, which is within the
  noise of a shared machine.
- Rebased onto 68fcf2f1, which adds only PR #2813 (JS/TS static and
  instance overrides, with a kernel change, so the kernel was rebuilt).
  There the Liquid tests pass, and re-indexing the 8 theme repos
  changes no Liquid or JSON line: 4 are byte-identical to the
  pre-rebase fix arm, and the others differ only in PR #2813's
  is_static flag on JavaScript methods (dawn 30, horizon 26,
  shopify-devstore-theme 14) and one synthesized TypeScript call in
  syncify.
- Kernel parity: not applicable, the kernel does not extract Liquid.

The issue also asks for README and docs-site wording about what Liquid
support covers. That is PR #2815, a docs-only change held for the
maintainer's review, so this change leaves the issue open.

Refs #2498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:49:29 +00:00
Colby MchenryandClaude Opus 5.5 68fcf2f1c7 fix(js,ts): static and instance methods override only their own kind (#2813)
The override-dispatch pass (interface-impl) linked a base class's
method to every subclass method of the same name, so a static method
counted as overriding an instance one, or the reverse, though a call to
one never runs the other (issue #2761). TypeScript nodes recorded
static already, so there the pass alone was wrong. JavaScript nodes
never did: the extractor had no isStatic hook and the kernel answered
nothing for the JS variants, so a JS class's static s() and s() were
both stored as instance methods.

JavaScript members now record their own static keyword with the hook
TypeScript uses (hasStaticModifier, which also keeps a method named
static() an instance one), and the kernel gives the JS variants the
same answer, so both engines agree. The pass skips a JS/TS base and
override whose static flags differ; a static method still overrides a
static one, and an instance method an instance one.

Validation: the new test fails 5 of 10 on main 08478eea (JS and JSX
static flags in both engines, and the override pairs) and passes 10 of
10 on the fix. A/B against main on 20 repos: webpack's lib loses 15
edges, every one a cross-kind pair, such as Module's instance
deserialize(context) to the static deserialize(context) factories of
NormalModule and six other modules, or Module's static
getSourceBasicTypes(module) to RuntimeModule's instance
getSourceBasicTypes(); the same-kind pairs stay and no edge is added.
No other repo's edges change. Nodes change only in is_static, 0 to 1,
on 315 JavaScript static members, none in TypeScript or another
language; the flask and gin control dumps are byte-identical. Kernel
parity over the 20 TS/JS repos: 16,612 of 16,873 files byte-parity,
none with diffs, the rest deferred to wasm as before. Index time,
median of 3 interleaved runs on vite, nuxt, webdriverio, insomnia and
webpack: -1.1% total, within noise.

Fixes #2761

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:08:41 +00:00
Colby MchenryandClaude Opus 5.5 efb091ec2f fix(liquid): editor-saved Shopify JSON and {% sections %} groups link their sections (#2812)
Shopify's theme editor writes a block comment ("IMPORTANT: The contents
of this file are auto-generated...") at the top of every JSON template
and section group it saves. The Liquid extractor read theme JSON with
JSON.parse, which rejects comments and trailing commas, so such a file
linked none of the sections it lists, and in an editor-saved theme most
sections looked unused. All 14 of Shopify/horizon's templates and
section groups carry the comment, so none of them linked. The links a
JSON file did produce all sat at line 1, column 0.

The same report found two more gaps. A layout's
{% sections 'header-group' %} tag, which renders the section group
sections/header-group.json, was never read. And a {% schema %} written
inside {% comment %} or {% raw %} was indexed as the section's own
schema, because only the tag scanner skipped those regions.

Fix:
- Read theme JSON with jsonc-parser's parseTree, which accepts
  comments, and allow trailing commas. Shopify's theme docs say theme
  JSON supports both, and Shopify's theme-check parses it with the same
  options. Any other parse error still links nothing.
- Put each section reference at its "type" value's line and column.
- Read the plural sections tag wherever the singular one is read:
  braced, whitespace-trimmed, or as a line of a {% liquid %} tag. It
  links to sections/<group>.json, and its component is named
  ::sections:<group>, apart from a section of the same name. The
  Shopify theme matcher already keeps any sections/ path inside the
  file's own theme, so a group resolves only there (its doc comment now
  says so).
- Compute the comment and raw regions once per file and share them
  between the tag scanner and the schema extractor. A schema inside one
  is skipped, and the search resumes after the region.

Validation:
- 10 new regression tests in __tests__/extraction.test.ts fail with
  main's extractor and pass with the fix. Re-run on 08478eea: 30 passed
  and 10 failed with main's extractor, 40 passed with the fix. 3
  strictness guards (a missing brace, an unclosed comment, a missing
  comma) pass on both.
- A/B on base 80c36800, each arm built from its own sources with the
  kernel: 8 theme repos (Shopify/dawn, Shopify/horizon,
  Shopify/skeleton-theme, ChumsInc/chums-impulse-dev,
  agencecinq/cinquante-et-un, noclocks/shopify-devstore-theme,
  panoply/syncify, Shopify/theme-tools) and 2 controls (express,
  requests). The controls and skeleton-theme are byte-identical, and no
  node or edge is removed anywhere. All 299 changed reference edges
  were checked against the source text: 299 right, 0 wrong. 197 are new
  links (horizon 25, chums-impulse-dev 138, cinquante-et-un 11,
  shopify-devstore-theme 16, dawn 2, syncify 2, theme-tools 3), 14 of
  them from sections tags, and 102 moved from 1:0 to their "type"
  value. 22 sections tags add 44 import and component nodes. The only
  new failed refs are a "multirow" section that cinquante-et-un's theme
  does not have, and 8 sections tags in theme-tools' prettier-plugin
  test fixtures that name a group no file provides.
- Cost: an extractor microbenchmark over 1,386 Liquid files (8 MB),
  median of 15 runs in 3 interleaved rounds, gave 436 ms on main and
  439 ms with the fix. Index wall time, median of 3 interleaved runs,
  main vs fix: horizon 2891 vs 3064 ms, chums-impulse-dev 2429 vs 2368,
  syncify 3846 vs 3810, theme-tools 6698 vs 6620. The byte-identical
  controls moved as much (express 2312 vs 2445, requests 1946 vs 2080),
  so that is noise on a shared machine.
- Rebased onto 08478eea, which adds only Pascal (PR #2810) and JS/TS
  resolution (PR #2811) changes. There the Liquid tests pass, and
  re-indexing the 8 theme repos gives graphs byte-identical to the
  pre-rebase fix arm, except for theme-tools' TypeScript calls, which
  PR #2811 changes.
- Kernel parity: not applicable, the kernel does not extract Liquid.

Fixes #2494

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:43:54 +00:00
Colby MchenryandClaude Opus 5.5 08478eea53 fix(js,ts): a receiver declared outside the calling function goes by its declared type (#2811)
react-dom's root.render(<X />) linked to the project's one class
component render() when root was declared outside the calling
function, a module's let root: Root | null or a let popupRoot: Root in
the function around the one rendering, or held what createRoot(el)
returned (#2589). The receiver reader scans only the calling function,
so the type that rules a react-dom Root out was never read, and the
call went to the one same-named method by its name.

A TS/JS receiver the calling function doesn't type, in a function
nested in another or one a module-scope variable names, is now read by
its binding in scope at the call. A per-file brace-scope reader finds
the innermost binding of the name: a const/let/var, a parameter, a
destructuring, a for head, a function or class, a lone or destructured
arrow parameter. A declaration or parameter written with a type, or a
const/let/var whose whole value is a new of a class, types the call as
the calling function's own declaration would; an annotated object
literal stays its own members', and a new of a parameter names no
class. A class component's render() and lifecycle methods are never a
by-name guess for a receiver other than this or super, since React
calls them, which also covers an untyped const r = createRoot(el). A
first version that also took any value a package function returned to
have no project method was dropped: it unlinked correct calls through
useContext(...) values in social-app and outline.

Validation: the new test fails 3 of 3 on main 89cd08fa and passes on
the fix. A/B against main on 14 TS/JS repos: 35 edges removed,
every one a wrong guess on a value whose type outside the calling
function comes from outside the project (rolldown's watcher, node's
Server and ChildProcess, playwright, unstorage, zod, NodeJS.WriteStream,
HTMLElement) or nitro's ReactDOM.createRoot root; 18 added and
7 retargeted, all precise (memfs's onAbort watcher.close() now
reaches FsaNodeFsWatcher::close, jest's currentSpec.cancel() Spec's);
92 same-target edges typed at 0.9 instead of guessed. The flask and
gin control dumps are byte-identical. Index time, median of 8
interleaved runs on vite, nuxt, webdriverio and insomnia: +1.7% total,
within noise (-3.6 to +5.2% per repo). A CLI sync that retypes a
module binding and adds files matches a fresh index.

Fixes #2589

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:11:37 +00:00
Colby MchenryandClaude Opus 5.5 f3fdba2955 fix(pascal): calls inside nested Delphi routines are indexed, each under its routine (#2810)
A Pascal routine may declare routines of its own between its header and
its begin. The extractor walked only a routine's body block, so a nested
routine's code was never read: what it called was missing, and a call to
it from the routine that declares it went by name to any routine
elsewhere with that name (in mORMot2's tests, Test, Check and One calls
landed on other classes' methods).

A nested routine is now a function node qualified under the routine it
is nested in (TFoo::Bar::Inner), contained by it and walked with itself
as the caller. It sees the parameters and locals of the routines around
it that are declared above it. Its name means it in the routine that
declares it, in the routines nested there after it and in its own body,
ahead of the class and the unit. A call resolved to a nested routine
from anywhere else is what the call's own scope declares, or nothing. A
nested routine's bare calls reach the class of the method it is nested
in first. Where a routine's tree has errors and its parse swallowed the
unit's routines after it (column 0, or after the body), those are walked
as the unit's own; of two bodies under {$IFDEF} the first is walked, as
before.

Validation against origin/main 7b50ca83, nodes and edges removed/added:
SynEdit +239, -6/+1127; HeidiSQL +404, -178/+2352; mORMot2 +574,
-956/+9565; DelphiMVCFramework +134, -279/+1049; DWScript +434,
-346/+3351; leveldb and SDWebImage byte-identical. Sampled: calls to
nested routines 38/38 right, calls made in them 44/59 (ordinary name
matching's misses), removed edges 30/30 wrong links. Index time within
about 5%, in proportion to the code now walked.

Covers the nested-routine part of #2550.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:50:35 +00:00
Colby MchenryandClaude Opus 5.5 c4dbc126b0 fix(bundle): bundle Node 24.21.0 so Windows commands no longer abort as they exit (#2806)
Root cause: every release bundles the Node that scripts/build-bundle.sh
defaults to, since the release workflow passes only the target, and that
default was v24.16.0. On Windows that Node can abort just as the process
exits, after the command's work is done: a V8 worker posts a delayed task
after the platform's delayed-task scheduler has started closing its uv_async
handle, and libuv fails "Assertion failed: !(handle->flags &
UV_HANDLE_CLOSING), file src\win\async.c, line 94" with status 0xC0000409
(nodejs/node issue 56645). Node fixed it in 24.20.0 (nodejs/node PR 61999).

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

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

Fixes #2617

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:26:56 +00:00
Colby MchenryandClaude Opus 5.5 89cd08fab8 fix(explore): a long query, like a pasted log, answers in seconds on a large index (#2804)
The prompt hook hands codegraph_explore the whole prompt, so a pasted
log, document or source file arrives as a query of hundreds of words.
findRelevantContext searched every word several times, and the reads
behind those searches scanned the index each time:

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

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

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

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

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

Fixes #2620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:48:53 +00:00
Colby MchenryandClaude Opus 5.5 7b50ca8321 fix(pascal): Delphi calls to compiler intrinsics and plain data no longer link namesakes (#2803)
A Pascal call to a name the compiler or the System unit gives meaning
to (SizeOf, TypeInfo, High, Low, Length, Inc, Dec, Assigned, Ord, Exit,
Include, Move, Assert, typecasts like Pointer(P)) went to name
matching, which linked any project member of that name: mORMot's
TRttiProp.TypeInfo, a record's length or high field, a property named
Pointer. The built-in name check compared case, so length(S) and dec(N)
got through too. And a call that resolved to a field, property or
variable linked whatever data had the name.

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

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

Follow-up to #2514.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:20:06 +00:00
Colby MchenryandClaude Opus 5.5 d82a4adc9d fix(js,ts): a call or new in a parameter's default value is the function's call (#2802)
A default runs inside the function each time a call leaves the argument
out, but both extractors pushed the function and walked only its body,
so `function renderWithNote(note = buildNote())` recorded nothing for
the call: no edge and no unresolved reference. Arrows, methods,
constructor parameter properties, class-field arrows, object-literal
methods and destructured parameters lost theirs the same way. Only an
anonymous arrow inside a body was walked whole, params included.

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

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

Fixes #2588

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:52:16 +00:00
Colby MchenryandClaude Opus 5.5 5d81d292fa fix(js,ts): a call or new never lands on an interface or a type alias (#2801)
A call or `new` that a strategy resolved by name or through an import could
target a TypeScript interface or type alias, which nothing can call or make
an instance of. memfs's fs-core `result.ts` declares `interface Ok` beside
`function Ok` (and `Err` beside `Err`), and `Ok(...)` through an import
reached the interface. A `new` of a global that a project only declares a
type for (`new Response()`, `new Worker()`, `new Event()`) was linked by
name to that type, and jest's `write()` parameter call to a `type write`.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:49:07 +00:00
Colby MchenryandClaude Opus 5.5 e8ddc4e2be fix(js,ts): imports of a repo's own packages follow their package.json entries (#2799)
Issue 2502: a pnpm (or npm, Yarn, Bun) workspace member that declares its
entry with `main`, `module`, `types` or `exports`, like
`"main": "./src/index.ts"`, has no index file at its root, so
`import { caseStatus } from '@acme/core'` failed and its calls fell back to
name matching, where a same-named private function nearer the caller took
them. Issue 2456: a sibling package another declares as a path dependency
(`"acme-shared": "file:../common"`), with no root `workspaces`, was not
known as a package at all.

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

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

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

Fixes #2502
Fixes #2456

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:54:48 +00:00
Colby MchenryandClaude Opus 5.5 c1744e7e76 fix(prompt-hook): desktop reminders and other sessions' messages no longer drive the hook (#2798)
Claude Code's desktop app puts <system-reminder> blocks (which local
sessions are linked, and similar status) in front of the prompt it
submits, and UserPromptSubmit hooks receive them inside `prompt`. The
hook gated, picked the project and explored on the whole string, so the
reminder's words drove it: a linked session titled after its work, like
"trace-order-flow", carries structural keywords, so every turn, a bare
"yes" included, got a full explore of the reminder. A reminder in front
of a task notification or a subagent hand-back also defeated the
start-anchored envelope checks, and a message from another session
(a <cross-session-message from=...> envelope) was never skipped at all.

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

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

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

Fixes #2635

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:44:29 +00:00
Colby MchenryandClaude Opus 5.5 407efb1c65 fix(pascal): Delphi calls written without parentheses link to the routine (#2797)
Delphi calls a routine that takes no arguments without parentheses
(`Before;`, `X := FProvider.Value + 1`, `if Ready then`), and only a
statement-level `Obj.Method;` was recorded, so most call sites were
missing from callers, callees and impact. Bare names in arguments and
assignments were captured as function values and matched by name alone,
which linked locals and parameters to other classes' methods.

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

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

Fixes #2514

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:56:50 +00:00
Colby MchenryandClaude Opus 5.5 1040d09f26 fix(mcp): a daemon that never answers no longer stalls a new session for minutes (#2795)
Root cause: a launcher that found no answering daemon spawned one and then
polled for it 240 times, 25ms apart. A probe that connects but gets no hello
waits out the 3s hello timeout, so a process that holds the daemon's pipe
and never says hello made the poll take about 240 x 3s, plus the first
probe: about 12 minutes, with the session's tool calls held meanwhile. In
practice that holder is a daemon whose main thread is blocked: the OS still
accepts connections on its pipe, and the daemon the launcher spawns cannot
take its lock, so it exits.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:23:24 +00:00
Colby MchenryandClaude Opus 5.5 8073637d66 fix(db): reopening an interrupted index no longer gets the server killed at every start (#2794)
Root cause: a run killed inside a bulk-load window (a crash, an OOM kill,
the liveness watchdog itself) leaves that window's secondary indexes
dropped, and since PR #1562 every writable open rebuilds them in
DatabaseConnection.open(), one CREATE INDEX after another on the caller's
thread. Each one scans and sorts its whole table. On a large index the sort
writes nothing to the database or its WAL, so on an MCP server the liveness
watchdog saw a silent main thread with no disk progress, and killed the
process before the index committed. Each restart died at the same index,
and the proxy's in-process fallback died the same way on the request it
re-served.

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

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

Fixes #1887

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:44:58 +00:00
Colby MchenryandClaude Opus 5.5 b2a84873ed fix(daemon): a long sync of another project no longer gets the shared daemon killed (#2793)
Root cause: the daemon arms its liveness watchdog with its own project's
index files (codegraph.db + -wal), and since PR #1231 a silent timeout
defers the kill while those files advance. But the daemon also writes every
other project a session opens with projectPath: the project lifecycle takes
that project's writer lock, starts its watcher and runs the catch-up sync,
all storing on the main thread. A long synchronous stretch of such a sync
left the daemon's own files still, so the watchdog read it as a wedge and
SIGKILLed a healthy daemon mid-sync; the next session reattached the project
and the cycle repeated. A proxy serving in-process, armed with no files at
all, had the same blind spot for its own project.

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

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

Fixes #2404

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:04:05 +00:00
Colby MchenryandClaude Opus 5.5 8651990abd fix(js,ts): an import spelled with a trailing slash or a ./ segment links its file (#2792)
Issue 2577: `import('@/widgets/Chart/')`, `from 'src/widgets/./Chart'`, a
doubled slash, a `..` after an alias, and a workspace package's
`from '@acme/ui/'` lost the edges the plain spelling gets. The file import
was dropped, the names were matched by name alone, and the lazily loaded
component rendered the importing file's own `lazy(...)` constant.

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

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

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

Fixes #2577

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:56 +00:00
Colby MchenryandClaude Opus 5.5 e69546e4a3 fix(js,ts): code outside tests resolves as if Jest manual mocks were not there (#2790)
Issue 2455: when production code imports a workspace package the resolver
cannot follow (`import { formatLabel } from 'acme-shared'`), its names fall
back to name matching, and a Jest manual mock beside the code
(`__mocks__/acme-shared.ts`) is the nearest namesake. So the call, the
import's names, type references, constructions and `extends` all linked to
the mock, and the real function lost its callers. The visibility gate keeps
test suites from production code, but `__mocks__/` is not one of them.
jsx-render, Vue templates and the event-emitter pass pick by name with
their own lookups, which no gate covers.

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

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

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

Fixes #2455

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:10:10 +00:00
Colby MchenryandClaude Opus 5.5 214ef50efe fix(explore): names the query spells that the index never saw are said, not glossed over (#2789)
Root cause: explore answers whatever the query's words match. When a name
the query spelled is not in the index at all, the answer is built from the
other words and says nothing about the missing ones. In the report, the
tool projects under a .NET repo's root build/ folder are skipped by
default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData
and AddFeatures returned an unrelated Program.Main and a C++ Parser, and
the agent read the wrong code. The miss diagnostics (issue #2026) ran only
when nothing at all matched.

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

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

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

Fixes #2462

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:45:00 +00:00
Colby MchenryandClaude Opus 5.5 0953084bc5 fix(pascal): the receiver calls of a chained Delphi call are indexed (#2787)
A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.

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

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

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

Fixes #2547

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:17:14 +00:00
Colby MchenryandClaude Opus 5.5 7180fd0666 fix(js,ts): a dynamic import from Node or a package no longer links to a project namesake (#2788)
Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`,
bare `'fs'` and `require('node:fs')` bindings linked their calls to
same-named project functions. Main no longer does that for any static
form. PR 2120 (released in 1.6.2) declines a name match for a name the
file imports from outside the repository. PR 2437 made that hold in
projects with a catch-all tsconfig path alias, and PR 2671 extended it to
function values such as `promisify(readFile)`.

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

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

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

Fixes #2491

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:10:12 +00:00
Colby MchenryandClaude Opus 5.5 c46523079f fix(context): the busiest file is found even when 20 test or generated files outrank it (#2785)
Root cause: getDominantFile and getTopRouteFile took the top 20 files in
SQL and only then dropped test and generated files in code. Twenty test
files busier than the app's busiest file left nothing, so getDominantFile
returned null and the context builder's core-directory boost never applied.
Nineteen left the dominant file without its runner-up, and nextEdgeCount 0
passes the "3x the next file" test for a file that is not dominant at all.
getTopRouteFile returned null the same way, and measured its 30% share
against the routes of the 20 files in the window, not the project's.

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

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

Fixes #2578

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:45:02 +00:00
Colby MchenryandClaude Opus 5.5 800e582d55 fix(go): conversions, external test packages and build-tag twins link as Go builds them (#2784)
Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.

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

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

Fixes #2581

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:19:32 +00:00
Colby MchenryandClaude Opus 5.5 1890b10cc4 fix(explore): a named function too long for its share renders instead of the file's tail (#2783)
Root cause: an oversize cluster keeps its first-ranked member however big it
is, and the ceiling windows it (CG-30). Ties on importance went to the
shorter span, so in the issue's file, one 200-line function plus the line
that calls it, the one-line `path = build_report()` took that guarantee. The
search matches it through its signature, so it is an entry point too. The
function the query named then no longer fit beside it and was dropped, and
`codegraph_explore build_report` returned the file's last six lines under
the verbatim-source header. A file asked for by path did the same, because
every definition in it ranks below the bare edge lines. Where a body was
windowed, nothing in the block said so. The footer's advice, ask for the
symbol by name, returned the same window, and it suggested `codegraph_node`,
which the default MCP surface does not list.

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

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

Fixes #2486

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:00:50 +00:00
Colby MchenryandClaude Opus 5.5 0c3e12ddbb fix(go): a call further along a chain links the method of the type the chain reaches (#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 c326b6be, 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 #2543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:53:59 +00:00
Colby MchenryandClaude Opus 5.5 88ebefa135 fix(pascal): calls inside a Delphi for var x in loop are indexed (#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 #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 929c65c8, 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 #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 #2546

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:23:40 +00:00
Colby MchenryandClaude Opus 5.5 929c65c83a fix(pascal): Delphi routines with a parameter attribute like const [ref] are indexed (#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 5a6917b4, 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 #2545

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 03:39:38 +00:00
Colby MchenryandClaude Opus 5.5 57ce5b9cdc fix(upgrade): a global npm install is upgraded with -g from any folder, even home (#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 #2477

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 03:11:17 +00:00