Conversation
The four name binders (resolveCallEdges, buildCallerIndex, buildSymbolGraph,
resolveRelations) filtered every same-name definition per call site and
built a `${from}|${to}` string per candidate to test import corroboration.
That is quadratic in homonyms: the TypeScript repo's test fixtures declare
`C` 5,335 times, so 236k call sites cost 6.8M candidate checks.
Definitions are now grouped by (name, language family) with a per-file
index, the import pairs are regrouped into per-file target sets, and the
corroborated candidates are an intersection walked from the smaller side.
The call binders also resolve each name once per file, and the proximity
score walks characters instead of splitting both paths per candidate.
Output is byte-identical (callers, recall callers, relations, call edges,
hierarchy and symbol graph compared on flask, gin and microsoft/TypeScript).
On the TypeScript repo: resolveRelations 1.35 s -> 0.13 s, buildCallerIndex
1.65 s -> 0.5 s, resolveCallEdges 0.7 s -> 0.27 s, buildTypeHierarchy
1.7 s -> 0.57 s, buildSymbolGraph 4.5 s -> 2.3 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Python: only the dirs holding top-level packages (plus the repo root and
pyproject/setup dirs and their src/) are import roots now. Treating every
package dir as a root bound stdlib `import typing` to src/flask/typing.py,
sent `import flask` to a 4-line test fixture and never reached src-layout
packages; on django it also made resolution ~40x slower. Absolute imports
try the importer's enclosing roots first and are memoized per directory.
TypeScript/JS: a baseUrl declared without paths now resolves bare names
(tried after paths, only a hit counts); paths follow tsc precedence (exact
alias, then longest prefix) instead of object order; `extends` can load a
config from an in-repo workspace package; `${configDir}` is substituted with
the extending config's dir; package.json `imports` (#subpath) resolve in the
nearest package.json; bundler `?query` suffixes are stripped before probing.
The asset-extension short-circuit is JS-family only, so Python `.map`, Java
`Map` and C# `*.Svg` resolve. A Go package's representative file is its first
non-test file. Markdown links starting with "/" resolve from the repo root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
RawRef gains an optional `soft: true` for speculative refs — Python `from pkg import name`, where `name` may be a submodule or just an attribute of pkg. buildGraph resolves a file's soft refs after its firm ones and turns one into an import edge only when it resolves to another in-repo file the file does not already link to: a miss is dropped silently (no external, no dangling edge) and a duplicate adds no weight. Records without soft refs produce byte-identical graphs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
defs and refs were plain objects, so `defs["__proto__"] = [...]` hit the prototype setter and every symbol of that name vanished from the artifact. Both maps are now prototype-less; every other name serializes byte-identically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…detection gaps Workspace detection missed valid layouts: pnpm flow-style `packages: [...]` lists, multi-line Gradle `include(...)` (and `projects.x` type-safe accessor edges), nested Maven aggregators, and multi-module Go repos without a go.work. All four now resolve; nested go.mod discovery skips vendor/testdata/fixtures. Manifests are read as JSONC like the resolver reads them, and malformed ones reach the `workspaces` JSON as `warnings` (only when non-empty, so clean output is byte-identical). `workspaces --check` (MCP `check: true`) compares each package's declared sibling dependencies with the link-graph's resolved imports: `undeclared` imports exit 1 as a CI gate, `unusedDeclared` is informational. `codeindex resolution` / MCP `resolution_report` aggregates the resolver pass per language (resolved / external / dangling by reason / unsupported, top dangling specs, top external packages, notes) with the config warnings the resolver collected but no surface ever read; `index` now prints those warnings to stderr as well. Mermaid node ids are injective (a colliding readable id takes `_2`, `_3`…; unique ones are unchanged), and a focus target resolves as a slug, a module directory or a file, failing on anything else instead of printing an empty diagram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The persisted index (.codeindex/cache.json + artifacts) was reused, or ignored, in ways its freshness keys never saw: - An absolute --index was joined under the repo (path.join does not reset on an absolute segment), so every read command silently rebuilt cold next to a fresh index (TS repo search: 55s -> 15s). - Read commands scanned an in-repo custom --index dir, so search returned idx/graph.json and the scan never matched the index to reuse it. The read path now excludes the index dir exactly as `index` excludes --out. - Records were reused whatever tier or call cap produced them: after --no-ast, a grammar pull, or another --max-calls, `index` reported "unchanged" and reads served the old records until each file changed. cache.json now records an extraction profile (AST grammars, max-calls); mismatching code entries are dropped per grammar key and re-extracted. - The npm layout ships only core wasms next to the bundle, and the pulled cache was never searched behind them, so extended grammars (Kotlin, Elixir, Zig, ...) never loaded. Lower tiers are now per-key fallbacks; `grammars status` adds extendedPullNeeded. - `index` ignored --no-index-cache and had no --full-hash, the escape hatches for a same-size edit under a restored mtime. Both work now. - `index --out .` (or an ancestor of --repo) excluded every file and wrote a 0-file graph with exit 0; only the artifacts are excluded there now, and an empty index warns. - --ignore-dir re-exposed .codeindex, which is now structural like .git. - graph.json's commit came from `rev-parse --short`, whose length follows the object count, so identical trees rendered different bytes; it is now a fixed 7-char prefix of HEAD. - A binary changing size under an equal (decoded-text) hash kept a stale record size, so cache.json was rewritten on every run. - Artifacts were truncated and rewritten in place; they are now written to a temp file and renamed, so readers never see a torn file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Eight shapes the audit measured on real repositories, fixed in the walk's
tables and readers:
- Doc comments now reach an item across Rust outer attributes and a
TypeScript decorator on its own line (memchr: 457 of 1187 symbols had
lost theirs; anyhow 14 of 14).
- Python definitions under if/elif/else, try/except/finally, with and
match are indexed (flask's `if TYPE_CHECKING:` classes); assignments
under an `if __name__ == "__main__"` guard stay out.
- Visibility is read from the header text before the declared name, so a
parameter named `private`/`internal` or a default string no longer flips
it, and one `private val` constructor parameter no longer hides a Scala
or Kotlin class. Modified `val`/`var` constructor parameters are now
indexed too.
- Go type aliases (`type B = int`) are indexed; the grammar-vocabulary
oracle now matches alias, #define and singleton-class node types.
- C/C++: members of `typedef struct {…} T` (parented to T) plus the struct
tag, `#define` macros (include guards excluded), function-pointer fields.
- C++: out-of-line definitions belong to their class (leveldb: 243 of 993
functions), operators/conversions/destructors are named, and
reference-returning functions are no longer named after their return
type. A `.h` whose content is C++ is parsed with the C++ grammar
(leveldb headers: 142 -> 963+ symbols); its symbols keep lang "c".
- C# operators are named by their token and indexers `this[]`.
- Generic bases resolve to the base, not the type argument (C#, C++).
Quality fixtures gain every shape (all scored 100%); Go's visibility
accuracy moves 0.9444 -> 0.9524 only because its denominator grew.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The breadth-first walk kept only the first edge that reached each node, and out-edges were visited first. On gin, `neighbors render` listed `root` and `binding` only as outgoing inferred calls (weights 30 and 1) and hid the real incoming imports (12 and 6) behind them. Every edge between the frontier and a node first reached at a depth is now a link, one per (node, direction, kind). A node's links are listed together, in the order the node was reached, with stated evidence (import, inheritance, corroborated call) before name-based edges and an inferred call last, so a consumer reading one link per node still gets the one that matters. EDGE_KINDS lists the kinds `--kind` may name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Every navigation query now reads a symbol the same way (src/symref.ts):
`name`, `name@file`, `file#name`, `file#Parent/name` (a callgraph id) and
`Parent/name`. Before, `hierarchy`/`implementations` only took the key
their map stored, so the FIRST homonym's `name@file` said "no type
named"; `callgraph` rejected `name@file`; find_references read the bare
key only, dropping the call sites of every other homonym. A bare name in
find_references now covers all homonyms and tags each call site with the
declaring file it binds to. implementationsOf also stopped walking at a
subtype that is itself a second homonym.
callers: an unknown symbol is an error (exit 2, MCP isError, as for
hierarchy/implementations/call_graph, which also stop returning `{error}`
as a successful result). A known symbol no site binds to answers with its
defs and the call sites that name it but bound nowhere (flask's
register_blueprint: 74, lost to a proximity tie), plus `callers --raw`
(MCP raw:true) to list every site before binding. A one-shot
`callers <name>` binds only that name's call sites through the same code
path as the full index (TypeScript repo: 8.8 s -> 6.1 s end to end).
CLI: `--limit` now caps complexity, risk and deadcode (deadcode with the
MCP {total, shown, truncated, candidates} envelope); `--kind` rejects
unknown edge kinds; callgraph reports `depthClamped` past 5 hops; file
arguments accept ./path, absolute and backslashed spellings, and
`complexity` exits 2 on a file the index does not hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Every walked file ran every rule in scope as a regex over its whole path, and the floating form `^base/(?:[^/]+/)*body$` backtracks over each directory level. On typescript-go (117 root rules, 66k files) that was over half the walk. parseGitignore now attaches a matcher with the same verdict: a basename compare for literals, a suffix test for `*<literal>`, the body regex on the basename for other floating patterns, and a literal-prefix precheck in front of anchored regexes. isIgnored scans the chain from its end and stops at the first match (last match wins). `scan --no-ast` on typescript-go: 2.3s -> 1.2s. Floating patterns may be tested on the basename only because no body can match `/`. That also exposed a real bug: `[!x]` compiled to `[^x]` and crossed the separator, so `a[!x]b` ignored `a/b`, which git keeps (checked with `git check-ignore --no-index`). Bracket expressions now exclude `/`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
scanRepoParallel queues every code file whose (size, mtime) misses the cache, and the worker always ran buildCodeRecord on it. After a touch, a branch round-trip or a CI cache restore the content is usually identical, so scanRepo took the hash-hit branch and threw the record away. On a 4.4k-file Go repo after touching every file, `index --workers 3` took 2.2s against 0.6s sequential. Read commands paid the same on every run because they never rewrite cache.json. Each job now carries the cached hash. The worker hashes first and, on a match, posts only (size, mtime, hash). scanRepo reads the hash from the entry and reuses the cached record, so the output is unchanged. The same touch case now takes 0.75s; the difference left is spawning the workers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…t classes
`module.exports = function middleware() {}` read as an assignment to a
property called `exports`, so every Express middleware, webpack loader and
ESLint rule became a private function named "exports"; `= class Foo {}`
lost its members too. It is now the module's default export: named after
the value's own name, else the file stem, exported, with its class or
function body walked as that symbol's.
An anonymous `export default class { … }` (React class components) had the
same dead end: a class EXPRESSION is neither a def nor a container, so its
members were never visited. Its body, and an anonymous default function's,
is now walked with the stem as parent, and a class expression bound this
way states its `extends`. The body of a class or function assigned to
`exports.x =` / `Foo.prototype.x =` is walked the same way.
JavaScript classes had no superclass relation at all: its grammar writes
`class_heritage` without clause nodes, and only `extends_clause` was read.
On 14 npm packages (365 files): 14 "exports" symbols renamed and exported,
22 nested declarations and 92 JS `extends` relations gained.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
RawRef gains an optional `soft: true` for speculative refs, which the Python extractor emits for `from pkg import name` (name may be a submodule or just an attribute of pkg). buildGraph resolves a file's soft refs after its firm ones and turns one into an import edge only when it resolves to another in-repo file the file does not already link to: a miss is dropped silently (no external, no dangling edge) and a duplicate adds no weight. These three files are byte-identical to the resolve work package's commit cbf64ac (the cross-package contract), so this branch builds correct graphs on its own and the two merge without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A guarded head — `def fetch(id) when is_integer(id)`, and every `defguard`, which always has one — wraps the declared call in a `when` binary operator, so the name reader found no call and dropped the clause, while the head itself registered as a call site. Idiomatic pattern-matched Elixir lost whole function families this way. The name reader and the declaration-head call filter now read through the guard. Also: `defguardp` is private like `defp`/`defmacrop`, and an operator definition (`def a <~> b`) is named by its operator instead of its left parameter. The regex tier (Elixir's default until `grammars pull`) gains `defguard`/`defguardp` and stops marking `defmacrop` exported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Java resolution probed every source root with a path join and normalize for each import, then again for every peeled segment. A Maven repo with 200 roots spent ~0.5ms per spec, most of it on stdlib names that end up external: 36,000 specs took 17s. The roots are now folded once into an FQN -> file map and a package -> first-file map (the shortest root still wins), so a lookup is a Map read: 45ms including the index build. `using System;` scanned every C# namespace with startsWith. Each dotted prefix now maps to the byStr-first file under it: 4,000 files x 10 usings went from 4.2s to 26ms. Resolutions are unchanged (checked on a randomized differential of 30k specs against the previous resolver). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Docs were exempt from the (size, mtime) fastpath only because the mention pass needs their text. Every warm `index` and every read command against a fresh index therefore read and hashed all of them, even when the artifacts were reused and the text was never used. On typescript-go that is 7.5k docs, 21.7MB kept in memory and about 250ms per run. A stat-matched doc now reuses its record like any other file. RepoScan's docText is a Map subclass that loads a deferred doc on first lookup, and loads all of them before any enumeration. Consumers see the same map, and a warm scan renders byte-identical artifacts to a cold one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The import scan ran regexes over raw text, so example code in a JSDoc block,
a `// import` comment, a code generator's template literal and a flask
docstring's `from flask import Flask` all became import edges (four src ->
tests fixture edges in flask, nine dangling ones in the TypeScript repo's
AST generator). It now lives in extract/imports.ts and runs over a masked
copy of the file: comments, string bodies, template text and regex bodies
blanked with offsets kept. JSDoc `import("./x")` types and `@import` tags
are real type dependencies and survive the mask.
The JS/TS `import ... from` clause is matched structurally instead of with
a lazy scan to the next quote, which ran to the end of a quote-free file:
a 916 KB `export function` table took 7-11 s and now takes 0.8 s end to end.
Dynamic `import("x", { with: ... })` is recognised.
Python `from X import a, b as c` keeps its module ref and adds one soft ref
per name (`.` + `cli` -> `.cli`, `flask` + `json` -> `flask.json`), so
`from . import cli` links cli.py rather than only the package __init__.
PHP group and comma `use` lists and `__DIR__`/`dirname()`-anchored includes
are captured; a trait `use` inside a class body is not an import.
extractAst's own import readers disagreed with the index on every language
(Python read the imported names, Go only the first spec of a group, Java
kept `;` and `static`); it now returns the same refs and pkg as extractCode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A ref's `soft` flag changes how the graph treats it, so a corrupt value must invalidate the cache like any other malformed record field instead of being read as truthy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
`function M.go()` was indexed as one symbol named "M.go", while its call sites record `u.go()` as "go" with receiver "u": the two never met, so no Lua module function had a caller and deadcode flagged every one. A table function (`M.go`, `M:start`, `M.sub.deep`, `M.alias = function() end`) is now named by its last segment and parented to the table path — how Go receivers, Rust impls and C++ out-of-line definitions are modelled. The regex tier splits the same way; the signature keeps the written form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The post-walk pass marked EVERY symbol whose name an export list,
`export default X` or `module.exports = { … }` mentioned, so `export { save,
helper }` also published `class Store { private save() }` and a helper
local to another function. Exported definitions feed call resolution's
candidate set and deadcode, so private members became call targets. The
names are now keyed by the scope the list is written in (module scope, or
an ambient `module "m" { … }` body) and match only that scope's symbols.
`export { a } from "./other"` re-exports another module's binding; it no
longer marks a same-named local of this file exported (the regex tier
already made that distinction).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A nested member was always emitted as `Parent#name` with an enclosing_symbol pointing at `Parent#`, whatever the parent was: a function's members hung off a type that does not exist, a class nested in a function lost its chain, and a Go method declared in a sibling file got a phantom type in its own file (691 dangling owners on flask, 28,752 on microsoft/TypeScript). Each symbol is now built from its parent's final symbol string: the innermost same-named declaration whose span contains it, else a container of that name in the file (Rust impl, Go receiver), else, for Go, the type declared in another file of the same package. enclosing_symbol is no longer written: the proto reserves it for local symbols. One table now maps every codeindex kind to its SCIP Kind and descriptor suffix, so namespaces/packages/modules use `/`, constructors, getters, signatures and subscripts `().`, macros `!`, and properties, fields and enum members get a Kind instead of UnspecifiedKind. Overloads are told apart by the method disambiguator. Re-exports become reference occurrences of what they forward instead of definitions (`export *` emitted names like `* (./x)`), and empty-named symbols from error recovery are skipped since they made `scip lint` report non-canonical symbols. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…lixir
These files produced no import refs at all, so a Kotlin or Scala file had
no edge to anything it imported, and mixed JVM repos lost every
Kotlin<->Java link (javaRoots were built from .java files only).
Extraction: Kotlin and Scala imports (Scala selector groups with renames,
hiding, `given` and wildcards expanded, several clauses per line) and
their `package` (Scala's chained clauses and package objects); Dart
import/export/part URIs; Lua `require`; shell `source`/`.` of a literal
path (`$(dirname "$0")/x` and `${BASH_SOURCE%/*}/x` read as script
relative, anything else with an expansion dropped); Elixir
alias/import/require/use, `alias A.{B, C}` expanded.
Resolution: .java, .kt and .scala share one JVM index. Kotlin/Scala
files answer by package + top-level declaration, then by stem and the
`<Stem>Kt` facade Java sees. A wildcard naming a class (`import static
a.B.*`) now lands on that class's file. Scala also tries names relative
to its enclosing packages. Unmatched names stay external: no guess at a
same-package file, which would link every Android file to whichever
file sorts first for `import x.R`. Dart resolves relative URIs and
`package:` of an in-repo pubspec.yaml (a missing `x.g.dart`-style
generated file is external, any other miss dangling), Lua probes a/b.lua
and a/b/init.lua under the repo root, lua/ src/ lib/ dirs and the file's
own dir, shell probes the script dir then the repo root, and Elixir
looks the module up in the repo's defmodule index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
… properties `constructor(private readonly dep: Dep)` declares a class property — the form every NestJS/Angular service injects its dependencies with — and PHP 8's `__construct(private Repo $repo)` promotes the same way. Neither declares the property anywhere else, so the class indexed without it. A constructor parameter carrying a visibility, `readonly` or `override` modifier (TS) or a `property_promotion_parameter` (PHP) is now a `property` of the class, placed, signed and given visibility from the parameter itself; the constructor's plain parameters stay arguments. `extraMembers` may now point at the narrower node that declares an extra, which is what places these on their own line with their own signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…ntersection The path filter ran on the walk's result. So --max-files counted files the filter then dropped: `--scope src/flask --max-files 10` spent the cap on root files and kept 0 of the 26 in scope. And the walk could not prune, so a --scope over 186 files of typescript-go still stat'ed all 66k (1.5s, now 0.1s). The filter is now WalkOptions.filter. Files must pass it, and a directory is entered only if some glob can still match beneath it, checked segment by segment. An exclude of `<dir>/**` prunes the whole directory. One builder, scanWalkOptions, now feeds all four walks that feed a scan (scanRepo, the CLI's grammar-warm walk, scanRepoParallel, preloadSessionLazy), so they cannot drift apart. --scope was ORed with --include, so `--scope src --include '**/*.py'` returned every Python file in the repo. It is now ANDed with them, and the MCP schema says so. Scope spellings `./src`, `src/`, `src\x` and an absolute path inside the repo now normalize to the same scope. A file scope keeps that file, and include/exclude globs drop a leading `./` or the repo's absolute path. The CLI now warns on stderr about a --scope that doesn't exist or is outside the repo, and about an --ignore-dir given a path. When no file is kept at all, the warning explains that globs are rooted. A trailing slash on --ignore-dir is accepted. grep still takes --scope as one more glob; that path belongs to the grep/rewrite work and is documented as the exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
… mods
A crate was known only by its [package] name, so `[lib] name =
"corelib"` left every `corelib::…` path external, and a dependency
renamed in Cargo.toml (`renamed = { package = "core-lib", … }`,
including the table form and `{ workspace = true }` entries from
[workspace.dependencies]) was never linked. The manifest is now read
line by line (also fixing a [package] whose `name` follows an array
value, which the old regex missed), and each crate carries its renames.
A bare `use net::http::Client;` in lib.rs names a child module under
2018+ uniform paths; it was only ever tried as a crate name. It is now
resolved local-first (rustc rejects a name that is both), from the crate
root for 2015-edition crates, whose `use` paths are crate-relative.
`#[path = "x.rs"] mod m;` produced a false dangling `mod m` edge. The
extractor now emits it as `mod-path x.rs`, resolved against the
declaring file's dir, and a missing target is still dangling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…ss backends
The ripgrep and JS backends were meant to be indistinguishable. In practice
they drifted, and grep could hang or flood its caller:
- ripgrep collected every hit just to return 200. On a 66k-file repo its
output overflowed the spawn buffer (ENOBUFS), and the whole run was thrown
away for a JS re-scan (4.7 s). It now runs in two phases: first
--files-with-matches, sorted, then a line search over sorted batches that
stops once maxHits+1 hits are collected (`grep e` on that repo: 0.86 s).
- The JS backend had no guard. `(a+)+\1b`, a backreference that rg rejects,
so it always runs in JS, took minutes and blocked the MCP server. The scan
now runs in a worker thread that is terminated at a wall-clock budget
(--timeout-ms / timeoutMs, default 10 s). The partial result is flagged
(timedOut plus a note naming the file where the scan stopped).
- A positive user glob passed to rg overrode .gitignore and the junk-dir
rules (`--include 'src/**'` searched src/node_modules). --scope was OR-ed
with --include, and a file scope matched nothing. Scope and globs are now
one predicate applied in JS on both backends: scope may name a file, is
ANDed with the globs, and accepts ./ and absolute spellings. Only negated
globs still reach rg, and only to prune.
- --ignore-dir, --no-gitignore and --max-bytes were silently ignored by
grep. Both backends now honour them the way walk() does. rg also gets
--no-config and reads .git/info/exclude, as the walker does.
- Dialect: the JS pattern is compiled with the u flag when valid. For rg it
is translated so \w, \b and \d stay ASCII and `.` stops at \r. Anything
that cannot be translated exactly runs in JS, with a note. Foreign syntax
(\A, \z, [[:alpha:]], \x{..}, && in a class) is rejected with an
explanation instead of matching as a literal. JS no longer reports a
phantom empty line after a final newline, and it decodes invalid UTF-8
the way rg does.
- Truncation was silent, and an 800 KB line came back whole. Hits gain
`col`. A line over 300 chars is cut to a window around the match. The CLI
keeps stdout as the bare array and reports a cap or a timeout on stderr.
MCP grep gains withMeta ({hits, truncated, filesMatched, notes}).
- parseFlags accepts `--`: the next token is the positional, so
`grep -- --out` no longer writes a file named after the next argument.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A file symlink whose target stays inside the repo was indexed as a full copy of the target under the link's path. `alias.py -> main.py` gave `main` two definitions, so no call to it resolved to a unique target, and `CLAUDE.md -> AGENTS.md` doubled every doc and search hit. Directory symlinks were already skipped for this reason. Git stores the link as a one-line blob naming its target, and ripgrep does not follow it either. These links are now skipped with the WalkSkip reason "file-symlink". The target is indexed under its own path. Inventory consumers can opt back in with WalkOptions.includeFileSymlinks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The index carried no SymbolInformation.relationships although the engine already resolves extends/implements, and every symbol used the `. . .` package, so all codeindex indexes shared one global namespace. Each resolved relation now makes the subtype an implementation of its supertype (for base classes too, as scip-typescript and scip-java do), and a method overriding a same-named supertype method an implementation and a reference of it, so Find implementations/references cross the hierarchy (6,681 relationships on microsoft/TypeScript). Symbols now carry `<manager> <name> <version>` from the nearest manifest of the file's language (npm, gomod, cargo, python, maven, composer), skipping manifests that name nothing on the way up and doubling spaces as the grammar requires; `. . .` remains when there is none. manifestCoordinates lives with the other manifest readers in workspaces.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The Cargo.toml and pubspec.yaml passes sorted every repo path to visit the few manifests in a stable order: two full sorts of 66k paths on the TypeScript repo for nothing. Filter first, then sort what matched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…onfig --repo pointing below the git toplevel (one package of a monorepo) got toplevel-relative paths from every git call: hotspots showed 0 commits for every file, churn listed paths outside the package and delta mixed both forms. Diffs now pass --relative; history is one `git log` pass (pathspec "." below the toplevel, --full-history) whose paths are projected onto --repo. Parsed output no longer depends on the user's git config: diff.noprefix, diff.mnemonicPrefix, color.*=always, diff.relative, log.showSignature, log.follow and log.showRoot are pinned or overridden, the patch for hunks drops external diff/textconv, and C-quoted `+++` paths (quote, tab, backslash) are unquoted instead of losing their hunks. History reading also: - skips shallow-clone boundary commits, which git diffs against an empty tree (a depth-1 clone reported churn 1 for every file), and says shallow; - resolves --since as a ref or a recognisable date, and throws on anything else instead of returning an empty window; - reports why it failed (not a repo, empty repo, git missing); - reads the log as a Buffer with a 1 GiB cap: past sh()'s 64 MiB a 26k commit history silently read as "no git"; - passes --no-renames (rename pairing is 100x slower on blobless clones, where it downloads blobs); - memoizes the parsed log per (dir, HEAD, window, shallow boundary). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
# Conflicts: # src/mcp.ts
…per search `index` wrote embeddings.bin and nothing read it: every `search --semantic` re-encoded the whole corpus (12.1 s on microsoft/TypeScript's 244k units), and the endpoint tier re-POSTed all of it, one batch at a time, on every call. The MCP memo rebuilt from scratch after any edit. Each embeddings.bin record now carries a hash of its unit text, and the builders take a `previous` index: a vector is reused only for the same text under the same model and EMBED_VERSION, so a stale, foreign or corrupt file costs a re-encode, never a wrong ranking, and the result is byte-identical to a fresh build. search reads <index>/embeddings.bin (TS: 2.3 s to read and reuse), `index` and `embed build` reuse the previous file, and MCP passes its stale in-memory index on a scan change. The endpoint tier sends only missing texts, four batches in flight, and the CLI caches its vectors under <index>/embed-cache/ when an index exists, keyed by URL and by a fingerprint of the model behind it. deserializeEmbeddings now validates the header and copies the body once instead of allocating per record; readEmbeddingsFile never throws. A file with no positive similarity no longer gets a semanticSymbol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
… set
Go never states that a type implements an interface, so `implementations
Render` on gin answered [] although render.go asserts sixteen of them. The
type hierarchy now adds a Go type as an implementation of an interface when
a `var _ I = (*T)(nil)` (or T{}, &T{}, new(T)) assertion says so, or when
its methods, own and promoted through struct embedding, cover the
interface's by name and parameter count (marked structural: true). Embedded
interfaces contribute their methods; one embedding an out-of-repo interface
is matched only through an assertion, and an interface with an unexported
method only inside its package. Hierarchy answers only: graph.json edges
are unchanged. gin: Render 0 -> 18, Binding 0 -> 13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A call binds to the method its receiver's declared type names, so `x.area()` on a Shape reached Shape/area and nothing else: callgraph never showed the overrides that actually run, and deadcode flagged them. The symbol graph now links each method to the nearest supertype method of the same name (`overrides`), including a Go method to the interface method its type implements (the Go implementations also become `implements` edges there). Walking out through a method reaches its overrides; walking in to an override reaches the base method's callers; an override is never listed as a caller of its base. deadcode treats an override of a called or public method as live, and the top of an override chain in a class whose base is outside the repo (a jinja loader's get_source) too. gin: 207 -> 170 candidates. Also: a type's node no longer loses to a same-named member of its file when inheritance edges are placed (Go's Render interface declares a Render method). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
The regex tier is the only one for Swift and Dart, and for Kotlin and Elixir until `grammars pull`, and it missed core constructs: Kotlin extension functions, `enum`/`value`/`annotation` classes, `sealed`/`fun` interfaces, typealiases and properties; Swift attributes before modifiers, actors, extensions, `init`/`deinit`/`subscript`, properties; Go generics and package-level const/var (grouped too); TS `function*`, every `declare` form (a .d.ts gave nothing) and namespaces; and any Unicode identifier. Kotlin `internal` is now exported, as the AST tier reads it. The brace languages are masked before matching (comments and string bodies blanked, lines kept), so a declaration quoted in a JSDoc example, a KDoc block or a code generator's template literal is no longer one. The same mask tells a type member from a function local, which is how Swift and Kotlin properties are indexed without the locals. Regex-tier symbols now carry the doc comment above them (past annotations; Elixir's @doc too) and, in brace languages, an endLine from brace matching. The span is kept only when the body closes on its own line or at the declaration's indentation, which a formatter always does; otherwise it is left out rather than guessed, since replace_symbol_body splices by it. On 1,500 TS, 1,500 Go, 400 Java, 400 Rust files etc. the spans agree with the AST tier on 99.9%; every remaining disagreement inspected was an AST parse error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Signatures, docs, parents and line spans were reachable only from an MCP
client (find_symbol, find_references, symbols_overview); the CLI's
`symbols` prints name -> {file, line, kind}. The three commands print the
MCP answers byte for byte, read through readScan() so a fresh persisted
index answers, and take the CLI's path spellings (./, absolute,
backslashes) via a helper now shared in src/patharg.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Sphinx is the Python ecosystem's default docs format, and every .rst file was indexed under its file name only: no title, no headings, no summary, no links. None of flask's 79 docs had any, and a search for "application factory" missed the page titled "Application Factories". extract/rst.ts is a line scanner in the markdown extractor's shape. Section titles are text under (and optionally over) a punctuation adornment, the first being the title. The summary is the first sentence of the intro's first plain paragraph, skipping directives, field lists, lists and literal blocks. Links are toctree entries, :doc: roles (outside code blocks) and include/literalinclude targets, as doc-link specs relative to the file. An absolute document name is relative to the Sphinx source directory, which only conf.py's location says, so each ancestor is offered as a soft ref and only an existing file becomes an edge. On flask: 182 doc-link edges, none dangling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A grep hit, a stack frame or a diagnostic names a line; every navigation query wants a symbol. enclosingSymbol was exported but surfaced nowhere. symbolAt answers the innermost declaration holding the line with its symbol id (what callers/callgraph read), the declarations around it, and `approximate: true` when a regex-tier file has no spans to bound it. symbol_at joins the find and edit profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
impactOf and neighborsOf were reachable from the CLI only, so an MCP agent's one answer to "what depends on this file" was the whole `graph` blob (118 KB on gin, capped on large repos). Both tools read the session's freshness-proven artifacts, take the CLI's path spellings, reject an unknown target or edge kind as an error, and join the impact profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Nothing answered "how does A reach B": the only shortest-path code lived in centrality and the rules' cycle check. callPath is a breadth-first search over the symbol graph that follows calls and, like callgraph, dispatch to overrides. It lists the shortest chains in id order (levels sorted, so successor order cannot leak), counts every equally short one, and says when the question was asked backwards (reverseHops). `--files` / files:true asks the same of two files over import/use/call edges with impact's rules (a Go import reaches its package; inferred calls only on request, otherwise inferredHops). One helper, src/paths.ts, serves both. call_path joins the impact profile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
A bound call site said file:line but not which function makes the call, though the symbol graph attributes every site to its enclosing declaration. withCallerIds adds that declaration's symbol id as `caller`, by the same enclosingAmong the graph uses, so it matches the callgraph edge's source. Opt-in: the default callers bytes do not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Conflicts resolved keeping both sides: - src/engine-cli.ts: mcp help lists both index_status (infra) and resolution_report (graph-extras) -> 35 tools; VALUE_FLAGS keeps --lang and --why. Both branches added a `check` flag (status --check, workspaces --check): one field, one parse branch, one help entry now. - src/mcp/tools.ts: index_status and resolution_report both appended to TOOLS/TOOL_META; orient profile keeps the mcp branch's graph and write_memory and gains index_status. - tests/mcp.test.ts, tests/mcp-output.test.ts: tool list and CASES carry both. - README: 35 tools, 29 read tools, 22 with outputSchema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
README conflict in the feature list: the extraction bullet takes wp/extract-ast's longer description (signatures, multi-name declarations, __all__, C++/Lua owners, .h dialect) and the import-resolution bullet keeps wp/resolve's detailed version; wp/extract-ast had left that bullet unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
wp/extract-imports moved import scanning out of src/extract/code.ts into the tier-independent src/extract/imports.ts (masked JS/TS and Python scans, soft refs, PHP forms), while wp/resolve had extended the old in-place extractImports with Kotlin, Scala, Dart, Lua, shell and Elixir scans, Rust `#[path]` modules and Kotlin/Scala package declarations. Both are kept: resolve's scans, helpers (jvmPath, expandScalaImport, shellSourcePath, scalaPackage) and package rules are ported verbatim into imports.ts, where they now emit through the same hard() channel, and extractPackage covers Kotlin and Scala. Because extractAst now calls the same scan, the public AST API gains these languages' refs too. Other conflicts: - src/ast/extract.ts: keep wp/extract-ast's loader/node/specs imports (grammarKeyFor, COMMENT_NODE, luaMember) minus findFirst, whose only uses wp/extract-imports deleted; collectAll returns no refs (extract-imports) and keeps the source-order caps via capCallSites (extract-ast). - src/extract/code.ts: take the extract-imports body (generated files, SFCs, fileSummary) plus capCallSites for the regex collector; mergeCalls (SFC script + template sites) now caps through capCallSites as well, so the source-order capping contract holds for components too. - README: the extraction bullet keeps extract-ast's symbol list and appends extract-imports' SFC, regex-tier, summary and docs paragraphs; resolve's import bullet stays. - src/ast/specs.ts: reword the stale `imports?` comment (the table no longer says how to read a specifier). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
- tests/graph.test.ts (wp/resolve) pinned the graph side of the soft-ref contract by injecting soft refs into a scan whose extractor emitted none. With wp/extract-imports the extractor itself turns `from . import util` into a soft `.util`, so the "resolves nowhere" case saw a util edge. The fixture now strips the extractor's soft refs before injecting its own. - tests/index-cache.test.ts (wp/infra) told the Kotlin AST tier from the regex tier by the `items` property, which the regex tier now also finds (wp/extract-imports' regex-tier work). It now checks `add`'s parent: the AST tier nests it under `Registry`, the regex tier reports no parent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Conflicts, both sides kept:
- src/engine-cli.ts: imports union (infra's preload/freshness/why,
graph-extras' resolution/workspaces check, navigation's query/symref/
patharg/traverse); help keeps graph-extras' scip/workspaces/mermaid text
and navigation's callers..symbol-at, impact and neighbors entries, and
the resolution command; --limit documents every command's default and
resolution's; parseFlags keeps --lang next to navigation's flags and two
positionals; impact/neighbors read the graph through infra's readGraph()
and try every reading of a file argument (navigation). The mcp line now
lists all 39 tools. Unused imports left by the union dropped.
- src/mcp.ts: navigation's typeEntry/symbol-ref lookups with the mcp
branch's NotFound (a not-found stays a JSON {error} text sent as isError,
which the outputSchema contract needs); imports union.
- src/mcp/tools.ts: symbolRefDescription plus the mcp branch's concise
wording; profiles merge (orient keeps index_status/graph/write_memory,
find gains symbol_at next to embed_status, impact gains call_path,
impact, neighbors next to resolution_report).
- src/calls.ts: the SFC languages join the JS family (extract-imports) in
the shared familyOf navigation's binder uses.
- src/render/scip.ts: graph-extras' parent-chained symbols keep
navigation's binder gate; a CodeSymbol -> symbol string map replaces the
one navigation built in its (rewritten) pass 1.
- src/ast/extract.ts, src/cache.ts, tests/cache-validation.test.ts:
importAliases (navigation) next to unquote and `generated`.
- tests/mcp.test.ts: 39-tool list. README: both new sections (index cache/
freshness/status/scope/scan skips; naming a symbol, find/refs/outline,
callpath...), 39 tools, 33 read tools, 26 with outputSchema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…rts and mcp - tests/call-binding.test.ts: the Go `use` case expected use.go -> context.go as a `use` edge, relying on the package import resolving to app_test.go (first in sort order). wp/resolve now resolves a Go package to its first non-test file, context.go, so that pair is an import edge and the `use` is (correctly) suppressed. The test pins that, and keeps its positive case through a second file of the imported package (logger.go, via a new `var _ app.LogParams` appended to the fixture). - tests/query-surfaces.test.ts: `find --concise` now carries `parent` for a member (wp/mcp's concise contract); and the regex tier now bounds brace-language declarations (wp/extract-imports), so symbol-at on nested.ts is exact there. The "approximate" case moves to a Python file, for which the regex tier still records no span. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Conflicts, both sides kept: - src/grep.ts: wp/search rewrote both backends (shared keep-predicate, bounded rg phases, a worker-thread JS scan). wp/infra's one change there, walking with trackedBuildDirs:false so the JS backend skips build-named dirs by name exactly like rg's `!**/<dir>/**` globs, is carried into the new jsBackend walk. - src/engine-cli.ts: search's grep/rewrite help, `--` end-of-options, the tolerant embed-model load and the reused-vector embed build, with navigation's two positionals (a `--` positional is recorded in both fields) and infra's atomic writeArtifact for embeddings.bin; the mcp line keeps the 39-tool list. - README: search's compound-file-name paragraph after extract-imports' template-text wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
wp/search implemented grep's scope handling itself because wp/infra's shared helpers did not exist on its branch; with both merged: - grep's scopeFilter now normalizes through scan.ts's normalizeScope, so `binding\`, `a/../b` and `./x/` mean for grep what they mean for every scanning command; a relative escape (`../x`) is refused like an absolute one outside the repo. - wp/infra made `.codeindex` structural in walk.ts (skipped whatever --ignore-dir says). The JS backend inherits that, but a custom --ignore-dir replaced rg's default exclusions and let rg search it: universeArgs now always passes `!**/.codeindex/**`, as wp/search noted. - --scope help and README no longer say grep adds the scope to its globs: it is the same intersection, applied to the files it searches. Regression tests: .codeindex with and without a custom ignore-dir on both backends (fails without the rg glob), and the new scope spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Conflicts, both sides kept: - src/viz.ts: graph-extras' injective mermaidIds (a colliding slug gets `_2`, …) now takes analytics' `m_` prefix, so no node id is a flowchart keyword, and nodes are labelled by module path (analytics). The clustered view already used both. tests/phase2.test.ts: both tests kept; the id collision test expects the prefixed ids and path labels. - src/engine-cli.ts: imports union (historyStatus, delta's diff API, parseRulesText); help keeps navigation's entries with analytics' churn, complexity and risk wording; --limit lists every command's default; --since/--min-together/--max-commit-files/--hidden/--fail-on join the flags; coupling leaves SCANLESS_COMMANDS (it now reads the index); delta keeps its deferred walk, which now walks with infra's scan options (walkRepo), and workspaces --check still scans; rules takes analytics' scan-aware check. complexity keeps navigation's multi-reading file argument with --limit (analytics' indexedFile stays the library helper). --fail-on is a value flag, so it also works before the subcommand. The mcp line lists 40 tools. - src/mcp.ts: imports union; complexity keeps the mcp branch's file resolution (did-you-mean suggestions); analytics' indexedFile is not imported there (the mcp module has its own). - src/mcp/tools.ts: sinceProp next to symbolRefDescription; delta appended after resolution_report and index_status; delta joins the impact and risk profiles. - tests/mcp.test.ts: 40-tool list. README: both sides' sections, the hotspots example, repo_map and delta concise notes, 40 tools, 34 read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
- src/mcp/protocol.ts: wp/mcp's invariant test requires every backticked word of a NARROWER hint to be an argument of that tool; wp/analytics' delta hint wrote `concise: true` and `format: "text"`. It now names `concise` and `format`, same advice. - src/engine-cli.ts: wp/analytics applied --scope/--include/--exclude to churn keys by globbing `<scope>/**` together with the includes, i.e. the union and the raw spelling wp/infra removed everywhere else (`./src/` matched nothing; `--scope src --include 'docs/**'` kept both trees). It now runs the scan's own scanPathFilter over each key. Regression test in tests/git-history.test.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Conflicts, both sides kept:
- src/mcp.ts: the LSP session pool (edit) lives next to the mcp branch's
--watch oracle; it is created once per server, closed in the same
`finally`, and threaded through toolsCall into callTool (new trailing
parameter) to find_references/callers, which keep navigation's resolved
symbol ref (lspRef, leaf name) and explainNoCallers. The symbolic edits
resolve `file` through the mcp branch's indexedFile (./, absolute,
suggestions) and pass edit's { line, strict }. edit's own fs.watch block
is superseded by the mcp branch's watcher, which already invalidates.
- src/mcp/tools.ts: editTargetProps (file, line, strict) next to the other
shared props. tests/mcp.test.ts: session imports union; both branches'
session tests kept (watcher invalidation, forgetting one file).
- src/engine-cli.ts: the 40-tool mcp line gains edit's note on checked
writes. README: the LSP timeout env paragraph (mcp) with edit's wording on
short-lived CLI sessions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
tests/edit-safety.test.ts (wp/edit) asserted that a formatted Swift function has no endLine, to prove the edit's own brace matcher is what bounds a regex-tier symbol. wp/extract-imports taught the regex tier to record that span itself, at the same closing brace. The premise now checks that the symbol is regex-tier (first-line signature) and bounded at line 7; the edit assertions are unchanged, and the unprovable-end case (a protocol requirement) still refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
wp/edit replaced sessionClear with sessionForgetFile, which drops the edited file's (size, mtime) proof in every session entry. wp/mcp added a proven walk to each entry: when the --watch oracle hands the same walk object back, getScan returns the cached scan without a stat. The two did not meet: after an edit, a call handed back the proven walk (the event not seen yet, or a write the watcher missed) still got the pre-edit scan. sessionForgetFile now clears the entry's proven walk too, and shares one poison helper with sessionInvalidate. Regression test in tests/mcp.test.ts (fails without the reset). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
tests/lsp-pool.test.ts "replaces a pooled server that died between queries" failed here on every run, and fails the same way on wp/edit alone (reproduced from a `git archive` of the branch): the pool checks session.alive() before reusing a server, but that flag flips only when the child's exit event is delivered. A server SIGKILLed just before the query (its zombie already reaped by the kernel, the event still queued) passed the check and the query failed with "language server exited". A reused session found dead after the query is now retired and the query asked once more on a fresh server; a fresh server is never retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Extraction output changed on several merged branches (wp/extract-ast, wp/extract-imports, wp/navigation's importAliases and curried receivers, wp/resolve's new import scans and `pkg` for Kotlin/Scala, wp/edit's UTF-8 decoding of files holding a literal U+FFFD). A persisted cache.json or freshness.json from v14 is now rejected (`extractor`), so every repo re-extracts once instead of mixing old and new records or serving stale graph.json/symbols.json through the index fastpath. SCHEMA_VERSION stays 5. graph.json and symbols.json only gained optional fields (FileNode.generated) and new values under existing keys: more or different edges, symbols, soft-ref-derived import edges, and a `commit` stamp always 7 characters long. No field was removed, renamed or retyped, and no edge kind was added, so a v5 reader parses every new artifact as before. The cache-only additions (FileRecord.importAliases, generated, RawRef.soft) are covered by the extractor bump. ENGINE_VERSION is left to semantic-release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
wp/extract-imports taught the regex tier to record a declaration's span in brace languages when the braces close as a formatter puts them, while comments and docs written on other branches (navigation's symbol_at and complexity, analytics' delta, literals, callers) still said regex records never have one. They now say a record without an end line is the regex tier's unprovable case; the symbol_at help, MCP description and README say when "approximate" applies. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
scripts/engine.mjs, engine.d.mts, engine.browser.mjs and engine.browser.d.mts rebuilt from the integrated src (every merged work package, the integration fixes and EXTRACTOR_VERSION 15); scripts/cli.mjs is unchanged by the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…erge wp/search moved the MCP search tool onto explainQuery/explainSemantic; the mcp side's searchIndex import survived the merge with no use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
…directly Two tests failed on the CI runner but not locally, both from environment differences: - `--since "100 years ago"` resolves to a pre-1970 timestamp. git 2.43 prints it wrapped to a huge unsigned --max-age; git 2.55 reads that back as a far-future cutoff, so the window matched nothing. A negative or wrapped age now means "the whole history" and is passed as no filter. - The grep "slower engine" test inferred ripgrep's presence from a plain pattern producing no note, which is also true without ripgrep. It now asks for `rg --version` directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every feature area was audited against fixtures and real repos (flask, gin, microsoft/TypeScript with 66k files, django, leveldb). Each finding was reproduced before it was fixed and has a regression test. Work landed in 11 parallel packages, merged here with
--no-ff. Each package's merge commit groups its commits (≈140 in total).Index, cache and walk
--indexpath is now honoured. Before, it was silently ignored and every read triggered a full rebuild: 55 s → 15 s on the TypeScript repo. A custom in-repo index directory is no longer scanned as source.--max-callsproduced each file. Switching--no-astor--max-calls, or pulling grammars, now re-extracts only the affected files; before, stale results were served.indexaccepts--full-hashand--no-index-cache.--out <repo root>no longer produces an empty index..codeindexis always skipped, even with--ignore-dir..gitignorematching is about 6× faster.--scope/--includenow prune the walk. They intersect with the other filters, and--max-filescounts kept files.graph/symbolsstream the sha-verified bytes.status [--check]: index freshness; also MCPindex_status.scan --why <path>andscan --skipped: why a file is not indexed.Extraction (
EXTRACTOR_VERSION14 → 15)if/try/with, and__all__.typedef structmembers, macros, out-of-line methods attached to their class, C++.hheaders parsed with the C++ grammar, operators.from . import xnow links to the submodule.usestatements andrequire_once __DIR__ . '/x.php'are extracted.Resolution and graph
typingwas binding tosrc/flask/typing.py), and src layouts resolve. Djangograph: 13.7 s → 6.0 s, 712 false edges gone.baseUrl;pathsfollow tsc precedence.extendsinto a workspace package, and${configDir}.imports(#subpath), Vite?querysuffixes._test.gofile.[lib] name, renamed dependencies, uniform paths,#[path]modules.include, nested Maven aggregators and multi-go.modrepos are detected. Newworkspaces --checkreports undeclared and unused dependencies, and warnings are surfaced.resolutionreport (CLI + MCP).scipCLI.__proto__are kept;/links in markdown are repo-root-relative.Navigation and analysis
errors.Newno longer binds to repo code.__init__re-exports, and through aliased/default imports, are resolved.neighborsshows every relation; Go tests cover their package.name,name@file,Parent/name) for every command. Misses are explained, and CLI flags such as--limitand--kindare honoured.find,refs,outline,symbol-at,callpath; on MCP:impact,neighbors,symbol_at,call_path,delta.Search, grep, rewrite
grep eon TypeScript: 4.7 s → 0.86 s).--files-with-matchesis new.Default,Get) are searchable.embeddings.bin, keeps the lexical fields and options, indexes docs, and degrades cleanly on a corrupt model.Git, delta, literals, rules
--repo(subdirectories work), and git runs immune to user config (color, mnemonic prefix, quoting).--sincewith an invalid ref is reported.MCP server, edits, LSP
ping,notifications/cancelledand progress notifications work.--watchdrops deleted files, ignores ignored trees and proves freshness without re-walking.Validation
pnpm typecheckpasses.pnpm run check:buildpasses (bundle rebuilt and committed).npx vitest run: 109 files passed, 3 skipped; 2,093 tests passed.graph.json/symbols.jsonwith--workers 0and--workers 3, and a secondindexrun reports "unchanged".Not done / follow-ups
tests/quality/external-oracles.jsonand the README need aCODEINDEX_ORACLE=1run with ctags installed.head, flags a command ignores, per-command help, and consistent exit codes.grepreturns[]silently when--scopeor--includeleave no files.using static, Swift imports, PEP 420 namespace roots).🤖 Generated with Claude Code
https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q
Generated by Claude Code