graph-indexer 1.0.3 → 1.2.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/README.md CHANGED
@@ -1,16 +1,24 @@
1
- # graph-indexer
1
+ <div align="center">
2
+ <!-- Reemplaza la URL de abajo con la ruta a tu nuevo logo profesional en formato SVG o PNG transparente -->
3
+ <img src="https://raw.githubusercontent.com/MaquinaTech/graph-indexer/main/assets/logo.jpg" alt="Graph Indexer Logo" width="250" />
2
4
 
3
- <p align="center">
4
- <strong>Zero-Database · Air-Gapped · AST-Precise · Hybrid Search</strong><br>
5
- <em>An MCP server that lets AI coding agents search your codebase by concept instead of reading files — reducing context usage by 65–88% per query.</em>
6
- </p>
5
+ <h1>Graph Indexer</h1>
7
6
 
8
- <p align="center">
9
- <a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/v/graph-indexer?color=blue&style=flat-square" alt="npm version"></a>
10
- <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square" alt="License: MIT"></a>
11
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-green?style=flat-square" alt="Node.js 18+"></a>
12
- <a href="#results"><img src="https://img.shields.io/badge/Recall@5-100%25-brightgreen?style=flat-square" alt="Recall@5 100%"></a>
13
- </p>
7
+ <p>
8
+ <strong>Zero-Database · Air-Gapped · AST-Precise · Hybrid Search · Monorepo-Scale</strong>
9
+ </p>
10
+ <p>
11
+ <em>An MCP server that lets AI coding agents search your codebase by concept instead of reading files, cutting context usage by 65–88% per query.</em>
12
+ </p>
13
+
14
+ <!-- Badges -->
15
+ <a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/v/graph-indexer?color=007acc&style=for-the-badge" alt="npm version"></a>
16
+ <a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/dt/graph-indexer?color=4caf50&style=for-the-badge" alt="NPM Downloads"></a>
17
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-brightgreen?style=for-the-badge&logo=nodedotjs" alt="Node.js 18+"></a>
18
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
19
+ </div>
20
+
21
+ <br />
14
22
 
15
23
  ---
16
24
 
