purecontext-mcp 1.1.1 → 1.1.2

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/docs/dev/API_STABILITY.md +0 -319
  3. package/docs/dev/DECISIONS.md +0 -22
  4. package/docs/dev/DOCUMENTATION_PLAN.md +0 -113
  5. package/docs/dev/PHASE10_TASKS.md +0 -476
  6. package/docs/dev/PHASE11_TASKS.md +0 -385
  7. package/docs/dev/PHASE12_TASKS.md +0 -335
  8. package/docs/dev/PHASE13_TASKS.md +0 -381
  9. package/docs/dev/PHASE14_TASKS.md +0 -371
  10. package/docs/dev/PHASE15_TASKS.md +0 -256
  11. package/docs/dev/PHASE16_TASKS.md +0 -314
  12. package/docs/dev/PHASE17_TASKS.md +0 -321
  13. package/docs/dev/PHASE18_TASKS.md +0 -345
  14. package/docs/dev/PHASE19_TASKS.md +0 -261
  15. package/docs/dev/PHASE1_TASKS.md +0 -443
  16. package/docs/dev/PHASE20_TASKS.md +0 -280
  17. package/docs/dev/PHASE21_TASKS.md +0 -355
  18. package/docs/dev/PHASE22_TASKS.md +0 -371
  19. package/docs/dev/PHASE23_TASKS.md +0 -274
  20. package/docs/dev/PHASE24_TASKS.md +0 -326
  21. package/docs/dev/PHASE25_TASKS.md +0 -452
  22. package/docs/dev/PHASE26_TASKS.md +0 -253
  23. package/docs/dev/PHASE27_TASKS.md +0 -410
  24. package/docs/dev/PHASE2_TASKS.md +0 -328
  25. package/docs/dev/PHASE3_TASKS.md +0 -571
  26. package/docs/dev/PHASE4_TASKS.md +0 -531
  27. package/docs/dev/PHASE5_TASKS.md +0 -835
  28. package/docs/dev/PHASE6_TASKS.md +0 -347
  29. package/docs/dev/PHASE7_TASKS.md +0 -257
  30. package/docs/dev/PHASE8_TASKS.md +0 -299
  31. package/docs/dev/PHASE9_TASKS.md +0 -320
  32. package/docs/dev/PureContext_MCP_PRD_v1.0.docx +0 -0
  33. package/docs/dev/SELF_HOSTING.md +0 -142
  34. package/docs/dev/TEAM_SETUP.md +0 -316
  35. package/docs/dev/TELEMETRY.md +0 -99
  36. package/docs/dev/feature-analysis.md +0 -305
  37. package/docs/dev/phase-1-notes.md +0 -3
