graph-indexer 1.2.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +854 -0
- package/README.md +258 -595
- package/SECURITY.md +85 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/colbert.mjs +168 -0
- package/config.mjs +408 -32
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +571 -0
- package/engine/binary.mjs +437 -0
- package/{core-engine.mjs → engine/memory.mjs} +321 -441
- package/{sqlite-store.mjs → engine/sqlite.mjs} +413 -45
- package/enrichment.mjs +206 -32
- package/git-signals.mjs +248 -0
- package/indexer.mjs +399 -45
- package/init.mjs +1626 -171
- package/layout.mjs +123 -0
- package/mcp/centrality.mjs +117 -0
- package/mcp/format.mjs +61 -0
- package/mcp/resolver.mjs +116 -0
- package/mcp/symbolgraph.mjs +137 -0
- package/mcp/taint.mjs +225 -0
- package/mcp/tools.mjs +1582 -0
- package/mcp/topology.mjs +676 -0
- package/mcp-server.mjs +80 -25
- package/package.json +66 -27
- package/parse/extractor.mjs +628 -0
- package/parse/imports.mjs +365 -0
- package/parse/interprocedural.mjs +134 -0
- package/parse/languages.mjs +253 -0
- package/parse/metadata.mjs +701 -0
- package/parse/routes.mjs +327 -0
- package/parse/scip.mjs +391 -0
- package/parse/taint-patterns.mjs +119 -0
- package/prompts/CORE.md +91 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +20 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +19 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +20 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +24 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/seal.mjs +400 -0
- package/search-core.mjs +405 -44
- package/search-sparse.mjs +183 -0
- package/storage.mjs +33 -3
- package/watch-daemon.mjs +75 -24
- package/PROMPT.md +0 -182
- package/mcp-tools.mjs +0 -545
- package/parser-utils.mjs +0 -1170
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to graph-indexer are documented here. Dates are in YYYY-MM-DD format.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Install-size fix: heavy optional packages no longer ship by default
|
|
10
|
+
|
|
11
|
+
- **`@huggingface/transformers` (→ onnxruntime-node, ~950 MB with the full model/runtime chain)
|
|
12
|
+
and `hnswlib-node` are no longer `optionalDependencies`.** npm's `optionalDependencies`
|
|
13
|
+
install eagerly regardless of the name — "optional" only means "don't fail the whole install
|
|
14
|
+
if this can't build on this platform" — so every `npm i graph-indexer` was downloading the
|
|
15
|
+
local-embeddings/cross-encoder-rerank stack even for lexical-only and Ollama-only users. They
|
|
16
|
+
are now `devDependencies` (available for this repo's own tests/CI, never installed as a
|
|
17
|
+
dependent). The runtime already lazy-`import()`s them with a clear "install it or change
|
|
18
|
+
provider" error — that design was correct; only the packaging contradicted it. New
|
|
19
|
+
`npm run embed:setup:local` one-liner installs it on demand, mirroring `embed:setup:mlx`; the
|
|
20
|
+
`init.mjs` wizard now offers to install it inline when "Local (in-process)" embeddings are
|
|
21
|
+
selected (or "Auto" falls through because Ollama isn't reachable).
|
|
22
|
+
- **The 15 Tree-sitter language grammars (~250 MB combined) are also no longer bundled.** Same
|
|
23
|
+
fix, larger surface: `parse/languages.mjs` now resolves each grammar ambiently first (this
|
|
24
|
+
repo's own devDependencies, or a project that installed one directly), then from a scoped
|
|
25
|
+
`.graph-indexer/node_modules` install dir, auto-installing whatever's missing there on the
|
|
26
|
+
first `idx-index`/`idx-mcp`/daemon run for the languages actually selected/detected — cached
|
|
27
|
+
from then on, works identically whether graph-indexer is a project dependency, a global
|
|
28
|
+
install, or run via bare `npx` (the install always targets the repo being indexed, not
|
|
29
|
+
graph-indexer's own location). Each grammar installs independently, so one platform-
|
|
30
|
+
incompatible package (no native toolchain) only skips that language, not the whole batch — the
|
|
31
|
+
same graceful degradation `optionalDependencies` used to give for free, now paired with a
|
|
32
|
+
negative-install-cache so a known-unbuildable grammar isn't retried on every run. `--sealed`
|
|
33
|
+
disables auto-install entirely and fails the run with the exact remedy command if a configured
|
|
34
|
+
language's grammar isn't already resolvable, rather than silently shipping a partial index.
|
|
35
|
+
Verified end-to-end: a fresh `npm install graph-indexer` drops from ~1.5 GB to ~27 MB of
|
|
36
|
+
`node_modules`; `test:all` (including `test:languages` and `test:seal`) stays green.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## [2.1.0] — 2026-06-25
|
|
41
|
+
|
|
42
|
+
Frontier upgrade (Phases 1–3). Every item is **opt-in**: the default path stays lexical-only,
|
|
43
|
+
in-memory, zero-dependency, with byte-identical `npm run test:eval` output and byte-identical
|
|
44
|
+
memory↔sqlite top-5 parity. Headline additions: code-aware retrieval knobs (B1/B2), an
|
|
45
|
+
inter-procedural receiver-type fixpoint (A3), a **persistent resolved symbol graph** with
|
|
46
|
+
symbol-level **centrality** (A4/A5), a **precise resolver** + cross-file **SCIP** resolution
|
|
47
|
+
(A1/A2), `impact_of_edit` / `explain_symbol` / `tests_for` MCP tools (C1/C4/C5/D1), **sealed mode**
|
|
48
|
+
with a runtime egress guard and **signed attestation** (F1), **taint analysis** (`trace_taint` /
|
|
49
|
+
`find_tainted_sinks`) with index-time serialization across JS/TS/Python/Java/Go (C2), an opt-in
|
|
50
|
+
**learned ranker** (D3, measured a tie — RRF stays the default), **SCIP-backed precise cross-file
|
|
51
|
+
references** for `find_references` (A2 v2), an opt-in **learned-sparse** vocabulary-expansion
|
|
52
|
+
channel (B3, measured net-neutral — lexical stays the default), and a **ColBERT-style
|
|
53
|
+
late-interaction reranker** (B4, measured helps-Go / neutral-JS at production depth — opt-in, default
|
|
54
|
+
off). Every capability ships behind a flag + env + config key with the trade-off printed at startup;
|
|
55
|
+
nothing changes the default path.
|
|
56
|
+
|
|
57
|
+
### Code-specialized embedding models — documented; NL vector-weight re-tune evaluated and rejected (B1)
|
|
58
|
+
|
|
59
|
+
- **Code-specialized embedders are now documented as a first-class opt-in.** `qwen3-embedding`,
|
|
60
|
+
`nomic-embed-code`, and `jina-embeddings-v2-base-code` embed code semantics more faithfully than
|
|
61
|
+
the general-purpose `nomic-embed-text` default. They are selected with `--embed-model <name>`
|
|
62
|
+
(no new code — Ollama models are chosen by name) and stamped into the index meta so query and
|
|
63
|
+
document vectors always share a space. **Measured (honest):** on a clean `qwen3-embedding:16k`
|
|
64
|
+
re-embed, semantic rank-1 lifted on Go (`gin` 0.20 → 0.40, MRR 0.41 → 0.67) but was neutral on
|
|
65
|
+
Python (`django` rank-1 0.33 → 0.33) and JavaScript (`express-js` 0.43 → 0.43); it also indexes
|
|
66
|
+
substantially slower. So a code embedder is a *measure-first* opt-in, not a default.
|
|
67
|
+
- **Rejected: re-weighting the NL vector channel upward for code embedders.** We implemented an
|
|
68
|
+
opt-in weight profile and swept the natural-language vector weight from 0.4 → 1.2 on
|
|
69
|
+
`gin`/`django` with `qwen3-embedding`. There was **no recall@5 gain at any weight** (gin s@5 flat
|
|
70
|
+
at 1.00, django at 0.83); rank-1 was flat-to-negative and symbolic regressed at higher weights
|
|
71
|
+
(`django` symbolic rank-1 0.72 → 0.61). The existing default weighting (`NL_VECTOR_WEIGHT_PLAIN
|
|
72
|
+
= 0.4`) is already optimal even for the stronger embedder — the vector channel's job here is
|
|
73
|
+
low-weight *rescue*, and recall is already saturated. The profile was therefore **not shipped**
|
|
74
|
+
(no placebo knob); the ranking core is unchanged. Full write-up:
|
|
75
|
+
`docs/internals/IMPROVEMENT_CODE_EMBED.md`.
|
|
76
|
+
|
|
77
|
+
### Local cross-encoder reranker provider (B2)
|
|
78
|
+
|
|
79
|
+
- **`--rerank-provider cross-encoder`** (`RERANK_PROVIDER` / `rerank.provider` config) adds a
|
|
80
|
+
second, **air-gapped** reranker alongside the generative LLM judge. It scores each
|
|
81
|
+
(query, candidate) pair with a local MS-MARCO cross-encoder
|
|
82
|
+
(`Xenova/ms-marco-MiniLM-L-6-v2`, via the optional `@huggingface/transformers` — the same dep
|
|
83
|
+
as the in-process embedder) and sorts by score. No Ollama, no daemon, **deterministic**, fast
|
|
84
|
+
(~tens of ms per NL query); the model downloads once on first use.
|
|
85
|
+
- **Measured (honest), core suites, lexical channel:** agent-style semantic rank-1 0.19 → 0.26
|
|
86
|
+
(held-out 0.30 → 0.35; held-out s@5 0.48 → 0.52), strongest on Go (gin 0.20 → 0.40) and Python
|
|
87
|
+
(fastapi 0.14 → 0.29). It is **weaker than the generative 7B judge** (0.42 agent-style rank-1)
|
|
88
|
+
and shows the usual reranker behaviour on JavaScript (mixed) plus a small symbolic wobble (long
|
|
89
|
+
symbolic queries that pass the NL gate). The generative judge stays the higher-reasoning tier;
|
|
90
|
+
the cross-encoder is the no-LLM-in-the-loop option. Full write-up:
|
|
91
|
+
`docs/internals/IMPROVEMENT_CROSS_ENCODER.md`.
|
|
92
|
+
- Implementation: `rerankCrossEncoder` / `crossEncoderScore` in `enrichment.mjs` (sibling to the
|
|
93
|
+
generative `rerankResults`, lazy-loads transformers, deterministic score-desc/id-asc sort,
|
|
94
|
+
never mutates the fused score, degrades gracefully); dispatched in `mcp/tools.mjs` on
|
|
95
|
+
`rerank.provider`, reusing the existing over-fetch pool unchanged. Default path (provider
|
|
96
|
+
`generative`, reranker off) is byte-identical; memory↔sqlite parity unaffected. New
|
|
97
|
+
`test/unit.mjs` tests inject the scorer (no model in CI).
|
|
98
|
+
|
|
99
|
+
### Inter-procedural receiver-type fixpoint (A3)
|
|
100
|
+
|
|
101
|
+
- **`--interprocedural`** (`INDEXER_INTERPROCEDURAL` / `interprocedural` config) adds an
|
|
102
|
+
index-time, whole-program fixpoint that propagates function return types along **factory call
|
|
103
|
+
chains**, so a call-site receiver whose type comes from a multi-hop or unannotated factory
|
|
104
|
+
resolves to its concrete class. Example: `makeRepoIndirect()` returns `makeRepo()` returns
|
|
105
|
+
`new OrderRepo()` — the indirect factory has no return type to read, but the fixpoint resolves
|
|
106
|
+
the chain, so `const m = makeRepoIndirect(); m.save()` is now a **high-confidence** caller of
|
|
107
|
+
`OrderRepo.save` in `get_call_graph` / `find_references` instead of a name-only match.
|
|
108
|
+
- **Design:** a new `parse/interprocedural.mjs` runs a bounded (`MAX_ITERS = 8`), monotone,
|
|
109
|
+
deterministic worklist over a per-symbol return-type lattice (conflicts — ≥2 distinct types —
|
|
110
|
+
are conservatively dropped). It writes `recv_resolved_type` onto the relevant call sites;
|
|
111
|
+
because `call_sites` is serialized identically by both backends, the resolved data is
|
|
112
|
+
**parity-free**. `classifyCallers` prefers `recv_resolved_type` and keeps the existing 1-hop
|
|
113
|
+
`recv_via_call` fallback, so an index built without the pass (or a per-file daemon update) is
|
|
114
|
+
never worse. The transient capture (`_return_via`) is stripped before serialization.
|
|
115
|
+
- **Invariants:** OFF by default → the index and `get_call_graph` output are byte-identical, and
|
|
116
|
+
`npm run test:eval` is unchanged (search ranking never reads receiver types). Air-gapped,
|
|
117
|
+
zero new runtime deps. **Residual limitation (documented):** the watch daemon re-indexes one
|
|
118
|
+
file at a time and does not re-run the whole-program pass, so cross-file chains refresh on the
|
|
119
|
+
next full `idx-index`. New `test/callgraph.mjs` tests prove a 2-hop chain promotes only with the
|
|
120
|
+
fixpoint, the conflict guard holds, and `resolveReturnTypes` is order-independent. Write-up:
|
|
121
|
+
`docs/internals/IMPROVEMENT_INTERPROCEDURAL.md`.
|
|
122
|
+
|
|
123
|
+
### Test→code mapping + two compound MCP tools (C5 + D1)
|
|
124
|
+
|
|
125
|
+
The MCP tool surface grows from 11 to 13. Both new tools are read-only and compose existing
|
|
126
|
+
store primitives — no ranking, parity, or default-path impact.
|
|
127
|
+
|
|
128
|
+
- **`tests_for(symbol)`** — the test/spec chunks that exercise a symbol: a chunk under a
|
|
129
|
+
test path (`TEST_FILE_RE`) that calls or references it. Reuses `findCallers` + `findReferers`
|
|
130
|
+
filtered to tests, deterministically ordered (file, line, id). Tells an agent which tests to
|
|
131
|
+
run or update before a change, and how the symbol is meant to behave.
|
|
132
|
+
- **`explain_symbol(symbol, target_class?)`** — a single-round-trip overview that previously took
|
|
133
|
+
four tool calls: signature(s) (`resolve_symbol`), callees (the symbol's `calls`), callers /
|
|
134
|
+
blast radius + subclasses + type users (`find_references` / `classifyCallers`), the HTTP routes
|
|
135
|
+
it handles (`find_routes`), the tests that exercise it (`tests_for`), and git recency/co-change.
|
|
136
|
+
Both `markdown` and `json` (`structuredContent`) formats.
|
|
137
|
+
- `test/mcp.mjs` asserts the 13-tool surface; `test/references.mjs` adds a test→code fixture
|
|
138
|
+
proving `tests_for` returns only the test chunk (not the production caller) and `explain_symbol`
|
|
139
|
+
composes definition + callees + callers + tests, in both response formats.
|
|
140
|
+
|
|
141
|
+
### Persistent resolved symbol graph (A4) — Phase 2
|
|
142
|
+
|
|
143
|
+
- **`--symbol-graph`** (`INDEXER_SYMBOL_GRAPH` / `symbolGraph` config) materializes a resolved
|
|
144
|
+
**chunk→chunk** graph at index time: one edge `{from_chunk_id, to_chunk_id, kind, confidence}`
|
|
145
|
+
per resolved reference, where `kind` ∈ `calls` | `extends` | `type` and `confidence` ∈
|
|
146
|
+
`high` | `name_only`. It is built by **reusing the query-time resolvers** (`classifyCallers` /
|
|
147
|
+
`findReferences`) over every defined symbol, so an edge's confidence is identical to what
|
|
148
|
+
`get_call_graph` / `find_references` report — no logic duplication, no drift.
|
|
149
|
+
- New store method **`getEdges(chunkId, { kind?, direction })`** (`direction: 'in'` = referrers,
|
|
150
|
+
`'out'` = referents) on both backends, deterministically ordered → byte-identical
|
|
151
|
+
memory↔sqlite. Stored in a SQLite `edges` table / the in-memory index JSON.
|
|
152
|
+
- `findCallers` / `findReferers` now **read the edge graph when present**, returning the *same*
|
|
153
|
+
sets as the name-match scan (a set-equivalence test is the load-bearing guard), and **fall back
|
|
154
|
+
to the scan** when the graph is absent or the name is undefined — so the default path is
|
|
155
|
+
byte-identical and an undefined-symbol lookup never returns empty-by-edges.
|
|
156
|
+
- **Invariants:** OFF by default → index + call graph byte-identical, `test:eval` unchanged
|
|
157
|
+
(edges never touch search). Air-gapped, zero new deps. Deterministic edge order. A per-file
|
|
158
|
+
daemon update **invalidates** the graph (it would otherwise go stale) so callers fall back to
|
|
159
|
+
the always-correct scan until the next full `idx-index`. New `test/edges.mjs` (6 tests):
|
|
160
|
+
set-equivalence, getEdges direction/kind/confidence, undefined-name fallback, memory↔sqlite
|
|
161
|
+
parity, daemon invalidation, deterministic ordering. Foundation for `impact_of_edit` (C4) and
|
|
162
|
+
symbol-level PageRank (A5). Write-up: `docs/internals/IMPROVEMENT_SYMBOL_GRAPH.md`.
|
|
163
|
+
|
|
164
|
+
### `impact_of_edit` — precise blast radius (C1 + C4) — Phase 2
|
|
165
|
+
|
|
166
|
+
- New MCP tool **`impact_of_edit({ symbols?, files? })`** (13 → 14 tools): pass what you are
|
|
167
|
+
about to change and get the **transitively-affected** code (callers, subclasses, type users by
|
|
168
|
+
hop depth), the HTTP routes that reach it, the tests to run, the direct same-named referrers to
|
|
169
|
+
verify, and git co-change — in one call instead of recursively walking `get_call_graph`.
|
|
170
|
+
- It follows **high-confidence** edges transitively (a precise closure that does not explode on
|
|
171
|
+
ambiguous names) and lists direct `name_only` referrers separately. It is **chunk-precise on a
|
|
172
|
+
`--symbol-graph` index** (walks `getEdges`) and falls back to query-time `classifyCallers` /
|
|
173
|
+
`findReferences` otherwise — a new `buildImpact` in `mcp/topology.mjs`, plus a `hasSymbolGraph()`
|
|
174
|
+
store predicate. Deterministic ordering (depth, file, line, id); both response formats.
|
|
175
|
+
- Additive and read-only — no ranking/parity/default-path impact. `test/edges.mjs` adds 4 tests
|
|
176
|
+
(transitive depth via the graph AND the fallback, the two agreeing, and the tool composing
|
|
177
|
+
changed + impacted + tests + routes); `test/mcp.mjs` asserts the 14-tool surface.
|
|
178
|
+
|
|
179
|
+
### Symbol-level centrality (A5) — Phase 2
|
|
180
|
+
|
|
181
|
+
- On a `--symbol-graph` index, the indexer now also computes **symbol centrality**: a
|
|
182
|
+
confidence-weighted **PageRank over the resolved chunk→chunk edges** (`high` edges weighted
|
|
183
|
+
1.0, `name_only` 0.5). A definition referenced — called / extended / used as a type — by many
|
|
184
|
+
*other central* symbols scores high; it surfaces the program's load-bearing hubs. New module
|
|
185
|
+
`mcp/centrality.mjs` (`computeSymbolCentrality`). This is **distinct** from the existing
|
|
186
|
+
file-level `computePageRank` (import graph): A5 is symbol-granular and call/type-aware.
|
|
187
|
+
- **Surfaced** through two existing tools (no new tool — the surface stays at 14): `explain_symbol`
|
|
188
|
+
attaches each definition's `{ score, rank, total }` (markdown shows `🎯 centrality #R/T`, with a
|
|
189
|
+
`(hub)` tag in the top decile); `get_repo_map`'s unfiltered view lists the most-central symbols.
|
|
190
|
+
Both **omit** centrality entirely when the graph is absent → default output unchanged.
|
|
191
|
+
- **Parity-free, like the edges:** computed **once at index time** and serialized
|
|
192
|
+
(memory `centrality` map; SQLite `centrality(chunk_id, score, rank)` table), so both backends
|
|
193
|
+
read byte-identical scores and ranks. It never touches `searchHybrid`, so `test:eval` and the
|
|
194
|
+
search parity gate are unaffected. New store methods `hasCentrality()`, `getCentrality(id)`,
|
|
195
|
+
`topCentral(limit)` on both backends. Deterministic (sorted node set, fixed-order power
|
|
196
|
+
iteration, rounded scores, rank tie-broken by id).
|
|
197
|
+
- **Daemon staleness:** a per-file update invalidates the centrality alongside the edges
|
|
198
|
+
(it is a whole-program quantity) — both engines drop it until the next full `idx-index`.
|
|
199
|
+
- `test/edges.mjs` adds 8 tests (PageRank determinism, hub-outranks-leaf, confidence weighting,
|
|
200
|
+
serialization round-trip + store methods, memory↔sqlite parity, daemon invalidation, default-path
|
|
201
|
+
gating, and the two tools surfacing/omitting it). Full write-up:
|
|
202
|
+
`docs/internals/IMPROVEMENT_SYMBOL_CENTRALITY.md`.
|
|
203
|
+
|
|
204
|
+
### Precise resolver provider + impact precision dial (A1) — Phase 2
|
|
205
|
+
|
|
206
|
+
- **`--resolver precise`** (`INDEXER_RESOLVER` / `resolver` config) adds a pluggable resolver
|
|
207
|
+
provider for the symbol graph. The default `heuristic` provider keeps edge confidence exactly
|
|
208
|
+
`{ high, name_only }` (byte-identical). `precise` lifts the **provably-unambiguous** subset into
|
|
209
|
+
a third, stronger tier — **`resolved`** — a reference is `resolved` when there is no question
|
|
210
|
+
*which* definition it binds to: a **sole definition** (no same-named rival), or a **type-pinned
|
|
211
|
+
receiver** (typed receiver, the A3 inter-procedural fixpoint, `this.m()` inside the class, or an
|
|
212
|
+
explicit target class — type-resolved, so shadow-immune). `high` then means "ambiguous name,
|
|
213
|
+
import/proximity-disambiguated"; `name_only` means "ambiguous, no evidence."
|
|
214
|
+
- **`impact_of_edit` gains a `precision` dial:** `standard` (default) follows the high-confidence
|
|
215
|
+
closure (`resolved` + `high`); **`strict`** follows **only `resolved`** edges — a
|
|
216
|
+
false-positive-free blast radius ("what *definitely* breaks"). Without a graph, `strict` follows
|
|
217
|
+
the `proven` query-time callers, so it degrades gracefully. Measured on the repo's own source:
|
|
218
|
+
of 325 `high` calls/refs edges, 303 are provably unambiguous (→ `resolved`), 22 remain heuristic
|
|
219
|
+
`high`; `name_only` is never promoted.
|
|
220
|
+
- **Additive and parity-free:** the resolver only changes the `confidence` STRING on edges that
|
|
221
|
+
already exist (never adds, drops, or reorders them), so `findCallers`/`findReferers`
|
|
222
|
+
set-equivalence holds and both backends round-trip the `resolved` tier identically. `resolved`
|
|
223
|
+
carries the same A5 centrality weight as `high`, so enabling it does not perturb centrality. The
|
|
224
|
+
symbol-graph dedupe now keeps the **strongest** confidence (`resolved > high > name_only`); on the
|
|
225
|
+
default path the values are unchanged so the dedupe is identical. New module `mcp/resolver.mjs`;
|
|
226
|
+
`classifyCallers` / `findReferences` gain an additive `proven` flag.
|
|
227
|
+
- **Honest scope:** v1 ships the unambiguous-binding tier the engine can already prove from
|
|
228
|
+
index-time signals. Detecting a local variable that **shadows** an import (and *refuting* that
|
|
229
|
+
edge), and cross-file precise resolution via stack-graphs, are the documented next layers — both
|
|
230
|
+
need per-language AST/scope analysis and an external toolchain that does not fit the air-gapped
|
|
231
|
+
default. `tree-sitter-stack-graphs` has no production Node binding, so a literal integration would
|
|
232
|
+
require a Rust CLI sidecar (rejected for v1). `test/edges.mjs` adds 5 tests; full write-up:
|
|
233
|
+
`docs/internals/IMPROVEMENT_PRECISE_RESOLVER.md`.
|
|
234
|
+
|
|
235
|
+
### Sealed mode (F1) — Phase 3
|
|
236
|
+
|
|
237
|
+
- **`--sealed [strict|local]`** (`INDEXER_SEALED`) turns graph-indexer's air-gapped default from a
|
|
238
|
+
behaviour you *trust* into a guarantee you can *verify and enforce*. Two tiers: **`strict`** =
|
|
239
|
+
zero network egress (lexical-only); **`local`** = loopback only (local Ollama/MLX on this box are
|
|
240
|
+
allowed, nothing leaves it). Bare `--sealed` = `strict`.
|
|
241
|
+
- **Fail-closed**: `resolveConfig` refuses to construct a sealed config whose enabled features would
|
|
242
|
+
egress beyond the tier — the build exits 2 with a message naming each offending feature
|
|
243
|
+
(embeddings/enrichment/rerank/hyde), its reach (loopback vs off-box), and the remedy. No silent
|
|
244
|
+
downgrade.
|
|
245
|
+
- **Runtime egress guard**: a deny-by-default hook on the in-process network paths (`net`/`tls`
|
|
246
|
+
sockets, `http(s)`, `fetch`) installed before any provider loads, so even an *accidental* call is
|
|
247
|
+
caught — turning "we don't call out" into "we *can't*." A best-effort `child_process` denylist
|
|
248
|
+
covers the obvious out-of-process egress tools (documented limitation: a child has its own socket
|
|
249
|
+
layer). `list_index_stats` reports `sealed` + `egress_guard: active`.
|
|
250
|
+
- **Attestation**: `idx-index --attest` prints a deterministic manifest (tier, providers, egress
|
|
251
|
+
posture, any egressing features) — an auditor/CI artifact for regulated/air-gapped deployments.
|
|
252
|
+
- **Opt-in, zero default impact**: `sealed: 'off'` default; sealing only *removes* capability
|
|
253
|
+
(fail-closed), never changes the store or ranking. The default lexical path is already
|
|
254
|
+
strict-compatible. New module `seal.mjs`; `test/seal.mjs` adds 11 tests (loopback classifier,
|
|
255
|
+
per-tier validation, guard block/permit/restore, child denylist, manifest, resolveConfig
|
|
256
|
+
fail-close). Design: `docs/internals/PHASE3_SEALED_MODE.md`.
|
|
257
|
+
|
|
258
|
+
### Taint analysis (C2) — Phase 3
|
|
259
|
+
|
|
260
|
+
- Two new **security** MCP tools (14 → 16): **`trace_taint`** traces untrusted data from a SOURCE
|
|
261
|
+
(request body/query/params, argv/env, stdin, socket/file reads) to a dangerous SINK (eval/exec,
|
|
262
|
+
SQL, fs/path, HTML, outbound request) across the call graph, returning the concrete source→sink
|
|
263
|
+
path tagged with category (rce/sqli/xss/path/ssrf) and confidence. **`find_tainted_sinks`** lists
|
|
264
|
+
every dangerous sink grouped by category, each flagged with whether an untrusted source reaches
|
|
265
|
+
it — the attack-surface map.
|
|
266
|
+
- **Confidence model**: a same-function source→sink is `high` (`direct`); a source function that
|
|
267
|
+
transitively calls a sink function is `medium` (`reachable`); a sanitizer
|
|
268
|
+
(escape/encode/parameterise/`Number()`/`int()`…) on the path drops it to `low`. Flows are
|
|
269
|
+
deterministically ordered by category severity, then confidence, then location.
|
|
270
|
+
- **Query-time, read-only, zero default cost**: implemented as on-demand tools (like
|
|
271
|
+
`get_call_graph`) — no index-time work, no serialization, **no parity surface**, the default index
|
|
272
|
+
is byte-identical, and the tools cost nothing unless invoked. **Air-gapped** (pure regex catalogs
|
|
273
|
+
+ call-graph traversal; no model, no network). Walks A4 `getEdges` when present, else `chunk.calls`.
|
|
274
|
+
- **Honest scope**: a **finder, not a verifier** (JS/TS + Python in v1). It favours precision but
|
|
275
|
+
misses flows through dynamic dispatch, reflection, ORM/query-builder indirection, and untyped
|
|
276
|
+
collections — every tool response says so, and "0 findings" ≠ "no vulnerabilities." New modules
|
|
277
|
+
`parse/taint-patterns.mjs`, `mcp/taint.mjs`; `test/taint.mjs` adds 9 tests; `test/mcp.mjs` asserts
|
|
278
|
+
the 16-tool surface. Design: `docs/internals/PHASE3_TAINT_ANALYSIS.md`.
|
|
279
|
+
|
|
280
|
+
### Learned re-ranker — opt-in, measured ≈neutral, RRF stays default (D3)
|
|
281
|
+
|
|
282
|
+
- **`--ranker learned` / `INDEXER_RANKER`** adds an opt-in, **zero-dependency** post-fusion
|
|
283
|
+
learning-to-rank layer: a small linear model re-orders the fused top-N using the RRF score plus the
|
|
284
|
+
program-structure signals the earlier phases produced — symbol **centrality** (A5), **resolved** /
|
|
285
|
+
SCIP **in-degree** (A4/A1) — and git recency. It is a *post-fusion* re-rank in the tool layer (like
|
|
286
|
+
the git boost / LLM reranker — never inside `db.searchHybrid`), so the **default RRF ordering is
|
|
287
|
+
byte-identical** and inference is a deterministic dot product (parity-safe). New module surface in
|
|
288
|
+
`search-core.mjs` (`learnedRerank`, `scoreLearned`, `extractRankerFeatures`,
|
|
289
|
+
`DEFAULT_RANKER_MODEL`); an offline trainer `bench/train-ranker.mjs` (zero-dep logistic regression,
|
|
290
|
+
**not shipped at runtime**); `test/ranker.mjs` (6 tests).
|
|
291
|
+
- **Honest measurement (the headline):** the trainer was run over all five eval fixtures built with
|
|
292
|
+
`--symbol-graph` (3 161 candidates, 410 positives). Held-out rank-1 (95 queries, grouped per
|
|
293
|
+
query) — **RRF-only 0.579 vs the data-fit model 0.579**: the learned model **ties** RRF, it does
|
|
294
|
+
not beat it. The structure features get near-zero learned weight because the correct symbol is
|
|
295
|
+
usually *not* the most central / most-called one. Per the honest-metrics + sacred-default
|
|
296
|
+
invariants, a tie is no reason to displace the simpler, faster default — **RRF remains the default
|
|
297
|
+
ranker.** What
|
|
298
|
+
ships is the *capability*: the shipped `DEFAULT_RANKER_MODEL` is conservative + RRF-dominant and
|
|
299
|
+
measures **byte-identical to RRF** on the benchmark (a safe, never-regressing opt-in), and the
|
|
300
|
+
trainer lets users fit a *repo-specific* model where their call structure does carry signal. Full
|
|
301
|
+
write-up: `docs/internals/IMPROVEMENT_LEARNED_RANKER.md`.
|
|
302
|
+
|
|
303
|
+
### Learned-sparse vocabulary expansion — opt-in, measured net-neutral, lexical stays default (B3)
|
|
304
|
+
|
|
305
|
+
- **`--learned-sparse` / `INDEXER_LEARNED_SPARSE`** adds an opt-in, **zero-dependency, air-gapped**
|
|
306
|
+
learned-sparse channel (SPLADE / uniCOIL family): a model expands an NL query into a *weighted term
|
|
307
|
+
set* — the literal terms plus learned associates — fused as an extra sparse channel via RRF, to
|
|
308
|
+
close lexical-gap misses the Porter-stemming bridge cannot (stemming normalises morphology; it
|
|
309
|
+
cannot connect `auth` → `credential`/`token`). Rather than a neural MLM (which needs a downloaded
|
|
310
|
+
model + `@huggingface/transformers` and can't be measured air-gapped), the shipped provider **learns
|
|
311
|
+
the associations FROM THE CORPUS** at index time via positive PMI over chunk co-occurrence — genuine
|
|
312
|
+
learned-sparse, zero new dependency, deterministic. New module `search-sparse.mjs`
|
|
313
|
+
(`buildSparseModel`, `expandSparseQuery`) + a shared `scoreSparseExpanded` in `search-core.mjs`; the
|
|
314
|
+
model serialises as an **isolated `sparse_model` artifact** (memory JSON key / single-row sqlite
|
|
315
|
+
table) so it cannot perturb the lexical postings; `test/sparse.mjs` (7 tests).
|
|
316
|
+
- **Presence-gated + NL-asymmetric + parity-by-construction.** A default index has no model → the
|
|
317
|
+
channel is inert → `test:eval` **byte-identical**. The channel fires only for natural-language
|
|
318
|
+
queries with a real expansion, so symbolic/exact lookups are byte-identical. Both backends score
|
|
319
|
+
through the *same* `scoreSparseExpanded` over identically-ordered terms → memory↔sqlite byte-identical.
|
|
320
|
+
- **Honest measurement (the headline):** in-process A/B over the five core eval suites, swept across
|
|
321
|
+
the fusion weight — **the learned-sparse channel does not beat the lexical baseline at any weight.**
|
|
322
|
+
Higher weights regress semantic s@5 (the co-occurrence associates add noise: axios semantic s@5
|
|
323
|
+
90.9→72.7, gin 83.3→33.3 at 0.5); the best case is ≈neutral at the shipped weight 0.2 (semantic +
|
|
324
|
+
symbolic rank-1 both exactly preserved). Like BM25F / AST / the code-embedding NL weight before it,
|
|
325
|
+
this measured net-neutral-to-negative, so **lexical stays the default and B3 is opt-in only.** What
|
|
326
|
+
ships is the air-gapped infrastructure + the seam for a neural SPLADE provider + the honest finding;
|
|
327
|
+
a repo with genuine query/code vocabulary mismatch (which the benchmark fixtures lack) may see the
|
|
328
|
+
recall win — *measure on your own repo*. Full write-up:
|
|
329
|
+
`docs/internals/IMPROVEMENT_LEARNED_SPARSE.md`.
|
|
330
|
+
|
|
331
|
+
### ColBERT-style late-interaction reranker — opt-in, measured helps-Go / neutral-JS (B4)
|
|
332
|
+
|
|
333
|
+
- **`--rerank-provider late-interaction`** adds a third reranker provider (sibling to the generative
|
|
334
|
+
judge and the B2 cross-encoder): the multi-vector **MaxSim** idea from ColBERT, deployed as a
|
|
335
|
+
reranker over the over-fetched pool. It keeps MULTIPLE vectors per side and scores
|
|
336
|
+
`Σ_qi max_dj cos(qi, dj)`, so each query sub-vector finds its best document sub-vector — the
|
|
337
|
+
compositional-query win single-vector pooling loses. New module `colbert.mjs` (`maxSimScore`,
|
|
338
|
+
`encodeMultiVector`, `loadChunkVectors`, `rerankLateInteraction`) + `collectVectorsByKey` in
|
|
339
|
+
`engine/binary.mjs` + `embeddingBinPath()` on both engines; `test/colbert.mjs` (7 tests).
|
|
340
|
+
- **No storage blow-up** (ColBERT's canonical cost): it reuses the document sub-vectors the engine
|
|
341
|
+
ALREADY stores (base + `|s` summary + `|wN` windows), reading a candidate's vectors from the shared
|
|
342
|
+
`.embeddings.bin` in one pass. **Air-gapped** (query multi-vectors from the same local embedder, no
|
|
343
|
+
new model/network), **parity-by-construction** (both backends read the same bin; MaxSim is
|
|
344
|
+
order-independent → identical re-order), and a post-retrieval re-order that **never mutates
|
|
345
|
+
`r.score`** and degrades to the original order on any shortfall → **default path byte-identical**.
|
|
346
|
+
- **Honest measurement (at production retrieval depth):** a reranker can only re-order the pool it is
|
|
347
|
+
handed, so the measurement uses production's over-fetch (pool 15 / topM 12; the harness default of
|
|
348
|
+
10/8 manufactured a *false* JS regression that vanishes at the right depth). On real Ollama indexes,
|
|
349
|
+
`gin` (Go) improves — semantic rank-1 **0.00 → 0.40**, overall **0.68 → 0.77**, with **no symbolic
|
|
350
|
+
regression** (0.88 → 0.88) — while `express-js` (JS) is **neutral** (every metric unchanged: overall
|
|
351
|
+
0.65, semantic rank-1 0.43, s@5 0.57). **At production depth B4 helps Go and is neutral on
|
|
352
|
+
JavaScript — the "helps Go/Python" half of the known reranker profile without the JS regression the
|
|
353
|
+
other rerankers show.** Small-n (gin semantic n=5), so directional. It still needs a query-time
|
|
354
|
+
model and does not uniformly win across languages, so it stays opt-in (default off), the trade-off
|
|
355
|
+
is printed at startup, and the default path is byte-identical. Requires `--embeddings` (a no-op
|
|
356
|
+
without it). Approximates true token-level ColBERT with a sentence embedder over query sub-units; a
|
|
357
|
+
token-level model is a clean future drop-in behind `encodeMultiVector`. Full write-up:
|
|
358
|
+
`docs/internals/IMPROVEMENT_LATE_INTERACTION.md`.
|
|
359
|
+
|
|
360
|
+
### Taint analysis hardening — index-time serialization + Java/Go (C2)
|
|
361
|
+
|
|
362
|
+
- **Opt-in index-time serialization (`--taint` / `INDEXER_TAINT`).** The taint flow set can now be
|
|
363
|
+
precomputed once at index time and **serialized into the index** (a `taint` table in SQLite, a
|
|
364
|
+
`taint` JSON key in memory) so `trace_taint` / `find_tainted_sinks` answer **instantly** instead of
|
|
365
|
+
walking the call graph per query — the flagship win for large, security-sensitive repos. Computed
|
|
366
|
+
against the symbol-graph edges when `--symbol-graph` is on (cross-function precision), else
|
|
367
|
+
`chunk.calls`. Like A4 edges / A5 centrality: **computed once → both backends serve byte-identical
|
|
368
|
+
flows** (`hasTaint()` / `getTaintFlows()` added to the store contract; an incremental file update
|
|
369
|
+
clears it; the tools fall back to a live scan when absent). `buildTaintGraph` gains a fast path
|
|
370
|
+
that serves cached flows filtered to the query's category/depth envelope (a deeper `max_depth` or
|
|
371
|
+
direct-only query transparently recomputes). **Default index byte-identical** (off → no
|
|
372
|
+
serialization, query-time as before); air-gapped; `list_index_stats` surfaces the flow count.
|
|
373
|
+
- **`trace_taint` / `find_tainted_sinks` now cover Java and Go** in addition to JS/TS and Python.
|
|
374
|
+
`parse/taint-patterns.mjs` gains `java` and `go` source / sink / sanitizer catalogs: Java —
|
|
375
|
+
`request.getParameter` / `@RequestParam` / `System.getenv` sources, `Runtime.exec` / `ProcessBuilder`
|
|
376
|
+
(rce), JDBC/JPA statement execute (sqli), servlet/JSP writers (xss), `File`/`Paths`/`Files` (path),
|
|
377
|
+
`URL`/`RestTemplate`/`HttpClient` (ssrf), with `PreparedStatement` / `URLEncoder` / `Integer.parseInt`
|
|
378
|
+
sanitizers; Go — `r.URL.Query()` / `r.FormValue` / gin·gorilla helpers / `os.Getenv` sources,
|
|
379
|
+
`exec.Command` (rce), `db.Query`/`Exec` (sqli, with a lookbehind so `r.URL.Query()` is not mistaken
|
|
380
|
+
for a DB call), response-writer / `template.HTML` (xss), `os.Open`/`filepath.Join` (path),
|
|
381
|
+
`http.Get`/`client.Do` (ssrf), with `strconv.Atoi` / `html.EscapeString` / `filepath.Clean`
|
|
382
|
+
sanitizers. `test/taint.mjs` adds Java + Go scan/flow/sanitizer tests plus serialization tests
|
|
383
|
+
(cache == live, fast-path category/depth filtering, memory↔sqlite parity, incremental-clear).
|
|
384
|
+
Design: `docs/internals/PHASE3_TAINT_ANALYSIS.md`.
|
|
385
|
+
|
|
386
|
+
### SCIP resolver provider (A2) — Phase 3
|
|
387
|
+
|
|
388
|
+
- **`--resolver scip` + `--scip-index <path>`** (`INDEXER_SCIP_INDEX` / `scipIndex` config)
|
|
389
|
+
completes the A1 precision story with the realistic, air-gapped cross-file backend. Where
|
|
390
|
+
`--resolver precise` only *relabels* edges the heuristic emitted, `scip` ingests a
|
|
391
|
+
**locally-generated SCIP index** (the user's own `scip-typescript` / `scip-python` /
|
|
392
|
+
`scip-java` / `rust-analyzer --scip` — produced out of band, on-box) and resolves edges by
|
|
393
|
+
**real cross-file bindings**: it promotes a confirmed `calls` edge to the `resolved` tier **and
|
|
394
|
+
suppresses the wrong-target fan-out** an ambiguous same-name reference would otherwise emit —
|
|
395
|
+
the disambiguation the index-time heuristic structurally cannot do. (Scoped to `calls` edges in
|
|
396
|
+
v1; `extends`/`type` edges stay heuristic because the binding relation is kind-agnostic.)
|
|
397
|
+
- **Zero new dependency.** The `.scip` protobuf is decoded by a **hand-rolled, zero-dependency**
|
|
398
|
+
wire reader (`parse/scip.mjs`) that walks only the four fields A2 needs; a SCIP-shaped JSON file
|
|
399
|
+
is also accepted. Occurrences align to chunks by 1-based line range (SCIP is 0-based; chunks
|
|
400
|
+
store no columns → line granularity). Air-gapped (a local file read only) — so **`--sealed
|
|
401
|
+
strict` + `--resolver scip` is a valid sealed, precisely-resolved index.**
|
|
402
|
+
- **Parity-free, default byte-identical, never-worse.** Like A1, it runs once at index time and
|
|
403
|
+
serializes one edge list into both backends (no per-backend code; memory↔sqlite identical). The
|
|
404
|
+
`resolveEdges` provider method exists only on the `scip` provider, so the default
|
|
405
|
+
`heuristic`/`precise` path is byte-identical. **Absence ≠ refutation, even under partial
|
|
406
|
+
coverage**: suppression requires that SCIP *actually recorded* the def it drops (`definedChunks`
|
|
407
|
+
gate), so a covered caller referencing an *uncovered* def (a `.ts`-only run that never saw a
|
|
408
|
+
`.js` def, a stale `.scip`) keeps the heuristic edge — never dropped. Coverage (matched docs,
|
|
409
|
+
bindings, resolved-tier count) is printed at index time; a missing/unreadable/0-match index warns
|
|
410
|
+
loudly and falls back to heuristic (the build never fails).
|
|
411
|
+
- **v2 — precise cross-file references for `find_references`.** The same binding relation is
|
|
412
|
+
inverted (`buildScipReferers`) into a precise *referenced-by* map and surfaced by
|
|
413
|
+
`find_references` (and `explain_symbol`) as a **`resolved`-tier reference dimension**. SCIP
|
|
414
|
+
occurrences are kind-agnostic ("symbol used here"), which is exactly what `find_references`
|
|
415
|
+
answers — so this single dimension both **disambiguates** same-named symbols (each referer is
|
|
416
|
+
attached to the one definition SCIP bound it to, not every same-named def the name heuristic
|
|
417
|
+
matches) and **recovers references the AST/name heuristic misses entirely** (the v1 "recall
|
|
418
|
+
concern" — including type usages in languages whose `type_refs` channel is empty). It is
|
|
419
|
+
serialized as an **isolated index artifact** (`scip_refs`), exactly like taint/centrality, so it
|
|
420
|
+
**cannot perturb the A4 edges or A5 centrality** and is parity-free; empty (and so default
|
|
421
|
+
byte-identical) on any index built without a SCIP resolver, and dropped on incremental update.
|
|
422
|
+
`list_index_stats` reports the count.
|
|
423
|
+
- **Module surface:** `parse/scip.mjs` (`loadScip`, `buildScipBindings`, `buildScipReferers`,
|
|
424
|
+
`normalizeScipPath`) + `createScipResolver` in `mcp/resolver.mjs`; the `scip_refs` artifact in
|
|
425
|
+
both engines (`hasScipRefs`/`getScipReferers`/`scipRefCount`). `test/scip.mjs` adds 15 tests
|
|
426
|
+
(protobuf round-trip, alignment, promote + suppress over a real ambiguous-name fixture,
|
|
427
|
+
**partial-coverage soundness**, **calls-only cross-kind isolation**, memory↔sqlite parity, plus
|
|
428
|
+
v2: inversion determinism, precise+attributed `find_references` dimension, references parity,
|
|
429
|
+
**sacred-default byte-identity**, incremental staleness). Write-up:
|
|
430
|
+
`docs/internals/IMPROVEMENT_SCIP_RESOLVER.md`.
|
|
431
|
+
|
|
432
|
+
### Sealed-mode attestation signing (F1 hardening)
|
|
433
|
+
|
|
434
|
+
- The sealed-mode attestation manifest can now be **cryptographically signed and verified** — turning
|
|
435
|
+
"here is my air-gapped posture" into a tamper-evident, auditor-verifiable statement. `seal.mjs`
|
|
436
|
+
adds `signManifest` (Ed25519 / RSA / EC) → a `{ manifest, signature: { alg, keyType,
|
|
437
|
+
publicKeyFingerprint, value } }` envelope, and `verifySignedManifest` → `{ valid, reason }` (never
|
|
438
|
+
throws). The signature is taken over a `canonicalJson` (recursively key-sorted) form, so it
|
|
439
|
+
survives a file round-trip and any future manifest key-order change.
|
|
440
|
+
- **CLI:** `idx-index --gen-attestation-key <prefix>` writes an Ed25519 keypair (`.key` private /
|
|
441
|
+
`.pub` public); `idx-index --attest --sign-key <path>` prints the signed envelope;
|
|
442
|
+
`idx-index --verify-attestation <file> --pub-key <path>` verifies the signature **and** reports
|
|
443
|
+
whether the attested manifest still matches the current effective config (policy drift → exit 3).
|
|
444
|
+
- **Zero dependency, air-gapped** (`node:crypto` only — no network, works under `--sealed strict`);
|
|
445
|
+
the default path is untouched (these are pre-indexing CLI actions that exit before any build).
|
|
446
|
+
`test/seal.mjs` adds 4 tests (canonical JSON, Ed25519 + RSA sign/verify, tamper/wrong-key/alg
|
|
447
|
+
rejection, JSON round-trip). Design: `docs/internals/PHASE3_SEALED_MODE.md`.
|
|
448
|
+
|
|
449
|
+
## [2.0.0] — 2026-06-21
|
|
450
|
+
|
|
451
|
+
This is a major release. The public API (MCP tools, CLI flags, config keys) has grown significantly, but the default behaviour — lexical-only search, in-memory store, zero external dependencies — is backward-compatible. Internal modules were reorganized into `engine/`, `mcp/`, and `parse/` (see [Module reorganization](#module-reorganization-breaking-only-for-deep-imports) below) — breaking only for code that deep-imported internal files.
|
|
452
|
+
|
|
453
|
+
### Onboarding (`graph-indexer init`) — environment-agnostic setup
|
|
454
|
+
|
|
455
|
+
The first-run setup now produces a working configuration in **any** repository, not just a Node repo that already has graph-indexer installed.
|
|
456
|
+
|
|
457
|
+
- **Self-contained MCP launch command.** The wired command is now decided per-project. When graph-indexer is resolvable as a local dependency, the wiring keeps using `npm run mcp:start`. Otherwise — **non-Node repos** (Python, Go, Rust, …) and **npx-only Node repos** — it wires a self-contained `npx -y -p graph-indexer idx-mcp --repo <abs path>` that launches the real server regardless of ecosystem. The same decision drives the npm scripts (wrapped in `npx -p` when not a local dep, so they aren't broken) and the global Claude Desktop config.
|
|
458
|
+
- **Subcommand dispatch.** `graph-indexer <idx-mcp|idx-index|idx-watch|idx-daemon> …` now delegates to the real bin (previously these tokens were swallowed by the init wizard, because `npx <pkg> <x>` runs the package's same-named bin, not the `<x>` bin). This makes the documented `npx -y graph-indexer idx-mcp` form work too.
|
|
459
|
+
- **Target a repo + CI flags.** `init` now honors a positional path and `--repo <path>` (resolved to an absolute root used everywhere), adds `--help`, and adds `--yes` / `--non-interactive` (a non-TTY stdin already implied this) for scripted/CI runs.
|
|
460
|
+
- **Claude Code MCP target.** Project-scoped servers are now written to `.mcp.json` (the file Claude Code reads for project MCP servers) instead of `.claude/settings.json`.
|
|
461
|
+
- **Claude Desktop on Linux.** Added the `~/.config/Claude` path (honoring `$XDG_CONFIG_HOME`); Linux users with Claude Desktop installed now get wired instead of reported "not installed".
|
|
462
|
+
- **Node version guard.** Selecting (or auto-resolving on a large repo to) the SQLite backend on Node < 22 now surfaces a clear warning in the summary instead of failing later at runtime.
|
|
463
|
+
- **Faster default path + ready to use.** The engine step opens with a single "Use recommended defaults?" confirm that skips the ~10 follow-up prompts, and the run ends by offering to build the index so the flow finishes genuinely ready to use. Editor wiring now distinguishes "detected" from "ready if you use it" in the summary.
|
|
464
|
+
|
|
465
|
+
Idempotency, merge-safe config writes, air-gapped defaults, and `--dry-run` are all preserved.
|
|
466
|
+
|
|
467
|
+
### New MCP tools
|
|
468
|
+
|
|
469
|
+
| Tool | Purpose |
|
|
470
|
+
|------|---------|
|
|
471
|
+
| `find_references` | Where a symbol is used: callers, subclasses, and type-annotation references, split into high-confidence vs name-only blast radius. |
|
|
472
|
+
| `find_routes` | HTTP routes mapped to their handler chunks — NestJS, Express/Koa, FastAPI/Flask, and Spring (Java); the controller/router prefix is joined onto the method path. Attribute-routed C#/ASP.NET, PHP (Laravel/Symfony), Rails, and Django URLconf are out of scope. |
|
|
473
|
+
| `get_subgraph` | Bounded connected subgraph (callers + callees + type/inheritance referers) around a seed symbol, in one call. Replaces multi-hop `search_code → get_call_graph → find_references` round-trips. |
|
|
474
|
+
|
|
475
|
+
HTTP route extraction (`parse/routes.mjs`) parses route definitions from the AST so `find_routes` can resolve an endpoint straight to its handler chunk; coverage is per-framework as noted above.
|
|
476
|
+
|
|
477
|
+
**All tools** now support a `response_format: 'json'` parameter that returns typed structured fields instead of markdown prose, for programmatic clients.
|
|
478
|
+
|
|
479
|
+
All tool responses carry an **index-freshness signal** (age, commit it was built at, pending uncommitted changes). The freshness footer is omitted on up-to-date indexes to keep card sizes lean.
|
|
480
|
+
|
|
481
|
+
### New language support
|
|
482
|
+
|
|
483
|
+
Added three languages, bringing the total to 14:
|
|
484
|
+
|
|
485
|
+
- **C** (`.c`, `.h`) — struct/function/declaration extraction with declarator name descender
|
|
486
|
+
- **Bash** (`.sh`, `.bash`) — command and function extraction with built-ins filter
|
|
487
|
+
- **Swift** (`.swift`) — class/struct/enum/function extraction
|
|
488
|
+
|
|
489
|
+
Existing language support: TypeScript/JavaScript, Python, Go, Rust, Java, Kotlin, C#, Ruby, PHP, CSS/SCSS.
|
|
490
|
+
|
|
491
|
+
### Prompt suite (new `prompts/` directory)
|
|
492
|
+
|
|
493
|
+
Replaced the single `PROMPT.md` with a structured, composable **3-layer prompt suite**:
|
|
494
|
+
|
|
495
|
+
- **`prompts/CORE.md`** — universal rules (4-call budget, batch-don't-iterate, consume-before-call, rule-of-one). Always required.
|
|
496
|
+
- **`prompts/languages/`** — 13 language-specific overlays: BASH, C, C#, CSS/SCSS, Go, Java, JavaScript/TypeScript, Kotlin, PHP, Python, Ruby, Rust, Swift
|
|
497
|
+
- **`prompts/frameworks/`** — 8 framework-specific overlays: Android, ASP.NET Core, FastAPI/Django, Laravel/Symfony, Node/Express/NestJS, Rails, React, Spring Boot
|
|
498
|
+
- **`prompts/DOMAIN_TEMPLATE.md`** — a copy-and-fill template for project-specific rules
|
|
499
|
+
- **`prompts/INTEGRATION.md`** — guide explaining the 3-layer architecture and how to compose the layers
|
|
500
|
+
|
|
501
|
+
### Embedding provider abstraction (`embeddings.mjs`)
|
|
502
|
+
|
|
503
|
+
New module with an `auto` provider selection policy:
|
|
504
|
+
|
|
505
|
+
1. Ollama (user's running daemon, highest quality)
|
|
506
|
+
2. In-process local model via the optional `@huggingface/transformers` dependency (`Xenova/all-MiniLM-L6-v2` by default) — runs with no external daemon
|
|
507
|
+
3. Lexical-only fallback (logged, never silent)
|
|
508
|
+
|
|
509
|
+
Embeddings remain **off by default**. The index stamps the resolved `{ provider, model, dim }` into its metadata; the server reads it back on load so index time and query time always use the same model.
|
|
510
|
+
|
|
511
|
+
New `--embed-provider <auto|ollama|local|off>` flag and `INDEXER_EMBED_PROVIDER` env variable to override provider selection.
|
|
512
|
+
|
|
513
|
+
New `mlx` provider uses a dedicated Python virtualenv (`embedders/venv-mlx/`) and the `mlx_embeddings` library to embed via the Apple Metal GPU as a persistent subprocess — no Ollama daemon required. Measured throughput on an **Apple M2 Mac mini (24 GB)**, `express-js` fixture, median of 3 cold builds: `mlx` **~42 ch/s** vs in-process Xenova **~18 ch/s** vs Ollama/nomic **~14 ch/s** (throughput is hardware-dependent — expect different figures on other chips). Note: on Apple Silicon, Ollama also uses the Metal GPU internally; the `mlx` provider's advantage is a smaller 4-bit model and no HTTP round-trip, not GPU vs CPU.
|
|
514
|
+
|
|
515
|
+
**Bug fix:** `indexer.mjs` (and the eval harness) now calls `_resetSubprocesses()` after encoding completes so the MLX Python subprocess is killed and the Node.js event loop can drain. Previously `npx idx-index --embed-provider mlx` would hang indefinitely after building a correct index.
|
|
516
|
+
|
|
517
|
+
**Setup & onboarding.** Install the MLX virtualenv with `npm run embed:setup:mlx` (`node embedders/setup-mlx.mjs`), or accept it during `graph-indexer init` when you select the MLX provider (onboarding also lets you choose the MLX embed model, default `mlx-community/all-MiniLM-L6-v2-4bit`, 384-dim). New MLX implementation files: `embedders/setup-mlx.mjs` (venv installer), `embedders/python/mlx_embed_server.py` (persistent MLX subprocess server), `embedders/python/requirements-mlx.txt`, `embedders/python/test_servers.mjs`.
|
|
518
|
+
|
|
519
|
+
**Graceful per-batch degradation.** Embedding generation no longer aborts the whole index when a batch fails or times out — those chunks are indexed lexical-only and retried on the next run, with a per-batch warning and an end-of-run summary of how many batches were skipped.
|
|
520
|
+
|
|
521
|
+
### Git signals (`git-signals.mjs`)
|
|
522
|
+
|
|
523
|
+
New module that computes three per-file ranking signals from the local git log (no network, no remote):
|
|
524
|
+
|
|
525
|
+
- **churn** — commit count per file
|
|
526
|
+
- **recency** — time since last commit
|
|
527
|
+
- **co-change** — files that historically change together
|
|
528
|
+
|
|
529
|
+
Signals are collected once at index time into a sidecar file (`code-index.git.json`). The co-change hint is surfaced in tool blast-radius responses. The recency/churn ranking weight is **off by default** (`gitRankBoost: 0`) — enable with `--git-rank-boost <0..1>` or `INDEXER_GIT_RANK_BOOST`.
|
|
530
|
+
|
|
531
|
+
### Daemon control CLI (`daemon-ctl.mjs` / `idx-daemon`)
|
|
532
|
+
|
|
533
|
+
New `idx-daemon` binary with subcommands:
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
idx-daemon start Start the watch daemon (no-op if already running)
|
|
537
|
+
idx-daemon stop Gracefully stop it
|
|
538
|
+
idx-daemon restart Stop then start
|
|
539
|
+
idx-daemon status Show daemon + index state (default)
|
|
540
|
+
idx-daemon logs [-f] Print recent daemon logs
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
New `daemon-lock.mjs` ensures exactly one daemon runs per project — `start` is safe to call repeatedly. New npm scripts: `mcp:daemon`, `mcp:daemon:start`, `mcp:daemon:stop`, `mcp:daemon:restart`, `mcp:daemon:status`, `mcp:daemon:logs`.
|
|
544
|
+
|
|
545
|
+
### Directory layout (`layout.mjs`)
|
|
546
|
+
|
|
547
|
+
New `layout.mjs` is the single source of truth for on-disk paths. All machine-generated runtime files (index, vectors, SQLite database, enrichment cache, daemon PID/log, resolved config) now live under `.graph-indexer/` at the project root instead of being scattered at the root level. `migrateLegacyLayout()` relocates pre-v1.4 artifacts transparently on next run. The config file is `config.json` inside that directory; the legacy `.graph-indexer.json` root file is still read as a fallback for backward compatibility.
|
|
548
|
+
|
|
549
|
+
### CLI UI (`cli-ui.mjs`)
|
|
550
|
+
|
|
551
|
+
New dependency-free shared console styling module for CLI utilities (init, daemon control). Provides ANSI colour with graceful degradation when stdout is not a TTY or `NO_COLOR` is set, plus boxed banners, rules, and status glyphs for a consistent look.
|
|
552
|
+
|
|
553
|
+
### Receiver-aware call graph
|
|
554
|
+
|
|
555
|
+
`get_call_graph` now uses receiver hints captured by the parser (`this.X()`, `Receiver.X()`, free-function `X()`) to classify callers as **high-confidence** (the real blast radius) vs **name-only** (callers of a same-named symbol elsewhere). Eliminates false positives on methods with common names (e.g., `save`, `find`, `validate`).
|
|
556
|
+
|
|
557
|
+
### Lexical search improvements
|
|
558
|
+
|
|
559
|
+
- **Porter stemming** — language-agnostic Porter stemmer (steps 1–5 plus an agent-noun `-or` rule for code) bridging vocabulary gaps: `"intercepting"` ↔ `Interceptor`, `"managing"` ↔ `Manager`, `"injection"` ↔ `Injectable`. Applied **additively** (raw token always emitted alongside the stem) so exact matches, IDF statistics, and name boosts are byte-for-byte unchanged.
|
|
560
|
+
- **IDF-gated path boost** — for NL queries, the path-segment boost is restricted to file-path terms that are rare in the corpus (high IDF). Symbol/keyword queries keep the original path boost unchanged — the IDF gate, not the boost itself, is NL-only.
|
|
561
|
+
- **NL-adaptive vector weight** — the semantic channel weight is `0.4` for plain lexical text and `0.6` for LLM-enriched chunks (the joint optimum across the eval suites), so the embedding signal matters most where the index has a rich semantic representation.
|
|
562
|
+
- **Dense-channel window sub-chunking** — oversized definitions (beyond the single-vector context limit) are embedded as up to 4 overlapping windows and retrieved by the maximum cosine across windows (max-sim), recovering tail recall that single-vector truncation lost. Window 0 is byte-identical to the prior single vector, so existing head vectors are unchanged; extra windows are stored under `<key>|w1..|wN` keys in the embeddings binary.
|
|
563
|
+
|
|
564
|
+
### LLM rerank: over-fetch + pool rescue
|
|
565
|
+
|
|
566
|
+
When `rerank` is enabled, the server now **over-fetches** a deeper candidate pool (configurable `poolSize`, default 15) and lets the LLM judge reorder it before truncating to `top_k`. This lets the reranker rescue a correct-but-deep result that would never reach the agent otherwise.
|
|
567
|
+
|
|
568
|
+
### HyDE (Hypothetical Document Embedding)
|
|
569
|
+
|
|
570
|
+
New opt-in query-side technique: generate a hypothetical code snippet for the NL query, embed it, and blend with the query vector to bridge vocabulary gaps. Off by default. Enable with `hyde: { enabled: true }` in config or the `hyde` parameter in `search_code`. The HyDE prompt is **language-aware** — the repo's dominant language (detected from extension counts and chunk metadata) selects a per-language hypothetical-snippet template, with a generic, never-regressing fallback when the language is unknown.
|
|
571
|
+
|
|
572
|
+
### Configuration overhaul
|
|
573
|
+
|
|
574
|
+
Default values changed:
|
|
575
|
+
|
|
576
|
+
| Key | Old default | New default | Reason |
|
|
577
|
+
|-----|-------------|-------------|--------|
|
|
578
|
+
| `storage` | `'memory'` | `'auto'` | Auto-selects SQLite above ~15k chunks |
|
|
579
|
+
| `embeddings` | (on unless `INDEXER_EMBEDDINGS=off`) | `false` | Explicit opt-in, lexical-first default |
|
|
580
|
+
| `embedProvider` | (none — Ollama was the only path) | `'auto'` | Falls back to local model, then lexical |
|
|
581
|
+
| `embedModel` | `'nomic-embed-text'` | `'nomic-embed-text'` | Unchanged |
|
|
582
|
+
| `rerank.topM` | `8` | `12` | Wider rerank pool |
|
|
583
|
+
| `rerank.poolSize` | (none) | `15` | New over-fetch parameter |
|
|
584
|
+
|
|
585
|
+
CLI flag rename: `--llm-enrichment` (and `--enrich`) → `--enrichment`. The old forms are no longer accepted.
|
|
586
|
+
|
|
587
|
+
Startup logging now prints the **effective configuration** (storage backend, model names, which optional features are active) and emits a warning for any opt-in feature with a known trade-off.
|
|
588
|
+
|
|
589
|
+
New embedding/enrichment flags and environment variables:
|
|
590
|
+
|
|
591
|
+
- `--mlx-embed-model` / `INDEXER_MLX_EMBED_MODEL` — MLX model id (default `mlx-community/all-MiniLM-L6-v2-4bit`).
|
|
592
|
+
- `EMBED_MODEL` — env alias for `--embed-model`.
|
|
593
|
+
- `ENRICH_MODEL` — env that enables enrichment and names its model. Setting `enrichment.model: 'auto'` probes the local Ollama and picks the strongest available code model (preference ladder `qwen2.5-coder:7b → 3b → deepseek-coder-v2 → 1.5b`), falling back to the `qwen2.5-coder:1.5b` floor — best-effort and non-throwing.
|
|
594
|
+
- `INDEXER_EMBED_CONCURRENCY` (parallel embed batches, default `4`), `INDEXER_EMBED_TIMEOUT_MS` (per-batch timeout, default `120000`), `INDEXER_MLX_BATCH_SIZE` (default `32`), `INDEXER_EMBED_STARTUP_TIMEOUT_MS` (default `120000`).
|
|
595
|
+
|
|
596
|
+
Fixed the Ollama endpoint in the embeddings test scripts from the non-default `:11435` to Ollama's real default `:11434` (`OLLAMA_HOST`).
|
|
597
|
+
|
|
598
|
+
### Symbol references (`find_references`)
|
|
599
|
+
|
|
600
|
+
`find_references` (`findReferences` in `mcp/topology.mjs`) fuses three reference kinds, combining `classifyCallers` (calls) with the new `db.findReferers` storage method (which returns the non-call referers — `extends` and `type_refs` matches):
|
|
601
|
+
|
|
602
|
+
- **calls** — `findCallers` + high/name-only classification
|
|
603
|
+
- **inherits** — chunks whose `extends` names the target (subclasses / implementers)
|
|
604
|
+
- **types** — chunks whose `type_refs` names the target (params, returns, fields)
|
|
605
|
+
|
|
606
|
+
Heritage is indexed for TS/JS, Python, Java, C#, Kotlin, Swift, Ruby, PHP, Rust, and Go (Go via struct/interface embedding rather than an inheritance keyword). Type-annotation users for TS/JS, Python, Java, C#, Kotlin, Swift, PHP, Rust, Go, and C (C# is field-precise: params, fields, properties, returns, base list).
|
|
607
|
+
|
|
608
|
+
### Symlink path traversal hardening
|
|
609
|
+
|
|
610
|
+
`get_file_skeleton` now resolves symlinks with `realpath` on both the project root and the requested target, then re-verifies containment. A symlink inside the project pointing outside it can no longer escape the sandbox.
|
|
611
|
+
|
|
612
|
+
### Benchmarking harness (`bench/`)
|
|
613
|
+
|
|
614
|
+
New reproducible benchmarking suite:
|
|
615
|
+
|
|
616
|
+
- `bench/cell.mjs` — cold-build per config, runs one fixture × one config combination
|
|
617
|
+
- `bench/configs.mjs` — config matrix definitions
|
|
618
|
+
- `bench/synth.mjs` / `bench/synth-agent.mjs` / `bench/synth-best.mjs` — report generators (cross-language, agent-facing, per-fixture best-config)
|
|
619
|
+
- `bench/tokens.mjs` — token-savings measurement (drives the token-savings tables below)
|
|
620
|
+
- `bench/repeat-score.mjs`, `bench/query.mjs`, `bench/dump-chunks.mjs`, `bench/fixtures-doc.mjs`, `bench/_confidence.mjs` — scoring, query, and reporting helpers
|
|
621
|
+
- `bench/verify-suite.mjs` / `bench/verify-ground-truth.mjs` / `bench/verify-structural.mjs` — fabrication guards: ensure query answers actually exist in the index
|
|
622
|
+
- `bench/structural.mjs` — structural fixture verifier
|
|
623
|
+
- `bench/parity.mjs` — byte-identical parity check between backends
|
|
624
|
+
- `bench/provenance.mjs` / `bench/provenance.json` — query provenance tracking
|
|
625
|
+
- Shell runners: `run-all.sh`, `run-costly.sh`, `run-final.sh`, `run-focused.sh`, `run-list.sh` (plus internal `_rerun-v2.sh`)
|
|
626
|
+
|
|
627
|
+
### Test suite expansion
|
|
628
|
+
|
|
629
|
+
New test files:
|
|
630
|
+
|
|
631
|
+
| File | What it covers |
|
|
632
|
+
|------|----------------|
|
|
633
|
+
| `test/callgraph.mjs` | Receiver-aware call graph, `classifyCallers` |
|
|
634
|
+
| `test/references.mjs` | `find_references` / `findReferers` |
|
|
635
|
+
| `test/routes.mjs` | `find_routes` / HTTP route extraction per framework |
|
|
636
|
+
| `test/json-output.mjs` | `response_format: 'json'` across all tools |
|
|
637
|
+
| `test/git-signals.mjs` | Git churn/recency/co-change sidecar |
|
|
638
|
+
| `test/security.mjs` | Path-traversal / symlink escape guard |
|
|
639
|
+
| `test/embeddings.mjs` | Embedding provider abstraction |
|
|
640
|
+
| `test/languages.mjs` | Per-language parsing (all 14 languages) |
|
|
641
|
+
|
|
642
|
+
(`test/unit.mjs` and `test/metrics.mjs` already existed in v1.2.0 and were extended, not added.)
|
|
643
|
+
|
|
644
|
+
New test suites for additional frameworks/languages: `alamofire`, `android`, `aspnet`, `cjson`, `css`, `django`, `laravel`, `nvm`, `rails`, `react`, `rust`, `spring`, `symfony`. (The `axios`, `express-js`, `fastapi`, `gin`, and `nestjs` suites already shipped in v1.2.0 and had their query sets expanded.)
|
|
645
|
+
|
|
646
|
+
New npm test scripts: `test:callgraph`, `test:references`, `test:routes`, `test:jsonout`, `test:gitsignals`, `test:security`, `test:embed`, `test:languages`.
|
|
647
|
+
|
|
648
|
+
### Agent benchmark harness (`test/agent/`)
|
|
649
|
+
|
|
650
|
+
New end-to-end agent benchmark that drives real MCP tools and traces an agent's call chain:
|
|
651
|
+
|
|
652
|
+
- `test/agent/agent-cli.mjs` — CLI entry point
|
|
653
|
+
- `test/agent/benchmark.config.mjs` — fixture and config registry
|
|
654
|
+
- `test/agent/fixtures.manifest.mjs` / `test/agent/setup-fixtures.mjs` — fixture manifest and setup
|
|
655
|
+
- `test/agent/search-cases.mjs` — search case definitions
|
|
656
|
+
- `test/agent/search-eval.mjs` — search evaluation with MRR/success@k scoring
|
|
657
|
+
- `test/agent/parse-eval.mjs` / `test/agent/score-answers.mjs` / `test/agent/se-mrr.mjs` — eval parsing and scoring
|
|
658
|
+
- `test/agent/analyze.mjs` / `assemble.mjs` — analysis pipeline
|
|
659
|
+
- `test/agent/tool-bridge.mjs` — bridges the agent to the MCP server under test
|
|
660
|
+
|
|
661
|
+
### Documentation
|
|
662
|
+
|
|
663
|
+
- **README.md** — rewritten to roughly a third its previous length (~700 → ~310 lines). Focused on the quick start, MCP tool reference, configuration trade-offs table, CLI flags, and environment variables. Benchmark numbers verifiable via `npm run test:eval`.
|
|
664
|
+
- **`docs/benchmarks/`** — new directory with detailed benchmark reports: `BENCH_BASELINE.md`, `BENCH_FULL_SUITE.md`, `BENCH_LANGUAGES.md`, `BENCH_SUMMARY.md`, `BENCH_AGENT.md`, `BENCH_PER_FIXTURE.md`, `FIXTURES.md`.
|
|
665
|
+
- **`docs/internals/IMPROVEMENT_STEMMING.md`** — internal design note for the Porter stemming bridge.
|
|
666
|
+
- **SECURITY.md** — updated to document the git-signals subprocess, the one-time model-weight download for the local embedding provider, and the strengthened symlink path guard.
|
|
667
|
+
|
|
668
|
+
### CI
|
|
669
|
+
|
|
670
|
+
- `.github/workflows/ci.yml` — verifies all 14 languages are present in `parse/languages.mjs`, checks the `.graph-indexer/` data directory is created (not the legacy root layout), validates that `prompts/CORE.md` and `prompts/INTEGRATION.md` are present, and runs a package-integrity step that `npm pack`s the tarball and asserts the `engine/`, `mcp/`, and `parse/` subdir modules ship and every entrypoint resolves its imports from the package.
|
|
671
|
+
- Removed `.github/workflows/publish-with-provenance.yml` (deprecated).
|
|
672
|
+
|
|
673
|
+
### Dependencies
|
|
674
|
+
|
|
675
|
+
New optional and production dependencies:
|
|
676
|
+
|
|
677
|
+
| Package | Version | Type | Purpose |
|
|
678
|
+
|---------|---------|------|---------|
|
|
679
|
+
| `tree-sitter-bash` | 0.23.3 | optional | Bash language parser |
|
|
680
|
+
| `tree-sitter-c` | 0.21.4 | optional | C language parser |
|
|
681
|
+
| `tree-sitter-swift` | 0.5.0 | optional | Swift language parser |
|
|
682
|
+
| `tree-sitter-scss` | 1.0.0 | optional | SCSS language parser |
|
|
683
|
+
| `@huggingface/transformers` | 3.8.1 | optional | In-process local embedding model |
|
|
684
|
+
|
|
685
|
+
(`hnswlib-node` 3.0.0, the optional approximate-nearest-neighbour vector index, already shipped in v1.2.0 — it is not new in v2.0.0.)
|
|
686
|
+
|
|
687
|
+
### Module reorganization (breaking only for deep imports)
|
|
688
|
+
|
|
689
|
+
Internal source files were grouped into directories. The CLI bins, the MCP server, and the default behaviour are unchanged; only code that imported a specific internal file path is affected. The package entry point (`import 'graph-indexer'`) now resolves to `engine/memory.mjs`.
|
|
690
|
+
|
|
691
|
+
| Old path | New path |
|
|
692
|
+
|----------|----------|
|
|
693
|
+
| `core-engine.mjs` | `engine/memory.mjs` |
|
|
694
|
+
| `sqlite-store.mjs` | `engine/sqlite.mjs` |
|
|
695
|
+
| `mcp-tools.mjs` | `mcp/tools.mjs` |
|
|
696
|
+
| `parser-utils.mjs` | split into `parse/extractor.mjs`, `parse/imports.mjs`, `parse/languages.mjs`, `parse/metadata.mjs`, `parse/routes.mjs` |
|
|
697
|
+
|
|
698
|
+
New modules added in the reorg: `engine/binary.mjs` (embedding binary codec + vector index), `mcp/format.mjs`, `mcp/topology.mjs`, `embeddings.mjs`, `git-signals.mjs`, `layout.mjs`, `daemon-ctl.mjs`, `daemon-lock.mjs`, `cli-ui.mjs`. (`config.mjs` and `storage.mjs` already existed in v1.2.0 and were extended, not added.)
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### Metrics
|
|
703
|
+
|
|
704
|
+
#### Honesty note
|
|
705
|
+
|
|
706
|
+
v1.2.0 reported benchmark numbers using the enrichment + rerank path (the "best" configuration). v2.0.0 adopts a lexical-first honest baseline: the default path is lexical-only (zero dependencies), and opt-in configurations (embeddings, rerank, enrichment) are measured separately.
|
|
707
|
+
|
|
708
|
+
#### 5-suite benchmark (axios, express, NestJS, FastAPI, gin — 100 queries: 69 symbolic + 31 semantic)
|
|
709
|
+
|
|
710
|
+
This is the primary eval harness (`npm run test:eval`). All numbers are strict symbol-level (a hit requires the exact symbol, not just the correct file).
|
|
711
|
+
|
|
712
|
+
| Channel | s@1 | s@5 | MRR | Semantic rank-1 | Semantic s@5 | File-only inflation |
|
|
713
|
+
|---|---|---|---|---|---|---|
|
|
714
|
+
| **v1.2.0 — Hybrid + enrichment + rerank (best path)** | — | **0.82** | 0.73 | 0.35 | 0.65 | ~11.6% |
|
|
715
|
+
| **v2.0.0 — Lexical default (+ stemming + IDF path boost)** | 0.58 | **0.81** | 0.65 | 0.19 | **0.61** | ~11.6% |
|
|
716
|
+
| v2.0.0 — Hybrid nomic, no rerank | 0.60 | 0.79 | 0.68 | 0.23 | 0.58 | ~11.6% |
|
|
717
|
+
| v2.0.0 — Hybrid nomic + LLM rerank | 0.64 | 0.80 | 0.71 | 0.35 | 0.61 | ~11.6% |
|
|
718
|
+
|
|
719
|
+
Key changes vs v1.2.0:
|
|
720
|
+
|
|
721
|
+
- **Lexical s@5 reaches 0.81** (within 1 point of v1.2.0's best path of 0.82) with zero external dependencies, driven by Porter stemming and the IDF-gated path boost.
|
|
722
|
+
- **Semantic s@5 (lexical):** 0.48 floor → **0.61** from stemming + the IDF-gated path boost combined (+13 points, +27%); the path boost alone contributes +0.097 (0.516 → 0.613).
|
|
723
|
+
- **Overall s@5 (lexical):** 0.775 → **0.807** after IDF-gated path boost (+0.032, ≈+3 points).
|
|
724
|
+
- **Symbolic rank-1 unchanged** at 0.75 (byte-identical — stemming and path boost are additive and non-destructive to exact-match ranking).
|
|
725
|
+
- **File-only inflation corrected:** the v1.2.0 `0.1%` figure was a 100× display-formatting bug (`fmtPct` rendered a 0–1 fraction without the ×100 scale); the true lexical value is **~11.6%** overall, while the held-out validation set has **0.0%** inflation. The hybrid path does not increase inflation over lexical.
|
|
726
|
+
- **Backend parity:** memory vs SQLite results are byte-identical on all 100 queries (enforced in CI).
|
|
727
|
+
|
|
728
|
+
#### Stemming: held-out semantic recall
|
|
729
|
+
|
|
730
|
+
Porter stemming was validated on the **held-out query set** (15 queries, never used for tuning):
|
|
731
|
+
|
|
732
|
+
| Metric | Before stemming | After stemming | Δ |
|
|
733
|
+
|---|---|---|---|
|
|
734
|
+
| Held-out rank-1 (overall) | 0.733 | **0.800** | **+0.067** |
|
|
735
|
+
| Held-out semantic rank-1 | 0.200 | **0.400** | **+0.20 (+100%)** |
|
|
736
|
+
| Held-out MRR (overall) | 0.789 | **0.833** | +0.044 |
|
|
737
|
+
| Symbolic rank-1 / MRR (strict, tuning) | 0.755 / 0.807 | 0.755 / 0.807 | 0 (byte-identical) |
|
|
738
|
+
|
|
739
|
+
#### IDF-gated path boost delta (lexical, tuning channel)
|
|
740
|
+
|
|
741
|
+
| Metric | Before | After | Δ |
|
|
742
|
+
|---|---|---|---|
|
|
743
|
+
| Overall s@5 | 0.7749 | **0.8065** | **+0.0316** |
|
|
744
|
+
| Semantic s@5 (31q) | 0.5161 | **0.6129** | **+0.0968** |
|
|
745
|
+
| Semantic rank-1 | 0.1935 | 0.1935 | 0 |
|
|
746
|
+
| Symbolic rank-1 | 0.7536 | 0.7536 | 0 (byte-identical) |
|
|
747
|
+
|
|
748
|
+
#### Embedding quality: qwen3-embedding:4b (express suite, 21 queries)
|
|
749
|
+
|
|
750
|
+
| Channel | Semantic rank-1 | Semantic s@5 | Symbolic rank-1 |
|
|
751
|
+
|---|---|---|---|
|
|
752
|
+
| Lexical floor | 0.43 | 0.57 | 0.79 |
|
|
753
|
+
| nomic-embed-text (768-dim) | 0.43 | 0.57 | 0.79 |
|
|
754
|
+
| **qwen3-embedding:4b (2560-dim)** | 0.43 | **0.71** | 0.79 |
|
|
755
|
+
|
|
756
|
+
qwen3-embedding:4b raises express semantic s@5 from 0.57 to 0.71 (+24%) while holding rank-1 and symbolic precision. Cost: qwen3 4b is **~23× slower** than nomic-embed-text. The embedding channel is **off by default**; enable with `--embed-provider ollama --embed-model qwen3-embedding:4b` or the equivalent config.
|
|
757
|
+
|
|
758
|
+
#### Enrichment + HyDE (gin suite, semantic, opt-in)
|
|
759
|
+
|
|
760
|
+
| Config | Semantic rank-1 | Semantic MRR | Semantic s@5 |
|
|
761
|
+
|---|---|---|---|
|
|
762
|
+
| Lexical floor | 0.20 | 0.30 | 0.60 |
|
|
763
|
+
| Plain + rerank (7B) | 0.60 | 0.80 | 1.00 |
|
|
764
|
+
| Enriched (7B) + rerank | **0.80** | **0.87** | 1.00 |
|
|
765
|
+
| HyDE (qwen2.5-coder:7B), enriched corpus | **0.00** | 0.34 | **1.00** |
|
|
766
|
+
|
|
767
|
+
Key finding: enrichment **inverts** depending on embedder strength — it helps with weak embedders (MiniLM) but **regresses** with strong ones (qwen3). Enrichment and rerank remain correctly off by default.
|
|
768
|
+
|
|
769
|
+
#### Cross-language benchmark (18 fixtures, new in v2.0.0)
|
|
770
|
+
|
|
771
|
+
A new reproducible 18-language × 8-config matrix (`bench/`), covering all 14 supported languages and 8 frameworks on pinned OSS fixtures. All cells are cold, isolated builds; scoring is strict (exact symbol). Backend parity is 18/18 byte-identical.
|
|
772
|
+
|
|
773
|
+
| Metric (default lexical path, 18 languages) | Mean | Min | Max |
|
|
774
|
+
|---|---|---|---|
|
|
775
|
+
| Symbolic rank-1 | **70%** | 43% | 86% |
|
|
776
|
+
| Semantic rank-1 | **22%** | 0% | 67% |
|
|
777
|
+
| Semantic s@5 | **60%** | 0% | 100% |
|
|
778
|
+
|
|
779
|
+
Per-language symbolic rank-1 highlights (default path):
|
|
780
|
+
|
|
781
|
+
| Language | Fixture | Symbolic rank-1 | Semantic rank-1 | Semantic s@5 |
|
|
782
|
+
|---|---|---|---|---|
|
|
783
|
+
| Go | gin | 85% | 20% | 100% |
|
|
784
|
+
| Java/Spring | spring | 86% | 25% | 100% |
|
|
785
|
+
| Python/Django | django | 83% | 33% | 67% |
|
|
786
|
+
| Swift | alamofire | 78% | 67% | 100% |
|
|
787
|
+
| TypeScript | nestjs | 57% | 14% | 43% |
|
|
788
|
+
| SCSS | css | 43% | 25% | 50% |
|
|
789
|
+
| Bash | nvm | 44% | 33% | 67% |
|
|
790
|
+
|
|
791
|
+
Known structural gaps (invocation-verified, per `docs/benchmarks/BENCH_LANGUAGES.md` — the single source of truth): `get_call_graph` is effectively empty for SCSS (only 6 trivial `@include`/`@function` edges) and degraded to class-granularity for Java/Spring (call edges resolve, but methods aren't their own chunks). C#/ASP.NET, PHP/Laravel, and PHP/Symfony all resolve call edges (the Tier-1 C# and PHP call-graph fixes landed in v2.0.0). The `find_references` type-usage (`type_refs`) channel is empty for express-js, Django, Rails, SCSS, and Bash.
|
|
792
|
+
|
|
793
|
+
#### Token savings (18 languages)
|
|
794
|
+
|
|
795
|
+
Savings are measured as the token footprint of top-5 compact cards vs reading the full source files of those hits. The amortized column models one `get_chunk` full-body expansion (the recommended agent pattern).
|
|
796
|
+
|
|
797
|
+
| Language | Top-5 vs full files | Amortized (net 1 `get_chunk`) |
|
|
798
|
+
|---|---|---|
|
|
799
|
+
| Bash | 95.9% | 98.2% |
|
|
800
|
+
| C | 91.9% | 96.8% |
|
|
801
|
+
| Rust | 89.2% | 95.0% |
|
|
802
|
+
| Express (JS) | 88.9% | 93.5% |
|
|
803
|
+
| Go | 87.9% | 91.0% |
|
|
804
|
+
| FastAPI (Python) | 84.0% | 91.4% |
|
|
805
|
+
| Swift | 83.1% | 94.2% |
|
|
806
|
+
| Django (Python) | 78.1% | 90.9% |
|
|
807
|
+
| axios (JS) | 73.1% | 85.2% |
|
|
808
|
+
| NestJS (TS) | 66.7% | 84.4% |
|
|
809
|
+
| SCSS | 65.4% | 79.4% |
|
|
810
|
+
| Kotlin/Android | 62.7% | 82.3% |
|
|
811
|
+
| Ruby/Rails | 58.6% | 81.3% |
|
|
812
|
+
| PHP/Symfony | 57.6% | 86.1% |
|
|
813
|
+
| React (TS) | 56.4% | 82.1% |
|
|
814
|
+
| Java/Spring | 50.8% | 79.8% |
|
|
815
|
+
| PHP/Laravel | 32.4% | 72.7% |
|
|
816
|
+
| C#/ASP.NET | 19.3% | 65.1% |
|
|
817
|
+
|
|
818
|
+
v1.2.0 reported a mean of 79.0% (gross, 5 suites). v2.0.0's gross mean across the original 5 suites is 79.6%. The amortized figure (new metric) accounts for get_chunk expansions and is 65%–98% across languages.
|
|
819
|
+
|
|
820
|
+
#### End-to-end agent trace (real MCP tools, LLM sub-agent)
|
|
821
|
+
|
|
822
|
+
New in v2.0.0. Tested on 3 fixtures with real MCP tool calls traced through a sub-agent:
|
|
823
|
+
|
|
824
|
+
| Fixture | Tasks | Success | Avg tool calls | Avg tokens/task |
|
|
825
|
+
|---|---|---|---|---|
|
|
826
|
+
| gin (Go) | 2 | 100% | 4.0 | 4,286 |
|
|
827
|
+
| express-js (JS) | 2 | 100% | 3.5 | 2,089 |
|
|
828
|
+
| django (Python) | 2 | 100% | 5.0 | 1,763 |
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## [1.2.0] — (main branch baseline)
|
|
833
|
+
|
|
834
|
+
Initial stable release with:
|
|
835
|
+
|
|
836
|
+
- AST indexing for 11 languages (TypeScript/JavaScript, Python, Go, Rust, Java, Kotlin, C#, Ruby, PHP, CSS/SCSS)
|
|
837
|
+
- Hybrid lexical + optional dense-vector search with RRF fusion
|
|
838
|
+
- 8 MCP tools: `search_code`, `get_chunk`, `resolve_symbol`, `get_chunk_summary`, `get_file_skeleton`, `get_call_graph`, `get_repo_map`, `list_index_stats`
|
|
839
|
+
- In-memory and SQLite storage backends with backend parity
|
|
840
|
+
- Optional LLM enrichment and reranker
|
|
841
|
+
- Watch daemon with file-change tracking
|
|
842
|
+
- Interactive `npx graph-indexer` setup wizard
|
|
843
|
+
|
|
844
|
+
### Metrics (v1.2.0, 5 suites — 100 queries)
|
|
845
|
+
|
|
846
|
+
Measured with the enrichment + rerank path active (the "best" configuration at the time).
|
|
847
|
+
|
|
848
|
+
| Channel | Strict s@5 | Rank-1 | MRR | Symbolic rank-1 | Semantic rank-1 | Semantic s@5 |
|
|
849
|
+
|---|---|---|---|---|---|---|
|
|
850
|
+
| Hybrid + enrichment + rerank | 0.82 | 0.66 | 0.73 | 0.80 | 0.35 | 0.65 |
|
|
851
|
+
| Hybrid (no rerank) | — | — | — | — | 0.23 | 0.55 |
|
|
852
|
+
| Lexical-only | — | — | — | — | 0.19 | 0.48 |
|
|
853
|
+
|
|
854
|
+
Token savings: 79.0% (gross, top-5 cards vs full-file reads). Loose recall@5: 0.90.
|