Skip to content

Full feature review: correctness, performance and new features - #22

Draft
maxgfr wants to merge 140 commits into
mainfrom
claude/features-review-optimization-x657hf
Draft

maxgfr wants to merge 140 commits into
mainfrom
claude/features-review-optimization-x657hf

Conversation

@maxgfr

@maxgfr maxgfr commented Sep 25, 2026

Copy link
Copy Markdown
Owner

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

  • An absolute --index path 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.
  • The cache records which grammar tier and which --max-calls produced each file. Switching --no-ast or --max-calls, or pulling grammars, now re-extracts only the affected files; before, stale results were served.
  • index accepts --full-hash and --no-index-cache. --out <repo root> no longer produces an empty index. .codeindex is always skipped, even with --ignore-dir.
  • The commit stamp is a fixed 7 characters (deterministic). Artifacts are written atomically (temp file + rename).
  • Extended grammars (Kotlin, Elixir, Zig, …) now load in the npm layout.
  • Faster:
    • .gitignore matching is about 6× faster.
    • --scope/--include now prune the walk. They intersect with the other filters, and --max-files counts kept files.
    • With workers, a file whose mtime changed but whose content hash is unchanged is no longer re-extracted.
    • Docs take the stat fastpath.
    • A commit that leaves the tree unchanged only re-stamps graph.json.
    • graph/symbols stream the sha-verified bytes.
  • New commands:
    • status [--check]: index freshness; also MCP index_status.
    • scan --why <path> and scan --skipped: why a file is not indexed.

Extraction (EXTRACTOR_VERSION 14 → 15)

  • Rust, TS, Python: Rust docs behind attributes, TS decorators on their own line. Python definitions under if/try/with, and __all__.
  • Visibility: read from the modifiers only.
  • Go: type aliases.
  • C/C++: typedef struct members, macros, out-of-line methods attached to their class, C++ .h headers parsed with the C++ grammar, operators.
  • JS and dynamic languages: CommonJS default exports, Elixir guards, Lua table functions, Ruby singleton classes.
  • TS/PHP: constructor parameter properties.
  • Multi-name declarations: every name is emitted.
  • Signatures: comment-free. Call sites are capped in source order (not alphabetically), and one-letter callees are kept.
  • Imports: read from code only, never from comments, docstrings or strings.
    • A quadratic regex is gone (11 s → linear on a 900 KB file).
    • Python from . import x now links to the submodule.
    • PHP group use statements and require_once __DIR__ . '/x.php' are extracted.
  • New formats: Vue, Svelte and Astro components, minified files (flagged, not flooded), reStructuredText, Markdown setext headings.
  • Cleanups: Dart no longer indexes keywords and calls as functions, file summaries skip license headers, config numbers are not double-counted.

