Commit Graph
1765 Commits
Author SHA1 Message Date
Eric MinishandClaude Opus 5.5 59eb0d2820 fix(resolution): an import no longer lands on a same-named method of an exported object
An exported object literal's methods (`export const d = { f() {} }`) are
indexed as exported nodes named `f` (qualified name `d::f`), and the
per-file export index kept the first exported node per name. So
`import { f } from './m'` could link to `d.f` instead of the module's own
`export function f`, and which one won depended only on declaration order.

A JS/TS member, a node whose qualified name differs from its name, now
ranks behind the module's own bindings wherever an import is matched to an
export: a named import (the module's own `f`, then its export clause, then
its re-exports, then a member), an export clause's local name,
`export default f` and `module.exports = f`. A member is never dropped:
`export const { f } = d` binds `f` with no node of its own, and `d::f` is
its only answer. The default-import guess no longer picks a member.

Validation on main de0a4eb3 (Windows 11):
- Red/green: 7 of the 13 new tests fail with main's import-resolver.ts;
  all 13 pass with the fix, with the kernel and with CODEGRAPH_KERNEL=0.
- A/B against main: express, vuejs/core and excalidraw are byte-identical,
  as are the flask and gin controls. codegraph's own source retargets 195
  sites, each from a `hashNavigation` method to the exported function of
  the same name in ui/src/lib/navigation.ts, the issue's case. vscode's
  base and editor retarget 328 sites: 321 imports and calls of `join`,
  `basename`, `dirname` and 5 other path functions move from the `win32`
  object's methods to path.ts's own exports
  (`export const join = (platformIsWin32 ? win32.join : posix.join)`), and
  7 `marked.lexer` and `marked.parse` calls move from a `_Tokenizer` field
  and a `_Parser` static method to the `lexer` and `parse` that marked.js
  exports. No edge is added or removed.
- Index time unchanged: median of 3 interleaved runs on vscode's base and
  editor, 16.6 s on main and 17.2 s with the fix.

Original PR #2990 by @inth3shadows.
Fixes #2945

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:20:22 +00:00
Max HsuandClaude Opus 5.5 1c40e34141 fix(mcp): a projectPath naming a file resolves through its directory
An agent that passed the file it was looking at as `projectPath` got
`isError: true` ("Path is not a directory"), while a path that doesn't
exist at all walked up to the project's `.codegraph/` fine. An error teaches
an agent to stop calling codegraph, and this mistake is recoverable.

`execute()` now takes an existing file to stand for its directory, right
after the length check, and rewrites `args.projectPath` there, so the
project gate, the worker dispatch, the worktree notice and getCodeGraph all
see that directory. A path that can't be stat'd walks up as before.

Path safety: the directory goes through the same checks as any
projectPath, the sensitive-folder refusal on the path itself and on the
root its up-walk lands on, so a file reaches exactly what its directory
reaches and never more. `/etc/hosts` is refused as `/etc` is, a file in
`~/.ssh` as `~/.ssh` is, and `win.ini` in the Windows folder as that folder
is. A symlink inside a project that points at a file in a sensitive folder
is answered from the link's own folder, as a symlinked folder already is;
nothing in the sensitive folder is opened.

Validation on main de0a4eb3 (Windows 11):
- Red/green: the PR's 2 tests (a file in an indexed project, and a file in
  a linked worktree, which keeps its worktree notice) and 1 of ours fail
  with main's tools.ts and pass with the fix. Ours asks for a file and for
  its directory in 4 cases, an indexed project, `~/.ssh`, a folder in no
  project, and the Windows folder's `win.ini`, and requires the same
  answer for both, refusals included.
- A real `serve --mcp` session over stdio with a private HOME: on main,
  codegraph_search and codegraph_status for `<project>/src/a.ts` both
  returned isError "Path is not a directory"; with the fix both answer from
  the project, and `win.ini` is refused as a sensitive system directory.

Original PR #2996 by @maxmilian.
Fixes #2982

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:00:53 +00:00
Eric MinishandClaude Opus 5.5 5526e33d1d fix(status): recommend a re-index when package.json alone adds or drops a framework
Since PR #2941, a sync records the frameworks that came or went since the
last full index, and `codegraph status` recommends a re-index. But a sync
detects frameworks again only when it extracts a file, so adding `express`
or `@sveltejs/kit` to package.json with no source file edited recorded
nothing: status said the index was up to date while an unchanged Express
app's routes, or the `goto('/login')` in an unchanged +layout.svelte,
stayed unlinked until a full index.

A full index now also stamps the resolver's framework set, and
getFrameworkChangesSinceIndex() adds the difference between that stamp and
the set the resolver detects now (it detects on every open) to what syncs
recorded. So a fresh `codegraph status` sees a manifest-only change whether
or not a sync ran. An index built before this has no stamp, so that half
starts at its next full index. A long-running server's resolver keeps its
old set until it extracts a file, as before.

Validation on main 61776927 (Windows 11):
- Red/green: 3 of the 4 new tests fail with main's src/index.ts; the 4th,
  an unchanged project staying silent, passes on both. All 4 pass with the
  fix, as do the 8 sync-framework-detection tests.
- The CLI before and after, on a SvelteKit fixture indexed without
  @sveltejs/kit, then given it in package.json, then synced: main's status
  says "Index is up to date", and --json has reindexRecommended false and
  frameworksChangedSinceIndex null. With the fix, status adds "Frameworks
  changed since the last full index (added sveltekit-router)", and --json
  has true and {added: [sveltekit-router]}. Dropping the dependency gives
  {removed: [sveltekit-router]}, adding express to an Express app gives
  {added: [express]}, an unchanged project gives null on both arms, and a
  full `codegraph index` clears the advice.
- No false advice: a fresh index and then `status --json` on 18 real repos
  reports no change on any. They include express, flask, gin, vuejs/core,
  excalidraw, vuejs/docs, starlight, codegraph's own source and vscode's
  base and editor; Express, Flask, Next.js, React, Vue, Vue Router, Astro,
  Svelte, Spring, Go and UIKit are detected among them.