@@ -19,6 +27,8 @@
19
27
  - [What it does](#what-it-does)
20
28
  - [Results](#results)
21
29
  - [Getting started](#getting-started)
30
+ - [Storage backends](#storage-backends)
31
+ - [Semantic enrichment](#semantic-enrichment)
22
32
  - [MCP tools](#mcp-tools)
23
33
  - [Configuration](#configuration)
24
34
  - [Supported languages](#supported-languages)
@@ -26,15 +36,19 @@
26
36
  - [Best practices](#best-practices)
27
37
  - [Troubleshooting](#troubleshooting)
28
38
  - [Development](#development)
39
+ - [Security](#security)
29
40
  - [License](#license)
30
41
 
31
42
  ---
32
43
 
33
44
  ## What it does
34
45
 
35
- graph-indexer pre-indexes your codebase into an AST-precise search index and exposes it as an MCP server. Instead of reading full files, AI agents call `search_code("payment validation")` and get back the exact functions that match — with type signatures, docstrings, dependency graph, and call sites — in a fraction of the tokens.
46
+ graph-indexer pre-indexes your codebase into an AST-precise search index and exposes it as an MCP server. Instead of reading full files, AI agents call `search_code("payment validation")` and get back the exact functions that match — with type signatures, docstrings, decorators, inheritance edges, dependency graph, and call sites — in a fraction of the tokens.
47
+
48
+ It runs entirely on your machine. No external database, no cloud APIs, no telemetry. Two design choices make it suitable for any repository size:
36
49
 
37
- It runs entirely on your machine. No external database, no cloud APIs, no telemetry.
50
+ - A **default in-memory engine** that is instant and dependency-free, and an optional **disk-backed SQLite backend** that holds the index on disk so retrieval RAM stays flat on enterprise monorepos that would otherwise exhaust Node's heap.
51
+ - Optional **local-LLM semantic enrichment** that teaches the index what each core component *does*, so conceptual queries match code that shares none of their words.
38
52
 
39
53
  **Token comparison:**
40
54
 
@@ -59,39 +73,96 @@ The difference compounds across a session. Agents that read files also lack topo
59
73
 
60
74
  ## Results
61
75
 
62
- Measured across 5 production open-source codebases (7,503 AST chunks, 70 ground-truth queries across 3 difficulty levels).
76
+ Measured across 5 production open-source codebases (8,296 AST chunks, 100 ground-truth queries:
77
+ 69 **symbolic** — the developer names or paraphrases the symbol — and 31 **semantic** — agent-style
78
+ behavioural descriptions sharing few or no words with the code).
63
79
 
64
- ### Token savings per query
65
-
66
- | Project | Language | Without (full files) | With graph-indexer | Savings | Latency |
67
- | :--- | :--- | ---: | ---: | ---: | ---: |
68
- | Axios v1.6.0 | JavaScript | ~2,290 tok | ~655 tok | **71.4%** | 0.2 ms |
69
- | Express 4.18.2 | JavaScript | ~7,330 tok | ~850 tok | **88.4%** | 0.2 ms |
70
- | NestJS v10.4.9 | TypeScript | ~2,535 tok | ~900 tok | **64.5%** | 0.7 ms |
71
- | FastAPI 0.103.0 | Python | ~4,030 tok | ~685 tok | **83.0%** | 1.0 ms |
72
- | Gin v1.9.1 | Go | ~2,530 tok | ~450 tok | **82.2%** | 0.4 ms |
73
- | **Mean** | | | | **77.9%** | **0.5 ms** |
80
+ * **Average Token Savings:** 79.0%
81
+ * **Strict Success@5 (Hybrid + Rerank):** 82%
82
+ * **Strict MRR (Symbolic queries):** 0.84
74
83
 
75
- > "Full files" = tokens in the source files containing the top-5 results. "With graph-indexer" = tokens in the returned chunk snippets only. Measured at 4 chars/token. NestJS is 700k tokens of source — larger than most LLMs' context windows.
84
+ ### Token savings per query
76
85
 
77
- ### Search quality
86
+ | Project | Language | Chunks | Recall@5 (loose) | Savings |
87
+ | :--- | :--- | ---: | ---: | ---: |
88
+ | Axios v1.6.0 | JavaScript | 450 | 0.89 | **69.7%** |
89
+ | Express 4.18.2 | JavaScript | 389 | 0.95 | **88.7%** |
90
+ | NestJS v10.4.9 | TypeScript | 2,675 | 0.95 | **67.5%** |
91
+ | FastAPI 0.103.0 | Python | 3,694 | 0.76 | **85.5%** |
92
+ | Gin v1.9.1 | Go | 1,088 | 0.94 | **83.7%** |
93
+ | **Mean** | | **8,296** | **0.90** | **79.0%** |
94
+
95
+ > Savings = tokens in the returned chunk snippets vs the full source files containing the top-5
96
+ > results, at 4 chars/token. Search latency is sub-millisecond lexical; the hybrid channel adds
97
+ > one local embedding call plus a streamed vector scan (a few ms at this corpus size).
98
+
99
+ ### Search quality (strict)
100
+
101
+ Strict scoring: a result counts **only** if its symbol name (or its class, for method chunks)
102
+ exactly equals the ground truth — no substring matching, no file-path credit. Hybrid =
103
+ BM25 + `nomic-embed-text` vectors with `qwen2.5-coder:1.5b` enrichment; rerank adds the
104
+ opt-in `qwen2.5-coder:7b` judge on natural-language queries.
105
+
106
+ | Project | Strict success@5 | **Rank-1 (strict)** | MRR (strict) |
107
+ | :--- | ---: | ---: | ---: |
108
+ | Axios v1.6.0 | 0.74 | 0.68 | 0.72 |
109
+ | Express 4.18.2 | 0.90 | 0.62 | 0.73 |
110
+ | NestJS v10.4.9 | 0.67 | 0.57 | 0.60 |
111
+ | FastAPI 0.103.0 | 0.81 | 0.67 | 0.74 |
112
+ | Gin v1.9.1 | 1.00 | 0.78 | 0.84 |
113
+ | **Mean (hybrid + rerank)** | **0.82** | **0.66** | **0.73** |
114
+
115
+ Split by query style and configuration (the numbers that matter for agent workflows):
116
+
117
+ | Channel | Rank-1 | MRR | Success@5 |
118
+ | :--- | ---: | ---: | ---: |
119
+ | **Symbolic** (name-lookup, 69q), hybrid | **0.80** | **0.84** | — |
120
+ | **Semantic** (behavioural, 31q), hybrid + rerank | **0.35** | **0.47** | **0.65** |
121
+ | Semantic, hybrid (no rerank) | 0.23 | 0.37 | 0.55 |
122
+ | Semantic, lexical-only (no Ollama) | 0.19 | 0.29 | 0.48 |
123
+
124
+ Read the semantic rows with their denominator in mind: under *strict* scoring several of those
125
+ queries are unwinnable in-repo (the expected symbol is an anonymous default export, or is
126
+ re-exported from a dependency whose implementation isn't in the codebase), and most strict
127
+ "misses" still land the right file in the top-2 — which the loose channel (0.89) credits and an
128
+ agent can use. Symbol-naming queries — the bulk of real agent traffic — find the exact chunk
129
+ first try 80% of the time and in the top ranks (MRR 0.84) almost always.
130
+
131
+ Ranking favours exact symbol matches even for short, high-signal names — `res.json`,
132
+ `req.get`, `app.all` — by gating the name boost on corpus **document frequency** instead of a
133
+ blunt length cutoff (with singular/plural equivalence, so `BackgroundTask` finds
134
+ `BackgroundTasks`). Natural-language queries are detected and ranked **vector-first** (with the
135
+ name/path boosts gated on semantic agreement), so behavioural questions aren't drowned by
136
+ keyword noise. Test, spec, example and sandbox chunks are demoted in both channels.
137
+
138
+ **Backend parity is exact**: every rank-assigning sort uses deterministic tie-breaking and both
139
+ stores funnel vector candidates through one shared finalizer, so the in-memory engine and the
140
+ SQLite store return **identical top-5 chunk ids for 100/100 benchmark queries** — verified, not
141
+ approximate.
142
+
143
+ Reproduce every view:
78
144
 
79
- | Project | Chunks | Recall@1 | Recall@3 | Recall@5 | MRR |
80
- | :--- | ---: | ---: | ---: | ---: | ---: |
81
- | Axios v1.6.0 | 435 | 0.86 | 1.00 | **1.00** | 0.92 |
82
- | Express 4.18.2 | 389 | 0.93 | 0.93 | **1.00** | 0.95 |
83
- | NestJS v10.4.9 | 2,019 | 1.00 | 1.00 | **1.00** | **1.00** |
84
- | FastAPI 0.103.0 | 3,572 | 0.93 | 1.00 | **1.00** | 0.96 |
85
- | Gin v1.9.1 | 1,088 | 0.85 | 1.00 | **1.00** | 0.92 |
86
- | **Mean** | **7,503** | **0.91** | **0.99** | **1.00** | **0.95** |
145
+ ```bash
146
+ npm run test:setup # clone the 5 fixture repos
147
+ npm run test # loose hit-rate + token savings + indexing stats
148
+ npm run test:eval # strict symbol-level accuracy, lexical channel
149
+ OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings # hybrid
150
+ OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings --use-sqlite # parity
151
+ OLLAMA_HOST=http://localhost:11434 RERANK_MODEL=qwen2.5-coder:7b \
152
+ node test/evaluate.mjs --embeddings --rerank # + judge
153
+ ```
87
154
 
88
- **Recall@5 = 1.00** across every project: the correct answer is always in the top 5. **MRR = 0.95**: the correct answer is rank 1 or 2 on average.
155
+ > The strict harness ([test/evaluate.mjs](test/evaluate.mjs)) exists specifically to keep
156
+ > these numbers honest and prevent the tool from being tuned to a friendly benchmark — it
157
+ > reports the inflation gap (loose minus strict) per suite so regressions in real precision
158
+ > can't hide behind a permissive hit-rate, and it splits the symbolic and semantic channels
159
+ > so neither inflates the other.
89
160
 
90
161
  ---
91
162
 
92
163
  ## Getting started
93
164
 
94
- **Requirements:** Node.js v18+ · Ollama (optional, for semantic search)
165
+ **Requirements:** Node.js v18+ · Ollama (optional, for semantic search and enrichment) · Node.js v22.5+ for the optional SQLite backend
95
166
 
96
167
  ### 1. Install
97
168
 
@@ -134,7 +205,20 @@ npm run mcp:index
134
205
  INDEXER_EMBEDDINGS=off npm run mcp:index
135
206
  ```
136
207
 
137
- Lexical-only still achieves Recall@5 = 1.00 on all benchmarks above.
208
+ Lexical-only still scores loose recall@5 = 0.90 and strict symbolic MRR 0.81 on the benchmarks above — only the semantic (behavioural-query) channel needs embeddings.
209
+
210
+ **On a large monorepo, keep retrieval RAM flat with the SQLite backend:**
211
+ ```bash
212
+ npm run mcp:index -- --use-sqlite
213
+ ```
214
+
215
+ **Sharpen conceptual recall with local-LLM enrichment:**
216
+ ```bash
217
+ ollama pull qwen2.5-coder:1.5b
218
+ npm run mcp:index -- --llm-enrichment
219
+ ```
220
+
221
+ The two options compose (`--use-sqlite --llm-enrichment`) and can be made permanent in `.graph-indexer.json` — see [Configuration](#configuration).
138
222
 
139
223
  ### 3. Configure your IDE
140
224
 
@@ -180,12 +264,99 @@ Lexical-only still achieves Recall@5 = 1.00 on all benchmarks above.
180
264
  }
181
265
  ```
182
266
 
267
+ The server reads the backend from `.graph-indexer.json`, so the same launch config works for both the in-memory and SQLite indexes.
268
+
183
269
  ### 4. Add the agent system prompt
184
270
 
185
271
  Copy [PROMPT.md](./PROMPT.md) into your AI agent's system prompt to instruct it to use graph-indexer tools instead of reading files directly.
186
272
 
187
273
  ---
188
274
 
275
+ ## Storage backends
276
+
277
+ The same index, the same tools, the same ranking — two ways to hold the data. The MCP tools are written against a storage contract and never see which backend is active.
278
+
279
+ | Backend | Default | Resident RAM* | Dependencies | Best for |
280
+ | :--- | :--- | ---: | :--- | :--- |
281
+ | **In-memory** | ✅ | ~586 MB | none | Single packages and services; instant cold start. |
282
+ | **SQLite** | `--use-sqlite` | ~79 MB | none — uses Node's built-in `node:sqlite` | Monorepos / 1M+ LOC where holding every chunk in the heap would OOM. |
283
+
284
+ > *Resident set serving the **same** 50,000-chunk corpus, measured in isolated processes by
285
+ > [test/scale.mjs](test/scale.mjs). The in-memory engine keeps every chunk and the full inverted
286
+ > index in the heap, so its footprint grows with the codebase; the SQLite store keeps only the
287
+ > small file-level dependency graph resident and reads chunks, posting lists and vectors from disk
288
+ > on demand, so its footprint stays flat regardless of corpus size — an **87% smaller** resident
289
+ > set here, and the gap widens as the repo grows.
290
+
291
+ The SQLite backend adds **no external dependency**: `node:sqlite` ships inside Node (v22.5+). Lexical BM25 reads from an indexed `postings` table, symbols and call edges from indexed columns, and vectors live in the shared `code-index.embeddings.bin` — point reads are `pread` on demand. Because both backends feed the same fusion-and-boost ranker with deterministic tie-breaking, switching is purely an operational choice with **zero** quality trade-off (identical top-5 ids on the full benchmark).
292
+
293
+ ### Vector search that stays fast at monorepo scale
294
+
295
+ Below 10k vectors the semantic channel runs an exact streaming scan of the bin (~20 ms worst
296
+ case). Above it, a **binary-quantized sketch** takes over: each vector's sign bits (768 dims →
297
+ 96 bytes, 0.1 % of the float data) are kept in RAM; a query does a Hamming-distance pass over
298
+ packed Uint32 words, then exact-cosine rescores only the top candidates from disk. Measured on
299
+ synthetic corpora (warm):
300
+
301
+ | Corpus | Exact scan | Sketch | Speedup | Sketch RAM |
302
+ | ---: | ---: | ---: | ---: | ---: |
303
+ | 50,000 vectors | 104 ms | **5–11 ms** | ~20× | ~9 MB |
304
+ | 200,000 vectors | 519 ms | **11 ms** | ~36× | ~35 MB |
305
+
306
+ The sketch recovers the exact top-10 (10/10 in validation) because it rescores 2× the candidate
307
+ budget with true cosine before ranking. It is **append-aware**: daemon updates extend it by
308
+ scanning only the unseen tail of the bin, and a full re-index is detected by fingerprint and
309
+ triggers a rebuild. `test/scale.mjs` asserts hybrid queries stay interactive (<60 ms) at 50k
310
+ chunks on both backends.
311
+
312
+ ```bash
313
+ npm run mcp:index -- --use-sqlite # writes code-index.db
314
+ ```
315
+
316
+ ### Live updates on both backends
317
+
318
+ The watch daemon keeps **whichever backend is configured** fresh, incrementally:
319
+
320
+ - **In-memory** — the daemon rewrites the JSON snapshot; a running MCP server watches the file and reloads it automatically.
321
+ - **SQLite** — each file save becomes one WAL transaction (`applyFileUpdate`): the file's old chunks, postings and call edges are replaced with exact BM25 bookkeeping, and new vectors are *appended* to the embeddings bin (O(changed chunks), never a full rewrite). Running MCP servers detect the commit via `PRAGMA data_version` on their next query and refresh themselves — no re-indexing, no restarts.
322
+
323
+ Daemon startup is also O(changed files): files older than the index artifact are skipped during the initial scan, so edits made while the daemon was down are picked up without re-parsing the whole repo.
324
+
325
+ ---
326
+
327
+ ## Semantic enrichment
328
+
329
+ Embeddings match text proximity, not intent. A query like *"payment gateway webhook bottleneck"* misses the function that handles it if that function never spells out those words. Enrichment closes that gap.
330
+
331
+ With `--llm-enrichment`, the indexer routes every substantive **production-source chunk** (tests, specs and example trees are excluded — agents search for implementations) through a local LLM via Ollama, producing per chunk:
332
+
333
+ - a one-line **summary** in developer vocabulary, and
334
+ - a set of **concept tags** (e.g. `authentication, JWT, middleware`).
335
+
336
+ These ride **three** retrieval paths: the tags join the chunk's BM25 lexical document as high-IDF domain terms; the summary leads the code-payload embedding; and — decisive for natural-language queries — each enriched chunk gets a **second, summary-only vector**. A one-line behavioural query embeds far closer to a one-line summary than to hundreds of characters of code, so conceptual searches hit code that shares none of their words.
337
+
338
+ Enrichment is **incremental**: results are cached in `code-index.enrichment.json` keyed by content hash, so a re-index only sends new or changed code to the LLM (the embedding cache keys account for enrichment too, so nothing is re-embedded needlessly). Each run enriches up to `maxChunks` new chunks, ordered by file centrality (PageRank), and coverage accumulates across runs. The watch daemon re-attaches cached enrichment on every file save and live-enriches changed chunks. Generation is best-effort — if the model is unreachable the index is built without enrichment rather than failing.
339
+
340
+ ```bash
341
+ ollama pull qwen2.5-coder:1.5b
342
+ npm run mcp:index -- --llm-enrichment
343
+ ```
344
+
345
+ The model, the file-selection ratio and the per-run cap are all configurable (see [Configuration](#configuration)). Enrichment is fully optional and leaves the default index byte-for-byte unchanged when disabled.
346
+
347
+ ### LLM reranking (query time, opt-in)
348
+
349
+ Fusion resolves most queries, but a natural-language query can end in a near-tie between the
350
+ semantically right chunk and a lexically similar neighbour — a gap no static boost can close.
351
+ With `rerank.enabled` (or `search_code(..., rerank: true)`), the fused top-8 is shown to a local
352
+ LLM judge — one line per candidate — which reorders them. Measured effect on the strict semantic
353
+ channel: **rank-1 0.23 → 0.35, MRR 0.37 → 0.47, success@5 0.55 → 0.65**, with the symbolic
354
+ channel untouched (the judge only fires on natural-language queries, never on symbol lookups or
355
+ `exact_tokens` calls). Cost: one generation call (~1–2 s with `qwen2.5-coder:7b`). Best-effort —
356
+ any model failure preserves the original order.
357
+
358
+ ---
359
+
189
360
  ## MCP tools
190
361
 
191
362
  ### `search_code`
@@ -201,6 +372,7 @@ Hybrid BM25 + vector search over the index. The primary entry point for agents.
201
372
  | `include_topology` | boolean | `true` | Include imports / used-by / calls |
202
373
  | `min_score` | number | `0.3` | Minimum relevance threshold |
203
374
  | `token_budget` | number? | auto | Cap total tokens returned |
375
+ | `rerank` | boolean? | config | LLM-judge rerank for natural-language queries (see `rerank.*` config) |
204
376
 
205
377
  **`detail` levels:**
206
378
 
@@ -210,6 +382,8 @@ Hybrid BM25 + vector search over the index. The primary entry point for agents.
210
382
  | `"smart"` (default) | ~150 tok | Signature + lines relevant to the query |
211
383
  | `"full"` | ~300 tok | Signature + complete source body |
212
384
 
385
+ For purely conceptual queries with no lexical overlap, `"smart"` falls back to a structural skeleton — control-flow lines and call sites — so the agent always gets meaningful signal about what the code does, never a blind truncation.
386
+
213
387
  ---
214
388
 
215
389
  ### `resolve_symbol`
@@ -237,9 +411,10 @@ Returns the source code of a single chunk by its ID.
237
411
 
238
412
  Signature + docstring + calls without the body. ~50 tokens vs ~300 for full.
239
413
 
240
- | Parameter | Type | Description |
241
- | :--- | :--- | :--- |
242
- | `chunk_id` | string | ID from `search_code` results |
414
+ | Parameter | Type | Default | Description |
415
+ | :--- | :--- | :--- | :--- |
416
+ | `chunk_id` | string | required | ID from `search_code` results |
417
+ | `expand_calls` | boolean | `false` | Resolve each outgoing call's signature inline (~150 tok) instead of issuing a follow-up tool call per dependency |
243
418
 
244
419
  ---
245
420
 
@@ -279,7 +454,7 @@ Compact symbol map ordered by PageRank (most-imported files first). Orients agen
279
454
 
280
455
  ### `list_index_stats`
281
456
 
282
- Index health snapshot: chunk count, file count, symbol table size, vector count, search mode, daemon status, index age.
457
+ Index health snapshot: storage backend, chunk count, file count, symbol table size, vector count, search mode, daemon status, index age.
283
458
 
284
459
  ---
285
460
 
@@ -291,32 +466,60 @@ Full bidirectional dependency topology for a file: what it imports and what impo
291
466
 
292
467
  ## Configuration
293
468
 
294
- ### Environment variables
469
+ ### `.graph-indexer.json`
295
470
 
296
- | Variable | Default | Description |
297
- | :--- | :--- | :--- |
298
- | `MCP_PROJECT_ROOT` | `process.cwd()` | Project root directory |
299
- | `OLLAMA_HOST` | `http://localhost:11434` | Ollama API endpoint |
300
- | `INDEXER_EMBEDDINGS` | — | Set to `off` to disable vector embeddings |
301
-
302
- ### Language selection
303
-
304
- `init` saves your selection to `.graph-indexer.json` in the project root:
471
+ All persistent settings live in one file at the project root, written by `init` and read by the indexer, watcher and server:
305
472
 
306
473
  ```json
307
- { "languages": ["typescript", "javascript", "python"] }
474
+ {
475
+ "languages": ["typescript", "javascript", "python"],
476
+ "storage": "sqlite",
477
+ "enrichment": {
478
+ "enabled": true,
479
+ "model": "qwen2.5-coder:1.5b",
480
+ "coreRatio": 0.15,
481
+ "maxChunks": 400
482
+ }
483
+ }
308
484
  ```
309
485
 
310
- Valid keys: `typescript`, `javascript`, `python`, `go`, `rust`, `php`, `java`, `kotlin`, `csharp`, `ruby`, `css`.
486
+ | Key | Default | Description |
487
+ | :--- | :--- | :--- |
488
+ | `languages` | all installed | Parsers to load. Valid keys: `typescript`, `javascript`, `python`, `go`, `rust`, `php`, `java`, `kotlin`, `csharp`, `ruby`, `css`. |
489
+ | `storage` | `"memory"` | `"memory"` (in-heap, zero-dependency) or `"sqlite"` (disk-backed). |
490
+ | `ollamaHost` | `"http://localhost:11434"` | Ollama endpoint (also settable via `OLLAMA_HOST`). |
491
+ | `embedModel` | `"nomic-embed-text"` | Embedding model (index and queries must use the same one). |
492
+ | `enrichment.enabled` | `false` | Run local-LLM enrichment during indexing. |
493
+ | `enrichment.model` | `"qwen2.5-coder:1.5b"` | Ollama model used for generation. |
494
+ | `enrichment.coreRatio` | `1.0` | Share of production files eligible (by PageRank). `1.0` = all; tests/examples are always excluded. |
495
+ | `enrichment.maxChunks` | `500` | Cap on **new** LLM calls per index run — the cache accumulates coverage across runs. |
496
+ | `enrichment.concurrency` | `12` | Parallel Ollama requests during enrichment. |
497
+ | `rerank.enabled` | `false` | LLM-judge reranking of natural-language queries (+50% semantic rank-1, ~1–2 s per NL query). |
498
+ | `rerank.model` | `"qwen2.5-coder:7b"` | Judge model. Quality matters: 7B measured a large gain where 1.5B measured ~none. |
499
+ | `rerank.topM` | `8` | Fused results shown to the judge (8 measured better than 10). |
500
+
501
+ ### CLI flags
502
+
503
+ Flags override the config file for a single run:
504
+
505
+ | Flag | Equivalent |
506
+ | :--- | :--- |
507
+ | `--repo <dir>` | Index a directory other than the cwd |
508
+ | `--use-sqlite` | `"storage": "sqlite"` |
509
+ | `--llm-enrichment` | `"enrichment.enabled": true` |
510
+ | `--enrich-model <name>` | `"enrichment.model"` |
511
+ | `--enrich-max <n>` | `"enrichment.maxChunks"` for this run |
512
+ | `--enrich-concurrency <n>` | `"enrichment.concurrency"` for this run |
311
513
 
312
- Update at any time by re-running `init`:
514
+ ### Environment variables
313
515
 
314
- ```bash
315
- npx graph-indexer init # Re-run interactive selector
316
- npx graph-indexer init --all-languages # Enable all, no prompt
317
- ```
516
+ | Variable | Default | Description |
517
+ | :--- | :--- | :--- |
518
+ | `MCP_PROJECT_ROOT` | `process.cwd()` | Project root directory |
519
+ | `OLLAMA_HOST` | `http://localhost:11434` | Ollama API endpoint |
520
+ | `INDEXER_EMBEDDINGS` | — | Set to `off` to disable vector embeddings |
318
521
 
319
- If `.graph-indexer.json` is absent or has no `languages` key, all installed parsers are used.
522
+ Re-run `init` at any time to change languages, or `init --all-languages` to enable every installed parser without the prompt.
320
523
 
321
524
  ---
322
525
 
@@ -336,21 +539,52 @@ If `.graph-indexer.json` is absent or has no `languages` key, all installed pars
336
539
  | PHP | `.php` | function, class |
337
540
  | CSS / SCSS | `.css`, `.scss` | rule sets |
338
541
 
542
+ Oversized "god classes" are split automatically: a class longer than ~200 lines is indexed as a compact skeleton header plus one independently searchable chunk per method, so a single `get_chunk` never blows the token budget and every method stays reachable by search.
543
+
339
544
  ---
340
545
 
341
546
  ## How it works
342
547
 
548
+ ### Architecture
549
+
550
+ graph-indexer is organised around a small set of cohesive modules with a strict separation between *retrieval math*, *storage*, and *transport*:
551
+
552
+ ```
553
+ config.mjs Resolves CLI flags > env > .graph-indexer.json into one config.
554
+ parser-utils.mjs Tree-sitter parsing, chunk extraction, Ollama embeddings.
555
+ search-core.mjs Shared retrieval math: tokenisation, BM25, RRF fusion + boosts
556
+ (query-adaptive NL weighting), PageRank, embedding cache keys.
557
+ The numbers are measured once here and reused.
558
+ core-engine.mjs MemoryGraphIndex — the default in-heap store — plus the
559
+ embeddings-bin codecs (write/read/append/scan) and the
560
+ binary-quantized vector sketch (Hamming prefilter + rescore).
561
+ sqlite-store.mjs SqliteGraphStore — the disk-backed store (node:sqlite) with
562
+ per-file incremental writes and data_version live refresh.
563
+ storage.mjs createStore(config) — picks a backend; documents the contract
564
+ both implement (searchHybrid, getChunk, applyFileUpdate, …).
565
+ enrichment.mjs Optional LLM summaries + concept tags, cached by content hash.
566
+ mcp-tools.mjs The eight tools, written against the storage contract only.
567
+ mcp-server.mjs Thin bootstrap: config → store → tools → stdio.
568
+ indexer.mjs Bootstrap indexer. watch-daemon.mjs Incremental updates.
569
+ ```
570
+
571
+ Because both stores call the identical `fuseAndRank` from `search-core`, the in-memory and SQLite backends are rank-consistent by construction, and a single change to the ranking math applies everywhere.
572
+
343
573
  ### Indexing
344
574
 
345
575
  When you run `npm run mcp:index`:
346
576
 
347
577
  1. **Parses** every source file with Tree-sitter — no regex, exact AST boundaries.
348
- 2. **Extracts chunks**: named functions, classes, methods, and exports, each with name, docstring, parameters, return type, call sites, and type references.
578
+ 2. **Extracts chunks**: named functions, classes, methods, and exports — including Go/Rust `type`/`struct` declarations by their real name — each with docstring, parameters, return type, call sites, type references, **decorators/annotations** (`@Controller`, `@Injectable`, `@app.route`), and **inheritance edges** (`extends`/`implements`/base classes).
349
579
  3. **Builds a dependency graph**: bidirectional import map so each chunk knows what it imports and what imports it.
350
- 4. **Creates two indexes**: a BM25 inverted index (lexical) and per-chunk float32 embeddings via Ollama `nomic-embed-text` (optional).
351
- 5. **Serializes to disk**: `code-index.json` + `code-index.embeddings.bin`.
580
+ 4. **(Optional) Enriches** production-source chunks with LLM summaries + concept tags (cache-first; only new code pays an LLM call).
581
+ 5. **Creates two indexes**: a BM25 inverted index (lexical) and per-chunk float32 embeddings via Ollama `nomic-embed-text` (optional) — two vectors per enriched chunk (code payload + summary-only).
582
+ 6. **Persists** to the configured backend: `code-index.json` + `code-index.embeddings.bin`, or `code-index.db` + the same embeddings binary.
352
583
 
353
- A background watcher daemon re-indexes changed files automatically.
584
+ A background watcher daemon applies changed files incrementally to **either** backend (JSON
585
+ snapshot rewrites for in-memory, per-file WAL transactions for SQLite). It respects
586
+ `.gitignore` and skips `node_modules`, build output, and dot-directories, so it never traverses
587
+ (or exhausts OS file-watcher limits on) dependency trees.
354
588
 
355
589
  ### Search pipeline
356
590
 
@@ -362,14 +596,20 @@ Tree-sitter AST
362
596
  │
363
597
  ├──► Chunks ──► BM25 inverted index ──┐
364
598
  │ └──► Float32 embeddings ──┤── RRF fusion ──► Ranked results ──► MCP tools
365
- │ │
599
+ │ │ (search-core)
366
600
  └──► Dependency graph ────────────────┘
367
601
  ```
368
602
 
369
- 1. **BM25 lexical search** — O(query_terms) over the inverted index.
370
- 2. **Vector search** — cosine similarity over pre-normalized float32 embeddings.
371
- 3. **RRF fusion** merges both lists (lexical 1.5×, vector 1.0×).
372
- 4. **Score boosts**: exact name match +2.0×, suffix match +1.4×, path token +1.4×, test files demoted.
603
+ 1. **BM25 lexical search** — O(query_terms) over the inverted index / `postings` table.
604
+ 2. **Vector search** — exact cosine below 10k vectors; binary-sketch Hamming prefilter +
605
+ exact rescore above it (5–11 ms at 50k+, see [Storage backends](#storage-backends)). Both
606
+ code and summary vectors compete; hits fold onto their chunk through one shared finalizer.
607
+ 3. **RRF fusion** merges both lists — lexical-led (1.5×/1.0×) for keyword queries,
608
+ vector-led (1.0×/1.6×) for natural-language ones.
609
+ 4. **Score boosts**: exact name match +2.0× (df-gated, plural-aware), suffix match +1.4×,
610
+ path token +1.4×, test/example files demoted; on NL queries, boosts require semantic
611
+ agreement (the chunk must also be a vector candidate).
612
+ 5. **(Opt-in) LLM rerank** of the fused top-8 on natural-language queries.
373
613
 
374
614
  ---
375
615
 
@@ -383,6 +623,8 @@ Tree-sitter AST
383
623
 
384
624
  **Avoid catch-all utility files.** Files that mix unrelated utilities produce weaker per-chunk signals. One responsibility per module indexes more cleanly.
385
625
 
626
+ **Reach for the SQLite backend when the heap gets tight.** If indexing a large monorepo pushes Node toward its memory limit, switch to `--use-sqlite`; retrieval quality is identical and resident memory stops scaling with the codebase.
627
+
386
628
  ---
387
629
 
388
630
  ## Troubleshooting
@@ -391,19 +633,26 @@ Tree-sitter AST
391
633
  - Verify Ollama is running: `curl http://localhost:11434/api/tags`
392
634
  - Use lexical-only mode: `INDEXER_EMBEDDINGS=off npm run mcp:index`
393
635
 
636
+ **Indexing a huge repo runs out of memory**
637
+ - Build with `--use-sqlite` so chunks live on disk instead of the heap
638
+ - Confirm the active backend with `list_index_stats()`
639
+
640
+ **`--use-sqlite` reports node:sqlite is unavailable**
641
+ - The SQLite backend needs Node v22.5+; upgrade Node, or use the default in-memory backend
642
+
394
643
  **Search returns irrelevant results**
395
644
  - Use `exact_tokens` for known symbol names
396
- - Check coverage with `list_index_stats()`
397
- - Increase `top_k` to see more candidates
645
+ - For conceptual queries, index with `--llm-enrichment` and embeddings enabled
646
+ - Check coverage with `list_index_stats()`; increase `top_k` to see more candidates
398
647
 
399
648
  **MCP server won't connect**
400
649
  - Verify `MCP_PROJECT_ROOT` points to the indexed directory
401
- - Check `code-index.json` exists: `ls -la code-index.json`
650
+ - Check the index exists: `ls -la code-index.json` (or `code-index.db`)
402
651
  - Test manually: `npm run mcp:start`
403
652
 
404
653
  **Results are stale after file changes**
405
- - The daemon auto-updates; if stale, run `npm run mcp:index` manually
406
- - Check daemon status with `list_index_stats()`
654
+ - The daemon updates both backends live and running servers refresh automatically; check `list_index_stats()` for daemon status
655
+ - If the daemon isn't running, restart the MCP server (it spawns the daemon) or run `npm run mcp:index` manually
407
656
 
408
657
  ---
409
658
 
@@ -414,10 +663,34 @@ git clone https://github.com/MaquinaTech/graph-indexer.git
414
663
  cd graph-indexer
415
664
  npm install
416
665
  npm run mcp:index # index this repo
417
- npm run test # run test suite (lexical-only)
418
666
  npm run mcp:start # start MCP server
419
667
  ```
420
668
 
669
+ ### Tests
670
+
671
+ | Command | Scope |
672
+ | :--- | :--- |
673
+ | `npm run test` | Integration suite: indexing + loose hit-rate + token savings across 5 fixtures |
674
+ | `npm run test:eval` | Strict symbol-level accuracy (rank-1, precision, nDCG) |
675
+ | `npm run test:unit` | Pure helpers, chunk splitting |
676
+ | `npm run test:sqlite` | SQLite round-trip + rank consistency vs the in-memory engine |
677
+ | `npm run test:enrich` | HyDE enrichment, including the lexically-disjoint rank-1 flip |
678
+ | `npm run test:mcp` | End-to-end MCP server over stdio |
679
+ | `npm run test:scale` | Mock 50k-chunk corpus proving SQLite RAM stays bounded |
680
+ | `npm run test:all` | Every dependency-free suite above, no Ollama required |
681
+
682
+ ---
683
+
684
+ ## Security
685
+
686
+ graph-indexer runs locally and is air-gapped by default — its only outbound calls
687
+ are to a local Ollama endpoint for embeddings and optional enrichment (skipped entirely with
688
+ `INDEXER_EMBEDDINGS=off` and without `--llm-enrichment`). It never executes the code it indexes,
689
+ and the index artifacts contain source snippets, so keep them git-ignored (as `init` configures).
690
+
691
+ See [SECURITY.md](SECURITY.md) for the full threat model and how to report a
692
+ vulnerability.
693
+
421
694
  ---
422
695
 
423
696
  ## License