pi-context-engine 0.1.3 → 0.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
@@ -97,6 +97,30 @@ unchanged repo took 237 ms.
97
97
 
98
98
  No Python. No build tools. No API key.
99
99
 
100
+ ### Embedding model choice (measured, not claimed)
101
+
102
+ Evaluated on this package's own 76-query suite (`npm run eval -- --local`, same
103
+ code state, CPU only):
104
+
105
+ | local model | R@5 | R@10 | nDCG@10 | distraction@10 | served tokens | p50/p95 |
106
+ |---|---|---|---|---|---|---|
107
+ | `Xenova/all-MiniLM-L6-v2` (default, ~25 MB) | 0.752 | 0.827 | 0.744 | 0.713 | 702 | 28/33 ms |
108
+ | `michaelfeil/jina-embeddings-v2-base-code` (recommended for code) | **0.798** | **0.899** | **0.813** | **0.640** | 654 | 55/61 ms |
109
+
110
+ Why the split default: the code-trained model wins every quality metric on
111
+ this codebase (natural-language queries most of all: R@10 0.433 → 0.700) at
112
+ ~2× CPU latency and a larger first download. Keep MiniLM as the default for
113
+ fast general use; point code-heavy projects at the jina model:
114
+
115
+ ```yaml
116
+ embedding:
117
+ model: michaelfeil/jina-embeddings-v2-base-code
118
+ ```
119
+
120
+ Long inputs are pre-truncated to ~2000 chars per embedding so long-context
121
+ models cannot OOM the CPU runtime, and changing the model (dimension change)
122
+ forces a full reindex automatically.
123
+
100
124
  ## Configuration
101
125
 
102
126
  Zero config by default. Override in `~/.pi-context-engine/config.yaml` or a
@@ -107,16 +131,28 @@ retrieval:
107
131
  top_k: 20
108
132
  confidence_threshold: 0.2
109
133
  marginal_ratio: 0.75 # stop adding chunks below this fraction of the top score
134
+ expand_context: true # serve a hit's module parent + adjacent chunk (small-to-big)
135
+ confidence_high: 0.55 # query-quality composite at/above which a search reports `high`
136
+ confidence_low: 0.45 # below this the result gets a `[query confidence: low ...]` line with hints
137
+ auto_elaborate: false # opt-in: fuse a deterministic query variant as an extra candidate source
138
+ refine_hints: true # append a `[refine: ...]` line with next-query candidates
139
+ rerank: off # off | local — opt-in cross-encoder reranking of the top 30 fused hits
140
+ rerank_model: Xenova/ms-marco-MiniLM-L-6-v2
141
+ rerank_topn: 30
110
142
  guidance:
111
143
  enabled: true # inject context_search guidance into the system prompt
112
144
  compression:
113
145
  level: standard # minimal | standard | full
114
146
  ollama_url: http://localhost:11434
147
+ query_aware: true # keep query-relevant lines when truncating
115
148
  embedding:
116
149
  model: local # or a specific transformers.js model id
117
150
  ollama_model: nomic-embed-text
118
151
  indexer:
119
152
  redact_secrets: true
153
+ context_headers: true # prepend `// file: ... | unit: ... | imports: ...` to chunk text
154
+ chunking: legacy # legacy (default) | ast-density — cAST-style budgeted split-merge (experimental)
155
+ chunk_budget_chars: 1200 # non-whitespace budget per ast-density chunk
120
156
  memory:
121
157
  redact_pii: true
122
158
  pricing:
@@ -130,6 +166,29 @@ Environment overrides:
130
166
  - **`PCE_EMBED_BACKEND`**: force `local` or `ollama` instead of auto-detect.
131
167
  - **`PCE_RECALL_DISPLAY_CAP`**: how many recall hits to show (default 7).
132
168
 
169
+ **Reranking caveat**: `retrieval.rerank: local` is opt-in because it downloads
170
+ an ~80 MB cross-encoder model on first use (into the same models dir as the
171
+ embedder) and adds CPU latency per query. It reorders only the top 30 fused
172
+ candidates; if the model cannot load, retrieval falls back to the fused order
173
+ instead of failing.
174
+
175
+ **Chunking caveat**: `indexer.chunking: ast-density` (cAST-style budgeted
176
+ split-merge) is experimental and measured WORSE than the legacy walk on this
177
+ package's eval (R@10 0.818 vs 0.936 with the deterministic embedder): tiling
178
+ trivia into chunks dilutes fixed-size result sets. It stays behind a flag for
179
+ A/B on other workloads; chunks concatenate byte-for-byte back to the source,
180
+ so it is safe to try on a branch.
181
+
182
+ **Embedding backends**: two options, picked automatically per project.
183
+
184
+ - **Ollama** (default when reachable): no extra model files on disk beyond the
185
+ Ollama model itself.
186
+ - **Local ONNX** (transformers.js): works with no server. Needs the
187
+ `@huggingface/transformers` optional dependency, which plain `npm` and `pi`
188
+ installs include. If it was stripped (e.g. `--omit=optional`), indexing fails
189
+ with a clear message telling you to reinstall or switch to Ollama, instead of
190
+ a bare module error.
191
+
133
192
  ## Security