Original PR #3005 by @inth3shadows.
Fixes #3007

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:36:12 +00:00
Colby MchenryandClaude Opus 5.5 063708ba53 fix(pascal): Delphi chained calls and method pointers follow declared types (#3065)
Four Delphi bugs that PR 2550 by @OleksiyPenkov found, reimplemented on
main's Pascal scope code rather than as a second resolver:

- A call through a chain of members and methods recorded only the outer
  method's bare name, which name matching gave to any namesake: SynEdit's
  Canvas.Font.Assign(Font) went to TProposalColumn::Assign, and
  TLogger2.Create.Write('y') linked nothing, a bare Write being a System
  name. The extractor now writes such a call as the typed chain
  pascal-scope already resolves for paren-less names ({call} for a
  statement, a new {args} mode for a last link written with its
  arguments). Each link is typed by its declaration: field and property
  types, function results, a local's declared type, through pointer and
  class reference types (PVirtualNode = ^TVirtualNode, TSynLogClass =
  class of TSynLog) and explicit dereferences (a^.Copy). A constructor
  called on a class gives an instance of it, a unit name, dotted ones
  too, qualifies its declarations, and a chain called with arguments may
  end at data that holds a routine. There is no bare fallback: a chain
  through a type from outside the project links nothing, and in a with
  body only a chain from Self or a typed local is read.
- A method given to a target was a method pointer only when the target
  was named On.../FOn...: Callback := FProvider.Value with Callback: TGet
  recorded a call, and FOnGet := @FProvider.Value nothing. A value given
  to an assignment now carries its target, whose declared type decides
  (routine types written out, of object, reference to, project aliases,
  the RTL's routine type names; a class, a record or a System value type
  holds no routine); the name decides only where the declaration can't
  be read. A function that returns a routine type is called for it, and
  @Obj.Method is a reference anywhere.
- A nested routine's locals leaked into the routine around it: the name
  matcher's Pascal receiver scan read every declaration above the call.
  It now steps over routines nested elsewhere, reads the calling
  routine's own name lists (Doc, Doc2: TDocVariantData), which main typed
  only by luck through a nested routine's parameter, and follows pointer
  and class reference types. Where the tree has errors, the source-text
  scopes give the outer routine its own names back after a nested one.
- A nested routine declared forward is in scope from that declaration.

Validation:
- New __tests__/pascal-reference-boundaries.test.ts, 41 tests, 24 of
  them ported from PR 2550: 22 fail on main's sources, the other 19 pin
  what main already got right. pascal-chained-calls and pascal-for-in-var
  pinned the bare outer-method names and now expect the typed chains
  (4 tests, failing on main). Twenty mutations, one per rule, each fail
  a test.
- A/B against main 80f6b505, both arms built from their own sources and
  diffed by natural keys, edges removed/added: SynEdit 195/66, HeidiSQL
  789/320, mORMot2 2553/4164, DelphiMVCFramework 2394/1850, DWScript
  without its Test folder of scripts (519 files) 864/801. leveldb and
  SDWebImage byte-identical.
- Removed edges, hand-triaged: SynEdit all 195, 193 namesakes or parse
  artifacts and 2 right ones lost (Get(i).LoadFromStream in a with
  body); samples of HeidiSQL 20/20, DelphiMVCFramework 30/30, DWScript
  12/12 and mORMot2 23/25 namesakes (of the other two, one now links the
  unit's own CompareMem, and one right edge is lost, through a field
  declared after the first name of a name list, which the extractor
  gives no node).
- Added edges sampled right: typed chains 186 of 187 (the wrong one
  reaches the FMX variant of a conditional ancestor), receiver calls
  40/40, method pointers, @-references and kind changes 57/57.
- CPU, median of 3 interleaved: SynEdit +6.6%, HeidiSQL +0.6%, mORMot2
  -2.5%, DWScript +4.7%, while the identical controls moved 0.0% and
  -5.9%. DelphiMVCFramework, timed before the scan's memo was split per
  routine (which only made it cheaper): -6.6%.

Reimplements part of PR 2550 by @OleksiyPenkov.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:29:03 +00:00
Colby MchenryandClaude Opus 5.5 fd57eaaa45 fix(mcp): a tools/call whose arguments aren't an object gets invalid params, not a crash (#3071)
A tools/call request passes its arguments as an object. MCPSession took
params.arguments || {} without checking that, so a string or number went
straight to the tool handler, where the session-view check
('_cgExploreSession' in args) threw. The call then came back as a tool
result flagged isError: "This is an internal codegraph error, retry the
call once; if it persists, continue without codegraph". That reports a
client's malformed request as codegraph's own malfunction, with advice to
stop using it. An array went through as if it were the arguments. The
stdio proxy, serving in process while the daemon is unavailable, had the
same unchecked read.

Both now read the arguments through one helper, toolCallArguments(), and
answer anything but an object with a JSON-RPC invalid-params error
(-32602, "Tool arguments must be an object"), as a call with no tool
name already is; no tool runs, and the proxy starts no engine for it.
Missing, null or another empty value still means no arguments.

Validation: new mcp-tool-call-arguments.test.ts covers the session (the
direct stdio server and every daemon client) and the real proxy entry
point in a child process with the daemon unavailable: red on main (2 of
3 failed; the well-formed control passed), green with the fix.
explore-session-state and proxy-shutdown pass. Driving serve --mcp over
stdio: on main, codegraph_explore, codegraph_node and codegraph_search
with a string argument returned isError "internal codegraph error" and
codegraph_status accepted [1], 42 and true; with the fix those six get
-32602, while "", null, {} and absent arguments run as before.

Reimplements PR #2986 by @kjgggggg.

Fixes #2981

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:16:43 +00:00
Colby MchenryandClaude Opus 5.5 de0a4eb362 fix(vue): a self-closing <script /> no longer swallows the next script (#3070)
The Vue extractor read a self-closing `<script src="./legacy.js" />` block
as an opening tag running on to the next `</script>`, as the Astro and
Svelte extractors did until PR #3063 (PRs #2995 and #2997) landed. The
`<script setup>` after it was parsed with whatever lay in between and
contributed nothing, and a `<template>` in between lost its calls and its
components too. One copy of the same pattern sat in each of 9 places in 5
files, and that fix reached the 6 copies in Astro, Svelte and the viewer.

sfc-script.ts, which the Vue, Svelte and Astro extractors already share,
now exports the script pattern and the script-or-style pattern, built from
@maxmilian's regexes, plus sfcBlockLineRanges, the line ranges a template
scan skips. A self-closing tag has no body, so it covers nothing and isn't
blanked, and calls in its attributes are read. All 9 sites use them: each
extractor's script blocks, Astro's covered ranges, both Svelte template
scans, Vue's template-components scan, the Vue template-calls blanking
and the viewer's highlight regions. The pair also accepts a closing tag
written with a space, `</script >`, as the template-calls blanking always
did; the viewer finds a block's body from its opening tag for that reason.

Validation on main 61776927 (Windows 11):
- Red/green: 4 Vue tests (a `<script src ... />` block before
  `<script setup>`, one before `<template>`, an attribute call on a
  self-closing script in the template, and `</script >`) fail with the
  previous sources and pass; the 6 Astro and Svelte tests still pass.
- A/B: 5 Vue repos (vuejs/core, vuejs/docs, NekoBox, justgage,
  element-plus-playground; none has a self-closing script) are
  byte-identical to main. 7 Astro repos, 2 Svelte subtrees (shadcn-svelte
  docs routes, SvelteKit's basics test app) and the flask and express
  controls are byte-identical to the Astro and Svelte fix before this one.

Fixes #2989

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:02:53 +00:00
Colby MchenryandClaude Opus 5.5 cb7fb61e76 fix(docs): agent and command lists match the code; worktree warning drops init -i (#3067)
The warning for a git worktree answered from another checkout's index
still said to run "codegraph init -i", a flag that has done nothing
since init started indexing by default. Its one-line form is prepended
to every read-tool result and codegraph_status embeds the long form, so
both told the agent to build the index, which the server instructions
rule out ("Indexing is the user's decision ... don't run it yourself").
The CLI's codegraph status now says: Run "codegraph init" in this
worktree. What an agent reads (the read-tool notice, which also reaches
it through the prompt hook and the explore/node CLI, and
codegraph_status) says the user can run "codegraph init" there.

Docs that no longer matched the code:
- AGENTS.md listed 4 of the 11 installer targets, called a new one "a
  5th agent", said the contract suite covers "all 4" targets, and listed
  11 of the 24 subcommands. It now lists every target and subcommand,
  and names the hidden ones (serve --mcp, prompt-hook, and ui/web unless
  CODEGRAPH_UI=1).
- README's Supported Agents section said no instructions file is
  written, but Claude Code, Codex CLI, Gemini CLI and opencode get the
  short CodeGraph section. Its Codex entry now says the Codex IDE
  extension reads the same config: $CODEX_HOME/config.toml (default
  ~/.codex/config.toml) for a global install, and .codex/config.toml,
  used only in a trusted project, for a local one.
- README's WSL2 "Transport closed" entry named only the in-process
  fallback. The daemon also moves its socket to the temp directory when
  the project's filesystem can't host one.
- init -i and -iv leftovers in internal docs, scripts, the add-lang
  skill and the repo's own Cursor rule, which now also leaves indexing
  to the user. Two stay on purpose: scripts/agent-eval/audit.sh can
  audit a published version older than 0.9.8, where init built the
  index only with -i (now commented there), and
  docs/benchmarks/codegraph-ab-matrix.md records a 0.9.4 run.

Validation:
- worktree-detection.test.ts: on main 61776927's worktree.ts and
  tools.ts, 4 of its 20 tests fail (the CLI wording, the agent wording,
  the read-tool notice and codegraph_status). With only the worktree.ts
  fix, the codegraph_status test still fails. All 20 pass here, and so
  do mcp-nested-repo-projectpath.test.ts's 13.
- Full suite on this patch over 61776927 (CODEGRAPH_KERNEL_EXPECT=1,
  4 workers): 815 files (809 passed, 6 skipped), 9608 tests passed,
  0 failed, 86 skipped.
- Rebased unchanged onto f67eafcb. Its three new commits share only
  CHANGELOG.md with this PR, in other bullets. On the rebased tree these
  pass (81 tests): worktree-detection, mcp-nested-repo-projectpath, and
  the new commits' default-export-declaration,
  rust-path-attribute-modules, rust-path-super-glob,
  search-special-characters and strip-comments.

Reimplements #549 by @ashishpatel26, #651 by @GaoSSR, #885 and #886 by @Dashsoap, and #1083 by @zengwenliang416.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:47:59 +00:00
Colby MchenryandClaude Opus 5.5 f67eafcbc5 fix(search): a pasted call like leaf(entry) finds both names instead of nothing (#3069)
Full-text search dropped the FTS5 syntax characters ( ) ^ " ' * : from a
query before quoting each word, so the words on either side of one were
glued together: leaf(entry) became the single prefix term "leafentry"*,
which no symbol has, and the LIKE and fuzzy fallbacks don't recover it.
codegraph query, the codegraph_search tool and searchNodes() all came
back empty for a pasted call like render(props).

The index's unicode61 tokenizer splits stored text at anything but
letters and digits (a signature's leaf(entry) is stored as leaf and
entry), so the query now splits there too: each of those characters
becomes a space, the treatment :: already gets (issue #173).

Validation: new search-special-characters.test.ts (8 tests) fails 8 of 8
on main and passes with the fix; fts5-fallback, search-field-filters,
search-query-parser, cli-query-command and symbol-lookup pass. A query
A/B on expressjs/express 0dee2c52, both arms over one index: 12 control
queries without such a character between words are byte-identical; 10
pasted calls changed, 7 of them from no results to results led by the
function they name (res.send(body), res.json(obj),
View.prototype.lookup(name)).

Reimplements PR #2987 by @kjgggggg.

Fixes #2971

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:31:02 +00:00
Colby MchenryandClaude Opus 5.5 cc17978174 fix(rust): a module a #[path] attribute loads is the module that declares it (#3064)
A Rust module loaded with `#[path = "…"]` was found by its file's directory:
fast-mlsirm's `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`
in `crates/core/src/scaling.rs` made `tests/unit/scaling_tests.rs`'s
`use super::*` walk to a `tests/unit` module that doesn't exist, so its
`predict_rating_multi(1)` linked to nothing, or to a same-named function of
another crate; its `crate::` had no crate, and the parent's private items
looked out of its reach. From the declaring side, a path through such a
module (`utils::mock_event(…)` under `#[path = "./utils.rs"] mod utils;`)
reached nothing.

The fix reads each file's `#[path]` declarations as the Rust Reference
reads the attribute (from the declaring file's directory, or inside an
inline `mod` from that module's own; a path that leaves the project, by
escapesRoot, or that a `cfg_attr` sets, names no file):
- rustSubmodule: a module the module declares once with a `#[path]` is
  the file it names, whatever else loads that file too (a helper several
  tests load is each test's); cfg alternatives that name a file each stay
  unknown, and a path's 2015 crate-root fallback no longer reaches past
  such a module to a namesake at the crate root.
- rustPathClaim: a file the regular rules don't place is the module of the
  one `#[path]` declaration that loads it, if exactly one does. Placed
  (rustPlacedByName) are crate roots, files with a `fn main` or
  `#![no_main]` of their own (trybuild cases, fuzz targets), and files a
  plain `mod name;` declares in the module owning their directory or in an
  inline module of one above it (dioxus's `mod components { pub mod home; }`
  in main.rs). Only for a file left over are the project's Rust files that
  write `path =` read (a byte check first), once per resolver. With a
  claim, rustSuperModule is the declaring module, rustFileModule owns the
  file's directory (rustc reads every `#[path]` file as a mod.rs),
  rustCrateRootModule is the declaring file's crate, and rustModuleWithin,
  isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the
  declaring module, so `use super::*` brings in its private items.
- rustCrateGraph: `[[example]]`, `[[test]]` and `[[bench]]` paths are
  target roots, as `[lib]`/`[[bin]]` ones are since PR #3014, and such an
  example is its own crate's root (dioxus's
  `examples/07-fullstack/handling_errors.rs`).

Validation:
- rust-path-super-glob.test.ts (PR 1892's case, credited) and
  rust-path-attribute-modules.test.ts (7 cases): on main all 8 fail; all
  pass. Every rust-* suite passes.
- A/B against main, native kernel: fast-mlsirm (its Rust files) +2,796
  links (1,839 calls, 427 references, 334 constructions, 196 imports), 71
  retargeted (exposure_tests.rs's `Lcg::next_f64` from cdm_tests.rs's
  private `Lcg` to exposure.rs's, the declaring module's helpers instead
  of bifactor_grm.rs's), 87 reaching the same item through the declaring
  module (confidence 0.4 to 0.9), 0 removed; dioxus +27 (each headless
  test's `utils::mock_event(…)` into the helper five tests load); tokio +2
  (`use super::AtomicU64` in the files atomic_u64_as_mutex.rs loads);
  ripgrep, axum, clap, serde, gleam, express and flask unchanged.
- Cost, native kernel, medians of 3 interleaved runs: fast-mlsirm CPU 15.3
  -> 15.8 s, wall 4.2 -> 5.2 s, peak RSS 825 -> 882 MB; tokio CPU 26.1 ->
  26.7 s, wall 10.4 -> 11.2 s, peak RSS 1,023 -> 1,028 MB; dioxus CPU 34.8
  -> 35.6 s, wall 10.3 -> 11.2 s, peak RSS 1,125 -> 1,120 MB; gleam CPU
  24.1 -> 23.8 s, wall 6.6 -> 6.7 s, peak RSS 1,027 -> 1,023 MB.

Builds on PR #1892 by @seonghobae.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:21:12 +00:00
Colby MchenryandClaude Opus 5.5 ed6b88e53a fix(js): an export default line inside a template literal no longer names the default (#3068)
statedDefaultExport reads a JS/TS file's own export default statement
by scanning comment-stripped source for line-initial `export default`
heads. Strings are deliberately not masked there (a masker that loses
step on nested templates blanked real statements), and a head counted
when it named a node of the file, the first such head winning. A code
generator's template text breaks that: in

  export function App() {}  export function Main() {}
  export const template = `
  export default App
  `;
  export default Main;

the template's line names the real App, so `import X from './gen'`
bound to App instead of Main (issue #2968). A template holding
`export default () => …` also made a module whose real default is an
expression read as anonymous, which keeps every guess away.

templateLiteralTextAt (strip-comments.ts) scans the source once for
template-literal text: it follows `${…}` nesting (a template inside an
interpolation, a brace inside a regex there), and skips comments,
quoted strings and regex literals, which may hold a backtick; a `/`
after a value divides, anywhere else it opens a regex. A head inside
template text is now skipped. When some template never closes the scan
vouches for nothing and every head counts, as before. Nothing else
changes: a head outside template text is read exactly as it was.

Validation: default-export-declaration.test.ts gains the issue's file,
a template returned by a function behind a nested template, one passed
at module scope and one in a class field, a template's anonymous
default above an expression default, and controls (a regex and a string
holding a backtick, a division after a parenthesis, and a file whose
scan can't close a template); strip-comments.test.ts tests the scan
directly. Red with main's resolver: 2 of 10 failed; green: all pass.
Checked against tree-sitter on every line-initial head of 4,191
JS/TS files in 16 corpora (outline, excalidraw, vben, bitwarden apps,
mastodon, shadcn docs, zod, bulletproof-react, express, astro, react,
trezor-suite, this repo's src, tests and ui): of 4,149 real export
default statements the scan puts none inside template text, and all
322 heads it does place there are template text. A/B on excalidraw
d3a9c2a1, vben 61c07936, astro e3bec3d2 (packages/astro/src and
packages/integrations), with gin-gonic/gin 0f09c3a9 and psf/requests
3c7baaa0 as controls, both arms on main 4b0f72b7 through the native
kernel (and again on 66449255 through wasm): all six byte-identical,
since no default import there is decided by template text; index time
unchanged.

Not in this change: hasEsmDefaultExport, which only gates
require('./x').default, still reads the raw text.

Reimplements PR #3042 by @johnhldiver-beep.

Fixes #2968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:55:21 +00:00
Max HsuandClaude Opus 5.5 617769279d fix(astro,svelte): a self-closing <script /> no longer swallows the next script
A self-closing `<script ... />`, such as Astro's
`<script is:inline set:html={code} />` or a third-party
`<script src="/analytics.js" />` in Svelte's `<svelte:head>`, matched the
extractors' script regex as an opening tag, so the match ran on to the next
`</script>`. The real script that followed was parsed with the markup in
between, failed with "parse produced no symbols", and contributed nothing.
The covered-range regexes had the same shape, so the template scans also
skipped the markup between the two tags, and a self-closing `<style />` hid
the markup after it.

The opening-tag pattern now matches a `/>` ending on its own as an empty
tag: in the Astro extractor's script blocks and covered ranges, the Svelte
extractor's script blocks and both of its covered-range scans, and the
viewer's highlight regions.

With a fix of ours: a self-closing tag has no body, so the covered-range
scans skip it, and a call in its attributes, like
`set:html={serializeJsonLd(schema)}`, is read as on main. Matched as a
range, such a tag hid those calls even where no script followed it.

Validation on main bc9291af (Windows 11):
- Red/green, 6 tests: 4 from the PRs, plus 2 of ours for an attribute call
  on a self-closing script in Astro and in Svelte. With main's
  astro-extractor.ts, svelte-extractor.ts and syntax-tokens.ts all 6 fail;
  with the PRs' code as written the 2 of ours fail; with this commit all 6
  pass.
- Svelte 5.56.10's own compiler reads the Svelte fixture as an empty head
  script plus an instance script, as the extractor now does.
- A/B against main on 7 Astro repos, with flask and express as controls.
  starlight, diploi.com and both controls are byte-identical, and
  devfest-website keeps every edge. Gains: the genkit docsite 2 import
  links, nodejsdesignpatterns.com 2 nodes and 2 import links, a portfolio
  25 component references, and wm-project-astro-components 3
  `serializeJsonLd` calls from JSON-LD scripts plus 1 call in a script main
  swallowed. That repo also drops 31 false calls, CSS like
  `border-radius: var(...)` that main read as calls after parsing a
  swallowed `<style>` block as TypeScript. No other edge changes.
- Index time unchanged: median of 3 interleaved runs on
  wm-project-astro-components, 4.67 s on main and 4.52 s with the fix.

Vue's extractor has the same pattern and the same bug; a follow-up shares
one pattern across the three extractors.

Original PRs #2995 and #2997 by @maxmilian.
Refs #2989

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:24:40 +00:00
Eric MinishandClaude Opus 5.5 4b0f72b7a7 fix(sync): a skipped file that now indexes re-links its importers
A file over the source-size limit, or one whose parse failed, is tracked
with no nodes and its error recorded, so the imports of it fail. When an
edit lets it be indexed, sync stored it as a modified file, and only an
added file is looked up by what it can be imported as: an unchanged
importer's import of the file itself stayed failed, and so did what a
barrel passes on from it, until a full re-index. Named imports and calls
bound, since they wait by name.

A re-indexed file whose stored row had no nodes and recorded errors now
joins addedFilePaths once the store gives it nodes, so the added-file
retries and the importer and barrel walks run for it. It still counts as
modified. Deciding after the store leaves a file that changes but stays
over the limit, or still fails, alone, so it does not re-run those
retries on every sync.

Review changes: a test for the barrel shape, the one behind most of the
real-repo divergence below, and the CHANGELOG entry beside the one for
files saved with no symbols.

Validation: 5 tests; on main's source 4 fail and the stays-over-the-limit
control passes. 9 fixture edits, each compared with a fresh index of the
same tree, in a later process and in the same process: main diverged in 7
of each, this in 1, a barrel growing over the limit, where only the
edges' resolvedBy differs (same targets), as the PR noted. excalidraw with
packages/math/src/point.ts over the limit at index time and put back:
main was 1,506 edges short of a fresh index, identical here over nodes,
edges, refs and files.

Original PR #3022 by @inth3shadows.
Fixes #3016

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:11:35 +00:00
Eric MinishandClaude Opus 5.5 2d98c1b69a fix(sync): a call left with one candidate links after a delete or a rename
x.go() with go in two classes stays unlinked while both exist. A sync that
deleted one of them, or renamed it away, never retried that call: the
failed-reference retry looks refs up by the names the changed files carry
now, which a deleted file or a renamed-away name no longer has, and a sync
that only deletes files skips the retry. Failed refs under the names a
sync's definitionDelta lost or gained now go back to pending for the
sweep, next to the CG-33 rebind that does the same for resolved edges,
under the same per-name ceiling. CODEGRAPH_NO_REBIND=1 turns both off.

Review changes: the CHANGELOG entry beside the sync entries for calls,
with credit.

Validation: 6 tests; on main's source the 4 delete and rename tests fail
and the 2 still-ambiguous controls pass. 10 fixture edits (Python,
TypeScript, JavaScript, Go, Java), each compared with a fresh index of the
same tree, in a later process and in the same process: main diverged in 6
of each, this in none. excalidraw with excalidraw-app/data/LocalData.ts
deleted (its `save` was one of two): main was 38 edges short of a fresh
index, identical here over nodes, edges, refs and files; renaming
BinaryHeap's `pop` away re-opens 28 refs that fail again, as in a fresh
index.

Original PR #3004 by @inth3shadows.
Fixes #3006

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:00:19 +00:00
Colby MchenryandClaude Opus 5.5 e431491972 fix(mcp): a session whose host stops reading its output shuts down cleanly (#3060)
Issue #2991: a host can stop reading an MCP stdio server's output while
it keeps the server's stdin open and stays alive. The next response the
server wrote then failed (EPIPE), and with no listener on stdout Node
handed the error to the last-resort fatal handler, which exits 1 at
once. That skipped the shutdown that stops the engine, releases the
writer lock and waits for query workers still starting, which on
Windows can crash a process that exits under them. Stdin, the host's
PID and the liveness watchdog all still look healthy, so nothing else
noticed.

A new treatStdoutFailureAsShutdown, the twin of the stdin teardown from
issue #799, routes a stdout 'error' (and the 'close' after it) into the
owner's own shutdown, at most once, destroys stdout so later writes are
dropped, and keeps its listener so a write queued before the failure
cannot escape during the shutdown. Backpressure is left alone. It is
wired into the direct-mode transport and into the local-handshake proxy,
whose shutdown closes only this session's daemon connection and stops
the engines it owns. The legacy socket proxy (runProxy) has had no
caller since PR #580 and is left as it is.

Validation: __tests__/mcp-stdout-failure.test.ts runs a real
direct-mode serve --mcp, and the local proxy with an in-process engine
and with a stand-in shared daemon. After the test destroys its end of
stdout and sends one more request, main exits 1 in all three, the
server with "Uncaught exception: Error: EPIPE"; with the fix all three
exit 0, the server's writer.pid is released, the proxy's engine stop
runs, and the daemon closes only the proxy's connection and still
answers its other client. __tests__/stdout-teardown.test.ts covers the
guard itself. proxy-shutdown, stdin-teardown and mcp-initialize pass.

Fixes #2991

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:48:05 +00:00
danusha2345andClaude Fable 5.1 664492551d fix(sync): a barrel whose re-export points elsewhere re-links its importers
`c.ts` imports `foo` through `index.ts`, which re-exports it from `a.ts`.
Change the clause to `./b` and sync, and `bar` kept calling `a.ts:foo`
where a fresh index gives `b.ts:foo`. The barrel declares nothing of
`foo`, so the definition delta has nothing to rebind; no file was added
for the walks below a barrel or the added-files pass; the binding never
failed, so the retries never see it; and the name-matched-import pass
reads only bindings a name match took. `export *` moved the same way, and
a scoped sync of the barrel alone too.

The orchestrator now reports the modified ES modules whose `export … from`
statements changed, read off their import nodes (named by the module, with
the statement as signature) before and after the store. Their importers'
bindings through them go back for the sweep, up through the modules that
pass them on in turn, with the failures parked under them and the edges
into the members of what the bindings pointed at (`w.run()` with
`w: Widget`). It is the importer walk the removed-files pass uses, run
after the name-matched-import pass.

Review changes: the trigger compares the `export … from` statements
themselves. The original compared the modules a file imports and counted a
module that lost any import, so dropping a plain import from excalidraw's
`types.ts` (218 importers, no re-exports) re-opened 2,186 refs for the same
graph; it now re-opens none. Comparing statements also catches a name
dropped from a clause whose module stays (`export { foo, qux } from './a'`
to `export { foo } from './a'`), which the module set missed. Plus tests
for both, and the doc comments the new code sat between moved back.

Validation: 9 tests; on main's source 7 fail (the 2 controls pass), and
on the original trigger the 2 new ones fail. 15 fixture edits, each
compared with a fresh index, in a later process and in the same process:
main diverged in 8 of each, this in none. excalidraw with
`export * from "./utils"` in its math barrel (116 importers) retargeted to
`./constants`: main was 224 edges off, identical here over nodes, edges,
refs and files. Fresh indexes are identical to main's.

Original PR #2985 by @danusha2345.
Fixes #2978

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:29:45 +00:00
Colby MchenryandClaude Opus 5.5 d139865f9f fix(db): processes opening an index that needs a migration all open it (#3058)
Issue #2980: opening an index whose schema is behind read its version,
then ran each pending migration in a deferred transaction. When several
processes opened it at the same moment, those that read the version
just before another one migrated applied the same migration again, and
their open failed: on this Windows machine with "UNIQUE constraint
failed: schema_versions.version", since the version was already
recorded, and in the report with SQLITE_BUSY, which a deferred
transaction that has already read gets at once when it comes to write.

Each migration now runs in a transaction that takes the write lock up
front (BEGIN IMMEDIATE, a new option of the adapter's transaction()),
so a process that finds another one migrating waits it out on
busy_timeout, and reads the version again inside it, skipping a
migration already applied. The version the caller read stays a floor,
as before: a database built from schema.sql records only v1 and the
current version, and tests replay migrations above a version they
rewound to. An open of a current index takes no lock and waits for
nothing, as before.

Validation: __tests__/migration-concurrent-opens.test.ts. A second
process holds the write lock with version 16 recorded but uncommitted,
so the open reads 15: main fails with the UNIQUE error after waiting
1.1 s; the fix waits, opens and leaves the migration to the other
process. Three processes released at once from a stdin barrier on an
index with 150k unresolved references: main fails in round 0, the fix
opens all three in both rounds. On excalidraw's real index, 8 rounds of
three openers: main 7 rounds with failures, the fix 0. The single
process and the current-index cases pass on both.

Fixes #2980

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:15:26 +00:00
danusha2345andClaude Fable 5.1 80f6b50504 fix(sync): a tsconfig, jsconfig or go.mod change re-resolves the imports it governs
The resolver read tsconfig / jsconfig `paths` and `baseUrl` and go.mod's
module path once and kept the maps for its lifetime ("callers re-create the
resolver if config changes", which none did), and the watcher follows source
files only. So after an edit to one of them a long-running MCP server kept
resolving imports by the old mapping, and so did later processes: a sync
re-resolves only the changed files, and a config-only edit changes none.

The index now carries a snapshot of the config files resolution read: every
tsconfig / jsconfig the alias loaders opened, `extends` chains included,
every go.mod, and the root's candidates whether present or not. Every sync
reads them again and, when one differs, drops the resolver's alias and
module maps with the memos resolved through them, re-opens every import the
file governs (the ES-module family for a tsconfig / jsconfig, Go for a
go.mod) for the sweep, as a moved workspace map does, and marks synthesis
for a rebuild. An index an older engine stamped records a snapshot without
re-opening anything.

Review changes: each file's digest covers what resolution reads from it, a
go.mod's module path and a tsconfig / jsconfig's `extends`, `baseUrl` and
`paths`, not its bytes. A dependency bump in go.mod or a `strict` flag in
tsconfig had re-opened every import of the language for the same graph
(caddy: 48,945 refs, sync 3.8 s against 0.9 s; excalidraw: 88,860 refs,
9.2 s against 1.1 s). Plus a test for that, the ReferenceResolver class doc
comment back on its class, and the CHANGELOG entry beside the workspace-map
one.

Validation: 7 tests; on main's source the 5 behaviour tests fail, and the
new one fails on the byte digest. 11 fixture edits, each compared with a
fresh index of the same tree, in a later process and in the same process:
main diverged in 7 and 9, this in none. Real repos, sync against a fresh
index over nodes, edges, refs and files: excalidraw with `@excalidraw/math`
retargeted was 27 edges off on main and is identical here; caddy with its
module path renamed was 6,511 edges off and is identical here. Fresh indexes
are identical to main's. A one-file sync on excalidraw, median of 5
interleaved runs: main 3,834 ms, this 3,269 ms.

Original PR #2984 by @danusha2345.
Fixes #2977

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:57:07 +00:00
Colby MchenryandClaude Opus 5.5 29ce0149cf fix(lock): only one process takes over a stale write lock (#3056)
Issue #2979: FileLock.acquire() read the lock file, saw a dead PID,
unlinked the file and created its own with an exclusive create. Two
processes that read the same dead PID could both succeed: the first one
replaced the stale lock, then the second one, acting on what it had read
a moment earlier, unlinked the first one's live lock and created its own.
Both returned from acquire() and wrote to the index as its only writer.
The same steal happened in two narrower ways: a lock file read in the
instant between its exclusive create and the write of its PID was empty,
so it counted as stale, and a PID the process may not signal (EPERM:
another user's process, or an elevated one on Windows) counted as dead.

The fix keeps the lock file and its format (a bare PID) and changes how
it is taken:
- A free lock costs one create, as before. The lock file is created with
  its PID already in it: a private file hard-linked into place, which
  fails if the lock exists. Where hard links are missing it falls back
  to the exclusive create, and an empty lock younger than 10 s counts as
  held, since its creator is still writing its PID.
- A stale lock (dead PID, malformed, or empty and old) is replaced only
  by the process holding <lock>.break, itself a FileLock, after reading
  the lock again there, the way Mercurial breaks its locks. A break lock
  a crashed process left is broken the same way, one level up, at most
  three levels.
- EPERM from a signal-0 probe means the holder is alive.

Validation: __tests__/filelock-stale-takeover.test.ts runs two real
processes with a barrier: the slow one stops right after reading the
dead PID and resumes only once the other holds the lock. On main both
held it, 5 runs of 5; with the fix the slow one is refused and the lock
stays the holder's. The other cases cover the slow one getting in once
the lock is free, a live and a dead break lock, the empty-lock window,
EPERM (mocked, and for real on Windows with the System process), and no
leftover files.

Fixes #2979

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:40:44 +00:00
Colby MchenryandClaude Opus 5.5 bc9291afe6 fix(java): a call on a value a type holds in a static field reaches the method of the field's type (#3055)
A Java call written through a type's static field, Consts.HELPER.go()
or Holder.SHARED.run(), was resolved by the method's name alone: the
receiver Type.FIELD said nothing of its type, so the call went to a
same-named method of whatever class the name matcher found, and a field
of a library class (Intent.ACTION_VIEW.equals(...), System.out.print())
could reach a project method of that name. An interface's constants were
not indexed at all (neither engine read a constant_declaration), so a
constant a type declares in an interface, or inherits from one, had no
declaration to read.

A Java Type.FIELD.m() (Outer.Inner.FIELD.m(), a package written in
front) now finds the type as the calling file names it (javaTypeRef),
then the field, the type's own or one it inherits (five levels up), and
calls the method on the value the field's declaration gives it, read
with the field's own file's imports: an enum constant's own override,
else the enum's method; the method of the anonymous class the field is
initialized with (a CREATOR's createFromParcel); the class a new X(...)
initializer builds; else the type the declaration writes. A Kotlin
object's INSTANCE or Companion reaches the object's member. A library
owner, or a field of a library type, links nothing. A receiver whose
head is a local or a field links as before. Both engines index an
interface's constant_declaration as a static final constant.

Validation:
- java-static-field-receivers.test.ts: 9 tests, 7 fail on main 60b0d7a9
  (two already pass there: a field typed by its own declaration and an
  anonymous class's method), all pass here.
- Kernel parity (java), 0 files with diffs: Torture.java 1/1,
  TortureRecord.java 1/1, jextract 218/218, spring-modulith 682/682,
  SystemUI 1141/1141, okhttp 71/71, Minestom 1859/1894 (deferrals are
  main's).
- A/B against main 60b0d7a9, sites added/removed/retargeted (conf-only):
  Minestom +26003/-8/122 (751; 12,584 interface constants now indexed,
  409 calls typed), spring-ai +14/-21/786 (823; 719 enum constants' own
  overrides), kestra +27/-273/14 (37), playframework +5/-1/0 (21),
  SystemUI 0/-11/0 (2), spring-modulith +5/-3/4 (26), jextract
  +1/-9/6 (1); okhttp, nowinandroid, mihon, click, gin, serilog
  identical. The removed sites are guesses on library fields and
  methods (kestra's HttpStatus.X.getCode() x180).
- Cost (median of 3 interleaved): SystemUI CPU 92.0 -> 93.7 s, wall
  59.4 -> 58.6 s; Minestom CPU 46.7 -> 56.4 s, wall 17.5 -> 19.4 s, as
  its graph grows by the generated registries' 12,584 interface
  constants (29% more nodes, 30% more edges).

Reimplements #1949 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:46:25 +00:00
Colby MchenryandClaude Opus 5.5 7021de5c30 fix(rust): calls through a type a path or a use names reach that type's own function (#3025)
A Rust call through a type, `sim::items::ItemPool::for_def(…)` into another
crate of the workspace or `ItemPool::for_def(…)` under `use
sim::items::ItemPool;`, was matched by its qualified name alone, so a
namesake type's function in another crate or module could take it (a
workspace's `client` crate's `ItemPool::for_def`). The module walk stopped
at the type, a `use` of a `pub const` or `static` (`use sim::RANK_COUNT;`)
linked nothing, and a type rustfmt wrapped below its path (`h:
&scheduler::\n Handle`) linked by the file's `use` of its last name.

The fix, on the module walk:
- rustPathTarget reads `Type::member` calls: the type is the item the
  path's modules name, or the one the `use` that binds its name names
  (rustUseItemsAt), and rustTypeMember finds its member: an enum's variant;
  else the type's own (inherent) function or `const` in the type's crate,
  a trait impl's only when it is the one of that name; a trait's own
  declared method. A type alias, `Self`, a type the module declares itself
  and a type path in any other position are left as they were.
- Module items include `variable`s (a module-level `pub const`/`static`).
- matchRustUseBoundName reads a qualifier written on the line above a
  type (rust-scopes' blanked code, so a commented-out path is none) and
  resolves the whole path.
- Memoized per (type, member) and per path.
- Manifest paths stay inside the project: a workspace member that leaves
  it (`../sibling`, an absolute path) is neither read nor listed
  (escapesRoot, in expandMembers), and rustCrateGraph drops a
  `[lib]`/`[[bin]]` path that leaves it, as it already did a dependency's.

Validation:
- rust-cross-crate.test.ts (PR 2631's 9 cases, credited),
  rust-type-member-paths.test.ts and rust-manifest-paths.test.ts: on main
  10 of 15 fail; all pass. Every rust-* suite passes. On main the workspace
  map read `../outside/Cargo.toml`, `../vendor/x/Cargo.toml` and
  `/abs/crate/Cargo.toml` and listed `../vendor`; now only the project's.
- A/B against main, native kernel, 7 Rust repos (every site the same
  as against PR #3014's own commit; re-checked on 60b0d7a9): tokio +205
  links / 139 retargeted / 230 chains gaining their head call, ripgrep
  +155 / 13 / 1, axum +25, clap +179 / 0 / 295, serde +28, dioxus +142 /
  6 / 31, gleam +114 / 47 / 0; 0 removed. Retargets go to the type the
  code imports or its own crate's (tokio's Semaphore::new and loom mocks
  to tokio::sync, ripgrep's Error::from, gleam's Printer::new); express
  and flask byte-identical.
- Cost, native kernel, medians of 3 interleaved runs (dioxus of 7):
  tokio CPU 32.1 -> 29.7 s, peak RSS 1,000 -> 1,010 MB; gleam 29.1 ->
  28.1 s, 1,010 -> 1,004 MB; dioxus 38.7 -> 40.1 s, 1,111 -> 1,115 MB
  (timed on one thread, rustPathTarget 1.24 -> 1.32 s over 17,198 calls).

Builds on PR #2631 by @BGamboa13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:28:10 +00:00
Colby MchenryandClaude Opus 5.5 cdfa31be54 fix(ruby): a call on an instance variable that initialize types links to its class's method (#3053)
Both extractors record a Ruby call on an instance variable as
`@writer.flush`, and no strategy read the variable's type, so every such
call stayed unresolved: 2,780 in activesupport and 3,239 in actionpack on
main, for example.

ruby-constant-paths now resolves `@name.m` written in an instance method of
a class (not a class method, not a module) by the class's `initialize`.
When every assignment to `@name` there starts a statement and is a lone
`Const.new(...)`, `A::B.new` or `::Foo.new ...` of one constant
(arguments with or without parentheses, a block or a statement modifier
may follow), the class is the one Ruby's constant lookup finds for it from
`initialize`, and the method is that class's own instance method or the
first its ancestry declares. Anything else leaves the variable untyped for
the other strategies, as before: a conditional, two classes, `.tap`,
`writer || Foo.new`, `@a, @b = ...`, a chain continued on the next line. A
class the project doesn't declare (`Logger.new($stdout)`) links nothing.

Validation, kernel engine, main 3a4cd3bf vs this change:
- jekyll 97c3bdc +8, activesupport +19, actionpack +14, activerecord +30
  (rails 793d744 subtrees), sinatra cb22afd 0; nothing removed. 68 of the
  71 are right; 3 in activerecord follow `@pool = NullPool.new` from
  `initialize`, which `pool=` later replaces with the real pool.
- Controls requests (Python) and cobra (Go): dump-graph byte-identical.
- New tests fail with main's resolver (the positive case, in both engines)
  and pass with the fix; the untyped and outside-class cases pass on both.
- Index time, median of 3 interleaved: activesupport 4.72 vs 4.79 s,
  activerecord 11.54 vs 11.54 s.

The gap was found in the triage of PR #684.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:37:36 +00:00
Colby MchenryandClaude Opus 5.5 6d3804cf57 fix(resolution): C, C++ and Objective-C rank as one language, and a lone candidate is judged by family (#3052)
Name matching compared a candidate's raw language tag with the
reference's: the single-candidate confidence in matchByExactName and
matchFuzzy, and findBestMatch's same-language skip and +50/-80 score.
So a call whose name had several definitions in another of C, C++ and
Objective-C scored -80 for each and linked to nothing (an Objective-C
file's _glfwPlatformSetTls(), a C++ call into Dear ImGui's imgui.h, which
reads as C), and the one definition of a name across TSX and TS, or C and
C++, was a 0.5 cross-language guess.

The single-candidate confidence now compares language families
(sameLanguageFamily). Ranking treats C, C++ and Objective-C as one
language (sameRankingLanguage), the very language worth a point that only
breaks a tie; other families keep the language itself, because ranking
them by family moved excalidraw's Store::scheduleCapture calls to the
App.tsx method beside them and gave okhttp 83 wrong Java-to-Kotlin
Builder picks.

Validation:
- __tests__/name-match-language-family.test.ts, 8 tests (default and
  wasm): 6 fail on the base main; the pin that a .ts call keeps its .ts
  target over a .tsx namesake beside it passes on both; all pass with
  the fix. 24 related test files (cross-language, React Native, Expo,
  Fabric, frameworks, mixed TS/JSX, Objective-C, resolution): 746 pass.
- A/B, main vs fix: glfw +4 calls from Objective-C into C, imgui +93
  from C++ into imgui.h (read as C) and imstb_rectpack.h, all to the
  definitions meant; excalidraw, okhttp, leveldb and jq gain or lose no
  edge (43, 9, 97 and 30 unique-name links go from 0.5 to 0.9 with the
  same targets); flask and gin byte-identical.
- Index time, median of 3 interleaved: imgui 7.9 s main, 7.7 s fix;
  excalidraw 9.2 s main, 9.4 s fix.

Reimplements PR #1662 by ferrine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:15:23 +00:00
Colby MchenryandClaude Opus 5.5 19b74d10f3 fix(deps): read .gitignore rules the way git does, with ignore 7.0.14 (#3050)
codegraph applies .gitignore rules itself, through the ignore package:
on the filesystem walk for every .gitignore, and in git repositories on
top of git ls-files for its built-in defaults merged with the root
.gitignore and git's exclude files. codegraph.json's include, exclude
and includeIgnored patterns go through it too. ignore 7.0.5 read several
forms differently from git: a backslash escape (\d was a regex digit),
POSIX classes and a ] or ! first in a bracket, and a run of three
asterisks. A lone ! line matched every path, so it re-included
everything the rules and the built-in defaults leave out, node_modules
included. 2,031 of 200,000 random patterns also threw at match time,
which readGitignorePatterns has to guard against (issue 682).

ignore 7.0.14, with the range raised to ^7.0.14, reads all of these as
git does, and none of the 200,000 patterns throws. Its code diff from
7.0.5 was read in full: pattern translation only, with no I/O, no
dependencies and no install scripts. The lockfile entry matches the npm
registry's own metadata on registry.npmjs.org. The package has no
provenance, like every release its maintainer has published.

Validation:
- New __tests__/gitignore-pattern-parity.test.ts checks scanDirectory
  against git's own answer for these patterns, on the filesystem walk
  and in a git repository: its 4 tests fail with 7.0.5 and pass with
  7.0.14.
- scanDirectory on react, astro and django, in git mode and in walk
  mode on a copy without .git, returns the same 4,631, 4,946 and 3,027
  files with either version.
- On a fixture of the changed forms, 7.0.14 leaves out exactly what git
  leaves out in both modes, node_modules aside, which codegraph's
  defaults always exclude. With a lone ! line, 7.0.5 kept 16 of the 18
  files on the walk, node_modules among them, where git keeps 6. Without
  that line it differed from git on 8 files on the walk and on 2 in git
  mode.
- The gitignore and scope test files (extraction,
  git-ignored-dirs-scope, the include, exclude and includeIgnored
  configs, src-build-folders and others): 853 passed.

Raised by PR 2885 (@johnhldiver-beep).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:49:46 +00:00
Colby MchenryandClaude Opus 5.5 2514467801 fix(jvm): a dotted supertype names its nested type, and a Kotlin object expression is an anonymous class (#3051)
A supertype written through its enclosing type was lost in three ways.
Java and C# kept the dotted path (class Named extends Outer.Inner,
class N : Outer.IListener), but the qualified-name match compared it
verbatim with p::Outer::Inner, so it stayed unresolved; a Java anonymous
class (new Api.Listener() {...}) kept only the last segment, which no
nested type is found by; and Kotlin (both engines) took the FIRST
segment, so class K : Outer.Inner() extended Outer. A Kotlin object
expression, object : Listener {...}, made no class at all: its overrides
became local functions of the code around it, which a call through the
interface never reached, and bare calls elsewhere could be guessed onto
them.

A dotted Java, Kotlin or C# supertype now resolves to the type whose
qualified name ends with the path, when the file can name the path's
first type (its own file, package or namespace, an import; C#'s usings);
of several, the one closest in the tree, or none on a tie. The Java
anonymous class keeps the written path in its extends reference, and
Kotlin joins every segment of a supertype (type arguments left out), in
the wasm extractor and the kernel alike. A Kotlin object expression is
indexed as Java's anonymous class is: a class <Listener$anon@line> that
extends each supertype as written, which the code around it
instantiates (the class it builds, else its first supertype, also as
written and only ever a type that can be a supertype), with its body's
declarations as members; its header (constructor arguments, a
by-delegate) stays the enclosing code's. An anonymous class's members,
Java's and Kotlin's, are named only inside the declaration holding the
class: a bare block() elsewhere is never an object's override val block.

The walk up a Java or Kotlin class's supertypes for an inherited method
asks each supertype by the chain of types that names it
(AssistantMessage::Builder) before its simple name, so a builder's
build() is its own AbstractBuilder's and not an MCP annotation's;
the name still answers when an extends edge reached a namesake
(Minestom's Player extends LivingEntity).

The code in an object expression still sees the scope it is written in,
as when its overrides were local functions: a property of the class
around the object types a receiver there (okhttp's server.url() in an
object : Callback is MockWebServer's), an override is not what a call
of its own name in it means (original!!.contentLength(),
this@Outer.onScroll(), an extension receiver's onDataChanged()), and an
object's members out of reach leave a by-name guess as it was
(okhttp's it.isReady() is still the SerialTask interface's).

Validation:
- jvm-dotted-supertypes.test.ts: 13 tests, 11 fail on main 60b0d7a9
  (the other two guard a dotted supertype the file cannot name and the
  calls inside an anonymous class), all pass here.
- Kernel parity, 0 files with diffs: torture.kt 1/1, Torture.java 1/1,
  okhttp 588/619 Kotlin and 71/71 Java, mihon 891/940, nowinandroid
  340/350, SystemUI 188/198 Kotlin and 1141/1141 Java, jextract 218/218,
  Minestom 1859/1894 (deferrals are main's).
- A/B against main 60b0d7a9, sites added/removed/retargeted: okhttp
  +640/-53/149, SystemUI +435/-53/116, Minestom +165/-3/6, kestra
  +108/-0/6, spring-ai +46/-0/38, playframework +27/-3/13, mihon
  +22/-5/6, jextract +22/-0/1, spring-modulith +9/-0/10, nowinandroid
  0/0/0; serilog, click, gin identical.
- Cost (median of 3 interleaved): okhttp CPU 27.8 -> 29.0 s, wall
  8.6 -> 8.8 s (its 360 object classes and 1,491 more edges); mihon
  23.6 -> 23.8 s, wall 7.8 -> 7.7 s.

Reimplements #1927 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:29:52 +00:00
Colby MchenryandClaude Opus 5.5 1941caf7bc fix(add-lang): the extraction check finds codegraph's npm shim on Windows, and takes CODEGRAPH_BIN (#3047)
scripts/add-lang/verify-extraction.mjs ran execFileSync('codegraph', ...).
On Windows, npm installs codegraph as a .cmd shim (so do npm link and
local-install.sh), and Node neither finds a .cmd by bare name nor runs
one without a shell. The check always died with "spawnSync codegraph
ENOENT" (exit 2), and /add-lang's bench.sh read that as broken
extraction and skipped its A/B.

The script now asks, in order:
- CODEGRAPH_BIN, when set. A .js, .mjs or .cjs path runs with the
  current node; anything else runs directly (a .cmd or .bat through
  cmd.exe).
- On Windows, the codegraph Windows itself would pick: the first PATH
  folder holding codegraph under one of PATHEXT's runnable extensions.
  A .cmd or .bat runs through cmd.exe (shell: true) as one command
  string. Each token is quoted for the C runtime and caret-escaped for
  cmd, so spaces, &, parentheses, a %NAME% and a trailing backslash in
  the repo path arrive intact. An .exe or .com needs no shell.
- Otherwise codegraph by bare name, exactly as before. On macOS and
  Linux without CODEGRAPH_BIN, the call and the output are unchanged.

bench.sh takes CODEGRAPH_BIN too. It makes a path absolute (indexing
runs from inside the repo), exports it so verify-extraction.mjs reads
the index with the same build, and runs its own --version and init calls
with it. Without it, bench.sh behaves exactly as before. The A/B's MCP
server stays run-all.sh's own CG_BIN.

Validation (Windows 11, Node 22.22.0):
- New __tests__/add-lang-verify-extraction.test.ts covers CODEGRAPH_BIN
  with a .js stand-in (all platforms), a .cmd shim on PATH with an
  awkward repo path (Windows only), and the bare-name call (macOS and
  Linux only). With main's script both tests that run here fail ("could
  not read codegraph status"); with the fix, 2 pass and 1 is skipped.
- The fixture from PR 1246's review (a codegraph.cmd on PATH): main
  exits 2 with ENOENT; the fix reads the status.
- 13 scenarios with npm's own generated shims, and a real index in a
  folder named "lua repo & co (x86) 100%": main fails every one that
  needs the shim or CODEGRAPH_BIN, and the fix passes them with the
  exact argv reaching codegraph. With no codegraph anywhere, both exit 2
  the same way.
- bench.sh with the A/B stubbed: with npm link's shims on PATH, main's
  copy skips the A/B (verify exit 2) and the fix reaches it (PASS).
  CODEGRAPH_BIN as an absolute or a relative path reaches it too, and a
  missing one stops with a clear message.
- With process.platform reported as linux or darwin, main's script and
  the new one make the same execFileSync call and print the same output.

Raised by PR 1246 (@bslatner).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 07:27:10 +00:00
Colby MchenryandClaude Opus 5.5 4e7a221089 chore(deps): take the security fixes for devalue and postcss, drop an unused types package (#3044)
devalue 5.9.1 (root, through svelte) and 5.8.1 (site/, through astro)
fall in seven advisories published 2026-10-01 and fixed in 5.9.3, among
them GHSA-j22f-vq7h-c4qm (CVE-2026-92708, high). postcss 8.5.6 (root,
through vite) and 8.5.15 (site/) fall in GHSA-r28c-9q8g-f849
(CVE-2026-73646, high, fixed in 8.5.18) and GHSA-fxqj-rqcc-2cmp (fixed
in 8.5.23); the root copy also in GHSA-6g55-p6wh-862q and
GHSA-qx2v-qp2m-jg93. All of these are build-time dependencies.

Lockfile-only updates inside the existing ranges: npm update devalue and
npm update postcss, in the root and in site/. devalue goes to 5.9.4 and
postcss to 8.5.32, whose own ranges move nanoid to 3.3.20 and
source-map-js to 1.2.2, which clears their advisories as well
(GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8, GHSA-68fv-2mgg-jv7q). The
@types/better-sqlite3 devDependency goes: nothing has imported
better-sqlite3 since the switch to node:sqlite. In site/, npm 10 also
drops five "peer": true markers that the npm which wrote that lockfile
had added; they are metadata only, and npm ci installs the same tree.

Validation:
- npm audit: root 14 advisories before, 10 after; site/ 13 before, 9
  after. devalue, postcss, nanoid and source-map-js are gone from both.
- npm run build on main's lockfile and on this one: all 1,264 files of
  dist/, the viewer included, are byte-identical. The site builds its
  21 pages, and its 101-file dist/ is byte-identical too.
- The release bundle's install (npm ci --omit=dev --ignore-scripts on
  the staged lockfile) installs the same 12 packages, file for file.
- All 8 changed resolved/integrity entries match the npm registry's own
  metadata and sit on registry.npmjs.org. No git, URL or tarball
  sources, and no new install scripts. devalue, postcss and nanoid carry
  provenance from their own repositories' release workflows.

Raised by PR 2885 (@johnhldiver-beep) and PR 1477 (@anupamme).

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


[skip actions]
2026-10-30 07:13:13 +00:00
Colby MchenryandClaude Opus 5.5 3a4cd3bf65 fix(ruby): calls to !/? methods and calls on what .new made link their method (#3049)
Two kinds of Ruby call linked nothing. A call to a method whose name ends
in `!` or `?` on a receiver (`writer.flush!`, `writer.ready?` after
`writer = CacheWriter.new`, `CacheWriter.create!(1)`) failed the method-call
pattern, whose method part took no `!` or `?`. A method called straight on
a new object (`CacheWriter.new(key).write_cache`, `Billing::Ledger.new.charge!`)
was recorded by both extractors with the receiver's raw text, arguments and
blocks included, which no strategy matched.

Both extractors now record a call on `Const.new(...)`, `A::B.new` or
`::Foo.new` as `new Const().m`, the constructed-receiver shape TS/JS, Java
and C# already use; the wasm extractor and the kernel's ruby.rs emit the
same refs. ruby-constant-paths resolves that shape before the name
pre-filter: the class by Ruby's constant lookup from the namespaces around
the call, the method as the class's own instance method or the first its
ancestry declares, and nothing when either is missing, so rack-protection's
`Request.new(env)` (rack's Rack::Request) links nothing. The Ruby dot pattern
in matchMethodCall takes a trailing `!` or `?`; the receiver-word scoring
among several project methods of the name stays off for such names on a
value receiver, where it was wrong on 55 of 74 new edges (`options.key?`
on a Hash -> InheritableOptions#key?). rubyAncestry no longer reads a
module a call returns (`include RouteSet.new.tap {...}.url_helpers`) as a
mixin of the constant the call starts at.

Validation, kernel engine, main 37052b18 vs this change:
- sinatra cb22afd +12, jekyll 97c3bdc +53, activesupport +398,
  actionpack +97 (2 removed: `ActionController::Base.with` had reached
  RoutesHelpers.with through a misread mixin), activerecord +1012
  (rails 793d744 subtrees). About 21 of 1,572 added edges are wrong, all
  from the class-name and single-method guesses that plain names get too.
- Controls requests (Python) and cobra (Go): dump-graph byte-identical.
- kernel-parity.mjs: fixtures 10/10, sinatra 147/147, jekyll + rails
  1044/1044 files byte-parity, none deferred.
- New tests: 10 fail with main's kernel and TS, all pass with the fix, in
  both engines. Dropping the scoring gate or the mixin fix fails its guard
  test.
- Index time, median of 3 interleaved: activesupport 5.35 vs 4.63 s,
  jekyll 2.66 vs 2.73 s (noise).

Reimplements PR #684 by @ricobl, who found both gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:47:47 +00:00
Colby MchenryandClaude Opus 5.5 b700a3eec2 fix(python): a parameter or local named like an imported module is not that module (#3046)
A Python call through a name the file imports as a module, like
other.update(...) after `from pkg import other`, linked to the module's
function whatever the calling function bound `other` to: a parameter, a
local, a with/except/loop target, a walrus, a case capture, a lambda or
comprehension variable. resolveViaImport read the file's import mappings
alone (its only scope check, pythonFunctionModuleAlias from PR #2600,
covers a function-local `import a.b as X`), and isPythonProjectModule,
which lets a builtin-named call like update/get/write past the builtin
filter for a project module, asked the same file-wide question. The
mappings also list from-imports at any depth ahead of plain imports, so a
function-local `from pkg import third as other` became every function's
`other`.

pythonReceiverBinding reads how the code around a reference binds the
name: the reference's own statement (lambda parameters, comprehension
targets), then each enclosing function, innermost first, through its
parameters and own statements (assignment, annotation, for, as, walrus,
del, nested def/class, case captures, imports; global keeps the module's
binding), memoized per function. pythonImportsInScope gives
resolveViaImport's Python branch and isPythonProjectModule only the
receiver's mappings that apply there.

Validation:
- __tests__/python-receiver-rebinding.test.ts, 80 tests (default and
  wasm, LF and CRLF): 48 fail on the base main (ten rebinding shapes,
  the local from-import, the cross-function leak), 32 pass (look-alikes:
  keyword argument, string, comment, comprehension over the module, lambda
  beside the call, global, keyword pattern, same-module local import);
  all pass with the fix. The 26 Python-related test files pass.
- A/B, main vs fix: django's own package (930 files, kernel path) changes
  2 edges' confidence (0.85 to 0.8, same targets: a closure over a
  parameter `apps` and a local `apps = model._meta.apps`); flask drops
  one cli.group() call and lowers 4 to 0.7 with the same targets (a
  nested `def cli` and a pytest fixture parameter `app` named like
  imports); requests, click, mealie and fastapi (package plus tests), and
  the express and gin controls are byte-identical.
- django package index time, median of 3 interleaved: 11.3 s main,
  10.7 s fix.

Reimplements PR #2632 by BGamboa13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:22:47 +00:00
Colby MchenryandClaude Opus 5.5 407ec4fba2 chore: keep the kernel's vendored grammars out of GitHub's language stats (#3045)
GitHub reports the repository as 79% C and 18% TypeScript. Nearly all of
that C is the third-party tree-sitter grammars the native kernel compiles
from source (codegraph-kernel/grammars/, 29 files, about 78 MB).

A new .gitattributes marks every file under codegraph-kernel/grammars/
linguist-vendored, so Linguist leaves them out of the language
statistics, and marks the five generated parser.c files
linguist-generated, so pull requests show those collapsed. The
hand-written scanner.c files stay expanded in diffs on purpose: that is
where C gets patched or added (PR 2173, and the open PR 2866), and every
contributor PR is security-reviewed, so such a change must stay visible.
The patterns use /** because an attributes pattern that names a
directory does not reach the files inside it.

Validation: git check-attr over all 1,722 tracked files reports
linguist-vendored set on exactly the 29 files under
codegraph-kernel/grammars/ and linguist-generated set on exactly the
five parser.c files; scanner.c, the headers and the LICENSE files are
vendored only, and everything else, src/index.ts included, has neither.
In a scratch repository the bare directory pattern leaves parser.c
unspecified. Git does not act on linguist attributes and codegraph never
reads .gitattributes, so builds and tests are unaffected.

Raised by PR 2513 (@build8421) and PR 1668 (@kidonng).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 06:14:08 +00:00
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