Commit Graph
1736 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 37052b1825 fix(go): a package-level var or const named in uppercase is exported (#3043)
Both extractors created a Go var/const node from its var_spec or
const_spec without exportedness: the TS Go branch passed no isExported
(the declaration-level value is read off the var_declaration, which has
no name), and the kernel's extract_variable set none. Every such node was
unexported, so findGoPackageMember, which looks up pkg.Name among the
package's exported declarations, skipped it: a call through
`var Hook = func...` from another package linked to nothing, though the
same call inside the package linked to the variable.

Both engines now mark the node exported by its own name's case, read
from the spec through goExtractor.isExported / is_exported, as
functions, methods and types already were.

Validation:
- __tests__/go-package-value-exported.test.ts, 16 tests (extraction on
  the kernel and wasm, LF and CRLF; cross-package calls on both paths):
  12 fail on the base main, 6 with only the TS half (the kernel
  checks), all pass with the fix.
- A/B, main vs fix, kernel path: gin 108 var/const flags, 0 edges;
  prometheus 625 flags, 0 edges; etcd 806 flags and 12 new calls,
  all right (e2e.GetVersionFromBinary x11 and
  model.NonDeterministicModel, both `var X = func(...)`); flask and
  express byte-identical. The fix's kernel and wasm graphs are
  byte-identical on all three Go repos, and the wasm A/B matches.
- kernel-parity --lang go: gin 99/99, prometheus 755/755, etcd
  1065/1105 with 40 deferred on main too; 0 diffs.
- etcd index time, median of 3 interleaved: 13.4 s main, 12.6 s fix.

Reimplements PR #1521 by ericquan8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:01:18 +00:00
Colby MchenryandClaude Opus 5.5 c12a7b6a74 fix(explore): Chinese, Japanese and Korean names reach explore, its Flow and context (#3031)
The index stores a name in any script as one full-text token, and
`codegraph query 로그인` found it, but every tokenizer between a question
and the index knew only ASCII: extractSearchTerms split on [^a-zA-Z0-9]
and kept 3+ characters, context's extractSymbolsFromQuery matched ASCII
patterns only, and explore's exploreQueryTokens and the Flow's flowTokens
required a leading [A-Za-z_$] and 3+ characters. So `explore 寻路服务`
answered "No relevant code found", `context 로그인` printed only its
header and `explore 入口 末端` had no Flow, while their ASCII twins worked.

Shared helpers in search/query-utils now read words in any script whole:
letters, digits and combining marks, cut where Han, kana or Hangul meets
another script (NavMeshAgent类型 is NavMeshAgent and 类型, never
agent类型), at least 2 characters in Han, kana or Hangul and 3 elsewhere,
so Russian на and из stay out. extractSearchTerms and the explore and
Flow tokens use them. Context adds names with a non-ASCII letter as whole
runs, and a mixed name like get用户信息 as written, with no n-grams, so
a partial Chinese word still never becomes a search of its own (PR #2005).
The ASCII-only patterns see a word with accented or other non-ASCII
letters blanked out, so état is no longer read as tat. A Han, kana or
Hangul token counts as a deliberately named identifier, as such a name
has no case or underscore to show it, so a query of such names is
neither damped as prose nor flagged low-confidence.

Validation: new cjk-query-tokens tests (12) cover CJK, mixed, Cyrillic
and accented words, reference copies of the old tokenizers over an ASCII
corpus, and explore and context end to end on Korean, Chinese and
Japanese fixtures; 9 fail on main's source. Explore and context A/B with
214 ASCII queries (gin 76, Alamofire 79, express 59; 49 lead with a
Flow): all byte-identical. Of 12 non-ASCII queries, 6 changed on gin and
4 on express, as intended: état, Señal and naïve are read whole, and
handleHTTPRequest处理 now names handleHTTPRequest. Tokenizing a
10,000-character query takes at most 1.8 ms, against 0.1-0.3 ms before.

Reimplements PR #516 by @andy-sg and PR #1262 by @yimingll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 05:51:15 +00:00
Colby MchenryandClaude Opus 5.5 9f379af79b fix(node): every language and folder of an overloaded name shows in codegraph_node (#3030)
codegraph_node's multi-definition answer rendered a name's definitions in
index order (getNodesByName: file_path, start_line) until its caps ran
out: 16 bodies within 12,000 characters, then 20 list lines, then a bare
"+N more". Forty definitions under an early-sorting folder hid the only
one in another language or a late folder: on the triage fixture (40
TypeScript `num` in aaa_client/, one Python `num` in zzz_server/),
zzz_server/format.py appeared nowhere. Without includeCode, a long list
was cut by the output cap at "... (output truncated)".

The bodies still fill in index order, so every overload that rendered
before still renders. On top of them, each language with no body gets its
first hand-written definition rendered, while the whole answer stays
under the output cap. The list of the rest fills its 20 slots in a new
definitionCapOrder (the first definition of each language, then of each
top-level folder within a language, then the rest, each tier in index
order) and is shown in index order; the overflow line names what it
leaves out by language and folder, like "+365 more: 365 typescript in
aaa_client/". The list-only answer is unchanged unless it is longer than
the output cap; then it fills in the same order and names the rest the
same way. Generated stubs stay behind hand-written definitions.

Promoting each folder's first definition into the bodies, as first
tried, cost Alamofire's `validate` three Validation.swift overloads to a
Tests/ helper, and `adapt`, `urlSession`, `encode` and `cancel` one or
two Source/ bodies each, so folders get early list slots only.

Validation: new node-overload-spread tests (6): 5 fail on main's source
(the fits-every-cap guard passes on both) and all pass with the fix. A/B
of every multi-definition name, with and without includeCode, main vs
fix: gin 171 names, 170 identical (`_` only names its overflow);
Alamofire 706 names, 702 identical. No body that rendered before is lost:
`cancel` adds the jQuery `cancel` from docs/ as a 13th body, and
`request`, `urlSession` and `encode` swap one Source/ list line for the
first Tests/ one and name their overflow. Alamofire's `validate`, `task`
and `didCompleteTask` and gin's `reset`, `Bind` and `Render` are
byte-identical.

Reimplements PR #1657 by @ferrine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 05:30:27 +00:00
Colby MchenryandClaude Opus 5.5 60b0d7a92d fix(affected): a --filter glob matches whole names, from any folder boundary (#3029)
`codegraph affected --filter` turned its glob into an unanchored regex by
hand (`**` to `.+`, `*` to `[^/]*`, no `^` or `$`), so it matched anywhere
inside a path: `tests/**/*.py` missed `tests/x.py` (`.+` needs a folder),
a changed `tests/x.py.bak` counted as a test under `tests/*.py`, the
changed test itself was missed under `tests/**/*.py`, `e2e/*.spec.ts`
took an `xe2e/` folder, and `/tests/*.py` matched only `other/tests/...`.

The filter is now a picomatch glob (an existing dependency, loaded only by
this command) with `dot: true`, matched against the whole project-relative
path. A pattern with no slash matches the file name at any depth. One with
a slash matches from any folder boundary, an implicit leading `**/`, which
keeps the help text's own `e2e/*.spec.ts` matching `apps/web/e2e/...` as
the filter always did; a leading `/` or `./` anchors it at the project
root, and a trailing `/` takes everything under that folder. A pattern
picomatch rejects is reported as an invalid --filter instead of failing
later.

Validation: new CLI-level cli-affected-filter-globs tests (6): 4 fail on
main's source and pass with the fix; the other 2 guard behavior kept on
purpose (file-name patterns at any depth, folders outside the test
conventions, dotfiles, a trailing-slash folder). The existing affected
CLI suites pass. The triage fixture shows the same before and after.

Reimplements PR #1273 by @Jseca666.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:26:45 +00:00
Colby MchenryandClaude Opus 5.5 3d19da2f59 fix(spring): a route links the method it annotates, including a package-private one (#3009)
The Spring extractor found a mapping's handler with a pattern that wanted
`public`, `private` or `protected` (or Kotlin's `fun`) before the name, in
the next 600 characters. A package-private handler, like Spring Boot 3's
`@GetMapping List<Article> list()`, matched nothing, so its route linked the
next method declared with a modifier, or nothing at all, and a method-level
`@RequestMapping(value = "/feed", method = GET)` on such a method made no
route. A generic Kotlin `fun <T> find()` handler fell through the same way.
Separately, a route bound by name alone took the wrong overload when a tall
annotation stack (`@Operation`, `@ApiResponses`) above the mapping left the
next overload's declaration nearer the annotation.

The extractor now reads the declaration the annotation is written on: past
it and every annotation stacked with it (arguments read as balanced groups,
strings and text blocks included), the modifiers (none at all for a
package-private method), type parameters and the return type (void, a
primitive, a qualified or nested generic type, an array) to `name(`; for
Kotlin, past its modifiers to `fun`, type parameters and `name(`. A field,
a constructor or a class under the annotation gives no handler. The mapping
annotation's own arguments are read the same balanced way. At resolution,
the route's handler reference binds to the method of that name whose span
(which starts at its first annotation) holds the annotation's line, the
innermost such method; any other case resolves by name as before.

Validation, kernel engine, main 01e0ac3a vs this commit:
- realworld-java21 93e018e: Spring routes linked 4/19 -> 19/19; the 15 new
  edges each go to the method the mapping is written on.
- spring-petclinic 500158f (17 routes), spring-petclinic-kotlin 817dba3
  (18), eladmin 55fbf70 (133), mall dcaa93b (246), dolphinscheduler
  21968bc (239): same targets, only the edge metadata changes (exact-match
  0.7/0.9 -> framework 0.95).
- Controls jsoup 0c508f1 (Java, no Spring), requests 3c7baaa (Python),
  cobra adbc881 (Go): dump-graph byte-identical.
- Index time, median of 3 interleaved: mall 12.3 s vs 12.6 s, eladmin
  3.02 s vs 3.03 s (noise on a shared machine).
- New tests fail on main's java.ts (20 failing) and pass with the fix, in
  both engines; dropping the positional binding fails only the overload
  test.

Reimplements PR #851 by @Liurchen, who found the package-private gap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:06:35 +00:00
Colby MchenryandClaude Opus 5.5 8e854d11e5 fix(js,ts): a function exported as exports["name"] is a symbol named after its export (#3032)
`exports.getItems = async (req, res) => {…}` names its function after the
export property, in both engines (since the CommonJS controller fix), but
the same export written with the name in quotes, `exports["getItems"] =
…` or `module.exports['deleteItem'] = function () {…}`, left the function
anonymous: no node, so nothing could find it or link to it, and its calls
counted as the file's. commonJsExportName (tree-sitter.ts) and its kernel
twin common_js_export_name (tsjs/extractors.rs) accepted only a
member_expression on the left of the assignment.

Both now also accept a subscript_expression on `exports` or
`module.exports` whose index is a string literal spelling a plain
identifier. The test for that string, which `this['svc']` receivers
already used in both engines, is now one helper per engine
(quotedIdentifier / quoted_identifier) shared by the two readers. A key
that no identifier can spell (`exports["not-a-name"]`), a computed key
and a template string still name nothing.

Validation: the new commonjs-exports cases fail on main and pass in both
engines, and a parity case compares the kernel's and wasm's nodes and
refs for the JS and TS forms.

Reimplements the bug part of PR #2896 by @johnhldiver-beep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:40:52 +00:00
Colby MchenryandClaude Opus 5.5 a38a1ec0c0 fix(paths): a folder named like ..generated is inside the project, not outside it (#3028)
Several checks asked whether a path relative to the project root leaves
it with rel.startsWith('..'), which is also true of a folder whose name
starts with two dots, like `..generated/client.ts`. So a tsconfig paths
alias or baseUrl into such a folder linked nothing, the file watcher
dropped every edit made there (and the Linux per-folder watch tree never
skipped a gitignored one), a gitignored folder of that name could not be
brought back with codegraph.json include, a .gitignore rule for one was
never applied on the filesystem walk of a project that isn't a git
repository, and deprioritize could not name one.

One escapesRoot(rel) helper in utils.ts now answers it: `..` itself, a
first segment of `..` (`../`, or `..\` from path.relative on Windows),
or an absolute path, which path.relative gives for another Windows drive
(applyAliases did not reject that case before). It replaces the bare
prefix test at both path-aliases.ts sites, the watcher's handleChange
and shouldIgnoreDir, the extraction include walk and nested-gitignore
check, and the deprioritize matcher.

Left as they were, since a folder named that way is not a real input
there: Python source roots from config values and C/C++ include
directories in import-resolver.ts, Laravel require targets, and the
workspace-package targets, entries and path dependencies in
workspace-packages.ts.

Validation: new dotdot-folder-names tests (9) fail 8 of 9 on the base
and pass with the fix; the helper's own unit test runs against the new
helper in both. A/B against main on bulletproof-react, excalidraw,
outline, vben, gin and flask: byte-identical (none has a folder named
that way).

Reimplements PR #1406 by @amplifierplus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:17:18 +00:00
Colby MchenryandClaude Opus 5.5 f1da755a91 fix(resolution): calls on the browser's own objects link no project method (#3026)
`window.open('/help')`, `localStorage.getItem('token')`,
`location.reload()` and `history.back()` linked to a project class's
method of the same name, like an app's `WindowService::open` or
`StorageService::getItem`, so callers and impact on such a wrapper
listed every direct browser call in the app. Three readings did it:
- an untyped receiver went to matchMethodCall's guesses, by the
  receiver's word (`window` ~ WindowService) or a lone method name;
- a chain from `window`, which the extractor records by its bare method
  name so a project object hung on `window` can be found (#1707), fell to
  the exact-name match when the chain names no project object:
  `window.localStorage.getItem()`;
- a JS `self.close()` was read as `this.close()` by bareCallReceiver and
  callReceiverKind, though a `self` the file doesn't bind is the global
  object (a worker's scope, or `window.self`).

Now, in the JS family:
- a call on one of JS_BROWSER_HOST_GLOBALS (window, self, globalThis,
  document, localStorage, sessionStorage, location, history, navigator,
  screen, performance) gets no edge when the file writes that name only
  as a receiver (`document.x`, `history.back()`, `self?.close()`) or after
  `typeof`, decided before the receiver-type reader, whose backward scan
  has no scope. Any other mention (a parameter, destructured ones
  included, a declaration, an import, an argument) keeps the name the
  file's own: outline passes its Document model as `({ document }:
  Props)`, and those `document.save()` calls keep their links;
- matchCollapsedObjectCall declines a `window`/`globalThis` chain whose
  first link is one of them, after its project-object lookup; a chain
  through a project object (`window.App.init()`) is unchanged;
- an unbound `self` reaches matchCollapsedObjectCall as
  `globalThis.self`, and callReceiverKind no longer counts it as the
  instance. A `self` the file binds (`var self = this`) reads as before.

Validation: new browser-host-globals tests (4) pass with the fix. On
main the first fails with 8 wrong edges and the third (outline's
destructured `document`, a `typeof window` guard) with 2; the two that
pin what must keep working (a bound, imported or parameter name; `this`,
`var self = this`, `window.App`) pass on both. A first version that
asked only whether the file binds the name dropped outline's
destructured `document` calls; the third test fails against it.
A/B against main: only removals. excalidraw 12 (`window.open` ->
Portal::open, `document.createElement` -> DraggableTool::createElement,
a worker's `self.postMessage` -> WorkerPool::postMessage), outline 21
(test and app `sessionStorage`/`localStorage` calls -> MemoryStorage,
`window.document.body.*`, `window.history.back()` -> a Props property),
bitwarden apps/browser + apps/web 7 (`window.postMessage`,
`window.location.reload()`, `window.navigator.permissions.query()`), and
express unchanged. 39 of the 40 linked a project method by name only;
the 40th linked outline's own `declare global` typing of a browser API
(Document.modelContext.registerTool).

Reimplements PR #2915 by @KissFeng.

Fixes #2907

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:57:27 +00:00
Colby MchenryandClaude Opus 5.5 38dc3741aa fix(lookup): a qualified symbol query names its scope whole (#3024)
`codegraph callers Format.Price` also selected `LegacyFormat::Price` and
merged its callers into the answer. matchesSymbol, which serves the CLI's
callers/callees/impact, codegraph_node and codegraph_explore's symbol
seeding, accepted a qualified query anywhere inside the qualifiedName
(`qualifiedName.includes('Format::Price')`), so any class whose name
ends with the qualifier matched. A second way in: a path segment could
stand for any qualifier, so a `Format.cs` holding `LegacyFormat` let it
match too.

The match now has to end the qualifiedName on a scope boundary, under one
canonical separator, which the existing boundary stage already did. The
raw substring test also covered two spellings that stage did not: a PHP
namespace's backslashes (`App\Models\Billing::Format::price`), now read
as separators on both sides, which also makes `Billing\Format::price`
work, and an Erlang arity (`my_server::start/0`), now dropped before the
comparison. A path qualifier no longer names the type of a type member
(method, property, field, enum member) unless it is that type's own
name, except in Rust, Python and Go, whose modules live only in paths. A
free function keeps its file or folder qualifier (`utils.formatDate`).

Query-time only: no re-index.

Validation: new qualified-lookup-boundary tests (7, including the CLI
and codegraph_node on the issue's repro) fail 6 of 7 on main and pass
with the fix. On real indexes, `callers` targets: eShopOnWeb
BasketService.AddItemToBasket 2 to 1 (the IBasketService declaration
no longer merged in), laravel ServiceProvider.register 37 to 1,
Store.get 14 to 2, and Support\ServiceProvider::register from not
found to 1. A differential oracle over seven indexes (C#, PHP, TS,
Vue, Go, Python) asked every boundary-aligned suffix of every member's
own qualified name, 201,582 queries: none stopped matching. Its cross
queries only drop matches, mostly classes whose name ends with the
qualifier.

Reimplements PR #2914 by @yang0228.

Fixes #2908

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:31:57 +00:00
Colby MchenryandClaude Opus 5.5 787eafbffa fix(jvm): a call through an import reaches the Java or Kotlin class it names (#3023)
A Java or Kotlin import was matched against a file path written with the
calling file's own extension (com/example/Foo.java from Java, .kt from
Kotlin), so an import across the two languages never bound: Kotlin's
JUtil.make() on an imported Java class went to a same-named class in
another package, JUtil.Nested.deep() and Outer.Inner() linked nothing,
and Java's KUtil.INSTANCE.kmake() went to another package's object.

The import is now read by qualified name in either language (the
package is some prefix of the name, so import p.Outer.Inner is
p::Outer::Inner), and the rest of the reference walks the imported
type's nested types: a Kotlin object's INSTANCE and a class's Companion,
as Java writes them, step onto the type, whose companion members the
index keeps on the class. Java's UtilsKt.helper() reaches the top-level
function of p/Utils.kt, and import p.UtilsKt links that file. A call
picks the member's method over a same-named field or nested class
(jextract's Index_h.clang_getRange() is the static method, not the
holder class of that name), the overload its argument count fits
(Minestom's NetworkBufferTemplate.template(a, g, b, h, ctor)), and, from
Kotlin, a nested Java class's own constructor. A Kotlin chain rooted at
an imported type is walked before the receiver-chain typing. Only an
imported type takes this path; an imported function or property is left
to the name strategies, which know a call's receiver.

A Kotlin extension declared in another package (fun Modes.hidden(),
indexed as Modes::hidden) is now a candidate only from a file that
imports it or its package, as Kotlin has it.

Validation:
- jvm-cross-language-imports.test.ts: 8 tests, all red on main, green
  here (cases from #1948's test and the T5 triage fixtures).
- A/B against main 86c85cff (the base, 8bd55fa7, gives identical graphs
  on jextract and nowinandroid, as 4955eac9 did there and on okhttp),
  sites added/removed/retargeted, with confidence raised at the same
  target:
  okhttp +172/-0/30 (167 raised), SystemUI +83/-0/25 (642), mihon
  +21/-26/28, nowinandroid +24/-0/5, jextract +96/-96/18 (calls that
  were instantiations of a same-named holder class now call the static
  method), spring-modulith 0/0/16, Minestom 0/0/843 (799 onto the
  overload their argument count fits, none against it), spring-ai
  +14/-12/73 (50 overloads; 22 onto the imported provider's nested
  class), playframework 0/0/37, kestra 0/0/302 (290 overloads); click
  and gin identical. Added: Java calls into Kotlin companions and objects
  (SystemUI's PhysicsAnimator.getInstance, okhttp's RequestBody.create),
  Kotlin calls into Java classes, type references across the two.
  Removed: guesses onto extensions a file cannot see (mihon's
  JsonObject.toByteArray on a String) and calls that become
  instantiations of a nested type.
- Cost (3 interleaved fresh indexes): mihon CPU 27.7 s on main vs
  27.5 s, wall 8.6 vs 8.7 s; okhttp CPU 31.5 s on both, wall 10.0 vs
  9.8 s.

Reimplements #1948 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:09:01 +00:00
Colby MchenryandClaude Opus 5.5 57dc7ae481 fix(search): path: and name: filters find their matches at any limit (#3018)
QueryBuilder.searchNodes applied `path:` and `name:` only to its final
list, after every candidate source had cut its rows to the limit: full-text
search to max(limit*5, 100) rows, the substring fallback to `limit`, the
typo fallback to 50 names, a filter-only search to the first limit*5 names
in name order, and the exact-name supplement to 20 rows. Whenever enough
symbols outside the filters ranked first, nothing the filters asked for was
left, so `codegraph query "authenticate path:product/"` found nothing at
limits 1, 2 and 10, and found the match at 200. That broke the query
parser's contract: filters narrow the set and the search ranks within it.

Every source now applies the filters before its own limit and offset. Its
statement is streamed and JavaScript decides each row, so matching keeps
toLowerCase's Unicode folding and literal substrings (SQLite's lower() and
LIKE fold ASCII only, and LIKE reads % and _ as wildcards). A page and its
offset count only the rows that pass, and the read stops once the page is
full. To spare JavaScript the rows that cannot pass, each statement also
carries a SQL pre-filter that keeps every row the filters could pass:
instr(lower(...)) for a printable ASCII value, which agrees exactly with
JavaScript on printable ASCII text, while text with any other character
passes through to JavaScript. The typo fallback looks names up until it
has its results instead of giving up after 50 names, and skips names that
`name:` rules out. A filter that no symbol's name, or no symbol's file,
passes returns nothing at once, checked against the memoized name list and
a memoized list of file paths. The old final filter stays as a safety net,
now before the cut. A search without these filters runs the same SQL as
before, except that a filter-only search now honours its offset (the same
rows at offset 0, which every caller in the codebase passes).

Validation:
- Kit repro through `codegraph query --json`: on main all 18 filtered
  queries (6 queries at limits 1, 2 and 10) returned nothing; with the fix
  each returns its match.
- 9 new tests in search-field-filters.test.ts; 7 fail on main's source.
  Mutations caught: each of the five candidate sources left unfiltered, the
  offset ignored, the non-ASCII pass-through removed or kept only for
  non-ASCII values, and the JavaScript check removed.
- Latency A/B, main vs fix in one process on one index, 5 interleaved
  rounds of the median of 5 calls: vscode src/vs/base + src/vs/editor
  (56,406 nodes) and a 10x replica of it (564,060 nodes). The 45 unfiltered
  queries (explore's kinds at limit 16, plain limit 10) return identical
  ids and scores: 89.7 vs 88.4 ms and 435.7 vs 441.1 ms in total. On the
  replica the filtered queries now find their matches in 0.2-26 ms; the
  slowest are a substring fallback that finds nothing (156 ms, what an
  unfiltered miss already costs) and `kind:method path:...` (116 ms). A
  filter nothing passes answers in 0.2 ms instead of reading every row.

Reimplements PR #2254 by @rudycelekli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:48:18 +00:00
Colby MchenryandClaude Opus 5.5 8bd55fa75c fix(frameworks): a component's same-folder preference compares whole folders (#3020)
The React, Svelte, Astro and Vue component resolvers each picked among a
name's definitions by preferring one whose path started with the
referencing file's folder, compared as text with no separator:
`src/ui/Card.tsx`'s `Widget` took `src/ui-kit/Widget.tsx`, since
`src/ui-kit/...` starts with `src/ui`. A file at the project root has the
folder `''`, which every path starts with, so its pick was whichever
definition came first, skipping the rule that a name with several
definitions and nothing to choose by links nothing (#764).

One helper, inReferencingFolder (frameworks/component-folder.ts), now
serves all four copies: a candidate counts when its path starts with the
folder plus `/`, so a folder below the referencing file's still counts,
and a root file has no folder to prefer.

Validation: new component-same-folder tests (7) fail 6 of 7 on main, one
per resolver plus the root and subfolder cases; the helper's unit test
runs on the new module in both. A/B against main: outline, mastodon
(app/javascript), excalidraw, vben, shadcn-svelte (docs/src) and gin
are byte-identical, since real code imports its components and the
framework pick runs only for a name the file doesn't import.

Reimplements PR #683 by @Mubashirrrr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:30:39 +00:00
Colby MchenryandClaude Opus 5.5 da1f26380f fix(index): a repeat full index keeps the name words of files it didn't change (#3017)
A library indexAll() over an existing index cleared the name-segment
vocabulary (the words in symbol names that the prompt hook and
codegraph_explore match plain-language questions against) before
extraction. The store skips files whose content hash is unchanged, so their
names were never written back: a second run with no edits left the table
empty, and a run after an edit kept only the edited files' names. Sync and
healSegmentVocabIfEmpty() repair only a completely empty table, so a
partial one stayed that way. A cancelled run emptied the table as well,
because the clear ran before the abort check. The CLI was not affected:
`codegraph index` recreates the database first.

The table is now cleared up front only when the database is fresh. Over an
existing graph it is kept, and once a run completes without being
cancelled, one DELETE drops the rows of names that no symbol carries any
more (file and import nodes don't count, as on the write path). That keeps
the orphan cleanup the clear used to do. The prune also resets the
session's set of names already written, so a pruned name that comes back
later in the same session gets its rows again instead of being skipped.

Validation:
- Kit repro with the PR's fixture: on main a second indexAll() took the
  table from 11 rows to 0, an edit plus a third run left 4, and a
  cancelled run emptied it. With the fix it stays at 11 rows, the renamed
  class's 3 rows give way to the new name's 3, and a cancel keeps them all.
- 5 new tests in segment-vocab.test.ts; 4 fail on main's source, and the
  fresh-database guard passes on both. Three mutations are caught: no
  prune, no reset of the written-names set, no clear on a fresh database.
- CLI: `codegraph init` then `codegraph index` write the same 11 rows on
  main and with the fix.
- Real repo, vscode src/vs/base + src/vs/editor (1,388 files, 56,406
  nodes, 66,250 vocabulary rows), a library indexAll() over the existing
  index, 3 interleaved runs per arm: main empties the table (66,250 to 0
  rows); the fix keeps all 66,250. The prune takes 40 ms of each run, and
  the runs take the same time (median 8.37 s on main, 8.13 s with the fix).
- Cost at scale: the prune statement takes 288 ms on a 10x replica of that
  index (564,060 nodes), and about 1.2 s on a synthetic graph with 505k
  vocabulary rows (170k distinct names); a full re-index of that size
  re-parses every file.

Reimplements PR #2252 by @rudycelekli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:06:05 +00:00
4955eac958 fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules (#3014)
A bare Rust name took the file's `use`s all at once: a `use` inside one
function (`fn first() { use crate::util::take; take() }`) bound `take` for
every other function too, and for the file's own `fn take`; a renamed one
(`use crate::util::take as consume; consume()`) linked nothing, since no
item carries the alias; a parameter or closure of the function's name
(`fn run(take: fn()) { take() }`, `let take = || {};`) linked to the
imported function; and `use crate::models::E::Good; Good(1)` took any
`E::Good`. Paths through modules (`resolveRustPathReference`) walked the
directories instead of the modules: `super::helper()` and `crate::config()`
with no module between linked nothing, `super` inside an inline
`mod tests { }` went to the parent directory's module, and an item a module
passes on (`pub use`, a glob) or another workspace crate's was not found.

The fix keeps one `use` reader, rustUsesOf:
- each `use` keeps the block it is written in (a function's body or a
  block; an inline `mod` and a macro's braces, `cfg_if!`'s nested ones
  included, are the module's), and std `use`s are kept by position;
- a reference reads the binding of its innermost scope (rustVisibleUse);
  isRustNameInScope uses it, so a block's `use` shadows the file's own item
  and a variant `use` names its enum's variant (rustUsePathItems' enum tail);
- matchRustUseBoundName, ahead of the pre-filter, resolves a rename to the
  item its path names, and a call to a name a local binds (rust-scopes'
  bindings, nearer than any `use`) to nothing, unless the local is a `let`
  holding a function by its path (serde's `let test = test::<i8>;`);
- resolveRustPathReference is rustWalkModules + rustModuleItems from the
  module the path is written in (rustPathTarget), with today's crate-root
  fallback; resolveRustModuleFile, collectRustUseBindings and
  rustItemPlacement are gone, and isBoundToOutOfRepoImport reads
  rustUsesOf's std `use`s;
- `use`s are read with strings blanked (blankRustCode), so a string's
  "use crate::x;" binds nothing;
- the crate graph reads a package's name by its section (axum-core writes
  `categories = [..]` first) and a `[lib]`/`[[bin]]` path of its own
  (ripgrep's `crates/core/main.rs`), which `crate::` then starts from; a
  file no manifest covers starts it from the nearest lib.rs or main.rs
  above it, as the directory walk did.

Validation:
- rust-use-call.test.ts (PR 2308's 15 cases), rust-use-scopes.test.ts,
  rust-crate-roots.test.ts and 5 new rust-module-path-call cases: on main's
  source 18 of 32 fail; all pass. Every rust-* suite passes.
- A/B on main 01e0ac3a, 7 repos, the same sites on the kernel and on wasm:
  tokio +2,434 sites (1,354 calls, 1,053 imports), 159 removed (152 locals
  called or passed as values, 6 `Result`s std's, 1 import through a path
  attribute module); ripgrep +248 / -149; axum +710 / -60; clap +730 / -112;
  serde +155 / -207 (192 calls to a closure named `test`); dioxus
  +2,950 / -272; gleam +1,573 / -359. Groups and samples inspected, every
  changed site's crate checked against the manifests' dependencies;
  express and flask byte-identical.
- Cost (kernel, medians of 3 interleaved): tokio CPU 34.5 -> 33.0 s, peak
  RSS 991 -> 1,007 MB; gleam 32.5 -> 30.6 s, 1,022 -> 1,012 MB; dioxus
  45.4 -> 45.4 s, 1,101 -> 1,107 MB.
- Original PR #2308 by @danusha2345; its module-path part landed as PR #2955.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:39:13 +00:00
Colby MchenryandClaude Opus 5.5 e7c9ffd068 fix(security): an MCP session no longer opens a sensitive folder as its default project (#3012)
The MCP server's default project comes from the client (initialize's
rootUri or first workspaceFolders entry, or its roots/list answer), the
launch cwd, or --path. Only an agent's projectPath argument went through
the sensitive-folder guard of PR #2929: the default root was opened with
no check, and opening it also started a file watcher and a catch-up sync,
and from the launcher a shared daemon keyed on that folder. A session
whose default was ~/.config, ~/.ssh or a system folder holding an index
served the files inside to the agent. (On main a warm query pool happened
to refuse such a call with isError; before it warmed, the main thread
served it.)

resolveServerRoot, which every default-project resolution goes through
(the engine's open and its per-call retry, the initialize handshake and
the launcher's daemon root), now applies the refusal a projectPath gets:
on the path searched from when it exists, and on the root it lands on,
by up-walk or by the sub-project down-scan. A refused root resolves to
null with the reason, so the session has no default project. Nothing
there is opened, watched or synced; no daemon, lock or socket is set up
in the folder; initialize sends the no-default-project instructions; and
a call without projectPath gets the usual success-shaped "pass
projectPath" guidance, saying why. A call that names the folder keeps its
PathRefusalError, and sub-projects below a refused folder aren't listed.
--path is refused the same way, as `codegraph ui` refuses its root, but
the server stays up with no default project instead of exiting, so the
agent keeps codegraph for its other projects. A daemon started by hand
for such a folder exits. The sensitive-folder half of validateProjectPath
is split out as sensitiveDirectoryRefusal; validateProjectPath behaves
as before.

Validation on Windows 11:
- 6 new tests in security.test.ts. On main's sources every entry point
  is red: the engine opened ~/.config from the folder, a folder inside
  it, a missing folder under it and a junction to it, serving a fake
  token and taking writer.pid; rootUri, workspaceFolders, cwd and
  roots/list sessions served the token; the down-scan adopted or listed
  sub-projects; a launch from the cwd started a daemon in the folder.
  The ordinary-project control passes on both. All 6 pass with the fix.
- security.test.ts: 70 passed, 4 skipped (POSIX and macOS gates). 51
  MCP, daemon, proxy and directory suites: 646 passed, 0 failed.
- The real serve --mcp against a private HOME, from a sensitive cwd and
  with a sensitive --path: no-default instructions, the guidance instead
  of the token, no daemon.pid or writer.pid. An ordinary project is
  unchanged.
- Cost: resolveServerRoot 141 -> 377 us per call on an ordinary project
  (median of 7 rounds of 2000 calls). It runs at the handshake, the
  engine open and the launcher, and per tool call only while a session
  has no default project.

Follow-up to PR #2929.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:23:06 +00:00
artttttandClaude Opus 5.5 86c85cff02 fix(kotlin): a call on an imported type reaches that package's class when two share a name
When several classes share a name, resolveMethodOnType picks the one the
call file imports by matching the import's path against the candidate's
file path (com/example/b/FooConverter.kt). That holds for Java, where a
public class names its file, but a Kotlin class often lives in a file
named otherwise (FooConverter in Converters.kt), so no candidate matched
and the call went to whichever class came first.

The imported name is now matched against each candidate's qualified
name first: import com.example.b.FooConverter expects the method
com.example.b::FooConverter::convert, which the package directive gives
every JVM declaration whatever its file is called. The file-path match
stays as the fallback.

The Java calls this once fixed, a Java file calling a Kotlin class it
imports (okhttp's Java tests and mockwebserver3.MockWebServer,
SystemUI's BlurUtils), are now resolved on main by the receiver's
declared type (#1942), so on today's main the change shows in Kotlin.

From #539 we take only this change and its test. Its other two parts,
indexing Kotlin class properties and reading Kotlin imports without a
trailing semicolon, have since landed on main another way.

Validation:
- resolution.test.ts: the Kotlin same-name case (#314's shape in
  Converters.kt files) is red on main, green here.
- A/B against main 40f58517 (with the Java receiver typing of #1942
  and the Kotlin changes of #1933, #1761 and #2258): no edge changes
  on okhttp, SystemUI, nowinandroid, mihon, jextract, spring-modulith,
  Minestom, spring-ai, kestra-java, playframework, click or gin; none
  of them has the test's shape on a resolved call. Against main
  4eb8c2a5, before #1942, it moved the 8 Java calls above onto the
  Kotlin class their file imports; main now reaches them at 0.9.
- Cost (mihon, median of 3 interleaved indexes, on main 4eb8c2a5): CPU
  25.7 s on main vs 25.2 s, wall 8.2 s vs 8.3 s.

Original PR #539 by @arttttt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:56:48 +00:00
danusha2345andClaude Opus 5.5 40f5851787 fix(kotlin): a call on an outside type reaches the project's extension on it
A Kotlin import from outside the project owns its name (#2196), so
Modifier.fillMaxSize() after import androidx.compose.ui.Modifier never
lands on a same-named project method. That rule returns before any other
strategy, so a project extension on the imported type, Compose's
fun Modifier.pad() (indexed as Modifier::pad), could not be reached by
Modifier.pad() either, nor Color.fromHex() on a
fun Color.Companion.fromHex().

Inside the rule, a Kotlin Type.member call now resolves to a project
extension Type::member that the calling file can see (same file or
package, or imported by name or with .*, as for other top-level Kotlin
declarations) and whose own file imports the same type. Anything else
stays unlinked, as before.

Validation:
- kotlin-extension-on-imported-type.test.ts: 2 cases, red on main,
  green here.
- A/B against main 13f983db (with the Kotlin receiver typing of #1933
  and #1761's private rule): mihon +26, all Compose Modifier
  extensions the project declares (secondaryItemAlpha 15,
  clickableNoIndication 5, selectedBackground 2, two scrollbars,
  keepInSection), nowinandroid +2 (Modifier.notificationDot,
  DeviceConfigurationOverride.WindowInsets), all right; okhttp,
  SystemUI, jextract, click and gin identical. The same sites as
  against main 4eb8c2a5, before #1933.
- Cost (mihon, median of 3 interleaved indexes, on main 4eb8c2a5): CPU
  25.7 s on main vs 25.6 s, wall 8.2 s vs 8.4 s.

Original PR #2258 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:42:25 +00:00
danusha2345andClaude Opus 5.5 13f983db5a fix(kotlin): a private property, and the members of a private class, stay in their file
A Kotlin property was indexed without its visibility modifier, so the
Kotlin file-local rule (#1745), which declines a private fun in another
file, read every property as public: a call like token(...) could link
to another file's @Volatile private var token, and JavaScript, Go or
Python callers to a Kotlin test fixture's private val. A public member
of a private class or object was reachable from other files too, though
nothing outside its file can name it.

Both extractors (wasm and the native kernel) now read a property's
modifiers the way they read a function's, and a candidate inside a
private Kotlin class or object is declined from another file. That
second rule is Kotlin's alone: a Scala private[pkg] class, or one with
a private constructor, is its package's, and a C# private nested class
serves every file of its partial class. Applied to every language where
private is file-local, as the PR had it, it removed 742 edges on http4s
(right ones among them, like stream.receiveHeaders() on a private[h2]
class H2Stream), 91 on scalachess and 10 on serilog (9 of them right
typed edges).

Validation:
- cross-file-visibility.test.ts: 2 cases, red on main, green here; the
  kernel parity fixture gains a private val and an internal var.
- Kernel parity (kotlin), 0 diffs: okhttp 588/619 files, mihon 891/940,
  nowinandroid 340/350, SystemUI's Kotlin 188/198 byte-identical, the
  same files deferred to wasm as with main's kernel; torture 1/1.
- A/B against main 01e0ac3a (with the Kotlin receiver typing of #1933):
  okhttp -274, mihon -177, SystemUI -94, every one a call from another
  file onto a private Kotlin property or a member of a private class or
  object (okhttp's private inner AndroidQuery.enqueue and
  RealWebSocketTest's private TestStreams, mihon's private IntentFilter
  filter that collections' filter { } reached, SystemUI's private
  MyExecutor.execute); nothing added. nowinandroid, jextract, click and
  gin identical; against main 4eb8c2a5, http4s, scalachess and serilog
  were identical too.
- Cost (mihon, median of 3 interleaved indexes, on main 4eb8c2a5): CPU
  25.7 s on main vs 25.5 s, wall 8.2 s on both.

Original PR #1761 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:07:50 +00:00
danusha2345andClaude Opus 5.5 01e0ac3aa7 fix(kotlin): a call through a property or a receiver chain resolves on its declared type
Kotlin indexes a class property without a signature, and a
primary-constructor property (class A(private val repo: Repo)) is no
node at all, so a call through either (probe.close() on a lateinit var
probe: HttpProbe) found no receiver type and fell to the name-only
strategies: the interface's method, or any same-named method elsewhere.
A call through a chain (engine.pump.drain()) was emitted as its bare
method name, and a local bound to a qualified call (val lease =
HardwareLock.tryBegin()) was untyped. tree-sitter-kotlin also predates
fun interface: such a declaration parsed as a broken function and, with
a doc comment after it, its error recovery swallowed the next
declaration too.

The declared type is now read from the declaration itself: a property's
own lines, the class header for a constructor property, a callee's
declared return type for a local bound to a call, an enum entry's enum,
each segment of a receiver chain in turn (the chain is kept in the
reference name, by the wasm extractor and the native kernel alike), a
superclass's property, and an outer function's local inside an
anonymous object. A receiver whose type the project does not declare
(Regex, Properties, a JDK class) gets no edge instead of a name-only
guess, unless the project extends that type. The extractor's preParse
blanks the fun of a fun interface, offset-preserving, so the file parses
cleanly in both paths.

Validation:
- kotlin-property-receiver.test.ts: 17 tests, 12 red on main, all
  green here; kernel-kotlin-parity.test.ts pins the fun interface
  fixture.
- Kernel parity (kotlin), 0 diffs: okhttp 588/619 files byte-identical
  (main's kernel 578, the rest deferred to wasm on parse errors), mihon
  891/940 (887), nowinandroid 340/350 (339): the fun interface files
  no longer defer.
- A/B against main with #1942 (positions only: the extractor now keeps
  a chain in the reference name): okhttp +299/-257, 2,554 retargeted,
  2,158 of them test calls moving from the deprecated
  okhttp3.mockwebserver.MockWebServer (a 0.65 name guess) to the
  mockwebserver3.MockWebServer each file imports; mihon +82/-118, 337
  retargeted (Preference get/changes/set/register onto the declared
  Preference); nowinandroid -9, 23 retargeted (properties onto their
  concrete test doubles); SystemUI's Kotlin +1/-104, 7 retargeted;
  jextract, click and gin identical. The removed are name guesses on
  library types (okio sinks, collections' filter and clear, OkHttp
  bodies, MutableStateFlow.update, Kotlin's apply). 7 right edges are
  lost where a chain passes through a library wrapper (Lazy.value,
  StateFlow.value, Pair.first, a ViewBinding's view).
- Cost (mihon, median of 3 interleaved indexes): CPU 25.8 s on main
  with #1942 vs 26.7 s, wall 8.3 vs 8.4 s.

Original PR #1933 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:14:52 +00:00
Colby MchenryandClaude Opus 5.5 77ee3484be fix(aliases): a paths wildcard no longer matches an import that leaves nothing for its * (#2969)
A tsconfig/jsconfig `paths` key with text on both sides of its `*`, like
"@x/*/index", matched an import that starts with the prefix and ends with
the suffix only by counting one character twice: `@x/index` starts with
`@x/` and ends with `/index` through its single `/`. applyAliases tested
startsWith and endsWith independently and sliced an empty capture, so the
import and every call through it linked to `widgets/index`, a file the
import never named.

The pattern is now skipped when the specifier is shorter than the prefix
and the suffix together, which is TypeScript's own isPatternMatch rule. A
less specific pattern can still match, and an empty capture at exactly
prefix + suffix length stays allowed, as tsc allows it.

Validation: new path-alias-overlap tests (4) fail 3 of 4 on main and pass
with the fix; the exact-length case passes on both. A/B against main on
bulletproof-react, excalidraw, outline and vben (all with paths aliases)
and gin and flask: byte-identical, as no paths key in them has text after
its `*` (0 of 44 configs with paths in our local clones). outline index
median of 3 interleaved: 19,165 ms main, 18,546 ms fix. Full suite: 9,226
passed, 0 failed, 85 skipped.

Reimplements PR #882 by @Dashsoap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:48:20 +00:00
danusha2345andClaude Opus 5.5 7973df0d09 fix(java): a call resolves on its receiver's declared type, and a library type gets no guess
Java typed a call receiver from a local declaration or a plain field of
the enclosing class only. A field of an outer class read inside an inner
or anonymous class (mHandler.post(...) inside new Runnable() {...}),
Outer.this.f, a chain of fields (this.mOwner.mRepo.save()) and a local
whose type has type arguments fell through to the name-only strategies,
and so did every receiver whose declared type is a library class:
parcel.readInt() on an android.os.Parcel went to a project
VersionedParcel.readInt, list.size() to any project size().

The declared type is now read for each of those shapes, a chain one hop
at a time with each hop's own imports, keeping a written package or
outer class (Map.Entry, play.api.mvc.BodyParser). A receiver whose
declared type the project does not declare, or that an import or a
written package pins outside the project, gets no edge. When no import
decides between same-named types, a Java call prefers the Java
declaration over a Scala or Kotlin one.

Also, from review: a field the class around the call declares is read
as written before the member reader, which keeps only the last name
(PauseTest.Suite, a field written with its package); a local or loop
variable keeps the package its declaration writes; a field typed by a
type parameter goes to its bound, and a class the class head names as
a type argument (Comparable<ApplicationModule>) is not taken for one; a
nested type a superclass declares, or one an on-demand import of its
class names (import play.mvc.Http.*), counts as named; and a typed miss
on a type no project class or interface is named (a URI, an enum's
name()) gets no guess, as on main.

Validation:
- java-receiver-type.test.ts: 19 tests, the PR's 5 red on main; the
  other 14 pass on main, and our 8 each fail without their change.
- A/B against main 25776014 (with #2942 and #2950; the same sites
  change against 7cceba52), sites added/removed/retargeted: jextract
  +2/-1/106, spring-modulith +1/-3/2, Minestom +52/-91/71, SystemUI
  +53/-319/96, spring-ai +69/-659/532, kestra-java +5/-504/162,
  playframework +0/-196/599; click and gin identical. Of the removed
  edges main had at 0.9, 1,004 were wrong (an import oracle plus 16
  read by hand) and 4 right (a lambda parameter, two nested types of a
  Scala object, a field read under a static import of a same-named
  member). Retargets land on the declared type or the supertype
  declaring the method, except about 16 build()/clone() calls on
  spring-ai's nested option builders, which move from one wrong target
  to another.
- Cost (3 interleaved rounds): spring-ai wall 27.9 s on main vs
  25.9 s, CPU 90.2 s vs 88.1 s. SystemUI read 71.3 vs 79.4 s wall
  under other jobs' load; CPU profiles of both arms put the new code at
  about 0.6 s of 115 s.

Original PR #1942 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:30:24 +00:00
Colby MchenryandClaude Opus 5.5 72f398af6e fix(index): a video named .ts counts as skipped, so a full index isn't PARTIAL (#2943)
The full index's batch reader recognises an MPEG transport stream named
`.ts` from its first bytes (Issue #1910) and skips it, but that branch
counted the clip only as processed. Every other file the scan finds lands
in filesIndexed, filesSkipped or filesErrored, and CodeGraph.indexAll
checks those tallies against filesDiscovered, so the clip left the run one
file short. `codegraph init` and `codegraph index` then warned "Index is
missing 1 of N discovered files ... The index is PARTIAL", stored
index_state = partial, which `codegraph status` reports, and left the
indexed commit unrecorded, so status fell back to its full scan. The
branch now counts the clip in filesSkipped, as the branch below it does
for a file gone from disk.

Validation:
- mpeg-ts-not-typescript.test.ts: a new case (a clip beside one source
  file) fails on main's source with the tallies at 1 of 2, and passes
  with the fix: no index_partial warning, index_state complete. The
  file's other 11 cases pass.
- CLI: on main, `init -y` of app.ts plus testdata/clip.ts prints the
  PARTIAL warning, status --json reads state "partial", and in a git repo
  indexed_at_commit stays empty. With the fix it prints "Indexed 1 files",
  the state is "complete" and indexed_at_commit is HEAD.

Follow-up to Issue #1910, reported by @Hetox; found while fixing
Issue #2904 (PR #2928).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:04:24 +00:00
5594843698 fix(go): a call on a value bound from a call or a conversion reaches its type's method (#2961)
A Go method call on a local bound from a call or a conversion, like
`r := newRing(); r.Reset()`, `v1 := router.Group(...); v1.GET(...)` or
`c, r := CreateTestContext(w); r.Use(...)`, was matched by the method's
name alone: inferLocalReceiverType reads a declaration's written type and a
few line patterns, none of which reads a call's result. The by-name guess
took a namesake (etcd's `lg := s.Logger()` linked `lg.Info(...)` to a
project logger's Info, prometheus's `NewWriteHandler(...)` results to the
read handler's ServeHTTP), or found nothing.

Main already types a value from its declaration for indexed and chained
calls (goDeclaredValueType, #2782). Plain receivers now use it too: a local
whose in-scope declaration has a call or a conversion as its value takes
that value's type (the k-th result of several, a method's result, a
conversion's type), read in the file that writes it (GoReceiverDecl.file),
for matchMethodCall, matchMemberFunctionRef and matchGoFieldChainCall. A
value of a type from outside the project links nothing, including one a
project function returns as an outside interface. Assertions stay with
goAssertedLocalType, and a type switch's guard is not typed from its value.

The typer itself, for every path that uses it:
- a trailing `.(T)` is a T whatever its prefix (`pool.Get().(*Conv)`);
- a call through a func-typed field gives the field's result type;
- a bare call a dot import brings in is typed (`package watch_test`
  calling `NewStreamWatcher(...)`);
- a method of a struct or interface literal, of an outside generic type
  (grpc's `ServerStreamingClient[Resp].Recv()`) or one the index doesn't
  show on a project type gives an unknown type, not "outside", so name
  matching runs as before.

Validation:
- go-receiver-type.test.ts (the PR's 23 cases, 2 expectations updated where
  the call is now typed, plus 4 ours), go-std-method-receivers.test.ts and
  go-chained-receivers.test.ts: on main 19 of 51 fail; all pass here. All
  45 go-*.test.ts files pass (491 tests).
- A/B vs main 25776014 (same kernel): gin +194/-52/65 moved, etcd
  +4,441/-359/743, prometheus +7,041/-239/1,409, a kubernetes staging
  subtree +1,154/-86/354, harbor without its portal +423/-138/366.
  express and flask byte-identical. Sampled additions right; removals are
  outside values (zap, os, grpc, otel, beego, pflag) and the outside-
  interface results, about 80% of which were wrong by sample. Known loss:
  a type defined over an interface (`type Codec Serializer`) has no methods
  in the index, so a coincidental guess on such a value goes, as in #2706.
- Compared with PR #1954 on the same repos: 4,962 more links and 360 of
  its edges gone, those mostly wrong guesses (166 of them the outside-
  interface results); its declaration-line restriction also lost right
  links on range variables and field values, which this leaves to main's
  scan.
- CPU (profile self time of receiver typing): prometheus 1.82 -> 1.72 s,
  k8s subtree 0.88 -> 0.92 s, harbor 1.02 -> 0.86 s.

Reimplements PR #1954 by @danusha2345 on main's value typer; its tests are
included.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:56:46 +00:00
danusha2345andClaude Opus 5.5 3f8e8b0f65 fix(rust): a module-path call links the function its module declares
A Rust path into a module, `crate::util::take(1)`, `super::util::take(1)`,
`util::take(1)` or a `use crate::util::take;`, went to the first node of
that name in the module's file, methods included, so an `impl Buf { fn
take }` above `pub fn take` caught every such call. A function written
inside another function, or in a `mod tests { ... }` above the module's
own item, could take it too, a path into the module's own file linked
nothing, and a path through a module a `use` takes from the standard
library (`fs::read` under `use std::fs;`) went to the project's own `fs`
module.

The path's item is now the module's own: never a method, an impl's or a
trait's associated item, or an item of a function body; one written at
the file's top level before one in a module the file declares inline,
which the path reaches through the module's re-export (`#[cfg(unix)] mod
imp { ... }` + `pub use imp::*;`). A path whose head a `use` binds from
the standard library is left to it (main's isBoundToOutOfRepoImport rule
for bare names), and a path into the module's own file resolves.

Validation:
- rust-module-path-call.test.ts: 8/8; on main 7 fail (method Buf::take,
  Deep::get, Only::grab, no import edge, the inline-module namesake, the
  re-export from its own file, std `fs::read` -> src/fs/mod.rs).
- A/B on main 7cceba52, 6 Rust repos: 162 changed sites, all inspected and
  right. gleam: 133 calls like `fs::write(...)` / `crate::fs::mkdir(...)` move
  from `impl ... for ProjectIO`'s methods to compiler-cli's free functions,
  4 wrong `type_::list(...)` -> `Type::list` edges are gone, 15 `use`s link;
  tokio: `crate::io::uring::open::open(...)` moves from `Op::open` to the free
  `open`, +4; dioxus +7, 1 more; ripgrep +1; clap and serde unchanged.
  express and flask byte-identical.
- Cost (medians of 3 interleaved runs): tokio CPU 36.6 -> 36.0 s, gleam
  34.3 -> 35.2 s, both within the runs' spread; peak RSS unchanged. In a
  CPU profile of tokio the changed functions take under 0.1 s. The
  standard-library check reads the file's `use`s, so it runs only when the
  path would otherwise link a project item (asked for every path, it cost
  0.3 s on tokio).

This is the separable part of PR #2308. Its bare-call bindings (`as`
aliases, block-local `use`, shadowing by locals, enum variants through a
`use`) and the second `use` parser it adds are left out: main's `use` walk
(rustUsesOf, rustWalkModules, rustModuleItems) and scope reader
(rust-scopes.ts) cover the same ground, and folding those cases into them
is proposed separately.

Original PR #2308 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:34:51 +00:00
Colby MchenryandClaude Opus 5.5 69cddc0156 docs(changelog): credit the earlier pull requests for three unreleased fixes (#2956)
While closing first-time contributors' pull requests whose fixes had
already landed, three were found to predate the fix that shipped, and
their close comments promised changelog credit:

- @meganemura's PR 1503 wrote installer config through symlinked files
  2.5 months before the fix for issue 2721.
- @rudycelekli's PR 2251 preserved Hermes settings after the MCP server
  block three weeks before the fix for issue 2485.
- @equipados's PR 1641 linked unqualified paren-less Delphi calls six
  weeks before the fix for issue 2514.

Changelog only; no code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:34:25 +00:00
danusha2345andClaude Opus 5.5 25776014e9 fix(resolution): a call on the caller's own class reaches that class's member, or what it inherits
Root cause: a bare call in Java or Scala, and a `self.`/`this.` call in
Python, TypeScript and JavaScript, reaches matchByExactName as a bare name.
With several same-named methods in reach, findBestMatch ranked them by file
and line distance, so a sibling class's method declared nearer the call won
over the caller's own class's or the one it inherits; above the 500
candidate ceiling the call was dropped.

Fix (contributor): matchEnclosingScopeMember runs before the ranking. It
reads the receiver back from the call line (bare, self/this, super) and
walks the caller's scopes innermost-out: the own class, then what it
inherits, then, for a bare call, the classes it is nested in. A member found
there is the answer (0.9), and its overloads are ranked among themselves.
A super call in a JS class expression is left to js-super-calls (PR 2497).

Ours:
- Inherited members first come from the declaration's own resolved
  supertypes (getSupertypeNodes), in the order its header lists them, a
  class further up before an interface that only declares the member; the
  PR's walk by name stays as the fallback when those edges reach nothing (an
  `extends` the index resolved to a namesake, as Minestom's Player to a
  nested EntityStatuses.LivingEntity). By name alone, Django's spatialite,
  oracle and mysql GIS editors reached the postgresql backend's
  DatabaseSchemaEditor.execute and _delete_index_sql (0.9), a class with two
  bases took either one's member (BaseDeleteView(DeletionMixin, FormMixin)
  got FormMixin.get_success_url), and SystemUI's RotationContextButton got
  RotationButton's declaration instead of ButtonDispatcher.getCurrentView.
- Only `super`, `this`/`self` and Python's `cls` name the receiver. The PR
  also read `parent.`/`base.` as super and `cls.`/`static.` as self, but
  Java's extractor drops a receiver variable of those names too: protobuf's
  `parent.getFullName()` on an EnumDescriptor went to GenericDescriptor.
- C++ is left to matchCppImplicitThisCall (PR 2479), which already looks
  members up in the class, its bases and its enclosing classes. The PR's
  C++ branch added the caller's own file's namespace functions, which beat
  the overload the arguments fit: fmt's 3-argument `copy<Char>(b, e, out)`
  calls went to format.h's 2-argument copy (0.9).
- Tests for each, and the CHANGELOG bullet in the house format.

Validation: enclosing-class-member-call.test.ts has 12 cases: 6 fail
against main's resolver, and all 5 of ours fail against the PR as written. Kernel A/B against main 7cceba52 on 15 repos (leveldb, fmt, gin and
serilog byte-identical). Every changed site was judged by a hierarchy
oracle (own class, then bases depth-first in header order, classes before
interfaces, enclosing classes for a bare call) or by hand. About 7,400
links moved from a wrong method to the right one (protobuf 3,208, vscode
base+editor 1,383, Play 806, django 766, netbox 316, SystemUI 314,
spring-ai 312), and about 9,100 right links rose from 0.7 to 0.9. One site
got worse (http4s `object RespPrelude { case class RespPrelude }`), and no
C++ site changed. CPU, median of 3 interleaved runs: kestra 154.9 s on main
vs 152.8 s, vscode base+editor 79.9 s vs 82.6 s (within the rounds' spread).
On a vendored-theme stand-in of 1,500 classes, the walk links the 4,500
calls main drops at the 500-candidate ceiling, for +2.2 s (JS) and +1.8 s
(Python). Full suite on 3df1cc64 (main 31ec9082): 768 files
passed, 6 skipped; 9,160 tests passed, 0 failed, 85 skipped.

Original PR #1952 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:11:08 +00:00
Colby MchenryandClaude Opus 5.5 760e31f2b4 fix(install): install.ps1 resolves the latest release without GitHub's rate-limited API (#2939)
install.ps1 resolved "latest" with one Invoke-RestMethod to the GitHub
API, with no fallback, under ErrorActionPreference Stop. The API allows
60 unauthenticated requests an hour per IP and then answers 403, so
`irm .../install.ps1 | iex` died with a raw "(403) Forbidden" on shared
networks and in CI. A pinned CODEGRAPH_VERSION=1.6.2 was used as-is, so
the download URL lacked the v that release tags carry and 404ed.
install.sh (PR #336) and `codegraph upgrade` (resolveLatestVersion)
already handle both; install.ps1 was never ported.

Fix: step 2 is now a function between Resolve-CodeGraphVersion markers.
It sends a HEAD to github.com/<repo>/releases/latest with
System.Net.HttpWebRequest and AllowAutoRedirect off, reads the tag from
the Location header with /releases/tag/([^/?#]+), falls back to the API
inside try/catch, adds the v to a bare version, and keeps the friendly
throw when both fail. Install-CodeGraphFiles is untouched (still equal to
WINDOWS_SWAP_FUNCTION).

Validation (Windows PowerShell 5.1.26100; pwsh 7 is not installed here):
- Each arm's unmodified step 2, with Invoke-RestMethod shadowed so the
  API answers a local 403: main stops with "(403) Forbidden"; the fix
  resolves v1.6.2 from the real redirect with no API call.
  CODEGRAPH_VERSION=1.6.2: main builds .../download/1.6.2/... (HEAD 404),
  the fix .../download/v1.6.2/... (HEAD 302). With the API available,
  main makes one API call and the fix none.
- New Windows-gated test runs the shipped resolver block in PowerShell
  against a local stand-in for github.com and the API: redirect read with
  one HEAD and no follow, API fallback for a 500 or a missing Location,
  the friendly error (no raw 403) when both fail, the v added to a pinned
  version with no request. 4 fail on main's install.ps1, 4 pass with the
  fix; upgrade.test.ts (the swap function pin) passes.
- Both scripts parse with no errors in PowerShell 5.1. The install steps
  were not run: they always add the install to the user PATH.

Reported in PR #560 by @hiyochi; this reimplements the fix
independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:10:27 +00:00
Colby MchenryandClaude Opus 5.5 6162d872be fix(watcher): a deleted link target no longer pins a CPU core on Windows (#2938)
On Windows, libuv reports a watched folder that is being deleted by the
folder's own absolute path (\\?\D:\...) and re-arms the read, which fails
the same way at once: about 70,000 events in half a second until the
stream is closed. The watcher gives each directory link in a project its
own recursive stream, so deleting a link's target outside the project
started that storm. handleDirEvent joined the absolute name onto the
watched folder, got a path that doesn't exist, scheduled a full sync and
nothing else. The stream was never closed, the server held a core until
restart, and the storm kept re-arming the debounce, so no sync ever ran:
the deleted files stayed in the index and later edits never auto-synced.
The project root's stream had the same blind spot.

Fix: handleDirEvent, which both stream callbacks use, now treats an
absolute name as the watched folder itself reporting it is gone. The
first such event on a stream arms one check 200 ms later; the rest of the
burst finds it armed. The check closes a link's stream when the link no
longer leads to the folder it watched (deleted, or pointed elsewhere) and
re-reads the tree, whose full sync drops the link's files; a link that
leads somewhere live is watched anew. A project root that is gone
degrades the watcher with a "project folder was deleted or moved away"
reason, which closes every stream. A normal event pays one
path.isAbsolute check.

Validation (Windows 11, Node 22.22, libuv 1.51):
- Raw probe: fs.watch on a junction whose target is deleted gets 70,009
  events named \\?\<the junction> in 500 ms (499 ms CPU), and none after
  close().
- Real MCP server over a project with a junction libs -> outside, outside
  deleted: main used 3484, 3406, 3391, 3281 ms of CPU per 3 s window and
  kept the linked symbol; an edit afterwards never synced. With the fix:
  296, 0, 0, 79 ms; the linked symbol left the index; the edit synced.
- New watcher tests: a fake fs.watch link storm (10,000 self-named
  events cost no fs call, the stream closes once, a full sync runs, the
  project stays watched), a fake root storm (degrades once, closes the
  stream), and a Windows-gated real junction whose storm must go quiet.
  All 3 fail on main's watcher.ts; watcher.test.ts passes 61/61.

Reported in PR #2487 by @L0garithmic; this reimplements the fix
independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:53:53 +00:00
Colby MchenryandClaude Opus 5.5 31ec908224 fix(sync): a long-running session picks up a framework added while it runs (#2941)
The MCP server's watcher holds one CodeGraph for the whole session, and
its framework detection was cached from the first time it ran: the
orchestrator reset it only in indexAll, and CodeGraph.sync never
re-initialized the resolver. A framework that arrived mid-session, like
Express in a new package.json, was missing from every file synced after
it, so a new route file got no route nodes until `codegraph index`. The
resolver's declared-dependencies cache was also keyed by the indexed file
COUNT (PR #2519): swapping one app for another of the same size, or
editing a manifest, kept the old dependency set, even through
reinitializeResolver().

Detection now records what it read: indexed source files, which a sync's
change lists cover, and every other path it read or probed, with a size
and mtime signature, including the per-app manifests the extractor gating
reads. Before a sync extracts files, it detects again only when files
were added or removed since, a source file detection read changed, or one
of those paths changed on disk. A scoped sync takes the file list from
the index plus the files it adds, so it never walks the project (the
first sync in a process used to run a full scan for its first detection).
What detection read is kept between detections and dropped by the same
change lists and signatures, so detecting again reads only what changed.
When the detected set differs from the resolver's, the sync
re-initializes the resolver. Its initialize() now clears its caches
before detecting, and clearCaches() forgets the declared-dependencies
cache with the other per-context memos.

Files indexed before a framework change are not re-extracted. A framework
like Express applies to every file of its language, and re-reading them
all inside a watcher's sync would cost what a full index does. Instead a
full index records the frameworks it used, a sync records how detection
moved since, and `codegraph status` (text, and --json with
reindexRecommended and frameworksChangedSinceIndex) recommends
`codegraph index` while they differ. Status only reads that record.

Validation:
- sync-framework-detection.test.ts (new, 8 cases): Express arriving with a
  new route file, by full and by scoped sync; a .svelte file added with no
  manifest change; an edited source file that detection read; a
  manifest-only edit picked up by the next sync, with the status record,
  cleared on revert and by a full re-index; a fresh process's scoped
  syncs detecting once, from the index, with no scan; the equal-size app
  swap; and the dependency cache forgetting on request. 7 of 8 fail on
  main's source (the 8th needs the new export). Ten mutations, one per
  mechanism, each fail a test.
- The triage probes: main detects [] after Express arrives and routes.js
  gets no route; the fix detects express and extracts GET /hello, by full
  and by scoped sync. For the swap, main's reinitializeResolver() gives
  [react] and the fix [express].
- Cost on excalidraw (737 files, scoped syncs in one long-lived
  instance): the detection step takes 8 ms on a sync where nothing it
  read changed and 8-13 ms when it detects again, against syncs of about
  260 ms (add) to 2 s (modify). The first sync in a process detects once
  (about 300 ms) and no longer scans the project.

Reported in PRs #2516 and #2519 by @nk33-dev. This reimplements both
fixes independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:31:26 +00:00
danusha2345andClaude Opus 5.5 7cceba52b0 fix(resolution): library calls on untyped Python, Scala and Java values stay unlinked
Root cause: when nothing types a call's receiver, matchMethodCall's last
strategy links `recv.m()` to the one project method named `m` (0.7), or to
the best receiver-word match among several. A standard-library call on a
plain value - `options.setdefault(...)` on a dict, `m.getOrElse(...)` on an
Option, `data.put(...)` on a map in a `var` - then linked to an unrelated
project method of that name. Go, Rust, Kotlin, C#, VB.NET and Dart already
keep such a guess only when the receiver names the method's owner
(stdMethodNames); Python, Scala and Java had no list.

Fix (contributor): Python, Scala and Java standard-library method lists in
stdMethodNames. Ours: the PR also exempted a receiver named `cls` from the
rule; both extractors already strip `cls.` from Python calls, so it never
applied there, while in C#, Dart, Rust and VB.NET it let
`var cls = Make(); cls.ToString()` link to an unrelated Money.ToString.
Removed, with a C# test case. CHANGELOG bullet rewritten and moved next to
the Python built-in method bullets.

Validation: library-method-calls.test.ts: 3 of its 5 cases fail on main
(Python build, Scala go, Java untyped), all 5 pass; the C# case fails when
`cls` is put back. Kernel A/B against main 0174f804, 11 repos: 0 added,
0 retargeted, 540 removed. Python: flask 12, netbox 2, django 140, fastapi 0,
all wrong (list/dict/file/re-match calls landing on ListMixin, QueryDict,
HttpRequest, AnonymousUser, _AppCtxGlobals). Java: Minestom 20, spring-ai 86,
all wrong (Map put/computeIfAbsent, reactor Context put, stream(), enum
valueOf). Scala: playframework 98 (4 of them Java), scalachess 31,
http4s 151; 267 wrong, 13 right links lost where a project type has
collection-like methods and the receiver is not named after it (scalachess
Board.put/replace through for-comprehension binds, ByColor through
`type Pockets`; http4s Headers.put; Play Form.fold, Router.orElse). gin and
serilog byte-identical. CPU on playframework, median of 3 interleaved:
71.7 s main, 71.1 s branch. Also settles the Holder.LOOKUP.get and
Mode.ON.equals cases of PR 1949.

Original PR #1947 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:57:59 +00:00
Colby MchenryandClaude Opus 5.5 45a29b4685 fix(worktree): a linked worktree is recognized however Windows spells its path (#2937)
On Windows one directory has several spellings. VS Code and Cursor pass a
lowercase drive letter, NTFS ignores case, a subst drive is a second name
for a folder, and long names have 8.3 aliases. git answers with the
spelling on disk, but src/sync/worktree.ts compared its paths with the
caller's as strings, after a JS realpath that keeps the caller's spelling.
The main checkout's git common dir comes back relative (".git"), so it
took the caller's spelling while a linked worktree's came back absolute
in git's. A worktree inside the indexed checkout, spelled unlike git,
failed all four comparisons:
- detectWorktreeIndexMismatch found no mismatch, so codegraph status
  (text and --json) dropped the borrowed-index warning;
- nestedRepositoryBelow took the worktree for a different repository, so
  the MCP tools refused it as unindexed and told the agent not to call
  codegraph for it again.

Fix: the four comparisons now ask whether two paths name one directory,
with the identity canonicalProjectRoot already uses (the native realpath,
lowercased on Windows), falling back to the JS realpath where the native
call fails. Returned paths keep the caller's spelling for display. A
nested repository below an index root outside git is still reported, as
before.

Validation (Windows 11, Node 22):
- New Windows-gated tests with a lowercase drive letter, at the helper
  level and through ToolHandler (search and status, with and without a
  default project): the 3 behaviour tests fail on main's worktree.ts and
  pass with the fix. Both files: 32 passed.
- Fixture: a checkout with a worktree under .claude/worktrees and an
  embedded clone, spelled four ways (on-disk, lowercase drive, other
  capitals, subst drive). Helper verdicts wrong: 6 on main, 0 with the
  fix. codegraph status warned in 2 of 8 runs on main, 8 of 8 with the
  fix. The real MCP server refused the lowercase, capitals and subst
  spellings on main; with the fix it serves all four with the
  borrowed-index notice, codegraph_status warns, and the embedded clone
  is still refused. An 8.3 short name (the contributor's case) went from
  2 wrong verdicts to 0.

Reported in PR #2517 by @nk33-dev; this reimplements the fix
independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:43:41 +00:00
Colby MchenryandClaude Opus 5.5 475e56b209 fix(cli): read commands and the prompt hook open the index read-only (#2936)
Running query, files, node, callers, callees, impact, context, explore or
affected while `codegraph index` rebuilt a project could make the rebuild
fail with "database is locked", and so could the hidden prompt-hook, which
Claude Code runs on its own for structural prompts. Each opened the index
read-write, so its open ran the writer's repairs: during the rebuild's bulk
load they recreated the secondary indexes the indexer had dropped, and the
indexer's next write, whose transaction had already read, failed with
SQLITE_BUSY_SNAPSHOT (517). PR #2930 fixed the same thing for status.

These commands now open the index read-only, through one helper, and leave
migrations, index recovery and WAL maintenance to the writer:
- query, files, node, callers, callees, impact, context, explore, affected
  and prompt-hook are read-only. explore and node run the MCP tool handler
  in process without a lifecycle, so they never sync or open another
  project.
- While another live process holds rebuild.pid they say so in one line on
  stderr (a --json answer stays parseable), as status does.
- The prompt hook stays out of a running rebuild altogether: the index is
  half-built until it ends, the MCP tools answer "rebuild in progress"
  meanwhile, and not opening it keeps the hook from holding the file the
  rebuild replaces. Its gate records the new outcome noop-rebuild.
- The hook's one write, filling an empty segment vocabulary (issue #1142),
  goes through a writer connection of its own, opened only once it holds
  the index lock, which every index and sync holds while it writes.
- status (read-only since PR #2930), sync, index, init and uninit are
  unchanged; the last four write.

A read-only open migrates nothing. QueryBuilder.hasSchema lets each read
that needs a later table or column answer as that migration's defaults
would: files.generated (v9), unresolved_refs.status and name_tail (v8), and
name_segment_vocab (v7). The status pending count from PR #2930 uses it too.

Validation (Windows 11, Node 22.22):
- The issue's interleaving, each command in another process inside the
  writer's edge transaction: on main all 14 command forms make the insert
  fail with errcode 517 and bring the 5 dropped edge indexes back; with
  this change all 14 inserts succeed and the indexes stay dropped.
- Real rebuilds of excalidraw 95307fa1 (736 files) with query and the
  prompt hook looping beside them, default settings and with the
  resolution window forced: main failed 6 of 14 ("store worker: database
  is locked" with an empty index, or "database is locked" partway through
  resolution), and 12 of its 109 query runs failed at open with
  SQLITE_BUSY. This change failed 0 of 14, with 0 failures in 299 query and
  284 hook runs and the same graph every time (14,236 nodes, 61,210 edges).
- Old indexes: 24 command forms on indexes at schemas 4 to 16 (11 schemas
  rebuilt from a current index, plus indexes built by 0.9.9, 1.4.0, 1.5.0,
  1.6.0 and 1.6.2): output identical to main's, which migrates first, 384 of
  384. The schema stays as it was, except where the hook fills the
  vocabulary of a pre-v7 index; that writer's open migrates it, as every
  read-write open did.
- Timing on excalidraw at rest, 22 interleaved runs per command: medians
  within -5.8% to +3.3%, -0.9% in total.
- read-commands-during-rebuild.test.ts: 38/38 pass. With main's sources 35
  fail; with only the CLI opens reverted 33, only the schema fallbacks 19,
  only the read-only vocabulary fill 2.

Follow-up to PR #2930.
Fixes #2913

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:26:03 +00:00
danusha2345andClaude Opus 5.5 cbd9817f7c fix(extraction): a damaged C #if group no longer turns into functions named if
tree-sitter-c parses every branch of a `#if … #elif … #else … #endif`
group as a run of block items, so a group is harmless only when each
branch is complete on its own. Three shapes in real C are not: a branch
that begins with `else` (an arm of an if-chain kept behind `#ifdef`), a
branch that ends with a bare `if (…)` header whose body follows the
`#endif` (ST's HAL latency tables), and a branch whose braces don't
balance (a function head that differs per build). Each leaves a
K&R-shaped `if (…) { … }` that the grammar reads as a function named
`if`, filed inside the enclosing function, and the functions after it
were nested under it or lost. Separately, a block macro in capitals in
statement position (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`) was skipped by the
statement-macro blank, which matched lowercase iterator macros only.

blankCUnbalancedConditionalBranches runs at the end of preParseCSource,
after the directive restore, since it edits directive lines. A group
where a branch shows one of those shapes, or is a fragment of an
expression or a list (`|| (h4 == NULL)` inside an `if (…)`), loses its
directive lines, offsets kept. When a branch can't stand next to the
others, one branch is kept verbatim: the `#else` of a positive test
(`#ifdef X`, `#if defined(X)`), the first of `#ifndef`, the live one of
`#if 0`. The statement-macro blank also takes ALL_CAPS names (PascalCase
still excluded, for C++ constructors). Both engines see it: the kernel
gets the pre-parsed bytes from the TS route point.

Ours on top:
- A group on a flag whose branches each balance (braces and parens)
  keeps every branch, only its directive lines blanked: read in a row
  they are still C (`} else if (a) {…} else if (b) {…}`, list entries,
  `|| x || y`). Dropping one lost real code of builds the project uses:
  betaflight's failsafe.c arms under `#if ENABLE_RESCUE_PLAN` (default-on
  in common_post.h), and adc_stm32h7xx.c's adcTagMap, whose inner MCU
  chain kept its `#else #error` branch. Unbalanced and literal groups
  keep one branch, as above. The PR's first unit test now expects both
  arms of its balanced else-led group.
- The group is read on a copy with comments and literals masked. A brace
  inside a block comment over several lines counted as code, so Lua's
  `** {======` banners made loadlib.c's platform group look damaged, and
  its dlopen, Windows and dyld implementations were dropped for the stub;
  on betaflight it dropped 19 CMSIS-DSP MVE functions.
- A line splice is read on the raw line: a `\` at the end of a comment
  line inside a #define body still continues the #define (masking hid
  it, which made jemalloc's tsd.h include guard look damaged).
- Only a directive's own `\` continuations are blanked with it. The PR
  blanked every directive line right under a collapsed group's `#if` or
  `#else`, so a kept branch lost a leading `#include` or `#define`.
- `#if 1` keeps its first branch; it kept the dead `#else`.
- Unit tests for each, mutation-checked; CHANGELOG bullet rewritten
  without numbers.
- Tried and dropped: keeping the feature branch of a flag the file
  #defines itself (the same-file evidence PR #2615 uses). Applied per
  group, it broke dtoa's coordinated groups in jq (`#ifdef
  Avoid_Underflow` kept its branch while a later `#ifndef
  Avoid_Underflow` still kept its `}`), cutting jvp_strtod in half.

Validation, main b6379e6d vs this:
- Phantom functions (named if or (…)) removed / real functions added:
  betaflight 49679ccd 257 / 40 (spiInternalStartDMA, gyroDetect,
  processSmartPortTelemetry, esc4wayProcess, the CMSIS matrix
  routines), stm32h7xx_hal_driver 7e541d92 68 / 6 (LL_SetFlashLatency,
  HAL_ADC_AnalogWDGConfig), stm32g4xx_hal_driver a6001282 30 / 2, redis 7f440372 8 / 16 (tre_parse, je_mallocx,
  lua_cmsgpack), jq 49fbca12 1 / 11 (strptime, gethex, jvp_dtoa). No
  real function lost except jq's WIN32-only wmain. Enum members added:
  betaflight 701, stm32h7 8.
- Calls move to the real function: on betaflight, 746 calls that sat on
  a phantom or on the file node are gone, and 770 now sit on real
  functions (369 on restored ones, 401 on functions that existed but
  had lost them to a phantom). Lost: references from `#if`
  conditions inside an initializer (betaflight TIM_N x12), test-macro
  references (redis TEST x10), and a few calls in feature branches of
  unbalanced groups.
- leveldb (C++) and gin (Go) byte-identical.
- kernel-parity (betaflight, stm32h7, redis, jq): 0 files with diffs on
  main and here; fewer files defer to wasm (betaflight 1216 -> 1095,
  redis 169 -> 150, jq 21 -> 15, stm32h7 156 -> 146).
- Cost: C preParse CPU over betaflight 5.7 -> 8.6 s (the PR as written
  8.4 s), redis 1.2 -> 1.6 s.
  Whole-index CPU, median of 3 interleaved: redis 41.2 vs 40.0 s,
  stm32h7xx_hal_driver 21.4 vs 21.9 s (noise).

Original PR #1736 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:25:21 +00:00
Aaron QueenandClaude Opus 5.5 c30696703d fix(cfnptr): a C function-pointer table links only to C and C++ functions
resolveFn in the C function-pointer pass looked a registered handler's
name up across every language, filtered only to function and method
kinds. In a repository that also holds Rust or Python, a C table could
link to a same-named Rust or Python function, and when that function
sorted first it also took the place of the real C handler.

The fix keeps candidates in C and C++ files only, using the pass's own
C_CPP_EXT, the file set it scans for tables and dispatch. Headers the
language detector labels Objective-C still count. Objective-C `.m` and
`.mm` files stay out, like they are out of the scan: a table in a `.m`
file is never read, and allowing them would let an Objective-C method
named `init` or `read` stand in for a C handler of that name.
On GLFW, which mixes C and Objective-C, main links 354 table entries,
none into a .m file; its one C table naming a .m function
(_glfwConnectCocoa) is an anonymous-struct table the pass doesn't
bridge on main either.

Validation, main b6379e6d vs this:
- c-fnptr-synthesizer.test.ts: the new case fails on main (impl_read
  links to aaa/gen.py) and passes here; 19/19.
- Linux d135ab45 subtree (rust/, drivers/android, drivers/base,
  drivers/net/dsa, fs, tools/net, include/linux; 6,580 files):
  byte-identical, 12,244 fn-pointer-dispatch edges, all into C or C++
  files on both arms. Main links none of this tree into Rust or Python,
  so the PR's 193 (a whole-kernel index on its fork) don't show here; a subtree holding its three example sites (pmcraid's
  `->reset_cmd = cmd`, afs's `->update_time = new`, map.c's
  `p->lock = lock`) has none either, since a plain assignment of a
  variable doesn't register on main.
- C and C++ controls byte-identical (dump-graph, every table): redis
  7f440372 (863 fn-pointer-dispatch edges), GLFW 92dcf4ce (354), jq
  49fbca12 and leveldb 7ee830d0.
- The resolver change is TS-side; the kernel ports only the per-file
  sweep, so both engines take the same edges. kernel-parity on the
  kernel's drivers/base, drivers/net/dsa and drivers/android (279 C
  files): 240 at parity, 0 diffs, 39 deferred to wasm by policy.

Ours on top: the CHANGELOG bullet moved from the released 1.6.1
section to Unreleased and reworded.

Original PR #1989 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:57:30 +00:00
Colby MchenryandClaude Opus 5.5 1cb4aab913 fix(status): a project below its repository root sees its uncommitted changes (#2932)
When the indexed project is a folder of its git repository (a monorepo
package, an app/ folder beside a server/ one), `codegraph status`,
`status --json` and the freshness check behind codegraph_status reported
the index up to date while `codegraph sync` found the very same changes.
`git status --porcelain` prints paths from the repository root whatever
folder it runs in, and reports the whole repository. Change detection
joined `app/src/index.js` under the project, found nothing at
`app/app/src/index.js`, and dropped it. The dirty paths a sync records
for the restore check (from PR #1878) were stored the same way, so a
file put back with git after a sync was missed too. And a project in a
folder its repository gitignores got a permanently clean answer from
git, which says nothing about an ignored folder.

Change detection now asks where the project sits in its repository
(`git rev-parse --show-prefix`, kept per folder, and skipped when the
folder has its own .git). Below the root it runs git status with a
`-- .` pathspec, takes the prefix off every path and leaves out anything
outside the folder. The project's ignore rules match those
project-relative paths, the recorded dirty paths are project-relative,
and a project its repository ignores declines the git path as the scan
already did, so callers take the full scan. A project at its repository
root runs exactly the git command it ran before.

Validation:
- git-status-subdir-project.test.ts (new, 7 cases): edited, added and
  deleted files in a subdir project, matching sync; edits outside the
  project left out, with the pathspec asserted; the restore case, across
  a reopen; the project's own anchored .gitignore rule; an embedded repo
  inside the project; the gitignored-project fallback; and a repo-root
  project running exactly today's status command with no prefix call.
  6 fail on main's source; the repo-root control passes on both. Four
  mutations (no pathspec, ignore rules on repo-relative paths, no
  fallback, no .git shortcut) each fail the test.
- CLI repros: after an edit and an added file in app/, status --json
  reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in
  a gitignored project; a subdir restore reads 0 on main and 1 modified
  with the fix (a repo-root project reads 1 on both).
- The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added
  + 1 modified with the fix.

Reported in PR #1636 by @innoxxicide. This reimplements the fix
independently, on top of the restore check that landed after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:56:18 +00:00
luojiyinandClaude Opus 5.5 0174f804c3 perf(resolution): read a bare JS/TS call's local binding once per match — graph byte-identical
matchByExactName asked isLocallyBoundJsName for every cross-file
candidate of a bare JavaScript or TypeScript call, though the answer
depends only on the reference's name and file, which are the same for
every candidate. The helper memoizes it per file and name, but each
call still built the key and read the memo. The answer is now kept in
a lazy local for the one match, at the same place in the filter, so
candidates, order, scoring and confidence are untouched.

Validation: fresh indexes of codegraph's own source, excalidraw, vben,
express and zod dump byte-identical to main's (nodes, edges with their
metadata, unresolved refs, files), and express stays identical through
sync after a JS file is added, edited and deleted. A CPU profile of
indexing codegraph's own source (all threads, two runs per side) puts
isLocallyBoundJsName's self time at 150-173 ms on main and 45-46 ms
here; the scans it guards (bindsAtSites) are unchanged. Whole-index CPU
on vben, 10 interleaved runs, moves less than its noise. The PR's 8
tests pin the matcher's behaviour and pass here and on main.

Original PR #2871 by @luojiyin1987.
Fixes #2870

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:32:43 +00:00
Colby MchenryandClaude Opus 5.5 2afee00cb0 fix(viewer): a file with % in its name opens as itself (#2933)
codegraph ui couldn't show a file whose name contains a %: 100%.ts was
refused as an unusable path, and a%20b.ts was served as changed on disk
with no lines. resolveProjectFile, the chokepoint for every file the
viewer reads, percent-decoded its path. But that path is either one the
index stored or a query value the URL parser had already decoded once,
so this was a second decode. It threw on 100%.ts and turned a%20b.ts into
a b.ts, while resolveRequestedFile looked the file up in the index by the
name as given: the bytes of a b.ts were hashed against a%20b.ts's record.

The chokepoint no longer decodes. It refuses raw control bytes, and on
Windows a backslash, then checks containment as before. Raw request
paths (the viewer's own assets) are still decoded where they are read.
The File view's /api/file/<path>, whose path the HTTP layer also decodes
once, had the same double decode and is fixed by the same change.
The POSIX half of PR #2009 (backslashes kept in names on POSIX) is left
out, as it needs a wider change and a POSIX run.

Validation on Windows 11, with a fixture holding src/a%20b.ts,
src/a b.ts and src/100%.ts:
- codegraph ui over HTTP, /api/source with the query a browser sends:
  main serves a%20b.ts as drift=true with no lines and answers 403 for
  100%.ts; the fix serves all three files as themselves, drift=false.
- Tests: 2 new tests (the chokepoint, and /api/source plus
  /api/file/<path> over HTTP) fail on main's source and pass with the
  fix. Two
  existing cases that fed an encoded ..%2f and %00 straight to the
  chokepoint now go through the URL parser's single decode, as requests
  do, and are still refused.

Reported in PR #2009 by @shrankr; this reimplements the viewer half of
the fix independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:18:41 +00:00
Colby MchenryandClaude Opus 5.5 983c4df374 fix(index): a deleted file git still lists is skipped, not reported as unreadable (#2928)
A full index reads every path the scan lists, and `git ls-files` keeps
listing a tracked file deleted from the working tree until the deletion is
staged. So `codegraph init` and `codegraph index` stat'ed the missing file,
got ENOENT and reported "1 files could not be read" with an errors.log
entry. The errored file also kept the run from recording the commit it
indexed, so `codegraph status` fell back to a full scan afterwards.

The batch reader in indexAll and the single-file indexFile now take ENOENT
and ENOTDIR to mean nothing is there: the file is skipped and counted as
skipped, so the run reads as complete rather than PARTIAL. A symlink that
points at nothing is skipped the same way, as the directory walk for
projects without git already did. Every other read failure (EACCES, EIO, a
folder where a file was) is still reported. On POSIX a path under a file
fails realpath with ENOTDIR, which validatePathWithinRoot rejected as if it
escaped the root; it is now judged like any missing leaf, by its nearest
existing ancestor, so it reaches the reader and is skipped too. Discovery
is unchanged, with no extra stat per tracked file.

Validation:
- index-gone-tracked-files.test.ts (new): all 5 cases fail on main's
  source and pass with the fix: committed and staged deletions, a folder
  replaced by a file, a dangling symlink, and EACCES still reported by
  both the full index and indexFiles. Mutating the single-file path, the
  skipped tally or the EACCES boundary fails the test each time.
- security.test.ts: a path under an existing file validates, and one under
  a symlink to a file outside the root is still refused.
- The issue's CLI repro: main prints "1 files could not be read" and writes
  errors.log; the fix prints "Indexed 1 files", writes no errors.log, and
  status reports nothing pending.

Reported in PR #2910 by @xianzhi-xzw, who also filed issue #2904. This
reimplements the fix independently, at read time rather than with an lstat
of every tracked file during discovery.

Fixes #2904

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:03:50 +00:00
Colby MchenryandClaude Opus 5.5 f93e89a82d fix(paths): a project at a Windows drive root indexes its files (#2931)
A project at the root of a drive, such as a mapped network drive Z:\, a
subst drive or a UNC share root, indexed nothing on Windows: every file
was refused as "Path traversal blocked", and code reads under the root
were refused too. The containment check appended a separator to the root
before comparing, and a root such as C:\ already ends with one, so the
prefix became C:\\ and no path under the root matched. The dead code
report's default file reader had its own copy of the same check, so it
treated every file of such a project as unreadable.

The check now adds a separator only when the root doesn't already end
with one. The dead code reader uses the shared lexical containment check
instead of its own copy.

Validation on Windows 11:
- subst R: onto a two-file fixture, then codegraph init -y --force R:\:
  main indexes 0 files ("all 2 files had errors", 2 "Path traversal
  blocked"), and validatePathWithinRoot(R:\, a.ts) is null. The fix
  indexes both files (6 nodes, 6 edges) and getCode returns both
  functions' source. The mapping was removed after each run.
- Dead code report against one R:\ index: main's reader drops the unused
  function as unreadable; the fix reports it.
- Tests: 3 new tests fail on main's source and pass with the fix: a file
  under the filesystem root (this drive's root here, / on POSIX), path
  math under Q:\ and \\server\share\, and read-only realpaths under C:\.

Reported in PR #415 by @Kamilslaoui93; this reimplements the fix
independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:47:28 +00:00
danusha2345andClaude Opus 5.5 0187366697 fix(cli): codegraph status opens the index read-only, and says when a rebuild is running
Status opened the database read-write, so its open ran the writer's
repairs: migrations, the FTS-trigger and secondary-index recovery an
interrupted bulk load leaves behind, and WAL maintenance. During a
`codegraph index`, that recovery recreated the indexes the indexer had
dropped for its bulk load, and the indexer's next write, whose
transaction had already read, failed with SQLITE_BUSY_SNAPSHOT (517):
"Failed to index: database is locked".

Status now opens with `readOnly: true` and leaves every repair to the
writer, as the MCP reader connections already do. When another live
process holds `rebuild.pid`, it says so: a warning in the text output and
`rebuildInProgress` in `--json`, the only new key. `isRebuildInProgress`
is the check `assertNoRebuild` already made.

Added when landing it:
- A read-only open migrates nothing, so status on an index built by
  1.4.0 or older (schema 7, no `unresolved_refs.status`) failed with "no
  such column: status". The pending-reference count counts every row
  when the column is missing, as the v8 migration marks them pending.
- While a rebuild runs, status skips the advice meant for an index at
  rest: the interrupted-run references, "run sync", "re-index", and the
  oversized-WAL warning, which mid-rebuild all describe the live run.
- Status no longer reclaims a killed session's oversized WAL, so the
  warning names the next `codegraph sync` instead of status itself.
- Tests: the issue's exact interleaving (status inside the writer's edge
  transaction, as in PR #2916's test), a pre-v8 index, and the
  mid-rebuild advice.

Validation (Windows 11, Node 22.22):
- The issue's reproduction with our build of main 69af19ac: status inside
  the transaction exits 0, the writer fails with errcode 517 and the five
  dropped edge indexes come back. With this change the insert succeeds
  and the indexes stay dropped.
- Real rebuilds of excalidraw 95307fa1 (716 files) with `codegraph
  status` looping beside them, 3 with default settings and 3 with the
  resolution window forced: main failed 6 of 6, each with "store worker:
  database is locked" and an empty index; this change failed 0 of 6 over
  46 status runs, with identical complete graphs (14,236 nodes, 61,210
  edges).
- status-during-rebuild.test.ts: 7/7 pass. With main's sources 6 of 7
  fail; without `readOnly` 3 fail, one with "database is locked"; with
  the PR as written the pre-v8 and mid-rebuild cases fail.
- Indexes built by v1.6.2 (schema 11) and v1.4.0 (schema 7): the same
  numbers as main, and the schema is left as it is. Interrupted-index
  warnings (indexing, partial, failed) are unchanged.

Original PR #2921 by @danusha2345.
Fixes #2913

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:36:28 +00:00
Colby MchenryandClaude Opus 5.5 5e0c42cb63 fix(security): the sensitive-folder guard refuses links, case variants and \?\ paths (#2929)
The guard that keeps ~/.ssh, ~/.gnupg, ~/.aws, ~/.config and system
folders such as /etc and C:\Windows from being opened as a project
compared the path as typed. A junction or symlink to one of them, a
different capitalization on Windows or macOS (~/.SSH), or a \\?\ or \\.\
spelling on Windows passed, so an MCP projectPath, `codegraph ui` and the
viewer's per-file reads all served what was inside. The MCP gate also
skipped the guard for a path that doesn't exist and then walked up from
it, so ~/.config/no-such-dir reached an index of ~/.config.

validateProjectPath now checks the path as spelled (with a Win32
namespace prefix stripped) and its fs.realpathSync.native form. Both are
compared with the system set and the system folders' own real paths
(macOS /private/etc), and with the home folders as spelled, under the
home's real path and by their own real paths (a linked ~/.ssh). Case is
folded on win32 and darwin. A path that can't be resolved keeps the
lexical check. It stays at one realpath per call: the system folders'
real paths are read once and the home folders' are cached for two
seconds. The MCP handler also checks the root its up-walk lands on.

Validation on Windows 11, with a private home holding a fake token:
- Probe: on main 11 of 16 sensitive spellings were allowed, and the
  token was served through 6 MCP projectPath shapes and 2 viewer roots.
  With the fix all are refused. Controls (an ordinary folder, its
  upper-case and \\?\ spellings, the home itself) are still allowed.
- codegraph ui end to end: main served the token for 4 of 6 spellings
  and started the viewer for a fifth; the fix refuses all 6 at startup.
- Tests: the 9 new tests that run on Windows fail on main's source and
  pass with the fix. The POSIX and macOS cases are gated and were not
  run here.
- Cost: validateProjectPath 48 -> 130 us per call, the viewer's per-file
  resolveProjectFile 700 -> 780 us (medians of 5 rounds, 3 runs).

Reported in PR #1930 by @mixxer; this reimplements the fix independently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:20:10 +00:00
bompusandClaude Opus 5.5 7ff2841e37 fix(resolution): ties that decide a link break by code unit, not the host's locale
Several resolution tie-breaks sorted paths or ids with a bare
localeCompare, which follows the host's locale, so two machines could
index the same repository differently: under Czech collation `ch/` sorts
after `h/`, under Danish `aa` sorts after `z`, and locale collation puts
`alpha/` before `Beta/` where code-unit order does the opposite.

The PR fixes the nearest-declaration tie in tsFieldCall, for
`this.field.method()` on a type declared in two equally near apps. On
top, the same bug at the three other sites that decide resolution:
- cpp-type-aliases: the declaration an alias resolves to when two
  headers declare it differently, for a C++ base class named through it;
- frameworks/react-native: the Swift copy of a module's class that a
  JS-to-native call reaches;
- go-func-field-synthesizer: the last tie-break of wiringOrder, which
  decides the values a capped channel keeps (its site comparison already
  names 'en' and stays).
All compare by code unit now. Comparators that only order output (CLI,
MCP and viewer listings, dead-code and hierarchy order, the context
builder's copybook entries) are left alone. The CHANGELOG bullet moves
from the released 1.6.1 section to Unreleased.

Validation: a test each for the TS, C++ and React Native sites; all 3
fail on main and pass here. Indexing `ch/` vs `h/` fixtures for those
three under a simulated English and a simulated Czech host (a
localeCompare preload that reaches the worker threads), main links
different targets in all three, this commit the same in both. A/B, main
vs this commit on an English-locale host, 12 repos (excalidraw, zod,
vben, bulletproof-react, jq, cobra, gin, etcd, prometheus, okhttp, with
flask and spring-modulith as controls): every dump byte-identical.
Indexed again under a simulated Danish host (the Go repos) and Czech
host (the TS repos), both arms match their English dumps, so these
repos reach no tie the locale decides; the fix matters where one does.

Original PR #1983 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:05:28 +00:00
Aaron QueenandClaude Opus 5.5 4eb8c2a5f2 perf(synthesis): line numbers from a newline index, not a split per match
Two quadratic idioms in callback-synthesizer.ts. Twelve passes computed
each match's line as `safe.slice(0, m.index).split('\n').length`, which
copies and splits the file's prefix for every match, and `sliceLines`
split the whole file again for every node a pass looked at (7 callers,
one per node of the same file in a row).

The twelve sites now call `lineOf(src, idx)`, which keeps the newline
index of the last text (built with synth-utils' `makeLineAt`, which
counts the newlines strictly before `idx`, exactly like the old idiom),
and `sliceLines` keeps the last file's lines.

Ours on top: `releaseLineCaches()` at the end of synthesizeCallbackEdges.
The pool workers' copies go with the workers (pool.destroy() after
synthesis), but a sync runs synthesis on the main thread, so a long-lived
MCP server or `watch` kept the last file's text, its lines and its newline
index until the next synthesis. And a celery test with two files of the
same length scanned in a row, which fails if the line index is reused
across texts (checked by keying the cache on length).

Validation, main 81559dcc vs this:
- dump-graph of every table (nodes, edges with metadata, refs, files)
  byte-identical on pretix c9f7bf86 (79,452 edges), trezor-suite
  505aece5 (493,138 edges), excalidraw d3a9c2a1 (46 react-render),
  vben 61c07936 (128 pinia-store), gin and eShopOnWeb (Go and C#
  controls with gin-middleware-chain and mediatr-dispatch edges).
- The pass outputs are identical, edge for edge and in the same order,
  when each touched pass runs alone over the same index. Their CPU, the
  median of 3 runs after a warm-up: pretix 1,018 -> 845 ms (React render
  204 -> 32 ms, field channels 235 -> 172 ms), trezor-suite 7,686 vs
  7,841 ms (noise; its Pinia and Vuex scans dominate and are untouched).
- Whole-index CPU on pretix, median of 3 interleaved: 99.8 vs 99.4 s.
  No CHANGELOG entry: the gain is per pass, not visible per index; the
  PR's bullet sat in the released 1.6.1 section and is dropped.

Original PR #1985 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:45:47 +00:00
danusha2345andClaude Opus 5.5 0b194448dc fix(resolution): a call a method hands on to another object no longer links back to it
matchMethodCall's strategy 3 guesses a method by its name when the
receiver's type is unknown. With one method of that name in the project
it was taken outright, so a wrapper handing its call on, like Play's
`def acceptsURL(url) = proxied.acceptsURL(url)` or vben's
`form.validate()` inside `FormApi.validate()`, linked to the method
making the call, which then looked recursive in callers and impact.

The single-candidate branch now gives no edge when that method is the
caller itself and the receiver is another object (not this, self, Self,
super or cls), as the word-overlap branch already did.

On top of the PR: Go is left out. A Go method's receiver and struct
fields are typed before this point, so a Go call that still reaches the
guess is on a local the resolver could not type, mostly a range variable
over the type's own children, which is real recursion: cobra's
`sub.checkCommandGroups()` over `c.commands`, etcd's `child.Repr(...)`,
prometheus's `subParameter.toLabels()`. Also a TypeScript wrapper test, a
Go range-variable recursion test, and a CHANGELOG bullet.

Validation, main (3a93f57a) vs this change, 24 repos in 11 languages: no
edge added; 63 self-loop edges removed, 56 of them wrong (delegations to
a library or another object: playframework 19, vben 9, http4s 8, mihon 8,
okhttp 5, ripgrep 3, flask 2, spring-modulith 2) and 7 real recursion
through another instance of the caller's type, which now gets no edge
(okhttp's Kotlin extension functions on File and Throwable 3, fastapi's
_IncludedRouter 2, a serilog C# extension method 1, spring-modulith 1).
As written the PR also removed the 8 Go recursion edges. Re-run on
playframework after #2875 changed Play's resolver: 17 removed, the same
less Java's two Multipart.* calls, which main now links to the Scala
object itself. Index CPU on okhttp, median of 8 interleaved runs: 32.8 s
on main, 32.3 s with this change. Tests: 2 of 4 fail on main; the PR as
written fails the Go range-variable case.

Original PR #1939 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:45:04 +00:00
danusha2345andClaude Opus 5.5 b6379e6d5e fix(scala): an Outer.Inner(...) call reaches the Scala case class or object it builds
Scala qualified names carry no package (Outer::Inner), so a call written
Outer.Inner(args), a case-class or companion apply, found no type by its
written name. It fell through to the name strategies, and under a return
type annotation (def f(): WebSocket = WebSocket.Accepted(flow)) the
local-declaration reader took WebSocket for the receiver's type and handed
over an unrelated Java constructor, Other::Accepted::Accepted.

Before the receiver-word strategy, a Scala call whose member is
capitalized now resolves to the Scala type Outer::Inner when that is
unambiguous: every match in one file (a class and its companion), or one
in the call's own file; a class or trait wins over a same-named object.
It is recorded as instantiates for a class, as a call for an object. The
typed-receiver path no longer hands a Scala call another language's
constructor. Real Java static methods are still reached from Scala.

Validation:
- resolution.test.ts: 5 cases, 4 red on main; the fifth (a Scala call
  to a real Java static) passes on both and guards the Java side.
- A/B against main 81559dcc (with #1950 and #1936): scalachess +77/-0,
  http4s +371/-1 (the one a retarget from a wrong defaults::Host to the
  Origin.Host case class), playframework +291/-1 (the one a retarget:
  Writeable's HttpEntity.Strict(...) from the Java play.http constructor
  to the Scala case class), play-samples +2 (two case-class applies #1950
  unlinked from Java constructors); CMAK, click and gin identical. Every
  addition is a Scala case-class or object apply; sampled all right
  (Uci.Move, Header.Raw, H2Frame.Data, RemoteNode.Ip,
  ApplicationLoader.Context, Request.Connection over headers.Connection).
- Cost (http4s, median of 3 interleaved fresh indexes, on main
  69af19ac): wall 8.6 s on main vs 7.8 s, process CPU 27.5 s vs
  26.2 s, within noise. The A/B gave the same edges on both bases.

Original PR #1951 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:59:08 +00:00
danusha2345andClaude Opus 5.5 81559dcc37 fix(resolution): a call reaches the caller's own copy of a duplicated module
A module kept in several copies, like two Android build flavors that each
hold one package, gives every name in it one candidate per copy.
Directory proximity is capped at six shared folders, so in a deep tree
the copies scored the same and the first indexed won: a call in one
flavor could land on the other flavor's copy. And since the name is not
in doubt among copies, only which copy, scoring the right pick by
proximity (0.7, or 0.4 in a shallow tree) understated it.

findBestMatch now breaks a tie among copies of one symbol (same
qualified name, kind and file name) toward the copy whose file shares
the longest directory prefix with the reference, when that copy is
uniquely closest. matchByExactName scores that pick 0.8: above a
proximity guess, below a unique name or the file's own definition (0.9,
which keeps precedence).

On top of the PR: the copies are judged among the candidates
findBestMatch weighed, so a namesake in another language no longer
blocks the 0.8; a match of the caller itself keeps the proximity score,
as it does for a same-file match; and Kotlin and Java flavor tests, since
the PR's Python fixture calls a function it never imports.

Validation, main (cde4cd74) vs this commit, 21 repos in 11 languages:
no edge added or removed; 4 targets moved, all right: spring-modulith's
example-full tests now reach example-full's own OrderManagement and
OrderCompleted, where Spring's naming rule (0.7) took the epr-jdbc
example's copy, and playframework's shutdown-happy-path test its own
src/main HomeController, not the copy its script swaps in later. 109
edges keep their target and go to 0.8, each onto the copy in the
caller's own module or app; 4 of them are vben's `defineConfig` calls
from `import { defineConfig } from 'tsdown'`, which main already links
to a namesake because that dependency is declared only in the root
package.json. In 18 sites the 0.8 now beats a framework rule's 0.7.
Index CPU, median of 3 interleaved: okhttp 34.8 vs 34.8 s, etcd 36.5 vs
36.5 s. Tests: 3 of 6 fail on main, 2 on the PR as written.

Original PR #1936 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:26:51 +00:00
Aaron QueenandClaude Opus 5.5 69af19ace2 fix(build): building from source with bun finishes instead of starting 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 #1764 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 02:16:17 +00:00
danusha2345andClaude Opus 5.5 4c572e07b6 fix(play): a Class.method reference reaches a member of the class it 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 cde4cd74: 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 #1950 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:52:27 +00:00
danusha2345andClaude Opus 5.5 cde4cd74eb fix(resolution): a lone same-file definition scores as a strong match
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 #1938 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:23:32 +00:00
Aaron QueenandClaude Opus 5.5 0b238452a3 fix(mcp): explore's maxFiles text gives the real default, not 12
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 #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 #2783, #2077).

Original PR #1972 by @bompus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:55:56 +00:00