134
193
 
135
194
  Indexing is local, but the index is still a copy of your code. Two guards ship on
@@ -0,0 +1,500 @@
1
+ # Implementation plan: retrieval improvements
2
+
3
+ Full build plan for the recommendations in
4
+ [RESEARCH-RETRIEVAL.md](./RESEARCH-RETRIEVAL.md). Nothing here is started yet:
5
+ implementation is deliberately delayed, this document defines exactly what will
6
+ be built, in what order, and how each item is proven before its status changes.
7
+
8
+ **Why a plan first:** every item changes retrieval behavior, and our only
9
+ current metrics are token savings and latency. Building the measurement harness
10
+ before the changes lets us prove each improvement instead of guessing.
11
+
12
+ ## How to use this document
13
+
14
+ - **Status legend**
15
+ - **Not started**: planned, nothing built yet. All items are here today.
16
+ - **Ready**: implementation can begin (dependencies met, baseline recorded).
17
+ - **In progress**: work branch open.
18
+ - **Review**: code done, eval deltas attached to the PR.
19
+ - **Done**: merged, released, status table updated.
20
+ - **Dropped**: decided against, with the reason recorded in the item.
21
+ - **Rules for flipping a status**
22
+ 1. Typecheck and unit tests pass (`npm run check`).
23
+ 2. Eval deltas (before/after) are recorded in the PR description.
24
+ 3. README and `src/config.ts` cover any new config keys.
25
+ 4. This table is updated in the same PR.
26
+ - **Every item lists**: objective, evidence, files touched, steps, tests,
27
+ acceptance criteria, risks, dependencies, effort (S < half day, M 1-2 days,
28
+ L 3-5 days).
29
+
30
+ ---
31
+
32
+ ## Status summary
33
+
34
+ | ID | Title | Phase | Priority | Effort | Status |
35
+ |----|-------|-------|----------|--------|--------|
36
+ | R4 | Labeled retrieval eval harness | 0 | P0 | M | Done (2026-10-18) |
37
+ | R1 | Contextual chunk headers | 1 | P0 | M | Done (2026-10-18) |
38
+ | R2 | BM25 phrases, column weights, symbol matches | 1 | P0 | S | Done (2026-10-18) |
39
+ | R3 | Query-aware compression | 1 | P0 | M | Done (2026-10-18) |
40
+ | R5 | Multi-path retrieval with symbol path | 2 | P1 | M | Done (2026-10-18) |
41
+ | R6 | Small-to-big context expansion | 2 | P1 | L | Done (2026-10-18) |
42
+ | R7 | Graph context packs + centrality prior | 2 | P1 | M | Done (2026-10-18) |
43
+ | R8 | Query-confidence signal + optional elaboration | 2 | P1 | M | Done (2026-10-18) |
44
+ | R9 | Refine suggestions in search results | 2 | P1 | S | Done (2026-10-18) |
45
+ | R10 | Opt-in local reranker | 3 | P2 | M | Done (2026-10-18, default off) |
46
+ | R11 | Code-specialized embedder option | 3 | P2 | S | Done (2026-10-18) |
47
+ | R12 | AST density chunking (split-merge) | 3 | P2 | L | Done (2026-10-18, flag only — lost A/B) |
48
+ | R13 | Linked memory (A-MEM style) | 3 | P2 | M | Done (2026-10-18) |
49
+
50
+ **Blocked by nothing right now:** only R4 (phase 0). Everything else waits on
51
+ R4's baseline so its impact can be measured.
52
+
53
+ **Status history**
54
+ - 2026-10-18: all items R1-R13 implemented and measured. Eval (fake
55
+ embedder, 76 queries): R@10 .910-.936, nDCG .813-.825, distraction
56
+ ~.695-.699 across the run series (gate green at every step; final
57
+ 2026-10-18 run: 214 tests, 24 files). Notable outcomes:
58
+ - R10 reranker shipped default-off; real-model eval not run (model
59
+ repo gated/offline for jina, layout broken for the unixcoder ONNX
60
+ port); pipeline ordering covered by stub tests.
61
+ - R11 measured: jina-code beats MiniLM on every quality metric
62
+ (R@10 .899 vs .827, nDCG .813 vs .744, nl R@10 .700 vs .433) at
63
+ ~2x CPU latency; MiniLM stays the default, README carries the table.
64
+ - R12 ast-density measured and REJECTED as default: R@10 .818 vs
65
+ .936 legacy (trivia tiling dilutes fixed-size result sets). Ships
66
+ behind `indexer.chunking: ast-density` with byte-exact concat
67
+ property and tests; default stays `legacy`.
68
+ - R13 linked memory: schema v4 (decision_links + tags/files columns),
69
+ auto-link (top-3, L2 <= 0.85, inbound cap 8), explicit `related_to`
70
+ annotations, links surfaced in session_recall.
71
+ - 2026-10-17: plan created from the research note. Implementation delayed by
72
+ owner decision. All items set to **Not started**.
73
+
74
+ ---
75
+
76
+ ## Phase 0: measurement foundation
77
+
78
+ ### R4. Labeled retrieval eval harness
79
+
80
+ - **Objective:** measure retrieval quality (not just tokens) so every later
81
+ item has a before/after number.
82
+ - **Evidence:** CORE-Bench (arXiv:2606.11864) shows code-search scores overstate
83
+ agentic usefulness; UDCG (arXiv:2510.21440) shows distracting chunks must be
84
+ counted; Voyage reports CoSQA has ~51% bad labels, so we label our own.
85
+ - **Files:**
86
+ - new `tests/eval/queries.jsonl` (labels)
87
+ - new `scripts/eval-retrieval.mjs` (runner, tsx)
88
+ - `scripts/smoke-real-world.mjs` (add a `quality` block to its report)
89
+ - new `tests/eval/baseline.json` (recorded thresholds)
90
+ - `package.json` (`eval` script, wire into `check`)
91
+ - **Steps:**
92
+ 1. Write 60-100 labeled queries over this repo, four kinds: exact identifier
93
+ (`HybridRetriever`), natural language (`where is the retry logic`),
94
+ bug-report style (paste an error string), file-name style
95
+ (`src/retrieval/retriever.ts`). Each label lists expected file paths and,
96
+ where easy, expected chunk line ranges.
97
+ 2. Runner indexes a fixture copy of the repo, runs each query through
98
+ `HybridRetriever`, and computes: Recall@5, Recall@10, nDCG@10, distraction
99
+ ratio (share of served chunks whose file is not in the label set), served
100
+ tokens, p50/p95 latency.
101
+ 3. Implement nDCG in the script (no dependency); binary relevance is enough.
102
+ 4. Record current numbers into `baseline.json`; the runner exits non-zero on
103
+ regression beyond a small tolerance (start at 2 points on Recall@10).
104
+ 5. Add an optional external slice later (CORE-Bench level-2 sample) as a
105
+ separate non-gating script.
106
+ - **Tests:** runner produces stable output twice in a row; a deliberately
107
+ broken query (random words) scores below baseline.
108
+ - **Acceptance:** `npm run eval` reports all metrics, fails on regression, and
109
+ is documented in README's Development section.
110
+ - **Risks:** labels drift when the repo changes. Mitigation: labels point at
111
+ files/paths, not exact line numbers, except where a test pins them.
112
+ - **Dependencies:** none. **Effort:** M.
113
+
114
+ ---
115
+
116
+ ## Phase 1: quick wins (P0)
117
+
118
+ ### R1. Contextual chunk headers
119
+
120
+ - **Objective:** give every chunk its surrounding context before embedding and
121
+ indexing, so "the retrieval module" style queries find `src/retrieval/*`
122
+ code.
123
+ - **Evidence:** Anthropic Contextual Retrieval: failed retrievals −49% with
124
+ contextual embeddings + contextual BM25; late chunking (arXiv:2409.04701)
125
+ +2.7-3.6% nDCG from the same missing-context problem.
126
+ - **Files:**
127
+ - `src/indexer/chunker.ts` (`nodeToChunk`, `walk`: collect enclosing class,
128
+ imports, exported symbol names)
129
+ - `src/types.ts` (optional `metadata.contextHeader`)
130
+ - `src/indexer/pipeline.ts` (`embedChunks` currently embeds `c.content`)
131
+ - `src/indexer/manifest.ts` (index format version bump)
132
+ - `src/indexer/embedding-cache.ts` (expect full cache invalidation)
133
+ - **Steps:**
134
+ 1. Define header format, deterministic and LLM-free:
135
+ `// file: src/retrieval/retriever.ts | lang: typescript | unit: class HybridRetriever | imports: ./confidence, ./query-parser`
136
+ Header must respect `indexer.redact_secrets` (path only, no file body).
137
+ 2. Prepend the header in one place at chunk build time so embed, FTS and
138
+ `expand_chunk` all see the same text. Decision: header goes into
139
+ `content`, single source of truth, no second code path.
140
+ 3. Bump `manifest` format version so existing installs reindex automatically
141
+ (content hash change already invalidates the embedding cache).
142
+ 4. Keep header length capped (~120 tokens) so small chunks do not become
143
+ header-dominated.
144
+ - **Tests:** header contains file path and unit name; cap respected; redacted
145
+ paths behave; ids stay unique (id hashes `content.slice(0,100)`, so the
146
+ header prefix changes ids: confirm full reindex, no stale rows).
147
+ - **Acceptance:** eval shows Recall@k up or flat, no regression on identifier
148
+ queries, latency within 10% of baseline.
149
+ - **Risks:** header noise on very small chunks; path-only leakage of private
150
+ directory names (accepted, index is local and paths already go to FTS).
151
+ - **Dependencies:** R4 baseline. **Effort:** M.
152
+
153
+ ### R2. Strengthen BM25: phrases, column weights, symbol matches
154
+
155
+ - **Objective:** make the strongest code retriever work properly for quoted
156
+ phrases and path queries.
157
+ - **Evidence:** Finding 6 (BM25 strongest for code, arXiv:2605.14503);
158
+ Anthropic reports contextual BM25 as the larger single jump (35% → 49%).
159
+ - **Files:** `src/storage/fts-store.ts` (`escapeFts5`, `search`),
160
+ `src/retrieval/retriever.ts` (existing `FTS_BOOST_CODE_LOOKUP`),
161
+ tests under `tests/`.
162
+ - **Steps:**
163
+ 1. Parse quoted spans before tokenizing; emit FTS5 phrase strings
164
+ (`"foo bar"`) for them and keep OR-joining unquoted tokens.
165
+ 2. Add explicit `bm25()` column weights in `ORDER BY`: weight `file_path`
166
+ highest, then `content`, then `chunk_type`; leave `language` neutral.
167
+ 3. Exact symbol shortcut: if a query token matches a chunk's symbol name
168
+ (metadata from R1/R12, fallback regex on content), give it a deterministic
169
+ score bonus in `retriever.ts`.
170
+ 4. Keep stopword fallback and the `code_lookup` FTS weight as they are.
171
+ - **Tests:** phrase query matches only adjacent terms; path query ranks its
172
+ file top-3; OR behavior for unquoted terms unchanged; empty/stopword-only
173
+ queries still return the existing fallback.
174
+ - **Acceptance:** identifier and file-name slices of the eval improve; no
175
+ slice regresses by more than 1 point.
176
+ - **Risks:** FTS5 phrase syntax with punctuation-heavy tokens (code symbols
177
+ contain `_`, `.`); mitigate by tokenizing phrase content the same way FTS5
178
+ tokenizer does for indexed content.
179
+ - **Dependencies:** R4. **Effort:** S.
180
+
181
+ ### R3. Query-aware compression
182
+
183
+ - **Objective:** stop cutting the lines that answer the question.
184
+ - **Evidence:** Finding 12 (aggressive compression consistently harmful,
185
+ budget ≥ 8000, arXiv:2605.14503); SWE-Pruner (arXiv:2601.16746) 23-54% token
186
+ cut with task-conditioned selection; LLMLingua budget controller.
187
+ - **Files:** `src/compression/compressor.ts` (`TRUNCATION_LIMITS`, prompts),
188
+ `src/retrieval/retriever.ts` (packing uses
189
+ `chunk.compressedContent` length), `src/config.ts`, README config block.
190
+ - **Steps:**
191
+ 1. Add `compress(content, level, query?)`. With a query: keep signature
192
+ lines, import block, any line containing a query token or identifier, then
193
+ apply the cap to the remainder.
194
+ 2. Pass query terms into the Ollama summary prompt (`{query}` slot) so
195
+ summaries mention the asked-for symbols; keep the existing
196
+ `QualityChecker` identifier-ratio gate as the safety net.
197
+ 3. `full` becomes budget-driven: no fixed cap, only the retrieval token
198
+ budget trims it (aligns with the ≥ 8000 guidance).
199
+ 4. New config: `compression.query_aware: true` (default on), so users can
200
+ revert to the old fixed behavior.
201
+ - **Tests:** query tokens always survive truncation; identifier ratio stays
202
+ ≥ 0.4; without query, output is byte-identical to today (regression guard).
203
+ - **Acceptance:** at equal or lower served tokens, Recall@k does not drop; on
204
+ the bug-report slice it rises.
205
+ - **Risks:** keeping many matched lines can exceed caps; cap logic must prefer
206
+ matched lines over head lines, with a hard ceiling.
207
+ - **Dependencies:** R4. **Effort:** M.
208
+
209
+ ---
210
+
211
+ ## Phase 2: retrieval depth (P1)
212
+
213
+ ### R5. Multi-path retrieval with a structured symbol path
214
+
215
+ - **Objective:** stop relying on similarity alone for "where is X defined".
216
+ - **Evidence:** CodeRAG (arXiv:2509.16112) multi-path retrieval is a core gain;
217
+ the coding-agent preprint found production agents mix lexical, semantic and
218
+ structural signals.
219
+ - **Files:** `src/storage/graph-store.ts` (symbol lookup), `src/retrieval/
220
+ retriever.ts` (third RRF list), `src/retrieval/query-parser.ts` (extract
221
+ symbol candidates), `src/storage/local-backend.ts`.
222
+ - **Steps:**
223
+ 1. Add `findSymbol(name)`: exact then case-insensitive then prefix match
224
+ over graph node names (function/class/file nodes).
225
+ 2. Add file-path exact match as its own path using existing `fileHints`.
226
+ 3. Fuse vector, FTS and symbol results with the existing RRF (k=60); symbol
227
+ and path lists get the same weight class as `code_lookup`'s FTS boost.
228
+ 4. Keep `MAX_CHUNKS_PER_FILE` and marginal-ratio logic after fusion.
229
+ - **Tests:** exact symbol query returns its chunk in top-3; unknown symbol
230
+ falls back to current behavior; fusion does not double-count a chunk.
231
+ - **Acceptance:** identifier slice Recall@5 improves; general slice flat.
232
+ - **Risks:** symbol collisions across files (dedupe by file, prefer file hint).
233
+ **Dependencies:** R4, benefits from R1 headers. **Effort:** M.
234
+
235
+ ### R6. Small-to-big context expansion
236
+
237
+ - **Objective:** serve enough surrounding context per hit to answer without a
238
+ second tool call.
239
+ - **Evidence:** cross-file context length was the dominant parameter
240
+ (2048 → 8192 tokens = up to +4.2 points, arXiv:2605.04763); small-to-big
241
+ retrieval is the standard fix; cAST metadata retention added +2.7 on SWE-bench.
242
+ - **Files:** `src/indexer/chunker.ts` (emit module/top-level chunks),
243
+ `src/retrieval/retriever.ts` (expansion stage, packing),
244
+ `src/config.ts`.
245
+ - **Steps:**
246
+ 1. Emit, for every AST file, a `module` chunk covering top-level statements,
247
+ imports and exported names, so code outside functions is retrievable.
248
+ 2. After ranking, expand each top hit: attach the file's module chunk plus
249
+ one neighboring chunk above/below when budget allows. Mark provenance in
250
+ metadata (`served_via: self|module|neighbor`) for eval analysis.
251
+ 3. Expansion must run inside the `maxTokens` budget and must not break the
252
+ marginal-ratio stop (expanded context counts as part of the parent hit).
253
+ 4. Fix the interaction with `dedupeOverlaps`: a module chunk overlaps every
254
+ function chunk in the file and would be dropped. Exempt parent-tier chunks
255
+ from overlap dedupe, dedupe only within a tier.
256
+ - **Tests:** module chunk survives dedupe; expansion respects budget;
257
+ provenance recorded; concat property of module + function chunks still
258
+ reconstructs the file.
259
+ - **Acceptance:** cross-file and NL slices improve Recall@10 and distraction
260
+ ratio, with served tokens up by no more than the configured budget share.
261
+ - **Risks:** token creep; duplicate content between module and function chunks
262
+ wastes budget (mitigate: serve module chunk in compressed form by default).
263
+ **Dependencies:** R4, R3 (compression runs on expanded content). **Effort:** L.
264
+
265
+ ### R7. Graph context packs with a centrality prior
266
+
267
+ - **Objective:** make the repository graph a real retrieval path, not a
268
+ 2-chunk bonus.
269
+ - **Evidence:** RepoGraph (ICLR 2025) gained +2.66/+2.34 absolute on
270
+ SWE-bench-Lite across four systems at roughly constant cost; Aider's PageRank
271
+ file ranking was observed working in practice.
272
+ - **Files:** `src/storage/graph-store.ts`, `src/storage/local-backend.ts`,
273
+ `src/indexer/pipeline.ts` (compute prior at index time),
274
+ `src/retrieval/retriever.ts`, `src/extension.ts` (`related_context` output).
275
+ - **Steps:**
276
+ 1. At index time, run a small power-iteration PageRank over the file-level
277
+ import graph and store it as node `properties.pagerank`.
278
+ 2. In retrieval, build a context pack per top hit: the node's callers,
279
+ callees, importers and importees with edge types and line refs, capped at
280
+ N lines and included in the token budget.
281
+ 3. Apply centrality as a bounded prior: score multiplier in
282
+ `[1 - p, 1 + p]` with `retrieval.graph_prior` default `0.05`. Never let it
283
+ outrank a relevance gap larger than the prior.
284
+ 4. Extend `related_context` to return the same packs for manual drilling.
285
+ - **Tests:** pack contains at least one caller/importer on a fixture repo;
286
+ prior is clamped to its range; graph failures stay best-effort (existing
287
+ try/catch behavior).
288
+ - **Acceptance:** cross-file slice improves; cost per query stays within
289
+ +20 ms of baseline warm latency.
290
+ - **Risks:** dense graphs flood the pack (cap by degree, prefer higher-ranked
291
+ neighbors); PageRank staleness on incremental reindex (recompute per
292
+ full index, reuse on partial).
293
+ - **Dependencies:** R4, R6 (packs ride the same budget path). **Effort:** M.
294
+
295
+ ### R8. Query-confidence signal and optional elaboration
296
+
297
+ - **Objective:** tell the agent when a search was weak, and let it recover,
298
+ without silent query rewriting.
299
+ - **Evidence:** predicted query performance correlates with answer quality
300
+ (arXiv:2507.10411); Findings 1/2/4 (raw query wins, elaborate only when
301
+ ambiguous, LLM rewrites drift); Search-R1 for the loop framing.
302
+ - **Files:** `src/retrieval/confidence.ts`, `src/retrieval/query-parser.ts`,
303
+ `src/guidance.ts`, `src/extension.ts` (tool result shape), `src/config.ts`.
304
+ - **Steps:**
305
+ 1. Compute query-level signals: top final score, margin to the next distinct
306
+ file, vector/FTS agreement (Jaccard of their top-10), candidate count.
307
+ 2. Map to `query_confidence: high|medium|low` with thresholds calibrated
308
+ against the eval set, stored in config so they are tunable.
309
+ 3. Return the signal plus 1-3 deterministic recovery hints (add a file path,
310
+ use an exact symbol, drop vague terms) when confidence is low.
311
+ 4. Optional elaboration behind `retrieval.auto_elaborate` (default `false`):
312
+ generate a deterministic variant (stopword-stripped, file hints injected,
313
+ terms from near-miss top hits), run both, fuse with RRF, always keep the
314
+ raw query's results as primary.
315
+ 5. Document the signal and the loop in the guidance section so the agent
316
+ knows to refine instead of re-reading files.
317
+ - **Tests:** weak synthetic query gets `low` and hints; strong identifier query
318
+ gets `high`; elaboration never removes raw-query results; no network calls.
319
+ - **Acceptance:** guidance renders, config off switches everything off, eval
320
+ unchanged when elaboration is disabled.
321
+ - **Risks:** noisy confidence on tiny indexes (calibrate on the fixture, floor
322
+ the candidate count). **Dependencies:** R4. **Effort:** M.
323
+
324
+ ### R9. Refine suggestions in search results
325
+
326
+ - **Objective:** turn a one-shot tool into a guided loop at near-zero token cost.
327
+ - **Evidence:** every production coding agent in the exploratory preprint
328
+ narrows iteratively; RepoCoder's loop drives its reported gain.
329
+ - **Files:** `src/extension.ts` (result shaping), optionally
330
+ `src/engine.ts`; guidance text in `src/guidance.ts`.
331
+ - **Steps:**
332
+ 1. From the top hits, derive up to 3 next queries: a sibling symbol from the
333
+ same file (graph `defines` edges), a near-miss file ranked 11-15 that was
334
+ not served, one alternate term that appears often in top hits.
335
+ 2. Emit as a single `refine:` line in the tool result; cap length.
336
+ 3. Config `retrieval.refine_hints` (default `true`), documented in README.
337
+ - **Tests:** cap of 3; never suggests a file already served; disabled by config.
338
+ - **Acceptance:** token overhead ≤ 40 tokens per call; no eval regression.
339
+ - **Risks:** hints duplicating what the agent would do anyway (measure usage in
340
+ session telemetry later). **Dependencies:** R5, R7 for better symbol/family
341
+ hints (can ship with weaker heuristics first). **Effort:** S.
342
+
343
+ ---
344
+
345
+ ## Phase 3: gated experiments (P2)
346
+
347
+ Each phase-3 item ships **behind config, default off**, and only graduates to
348
+ default-on if the eval shows a win and latency budget holds.
349
+
350
+ ### R10. Opt-in local reranker
351
+
352
+ - **Objective:** test whether cross-encoder reranking helps *our* workloads.
353
+ - **Evidence is split, on purpose:** harmful for SE tasks with 815 ms GPU cost
354
+ (Findings 10/11), a core gain in CodeRAG, −67% failures with Anthropic's
355
+ stack. Only our eval can decide.
356
+ - **Files:** new `src/retrieval/reranker.ts`, `src/config.ts`, README.
357
+ - **Steps:**
358
+ 1. Use the already-shipped `@huggingface/transformers` for
359
+ `text-classification` with an ONNX reranker (candidate:
360
+ `Xenova/ms-marco-MiniLM-L-6-v2`), cached in the existing models dir.
361
+ 2. Rerank only the fused candidate list (cap 30), then re-apply path penalty,
362
+ file diversity and marginal stop.
363
+ 3. Config: `retrieval.rerank: off | local` (default `off`),
364
+ `retrieval.rerank_model`, `retrieval.rerank_topn`.
365
+ 4. One-time model download must be explicit (never on by default) because it
366
+ adds ~80 MB to a package that markets "one npm command".
367
+ - **Tests:** mock pipeline ordering (no model in unit tests); opt-in path
368
+ exercised in a smoke test with a stubbed model.
369
+ - **Acceptance:** measured eval delta (up or down) and p95 latency recorded in
370
+ the PR; default stays `off` unless quality wins justify the download.
371
+ - **Risks:** CPU latency; ONNX export availability for the chosen model.
372
+ **Dependencies:** R4 mandatory. **Effort:** M.
373
+
374
+ ### R11. Code-specialized embedder option
375
+
376
+ - **Objective:** offer, and only recommend, a code-trained local embedder.
377
+ - **Evidence:** Findings 7/8 say scale and code-specialization did not decide
378
+ outcomes, but CORE-Bench shows agentic retrieval is harder and in-domain
379
+ training helped a lot.
380
+ - **Files:** `src/indexer/embedder.ts` (model list), README embedding section.
381
+ - **Steps:**
382
+ 1. Document `embedding.model: <hf-id>` with one tested code-oriented model
383
+ and the existing general defaults.
384
+ 2. Run the eval for each backend; publish the table in README.
385
+ 3. Dimension change already forces full reindex; verify the migration path.
386
+ - **Tests:** backend selection unit test; reindex-on-dimension-change test
387
+ (logic exists, add a regression test if missing).
388
+ - **Acceptance:** README shows measured numbers per model, not claims.
389
+ - **Dependencies:** R4. **Effort:** S.
390
+
391
+ ### R12. AST density chunking (cAST-style split-merge)
392
+
393
+ - **Objective:** replace worst-measured strategy (whole function nodes) with
394
+ budgeted, self-contained chunks.
395
+ - **Evidence:** function chunking −3.57 to −5.64 points and never
396
+ Pareto-optimal (arXiv:2605.04763); cAST +4.3 Recall@5, +2.67 Pass@1
397
+ (arXiv:2506.15655).
398
+ - **Files:** `src/indexer/chunker.ts` (core rework), `src/types.ts`
399
+ (metadata: `symbol`, `parent`, `segment`), `src/config.ts`, chunk-id scheme
400
+ in `makeChunk`.
401
+ - **Steps:**
402
+ 1. Top-down fit: a node fits if its non-whitespace chars ≤
403
+ `indexer.chunk_budget_chars` (default ~1200, configurable).
404
+ 2. Oversized nodes split at syntactic children; only leaf-level overflow uses
405
+ line windows. Greedy-merge small adjacent siblings up to budget.
406
+ 3. Emit module/top-level chunks (shared with R6).
407
+ 4. Preserve chunk-id uniqueness for split segments (add segment index; keep
408
+ the upstream column-offset fix that the README documents).
409
+ 5. Keep the legacy chunker behind `indexer.chunking: legacy | ast-density`
410
+ (default `legacy`) for A/B on the same index format.
411
+ 6. Verify the concat property: chunks concatenated reproduce the file.
412
+ - **Tests:** concat round-trip; id uniqueness on duplicate spans; budget
413
+ respected; legacy mode byte-identical to today.
414
+ - **Acceptance:** `ast-density` wins the eval on at least Recall@10 without
415
+ inflating chunk count beyond +30%; then flip the default in its own release.
416
+ - **Risks:** more chunks = more embeddings = slower first index (content-hash
417
+ cache limits repeat cost); id scheme changes force a full reindex.
418
+ **Dependencies:** R4, R6 (module chunks shared), ideally after R1 so headers
419
+ apply to the new chunk shapes. **Effort:** L.
420
+
421
+ ### R13. Linked memory (A-MEM style)
422
+
423
+ - **Objective:** make cross-session recall improve with age instead of
424
+ degrading.
425
+ - **Evidence:** A-MEM (NeurIPS 2025): dynamic indexing, linking between
426
+ memories and memory evolution beat fixed structures on six models.
427
+ - **Files:** `src/memory/db.ts`, `decision-extractor.ts`,
428
+ `session-capture.ts`, `record_decision` tool surface in `extension.ts`.
429
+ - **Steps:**
430
+ 1. On `record_decision`: embed it, find top-3 past decisions above a
431
+ similarity threshold, store `related_to` edges (new edge type alongside
432
+ `decided`/`discussed_in`), cap links per memory.
433
+ 2. Store structured attributes on write: keywords, tags, affected files
434
+ (files already feed `record_code_area`).
435
+ 3. Memory evolution: allow a later decision to annotate an earlier one
436
+ (`relatedTo` tool parameter) instead of only appending rows; keep the
437
+ original text immutable, append annotations with timestamps.
438
+ 4. Surface links in `session_recall` output so the agent can walk them.
439
+ - **Tests:** threshold and cap behavior; no self-links; redaction still applies
440
+ before any write (PII guard must run before link text is stored).
441
+ - **Acceptance:** a seeded recall test shows linked neighbours surfacing;
442
+ latency of `record_decision` stays under +100 ms.
443
+ - **Risks:** link noise from loose thresholds (calibrate on a fixture set).
444
+ **Dependencies:** R4-style eval extended to memory recall. **Effort:** M.
445
+
446
+ ---
447
+
448
+ ## Cross-cutting work
449
+
450
+ - **Index format versioning:** R1, R6 and R12 each change on-disk content.
451
+ Add `manifest.formatVersion`; bump it per item and force a full reindex with
452
+ a logged migration line (the dimension-change path in `pipeline.ts` is the
453
+ template).
454
+ - **Config surface:** new keys land in `src/config.ts`, the README
455
+ Configuration block, and the guidance text in one PR per item. Planned keys:
456
+ - `compression.query_aware`, `retrieval.auto_elaborate`,
457
+ `retrieval.graph_prior`, `retrieval.refine_hints`,
458
+ `retrieval.rerank`, `retrieval.rerank_model`, `retrieval.rerank_topn`,
459
+ `indexer.chunk_budget_chars`, `indexer.chunking`.
460
+ - **Guidance updates:** R8 and R9 change how the agent should use the tool, so
461
+ `src/guidance.ts` must document the confidence signal and refine hints in the
462
+ same release, keeping the ~900-token budget.
463
+ - **Docs:** README tables (What you get, How it works, Configuration,
464
+ Differences from upstream) updated per release; research evidence lives in
465
+ `RESEARCH-RETRIEVAL.md`, build detail here.
466
+ - **Releases:** every item ships through a PR to `main` and a `v*.*.*` tag per
467
+ the packages AGENTS.md recipe. Suggested grouping, adjustable once baselines
468
+ exist:
469
+ - **0.2.0**: R4 + R1 + R2 + R3 (measurement + phase 1), format version bump.
470
+ - **0.3.0**: R5 + R6 + R7 (retrieval depth).
471
+ - **0.4.0**: R8 + R9 (agentic loop).
472
+ - **0.5.0+**: whichever of R10-R13 wins its experiment, one per release.
473
+ - **Definition of done (per item):** `npm run check` green, eval deltas in the
474
+ PR, README/config/guidance updated, status table in this file flipped.
475
+
476
+ ---
477
+
478
+ ## Explicit non-goals
479
+
480
+ Recorded so they are not re-litigated later:
481
+
482
+ - **No always-on LLM query rewriting** (Finding 4, +987 tokens per rewrite).
483
+ - **No default reranking** (Findings 10-11; R10 stays opt-in).
484
+ - **No aggressive compression defaults** (Finding 12).
485
+ - **No chunk overlap** by default (two evaluations found no clear benefit).
486
+ - **No bigger-embedder reflex** (Findings 7-8); R11 must win on the eval.
487
+ - **No full-file indexing** as a substitute for retrieval (Anthropic notes it
488
+ only makes sense under ~200k tokens).
489
+
490
+ ---
491
+
492
+ ## Open questions that gate work
493
+
494
+ 1. Does the function-chunking penalty transfer to our question workload?
495
+ R4 then R12 answer this with data.
496
+ 2. Which side of the reranking debate applies to pi? R10 answers it.
497
+ 3. Should confidence weights (0.5 vector / 0.4 keyword / 0.1 recency) be
498
+ fitted on the eval set instead of fixed? A cheap follow-up after R4.
499
+ 4. Do refine suggestions change agent behavior in real sessions? Needs opt-in
500
+ session telemetry after R9.