Repository navigation
Merge upstream main through 69af19ac (native-kernel reconciliation) - #552
Merged
Merged
Conversation
…od (colbymchenry#2755) A Go call on an indexed element (kubectl's `schemas[ix].ValidateBytes(data)`, deepcopy-gen's `(*in)[i].DeepCopyInto(&(*out)[i])`, go-to-protobuf's `m.Items[len(m.Items)-1].Unmarshal(...)`) reaches the resolver by its bare name, at the column where the receiver starts, and nothing typed the receiver: a method one type declares was guessed by name, often onto a namesake, and a common one like `DeepCopyInto` got nothing. matchGoIndexedCall, an early hook beside matchGoAssertedCall, reads the receiver chain with goChainLinks and types it as Go does: the head from the declaration in scope (goLocalDecl; readGoScopes now records where each `:=` or `var` value starts and which name it is) or a package-level variable; values (composite literals, make, new, append, conversions, fields, elements, addresses, range elements); struct fields, promoted ones too; and the element type of a slice, array, array pointer, map or named slice type, with `(*p)` dereferenced. The method is the element type's own, its interface's or one embedding promotes, and a type parameter's comes from its constraint. An element type from outside the project, a predeclared one, a type literal or an outside alias links nothing, and so does a method the type declares in a file of its package the index couldn't read (kubernetes' core/v1 generated.pb.go), which is read from that file's text. What can't be typed (a call's result) is left to the other strategies. On kubernetes 4c05e667 the indexed-element calls go from 760 resolved and 2,284 unresolved to 2,666 resolved and 378 unresolved; every changed site in kubernetes, prometheus and etcd is such a call, nodes are identical, and the express and flask controls are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s not its embedded type's (colbymchenry#2757) kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB limit, so every API type's own String, Marshal, Unmarshal, Size and MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over embedded types found the method the embedded TypeMeta or ObjectMeta has and linked that, a method the call never runs. resolveGoMethodInPackage, which every typed Go receiver goes through (parameters, locals, fields, assertions, package-level values, indexed elements), now reads the method declarations of the files of the type's package that the index couldn't read, from their text, and links nothing when the type declares the method there. matchMethodCall's typed branch and matchGoPackageValueCall keep that null instead of falling through to name matching, which found the same promoted method. A method the type really gets from an embedded type, like GetName from ObjectMeta, still links. The check that colbymchenry#2755 added for indexed elements moves into the shared path. On kubernetes 4c05e667, 63 calls lose that link and nothing else changes; etcd, prometheus, express and flask are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nts its methods (colbymchenry#2759) goImplementsEdges (colbymchenry#584) follows declared embeddings since colbymchenry#2402, but an embedding of a type outside the index resolves to nothing, so it added no methods: prometheus's TBRun (testing.TB plus Run) went to a struct with only a Run, QueryLogger (slog.Handler plus io.Closer) had no implementations, kapacitor's Diagnostic (a third-party Logger plus Log) took every struct with a Log, and the predeclared `error` was not recorded at all. - go-stdlib-methods.ts lists the methods of 130 standard-library types, each as `name:params/results`, generated from go1.27.3's source and counted the way goSignatureArity counts a stored signature. - Both extractors record an embedded `error`, which stays unresolved. The predeclared-type set is unchanged, so alias and defined-type references still skip it. - goImplementsEdges reads each Go type's failed `extends` rows and reads the package back off the line (goRefQualification, now exported). A standard-library type's methods join the method set, keyed as goMethodKey keys the project's (`std:<import path>` for an unexported one), and the arity gate holds them to the table's counts on both sides: a `sync.Mutex` makes a struct hugo's `Locker`, not etcd's gRPC `LockClient`. Any other outside type is met by a type embedding the same one, or by the only type with every other method the interface names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Go's source (colbymchenry#2760) go-stdlib-methods.ts lists the methods of 130 standard-library types with their parameter and result counts. Its generator lived in a validation kit. scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's Go grammar, counts with the engine's own goSignatureArity, and prints (or --write's) the table for the types it lists, so a later Go release's methods are one command away. Against go1.27.3 it reproduces the table byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… run the parameterless overload (colbymchenry#2762) Three kinds of VB.NET member read that colbymchenry#2355 and colbymchenry#2377 left unlinked: - A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`, a constructor's `Me.New` / `MyBase.New`) went to the first overload declared, so `Me.Load` reached `Load(path)` instead of `Load(Optional force = False)`, and a `Me.New` whose first overload was the calling constructor itself linked nothing. - A read whose first name is a namespace (`Billing.Invoice.MaxLines`, `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front, a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was looked up as a value or a type only. - A field, property or Module variable named bare before the dot (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was not counted as read: a bare name is no member access, so only what is read through it could link, and nothing when that is .NET's (`Text`). Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output unchanged): - A method a reference runs without parentheses resolves among its type's overloads to one that takes no arguments (none, or only Optional and ParamArray parameters, read from the declaration), never the member the reference is in. AddressOf / NameOf keep the first overload, as before. - A receiver name that is neither a value nor a type is looked up as a namespace the way VB.NET does: nested in a namespace around the read, outward to the root namespaces, then in the file's and project's Imports. Each further link is a type declared in it, a nested namespace, or a member of one of its Modules. The read links the type (`Billing.Invoice`) or the member (`Assets.Logo`), as colbymchenry#2355's `Outer.Mode` reads do. - A bare name before the dot that VB.NET binds to a field, property or Module variable (not a local or parameter, which hide it) is linked as a read alongside what is read through it. Not for a `With` block's `.Text`, a method run through the name (the call is the method's alone, as with parentheses), another project's Module, or a member declared with an outside type of its own name (`Responser As Responser`), whose Shared members and nested types VB.NET reads through the type; a project type of its own name follows colbymchenry#2377's Color Color rule. - membersNamed looked a type's members up in every project's same-named type and memoised the list by qualified name alone, so SCrawler's two plugin `UserData` classes shared one list ordered by whichever was asked first. It now keeps to the type's own project. Alone this leaves main's graph unchanged on SCrawler and staxrip; without it, 17 of the new bare-name reads in one plugin linked the other plugin's members. Dropped from the 2026-10-04 test set: a method named through its class also linking the class (colbymchenry#2355 treats it as a call, and its test pins that); `AppSession.SessionId` through a property typed OtherSession linking nothing (colbymchenry#2377 links OtherSession's field, pinned the same way); `SCrawler.My.Resources...` reads (colbymchenry#2355/colbymchenry#2377 send no path that starts with `My` or `Global`); and same-named classes picked by namespace, Imports and project, which main already does once the fixture declares its RootNamespace. Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on main and passes; a mutation that restores main's membersNamed fails the own-project test. The other VB.NET, C#, extraction, resolution, sync and framework suites pass; tsc and build clean. Validation, main vs this branch (indexes from each arm's own build; nodes unchanged; self-loops unchanged; serilog and gin dumps byte-identical): - SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet` re-pointed from the two-argument overload to `ColorsGet() As DataColor`. Added: 4,973 bare-name reads (3,731 own class, 957 Module variables, 285 inherited), 192 namespace reads, 12 reads through a Module member reached by namespace, and that 1 overload. - staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with the root namespace in front, and `box.Show` reaching `Function Show() As DialogResult` from its Shared overload. - Checked against source: every bare-name read's name starts the access, has no local of that name, and binds to its class, an ancestor (23 through a qualified `Inherits Instagram.UserData` the graph's own extends edge misses) or a Module of its project; every namespace read's path ends its target's full name and the target's project is the reader's or one it references; no edge crosses between staxrip and its AutoCrop tool; 48 sampled edges hand-checked, all correct. - Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall 5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall 7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the growth being the new edges (unresolved refs fall 47,074 -> 43,607 and 38,051 -> 36,165). Not covered: a call through a bare name (`Button1.Focus()`) or a function named before the dot, reads in a `With Button1` block, and `My.` / `Global.` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In
class Report { final int score; const Report(this.score); }
typedef ReportCallback = void Function(Report r);
`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.
Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.
Validation, kernel path, arms built from dbb6aa9 and from this change,
each with its own kernel:
- felangel/bloc b9be1e25: 17,875 -> 17,884 edges (+9, 0 removed).
- rrousselGit/riverpod 4ded4f01: 84,726 -> 84,761 (+35, 0 removed).
- jonataslaw/getx e3c8fa04: 8,436 -> 8,456 (+20, 0 removed).
- alibaba/flutter-go a41b2124 (9 legacy typedefs, 6 with a return
type): 7,994 -> 7,968; 39 wrong links removed (34 `Widget` and 5
`Future` uses that had gone to typedefs misnamed `Widget` and
`Future`), 13 added (4 from typedefs, 9 fields typed by the renamed
ones).
- peng8350/flutter_pulltorefresh 0be096c1 (8 legacy, 5 with a return
type): 3,186 -> 2,976; 221 wrong `Widget` links removed, 11 added.
All 88 added non-contains edges were checked against the source; every
removed edge pointed at a typedef misnamed after its return type. The
other new typedef refs name SDK or Flutter types and stay unresolved.
gin and zustand dumps are byte-identical across the arms, kernel and
wasm dumps are byte-identical on bloc, riverpod and pulltorefresh, and
the parity sweep over the five Dart repos is 2,330/2,633 files at
parity, 0 diffs, 303 deferred on both arms. Index time is unchanged
within noise (median of 5 interleaved runs: riverpod 12.6 s vs 11.9 s,
bloc 5.0 s vs 5.0 s, getx 2.8 s vs 2.8 s), and the DB grows by 8 to
24 KB (pulltorefresh shrinks by 64 KB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kage (colbymchenry#2764) A Go type written without a package qualifier was looked up only in the file's own package. In a file that dot-imports a project package, like harbor's config tests (`import . ".../controller/config"`, then a `controller Controller` field), the type isn't there, so the call `c.controller.AllConfigs()` fell back to name matching. Name matching took the same-named type of a package the file doesn't import, the one that sorts first: matchGoFieldChainCall used the struct's own directory for a bare field type, resolveGoMethodInPackage found no such type there, and resolveMethodOnType picked the first of every `Controller::AllConfigs`. The same gap affected calls through parameters, locals and package-level variables of such a type (one set by a conversion), through an alias of it, and through methods promoted from an embedded field. It also affected method values, type references, and the embedding edge itself where the namesake was as near. The field chain, the typed-receiver and method-value package lookup, embedded types, alias targets, package-level values and Go type positions now ask goBareTypeDir. It reads a bare name as Go does: the file's own package first, then a project package the file dot-imports that declares it. An exported name that only a dot import of a package outside the project's modules can supply (`. "github.com/onsi/gomega"`) names none of the project's types. The exception is a type that one of the file's functions declares, which the index holds no node for: name matching still handles it, as before. A bare embedding is retargeted only when a dot import decides, and files without dot imports resolve exactly as before. Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel and wasm backends. 18 tests fail on main and 2 guards pass. All go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test files pass (65 files, 1,140 tests). tsc and the build are clean. Validation compares main bf6f1fa with this commit, each built with a kernel from its own sources. Each repo was indexed from scratch and diffed by call site: - gomega 820ad4d5: 30 calls retargeted from internal.Gomega's Expect to types.Gomega's, the type the dot-imported root package's `Gomega` alias names. All 30 were checked against source. - ginkgo 7ff96d7d: 135 calls added and 1,022 confirmed (same target, 0.7 to 0.9), all through package-level vars typed by the dot-imported internal/test_helpers. 9 retargeted (8 composite literals to the root package's SemVerConstraints aliases, 1 Status call to the fake handler). None removed. - harbor src/ 4e07c69c and kubernetes d2c4b85b: byte-identical dumps. Their dot-imported types' methods have no namesakes, so main already linked them. - Controls gin 0f09c3a9 and zustand d7a5583c (TypeScript): byte-identical. - Index time and DB size are within noise. On kubernetes, the median of 3 interleaved rounds was 151.5 s on main and 148.7 s with the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n instructions files (colbymchenry#2765) Root cause: the write that refreshes CodeGraph's marker-fenced section in an agent's instructions file (CLAUDE.md, AGENTS.md, GEMINI.md) located the section with two independent searches: the first start marker anywhere in the file and the first end marker anywhere in the file. Everything between them was treated as CodeGraph's and replaced. A stray start marker above the real section (what is left when part of an older section is deleted by hand, or the marker quoted in the user's own notes) therefore stretched the "section" over the user's text, and when that marker was the first line the file came back as just the block, which is the report. The `codegraph upgrade` refresh (`codegraph install --refresh`) and plain `codegraph install` both go through this write, for the Claude Code, Codex, Gemini and opencode targets. The same pairing made a lone end marker look like "no section", so every run appended another block, and left a second block stale. Uninstall (and the npm preuninstall hook) and the Cursor rules-file cleanup used the same pairing to delete the same span. Fix: one parser finds the well-formed sections. Each end marker closes the nearest start marker before it, and a marker counts only when it is alone on its line, as every block codegraph has written puts it. A stray start or end marker is left as plain text. The refresh rewrites only the bytes between the first section's markers, using the line breaks that section already has, so a CRLF file stays CRLF and a current CRLF block is unchanged. Further sections are duplicates and are removed, keeping the text around them. With no section yet the block is appended after the existing bytes, which are no longer trimmed. Uninstall and the Cursor cleanup strip only well-formed sections. Validation: - 18 new tests (17 in installer-targets.test.ts: exact bytes for a stray start marker on line 1, a stray start marker with no section after it, a marker quoted in prose, a lone end marker, two sections, CRLF, no final newline, appending, a BOM, uninstall; a refresh plus uninstall per target for Claude Code, Codex, Gemini and opencode; the Cursor rules file. 1 in upgrade.test.ts: an npm upgrade whose refresh runs the built CLI against a private home). 16 fail on main's sources (the other 2 are guards that hold on both), all pass with the fix. - The real CLI on this Windows machine against private temp homes: `codegraph install --refresh`, 14 file shapes x 4 targets x 2 runs. Main had 28 bad results of 112 (the user's text lost in 3 shapes, a block added on every run after a lone end marker, a stale second block kept), the fix 0. With the fix, the bytes outside the section are identical in every single-section shape, and the input stays a byte prefix in every append. - `codegraph install --yes` and `codegraph uninstall --yes --keep-cli` on the reported shape: main kept none of the notes (install left only the 802-byte block, uninstall left 19 bytes); the fix kept them all. Fixes colbymchenry#2506 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rough, not replaced (colbymchenry#2768) Root cause: every installer write goes through one helper that writes `<path>.tmp.<pid>` and renames it onto the path. A rename replaces a symbolic link itself, not the file it points to. So a `~/.codex/AGENTS.md` or `~/.claude.json` linked from a dotfiles repo became a detached regular copy on install: the shared file never got CodeGraph's settings, and later edits to it no longer reached the agent. This held for every target (Claude Code, Codex, opencode, Cursor, Gemini, Antigravity, Kiro, Hermes and the Copilot family) and for install, the refresh that `codegraph upgrade` runs, and uninstall. Uninstall also deleted a link whose file it had emptied, leaving CodeGraph's entry behind in the shared file. Fix: - The write helper resolves a link to the file at the end of its chain and writes and renames there, so the link stays exactly as it was, relative or absolute. - A link to a file that doesn't exist yet gets that file created, but only in a folder that already exists, as writing through it in a shell would. No folder is ever created where a link points. - A dangling chain is followed hop by hop from the real folder holding each link, so a relative link under a linked folder lands where the OS would put it. - When the linked file can't be written (a read-only file, a folder that doesn't exist), the error names the link and its target and nothing changes. - Where uninstall used to delete a user's file it had emptied (an instructions file, Codex's config.toml, Copilot CLI's mcp-config.json, the legacy project .claude.json), a link is kept and the emptied content is written through it. - The refresh sweep catches a failure per agent, reports it, still refreshes the others, and exits non-zero, so the upgrade warns. Decided: links to CodeGraph's own legacy files (Cursor's codegraph.mdc, Kiro's steering doc), which install only ever deletes, are still removed. Validation: - 28 new tests. 25 are in installer-targets.test.ts: - every target and location: install and uninstall keep each of its files that is a link (18 cases); - the shared files' content through install and uninstall for Claude Code, Codex, opencode (two agents sharing one AGENTS.md) and Cursor; - relative links under a linked folder; - a link into a missing folder; - a link to a read-only file; - uninstall emptying through links; - a failing link in a refresh; - a folder link guard. The other 3: - 1 in upgrade.test.ts, where the refresh runs the built CLI; - 2 in a new installer-links-simulated.test.ts, which simulates the link with spied lstat/realpath/readlink, so it runs on machines that can't create file symlinks. With main's sources put back, 27 fail; the folder-link guard passes on both. All pass with the fix. The real-link tests are skipped, with the reason in their names, where file symlinks can't be created (Windows without Developer Mode or admin). - This Windows machine has Developer Mode on, so the real-link tests ran for real. - The real CLI against a private home (`install --yes`, `install --refresh`, `uninstall --yes --keep-cli`), 10 linked files for five agents plus relative, dangling and read-only links: - on main, all 29 link checks found the link replaced by a copy; - with the fix, none did, every link's text was unchanged, the shared files got CodeGraph's settings and lost them again on uninstall, and the missing-folder and read-only cases exited 1 with the new message and changed nothing. Fixes colbymchenry#2721 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kouts alike (colbymchenry#2772) explore-cross-call-dedup's "never re-sends a line it already sent" case has failed on every LF checkout (Linux, macOS, CI) since PR colbymchenry#2655. It passed on Windows only because core.autocrlf writes the fixture with CRLF. Root cause. Since PR colbymchenry#2655 the payroll fixture's RunResult struct (cycle.go lines 31-37) references Payslip through a field, so call 1 renders it. Call 2 renders the file's head, and that 7-line held run comes back inside it: dedupeRange keeps a covered run shorter than MIN_COVERED_LINES (8) in the emit set on purpose, rule 3 of the range algebra, which its own unit test pins. The handler case counted any repeated line as a duplicate. On a CRLF checkout explore's per-file character budget also counts each line's carriage return, so cycle.go rendered fewer lines and the case never met the short run. Fix, test only: - The case groups the repeated lines into consecutive runs and fails on a run of MIN_COVERED_LINES lines or more, read from EXPLORE_DEDUP. A shorter run is the algebra's deliberate re-send. What agents receive is unchanged. - The suite gives its copy of the fixture LF line endings, so every checkout indexes and renders the same bytes. Validation on Windows, with the kernel and with CODEGRAPH_KERNEL=0: - main's test fails with an LF fixture ("call 2 re-sent internal/usecase/payroll/cycle.go lines 31,32,33,34,35") and passes with the CRLF one. The new test passes 26/26 with both. - With the normalisation and main's assertion, a CRLF checkout fails the way an LF one does (lines 31-37): both checkouts now test one scenario. - Teeth: an algebra mutated to re-send held runs of 8-23 lines fails the new case ("call 2 re-sent internal/usecase/payroll/cycle.go lines 42-51 (10 lines)"). - tsc on the test file is clean. Contributor PR colbymchenry#2758 by @danusha2345 reached the same diagnosis. This change was written separately and also normalises the fixture. Fixes colbymchenry#2756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndex or sync (colbymchenry#2769) Indexing or syncing a C++ project stopped at "Resolving refs" with "Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/: Nothing to repeat" as soon as its code called an overloaded operator by name, as `this->operator++();` inside `operator++(int)` does. Choosing a method's overload reads the argument list after the name, and cppParenListIn and cppOverloadParameters wrote the name into that pattern with only `~` escaped. `operator++` did not compile. `operator()` matched the name's own empty parentheses as the arguments, `operator[]` never matched, and `operator+=` matched the next `operator=`. The same helper serves every language that overloads by arity, so Scala `++` overloads and Kotlin names in backticks that hold a `(` failed the same way. The fix adds one escape helper, escapeRegExp in src/utils.ts, the same body as the private copies elsewhere. It is used at every place in src/ where a node name, a reference name, a piece of one, or a word read from source goes into a RegExp unchecked: 114 interpolations in 13 files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and the 2 overload readers only `~`. An AST pass over all 490 dynamic RegExp constructions followed each interpolated value to its sources. The 72 it still flags were each read by hand. They are identifier-checked first, built from escaped names, constants, or the CLI/MCP glob converters. The kernel's one dynamic regex already escapes. With operator arguments now read, nlohmann/json's `p->operator[](0)` reached the argument-type scorer and picked `template <typename T> operator[](T* key)`: a `size_type` parameter scored as an unknown class, a `0` as a null pointer. `size_type` and `difference_type` now count as numbers, as `size_t` does. Validation: - New __tests__/operator-method-names.test.ts (5 tests: init and sync of an operator fixture, operator() and operator+= overload choice, the json operator[](0) shape, Scala and Kotlin). With main's sources, 4 fail (3 throw "Nothing to repeat"); the json shape fails only with size_type taken out of the number types. All pass on the kernel and with CODEGRAPH_KERNEL=0. - Census with a preload that logs every throwing RegExp: main threw only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in all 12 repos below. - A/B on main efff745, graph dumps: - fmt, glm, rocksdb, protobuf: byte-identical. - json: 9 calls retargeted, all right (`j.at("name")` to the key overload, `v.at(i)` to size_type). - abseil-cpp: 3 retargeted, all right (operator() self-call to the two-argument overload, assign(3, 10) to the count overload). - CTRE and folly crash on main; both index now. CTRE: 10 of 12 calls retargeted against main's census run are right; 2 single-header calls reach the identical copy in include/. folly: 4 of 6 are right; 2 small_vector assign(int, int) calls link the count overload where C++ takes the unconstrained template. - Controls gin (Go), bulletproof-react (TS), ktlint (Kotlin, 86 backtick names with pattern characters) and cats (Scala symbolic methods): byte-identical. - Cost: none measurable. Medians of 3 interleaved init runs: fmt -1.5%, json +2.1%, glm +0.2%, abseil -2.5%, gin -2.1%, bulletproof-react -0.3%, rocksdb +0.4% (45.2 s vs 45.3 s). - kernel-parity on fmt + json (cpp/c): 458/669 at parity, 0 diffs, both arms. Fixes colbymchenry#2457 Fixes colbymchenry#2482 Fixes colbymchenry#2636 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es it (colbymchenry#2771) Root cause: the TOML helpers behind Codex's install, refresh and uninstall found CodeGraph's table with two literal searches, `[mcp_servers.codegraph]` at the start of the file or right after a newline, and detection used a plain substring check. Some valid ways of writing that table went unseen: - an indented header, which formatters that indent sub-tables write; - spaces inside the brackets or around the dot; - quoted or escaped keys. Install then appended a second `[mcp_servers.codegraph]`. A table defined twice is invalid TOML, so Codex loaded none of its config. Install and refresh also appended a table after a definition written with dotted keys or as an inline table, or under an `mcp_servers` that is an inline table or an array of tables (both invalid too). The literal search also matched header-shaped text inside a multiline string and spliced into the string. Uninstall left the user's original table, a `[mcp_servers.codegraph.env]` sub-table (a server with no command), and dotted or inline definitions in place. CRLF files came back with mixed line endings. Fix: a small structure scan of the file finds every statement (table header, array-of-tables header, key/value). It decodes key paths the way TOML reads them (bare, "basic" with escapes, 'literal', spaces around dots), tracks the table each key sits in, and folds the lines of a value that spans several into the key that opened it. On top of that scan: - The `[mcp_servers.codegraph]` table is found however its header line is written. Install and refresh replace the first one in place and remove any further copies, so a file the old bug broke parses again. Sub-tables such as `.env` are kept. - A block ends at its last key, so a comment heading the next table stays with that table. - Uninstall removes the table, every copy, its sub-tables, and definitions written with dotted keys or as an inline table, as whole statements. - An entry the user wrote with dotted keys or as an inline table is left as written, with a note. - Where a table can't be added (`mcp_servers` set as an inline table, a value or an array of tables), install refuses with a clear error and changes nothing. - An entry inside an inline `mcp_servers` table is reported by uninstall, never edited. The uninstaller now prints such notes. - Everything is written in the file's own line breaks, and the existing block is compared regardless of them. Validation: - 25 new tests in installer-targets.test.ts: - 20 on the TOML helpers: 8 header forms, CRLF, the old bug's duplicate, look-alikes (other names, header-shaped text inside strings and arrays), sub-tables, comments, 4 dotted/inline/array definitions, 2 shapes where a table can't be added, an entry inside an inline table; - 5 on Codex install, re-install, refresh and uninstall end to end. With main's toml.ts, codex.ts and installer index.ts put back, all 25 fail: 24 on the behavior itself, and one, a header with a trailing comment, only on the new detection helper, since main already handled that form. The 13 existing TOML tests pass on both. - The real CLI on this Windows machine against private homes: `install --yes`, a second `install --yes`, `install --refresh` and `uninstall --yes --keep-cli`, for 23 config.toml shapes, each result parsed with Python's strict tomllib. - main (0abae11): 46 of 92 results were invalid TOML (15 of 23 right after install), the entry was still there after uninstall in 16 shapes, and 8 results had mixed line endings. - The fix: 0 invalid and no mixed line endings. The entry remains after uninstall only inside an inline `mcp_servers` table, which uninstall reports. Install refuses with exit 1 and an unchanged file for the two shapes where a table can't be added. Fixes colbymchenry#2562 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ymchenry#2773) In C, a function declared with its return type written as a macro call, like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node. blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`. Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is often the return type. Blanked together with its argument, it left `cJSON_Print(...)` and a block, which parse as a call statement. The function was lost, the calls in its body went to the file, its locals and parameters became file-level variables, and a header's prototype read as a call from the header. C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter reads it as a macro_type_specifier, when all of these hold: - it is in column 0; - its one argument reads as a type; - only a function declarator follows: a name, its parameter list, `__attribute__` or ALL-CAPS attributes, then `{` or `;`; - the line right above leaves no declaration open. jemalloc writes `size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`. C++ is unchanged. Validation: - New __tests__/c-macro-return-types.test.ts (3 tests: the issue's reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT, REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel and with CODEGRAPH_KERNEL=0. - Pre-parse output compared file by file on 12 repos. The only changes are macro text kept where main blanked it; no file's parse-error count got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1, APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq, redis (with jemalloc), sqlite and curl, and on the C++ controls fmt, glm, json and abseil-cpp. - A/B on main 6bd217a, graph dumps: - cJSON: +92 functions; 7 functions named `(void)` and 100 phantom variables and constants gone; edges 2,882 -> 3,750. - libgit2: +403 functions; 3 `(void)` functions and 293 phantoms gone; 37 calls that went to a phantom parameter gone; edges 81,984 -> 85,834. - APR: +1,551 functions; 7 bogus ones and 1,723 phantoms gone; edges 16,163 -> 23,278. 39 calls now reach the real APR_DECLARE definition instead of its EBCDIC stub. 283 portable calls now see the OS/2 and Windows copies beside the Unix one: 278 go to OS/2 at 0.4 (was Unix at 0.9), and 5 from win32 code go to win32. - expat: 22 phantom parameters gone. - kernel-parity: 0 diffs on both arms; deferred files fall from 33 to 31 (cJSON), 218 to 161 (APR) and 208 to 128 (libgit2). - Cost: medians of 3 interleaved init runs: cJSON -1.6%, expat -4.3%, libgit2 -6.9%, APR +18.4%. APR's graph is 44% larger. The pre-parse itself is unchanged (APR 262 vs 257 ms, libgit2 598 vs 599 ms). Fixes colbymchenry#2470 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sets line (colbymchenry#2774) Root cause, two parts: 1. Install and refresh appended `mcp-codegraph` to `platform_toolsets.cli` in `~/.hermes/config.yaml`. It was never needed. Hermes loads every enabled MCP server on a platform whose list names none (hermes_cli/tools_config.py `_merge_mcp_servers`, a rule present back in May 2026 when the target was written). The name itself is harmful: - Hermes up to 0.22.2 rejects it at startup ("Unknown toolsets"). - Hermes 0.22.4 maps `mcp-<server>` onto the server, so the list becomes an MCP allowlist and every other server disappears from CLI sessions. A user who deleted the line got it back on the next upgrade. 2. When the `mcp_servers.codegraph` block differed from the template, it was replaced wholesale. That reverted an absolute command path, the timeouts, `enabled: false` and `env`. The line-based YAML code also missed some shapes: - a codegraph key written on one line or quoted, which got a second codegraph key; - a 4-space-indented `mcp_servers`, which came out unparseable; - a one-line `mcp_servers: {…}`, which got a second top-level key, and Hermes's PyYAML reader then dropped the user's servers. CRLF files came back LF. Fix: - CodeGraph never writes `platform_toolsets`. - A `- mcp-codegraph` line an earlier install left in `platform_toolsets.cli` is removed, or becomes `- codegraph` when the list is an allowlist naming other MCP servers (the spelling every Hermes version reads as the server). An allowlist that leaves CodeGraph out gets a note, not an edit. - An existing `mcp_servers.codegraph` entry is the user's: nothing in it changes. A block entry missing `command` or `args` gets just those, at its own indent. - A new entry goes in at the indent its siblings use, and a codegraph key is found however it is written. `mcp_servers: {}` becomes a block. - A one-line mapping that holds other servers makes install refuse with a clear error and change nothing. One that holds codegraph is left as written, with a note, and uninstall says it can't remove it. - The file keeps its line breaks, and is written only when something was edited. - Uninstall also removes a `- codegraph` the refresh may have written. Hermes references (NousResearch/hermes-agent v0.22.4, 64dbae64): - hermes_cli/tools_config.py `_get_platform_tools` / `_merge_mcp_servers` ("mcp-<server> names the same server as its bare alias: either spelling selects only that server"). - hermes_cli/toolset_validation.py `saved_toolset_resolver`. - website/docs/reference/mcp-config-reference.md (command, args, env, enabled, timeout, connect_timeout). - hermes_cli/tools_config.py at 1b9c539c (2026-05-17) for the May 2026 rule. Validation: - installer-targets.test.ts: 13 new Hermes tests. 3 existing tests that asserted the `mcp-codegraph` write are updated; the #456 same-indent list case now proves the old line is removed cleanly. With main's hermes.ts put back, those 16 fail, all on behavior. The 7 other Hermes tests pass on both. - The real CLI on this Windows machine against a private HERMES_HOME: `install --yes`, a second `install --yes`, `install --refresh` and `uninstall --yes --keep-cli`, for 14 config.yaml shapes. Each result was loaded with PyYAML (the reader Hermes uses), checked for duplicate keys, and run through a re-implementation of the cited selection rule. Of 42 results: - main: 2 unparseable, 10 with duplicate keys, 26 carrying `mcp-codegraph`, 10 where Hermes 0.22.4 would load only codegraph in CLI sessions, 3 converted CRLF to LF, and the customized entry reverted. - The fix: 0, 0, 0, 0, 0, the customized entry kept byte-for-byte, and 1 install refused by design (a one-line `mcp_servers` holding other servers). Fixes colbymchenry#2485 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry#2776) In Go, a function used as a value, like `apply(run)`, was recorded once per function and name, at its first use. Since PR colbymchenry#2658 the resolver drops a use that a local or parameter of that name shadows. When a function's first use was the shadowed one and a later use, outside the local's scope, was the package function, that later use had no reference left, and the function lost its references edge to the package function (an inner-block `run := 2`, a closure's `func(run int)`, a case clause, an if header or a range variable, then `apply(run)`). Issue colbymchenry#2596 predicted this. Fix, in both extractors (function-ref.ts with tree-sitter.ts's flushFnRefCandidates, and the kernel's go.rs flush_fn_ref_candidates): - A Go function that binds the name itself records one use per scope instead of one per function. Bindings are a func literal's parameters, :=, var, const, range and type-switch variables and a select case's value. Scopes are Go's: a block, an if/for/switch/select statement's implicit block and each case clause. Within one scope a local declared before the first use also covers every later use, so the first use still speaks for the scope. - The function's own receiver, parameters and results keep a single reference: they cover the whole body. - A func literal at file scope is its own unit. - Every other function is unchanged. The scope and enclosing function are read from the tree only for names a function uses at least twice, after the name gate (reading them for every container made kernel extraction of prometheus 2.4x slower). Validation: - go-shadowed-function-value.test.ts: 14 cases, kernel and wasm, LF and CRLF. On main's sources and kernel 10 fail; with the fix 14 pass. Per-scope keys without the binding check, and dropping range variables, case clauses or func literal parameters, each fail 3. - Kernel parity, Go: gin 99/99, etcd 1065/1105 (40 deferred, as main), prometheus 746/746, kubernetes 17779/17952 (173 deferred, as main), 0 files with diffs. - A/B against 6bd217a (fresh indexes, each arm with its own kernel): gin 0f09c3a9, etcd bf9cadf0, prometheus 017fcf2e and kubernetes 006cdf87 keep identical nodes and edges. The only change is 0, 4, 13 and 163 more function_ref rows, all failed: per-scope uses of a local (all 17 on etcd and prometheus and 36 on kubernetes read by hand). express and flask dump byte-identically. - Cost: kernel extraction of prometheus 2.90 s vs 2.90 s, wasm 6.06 vs 6.07 s; kubernetes 34.9 vs 34.9 s; prometheus index medians 12.35 s / 49.1 s CPU vs 12.45 s / 48.3 s over 4 interleaved rounds. Fixes colbymchenry#2596 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r, even home (colbymchenry#2777) codegraph upgrade told a global npm install from a local one by whether the running file sat under the current folder. npm's global folder is often inside the home folder (Windows %APPDATA%\npm, nvm, fnm, ~/.npm-global), so run from home every such global install was taken for a local one: npm ran without -g, the global copy stayed on the old version and a second copy landed in the home folder. The same test sent a project's own install to npm -g when upgrade ran from one of its subfolders or from outside it, and installed into the parent folder instead when it ran from one. The scope now comes from npm's layout around the outermost node_modules. A global launcher beside it (<prefix>\codegraph.cmd on Windows, <prefix>/bin/codegraph next to <prefix>/lib on POSIX) means global. A project around it (node_modules/.bin/codegraph or a package.json) means local, and npm then runs in that project, wherever upgrade was run from; the version npm left there is read back, with a warning when it did not change. A pnpm, yarn or bun install (a .pnpm layout, or that manager's lockfile beside node_modules) is named and given that manager's command, since npm run in its folder would only add a second copy. Validation: on Windows, the real CLI was run with a private USERPROFILE, HOME, APPDATA and LOCALAPPDATA (npm config get prefix = the private AppData\Roaming\npm) against installs laid out the way npm lays them out, with a fake npm first on PATH that logs its command and folder. main ran the wrong npm command in 6 of 9 cases (a global install under %APPDATA%\npm or ~\.npm-global run from home, and project installs run from a subfolder, from home, from the drive root or from another project), the fix ran the right one in 9 of 9; an npx cache run and the bundled runtime are detected the same by both. On main's code 14 of the 91 upgrade tests fail on assertions (12 new ones, and 2 existing ones that now also expect the project folder); all pass with the fix. Fixes colbymchenry#2477 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ref]` are indexed (colbymchenry#2778) The Pascal grammar's parameter rule takes no RTTI attribute, so a declaration like `procedure WithRef(const [ref] AValue: Integer; ...)` or DelphiMVCFramework's `function CreatePerson([MVCFromBody] const Person: TPerson)` broke the parse at the `[`. The method was lost, the parameter after the attribute became a field of the class, every member declared after it went missing until the next class, and in an implementation the routine's locals became unit variables and its calls were credited to the file node. The Pascal extractor now has an offset-preserving preParse that blanks an attribute group at the start of a routine parameter, before or after its modifier (const/var/out/constref), in any routine header, procedural type or anonymous method (`operator` counts only after `class`, since Delphi doesn't reserve it). It is a linear hand-written scanner that skips strings, comments and directives, reads generic and qualified routine names, and leaves every other bracket alone (set defaults, array types, member and method attributes, property index lists, code). A new LanguageExtractor flag, signatureAsWritten, lets Pascal signatures read the source as written, so `(const [MVCFromBody] Person: TPerson)` keeps its attribute instead of showing blank space. Only Pascal sets the flag. Validation: - New __tests__/pascal-param-attributes.test.ts (8 tests, the issue's unit included): 6 fail on main's wiring, all pass with the fix. - A/B against main 5a6917b, both arms built from their own sources: SynEdit, HeidiSQL and DWScript (2,497 files) are byte-identical, as is mORMot2 (its two `[ref] const` signatures keep the attribute). DelphiMVCFramework (32 files changed by the preParse, 150 attributes): 4 nodes recovered (2 methods, a function and a constant), 10 removed, all misparses (9 method locals hoisted to unit variables and a parameter read as a field), 18 methods get a correct signature, end line and return type; edges -39 +90: 29 calls move from the file node or the wrong method to the method making them, 60 new edges come from bodies and declarations main lost, 10 contains edges went with the misparsed nodes. Every changed edge was triaged. - Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm with its own preParse) are byte-identical in dump-graph. - Cost: the preParse takes about 4 ms per MB (95 ms on mORMot2's 25 MB); index time is within noise (3 interleaved rounds, medians: HeidiSQL 6.53 to 6.47 s, mORMot2 16.7 to 17.2 s, DWScript 16.3 to 16.1 s wall). - The shipped scanner matches the prototype used for triage on all 4,578 Pascal files of the five repos, with lengths and newlines kept. Fixes colbymchenry#2545 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olbymchenry#2781) The Pascal grammar's for-in rule takes a plain variable, so a loop that declares its variable inline (Delphi 10.3 and later), like `for var lItem in AList do lItem.DropIfEmpty;` or `for var lHandle: THandle in lHandles do`, became an ERROR node. A paren-less call in its body lost its statement shape and reached no graph, so the routine was missing from the callee's callers. The Pascal preParse added for issue colbymchenry#2545 now also blanks the inline `var` and the loop variable's `: Type`, offsets kept, so the loop parses as the grammar's `for x in` form. The type runs to the `in` at nesting depth 0, so generic and dotted types are whole; `for var i := 0 to`, which the grammar already parses, and `for var` inside comments or strings are left alone. The resolver still types the loop variable: it reads a receiver's declaration from the source as written, so `lHandle.Close` reaches THandle's Close, not a namesake. Validation: - New __tests__/pascal-for-in-var.test.ts (6 tests, the issue's unit included): 5 fail with main's pascal-preparse.ts, all pass with it. - Every textual `for var x in` in the corpus is blanked except 9 that sit in comments or strings (DelphiMVCFramework 47 of 49, DWScript 89 of 96), no other text is touched, and no file parses worse. - A/B against main 929c65c, both arms built from their own sources: SynEdit, HeidiSQL and mORMot2 are byte-identical. DelphiMVCFramework: errors in the 20 changed files drop from 57 to 10, edges are identical (its loop bodies hold parenthesized calls, which main's error recovery already kept), +1 unresolved ref for the VCL's own GetControls in a loop's iterator. DWScript (2,497 files, mostly DWScript-language test scripts): edges -41 +30, every one triaged. The two real-library files gain correct calls; one paren-less right-hand-side read main kept only through error recovery (`var v := FIter.CollectInteger`) is gone until issue colbymchenry#2514's paren-less calls land. The rest are script files: 7 new calls inside the loops, one call moved from the file node to its routine, one stray top-level statement no longer credited to the routine error recovery had stretched over it, 21 file-level guesses error recovery made on one script's stray top-level statements, and 8 garbage nodes per enumerations script whose multi-line names now hold the blanked text. - Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are byte-identical in dump-graph. - Cost: no measurable change; index time medians of 3 interleaved rounds: HeidiSQL 5.50 to 5.49 s, mORMot2 13.96 to 13.94 s, DWScript 13.98 to 13.94 s wall. Fixes colbymchenry#2546 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e chain reaches (colbymchenry#2782) A Go method called on a receiver the extractor keeps no name for, such as a fluent chain's later link (model.WithContext(0).Where(1).First()), a call on what a function returns (time.Now().UTC().Format(...)) or a composite literal (GitOracle{}.Worktree()), reached the resolver as its bare name at the column where the chain starts. Name matching then took a project method of that name by proximity, or the only one: the issue's GORM-style Where and First went to another query type, the standard library's Format to the one project Format, and a literal's method to a namesake type. A new Go resolver step reads the chain at that column and types the receiver link by link: a call's result, or the one a multi-value assignment takes; a conversion's or a composite literal's type; a generic call given its type arguments; an embedded field by its type's name; a package-level variable from the call it is set to. A result written with type parameters keeps them as parameters, read from the declaration, since the indexed signature leaves them out, and the helper that reads them now includes a one-line function's only line. The method is the one Go selects on that type. A type from outside the project, or one that can't be read, links nothing instead of a guess. When a chain calls one name twice, the bare ref is the later call. Validation: 8 new tests; 7 fail on main, and the repeated-name case fails under a first-link rule; the 43 Go test files pass. A/B against main c326b6b, edges removed/added: gin 80/72, etcd 1726/1327, prometheus 2249/1265, sonic (gorm/gen) 910/877, kubernetes 88288/84331. Every removed edge was a name-match guess except 173 in kubernetes: 171 are the same edge now owned by another ref of its chain, and 2 wrong links of metav1.Now().Time.Unix(). Sampled retargets, drops and additions read correct; express and flask dumps are byte-identical. Indexing cost is within noise, median of 3 interleaved runs, wall/CPU seconds main -> fix: etcd 9.5/28.6 -> 8.6/25.4, prometheus 10.5/42.1 -> 10.5/42.6, kubernetes 121.9/413.7 -> 121.3/416.8. Fixes colbymchenry#2543 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of the file's tail (colbymchenry#2783) Root cause: an oversize cluster keeps its first-ranked member however big it is, and the ceiling windows it (CG-30). Ties on importance went to the shorter span, so in the issue's file, one 200-line function plus the line that calls it, the one-line `path = build_report()` took that guarantee. The search matches it through its signature, so it is an entry point too. The function the query named then no longer fit beside it and was dropped, and `codegraph_explore build_report` returned the file's last six lines under the verbatim-source header. A file asked for by path did the same, because every definition in it ranks below the bare edge lines. Where a body was windowed, nothing in the block said so. The footer's advice, ask for the symbol by name, returned the same window, and it suggested `codegraph_node`, which the default MCP surface does not list. Fix, all in the explore render path (query time only): - Rule 1: among cluster members of one importance, a definition the query named ranks ahead of one its search only matched, before the shorter span. - Rule 2: a fragment does not spend the oversize guarantee. When the kept members render under MIN_CHARS, the member they priced out is kept and windowed like a first-ranked one. That member must be one the query named, or a definition in a file pinned by path with nothing named in the cluster. - Rule 3: a body the ceiling window cuts ends with the per-symbol view's note, "... lines X-Y of `name` elided - codegraph_explore `file:X-Y` returns them" (one shared helper now). Each note is paid for by its own part's last lines, never a focus line, the member's first line or a member the part shows whole. So no section grows and no other file's budget moves. - Rule 4: the trim footer and the dynamic-boundary note name `codegraph_node` only when it is on the served tool surface. Validation: - New explore-queried-oversize-symbol.test.ts, 9 tests: 6 fail on main. Reverting any one rule fails that rule's own tests (2, 2, 3 and 1). - All 37 explore suites pass (414 tests). - Deterministic A/B: one index per repo, a fresh handler per query, five arms (main, then each rule added in turn). 820 queries per arm: the 50 longest functions by name, their files by path, caller/callee pairs, call chains, random names and flow questions. - The queried function's head (def plus 10 lines) renders, main -> fix: express 50 -> 50, flask 49 -> 50, excalidraw 33 -> 48, django 43 -> 47, airflow 43 -> 48, of 50 each. - Queried body lines rendered, main -> fix: excalidraw 6,615 -> 9,391, django 5,337 -> 5,662, airflow 6,320 -> 6,749. - Rule 1 changed 32 responses, each hand-triaged as the queried function now rendering. Only 4 of the 504 ordinary flow queries are among them. - Rule 2 changed none. - Rule 3 wrote 838 notes, all checked against source and index, at 2.8 to 3.9 source lines each. Bytes a note frees flow to lower files by the existing carry-forward. - Rule 4's 236 diffs are exactly the removed mention. - No response exceeds the 25,000-char inline cap. Fixes colbymchenry#2486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as Go builds them (colbymchenry#2784) Three Go shapes recorded edges the source doesn't make. A conversion like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to the type, or an instantiates edge to a struct. A bare call in a package's external tests (package a_test) could bind to a same-named helper of package a in the same directory, which a_test can't see. And a name a package defines once per build target, like sep in sep_unix.go and sep_other.go, linked to whichever definition came first, so a Linux caller reached the !unix file. A Go call whose target is a type is now a references edge. Two files of one directory under different package clauses see each other's top-level names only through an import: qualified, or bare after a dot import and exported. A reference whose definition has build-constrained twins in its package reaches the one that builds for the most typical target its own file builds for: linux/amd64, then darwin/arm64, windows/amd64 and the rest. Constraints are read from the file name's GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as go build sets them by default. Validation: 9 new tests in three files, all failing on main, and each part switched off fails its own file; the Go tests pass. A/B against main 0c3e12d, edges changed as conversions / twin retargets: gin 56/0, etcd 556/89, prometheus 812/5270, sonic 49/0, x/sys 550/3072, gopsutil 31/179, fsnotify 3/7, kubernetes 16039/328. Nodes and refs are unchanged, and express and flask dumps are byte-identical. Every kind change sits on a conversion-shaped call, and the twin retargets read right by their files' build lines (prometheus labels to the default stringlabels file, x/sys syscalls to syscall_unix_gc.go). Indexing cost, wall/CPU seconds, median of 3: x/sys 6.7/20.5 -> 7.1/21.7, etcd 9.6/29.9 -> 9.8/30.0, prometheus 12.5/49.2 -> 12.9/50.2; kubernetes, 2 runs each, 108.1/385.8 -> 110.4/390.0. Fixes colbymchenry#2581 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d files outrank it (colbymchenry#2785) Root cause: getDominantFile and getTopRouteFile took the top 20 files in SQL and only then dropped test and generated files in code. Twenty test files busier than the app's busiest file left nothing, so getDominantFile returned null and the context builder's core-directory boost never applied. Nineteen left the dominant file without its runner-up, and nextEdgeCount 0 passes the "3x the next file" test for a file that is not dominant at all. getTopRouteFile returned null the same way, and measured its 30% share against the routes of the 20 files in the window, not the project's. Fix: - DOMINANT_FILE_SQL ranks every file. The window saved no work: ORDER BY needs every group, so the aggregate runs to the end either way, and only one row per file comes back. - computeDominantFile filters the ranking 20 rows at a time until it has the dominant file and its runner-up. The generated-file lookup stays a bounded probe per batch. - getTopRouteFile counts every route file, with ties broken by path. Validation: - New dominant-file-window.test.ts, 6 tests, all failing on main: the issue's layout with 20 and with 19 test files, 25 header-declared generated files (past the first batch), and three route shapes. - The dominant-file cache, plan and allocation suites and the route-file detector test pass. - Seven of the eight real repos give the same dominant file before and after (express, flask, excalidraw, django, airflow, grpc-go, etcd). The closest to the window was airflow, with the runner-up at rank 16. - envoyproxy/go-control-plane crosses the window: 254 generated and test files outrank its first hand-written file. Main returns null; the fix returns xdsmatcher/pkg/matcher/matcher.go (167 edges, runner-up 165). - getTopRouteFile on main reported django's admin/sites.py at 8 of 16 routes and airflow's ui router at 65 of 65. Counting every route file gives 8 of 32 and 65 of 332, both under 30%, so null as documented. - Cost, median of 3 interleaved rounds of 5: getDominantFile on airflow (8,564 ranked files) 581 -> 587 ms, django 189 -> 181 ms, go-control-plane 277 -> 284 ms. getTopRouteFile on airflow 1.3 -> 2.5 ms. - Explore A/B, main vs fix: 996 queries on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical. Fixes colbymchenry#2578 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o a project namesake (colbymchenry#2788) Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`, bare `'fs'` and `require('node:fs')` bindings linked their calls to same-named project functions. Main no longer does that for any static form. PR 2120 (released in 1.6.2) declines a name match for a name the file imports from outside the repository. PR 2437 made that hold in projects with a catch-all tsconfig path alias, and PR 2671 extended it to function values such as `promisify(readFile)`. What still linked is the same binding written as a dynamic import. The out-of-repo check reads the file's static import mappings only. The name matcher deliberately treats `const { x } = await import('./m')` as an import, not a local shadow, so the call fell through to a same-named project function. vite's `const { createServer } = await import('node:http'); return createServer(app)` linked to vite's own `createServer`. Fix (name-matcher.ts): - isOutOfRepoBinding also asks dynamicImportSource for the module a dynamic import binds the name from, where the reference sits. - A per-file scan (FIFO of 64, cleared with the other name-matcher memos) finds `const { a, b: c, default: d } = await import('x')` declarations and `import('x').then(({ a }) => ...)` callbacks on the raw text. Only files with a match are comment- and string-blanked, to drop matches in comments and strings and to find where each binding's scope ends: the enclosing block (a top-level await binds the whole module) or the callback body. - A binding counts only when the reference sits inside that scope after it, and the innermost one wins, also over a static import of the name. nitro's `async function createWebSocketProxy()` imports crossws's `createWebSocketProxy` inside itself: only that inner call is dropped, and the file's other calls still reach the local function. - jsBlockEnd and jsBodyEnd are the forward forms of jsBlockOpenAtEnd and jsBodyOpenAtEnd, which now call them. Validation: - __tests__/dynamic-import-binding.test.ts: 5 of 6 tests fail with main's name-matcher.ts (the project-module control passes there); all 6 pass on the kernel and with CODEGRAPH_KERNEL=0. Four mutations each fail their test: no scope end, no callback form, no dynamic source, and no memo clearing (the in-process sync test). - Sync equals a fresh index (CLI) for adding and removing the dynamic import, for the namesake file arriving later, and for the nitro shape. - A/B on 9 TS/JS repos plus 2 controls, both arms with the kernel: 6 edges removed, 0 added, 0 retargeted, all 6 wrong. vite has 3 (node:http createServer, node:fs/promises readFile, rolldown watch), memfs 2 (node:util promisify), nitro 1 (the crossws self-loop). pathe, c12, unbuild, fs-extra, unenv and nuxt are byte-identical, as are the flask and gin controls. - Cost: fresh index median of 3, interleaved: vite 9.17 s main vs 9.50 s, nitro 7.43 s vs 7.12 s (noise on a shared box). The scan itself over every vite source file takes 40 ms (24 files reach the blanking pass). - Kernel parity is unchanged: 772/776 files byte-parity, 4 deferred, over nitro/src and vite's src (this is a resolver-only change). Fixes colbymchenry#2491 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olbymchenry#2787) A Pascal call whose receiver is itself a call recorded only its outer method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its arguments were dropped, as were paren'd calls deeper in a chain (`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor read a receiver call only for a `TFoo.GetInstance()` factory chain. The extractor now walks a call's receiver chain and records each paren'd call in it, with the calls in its arguments and indexes. A paren-less link stays a read. A link inside a chain is recorded only when it can be named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X` chain), never as the bare method name a chain like `Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main already read keep their names. Receivers are very often typecasts (`TButton(Sender).Caption`), which read like calls. Delphi builds objects only through a constructor (`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a cast: resolution now gives it a `references` edge to the type instead of `instantiates` or `calls`. That also corrects the edges main already made for value casts (`F := TFoo(Obj)`). Validation: - New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit included): all 7 fail with main's sources. Without only the resolver rule the typecast test fails; without only the chain-link gate the fluent-chain test fails. - A/B against main 88ebefa, both arms built from their own sources, diffed by natural keys: - Every removed edge is a cast changing kind at the same site, with the same source and target (SynEdit 491, HeidiSQL 1,262, mORMot2 4,832, DWScript 3,115, DelphiMVCFramework 2,761), apart from 10 function references that moved to an earlier use in the same routine. - New edges: SynEdit 55, HeidiSQL 249, mORMot2 1,196, DWScript 690, DelphiMVCFramework 2,727. Mostly casts read as references to their type (14 of 14 sampled right) and receiver calls. - 178 new call edges sampled: 140 right. instance-method 30/30, qualified-name 10/10, bare functions 26/30, bare methods 70/92, calls landing on a property, field or variable 0/12. The wrong ones follow rules main applies to every call: the TypeInfo intrinsic (main already links 1,709 such calls to a project method), calls through local procedural variables, and calls now read inside a receiver's arguments, named as main names them. - Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are byte-identical in dump-graph. - Cost within noise: index time medians of 3 interleaved rounds, HeidiSQL 7.10 to 7.30 s, mORMot2 18.80 to 18.90 s, DWScript 20.00 to 19.40 s wall. Fixes colbymchenry#2547 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, not glossed over (colbymchenry#2789) Root cause: explore answers whatever the query's words match. When a name the query spelled is not in the index at all, the answer is built from the other words and says nothing about the missing ones. In the report, the tool projects under a .NET repo's root build/ folder are skipped by default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData and AddFeatures returned an unrelated Program.Main and a C++ Parser, and the agent read the wrong code. The miss diagnostics (issue colbymchenry#2026) ran only when nothing at all matched. Fix: - Query tokens written like code names (a camelCase hump, snake_case, or a qualified name) that resolve to no node at all are checked with a new bounded probe, getUnseenNames. It reports names the index has never seen: no node called that in any case, and no name, qualified name or path, signature or docstring word starting with it. - A qualified name is judged by its member. Express's app.handle is indexed as handle, so the receiver alone never makes a name missing. - Names that pass are listed in a note right under the summary line, success-shaped. The note points to codegraph status and never to Read. It names the .gitignore re-include for a default-skipped folder like build/ as the user's choice, since indexing scope is the user's call. - A single word (Program, Main, How) is never named: it may be English. - The total-miss answer gains the same note. - The seeding's query tokenizer moved into one shared function, unchanged. The skip itself is unchanged here. On main a root build/ folder is still skipped by default: PRs 2681 and 2687 re-opened build folders below src/ only. The documented opt-in, !/build/ in .gitignore, indexes it, and sync picks the opt-in up, verified on main and on 1.6.2. Validation: - New explore-unindexed-names.test.ts, 8 tests: 6 fail on main. - All 38 explore suites pass, plus the tool-allowlist and dynamic-boundary suites (466 tests). - The reporter's repo, newrelic/newrelic-dotnet-agent 418a0f2f, now commits the !/build/ workaround. With that line removed and the index synced, main answers the reporter's query with VersionFileGenerator/Program.cs Main and Sicily/Parser.cpp, saying nothing. The fix adds the note naming exactly the four missing names. - Ordinary queries: 996 on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical to main. - Miss probes: 120 queries mixing real names with invented ones, or only invented ones. Main never notes anything. The fix names every invented name in all 120 and no real name. The note costs about 5 source lines on a full response. Fixes colbymchenry#2462 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not there (colbymchenry#2790) Issue 2455: when production code imports a workspace package the resolver cannot follow (`import { formatLabel } from 'acme-shared'`), its names fall back to name matching, and a Jest manual mock beside the code (`__mocks__/acme-shared.ts`) is the nearest namesake. So the call, the import's names, type references, constructions and `extends` all linked to the mock, and the real function lost its callers. The visibility gate keeps test suites from production code, but `__mocks__/` is not one of them. jsx-render, Vue templates and the event-emitter pass pick by name with their own lookups, which no gate covers. A manual mock stands in for a module only while Jest or Vitest runs, and it re-declares that module's names by design. Rejecting it after the ranking picked it (what the gate does for `__tests__/`) leaves the call unlinked even where the real function is the only other candidate. It also leaves sync behind a fresh index: deleting the mock re-opens nothing, while a fresh index then links the real function (main has this today for `__tests__/`). Fix: - ReferenceResolver.resolveOne resolves a reference from code that is not a test (isTestPath) on a view of the graph without manual mocks, when the project has any. Every lookup by name, lower-case name, qualified name, kind and method answers as if no `__mocks__/` file were indexed. Only names a mock declares are filtered, and those answers are cached. The view is swapped in as this.context for the call, so every strategy (frameworks, imports, name matching, fuzzy, function refs) reads it, and clearCaches drops it with the memos keyed by it. Test code keeps the whole graph. - jsxChild, the Vue template resolver and the event-emitter handler lookup skip a mock's component or function for a non-test file (isManualMockHiddenFrom). - The synthesized-edge merge drops any edge from non-test code into a node a manual mock declares, whatever pass made it. - An import that names the mock's own path (`import x from './__mocks__/uuid'` in a vitest setup file) still links. Jest's `moduleNameMapper` and automock are not read: they act only while Jest runs, so they never change what production code calls. Validation: - __tests__/jest-manual-mocks.test.ts: 5 of 6 tests fail with main's sources. Each of five rules fails a test when removed: the view (three tests, the in-process sync case among them, which adds and deletes a mock), the JSX, Vue and event lookups, and the merge guard. - Sync equals a fresh index (CLI) when the mock or the real module arrives later, when the production file is edited, and when the mock is deleted. - A/B on 8 TS/JS repos plus 2 controls, both arms with the kernel. Edges from production code into mocks: webdriverio 23 to 0, outline 21 to 0, social-app 3 to 0, insomnia 19 to 13 (the 13 left are setup-vitest.ts importing mocks by path). webdriverio gains 23 right edges and 142 confidence raises that the mocks had blocked. jest, nuxt, flask and gin are byte-identical. On webdriverio, production code's edges equal main's on a copy without the mocks, 20,885 of 20,885 with metadata. insomnia matches the same way, apart from the 13 path imports. - Cost: fresh index, median of 3 interleaved: webdriverio 12.67 s main vs 12.71 s, insomnia 12.84 s vs 12.60 s. Fixes colbymchenry#2455 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inks its file (colbymchenry#2792) Issue 2577: `import('@/widgets/Chart/')`, `from 'src/widgets/./Chart'`, a doubled slash, a `..` after an alias, and a workspace package's `from '@acme/ui/'` lost the edges the plain spelling gets. The file import was dropped, the names were matched by name alone, and the lazily loaded component rendered the importing file's own `lazy(...)` constant. Root cause: the fallback-alias, workspace, direct-path and own-config branches of the import resolver join the specifier as written and probe the result through tryWithExtensions. `fileExists` falls back to the disk, which accepts any spelling of a file, so the resolver returned `src/widgets/Chart//index.tsx` or `src/widgets/./Chart/index.tsx`, and every lookup keyed by the indexed path missed it. The relative and tsconfig-path branches go through path.resolve and were already fine. Fix (import-resolver.ts): tryWithExtensions spells each candidate as the index keys files (indexedSpelling): for the ES module family, a path with a doubled slash or a `.`/`..` segment is posix-normalized before it is probed and returned. That is how Node, tsc and the bundlers read a specifier. A trailing slash still names only a folder, because `Chart/` plus `.tsx` is no file. Other languages are untouched: a C include's `a/../b.h` follows the folder on disk, as PR 2610 noted. Validation: - __tests__/import-path-spelling.test.ts: 4 of 5 fail on main (the plain spelling passes). They cover the issue's spellings, a doubled slash, `..`, a trailing slash beside a same-named file, and a workspace member. All pass on the kernel and with CODEGRAPH_KERNEL=0. Dropping the normalization, or its doubled-slash check, fails 4. - Sync equals a fresh index (CLI) when the folder arrives after its importer, when the importer is re-spelled, when the folder is deleted and when the target is edited. - A/B on 11 TS/JS repos plus 2 controls, both arms with the kernel: pulse-sms-web (53 imports like '@/utils/') gains 65 edges and corrects 7, including 2 imports that had name-matched to same-named API classes; react-one gains 11 file imports and corrects 2 bindings. All 85 are right. The other nine (jvue-admin, vue-invoice-app, vue3-antdv-admin, v-ui, vue-blog-admin, dreammart, vite, outline, social-app) and the flask and gin controls are byte-identical. - Cost: fresh index, median of 3 interleaved: vite 7.59 s main vs 7.49 s, outline 16.63 s vs 16.94 s. Fixes colbymchenry#2577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… daemon killed (colbymchenry#2793) Root cause: the daemon arms its liveness watchdog with its own project's index files (codegraph.db + -wal), and since PR colbymchenry#1231 a silent timeout defers the kill while those files advance. But the daemon also writes every other project a session opens with projectPath: the project lifecycle takes that project's writer lock, starts its watcher and runs the catch-up sync, all storing on the main thread. A long synchronous stretch of such a sync left the daemon's own files still, so the watchdog read it as a wedge and SIGKILLed a healthy daemon mid-sync; the next session reattached the project and the cycle repeated. A proxy serving in-process, armed with no files at all, had the same blind spot for its own project. Fix: addWatchdogProgressPaths() lets a process add files to its watchdog at runtime. The project lifecycle adds a project's db and -wal the moment this process becomes the project's writer, and drops them after it closes the project, so only indexes this process writes count, never one another process owns. Each change re-arms the watchdog with a new child whose argv carries the whole list; the old child is stopped after the new one starts. Added files take effect at once (a sync may block the loop right after); dropped ones wait a turn, so a server releasing its projects on the way out re-arms once or not at all. A message over the heartbeat pipe was tried first and dropped: Node's Writable holds a second write while one is still in flight, so the -wal line of a db + -wal registration never reached the child before a block that followed in the same tick (4 of 4 runs). Validation: - New __tests__/liveness-watchdog-projects.test.ts (11 cases, real watchdog child, lowered timeout): main's source 9 fail / 2 pass (the two guards), fix 11 pass. The lifecycle case arms the watchdog for project A, attaches B through the project lifecycle and stores into B's index on the main thread for 3s: killed at ~1s on main, runs to completion with the fix. - 8 mutations, each failed at least one case: no lifecycle registration, registration before the writer-lock check, no drop on release, no re-arm on add, no re-arm on drop, old child not stopped, drops re-armed at once, adds deferred. - Real daemon on Windows (watchdog timeout 5s, daemon keyed on a 2-file project, second project excalidraw 1d4dc2c with all 687 TS files edited, attached with codegraph_status projectPath), 3 interleaved rounds: main killed 3/3 about 13s after attaching B ("Main thread unresponsive for ~5s"), leaving B with 27,698 edges and 72,534 pending refs; the fix survived 3/3 through 7.8-9.3s main-thread blocks and finished B (61,120 edges, 0 pending), and 3/3 again on the final build (9.7-9.9s blocks). Fixes colbymchenry#2404 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…led at every start (colbymchenry#2794) Root cause: a run killed inside a bulk-load window (a crash, an OOM kill, the liveness watchdog itself) leaves that window's secondary indexes dropped, and since PR colbymchenry#1562 every writable open rebuilds them in DatabaseConnection.open(), one CREATE INDEX after another on the caller's thread. Each one scans and sorts its whole table. On a large index the sort writes nothing to the database or its WAL, so on an MCP server the liveness watchdog saw a silent main thread with no disk progress, and killed the process before the index committed. Each restart died at the same index, and the proxy's in-process fallback died the same way on the request it re-served. Fix: an open on a watched event loop leaves the rebuild to a worker thread with its own connection, and the project is served once the rebuild is done. - DatabaseConnection.open({ deferIndexRecovery }) records the missing DDL instead of running it; recoverIndexesOffThread() runs it on a worker (single-flight, rejects on failure so the next call retries; SQLite's default temp store, so the sort spills to disk; the worker collects its garbage and exits by itself, per worker-teardown.ts). - CodeGraph.open() (the MCP default project, the CLI) defers and awaits it. - openSync({ deferIndexRecovery }) plus cg.recoverIndexes() for the MCP's synchronous opens: the ToolHandler's projectPath projects and the engine's retry path. ToolHandler.execute awaits the rebuild before serving (not time-boxed: reads without the indexes scan whole tables), the project lifecycle awaits it before its catch-up sync writes, and the retry path starts the query pool after it. - Synchronous opens that ask for nothing keep rebuilding in line, as before. Validation: - Reproduced on main b2a8487: a django 79502d0 init killed 1.5s into the resolution window left the field report's index set missing (5 ref + 5 edge indexes, index_state indexing); the in-line rebuild is about 750ms of main-thread work there. - Real daemon on Windows, a 3M-ref / 1M-edge index left the same way (781 MB), watchdog 1.5s: main was killed in 4 of 4 starts, stuck at 2 of 10 indexes, the launcher's in-process fallback killed too, no reply. The fix rebuilt all 10 in about 11s with no main-thread block over 506ms, answered the call, and the next start opened in about 120ms. - New __tests__/index-recovery-off-thread.test.ts (9 cases): main's source 8 fail / 1 pass (the in-line guard), fix 9 pass. Spies show no CREATE INDEX on the server thread for CodeGraph.open, a projectPath call (watched and not), the retry path and the startup path; a real watchdog (500ms) kills main mid-rebuild of a 300k-ref index and leaves the fix alone. 6 mutations each failed at least one case. - Contributor PR colbymchenry#1888 and the stale draft PR colbymchenry#1919 were read for guidance; this ports their worker recovery to current main's open paths. Fixes colbymchenry#1887 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for minutes (colbymchenry#2795) Root cause: a launcher that found no answering daemon spawned one and then polled for it 240 times, 25ms apart. A probe that connects but gets no hello waits out the 3s hello timeout, so a process that holds the daemon's pipe and never says hello made the poll take about 240 x 3s, plus the first probe: about 12 minutes, with the session's tool calls held meanwhile. In practice that holder is a daemon whose main thread is blocked: the OS still accepts connections on its pipe, and the daemon the launcher spawns cannot take its lock, so it exits. Fix: the poll has a wall-clock budget instead of a probe count (the 6s it was meant to be), and each probe's hello wait is capped by what is left of it (pollForDaemon, connectAnyDaemonCandidate, a hello timeout on connectWithHello). Spent, the session is served in-process exactly as when a spawned daemon never binds: read-only while the stuck daemon still holds the writer slot. The proxy's existing retry with backoff then reaches the daemon again. The liveness watchdog kills a wedged daemon after its 60s window (longer while its disk advances), and the next retry spawns the replacement and proxies to it. The bound sits well inside that window, so a session neither waits on the wedge nor gives up on the daemon. Validation: - A real daemon wedged by a preload's busy loop on Windows, then a second session's codegraph_status: - no watchdog on the wedged daemon: main answered after 733.2s; the fix after 9.9s, and later calls in 0.5s while the daemon stayed wedged; - the default 60s watchdog (it killed the wedged daemon at about 121s, one deferral for its own off-thread writes): main answered after 125.2s; the fix after 9.9s in-process, then proxied to the replacement daemon at 121.1s. - New __tests__/daemon-silent-holder.test.ts (8 cases): main's source 0 pass, fix 8 pass. Fake-clock tests of the budget and the per-probe cap; a real silent socket honouring the hello timeout; the real launcher against a project whose pipe, lock and writer slot are held by a holder that never says hello: first call served in-process in about 10s (main: none in 45s); and after the holder dies, the session proxies to the daemon that replaces it. 3 mutations each failed at least one case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stored (colbymchenry#2845) CodeGraph.indexFiles returned the orchestrator's indexing promise from inside the try whose finally releases the cross-process write lock. A returned promise leaves the try at once, so the finally ran as soon as indexing started, and the lock was gone while the files were still being extracted and stored. Another process's sync or index of the same project, such as a git hook, the CLI or an MCP server, could then write to the index at the same time. indexAll and sync await inside their try and hold the lock to the end; the in-process mutex already covered the whole call. Fix: await the indexing inside the try, so the lock is let go only once the files are stored, or when indexing fails. Validation: - New __tests__/index-files-write-lock.test.ts (2 cases). A second handle on the project stands in for another process, and the real indexing waits at a gate the test opens. While indexFiles runs, the other handle's sync must be refused with LockUnavailableError. With main's src/index.ts it ran to completion instead (red); with the fix it is refused, and admitted once the files are stored (green). When indexing fails, the lock is let go (passes on both). - The lock and sync test files pass alongside: sync.test.ts, cli-sync.test.ts, cli-unlock.test.ts, orphaned-refs-sweep.test.ts and sync-rebuild-convergence.test.ts, the last two of which call indexFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ethod calls in it (colbymchenry#2846) A Rust method call made through more than one field, or on what another method returns, was recorded by its bare method name in both extractors: self.services.store.put("a") became `put`, as did self.store().put("m"). Only a single field kept its receiver (self.store.put). The edge's refName then said nothing of which value the call goes through, and a call on a method call's value was never typed, so self.store().put("m") stayed unresolved. PR colbymchenry#2831 had already made the field-chain calls link by reading the receiver from the source text, but their names stayed bare. Both extractors now keep a receiver that is a chain of fields and method calls on `self` or on a name, a call written `name()` with its arguments and turbofish dropped, up to 16 links: self.services.store.put, self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer chain rooted at a path keep the bare name. The kernel's receiver_chain mirrors the wasm extractor's rustReceiverChain byte for byte. Resolution reads such a call as the bare-named call was read before (rustChainedCallByName, at the top of resolveOneIn), so the frameworks, the strategies and the scope gates see what they saw, and the edge keeps the call as written. The chain goes along: the written-receiver hook reads the receiver the chain names where the call is written (rustChainedReceiver), so a method call's value is typed from that method's signature, and a later link that calls the same name again is told apart instead of being left to a guess by the name. Validation: - New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's workspace verbatim plus self.store().put("m"), chains whose links call the same name through self, a tuple field and a parameter, a call on a method's std return value, the recorded shapes and the 16-link cap, and a sync that adds the missing method. Red on main's sources, 7 of 10 failing on both the wasm and the kernel path; green with the fix. The Rust kernel parity suite gains a chain case. The extraction test that pinned the old collapse now pins the chains. - A/B against main 865ddf4 (same groups against d833827), both arms with the native kernel, every changed call edge read. Per repo: edges that only gained their receiver in refName / same target now by type / wrong removed / right added / wrong to right: ripgrep 3fce3b5b: 159 / 45 / 6 / 6 / 15 axum 86aa6153: 115 / 25 / 0 / 14 / 7 tokio 8a2408ed: 1039 / 124 / 36 / 29 / 27, and 1 right link lost lemmy 92e077b5: 190 / 24 / 3 / 1 / 3, 1 right lost, 1 wrong added The two lost links and the wrong one follow rules the typed-receiver reading already applies to typed locals on main: a function-local glob `use` does not shadow a file-level named `use` (tokio), a call on a dependency's type reaches only impls of project traits, and `x.into()` takes the type's explicit `impl Into` (lemmy). Controls gin and express: byte-identical dumps. Nodes are identical on every repo. - Kernel parity (Rust), ripgrep, axum, tokio, lemmy: 1752/1752 files at byte parity, 0 deferred. - Index cost, interleaved median of 3 (wall): ripgrep 3.15 s -> 3.15 s, axum 3.39 s -> 3.42 s, tokio 10.48 s -> 10.54 s, lemmy 5.14 s -> 5.15 s; CPU within noise. Fixes colbymchenry#2836 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and named options
The static guard in child-process-windows-hide.test.ts, which checks that
every spawn under src/ sets windowsHide: true, matched the key by its
source text. A quoted key was a different property to it, so
{ windowsHide: true, 'windowsHide': false } passed as hidden and
{ 'windowsHide': true } was flagged. A computed key written after
windowsHide: true, like [key]: false, was skipped, so it passed too. And
(file, options) with the args array omitted was recognized only when the
options were an object literal, so execFileSync('git', opts) was read as
passing no options and flagged.
A quoted key is now the same property, a computed key makes the options
unprovable from that point on, and with exactly two arguments the second
one is what gets checked, so an args array there is still an offender.
Validation: with main's guard, exactly the 4 new table rows fail and the
other 29 pass; with this change all 33 pass, including the scan of src/,
which flags nothing on either side. Test-only, so no CHANGELOG entry.
Original PR colbymchenry#2371 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CODEGRAPH_KERNEL=0 sends every language through wasm, and running the suite with it is how the wasm path is exercised on a machine that has the kernel built. Three kernel-only blocks failed in that run instead of skipping, because they skipped only when the .node file was missing: kernel-deep-nesting asserts in beforeAll that some language is routed to the kernel, kernel-retry-materialize asks for a raw kernel result and gets null, and the C++ brace-scope kernel parity block in cpp-brace-scopes gets no kernel result either. Each of the three describe.skipIf conditions now also skips when CODEGRAPH_KERNEL is '0', the same outcome as a checkout without a kernel. The kernel-independent self-heal block in kernel-retry-materialize still runs. Validation, the three files: on main with CODEGRAPH_KERNEL=0, 5 tests fail and kernel-deep-nesting fails at beforeAll; here, 13 pass and 13 skip. With the kernel (CODEGRAPH_KERNEL_EXPECT=1), 26 of 26 pass, nothing newly skipped. All 75 test files that reference the kernel pass with CODEGRAPH_KERNEL=0 on this commit (1,832 passed, 0 failed), so these were the only kernel-only blocks that failed that way. Test-only, so no CHANGELOG entry. Original PR colbymchenry#2372 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alls The routing manifest has a row per (route, symbol it reaches), and the routes list behind Entry points and the Steps picker folds those rows to one per route. To have rows to fold, it fetched limit * 4 + 1 of them, so an inline handler, which has a row per call in its body, could use up the window by itself, and the routes after it were never fetched. On an Express app with one 24-call inline handler before two routes, limit=3 listed only GET /dense. getRoutingManifest takes a perRoute flag that applies its limit to routes, in the same file and line order, and returns every row of those routes. Without the flag the query and its result are unchanged. Added on top of the contributor's change: the window is still over-fetched, four routes per route shown, because some routes are dropped only after their rows come back (a test or generated handler, an inline route written in a test). A window of exactly limit + 1 routes could hold only those. On hono, whose routes are nearly all registered in test files, Entry points then listed 0 routes instead of main's 28, and the routes list 21 instead of 32. With the over-fetch, every route main lists is still listed at each limit tried (3, 12, 60, 200 and 300), and hono's Entry points list 32. truncated also keeps main's check that the window came back full. Validation: the new limit=3 test fails on main (only GET /dense) and passes here. A project whose test-file routes sort first fails with the PR as first written (it reads as unrouted) and passes here and on main. Routes listed on Wiki.js (132 routes) and realworld-express are identical to main, and on hono they differ only by the additions above. The ui suites and the other route consumers: 34 files, 723 passed, 0 failed, 3 skipped (environment-gated). The changelog line is in docs/viewer-launch-changelog.md, since the viewer is not released yet. Original PR colbymchenry#2370 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHP reads namespace names without regard to case, so after
use App\Fields as Field; the name field\FirstName is the same class as
Field\FirstName. The qualified-name resolver compared the alias as
written, found no import, and read the name relative to the file's own
namespace instead, so new field\FirstName() stayed unlinked. The alias
is now matched without regard to case.
This lands the first half of the contributor's PR. Its second half
refused an alias that two namespace { } blocks of one file bind to
different namespaces. That also dropped the first block's correct edge,
because the use map is file-wide, and the case doesn't occur in real
code: across 8 PHP repos (33,820 files), 185 files declare more than
one namespace, and none binds one alias twice outside strings. The fix
for those files is to scope each use statement to its namespace block,
which needs block positions the index doesn't keep yet: a braced
namespace gets no namespace node, and neither does a second unbraced
one, so it is left for its own change.
Validation: the new test fails on main and passes here, with the
native kernel and with CODEGRAPH_KERNEL=0. Graphs of php-parser,
composer, psalm, monica, laravel and phpunit are byte-identical to
main, as are the controls express and flask. Index CPU on laravel,
median of 3 interleaved: 53.3 s on main, 52.9 s here. The PHP and
Laravel suites pass in both engines (10 files, 67 tests).
Original PR colbymchenry#2369 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s type A TypeScript or JavaScript method called on an element of a class field, like this.handlers[type].handle() or this.items[0].save(), kept the bare method name: a member chain rooted at this collapses to it so the resolver can read the owner off the enclosing class. A subscript in the chain leaves the resolver nothing to read, so the bare name exact-matched any project method of that name, often another class's (this.listeners[type].push(h) reached an unrelated UndoStack.push). Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain, or under window, emits nothing, as arr[0].run() already did. The resolver reads the element type the field's declaration writes (X[], Array<X>, ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and resolves the method on it or leaves the call unresolved: a type parameter of the declaring class, a union with another type, an array of arrays or a non-plain element hold no one type. The class type-parameter reader now reads from the keyword to the list's own >, over several lines and through braces in constraints. Our changes: kept escapeRegExp from PR colbymchenry#2769 in the conflict; the type-parameter reader also accepts the type keyword, since the T['p'] path passes type-alias owners and the PR as written read State<Item>['current'] as the project class Item (new case in ts-array-annotated-receivers); CHANGELOG covers JavaScript and asks for a re-index. Validation: the element tests fail 10 of 11 on main with both engines; the type-alias case fails on the PR as written and its brace half on main. A/B against main, kernel built per arm: flask and gin byte-identical. TS: pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9 wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a merged mixin member, an index-signature type, an any field); typeorm 3 wrong removed, 1 right lost, 2 retargeted to the declared interface; excalidraw 1 wrong removed; vscode's base and editor 19 added, 24 retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by name: a getter, an index-signature field, deep chains, a T[] parameter, a union alias). JS: three.js, playcanvas and pdf.js lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM calls on project classes). Kernel parity holds on every repo with deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s, three.js 64.7 s to 67.9 s, medians of 3 interleaved). Original PR colbymchenry#2766 by @inth3shadows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot the standard library A Go reference written through a package qualifier was dropped as a standard-library reference whenever the qualifier matched a name in the built-in list (errors, log, types, parser, encoding, template, ...). The check looked at the name alone, never at what the file imports under that name, so a project with its own package of such a name lost nearly every call into it: harbor's errors.New(...) through its lib/errors, prometheus's parser.Inspect(...) through promql/parser. What the file imports now decides before the name does. A qualifier the file imports, by its last path segment, the name goimports assumes for an unaliased import, or an alias, from a package of one of the project's own modules is a project package, and the reference goes on to normal resolution. A file that imports the standard library's package of that name, and a qualifier the file doesn't import at all, are skipped as before. Validation: the new test file has 7 cases, 4 of which fail on main; all pass here, with the native kernel and with CODEGRAPH_KERNEL=0. A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889 edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both, nodes unchanged. Every added edge was checked against its source: the line spells qualifier.Name and the file imports the target's package directory under that qualifier from inside the module (3,343 of 3,343), and a sample of 10 was read by eye. gin and cobra, and the controls express and flask, are byte-identical. Index CPU on harbor, median of 3 interleaved: 40.0 s on main, 39.9 s here. Original PR colbymchenry#2368 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TypeScript or JavaScript call on a constructed object, new C(args).m(), reached the resolver as the bare method name in both engines, so exact matching bound it to any project method called m: new RegExp(p).exec(s) to a database wrapper's exec, new Array(n).fill(0) to a text class's fill, new TextEncoder().encode(s) to a project encode function. Both engines now emit new C().m (new ns.C().m for a namespaced class, and nothing for a constructor that is not a plain name or member chain). The resolver looks m up on C with resolveMethodOnType, inherited methods on the conformance pass once extends edges exist, an aliased named import mapped back to its class, and a JS built-in (RegExp, Map, Date...) never a project class unless the file imports or declares one. An ES5 constructor function keeps its prototype method. Our changes: the lookup passes the file the call's file imports the class from (tsImportedTypeFile), so of two classes sharing a name the imported one is the one constructed (the PR as written took the first indexed copy, at 0.9); new this.constructor() and a static new this() keep the bare method name, as a call on this does (the PR dropped three.js's clone idiom, 29 links to a class's own copy); rebased onto PR colbymchenry#2852, which edits the same keepsBareTsJsReceiver and test lists; resolved the import list conflict with rustChainedCallByName from PR colbymchenry#2846; CHANGELOG bullet moved next to the JS/TS receiver bullets. Validation: the new tests fail 4 of 6 on main 449eefa with both engines and pass here. A/B against main, kernel per arm: flask, gin and vite byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2 retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and test, Array fill, Set add) and 1 right one lost to the grammar's parse of new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder, TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted and 68 typed, all right, 76 wrong removed (Array keys and fill, typed arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy) and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right only by name (Loader methods inherited through a base imported from the three package). No default-imported class under another name lost an edge. Kernel parity holds on all six TS/JS repos and this repository, 0 diffs. Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js 72.0 s to 71.2 s (medians of 3, interleaved). Original PR colbymchenry#1945 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from a package
When a JavaScript or TypeScript file imports a name from outside the
repository, like import { resolve } from 'node:path', matchByExactName
returns no edge for a bare call of that name (the early return main added
for issue 2101). A call to a function declared inside the calling function
under that name, which shadows the import, lost its edge with it.
The candidates for such a call are now narrowed to the calling file's
declarations nested in a function or method (innerScopeShadows), instead
of giving up; a top-level one is the import's own binding. Lexical
reachability, applied later, keeps only the nested declarations visible
where the call is.
Our changes: tests for a sibling function's nested declaration and one
nested a level below the call, which neither may take the call (both hold
through the reachability rule), and for a call from a function nested in
the declaring one; the CHANGELOG bullet moved next to the JS/TS import
bullets, with credit.
Validation: the tests fail 2 of 6 on main and pass here. A/B against main
on 8 repos, kernel per arm: flask, gin, excalidraw, codegraph, pixijs,
pdf.js and three.js byte-identical; vite gains 1 right link, scanImports'
call to its nested scan() beside import { scan } from
'rolldown/experimental'. Index CPU
time on codegraph, test files full of vitest imports, within noise:
81.7 s to 81.9 s (median of 3, interleaved).
Original PR colbymchenry#1715 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts Store<String> A Java class implementing a parameterized interface (implements Store<String>), one extending a generic base (extends Base<Integer>), a Java interface's extends Store<T>, and a TypeScript class's implements Store<string> got no implements or extends edge. The grammars wrap the type in a generic_type node, and the supertype reader took its whole text, which no node is named. Without the edge the interface-impl synthesizer had nothing to walk, so a call on the interface stopped there. Both engines (extractInheritance on the wasm path, the kernel's java.rs and tsjs) now name a generic_type by its first child that is not type_arguments; any other supertype node is its own text, as before. Validation: the new tests fail 3 of 3 on main with both engines and pass here. A/B against main 0d46dba, kernel per arm: flask and Slim (PHP) byte-identical. mybatis-3 gains 107 supertype edges (extends BaseTypeHandler<T> 58 times, TypeHandler<T>, ResultHandler<T>, BaseMapper<T>...) with 31 interface-impl and inherited calls, 2 calls retargeted from another package's Mapper to the BaseMapper the interface extends, and 17 inherited calls typed. pixijs gains 43 implements (System<T>, RenderPipe<T>, InstructionPipe<T>...) and 11 interface-impl calls; excalidraw 2 implements DeltaContainer<T> and 4 interface-impl calls; vscode's base and editor 142 implements (IListRenderer<T>, IListVirtualDelegate<T>, ISpliceable<T>...) and 183 interface-impl calls. Nothing removed. Kernel parity holds on mybatis-3 (1,396 Java files), pixijs, excalidraw and vscode's base and editor, 0 diffs. Index CPU time within noise: mybatis-3 29.9 s to 30.1 s, pixijs 37.3 s to 38.1 s over 6 interleaved rounds in both orders (spread 35.8 to 38.5 s). The older PR colbymchenry#729 by @paiyahooJay made the same fix in the wasm extractor; it is credited in the CHANGELOG. Original PR colbymchenry#2705 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Go method was indexed as unexported, whatever the case of its name, in both engines: extractMethod never set isExported, and the kernel's go.rs mirrored the omission. Plain functions carried the flag, so dead-code results listed exported methods like Close as dead. A method that is a top-level declaration (methodsAreTopLevel, only Go) now gets the extractor's isExported, as a function does, and go.rs sets is_exported from the same name-case rule in extract_method. Added on top of the contributor's change: findGoPackageMember, which resolves pkg.Name through a project import, matched any exported node of that name in the package's directory, and methods stayed out only because none was exported. With the flag set, a package-level call like harbor's log.Errorf(...) went to the method Logger::Errorf declared beside the function, and prometheus's embedded storage.Appender resolved to a method named Appender, which dropped its implements edges. A method is no member of its package, so it is now skipped there. Two comments in name-matcher.ts that said the flag is unset on every Go method now say why the name's case is still read. Validation: the contributor's test fails on main and passes here, and the new package-call test fails with the PR as first written and passes here and on main, in both engines. A/B against main on harbor 296f304, prometheus a2fda0e, gin 0f09c3a and cobra adbc881: nodes differ only in is_exported 0 -> 1 on uppercase Go methods (5,704, 3,880, 391 and 141). Edges are identical on prometheus and cobra; 22 retargets remain, all name-only guesses between an interface's method declaration and the struct method implementing it, which now win: gin's engine().GET and its siblings reach RouterGroup's methods, and harbor's jobservice routes reach DefaultHandler's. The PR as first written had retargeted 1,384 harbor calls and removed 60 prometheus implements and dispatch sites. The dead-code report lists 3 Go symbols on harbor instead of 167, and 0 on prometheus instead of 11. Kernel parity: 2,493 of 2,493 Go files byte-identical. The controls express and flask are byte-identical. Index CPU on harbor, median of 5 interleaved: 40.8 s on main, 39.8 s here. EXTRACTION_VERSION stays: main already moved it to 28 after 1.6.2, so 1.6.2 indexes get the re-index hint anyway. Original PR colbymchenry#1734 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ument Since PR colbymchenry#1615 made arity part of an Erlang function's identity, arity is counted from the argument list's named children. tree-sitter reports a comment as a named child, so a comment at the top level of a parameter or argument list raised the count by one. A call written plain( % note followed by two arguments was looked up as plain/3 and linked to nothing, a definition with a comment between its parameters was indexed one arity too high, and the MFA list of spawn(M, f, [A, B]) with a comment between A and B gave f/3. A comment nested deeper, inside a tuple, a record or a list argument, was never affected. The three counts now go through one namedArgCount helper that leaves comments out: a clause's and a spec's arity, a call site's arity, and the MFA list's. Conflict resolution: the import block of tree-sitter.ts moved on main since the PR; main's imports are kept, with namedArgCount added. Validation: the new test fails on main (its fixture gets no call edge at all) and passes here. A/B against main: rebar3 97e9367 gains 2 edges (rebar_compiler.erl:403 and rebar_release_SUITE.erl:247, both calls whose argument lists hold a comment, both checked against the source), 0 removed, nodes unchanged; cowboy 6475600 and ranch 7ec436d are byte-identical, as are the controls express and flask. Erlang has no kernel extractor, so there is no parity to run. Index CPU on rebar3, median of 3 interleaved: 9.9 s on main, 9.3 s here. Original PR colbymchenry#1619 by @Dshuishui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r with its name (colbymchenry#2858) The scope matcher that the indexer, sync and the file watcher share also takes in the folders `git ls-files -o -i --exclude-standard --directory` reports as ignored (PR colbymchenry#1754, for issue colbymchenry#1728). They were added to it as gitignore patterns. git reports them relative to the root, but a pattern of one segment, like `src/`, has no anchor in gitignore syntax and matches at any depth. So an ignored root `src/`, holding only a gitignored jib agent, hid every module's `src/` in a Maven monorepo, and the index came up empty. As patterns, folder names were also read as syntax: `[draft]` as a character class, `a[b` as a regex that throws on the first match (the git listing then fell back to a filesystem walk, and the watcher threw on every event), `#tmp` as a comment and `!keep` as a negation. The reported folders now stay root-relative paths in a set, and a path is out of scope when it is one of them or lies inside one. Nothing is parsed, so there is nothing to anchor or escape. Escaping was not an option: in ignore 7.0.5 an escaped `?` never matches a literal `?`, an escaped backslash before a `.` turns the dot into a wildcard, and every match is case-insensitive. git prints these paths relative to its cwd, so for a project in a subfolder of a repo they are relative to that subfolder, the root the matcher is asked about. The CLI hint for gitignored nested repos wrote their paths into codegraph.json `includeIgnored` the same unanchored way, so an opt-in for `child/` also took in any later `archive/child/`. It now writes a root-anchored pattern with `*` and `[` escaped. Validation: - New git-ignored-dirs-scope tests (9): all fail on main and pass with the fix. They cover the issue's layout (scan, index, scoped sync), the lib/, build/ and dist/ variants, a `[draft]` folder, the watcher with a `scratch[1` folder, a project in a subfolder of its repo with `#tmp` and `!keep` folders, and the CLI hint end to end. - A/B with the kernel in both arms, on real repos after writing the root folders a local build writes, against git's own view (ls-files --cached --others --exclude-standard, less codegraph's defaults). Files indexed of those in scope, main then fix: stencil 1015 and 1707 of 1707, Sentinel with the reporter's jib layout 104 and 1423 of 1423, ant-design 2851 and 3138 of 3138, wger 767 and 770 of 770. The fix misses no file in scope and adds none outside it. - For Sentinel, ant-design and wger, the fix's graph on the built tree is byte-identical to main's on a clean checkout. ant-design's 53 changed call sites move `useLocale` from the docs site's hook to components/locale/useLocale.ts. In stencil, every edge the fix adds on the built tree is also in main's graph on a clean checkout, and the 62 name matches it no longer makes are unresolved there as well. - Controls gin and flask: byte-identical graphs. - Index time, median of 3 interleaved: gin 2572 and 2545 ms, flask 2400 and 2378 ms; the repos above take longer only by their restored files. The scope step with 486 and 599 reported entries (a `.DS_Store` in every folder): main 206 and 416 ms, fix 105 and 155 ms. Fixes colbymchenry#2849 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s ancestors getTypeHierarchy() walked a type's ancestors and then its descendants with one visited set. The ancestor walk marks the focal node first, so the descendant walk returned at its visited guard before reading a single incoming edge: no type ever had subtypes or implementors, in any language. Each direction now keeps its own visited set. The same walk feeds the hierarchy step of findRelevantContext, which codegraph_explore and codegraph context build on. That step promised subclasses within a budget of a quarter of maxNodes, but checked the budget only between entry points, which was harmless while the walk returned ancestors alone. With descendants coming back, one widely implemented interface brought its whole tree: vscode's IDisposable, in src/vs/base and src/vs/editor, has 613 types in its hierarchy, and codegraph context IDisposable then traded the interface's own dispose for an arbitrary implementor. Added on top of the contributor's fix: the budget now holds per node, and only edges between kept nodes are added. Validation: graph.test.ts gains a test that getTypeHierarchy() returns a base class's subclass and an interface's implementor; it fails on main. context.test.ts gains one that the hierarchy step adds an interface's implementors within its budget (12 nodes, no traversal: 1 to 3); main gives 0, and the traversal fix without the cap gives 10. On vscode's base and editor subtree, codegraph context and codegraph_explore keep main's size and speed (0.8 to 1.3 s), and the hierarchy they show is now the queried type's own: StandaloneCodeEditor and EmbeddedCodeEditorWidget for "ICodeEditor CodeEditorWidget". On prometheus, 3 of 4 explore answers are identical, and the fourth names more of storage.Appender's implementors. The contributor's codegraph implementations command and its test-code helper are not part of this change. This is the getTypeHierarchy() half of issue colbymchenry#2833. Original PR colbymchenry#2839 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mports, not a namesake (colbymchenry#2864) PR colbymchenry#2854 sends Java's and C#'s no-argument new Foo().bar() calls, which the chain conformance pass retries, to the constructed-receiver resolver. That links them (mybatis-3 +265, Newtonsoft.Json, serilog and jellyfin +36, all right), but the hint for which Foo it passed was a file path, which the resolver reads for these languages as a dotted package name, and C#'s using names namespaces, not classes. So of two classes named Foo the first indexed one won, whatever the file imported: import b.Foo; new Foo().bar() linked a.Foo::bar at 0.9, and so did a call the language itself cannot resolve, after import a.*; import b.*;. When several project classes share the name, Java and C# now pick as the language does: Java the written package (new a.Foo()), the file's single-type import, else the caller's own package; C# the one type the file's namespaces and usings can see. Only that class's own method links, otherwise nothing. A name only one class has resolves as before, inherited methods included. Validation: the new test fails 4 of 4 on main 44adef8 with the kernel and with CODEGRAPH_KERNEL=0 (import b.Foo linked a.Foo::bar, and the ambiguous calls linked a.Foo and A.Foo), and passes here with both; new-expression-receiver and kernel-tsjs-parity pass. A/B against main, kernel per arm: flask, mybatis-3, Newtonsoft.Json, serilog and jellyfin byte-identical, so the 301 links PR colbymchenry#2854 added there stay, mybatis-3's 2 calls on a class with a same-named copy in another package among them, which resolve to the caller's own package as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uess no target A member call on a nested receiver, like holder?.values.get(readKey()), has failed both ways. Before PR colbymchenry#1790, each such call was linked by its bare method name, to any project method called get. PR colbymchenry#1790 stopped that by dropping every call on a chain rooted at a name, which also dropped the call sites effects are drawn from (issue colbymchenry#1794). PR colbymchenry#1862 brought the dotted chains back as unresolved references; shapes its chain pattern rejects, like holder[0].get(), are still dropped. The test indexes eight receiver shapes in .ts, .tsx, .js and .jsx: optional access, computed and numeric keys, a quoted key, a comment inside the receiver, a non-ASCII name, an imported root and a namespace import. It checks that the readKey() argument call survives, that nothing links to Collision.get or to the imported object, and that the three dotted chains (holder.values.get, imported.values.get and data.holder.values.get) are still in the index, unresolved. That last check is ours. As first written, the test passed on PR 1790's commit de5adba, so it could not see the drop issue 1794 is about. With it, the test fails 4 of 4 on de5adba (the chains are gone) and 4 of 4 on its parent 72c1ff1 (every shape linked Collision::get), with the kernel and with CODEGRAPH_KERNEL=0, and passes 4 of 4 on main d02a88d with both. On main's code, putting the drop back fails it, as it fails release-main-regressions' retention test; letting the shapes the chain pattern rejects fall back to the bare method name fails only this test. The non-ASCII name is written with U+00F6 and U+00E9, Latin-1 letters, not homoglyphs. Test-only, so no CHANGELOG entry. Original PR colbymchenry#1844 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ail file is a link validatePathWithinRoot returned the lexical path whenever the path did not exist yet (ENOENT). With .codegraph/ui a link to a folder outside the project and no trails folder yet, the trails folder passed the check, mkdir followed the link, and the trail was written outside the project. The save still answered 403, but only because the list it sends back re-checked the folder that now existed. The trails list also joined each file name onto the checked folder and read it with statSync and readFileSync, which follow links, so a trail file linking to a trail-shaped file outside the project was listed and served. validatePathWithinRoot now judges a missing path by its nearest existing ancestor, and returns null when the root itself is missing. The list reads every file through the same per-file check as a single read, opens it with O_NOFOLLOW | O_NONBLOCK where the platform has them, and checks the open file with fstat. Windows has neither flag; there the per-file check is what refuses a planted link. A save re-checks the folder after mkdir and writes its temp file under a random name with flag wx and mode 0600. The trail author is cached per project instead of per process, and a trail file that declares an unknown format version is skipped. Added on top of the contributor's change: the link tests also run on Windows (a junction and a directory symlink at .codegraph/ui, a file symlink in the trails folder), each test removes its own links before deleting its folder, the helper gets a junction case and three controls (an in-root link above a missing leaf, allowSymlinkEscape above a missing leaf, a missing root), its doc comment names the new missing-path rules, and the viewer changelog lines are reworded and moved next to the other saved-trails line. Validation: 8 tests fail on main and pass here (the helper below a symlink and below a junction, a missing root, a save below a junction and below a symlink, a linked trail file, the per-project author, the format version). Through the real CLI (codegraph ui), main wrote the trail outside the project through a junction and through a directory symlink, and listed a linked outside file with its note; this branch refused all three and wrote nothing outside. Graph dumps of express, zod, excalidraw, flask and gin are byte-identical to main's, and 55 CLI read commands and 45 MCP tool calls answer the same (codegraph_status differs only by its last-indexed time). Index time is unchanged (gin over 9 interleaved rounds: 2645 ms on main, 2687 ms here). Listing 200 saved trails, the list's cap, costs 108 ms on main and 236 ms here on Windows; 10 trails cost 6 ms and 14 ms. Original PR colbymchenry#1667 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and accessors
Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.
The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.
An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.
Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.
Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
main; the sealed-permits and name-only-guess cases are each red without
their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
sweeps at full parity with deferrals unchanged (jextract 218/218,
spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main d02a88d, every bucket triaged by sampling (edge sites
added/removed): jextract +207/-1, spring-modulith +375/-1, playframework
+80/-2, Minestom +17,963/-287, spring-ai +8,806/-471, kestra
+10,077/-497; most retargets are a constructor function becoming its
record. The accessor rule drops only name-only results (spring-ai 1,063,
kestra 765, Minestom 408 sites at 0.65-0.8 confidence); no typed call,
method reference or qualified call moves. click and gin byte-identical.
- Cost (process CPU, median of 3 interleaved indexes): Minestom
47.2s -> 50.3s (+6.6%, for 17% more nodes and 23% more edges),
spring-ai 93.5s -> 94.8s (+1.4%).
Original PR colbymchenry#1946 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e deadline The handler keeps one idle timer, armed for the oldest open project a trim can release, and it skips a project whose catch-up is still running. scheduleIdleRelease returned early while a timer was armed. So when a project's catch-up settled after a newer project had armed the timer, the trim found the older project not yet idle and kept the newer one's deadline. The older project stayed open, writer lock included, until then: up to nearly twice the idle timeout. With the 10-minute default, a project last used at 0:00 whose catch-up settles at 9:50, while another was used at 8:20, went at 18:20, not 10:00. Each trim that gets as far as scheduling now replaces the timer, re-armed for the oldest releasable project. Trims skip scheduling while calls are in flight, and the last call to finish trims again, so a settled catch-up still gets its own deadline. There is still one timer and no polling. Validation: the new fake-timer test drives that order through execute(). On main a565f21 nothing is released at 3.6 s (expected [] to deeply equal [ 'a' ]); with the fix A goes at 3.5 s and B at 4.5 s. Its afterEach restores both CODEGRAPH_* variables the test sets, also when the test fails: checked with the test failing on main's code, with the variables preset and with them unset. Follow-up to issue 2087, whose idle release (PR colbymchenry#2283) shipped in 1.6.2. Original PR colbymchenry#2306 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codegraph_explore's maxFiles parameter said "(default: 12)" and its
schema carried default: 12, but the handler picks 4, 5 or 8 files by
project size (clamped to 1 to 20), and nothing applies 12. A client that
fills in schema defaults sent maxFiles 12 and overrode the size-based
default. The description now gives the real default and says total
output is also capped in characters by project size, and the schema
default is gone. The query description no longer tells the agent it
needs no prior codegraph_search, a tool not on the default list.
explore-output-budget's small-tier test asserted that the output does
not contain "Complete source code is included above", text the
completeness note no longer has, so it could not fail. On this fixture
the note reads "Complete source for 1 file is included above", which
the PR's replacement ("files is included above") misses too. It now
checks "is included above", and the medium-tier test checks the note is
there, so a rewording can't leave the absence check behind again. With
the small tier's completeness note switched on, main's assertion and
the PR's both still pass, and this one fails. The interface comment
names the note by its current wording.
Left out of PR colbymchenry#1972: its rewrite of the server instructions' "Trust
codegraph's results" line, which is agent steering and needs an A/B
first; and its codegraph_node hint and footer changes, which main
already has (PRs colbymchenry#2783, colbymchenry#2077).
Original PR colbymchenry#1972 by @bompus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matchByExactName scored a winner among several same-named candidates by directory proximity alone: 0.7 when the files share two folders, else 0.4. findBestMatch already ranks the definition in the reference's own file first, so the target was right, but a shallow file's own function read as an uncertain guess (below 0.6), and because the resolver keeps the most confident strategy, the same-file definition lost to a framework's naming guess in another file (ASP.NET's PascalCase and Service rules at 0.7 and 0.85). isDecisiveSameFileMatch now scores it 0.9, as a lone candidate, when it is the only candidate in the file, is not the caller itself, and, as a member of a type, module or namespace, belongs to one the caller is in. The PR checked that last rule for methods only. We extended it to every member kind whose owner the file declares, other than a function, so a sibling enum's entry or a sibling class's field is not raised. Validation, main vs this commit, 21 repos in 11 languages (flask, click, fastapi, gin, cobra, etcd, ripgrep, jq, Slim, serilog, eShopOnWeb, nowinandroid, okhttp, http4s, playframework, spring-modulith, express, zod, bulletproof-react, excalidraw, vben): - no edge added or removed; - 9 targets moved, all in eShopOnWeb and all right: C# properties and fields named after their type that the code reads (Color Color), which ASP.NET's naming rule had linked to the class in another project; - 6,927 edges keep their target and gain confidence, 535 of them from 0.4; in 874 a framework rule's 0.7-0.85 now loses to the same target by exact name; 30 end in another file, Go type positions the type gate moves to the package's own type; - as written, the PR also raised 77 edges onto another type's member to 0.9, about 48 of them wrong (okhttp's InterceptorOverridesTest enum entries, playframework's sibling providers' `get`); they keep main's confidence. Index CPU, median of interleaved runs: okhttp 32.3 vs 32.4 s, etcd 33.4 vs 33.1 s. Tests: 3 of 7 fail on main; the PR as written fails the sibling-member case. js-class-expressions pinned the old 0.4 for `this.run()` reaching the class's own `run` in a shallow file; it now expects 0.9. Original PR colbymchenry#1938 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names The Play resolver claims every Class.method reference in a Play project, in routes files and in ordinary Java and Scala code alike, and took the first method of that name anywhere in the class's file. In a file holding several classes (Java nested classes, a Scala class with its companion and siblings) that was usually another class's method: MediaType.parse(...) went to Cookie.parse, a route controllers.Application.index to a nested Helper.index, and Scala RequestAuthority.parse(...) to AuthorityPort.parse. A method now counts only when its qualified name is the named type's own, cls::method. A Scala object is an owner too, so a companion's own members are found through it, and for a call in code so are a Java interface's and enum's static methods. For a call written in code, the types of the caller's language are tried first: Play declares RequestAuthority both as a Scala case class and as a Java record, and each language's callers mean their own; the other language's types still follow, since Play's Java API calls its Scala classes. Among same-named types the caller's own file comes first (Scala names carry no package), an overloaded member gives the overload the call's arguments fit, and a Java file's import of the name from outside the project leaves the call to the generic resolver. A route keeps the plain order and takes a class or object. An inherited member is left to the generic resolver, which finds it through declared supertypes. Validation: - frameworks-integration.test.ts: the PR's case (nested Http.Cookie / MediaType, Scala sibling objects, a route beside a nested Helper.index), red on main; ours, Scala and Java callers of a RequestAuthority declared in both languages (red on main and with the PR's own resolver) and an inherited route handler; and a guard for overloads, same-named objects in two packages and a Java interface's static method. - A/B against main cde4cd7: playframework +113 / -257, 510 retargeted; CMAK 47 retargeted, all first-in-file picks corrected; play-samples -2; click and gin identical. The 50 Scala parse calls that records sent to the Java record reach their Scala type again. - Cost: playframework process CPU, median of 3 interleaved fresh indexes, 63.3 s on main and 62.8 s here. Known costs, left as follow-ups: - 177 right edges main reached only through its first-in-file rule are lost: 171 Assets.resourceNameAt calls in AssetsSpec through a local val Assets = new AssetsBuilder(...), which the generic resolver drops because it reads Scala's private[controllers] as file-private, and 6 Writeable.writeableOf_* calls inherited by object Writeable extends DefaultWriteables, an object's supertypes the generic resolver does not follow. - 8 calls in Scala files that import Play's Java EventSource, Comet, Environment or (in a nested scope) Accumulator reach the same-named Scala object: no Scala import is read. Original PR colbymchenry#1950 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g itself over build:ui and build:lib ran npm run build --workspace ui. Under bun, a package script's npm run becomes bun run, and bun ignores --workspace, so build:ui ran the ROOT build again, which ran build:ui again. bun run build:ui on main nests without end: with bun 1.3.8 here, 13 bun processes deep after 62 s, when a watchdog killed the tree. npm is unaffected, which is why it went unnoticed. The scripts now name the workspace by directory: cd ui && npm run build (and cd ui && npm run build:lib), which runs the same ui script under either tool. Validation: with the fix, bun run build, build:ui and build:lib finish by themselves in 16 s, 6 s and 6 s, and check-ui-build and check-ui-package pass. npm run build plus npm run build:lib give the same output before and after, under npm's default script shell (cmd.exe here) and under bash (npm --script-shell bash): dist/ (1,260 files) and ui/dist (171) are identical by SHA-256 in all four runs, built in one checkout from clean outputs, and bun's build gives that same tree. check-ui-build and check-ui-package pass in every run. scripts/pack-npm.sh still runs npm run build:lib --workspace ui; that runs only under npm, in the release workflow, so it is left as is. Original PR colbymchenry#1764 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reconcile the fork's native Rust extraction/resolution architecture with upstream through 69af19a (94 commits past the prior merge point 553d5d1). All 57 conflicts resolved: upstream's deleted TypeScript resolver spine stays deleted and its behavior was ported into the kernel or surviving modules. Kernel ports and fixes: - new X().m receiver resolution (upstream newReceiver arm) into resolve_call_chain + match_new_receiver_call; C# isCsharpTypeVisible. - Dart typedef classes and type-alias refs into dart/ walkers. - Java record classes/components (java/records.rs), parameterized supertypes, is_class_type for compact constructors; Java typeParameters parity in the generic extractor. - Ruby singleton-method is_static flags; ruby qualified-name fixes. - Go colbymchenry#2596 scoped fn-ref candidate threading (Cand.node) + flush, StrRef/uses; Go type refs demote to `references` (colbymchenry#2581). - TSJS call extraction: this[...] subscript receivers, new-expression receivers, is_unresolved_member_chain, window exclusion removed to keep fork window.Api.start emit; visit_parameter_defaults + visit_function_body alias. - Python `from X import Y` module-file binding in the import resolver. - PHP namespace-alias case-insensitive lookups in file scope and resolve_php_qualified_class. - ts_class_type_parameter: stop truncating generic heads at `{` (Shaped<Item extends { id: string }>). - Pascal bare function-reference capture retained (upstream's addressOfOnly assumed pascal-scope.ts, which the kernel covers). - C macro NAME(a)(b){ shape kept by isCReturnTypeMacro (colbymchenry#2773 shape). Restored fork behavior the merge had dropped: - cg_folded_contains SQLite registration + defensive nested transaction rollback in the adapter. - searchNodesFTS two-phase ranked query head (malformed union had silently disabled FTS). - C/C++ blankers: comment stripping, elifdef, expression guards, const/volatile traversal in followsDeclaredName. - Nuxt route-group/interior-index normalization and server-route method-suffix handler extraction. - Explore-budget advisory mock (getSizeStats), release-workflow comment rewording. Adapted instead of restored: - resolution.test.ts: upstream-only API describes excised (kernel covers the behavior); upstream parity suites for the deleted TS resolver were removed, as was the wasm check-grammar script/test. - jvm-overloads.ts carries the two name-matcher helpers play.ts still needs. Golden dumps rebased (UPDATE_GOLDEN=1): all deltas traced to intended upstream ports — Ruby is_static, Java record classes, Dart typedef classes, Go references demotion, JS duplicate-node fix, Pascal read-refs. Known remaining gaps (upstream-new suite coverage, documented for follow-up): rust nested `self.field.method().outer` chains resolve the outer member to the field's type instead of the inner call's return type (1 test); a set of upstream-new language suites still fails (CFML include/create-object/inheritance, Ruby namespaced receivers/singletons, JS super calls, Pascal paren-less calls) and needs dedicated porting passes.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
There was a problem hiding this comment.
bompus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
…lution to the kernel The merge at 69af19a left 43 upstream-new tests failing where the kernel resolver lacked the behavior upstream added to its TypeScript resolver. All are ported semantically into Rust; every suite is green. - Ruby namespaced constants and singleton methods (colbymchenry#2503, colbymchenry#2827, colbymchenry#2834): resolve/ruby_paths.rs carries matchRubyConstantPathRef — `Billing::ChargeCard`, `::Foo.call` resolve to project constants by lexical-then-top-level lookup, calls through the owner's Ruby ancestry, and an unknown path resolves to nothing instead of a same-name guess. rubyMethodsOfKindFirst/`CallerKind` split `def self.m` from `def m` for class receivers, instance receivers typed by `.new`, and bare calls inside a method of the same kind. - Pascal calls without parentheses and the colbymchenry#2803/colbymchenry#2810/colbymchenry#2816 scope rules (colbymchenry#2514): resolve/pascal_scope.rs ports pascal-scope.ts — read/ref/recv modes, unit and used-unit scope, include files, nested routines, system intrinsics, overloads and declared types — and the extraction's surviving function-value capture is flipped to address-of only, as its comment anticipated. - CFML include, createObject and suite-mirrored inheritance (colbymchenry#2650, colbymchenry#2651, colbymchenry#2652): includes resolve their template by path then by file name (never the includer), `createObject("component", path)` and calls chained onto a construction resolve through the component path resolver, which now skips test suites for production callers, breaks their ties toward the component, and ranks a one-folder mapping onto the project root. - JS `super.m()` calls: matchJsSuperCall resolves through the enclosing class's extends/implements ancestry in js_objects_upstream, and the fork's super-self gate now reads the receiver at the ref's position so `this.m()` beside `super.m()` on one line keeps its own edge. - Rust `self.field.m1().m2`: `()`-suffixed refs anchor to the chain's last call inside rust_written_call, tuple-struct fields type through rust_field_written, and named roots walk standalone calls in rust_chain_member. Golden dumps re-baselined: the pascal torture fixture's paren-less refs resolve, and a Ruby call upstream scores 0.9 now scores 0.9 in the kernel instead of 0.4.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reconciles the fork's native Rust extraction/resolution architecture with upstream
mainthrough69af19ac— 94 commits past the prior merge point553d5d10. Upstream's deleted TypeScript resolver spine stays deleted; its behavior was ported into the kernel (codegraph-kernel/src/resolve/) or surviving modules.57 conflicts resolved: 46 both-modified, 11 modify/delete, 1 no-base. Golden dumps rebased — every
.dumpdelta traced to an intended upstream port.Ported into the kernel
new X().mreceiver resolution (upstreamnewReceiverarm) intoresolve_call_chain+match_new_receiver_call; C#isCsharpTypeVisible.is_class_type+ parameterized supertypes (java/records.rs); JavatypeParametersparity in the generic extractor.is_static; Go#2596scoped fn-refCand.nodethreading; Go type refs demote toreferences(Go: three smaller call-edge inaccuracies (type conversions, external test packages, build tags) colbymchenry/codegraph#2581).this[…]subscript andnew-expression receivers,is_unresolved_member_chain,visit_parameter_defaults.from X import Ymodule-file binding; PHP namespace-alias case-insensitive lookups (file scope +resolve_php_qualified_class).ts_class_type_parameterno longer truncates generic heads at{(Shaped<Item extends { id: string }>).addressOfOnlyassumespascal-scope.ts, which the kernel covers); C macroNAME(a)(b){shape kept byisCReturnTypeMacro(fix(c): a function whose return type is a macro call is indexed colbymchenry/codegraph#2773).Fork behavior restored after the merge dropped it
cg_folded_containsSQLite registration + defensive nested transaction rollback.searchNodesFTStwo-phase ranked query head — the malformed union had silently disabled FTS for all queries.elifdef, expression guards, const/volatile traversal).indexnormalization + server-route method-suffix handlers.getSizeStats); release-workflow comment rewording.Adapted rather than restored
resolution.test.ts: upstream-only API describes excised — the kernel covers the behavior natively. Upstream's parity suites for the deleted TS resolver and the wasmcheck-grammarscript/test removed.src/resolution/jvm-overloads.tsadded carrying the two name-matcher helpersplay.tsstill needs.Known remaining gaps (follow-up work, not regressions)
43 tests fail in upstream-NEW suites — these exercise upstream features the kernel doesn't yet implement; they need dedicated porting passes, not merge fixes:
cfml-include,cfml-create-object,cfml-inheritance-resolution): 10ruby-namespaced-receiver,ruby-singleton-methods): 19 — includes(wasm)describes that can never pass on the fork (wasm removed); the(kernel)variants show real namespaced-constant gapsjs-super-calls: 3 —super.m()edge emissionpascal-parenless-calls: 10 — upstreampascal-scope.tsparen-less call linkingrust-receiver-chains: 1 — nestedself.field.m1().m2resolves the outer member to the field's type instead of the inner call's return typePreviously verified green (focused runs this session):
python-module-resolution50/50,function-ref44/44,nuxt-routes27/27,ts-array-annotated-receivers14/14,php-namespace-alias-instantiation9/9,kernel-generic-extractor-treeparity,expo-modules,resolution.test.ts,expression-receiver-calls,cs-twin-class,kernel-typed-styled,esm-externals.Test plan
cargo clippy --profile quick --libclean; TypeScript typecheck cleanUPDATE_GOLDEN=1); each.dumpdelta explained in the commit messagenpm run build:kerneloptimized build +EVAL_REPOS=… npm run eval:precision)README rows checked
69af19ac(README:55). No user-visible feature/CLI/MCP/language/agent-target changes; no quoted numbers moved.