@@ -1,371 +0,0 @@
1
- # Phase 14 — Task Breakdown
2
-
3
- **Goal**: Fix PureContext's keyword search quality, which the Phase 14 competitive benchmark exposed as 0% Precision@1 and 0% Recall@5 against natural-language queries — while jcodemunch-mcp achieves 25% P@1 and 66.7% R@5 on the same task corpus. This phase closes that gap by wiring in the existing FTS5 infrastructure, adding a proper query preprocessor, and implementing a relevance re-ranking layer.
4
-
5
- **Scope rationale**: The benchmark revealed three root causes:
6
-
7
- | Root cause | Where | Impact |
8
- |---|---|---|
9
- | FTS5 table never populated | `index-manager.ts` never calls `bulkUpsertFtsSymbols` | No full-text index exists |
10
- | LIKE-only keyword search | `search-symbols.ts` uses `name LIKE '%query%'` | Multi-word queries find nothing |
11
- | No relevance ranking | Results sorted by name, not relevance | Wrong symbols ranked first |
12
-
13
- All three fixes are self-contained and carry no architectural risk — the FTS5 schema, `bulkUpsertFtsSymbols`, `ftsSearchSymbols`, and the FTS5 DDL already exist. This phase wires them together and adds the scoring layer.
14
-
15
- **Benchmark baseline (to beat):**
16
-
17
- | Metric | jcodemunch | PureContext before | PureContext target |
18
- |--------|-----------|-----------------|-----------------|
19
- | Search P@1 (keyword) | 25.0% | 0.0% | ≥ 50% |
20
- | Search R@5 (keyword) | 66.7% | 0.0% | ≥ 80% |
21
- | Token efficiency | 99.7% | 99.8% | ≥ 99.7% |
22
-
23
- ---
24
-
25
- ## Task 108: FTS5 Population Pipeline
26
-
27
- Wire `bulkUpsertFtsSymbols` into the indexing pipeline so the FTS5 table is populated on every full index and incremental re-index.
28
-
29
- **Root cause**: `indexFolder` and `reindexFiles` in `src/core/index-manager.ts` insert symbols into the `symbols` table but never call `bulkUpsertFtsSymbols`. The `fts_symbols` table is therefore always empty for any repo indexed without the semantic path.
30
-
31
- **Deliverables:**
32
-
33
- - `src/core/index-manager.ts`:
34
- - In the **full index path** (after `insertSymbols`): call `bulkUpsertFtsSymbols(db, repoId, allSymbols)` for the batch of newly inserted symbols.
35
- - In the **incremental path** (`reindexFiles`): for each file being reindexed, call `deleteFtsByFile(db, repoId, filePath)` before re-inserting, then call `bulkUpsertFtsSymbols(db, repoId, newSymbols)` for the file's new symbols.
36
- - When a file is deleted from the index, call `deleteFtsByFile` to remove stale FTS entries.
37
-
38
- - `src/core/db/symbol-store.ts`:
39
- - Enrich the FTS content string: currently `${name} ${signature} ${summary}`. Extend to also include the file path stem (last path component without extension) so queries like "handler" find `typescript-handler.ts` symbols.
40
- - New format: `${name} ${fileStem} ${signature} ${summary}`
41
- - No schema change needed — content is a plain text column.
42
-
43
- - `test/core/fts-population.test.ts`:
44
- - After a full index of `test/fixtures/basic-ts-project/`: verify `SELECT COUNT(*) FROM fts_symbols WHERE repo_id = ?` equals the symbol count.
45
- - After an incremental re-index (modify one file): verify old FTS entries for that file are removed and new ones added.
46
- - After a file deletion: verify its FTS entries are removed.
47
-
48
- **Key technical notes:**
49
- - FTS5 `INSERT` is fast (< 1ms per row typical). Even 5000 symbols adds < 50ms overhead.
50
- - The `bulkUpsertFtsSymbols` function already runs in a single transaction — no additional batching needed.
51
- - File stem in content is intentionally low-weight: it will rarely be the deciding factor but helps for queries like "discover files" → `file-discovery.ts`.
52
-
53
- **Verify:** Index the PureContext repo. `SELECT COUNT(*) FROM fts_symbols WHERE repo_id = ?` should equal `SELECT COUNT(*) FROM symbols WHERE repo_id = ?`.
54
-
55
- **Tests:** FTS population count matches symbol count. Incremental update replaces file entries. Deletion removes entries. File stem appears in FTS content.
56
-
57
- ---
58
-
59
- ## Task 109: FTS5 Keyword Search Integration
60
-
61
- Replace the `name LIKE '%query%'` path in `search-symbols.ts` with `ftsSearchSymbols`, making multi-word natural-language queries work.
62
-
63
- **Root cause**: `search-symbols.ts` calls `searchSymbols(db, repoId, query)` which does:
64
- ```sql
65
- SELECT * FROM symbols WHERE repo_id = ? AND name LIKE ?
66
- ```
67
- with `%query%`. "orchestrate indexing pipeline" never matches any symbol name as a substring. `ftsSearchSymbols` uses FTS5 `MATCH` which tokenizes the query and finds documents containing those tokens anywhere in `name + fileStem + signature + summary`.
68
-
69
- **Deliverables:**
70
-
71
- - `src/server/tools/search-symbols.ts`:
72
- - In the keyword mode path, replace `searchSymbols(db, ...)` with `ftsSearchSymbols(db, repoId, sanitizedQuery, limit)`.
73
- - Keep `searchSymbols` (LIKE) as a fallback: if `ftsSearchSymbols` throws or the FTS table is empty (count = 0), fall back to LIKE. Log a warning when falling back.
74
- - Add a `_meta.search_mode` field to the response: `'fts'` or `'like_fallback'` so callers can distinguish.
75
-
76
- - `src/core/db/symbol-store.ts` — `ftsSearchSymbols`:
77
- - Apply `kind` filter by joining to the `symbols` table after FTS match (already does this via JOIN; add `AND s.kind = ?` when kind is provided).
78
- - Apply `filePath` filter: `AND s.file_path LIKE ?` when filePath is provided.
79
- - Use `bm25(fts_symbols)` for ordering instead of the default `rank` to get proper BM25 relevance ordering.
80
-
81
- - `src/core/db/symbol-store.ts` — empty FTS guard:
82
- ```typescript
83
- export function hasFtsIndex(db: Database.Database, repoId: string): boolean
84
- ```
85
- Returns true if `SELECT COUNT(*) FROM fts_symbols WHERE repo_id = ?` > 0. Used by search-symbols.ts to decide whether to use FTS or LIKE.
86
-
87
- - `test/server/search-fts.test.ts`:
88
- - Fixture repo indexed → `search_symbols` with multi-word query returns expected symbol.
89
- - Empty FTS table → falls back to LIKE, sets `search_mode: 'like_fallback'`.
90
- - Kind filter applied correctly in FTS mode.
91
- - FilePath filter applied correctly in FTS mode.
92
-
93
- **Key technical notes:**
94
- - FTS5 MATCH syntax: multi-word query `"orchestrate indexing pipeline"` → pass as `orchestrate indexing pipeline` (space-separated, FTS5 treats as implicit AND by default in SQLite's FTS5).
95
- - Special characters in user queries (`"`, `*`, `(`, `)`) must be escaped before passing to MATCH — handled in Task 110.
96
- - `bm25(fts_symbols)` returns a negative float; ORDER BY it ASC gives most-relevant first.
97
-
98
- **Verify:** Index the PureContext repo. `search_symbols("orchestrate indexing pipeline")` returns `indexFolder` in top 5.
99
-
100
- **Tests:** Multi-word query returns results. Single-word query still works. Kind filter works in FTS mode. Fallback triggers when FTS empty. bm25 ordering: exact name match ranked higher than summary-only match.
101
-
102
- ---
103
-
104
- ## Task 110: Query Preprocessing & CamelCase Tokenization
105
-
106
- Add a query preprocessor that expands camelCase identifiers and normalizes queries before passing to FTS5, dramatically improving recall for code-specific queries.
107
-
108
- **Problem**: FTS5 tokenizes on whitespace and punctuation but not on camelCase boundaries. The query `parseFile` is one FTS5 token; it won't match a document containing `parse` and `file` separately. And a query like "parse source file" won't match the name `parseFile` unless it's in the signature/summary.
109
-
110
- **Deliverables:**
111
-
112
- - `src/core/search/query-preprocessor.ts` (new file):
113
- ```typescript
114
- export function preprocessQuery(raw: string): string
115
- ```
116
- Pipeline:
117
- 1. **Trim & lowercase** for comparison; keep original case for FTS (FTS5 is case-insensitive by default in SQLite).
118
- 2. **camelCase split**: `parseFile` → `parseFile OR parse OR file`. `HybridSearcher` → `HybridSearcher OR Hybrid OR Searcher`.
119
- 3. **snake_case split**: `get_symbol_source` → `get_symbol_source OR get OR symbol OR source`.
120
- 4. **Special char escape**: strip or escape `"()^*` that would break FTS5 MATCH.
121
- 5. **Short token filter**: drop tokens < 2 chars after splitting to avoid noise.
122
- 6. **Phrase handling**: if the raw query is a multi-word phrase (not a camelCase identifier), pass tokens as implicit AND: `orchestrate indexing pipeline` → `orchestrate indexing pipeline` (FTS5 already treats space as AND).
123
- 7. Return the preprocessed FTS5 query string.
124
-
125
- Edge cases:
126
- - All-caps acronyms (`MCP`, `HTTP`) → treat as single token, don't split.
127
- - Numbers in identifiers (`phase14`, `v2`) → keep as-is.
128
- - Already space-separated natural language → pass through (maybe with individual word expansion if words look like camelCase).
129
-
130
- - `src/server/tools/search-symbols.ts`:
131
- - Apply `preprocessQuery(args.query)` before passing to `ftsSearchSymbols`.
132
-
133
- - `test/core/query-preprocessor.test.ts`:
134
- - `parseFile` → includes `parse` and `file` in output.
135
- - `HybridSearcher` → includes `hybrid` and `searcher`.
136
- - `orchestrate indexing pipeline` → passes through unchanged (natural language).
137
- - `get_symbol_source` → includes `get`, `symbol`, `source`.
138
- - `"dangerous query"` → special chars escaped.
139
- - Short tokens filtered: `a` dropped, `go` kept.
140
-
141
- **Key technical notes:**
142
- - The preprocessor produces an FTS5 query string, not a list of tokens. The format must be valid FTS5 MATCH syntax.
143
- - Use OR expansion (not AND) for camelCase splits so that a document matching any component is included — then the re-ranking layer (Task 111) promotes exact name matches.
144
- - This is stateless pure string transformation — no DB access, trivially unit testable.
145
-
146
- **Verify:** `search_symbols("parseFile")` returns `parseFile` in top 3. `search_symbols("indexFolder")` returns `indexFolder` at rank 1. `search_symbols("orchestrate indexing pipeline")` returns `indexFolder` in top 5.
147
-
148
- **Tests:** camelCase splitting, snake_case splitting, FTS5 escaping, natural language passthrough, short token filter. Integration: preprocessor hooked up in search-symbols.ts.
149
-
150
- ---
151
-
152
- ## Task 111: Relevance Re-Ranking
153
-
154
- Add a post-FTS5 scoring layer that promotes exact and prefix name matches to the top, ensuring that when an agent queries `indexFolder` they get `indexFolder` at rank 1, not a summary-match ranked higher by BM25.
155
-
156
- **Problem**: BM25 ranks by term frequency across all FTS content fields. A symbol with "index" appearing 5 times in a long summary may outrank `indexFolder` whose name is an exact match. jcodemunch explicitly scores: exact name (20pts) > name contains (10pts) > name word overlap (5pts each) > signature match > summary match.
157
-
158
- **Deliverables:**
159
-
160
- - `src/core/search/relevance-ranker.ts` (new file):
161
- ```typescript
162
- export interface ScoredSymbol {
163
- symbol: SymbolRecord;
164
- score: number;
165
- matchReason: 'exact_name' | 'prefix_name' | 'name_contains' | 'word_overlap' | 'content_match';
166
- }
167
-
168
- export function rankSymbols(symbols: SymbolRecord[], query: string): ScoredSymbol[]
169
- ```
170
-
171
- Scoring (additive, higher = more relevant):
172
- | Rule | Points |
173
- |------|--------|
174
- | Exact name match (case-insensitive) | 100 |
175
- | Name starts with query | 60 |
176
- | Name contains query as substring | 40 |
177
- | All query words appear in name | 30 |
178
- | Any query word is exact name match | 20 |
179
- | Any query word appears in name | 10 per word |
180
- | Query phrase in signature | 8 |
181
- | Any query word in signature | 2 per word |
182
- | Query phrase in summary | 5 |
183
- | Any query word in summary | 1 per word |
184
-
185
- For camelCase queries, scoring runs against both the raw query and the split tokens.
186
-
187
- Returns `ScoredSymbol[]` sorted by score descending; ties broken by FTS5 order (already BM25-ranked).
188
-
189
- - `src/server/tools/search-symbols.ts`:
190
- - After `ftsSearchSymbols` returns results, apply `rankSymbols(results, args.query)`.
191
- - Include `score` and `matchReason` in the response JSON for each symbol (helps agents understand why a result was returned).
192
- - Apply ranking in LIKE fallback mode too (re-rank the LIKE results).
193
-
194
- - `test/core/relevance-ranker.test.ts`:
195
- - `indexFolder` as query against a set containing `indexFolder`, `indexRepo`, `getBlastRadius` → `indexFolder` ranked 1st.
196
- - Multi-word query "blast radius" → `getBlastRadius` (name words overlap) ranked above `buildGraph` (no overlap).
197
- - Exact match always beats prefix which beats contains which beats word overlap.
198
- - Tie-breaking: when scores equal, original FTS order preserved.
199
-
200
- **Key technical notes:**
201
- - Scoring is pure in-memory (no DB access). Input is the FTS5 result set (already filtered), typically ≤ 50 items.
202
- - The `matchReason` field documents the highest-scoring rule that fired (not all rules).
203
- - Including score in MCP responses is intentional — agents can use it to decide whether to fetch source for a result.
204
-
205
- **Verify:** `search_symbols("blast radius")` → `getBlastRadius` at rank 1, score ≥ 30. `search_symbols("indexFolder")` → `indexFolder` at rank 1 with matchReason `exact_name`.
206
-
207
- **Tests:** Scoring rules fire correctly. Ranking order matches expected. Multi-word queries. Tie-breaking. Score and matchReason in response.
208
-
209
- ---
210
-
211
- ## Task 112: Symbol Count & IndexResult Stats Fix
212
-
213
- Fix the `getSymbolsByRepo` default-limit bug that causes the repos table to record a capped symbol count (1000) instead of the actual count.
214
-
215
- **Root cause (`src/core/index-manager.ts` line ~346):**
216
- ```typescript
217
- const totalSymbols = getSymbolsByRepo(db, repoId).length; // limit defaults to 1000
218
- ```
219
- Any repo with > 1000 symbols stores 1000 in the repos table. The benchmark exposed this: a repo with 3832 symbols was reported as 1000.
220
-
221
- **Deliverables:**
222
-
223
- - `src/core/index-manager.ts`:
224
- - Replace `getSymbolsByRepo(db, repoId).length` with a direct `COUNT(*)` query:
225
- ```typescript
226
- const totalSymbols = (
227
- db.prepare<[string], { c: number }>('SELECT COUNT(*) AS c FROM symbols WHERE repo_id = ?')
228
- .get(repoId)?.c ?? 0
229
- );
230
- ```
231
- - Same fix for the `reindexFiles` path.
232
-
233
- - `src/core/types.ts` — `IndexResult`:
234
- - Add `totalSymbolsInDb: number` — the COUNT(*) from the DB after indexing (not just this run's `symbolsFound`).
235
- - Add `totalFilesInDb: number` — same for files.
236
- - Keep existing `filesIndexed`, `symbolsFound` (incremental stats for this run).
237
-
238
- - `benchmarks/harness/run_purecontext.ts`:
239
- - Remove the workaround `COUNT(*)` query added in the harness; use `indexResult.totalSymbolsInDb` instead.
240
-
241
- - `benchmarks/harness/compare.ts`:
242
- - Fix "Winner" display for ties: when values are equal, show `—` instead of a tool name.
243
-
244
- - `test/core/index-stats.test.ts`:
245
- - Index a fixture with > 10 symbols. Verify `IndexResult.totalSymbolsInDb` matches `SELECT COUNT(*) FROM symbols`.
246
- - Verify repos table stores the correct count (not capped at 1000).
247
- - Incremental re-index: verify totalSymbolsInDb reflects the full DB count, not just the files processed.
248
-
249
- **Key technical notes:**
250
- - This is a one-line fix but has visible impact on the benchmark and on any UI/tool that reads `repo.symbolCount`.
251
- - `IndexResult.symbolsFound` remains the incremental "how many new symbols did this run find" — useful for progress reporting. `totalSymbolsInDb` is the authoritative total.
252
-
253
- **Verify:** Index the PureContext repo (3800+ symbols). `list_repos` returns the correct symbol count. `IndexResult.totalSymbolsInDb` equals `SELECT COUNT(*) FROM symbols WHERE repo_id = ?`.
254
-
255
- **Tests:** Symbol count stored correctly for repos > 1000 symbols. `totalSymbolsInDb` vs `symbolsFound` semantics verified. Tie display in compare.ts.
256
-
257
- ---
258
-
259
- ## Task 113: Ground Truth Expansion
260
-
261
- Expand the benchmark ground truth from 12 to 25+ tasks, add coverage for adapters and server tools, and re-run the benchmark to validate Phase 14 improvements.
262
-
263
- **Deliverables:**
264
-
265
- - `benchmarks/ground-truth.json`:
266
- - Expand from 12 to 25+ tasks covering:
267
- - **Core pipeline**: indexFolder, parseFile, discoverFiles, startWatching, buildGraph, getBlastRadius
268
- - **Search layer**: searchSymbols, ftsSearchSymbols, HybridSearcher, rankSymbols (new)
269
- - **Server tools**: handler functions in search-symbols, get-symbol-source, get-file-outline, find-dead-code
270
- - **Adapters**: vuePreproccesor, expressAdapter, reactHandler (tests coverage of framework layer)
271
- - **Config**: getConfig, loadConfig
272
- - **Error types**: PureContextError, ParseError, StorageError
273
- - Each task: `id`, `query` (natural language), `expected_symbol`, `expected_file`, `description`.
274
- - Queries should represent realistic agent usage — not reverse-engineered from symbol names.
275
-
276
- - `benchmarks/harness/run_purecontext.ts`:
277
- - After Phase 14 fixes, Dim 2 should report meaningful P@1 / R@5.
278
- - Add `dim2.by_query` detail: for each task, show the actual top-5 names returned (aids debugging).
279
- - Add latency stats: median and p95 query time across all Dim 2 tasks.
280
-
281
- - `benchmarks/harness/run_jmunch.py`:
282
- - Mirror the `dim2.by_query` detail addition.
283
-
284
- - `benchmarks/harness/compare.ts`:
285
- - In Dim 2 table: add "Top-5 results" column (expandable `<details>` per row) showing actual symbol names returned by each tool.
286
- - Add Dim 2 improvement summary box showing delta vs Phase 14 baseline.
287
-
288
- - `benchmarks/results/comparison_v2.md`:
289
- - Full benchmark re-run after all Phase 14 tasks complete.
290
- - Expected improvement documented.
291
-
292
- **Key technical notes:**
293
- - Ground truth queries must be written before running the benchmark (not after looking at results) to avoid overfitting.
294
- - Negative precision is not tested in this phase — all 25+ tasks are positive (expected symbol should be found).
295
- - The `by_query` debug output in the JSON is diagnostic only; it shows what the tool actually returned, not what was expected.
296
-
297
- **Verify:** `npm run bench:purecontext` produces P@1 ≥ 50% and R@5 ≥ 80% on the expanded 25-task corpus.
298
-
299
- **Tests:** Ground truth file validates against JSON schema. All expected symbols exist in the indexed repo. Benchmark re-run completes without errors. Improvement over baseline documented.
300
-
301
- ---
302
-
303
- ## Task 114: Phase 14 Integration Tests
304
-
305
- Validate all Phase 14 changes with focused integration tests that exercise the full search path from query string to ranked results.
306
-
307
- **Deliverables:**
308
-
309
- - `test/server/search-quality.test.ts`:
310
- - Index the `test/fixtures/basic-ts-project/` fixture.
311
- - For each of the 12 original ground-truth queries, call the `search_symbols` handler directly.
312
- - Assert: expected symbol appears in top 5 results (R@5 gate).
313
- - Assert: for the highest-quality queries (exact name fragments), symbol appears at rank 1 (P@1 gate).
314
- - Assert: `_meta.search_mode` is `'fts'` (not `'like_fallback'`).
315
- - Assert: each result includes `score` and `matchReason` fields.
316
-
317
- - `test/core/search-pipeline.test.ts`:
318
- - End-to-end pipeline test: `preprocessQuery` → `ftsSearchSymbols` → `rankSymbols`.
319
- - Test vectors covering: camelCase query, natural language query, snake_case query, single-word query, empty query (expect empty results, not error).
320
-
321
- - `test/core/fts-lifecycle.test.ts`:
322
- - Full lifecycle: index → modify file → re-index → verify FTS reflects new symbols, old symbols removed.
323
- - Delete file → verify FTS entries cleaned up.
324
- - Empty query → safe (no FTS5 syntax error).
325
-
326
- - Regression: existing tests must still pass:
327
- - `npm test` green.
328
- - Token efficiency: Dim 1 benchmark should not regress below 99.5% reduction.
329
-
330
- **Key technical notes:**
331
- - The `basic-ts-project` fixture may need a few more symbols added to be useful as a search quality fixture — add 3–5 well-named functions with distinct summaries if coverage is thin.
332
- - P@1 gate in tests should be ≥ 8/12 (67%) — this is the minimum acceptable bar; Task 113's target is ≥ 50% on the harder 25-task corpus.
333
- - Integration tests are the acceptance criteria for this phase.
334
-
335
- **Verify:** `npm test` passes. Search quality tests achieve P@1 ≥ 67% on the 12-task fixture set. FTS lifecycle tests pass. Token efficiency not regressed.
336
-
337
- ---
338
-
339
- ## Order of Execution
340
-
341
- ```
342
- Task 108: FTS5 population pipeline ████░░░░░░ Prerequisite (no FTS = nothing works)
343
- Task 109: FTS5 keyword integration ██████░░░░ Core fix (wire FTS5 into search path)
344
- Task 110: Query preprocessing ████████░░ Enhancement (camelCase, multi-word)
345
- Task 111: Relevance re-ranking █████████░ Enhancement (exact match floats up)
346
- Task 112: Symbol count & stats fix ██░░░░░░░░ Bug fix (parallel-able with 110/111) DONE
347
- Task 113: Ground truth expansion + re-run ████████░░ Validation (after 108–112)
348
- Task 114: Integration tests ██████████ Gate (final acceptance)
349
- ```
350
-
351
- **Critical path:** 108 → 109 → 110 → 111 → 113 → 114.
352
- Task 112 is independent and can be done in parallel with 110 or 111.
353
- Task 113 must run after all code changes are complete.
354
-
355
- ---
356
-
357
- ## Phase 14 Completion
358
-
359
- With Phase 14 complete, PureContext's keyword search will be competitive with jcodemunch:
360
-
361
- **Before Phase 14:**
362
- - `searchSymbols`: LIKE substring match on name only. Multi-word queries return 0 results.
363
- - P@1: 0%, R@5: 0% on 12-task ground truth corpus.
364
-
365
- **After Phase 14:**
366
- - `ftsSearchSymbols`: FTS5 full-text match on name + fileStem + signature + summary, via BM25 ranking.
367
- - `preprocessQuery`: camelCase/snake_case expansion, FTS5 escaping.
368
- - `rankSymbols`: explicit relevance scoring (exact name > prefix > word overlap > content).
369
- - P@1 target: ≥ 50%, R@5 target: ≥ 80%.
370
-
371
- This makes PureContext a genuinely useful code navigation tool for natural-language AI agent queries, not just exact name lookups.
@@ -1,256 +0,0 @@
1
- # Phase 15 — Task Breakdown
2
-
3
- **Goal**: Make PureContext viable for enterprise-scale repositories (10k–50k files) by eliminating three concrete bottlenecks in the indexing pipeline: redundant file I/O, redundant DB queries, and sequential single-threaded parsing.
4
-
5
- **Scope**: All changes are confined to `src/core/index-manager.ts` and new files under `src/core/`. No schema changes, no tool API changes, no adapter changes.
6
-
7
- **Baseline to beat:**
8
- The PureContext repo itself (~130 files, ~3800 symbols) indexes in ~400ms. A 10k-file enterprise repo indexing sequentially would take ~70s at ~7ms/file (1ms I/O + 5ms parse + 1ms extract). Target: ≤ 15s on the same corpus (4–5× speedup via parallelism + I/O fix).
9
-
10
- ---
11
-
12
- ## Task 115: Fix Double File Reads
13
-
14
- **Problem**: `indexFolder` reads every changed file twice.
15
-
16
- - **Step 5** (line ~229): reads file to compute hash for change detection — then discards the `Buffer`, pushes only `relPath` to `toProcess`.
17
- - **Step 6** (line ~254): reads the same file again to parse it.
18
- - **Bonus**: `computeHash(content)` is also called a second time in step 6 (line ~280) for `upsertFile`. Hash was already computed in step 5.
19
-
20
- For a 10k-file repo with 5KB average file size, this is 100MB of redundant I/O and 10k redundant SHA-256 computations.
21
-
22
- **Deliverables:**
23
-
24
- - `src/core/index-manager.ts`:
25
- - Change `toProcess` from `string[]` to `Array<{ relPath: string; content: Buffer; hash: string }>`.
26
- - In step 5: populate `{ relPath, content, hash }` tuples (content already in memory, hash already computed).
27
- - In step 6: use `entry.content` and `entry.hash` directly — no re-read, no re-hash.
28
- - Remove the second `computeHash(content)` call (line ~280); use `entry.hash` in `upsertFile`.
29
-
30
- - `reindexFiles`: no change needed — it has no pre-pass hash step, so no double-read exists there.
31
-
32
- **Tests:**
33
-
34
- - `test/core/index-double-read.test.ts`:
35
- - Spy on `readFileSync` (or `fs.readFile`). Index a fixture. Assert each file path was read exactly once.
36
- - Assert `IndexResult.durationMs` is lower than a sequential double-read baseline (rough check via mocking with 1ms delay per read).
37
-
38
- **Verify:** `npm test` green. Index the PureContext repo — log shows same file count, symbol count as before.
39
-
40
- ---
41
-
42
- ## Task 116: Consolidate Redundant `getSymbolsByRepo` Calls
43
-
44
- **Problem**: `indexFolder` calls `getSymbolsByRepo(db, repoId)` up to **three times** in the optional post-processing block:
45
-
46
- ```typescript
47
- // Line ~303 — semantic indexer shouldIndex check
48
- const totalSoFar = getSymbolsByRepo(db, repoId).length; // call 1
49
- if (options.semanticIndexer.shouldIndex(repoId, totalSoFar)) {
50
- const allSymbols = getSymbolsByRepo(db, repoId); // call 2
51
- await options.semanticIndexer.index(repoId, allSymbols, db);
52
- }
53
-
54
- // Line ~326 — AI summarizer
55
- const candidateSymbols = getSymbolsByRepo(db, repoId); // call 3
56
- ```
57
-
58
- For a repo with 20k symbols, each call deserializes 20k rows. Three calls = 60k row deserializations that could be 20k.
59
-
60
- **Deliverables:**
61
-
62
- - `src/core/index-manager.ts`:
63
- - Load `allSymbols` once, lazily, if either `semanticIndexer` or `aiSummarizer` is active:
64
- ```typescript
65
- const allSymbols =
66
- options.semanticIndexer || options.aiSummarizer
67
- ? getSymbolsByRepo(db, repoId)
68
- : [];
69
- ```
70
- - Replace all three call sites with this single result:
71
- - `shouldIndex(repoId, allSymbols.length)` — use `.length` on the already-loaded array.
72
- - `semanticIndexer.index(repoId, allSymbols, db)` — pass `allSymbols` directly.
73
- - `candidateSymbols` in the AI summarizer block → `allSymbols`.
74
- - The lazy guard (`options.semanticIndexer || options.aiSummarizer`) ensures zero overhead when neither is enabled (the common case for most users).
75
-
76
- **Tests:**
77
-
78
- - `test/core/consolidate-queries.test.ts`:
79
- - Spy on `getSymbolsByRepo`. Index a fixture with both `semanticIndexer` and `aiSummarizer` mocked. Assert `getSymbolsByRepo` called exactly once.
80
- - With neither option: assert `getSymbolsByRepo` not called at all.
81
- - With only `semanticIndexer`: assert called once and `shouldIndex` received correct count.
82
-
83
- **Verify:** `npm test` green. Behavior identical to before — just fewer DB round-trips.
84
-
85
- ---
86
-
87
- ## Task 117: Worker Thread Pool for Parallel Parsing
88
-
89
- **Problem**: The parse loop in `indexFolder` (step 6, line ~250) processes files sequentially:
90
-
91
- ```typescript
92
- for (const relPath of toProcess) {
93
- const { symbols, imports } = await processFile(relPath, content, adapters);
94
- // DB writes per file...
95
- }
96
- ```
97
-
98
- `web-tree-sitter` is synchronous WASM. `await processFile` does not yield the event loop during parsing — it blocks. On a 16-core machine, only 1 core is used.
99
-
100
- **Architecture:**
101
-
102
- ```
103
- Main thread
104
- ├── Reads files (step 5, after Task 115: already has content)
105
- ├── Creates WorkerPool(concurrency = min(cpuCount, 8))
106
- ├── Dispatches {relPath, content (ArrayBuffer), adapterNames[]} to pool
107
- ├── Collects all ParseResult[] back
108
- ├── Single db.transaction() to write all symbols + imports
109
- └── Closes pool
110
-
111
- Worker thread (N instances)
112
- ├── Lazy-initializes its own web-tree-sitter parser
113
- ├── Instantiates adapters from adapterNames via adapter-registry
114
- ├── processFile(relPath, content, adapters)
115
- └── Returns {relPath, symbols, imports} or {relPath, error}
116
- ```
117
-
118
- **Key design decisions:**
119
- - `content` is transferred as `ArrayBuffer` (zero-copy via `Worker.postMessage(msg, [msg.content.buffer])`), not cloned.
120
- - Each worker has its own `Parser` instance — no shared state, no mutex.
121
- - DB writes happen **only on the main thread** in a single outer `db.transaction()` wrapping all file inserts. This replaces per-file transactions and is the second major speedup (especially for large repos on spinning disk).
122
- - `adapterNames: string[]` are serialized as plain strings. Each worker calls `getAdapterByName(name)` from the adapter registry to get the adapter instance.
123
- - Default concurrency: `Math.min(os.cpus().length, 8)`. Configurable via `IndexOptions.concurrency`.
124
-
125
- **Deliverables:**
126
-
127
- - `src/core/indexing-worker.ts` (new file):
128
- - Worker entry point. Listens for `ParseJob` messages:
129
- ```typescript
130
- interface ParseJob {
131
- relPath: string;
132
- content: ArrayBuffer; // transferable
133
- adapterNames: string[];
134
- }
135
- interface ParseResult {
136
- relPath: string;
137
- symbols: SymbolRecord[];
138
- imports: ImportRecord[];
139
- error?: string;
140
- }
141
- ```
142
- - Lazy-initializes parser on first message (`initParser()` once per worker).
143
- - Calls `processFile(relPath, Buffer.from(content), adapters)`.
144
- - Posts `ParseResult` back to main thread.
145
- - Handles errors: catches `ParseError`, posts `{ relPath, symbols: [], imports: [], error: message }` — does not crash the worker.
146
-
147
- - `src/core/worker-pool.ts` (new file):
148
- ```typescript
149
- export interface WorkerPool {
150
- run(jobs: ParseJob[]): Promise<ParseResult[]>;
151
- terminate(): Promise<void>;
152
- }
153
- export function createWorkerPool(concurrency: number): WorkerPool
154
- ```
155
- - Creates `concurrency` Worker instances pointing at `indexing-worker.ts`.
156
- - Maintains a job queue. Each idle worker pulls the next job.
157
- - Returns a Promise that resolves when all jobs are complete.
158
- - `terminate()`: sends `{ type: 'shutdown' }` to all workers, awaits their exit.
159
-
160
- - `src/core/index-manager.ts`:
161
- - Add `concurrency?: number` to `IndexOptions` (default: `Math.min(os.cpus().length, 8)`).
162
- - Replace the sequential step 6 loop with:
163
- 1. Build `ParseJob[]` from `toProcess` entries (using carried content from Task 115).
164
- 2. `const results = await pool.run(jobs)`.
165
- 3. Single `db.transaction()` that processes all `ParseResult[]`:
166
- - For each result: `deleteByFile`, `insertSymbols`, `upsertFile` (batched).
167
- - `allImports` accumulated from all results.
168
- - Create pool before the loop, `terminate()` in a `finally` block.
169
- - Log errors from workers alongside other per-file errors.
170
-
171
- - `src/core/db/symbol-store.ts` — optional micro-optimization:
172
- - If `insertSymbols` is called once per file inside a loop that's already in a transaction, the inner `db.transaction()` becomes a savepoint (fine). No change required — outer transaction in index-manager is sufficient.
173
-
174
- - `IndexOptions` update in `src/core/types.ts`:
175
- ```typescript
176
- concurrency?: number; // Worker thread count. Default: min(cpuCount, 8). Set to 1 to disable.
177
- ```
178
-
179
- **Tests:**
180
-
181
- - `test/core/worker-pool.test.ts`:
182
- - Pool with concurrency 2 processes 10 jobs in parallel (verify via timing: 10 jobs each taking 10ms should complete in ~50ms, not 100ms).
183
- - Failed job (parse error) does not crash pool — error appears in results.
184
- - `terminate()` cleans up all workers without hanging.
185
-
186
- - `test/core/parallel-indexing.test.ts`:
187
- - Index `test/fixtures/basic-ts-project/` with `concurrency: 2`. Assert same symbol count as sequential path.
188
- - Index with `concurrency: 1` (sequential mode). Assert identical results.
189
- - Assert all symbols appear in DB after parallel index (regression against race conditions in DB writes).
190
-
191
- **Verify:**
192
- ```bash
193
- # Time sequential (concurrency: 1) vs parallel (concurrency: 8) on a large repo
194
- node -e "
195
- const { indexFolder } = await import('./dist/core/index-manager.js');
196
- const r1 = await indexFolder('/path/to/large-repo', { concurrency: 1 });
197
- const r2 = await indexFolder('/path/to/large-repo', { concurrency: 8 });
198
- console.log('seq:', r1.durationMs, 'par:', r2.durationMs, 'speedup:', r1.durationMs / r2.durationMs);
199
- "
200
- ```
201
- Expected: ≥ 3× speedup on repos with > 500 files.
202
-
203
- ---
204
-
205
- ## Task 118: Performance Regression Tests
206
-
207
- Ensure the three fixes don't change observable behavior and lock in the performance improvements.
208
-
209
- **Deliverables:**
210
-
211
- - `test/core/indexing-correctness.test.ts`:
212
- - Index `basic-ts-project` with all three optimizations active. Assert:
213
- - Symbol count equals baseline (no symbols dropped or duplicated by parallel writes).
214
- - Import edges count equals baseline.
215
- - `IndexResult.totalSymbolsInDb` equals `SELECT COUNT(*) FROM symbols`.
216
- - FTS table populated (count matches symbols table).
217
- - Re-index same fixture (all files unchanged). Assert `filesIndexed = 0`, `filesSkipped = N`.
218
-
219
- - `test/core/performance-guard.test.ts`:
220
- - Synthetic 200-file fixture (generated in `beforeAll` using a loop writing temp files).
221
- - Index with `concurrency: 4`. Assert `durationMs < 5000` (generous upper bound, CI-safe).
222
- - Assert no errors in `IndexResult.errors`.
223
- - Cleanup temp files in `afterAll`.
224
-
225
- - `benchmarks/harness/run_purecontext.ts`:
226
- - Add `Dimension 0 — Indexing Speed` to the benchmark output:
227
- - Time `indexFolder` on the PureContext repo itself with `concurrency: 1` and `concurrency: auto`.
228
- - Report: files/sec, symbols/sec, durationMs.
229
-
230
- **Verify:** `npm test` green. `npm run bench:purecontext` includes Dim 0 indexing speed numbers.
231
-
232
- ---
233
-
234
- ## Order of Execution
235
-
236
- ```
237
- Task 115: Fix double file reads ██░░░░░░░░ Quick win, prerequisite for Task 117
238
- Task 116: Consolidate getSymbolsByRepo ██░░░░░░░░ Independent quick win
239
- Task 117: Worker thread pool ████████░░ Main feature (depends on 115 for content carry-through)
240
- Task 118: Performance regression tests ██████████ Gate (after all code changes)
241
- ```
242
-
243
- **Critical path:** 115 → 117 → 118. Task 116 is parallel with 115.
244
-
245
- ---
246
-
247
- ## Phase 15 Completion
248
-
249
- **Before Phase 15 (10k-file repo estimate):**
250
- - Indexing: ~70s (sequential, double-read, triple DB load)
251
- - CPU utilization: 1 core
252
-
253
- **After Phase 15:**
254
- - Indexing: ~15s (8 workers, single-read, single DB transaction, one `getSymbolsByRepo`)
255
- - CPU utilization: up to 8 cores
256
- - Enterprise repos (50k files): ~75s vs ~350s — viable vs not viable