Resolution and graph

  • Python import roots: stdlib imports no longer bind to repo files (typing was binding to src/flask/typing.py), and src layouts resolve. Django graph: 13.7 s → 6.0 s, 712 false edges gone.
  • TS/JS:
    • tsconfig baseUrl; paths follow tsc precedence.
    • extends into a workspace package, and ${configDir}.
    • package.json imports (#subpath), Vite ?query suffixes.
  • Go, Rust, Java, C#:
    • A Go package never resolves to a _test.go file.
    • Rust: [lib] name, renamed dependencies, uniform paths, #[path] modules.
    • Java/C# resolution is indexed: 17 s → 45 ms and 4.2 s → 26 ms.
  • New import edges: Kotlin, Scala, Dart, Lua, shell and Elixir. Java, Kotlin and Scala share one JVM index.
  • Workspaces: pnpm flow lists, multi-line Gradle include, nested Maven aggregators and multi-go.mod repos are detected. New workspaces --check reports undeclared and unused dependencies, and warnings are surfaced.
  • New resolution report (CLI + MCP).
  • SCIP: nested symbols are chained through their real parent, every kind is mapped, and implementation relationships and a package identity are emitted. Validated with the official scip CLI.
  • Smaller fixes: mermaid ids are collision-free and never a flowchart keyword; symbols named __proto__ are kept; / links in markdown are repo-root-relative.

Navigation and analysis

  • Call binding:
    • Uses the receiver, the imports and the package: errors.New no longer binds to repo code.
    • Go same-package calls bind.
    • Calls through JS barrels and Python __init__ re-exports, and through aliased/default imports, are resolved.
    • Binders are 3–6× faster.
  • impact / neighbors / tests-map: no impossible Go dependents; neighbors shows every relation; Go tests cover their package.
  • deadcode: no longer flags symbols that are imported, extended or called. Callables only by default, and public API roots are derived from the manifests.
  • complexity: comments and docstrings are ignored.
  • Go implementations: detected structurally; override edges added.
  • Symbol syntax: one syntax (name, name@file, Parent/name) for every command. Misses are explained, and CLI flags such as --limit and --kind are honoured.
  • New commands: find, refs, outline, symbol-at, callpath; on MCP: impact, neighbors, symbol_at, call_path, delta.

Search, grep, rewrite

  • grep:
    • ripgrep runs in two phases with batching (grep e on TypeScript: 4.7 s → 0.86 s).
    • The JS fallback runs in a worker with a time budget, so a ReDoS pattern no longer blocks the CLI or the MCP server.
    • Both backends use the same regex dialect, the same file universe and honour the walk flags.
    • Capped results are signalled, and --files-with-matches is new.
  • rewrite: POSIX-faithful quoting and regex translation, and flag injection is no longer possible. 32 command forms are checked against real grep, rg and git grep.
  • BM25:
    • Stems such as "queries" and "retries" now match.
    • Stopword identifiers (Default, Get) are searchable.
    • Barrels no longer outrank defining modules, and testdata/ fixtures are demoted.
    • Warm queries are faster.
    • Compound file names are split.
  • Semantic search: reuses embeddings.bin, keeps the lexical fields and options, indexes docs, and degrades cleanly on a corrupt model.

Git, delta, literals, rules

  • Git: paths are relative to --repo (subdirectories work), and git runs immune to user config (color, mnemonic prefix, quoting).
  • History handling:
    • Shallow clones are handled.
    • --since with an invalid ref is reported.
    • Histories whose git log output exceeds 64 MiB are no longer lost.
    • Coupling is 7.8× faster.
  • delta: traces the importers of deleted or renamed files and ignores its own index.
  • Literals: Python strings are counted, and the CI gate no longer reads the capped headline.
  • Rules: configs are validated.
  • repomap and onboard: less noise.

MCP server, edits, LSP

  • SDK compatibility: the official TypeScript SDK client now lists and calls all 40 tools; before, it listed none.
  • Validation: arguments are validated before any scan (13.5 s → 1 ms). Invariant tests loop over every tool.
  • Request loop: no longer blocks; ping, notifications/cancelled and progress notifications work.
  • Freshness: --watch drops deleted files, ignores ignored trees and proves freshness without re-walking.
  • Symbolic edits: no corruption on regex-tier symbols; decorators and docs are respected; encoding and line endings are preserved. Only the edited file is invalidated, and the structure is checked after the edit.
  • LSP: a language-server pool per session, a correct anchor column, and server errors surfaced.

Validation

  • pnpm typecheck passes.
  • pnpm run check:build passes (bundle rebuilt and committed).
  • npx vitest run: 109 files passed, 3 skipped; 2,093 tests passed.
  • Quality ratchet: every metric kept or improved, on a larger labelled set (265 → 346 symbols). Go visibility accuracy 94.4% → 95.8%. Search MRR/nDCG unchanged.
  • Determinism: flask and gin produce byte-identical graph.json/symbols.json with --workers 0 and --workers 3, and a second index run reports "unchanged".

Not done / follow-ups

  • The ctags figures in tests/quality/external-oracles.json and the README need a CODEINDEX_ORACLE=1 run with ctags installed.
  • Cross-cutting CLI UX found by the audit: an EPIPE stack trace when piping to head, flags a command ignores, per-command help, and consistent exit codes.
  • A dedicated performance profiling pass on the merged code.
  • MCP tools still ignore unknown arguments, and grep returns [] silently when --scope or --include leave no files.
  • Known gaps listed in the commits (e.g. C# using static, Swift imports, PEP 420 namespace roots).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XWLTLwAs9kt9Ac2YvAXV1q


Generated by Claude Code

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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants