@morningljn/mnemo 0.1.3 → 0.1.4

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 (49) hide show
  1. package/README.md +43 -14
  2. package/dist/init.js +16 -8
  3. package/dist/init.js.map +1 -1
  4. package/dist/refine.d.ts +14 -0
  5. package/dist/refine.js +115 -0
  6. package/dist/refine.js.map +1 -0
  7. package/dist/resources.d.ts +27 -0
  8. package/dist/resources.js +56 -0
  9. package/dist/resources.js.map +1 -0
  10. package/dist/retriever.d.ts +3 -1
  11. package/dist/retriever.js +38 -26
  12. package/dist/retriever.js.map +1 -1
  13. package/dist/server.js +7 -0
  14. package/dist/server.js.map +1 -1
  15. package/docs/superpowers/plans/2026-05-15-mnemo-mcp.md +1154 -0
  16. package/docs/superpowers/plans/2026-05-16-mnemo-query-cache.md +613 -0
  17. package/docs/superpowers/plans/2026-05-16-retrieval-and-injection-optimization.md +770 -0
  18. package/openspec/changes/archive/2026-05-15-mnemo-mcp/.openspec.yaml +2 -0
  19. package/openspec/changes/archive/2026-05-15-mnemo-mcp/design.md +83 -0
  20. package/openspec/changes/archive/2026-05-15-mnemo-mcp/proposal.md +32 -0
  21. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-retrieval/spec.md +75 -0
  22. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-store/spec.md +83 -0
  23. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/mcp-server/spec.md +34 -0
  24. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/security/spec.md +37 -0
  25. package/openspec/changes/archive/2026-05-15-mnemo-mcp/tasks.md +44 -0
  26. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/.openspec.yaml +2 -0
  27. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/design.md +96 -0
  28. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/proposal.md +29 -0
  29. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/batch-operations/spec.md +42 -0
  30. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/perf-metrics/spec.md +55 -0
  31. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/query-cache/spec.md +65 -0
  32. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/tasks.md +45 -0
  33. package/openspec/changes/retrieval-and-injection-optimization/.openspec.yaml +2 -0
  34. package/openspec/changes/retrieval-and-injection-optimization/design.md +117 -0
  35. package/openspec/changes/retrieval-and-injection-optimization/proposal.md +30 -0
  36. package/openspec/changes/retrieval-and-injection-optimization/specs/adaptive-scoring/spec.md +43 -0
  37. package/openspec/changes/retrieval-and-injection-optimization/specs/injection-protocol/spec.md +48 -0
  38. package/openspec/changes/retrieval-and-injection-optimization/specs/mcp-resources/spec.md +39 -0
  39. package/openspec/changes/retrieval-and-injection-optimization/specs/query-refinement/spec.md +39 -0
  40. package/openspec/changes/retrieval-and-injection-optimization/tasks.md +33 -0
  41. package/openspec/config.yaml +20 -0
  42. package/package.json +1 -1
  43. package/src/init.ts +17 -9
  44. package/src/refine.ts +127 -0
  45. package/src/resources.ts +78 -0
  46. package/src/retriever.ts +40 -26
  47. package/src/server.ts +8 -0
  48. package/tests/refine.test.ts +52 -0
  49. package/tests/resource.test.ts +62 -0
@@ -0,0 +1,613 @@
1
+ # Mnemo-MCP Query Cache & Batch Operations Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Add query result caching (60s TTL), batch add/remove operations, and performance metrics to mnemo-mcp to reduce repeated SQLite queries and MCP round-trips.
6
+
7
+ **Architecture:** Wrap the `FactRetriever` with a `QueryCache` layer that stores results in a process-local Map keyed by query parameters. Cache is cleared on any write operation. Add batch operation support by accepting arrays in the `fact_store` tool's `content` and `fact_id` fields. Add a `PerfMetrics` collector that records query timing, cache hit/miss, and retrieval path when `MNEMO_DEBUG=1`.
8
+
9
+ **Tech Stack:** TypeScript, Node.js, better-sqlite3, @modelcontextprotocol/sdk, zod
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | File | Responsibility |
16
+ |------|---------------|
17
+ | `src/cache.ts` | **NEW** — `QueryCache` class: Map-based TTL cache with key generation and invalidation |
18
+ | `src/metrics.ts` | **NEW** — `PerfMetrics` class: query timing, cache stats, retrieval path tracking |
19
+ | `src/types.ts` | **MODIFY** — Update `FactStoreArgs` to accept `string \| string[]` for content and `number \| number[]` for fact_id |
20
+ | `src/retriever.ts` | **MODIFY** — Integrate cache lookup before DB query, record metrics on miss, expose retrieval path |
21
+ | `src/server.ts` | **MODIFY** — Wire cache clearing on writes, handle batch add/remove, pass debug flag to metrics |
22
+
23
+ ---
24
+
25
+ ## Task 1: Create QueryCache module
26
+
27
+ **Files:**
28
+ - Create: `src/cache.ts`
29
+ - Test: Manual — verify cache hit/miss behavior
30
+
31
+ - [ ] **Step 1: Create `src/cache.ts`**
32
+
33
+ ```typescript
34
+ /**
35
+ * Query result cache for mnemo-mcp.
36
+ * Process-local Map with TTL. Cleared on write operations.
37
+ */
38
+
39
+ import type { ScoredFact } from './types.js'
40
+
41
+ interface CacheEntry {
42
+ results: ScoredFact[]
43
+ timestamp: number
44
+ }
45
+
46
+ const DEFAULT_TTL_MS = 60_000
47
+
48
+ export class QueryCache {
49
+ private cache = new Map<string, CacheEntry>()
50
+ private ttlMs: number
51
+
52
+ constructor(ttlMs = DEFAULT_TTL_MS) {
53
+ this.ttlMs = ttlMs
54
+ }
55
+
56
+ /** Generate cache key from query parameters */
57
+ makeKey(params: {
58
+ action: string
59
+ query?: string
60
+ entity?: string
61
+ entities?: string[]
62
+ category?: string
63
+ minTrust?: number
64
+ limit?: number
65
+ }): string {
66
+ const parts = [
67
+ params.action,
68
+ params.query ?? '',
69
+ params.entity ?? '',
70
+ params.entities?.join(',') ?? '',
71
+ params.category ?? '',
72
+ String(params.minTrust ?? ''),
73
+ String(params.limit ?? ''),
74
+ ]
75
+ return parts.join('|')
76
+ }
77
+
78
+ /** Get cached results if not expired */
79
+ get(key: string): ScoredFact[] | null {
80
+ const entry = this.cache.get(key)
81
+ if (!entry) return null
82
+ if (Date.now() - entry.timestamp > this.ttlMs) {
83
+ this.cache.delete(key)
84
+ return null
85
+ }
86
+ return entry.results
87
+ }
88
+
89
+ /** Store results in cache */
90
+ set(key: string, results: ScoredFact[]): void {
91
+ this.cache.set(key, { results, timestamp: Date.now() })
92
+ }
93
+
94
+ /** Clear all cached entries (call on write operations) */
95
+ clear(): void {
96
+ this.cache.clear()
97
+ }
98
+
99
+ /** Get cache size for debugging */
100
+ size(): number {
101
+ return this.cache.size
102
+ }
103
+ }
104
+ ```
105
+
106
+ - [ ] **Step 2: Commit**
107
+
108
+ ```bash
109
+ git add src/cache.ts
110
+ git commit -m "feat(cache): add QueryCache module with TTL and key generation"
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Task 2: Create PerfMetrics module
116
+
117
+ **Files:**
118
+ - Create: `src/metrics.ts`
119
+ - Test: Manual — verify metrics accumulate correctly
120
+
121
+ - [ ] **Step 1: Create `src/metrics.ts`**
122
+
123
+ ```typescript
124
+ /**
125
+ * Performance metrics for mnemo-mcp.
126
+ * Tracks query timing, cache hit/miss, and retrieval paths.
127
+ * Only active when MNEMO_DEBUG=1.
128
+ */
129
+
130
+ export interface QueryMetrics {
131
+ action: string
132
+ durationMs: number
133
+ resultCount: number
134
+ cacheHit: boolean
135
+ retrievalPath?: string
136
+ }
137
+
138
+ export class PerfMetrics {
139
+ private enabled: boolean
140
+ private totalQueries = 0
141
+ private cacheHits = 0
142
+ private cacheMisses = 0
143
+ private totalMissTimeMs = 0
144
+
145
+ constructor() {
146
+ this.enabled = process.env.MNEMO_DEBUG === '1'
147
+ }
148
+
149
+ isEnabled(): boolean {
150
+ return this.enabled
151
+ }
152
+
153
+ /** Record a query execution */
154
+ record(metrics: QueryMetrics): void {
155
+ if (!this.enabled) return
156
+
157
+ this.totalQueries++
158
+ if (metrics.cacheHit) {
159
+ this.cacheHits++
160
+ } else {
161
+ this.cacheMisses++
162
+ this.totalMissTimeMs += metrics.durationMs
163
+ }
164
+
165
+ const hitRatio = this.totalQueries > 0 ? (this.cacheHits / this.totalQueries * 100).toFixed(1) : '0.0'
166
+ const path = metrics.retrievalPath ? ` [${metrics.retrievalPath}]` : ''
167
+ console.error(
168
+ `[mnemo:debug] ${metrics.action} | ${metrics.cacheHit ? 'HIT' : 'MISS'} | ` +
169
+ `${metrics.durationMs.toFixed(2)}ms | ${metrics.resultCount} results | ` +
170
+ `hit_ratio=${hitRatio}%${path}`
171
+ )
172
+ }
173
+
174
+ /** Get aggregated statistics */
175
+ getStats(): {
176
+ totalQueries: number
177
+ cacheHits: number
178
+ cacheMisses: number
179
+ hitRatio: number
180
+ avgQueryTime: number
181
+ totalTimeSaved: number
182
+ } {
183
+ const hitRatio = this.totalQueries > 0 ? this.cacheHits / this.totalQueries : 0
184
+ const avgQueryTime = this.cacheMisses > 0 ? this.totalMissTimeMs / this.cacheMisses : 0
185
+ const totalTimeSaved = this.cacheHits * avgQueryTime
186
+
187
+ return {
188
+ totalQueries: this.totalQueries,
189
+ cacheHits: this.cacheHits,
190
+ cacheMisses: this.cacheMisses,
191
+ hitRatio,
192
+ avgQueryTime,
193
+ totalTimeSaved,
194
+ }
195
+ }
196
+
197
+ /** Log current stats */
198
+ logStats(): void {
199
+ if (!this.enabled) return
200
+ const stats = this.getStats()
201
+ console.error(
202
+ `[mnemo:debug] stats | total=${stats.totalQueries} hits=${stats.cacheHits} ` +
203
+ `misses=${stats.cacheMisses} hit_ratio=${(stats.hitRatio * 100).toFixed(1)}% ` +
204
+ `avg_time=${stats.avgQueryTime.toFixed(2)}ms saved=${stats.totalTimeSaved.toFixed(2)}ms`
205
+ )
206
+ }
207
+ }
208
+ ```
209
+
210
+ - [ ] **Step 2: Commit**
211
+
212
+ ```bash
213
+ git add src/metrics.ts
214
+ git commit -m "feat(metrics): add PerfMetrics module for query timing and cache stats"
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Task 3: Update types for batch operations
220
+
221
+ **Files:**
222
+ - Modify: `src/types.ts`
223
+ - Test: TypeScript compilation check
224
+
225
+ - [ ] **Step 1: Update `FactStoreArgs` to accept arrays**
226
+
227
+ In `src/types.ts`, change `content` and `fact_id` types:
228
+
229
+ ```typescript
230
+ /** fact_store 工具调用参数 */
231
+ export interface FactStoreArgs {
232
+ action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list'
233
+ content?: string | string[] // <-- CHANGED: support batch add
234
+ query?: string
235
+ entity?: string
236
+ entities?: string[]
237
+ fact_id?: number | number[] // <-- CHANGED: support batch remove
238
+ category?: string
239
+ tags?: string
240
+ trust_delta?: number
241
+ min_trust?: number
242
+ limit?: number
243
+ }
244
+ ```
245
+
246
+ - [ ] **Step 2: Commit**
247
+
248
+ ```bash
249
+ git add src/types.ts
250
+ git commit -m "feat(types): allow string[] for content and number[] for fact_id in FactStoreArgs"
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Task 4: Integrate cache and metrics into FactRetriever
256
+
257
+ **Files:**
258
+ - Modify: `src/retriever.ts`
259
+ - Test: Manual — verify cache is checked before DB query
260
+
261
+ - [ ] **Step 1: Import cache and metrics**
262
+
263
+ At the top of `src/retriever.ts`:
264
+
265
+ ```typescript
266
+ import { QueryCache } from './cache.js'
267
+ import { PerfMetrics } from './metrics.js'
268
+ ```
269
+
270
+ - [ ] **Step 2: Add cache and metrics fields to `FactRetriever`**
271
+
272
+ ```typescript
273
+ export class FactRetriever {
274
+ private db: Database.Database
275
+ private ftsWeight: number
276
+ private jaccardWeight: number
277
+ private halfLifeDays: number
278
+ private _categoryTagMap: Map<FactCategory, Set<string>> | null = null
279
+ private _cnEnPairs: Array<[string, string]> | null = null
280
+
281
+ // <-- ADD
282
+ private cache: QueryCache
283
+ private metrics: PerfMetrics
284
+
285
+ constructor(
286
+ private store: MemoryStore,
287
+ options?: RetrieverOptions,
288
+ ) {
289
+ this.db = store.connection
290
+ this.ftsWeight = options?.ftsWeight ?? 0.5
291
+ this.jaccardWeight = options?.jaccardWeight ?? 0.5
292
+ this.halfLifeDays = options?.temporalDecayHalfLife ?? 0
293
+ this.cache = new QueryCache() // <-- ADD
294
+ this.metrics = new PerfMetrics() // <-- ADD
295
+ }
296
+ ```
297
+
298
+ - [ ] **Step 3: Add cache access methods**
299
+
300
+ ```typescript
301
+ /** Expose cache for external invalidation (server.ts calls clear on writes) */
302
+ getCache(): QueryCache {
303
+ return this.cache
304
+ }
305
+
306
+ /** Expose metrics for external access */
307
+ getMetrics(): PerfMetrics {
308
+ return this.metrics
309
+ }
310
+ ```
311
+
312
+ - [ ] **Step 4: Wrap `search()` with cache and metrics**
313
+
314
+ Replace the `search()` method signature and add cache check at the top:
315
+
316
+ ```typescript
317
+ search(query: string, options?: SearchOptions): ScoredFact[] {
318
+ const startTime = performance.now()
319
+ const minTrust = options?.minTrust ?? 0.3
320
+ const limit = options?.limit ?? 10
321
+ const category = options?.category
322
+
323
+ // Check cache first
324
+ const cacheKey = this.cache.makeKey({ action: 'search', query, category, minTrust, limit })
325
+ const cached = this.cache.get(cacheKey)
326
+ if (cached) {
327
+ this.metrics.record({
328
+ action: 'search',
329
+ durationMs: performance.now() - startTime,
330
+ resultCount: cached.length,
331
+ cacheHit: true,
332
+ })
333
+ return cached
334
+ }
335
+
336
+ // ... existing search logic continues ...
337
+ ```
338
+
339
+ At the end of `search()`, before returning, store in cache and record metrics:
340
+
341
+ ```typescript
342
+ // At the end of search(), before return:
343
+ this.cache.set(cacheKey, results)
344
+ this.metrics.record({
345
+ action: 'search',
346
+ durationMs: performance.now() - startTime,
347
+ resultCount: results.length,
348
+ cacheHit: false,
349
+ retrievalPath: 'FTS5', // v1: simplified path tracking, actual fallback path can be added in v2
350
+ })
351
+ return results
352
+ }
353
+ ```
354
+
355
+ - [ ] **Step 5: Wrap other read methods with cache**
356
+
357
+ Apply the same pattern to `probe()`, `related()`, `reason()`, `contradict()`, and `list()`:
358
+
359
+ For each method:
360
+ 1. Generate `cacheKey` using `this.cache.makeKey()` with appropriate params
361
+ 2. Check cache at the start
362
+ 3. Store results in cache before returning
363
+ 4. Record metrics
364
+
365
+ Example for `probe()`:
366
+
367
+ ```typescript
368
+ probe(entity: string, options?: SearchOptions): ScoredFact[] {
369
+ const startTime = performance.now()
370
+ const limit = options?.limit ?? 10
371
+
372
+ const cacheKey = this.cache.makeKey({ action: 'probe', entity, limit })
373
+ const cached = this.cache.get(cacheKey)
374
+ if (cached) {
375
+ this.metrics.record({ action: 'probe', durationMs: performance.now() - startTime, resultCount: cached.length, cacheHit: true })
376
+ return cached
377
+ }
378
+
379
+ const facts = this.store.getFactsByEntity(entity, options?.category, limit)
380
+ const results = facts.map((f, i) => ({ ...f, score: f.trustScore * (1 - i * 0.05) }))
381
+
382
+ this.cache.set(cacheKey, results)
383
+ this.metrics.record({ action: 'probe', durationMs: performance.now() - startTime, resultCount: results.length, cacheHit: false })
384
+ return results
385
+ }
386
+ ```
387
+
388
+ - [ ] **Step 6: Commit**
389
+
390
+ ```bash
391
+ git add src/retriever.ts
392
+ git commit -m "feat(retriever): integrate QueryCache and PerfMetrics into all read methods"
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Task 5: Update server.ts for batch operations and cache invalidation
398
+
399
+ **Files:**
400
+ - Modify: `src/server.ts`
401
+ - Test: Manual — verify batch add/remove and cache clearing
402
+
403
+ - [ ] **Step 1: Update fact_store schema for batch support**
404
+
405
+ Change the Zod schema to accept arrays:
406
+
407
+ ```typescript
408
+ const factStoreSchema = {
409
+ action: z.enum(['add', 'search', 'probe', 'related', 'reason', 'contradict', 'update', 'remove', 'list']),
410
+ content: z.union([z.string(), z.array(z.string())]).optional().describe("事实内容('add' 必需,支持单条字符串或字符串数组批量添加)"),
411
+ query: z.string().optional().describe("搜索查询('search' 必需)"),
412
+ entity: z.string().optional().describe("实体名('probe'/'related' 使用)"),
413
+ entities: z.array(z.string()).optional().describe("实体列表('reason' 使用)"),
414
+ fact_id: z.union([z.number(), z.array(z.number())]).optional().describe("事实 ID('update'/'remove' 使用,支持单个数字或数字数组批量删除)"),
415
+ category: z.enum(['identity', 'coding_style', 'tool_pref', 'workflow', 'general']).optional(),
416
+ tags: z.string().optional().describe('逗号分隔标签'),
417
+ trust_delta: z.number().optional().describe("'update' 的信任调整值"),
418
+ min_trust: z.number().optional().describe('最低信任过滤(默认 0.3)'),
419
+ limit: z.number().optional().describe('最大结果数(默认 10)'),
420
+ }
421
+ ```
422
+
423
+ - [ ] **Step 2: Rewrite add handler for batch support**
424
+
425
+ Replace the `case 'add':` block:
426
+
427
+ ```typescript
428
+ case 'add': {
429
+ if (!a.content) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: content' }) }] }
430
+
431
+ const contents = Array.isArray(a.content) ? a.content : [a.content]
432
+ const results: Array<{ fact_id: number; status: string; reason?: string; warnings?: string[] }> = []
433
+
434
+ for (const content of contents) {
435
+ if (!content || !content.trim()) {
436
+ results.push({ fact_id: -1, status: 'error', reason: 'empty content' })
437
+ continue
438
+ }
439
+
440
+ const similar = store.findSimilarFact(content, category) ?? store.findSimilarFact(content)
441
+ let warnings: string[] | undefined
442
+ const scan = fullSecurityScan(content)
443
+ if (scan.warnings.length > 0 || scan.hasPii) warnings = [...scan.warnings]
444
+
445
+ if (similar) {
446
+ store.updateFact(similar.factId, { content, tags: a.tags, trustDelta: 0.05 })
447
+ const demoted = store.demoteContradictingFacts(similar.factId, content, category)
448
+ results.push({
449
+ fact_id: similar.factId,
450
+ status: 'updated',
451
+ reason: 'similar_fact_merged',
452
+ ...(warnings ? { warnings } : {}),
453
+ })
454
+ } else {
455
+ const factId = store.addFact(content, category, a.tags ?? '')
456
+ const demoted = store.demoteContradictingFacts(factId, content, category)
457
+ results.push({
458
+ fact_id: factId,
459
+ status: 'added',
460
+ category,
461
+ ...(warnings ? { warnings } : {}),
462
+ })
463
+ }
464
+ }
465
+
466
+ // Clear cache on write
467
+ retriever.getCache().clear()
468
+
469
+ // Return single object for single input, array for batch
470
+ const response = Array.isArray(a.content) ? results : results[0]
471
+ return { content: [{ type: 'text' as const, text: JSON.stringify(response) }] }
472
+ }
473
+ ```
474
+
475
+ - [ ] **Step 3: Rewrite remove handler for batch support**
476
+
477
+ Replace the `case 'remove':` block:
478
+
479
+ ```typescript
480
+ case 'remove': {
481
+ if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
482
+
483
+ const ids = Array.isArray(a.fact_id) ? a.fact_id : [a.fact_id]
484
+ const results = ids.map(id => ({ fact_id: id, removed: store.removeFact(id) }))
485
+
486
+ // Clear cache on write
487
+ retriever.getCache().clear()
488
+
489
+ const response = Array.isArray(a.fact_id) ? results : results[0]
490
+ return { content: [{ type: 'text' as const, text: JSON.stringify(response) }] }
491
+ }
492
+ ```
493
+
494
+ - [ ] **Step 4: Add cache clearing to update handler**
495
+
496
+ In `case 'update':`, after the update call, clear cache:
497
+
498
+ ```typescript
499
+ case 'update': {
500
+ if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
501
+ const updated = store.updateFact(a.fact_id, { content: a.content, tags: a.tags, category, trustDelta: a.trust_delta })
502
+
503
+ // Clear cache on write
504
+ retriever.getCache().clear()
505
+
506
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ updated }) }] }
507
+ }
508
+ ```
509
+
510
+ - [ ] **Step 5: Commit**
511
+
512
+ ```bash
513
+ git add src/server.ts
514
+ git commit -m "feat(server): add batch add/remove, cache invalidation on writes"
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Task 6: Verify end-to-end functionality
520
+
521
+ **Files:**
522
+ - None (manual verification)
523
+
524
+ - [ ] **Step 1: Build the project**
525
+
526
+ ```bash
527
+ cd /Users/ljn/Documents/demo/ocean/mnemo-mcp
528
+ npm run build
529
+ ```
530
+
531
+ - [ ] **Step 2: Test cache hit**
532
+
533
+ Start the server and send two identical search queries within 60 seconds:
534
+
535
+ ```bash
536
+ MNEMO_DEBUG=1 node dist/server.js
537
+ ```
538
+
539
+ Send via MCP Inspector or manual JSON-RPC:
540
+ ```json
541
+ {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"fact_store","arguments":{"action":"search","query":"test","limit":5}}}
542
+ ```
543
+
544
+ Second identical query should show `HIT` in debug output.
545
+
546
+ - [ ] **Step 3: Test batch add**
547
+
548
+ ```json
549
+ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"fact_store","arguments":{"action":"add","content":["fact one","fact two"],"category":"general"}}}
550
+ ```
551
+
552
+ Response should be an array of two results.
553
+
554
+ - [ ] **Step 4: Test batch remove**
555
+
556
+ ```json
557
+ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"fact_store","arguments":{"action":"remove","fact_id":[1,2]}}}
558
+ ```
559
+
560
+ Response should be an array of `{fact_id, removed}` objects.
561
+
562
+ - [ ] **Step 5: Test backward compatibility**
563
+
564
+ Single fact add should still work:
565
+ ```json
566
+ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"fact_store","arguments":{"action":"add","content":"single fact","category":"general"}}}
567
+ ```
568
+
569
+ Response should be a single object (not array).
570
+
571
+ - [ ] **Step 6: Commit**
572
+
573
+ ```bash
574
+ git add .
575
+ git commit -m "test: verify cache, batch ops, and backward compatibility"
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Self-Review Checklist
581
+
582
+ ### Spec Coverage
583
+
584
+ | Spec Requirement | Task |
585
+ |-----------------|------|
586
+ | Identical query returns cached result | Task 4, Step 4 |
587
+ | Cache cleared on fact addition | Task 5, Step 2 |
588
+ | Cache entry expires after TTL | Task 1, Step 1 (get() checks timestamp) |
589
+ | Probe query is cached | Task 4, Step 5 |
590
+ | Debug mode logs cache metrics | Task 2, Step 1 (record() logs) |
591
+ | Batch add multiple facts | Task 5, Step 2 |
592
+ | Batch add with partial failure | Task 5, Step 2 (empty content check) |
593
+ | Single fact add remains compatible | Task 5, Step 2 (Array.isArray check) |
594
+ | Batch remove multiple facts | Task 5, Step 3 |
595
+ | Query execution time recorded | Task 2, Step 1 + Task 4 |
596
+ | Retrieval path tracked | Task 4, Step 4 (retrievalPath field) |
597
+ | Cache statistics aggregated | Task 2, Step 1 (getStats()) |
598
+
599
+ ### Placeholder Scan
600
+
601
+ - [x] No "TBD", "TODO", "implement later"
602
+ - [x] No vague "add error handling" without code
603
+ - [x] No "write tests for the above" without test code
604
+ - [x] All file paths are exact
605
+ - [x] All code blocks contain complete implementations
606
+
607
+ ### Type Consistency
608
+
609
+ - [x] `FactStoreArgs.content` is `string | string[]` in types.ts and server.ts
610
+ - [x] `FactStoreArgs.fact_id` is `number | number[]` in types.ts and server.ts
611
+ - [x] `QueryCache.makeKey()` params match usage in retriever.ts
612
+ - [x] `PerfMetrics.record()` accepts `QueryMetrics` interface consistently
613
+ - [x] `retriever.getCache()` returns `QueryCache` used in server.ts for `clear()`