claude-brain 0.17.13 → 0.22.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.
Files changed (46) hide show
  1. package/VERSION +1 -1
  2. package/package.json +3 -1
  3. package/scripts/postinstall.mjs +80 -104
  4. package/src/cli/auto-setup.ts +1 -9
  5. package/src/cli/bin.ts +23 -2
  6. package/src/cli/commands/export.ts +130 -0
  7. package/src/cli/commands/reindex.ts +107 -0
  8. package/src/cli/commands/serve.ts +54 -0
  9. package/src/cli/commands/status.ts +158 -0
  10. package/src/code-intelligence/indexer.ts +315 -0
  11. package/src/code-intelligence/linker.ts +178 -0
  12. package/src/code-intelligence/parser.ts +484 -0
  13. package/src/code-intelligence/query.ts +291 -0
  14. package/src/code-intelligence/schema.ts +83 -0
  15. package/src/code-intelligence/types.ts +95 -0
  16. package/src/config/defaults.ts +3 -3
  17. package/src/config/loader.ts +6 -0
  18. package/src/config/schema.ts +28 -2
  19. package/src/health/index.ts +5 -2
  20. package/src/hooks/brain-hook.ts +4 -1
  21. package/src/hooks/context-hook.ts +69 -10
  22. package/src/hooks/installer.ts +4 -7
  23. package/src/intelligence/cross-project/index.ts +1 -7
  24. package/src/intelligence/prediction/index.ts +1 -7
  25. package/src/intelligence/reasoning/index.ts +1 -7
  26. package/src/memory/compression.ts +105 -0
  27. package/src/memory/fts5-search.ts +456 -0
  28. package/src/memory/index.ts +342 -38
  29. package/src/memory/migrations/add-fts5.ts +98 -0
  30. package/src/memory/pruning.ts +60 -0
  31. package/src/routing/intent-classifier.ts +58 -1
  32. package/src/routing/response-filter.ts +128 -0
  33. package/src/routing/router.ts +457 -54
  34. package/src/server/http-api.ts +319 -1
  35. package/src/server/providers/resources.ts +1 -42
  36. package/src/server/services.ts +113 -12
  37. package/src/server/web-viewer.ts +1115 -0
  38. package/src/setup/index.ts +12 -22
  39. package/src/tools/schemas.ts +1 -1
  40. package/src/intelligence/cross-project/affinity.ts +0 -159
  41. package/src/intelligence/cross-project/transfer.ts +0 -201
  42. package/src/intelligence/prediction/context-anticipator.ts +0 -198
  43. package/src/intelligence/prediction/decision-predictor.ts +0 -184
  44. package/src/intelligence/reasoning/counterfactual.ts +0 -248
  45. package/src/intelligence/reasoning/synthesizer.ts +0 -167
  46. package/src/setup/wizard.ts +0 -459
@@ -13,6 +13,8 @@ import { SemanticSearch } from './search'
13
13
  import { MemoryContextBuilder } from './context-builder'
14
14
  import type { MemorySystemStats } from './types'
15
15
  import { ChromaManager, DEFAULT_CHROMA_CONFIG, getChromaConfigFromEnv, ChromaMigration, type MigrationOptions } from './chroma'
16
+ import { FTS5Search } from './fts5-search'
17
+ import { addFTS5Tables } from './migrations/add-fts5'
16
18
 
17
19
  // Re-export all types and classes for external use
18
20
  export * from './types'
@@ -38,6 +40,10 @@ export { PatternRecognizer, type Pattern } from './patterns'
38
40
  export { LearningSystem, type Correction, type Preference, type LearningInsights } from './learning'
39
41
  export { KnowledgeExtractor, type ExtractedKnowledge, type ExtractionResult } from './knowledge-extractor'
40
42
 
43
+ // Phase 26: FTS5 Search
44
+ export { FTS5Search } from './fts5-search'
45
+ export type { ObservationCategory, NewObservation, ObservationResult, ScoredResult } from './fts5-search'
46
+
41
47
  /**
42
48
  * Unified memory system manager
43
49
  * Combines database, embeddings, store, search, and context building
@@ -48,6 +54,9 @@ export class MemoryManager {
48
54
  readonly contextBuilder: MemoryContextBuilder
49
55
  readonly chroma: ChromaManager
50
56
 
57
+ // Phase 26: FTS5 search (always available, no external deps)
58
+ private _fts5: FTS5Search | null = null
59
+
51
60
  // Store and search are initialized after database is ready
52
61
  private _store: MemoryStore | null = null
53
62
  private _search: SemanticSearch | null = null
@@ -98,6 +107,15 @@ export class MemoryManager {
98
107
  this._store = new MemoryStore(db, this.embeddings, this.logger)
99
108
  this._search = new SemanticSearch(db, this.embeddings, this.logger)
100
109
 
110
+ // Phase 26: Always initialize FTS5 (just SQLite, no external deps)
111
+ try {
112
+ addFTS5Tables(db)
113
+ this._fts5 = new FTS5Search(db, this.logger)
114
+ this.logger.info('FTS5 search initialized')
115
+ } catch (error) {
116
+ this.logger.warn({ error }, 'Failed to initialize FTS5, continuing without it')
117
+ }
118
+
101
119
  if (this.useChromaDB) {
102
120
  try {
103
121
  await this.chroma.initialize()
@@ -136,6 +154,13 @@ export class MemoryManager {
136
154
  return this._search
137
155
  }
138
156
 
157
+ /**
158
+ * Get the FTS5 search engine (null if not initialized)
159
+ */
160
+ get fts5(): FTS5Search | null {
161
+ return this._fts5
162
+ }
163
+
139
164
  /**
140
165
  * Check if memory system is initialized
141
166
  */
@@ -219,8 +244,51 @@ export class MemoryManager {
219
244
  reasoning: string,
220
245
  options?: { alternatives?: string; tags?: string[] }
221
246
  ): Promise<string> {
247
+ // Phase 26: Always store in FTS5 if available
248
+ let fts5Id: string | undefined
249
+ if (this._fts5) {
250
+ try {
251
+ // Check for duplicates first
252
+ const dup = this._fts5.searchForDuplicates(decision, project)
253
+ if (dup) {
254
+ this.logger.info({ existingId: dup.id, score: dup.score }, 'FTS5 skipping duplicate decision')
255
+ return dup.id
256
+ }
257
+
258
+ fts5Id = this._fts5.store({
259
+ project,
260
+ category: 'decision',
261
+ content: decision,
262
+ reasoning,
263
+ context,
264
+ tags: options?.tags
265
+ })
266
+ } catch (error) {
267
+ this.logger.warn({ error }, 'FTS5 store failed, continuing with other backends')
268
+ }
269
+ }
270
+
271
+ // Store in ChromaDB if available
222
272
  if (this.useChromaDB) {
223
- return this.chroma.store.storeDecision({
273
+ try {
274
+ const chromaId = await this.chroma.store.storeDecision({
275
+ project,
276
+ context,
277
+ decision,
278
+ reasoning,
279
+ alternatives: options?.alternatives,
280
+ tags: options?.tags
281
+ })
282
+ return fts5Id || chromaId
283
+ } catch (error) {
284
+ this.logger.warn({ error }, 'ChromaDB store failed')
285
+ if (fts5Id) return fts5Id
286
+ }
287
+ }
288
+
289
+ // Fallback to legacy SQLite store if no FTS5 and no ChromaDB
290
+ if (!fts5Id) {
291
+ const id = await this.store.storeDecision({
224
292
  project,
225
293
  context,
226
294
  decision,
@@ -228,40 +296,83 @@ export class MemoryManager {
228
296
  alternatives: options?.alternatives,
229
297
  tags: options?.tags
230
298
  })
299
+ fts5Id = id
231
300
  }
232
- const id = await this.store.storeDecision({
233
- project,
234
- context,
235
- decision,
236
- reasoning,
237
- alternatives: options?.alternatives,
238
- tags: options?.tags
239
- })
240
- // Notify listeners (e.g., knowledge graph builder) for SQLite path
301
+
302
+ // Notify listeners (e.g., knowledge graph builder)
241
303
  for (const cb of this.onDecisionStoredCallbacks) {
242
304
  try {
243
- cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id })
305
+ cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id: fts5Id })
244
306
  } catch {}
245
307
  }
246
- return id
308
+ return fts5Id!
247
309
  }
248
310
 
249
311
  /**
250
- * Get raw search results - routes to ChromaDB when enabled
312
+ * Get raw search results - uses FTS5 as primary, ChromaDB as enrichment
251
313
  * Use this for internal operations that need raw results
252
314
  */
253
315
  async searchRaw(
254
316
  query: string,
255
317
  options?: { project?: string; limit?: number; minSimilarity?: number }
256
318
  ): Promise<any[]> {
319
+ const limit = options?.limit || 5
320
+
321
+ // Phase 26: Try FTS5 first (always available)
322
+ if (this._fts5) {
323
+ const ftsResults = this._fts5.searchWithConfidence(query, options?.project, limit)
324
+
325
+ if (ftsResults.length > 0) {
326
+ // Transform FTS5 results to match expected MemorySearchResult structure
327
+ const results = ftsResults.map(r => ({
328
+ id: r.id,
329
+ content: r.content,
330
+ memory: {
331
+ id: r.id,
332
+ project: r.project,
333
+ content: r.content,
334
+ createdAt: new Date(r.created_at),
335
+ metadata: {
336
+ project: r.project,
337
+ category: r.category,
338
+ context: r.context || '',
339
+ reasoning: r.reasoning || '',
340
+ tags: r.tags,
341
+ created_at: r.created_at
342
+ }
343
+ },
344
+ similarity: r.score,
345
+ decision: r.category === 'decision' ? {
346
+ id: r.id,
347
+ project: r.project,
348
+ context: r.context || '',
349
+ decision: r.content,
350
+ reasoning: r.reasoning || '',
351
+ alternatives: '',
352
+ tags: r.tags,
353
+ createdAt: new Date(r.created_at)
354
+ } : undefined,
355
+ metadata: {
356
+ project: r.project,
357
+ category: r.category,
358
+ context: r.context || '',
359
+ reasoning: r.reasoning || '',
360
+ tags: r.tags,
361
+ created_at: r.created_at
362
+ }
363
+ }))
364
+
365
+ return results
366
+ }
367
+ }
368
+
369
+ // Fallback: Try ChromaDB if available
257
370
  if (this.useChromaDB) {
258
371
  const chromaResults = await this.chroma.search.searchDecisions(query, {
259
372
  project: options?.project,
260
- limit: options?.limit || 5,
373
+ limit,
261
374
  minSimilarity: options?.minSimilarity || 0.5
262
375
  })
263
- // Transform ChromaDB results to match MemorySearchResult structure
264
- // Includes flat `content` field for direct access (Phase 19)
265
376
  return chromaResults.map(r => {
266
377
  const memoryContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content)
267
378
  const decisionObj = r.metadata.decision ? {
@@ -277,10 +388,8 @@ export class MemoryManager {
277
388
  } : undefined
278
389
 
279
390
  return {
280
- // Flat fields for direct access (Phase 19)
281
391
  id: r.id,
282
392
  content: decisionObj ? decisionObj.decision : memoryContent,
283
- // Nested fields for backward compatibility
284
393
  memory: {
285
394
  id: r.id,
286
395
  project: r.metadata.project || options?.project || 'unknown',
@@ -293,13 +402,14 @@ export class MemoryManager {
293
402
  metadata: r.metadata
294
403
  }
295
404
  })
296
- } else {
297
- return await this.search.search(query, {
298
- project: options?.project,
299
- limit: options?.limit || 5,
300
- minSimilarity: options?.minSimilarity || 0.5
301
- })
302
405
  }
406
+
407
+ // Final fallback: legacy SQLite search
408
+ return await this.search.search(query, {
409
+ project: options?.project,
410
+ limit,
411
+ minSimilarity: options?.minSimilarity || 0.5
412
+ })
303
413
  }
304
414
 
305
415
  async recallSimilar(
@@ -335,7 +445,7 @@ export class MemoryManager {
335
445
  }
336
446
 
337
447
  /**
338
- * Store a pattern in memory — routes to ChromaDB or SQLite
448
+ * Store a pattern in memory — dual-writes to FTS5 + ChromaDB/SQLite
339
449
  */
340
450
  async storePattern(input: {
341
451
  project: string
@@ -346,14 +456,41 @@ export class MemoryManager {
346
456
  context?: string
347
457
  source?: string
348
458
  }): Promise<string> {
459
+ // Phase 26: Dual-write to FTS5
460
+ let fts5Id: string | undefined
461
+ if (this._fts5) {
462
+ try {
463
+ fts5Id = this._fts5.store({
464
+ project: input.project,
465
+ category: 'pattern',
466
+ content: input.description,
467
+ context: input.context,
468
+ confidence: input.confidence,
469
+ source: input.source
470
+ })
471
+ } catch (error) {
472
+ this.logger.warn({ error }, 'FTS5 pattern store failed')
473
+ }
474
+ }
475
+
349
476
  if (this.useChromaDB) {
350
- return this.chroma.store.storePattern(input)
477
+ try {
478
+ const chromaId = await this.chroma.store.storePattern(input)
479
+ return fts5Id || chromaId
480
+ } catch (error) {
481
+ this.logger.warn({ error }, 'ChromaDB pattern store failed')
482
+ if (fts5Id) return fts5Id
483
+ }
484
+ }
485
+
486
+ if (!fts5Id) {
487
+ return this.store.storePattern(input)
351
488
  }
352
- return this.store.storePattern(input)
489
+ return fts5Id
353
490
  }
354
491
 
355
492
  /**
356
- * Store a correction/lesson learned — routes to ChromaDB or SQLite
493
+ * Store a correction/lesson learned — dual-writes to FTS5 + ChromaDB/SQLite
357
494
  */
358
495
  async storeCorrection(input: {
359
496
  project: string
@@ -363,14 +500,41 @@ export class MemoryManager {
363
500
  context?: string
364
501
  confidence: number
365
502
  }): Promise<string> {
503
+ // Phase 26: Dual-write to FTS5
504
+ let fts5Id: string | undefined
505
+ if (this._fts5) {
506
+ try {
507
+ fts5Id = this._fts5.store({
508
+ project: input.project,
509
+ category: 'correction',
510
+ content: input.correction,
511
+ reasoning: input.reasoning,
512
+ context: input.context,
513
+ confidence: input.confidence
514
+ })
515
+ } catch (error) {
516
+ this.logger.warn({ error }, 'FTS5 correction store failed')
517
+ }
518
+ }
519
+
366
520
  if (this.useChromaDB) {
367
- return this.chroma.store.storeCorrection(input)
521
+ try {
522
+ const chromaId = await this.chroma.store.storeCorrection(input)
523
+ return fts5Id || chromaId
524
+ } catch (error) {
525
+ this.logger.warn({ error }, 'ChromaDB correction store failed')
526
+ if (fts5Id) return fts5Id
527
+ }
368
528
  }
369
- return this.store.storeCorrection(input)
529
+
530
+ if (!fts5Id) {
531
+ return this.store.storeCorrection(input)
532
+ }
533
+ return fts5Id
370
534
  }
371
535
 
372
536
  /**
373
- * Get patterns for a project — routes to ChromaDB or SQLite
537
+ * Get patterns for a project — routes to FTS5, ChromaDB, or legacy SQLite
374
538
  */
375
539
  async getPatterns(
376
540
  project?: string,
@@ -379,6 +543,24 @@ export class MemoryManager {
379
543
  limit?: number
380
544
  }
381
545
  ): Promise<any[]> {
546
+ // Phase 26: Try FTS5 first
547
+ if (this._fts5 && project) {
548
+ const results = this._fts5.fetchAll(project, 'pattern')
549
+ if (results.length > 0) {
550
+ return results.map(r => ({
551
+ id: r.id,
552
+ description: r.content,
553
+ metadata: {
554
+ project: r.project,
555
+ pattern_type: r.category,
556
+ confidence: r.confidence,
557
+ context: r.context || '',
558
+ created_at: r.created_at
559
+ }
560
+ }))
561
+ }
562
+ }
563
+
382
564
  if (this.useChromaDB) {
383
565
  if (project) {
384
566
  return this.chroma.store.getPatternsByProject(project, options)
@@ -392,12 +574,30 @@ export class MemoryManager {
392
574
  }
393
575
 
394
576
  /**
395
- * Get corrections for a project — routes to ChromaDB or SQLite
577
+ * Get corrections for a project — routes to FTS5, ChromaDB, or legacy SQLite
396
578
  */
397
579
  async getCorrections(
398
580
  project?: string,
399
581
  options?: { limit?: number }
400
582
  ): Promise<any[]> {
583
+ // Phase 26: Try FTS5 first
584
+ if (this._fts5 && project) {
585
+ const results = this._fts5.fetchAll(project, 'correction')
586
+ if (results.length > 0) {
587
+ return results.map(r => ({
588
+ id: r.id,
589
+ correction: r.content,
590
+ metadata: {
591
+ project: r.project,
592
+ reasoning: r.reasoning || '',
593
+ context: r.context || '',
594
+ confidence: r.confidence,
595
+ created_at: r.created_at
596
+ }
597
+ }))
598
+ }
599
+ }
600
+
401
601
  if (this.useChromaDB) {
402
602
  if (project) {
403
603
  return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
@@ -411,10 +611,28 @@ export class MemoryManager {
411
611
  }
412
612
 
413
613
  /**
414
- * Fetch all decisions with content — routes to ChromaDB or SQLite
614
+ * Fetch all decisions with content — routes to FTS5, ChromaDB, or legacy SQLite
415
615
  * Used by analytical tools that need bulk access to decision data
416
616
  */
417
617
  async fetchAllDecisions(project?: string): Promise<any[]> {
618
+ // Phase 26: Try FTS5 first
619
+ if (this._fts5 && project) {
620
+ const results = this._fts5.fetchAll(project, 'decision')
621
+ if (results.length > 0) {
622
+ return results.map(r => ({
623
+ id: r.id,
624
+ content: r.content,
625
+ date: r.created_at,
626
+ project: r.project,
627
+ context: r.context || '',
628
+ decision: r.content,
629
+ reasoning: r.reasoning || '',
630
+ alternatives: '',
631
+ tags: r.tags
632
+ }))
633
+ }
634
+ }
635
+
418
636
  if (this.useChromaDB) {
419
637
  try {
420
638
  const collection = await this.chroma.collections.getDecisions()
@@ -443,9 +661,27 @@ export class MemoryManager {
443
661
  }
444
662
 
445
663
  /**
446
- * Fetch all patterns with content — routes to ChromaDB or SQLite
664
+ * Fetch all patterns with content — routes to FTS5, ChromaDB, or legacy SQLite
447
665
  */
448
666
  async fetchAllPatterns(project?: string): Promise<any[]> {
667
+ // Phase 26: Try FTS5 first
668
+ if (this._fts5 && project) {
669
+ const results = this._fts5.fetchAll(project, 'pattern')
670
+ if (results.length > 0) {
671
+ return results.map(r => ({
672
+ id: r.id,
673
+ content: r.content,
674
+ date: r.created_at,
675
+ project: r.project,
676
+ pattern_type: '',
677
+ description: r.content,
678
+ example: '',
679
+ confidence: r.confidence,
680
+ context: r.context || ''
681
+ }))
682
+ }
683
+ }
684
+
449
685
  if (this.useChromaDB) {
450
686
  try {
451
687
  const collection = await this.chroma.collections.getPatterns()
@@ -474,9 +710,27 @@ export class MemoryManager {
474
710
  }
475
711
 
476
712
  /**
477
- * Fetch all corrections with content — routes to ChromaDB or SQLite
713
+ * Fetch all corrections with content — routes to FTS5, ChromaDB, or legacy SQLite
478
714
  */
479
715
  async fetchAllCorrections(project?: string): Promise<any[]> {
716
+ // Phase 26: Try FTS5 first
717
+ if (this._fts5 && project) {
718
+ const results = this._fts5.fetchAll(project, 'correction')
719
+ if (results.length > 0) {
720
+ return results.map(r => ({
721
+ id: r.id,
722
+ content: r.content,
723
+ date: r.created_at,
724
+ project: r.project,
725
+ original: '',
726
+ correction: r.content,
727
+ reasoning: r.reasoning || '',
728
+ context: r.context || '',
729
+ confidence: r.confidence
730
+ }))
731
+ }
732
+ }
733
+
480
734
  if (this.useChromaDB) {
481
735
  try {
482
736
  const collection = await this.chroma.collections.getCorrections()
@@ -505,7 +759,7 @@ export class MemoryManager {
505
759
  }
506
760
 
507
761
  /**
508
- * Search patterns by query — routes to ChromaDB or SQLite
762
+ * Search patterns by query — routes to FTS5, ChromaDB, or legacy SQLite
509
763
  */
510
764
  async searchPatterns(
511
765
  query: string,
@@ -516,6 +770,27 @@ export class MemoryManager {
516
770
  minSimilarity?: number
517
771
  }
518
772
  ): Promise<any[]> {
773
+ // Phase 26: Try FTS5 first
774
+ if (this._fts5 && query) {
775
+ const results = this._fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
776
+ const patterns = results.filter(r => r.category === 'pattern')
777
+ if (patterns.length > 0) {
778
+ return patterns.map(r => ({
779
+ id: r.id,
780
+ content: r.content,
781
+ metadata: {
782
+ project: r.project,
783
+ pattern_type: '',
784
+ description: r.content,
785
+ confidence: r.confidence,
786
+ context: r.context || '',
787
+ created_at: r.created_at
788
+ },
789
+ similarity: r.score
790
+ }))
791
+ }
792
+ }
793
+
519
794
  if (this.useChromaDB) {
520
795
  return this.chroma.store.searchPatterns(query, options)
521
796
  }
@@ -523,7 +798,7 @@ export class MemoryManager {
523
798
  }
524
799
 
525
800
  /**
526
- * Search corrections by query — routes to ChromaDB or SQLite
801
+ * Search corrections by query — routes to FTS5, ChromaDB, or legacy SQLite
527
802
  */
528
803
  async searchCorrections(
529
804
  query: string,
@@ -533,6 +808,26 @@ export class MemoryManager {
533
808
  minSimilarity?: number
534
809
  }
535
810
  ): Promise<any[]> {
811
+ // Phase 26: Try FTS5 first
812
+ if (this._fts5 && query) {
813
+ const results = this._fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
814
+ const corrections = results.filter(r => r.category === 'correction')
815
+ if (corrections.length > 0) {
816
+ return corrections.map(r => ({
817
+ id: r.id,
818
+ content: r.content,
819
+ metadata: {
820
+ project: r.project,
821
+ reasoning: r.reasoning || '',
822
+ context: r.context || '',
823
+ confidence: r.confidence,
824
+ created_at: r.created_at
825
+ },
826
+ similarity: r.score
827
+ }))
828
+ }
829
+ }
830
+
536
831
  if (this.useChromaDB) {
537
832
  return this.chroma.store.searchCorrections(query, options)
538
833
  }
@@ -540,9 +835,18 @@ export class MemoryManager {
540
835
  }
541
836
 
542
837
  /**
543
- * Delete a decision by ID — routes to ChromaDB or SQLite
838
+ * Delete a decision by ID — removes from FTS5 + ChromaDB/SQLite
544
839
  */
545
840
  async deleteDecision(id: string): Promise<void> {
841
+ // Phase 26: Delete from FTS5
842
+ if (this._fts5) {
843
+ try {
844
+ this._fts5.delete(id)
845
+ } catch (error) {
846
+ this.logger.warn({ error, id }, 'FTS5 delete failed')
847
+ }
848
+ }
849
+
546
850
  if (this.useChromaDB) {
547
851
  await this.chroma.store.deleteDecision(id)
548
852
  } else {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * FTS5 Migration — Phase 26
3
+ * Adds observations table with FTS5 virtual table for full-text search.
4
+ * This replaces ChromaDB as the primary search backend.
5
+ */
6
+
7
+ import type { Database } from 'bun:sqlite'
8
+
9
+ export function addFTS5Tables(db: Database): void {
10
+ // Observations table (unified storage replacing separate ChromaDB collections)
11
+ db.run(`
12
+ CREATE TABLE IF NOT EXISTS observations (
13
+ id TEXT PRIMARY KEY,
14
+ project TEXT NOT NULL,
15
+ category TEXT NOT NULL CHECK(category IN ('decision', 'pattern', 'correction', 'insight', 'preference')),
16
+ content TEXT NOT NULL,
17
+ reasoning TEXT,
18
+ context TEXT,
19
+ confidence REAL DEFAULT 0.8,
20
+ source TEXT DEFAULT 'explicit',
21
+ tags TEXT,
22
+ file_paths TEXT,
23
+ symbols TEXT,
24
+ access_count INTEGER DEFAULT 0,
25
+ last_accessed TEXT,
26
+ created_at TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL,
28
+ archived INTEGER DEFAULT 0
29
+ )
30
+ `)
31
+
32
+ // FTS5 virtual table for full-text search
33
+ db.run(`
34
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
35
+ content, reasoning, context, tags,
36
+ content='observations', content_rowid='rowid'
37
+ )
38
+ `)
39
+
40
+ // Keep FTS index in sync with observations table
41
+ db.run(`
42
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
43
+ INSERT INTO observations_fts(rowid, content, reasoning, context, tags)
44
+ VALUES (new.rowid, new.content, new.reasoning, new.context, new.tags);
45
+ END
46
+ `)
47
+
48
+ db.run(`
49
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
50
+ INSERT INTO observations_fts(observations_fts, rowid, content, reasoning, context, tags)
51
+ VALUES ('delete', old.rowid, old.content, old.reasoning, old.context, old.tags);
52
+ END
53
+ `)
54
+
55
+ db.run(`
56
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
57
+ INSERT INTO observations_fts(observations_fts, rowid, content, reasoning, context, tags)
58
+ VALUES ('delete', old.rowid, old.content, old.reasoning, old.context, old.tags);
59
+ INSERT INTO observations_fts(rowid, content, reasoning, context, tags)
60
+ VALUES (new.rowid, new.content, new.reasoning, new.context, new.tags);
61
+ END
62
+ `)
63
+
64
+ // Indexes for common queries
65
+ db.run(`CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project)`)
66
+ db.run(`CREATE INDEX IF NOT EXISTS idx_obs_category ON observations(category)`)
67
+ db.run(`CREATE INDEX IF NOT EXISTS idx_obs_created ON observations(created_at)`)
68
+ db.run(`CREATE INDEX IF NOT EXISTS idx_obs_archived ON observations(archived)`)
69
+
70
+ // Activity log table
71
+ db.run(`
72
+ CREATE TABLE IF NOT EXISTS activity_log (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ session_id TEXT,
75
+ tool_name TEXT NOT NULL,
76
+ tool_input TEXT,
77
+ tool_output TEXT,
78
+ project TEXT,
79
+ file_path TEXT,
80
+ created_at TEXT NOT NULL
81
+ )
82
+ `)
83
+
84
+ db.run(`CREATE INDEX IF NOT EXISTS idx_activity_session ON activity_log(session_id)`)
85
+ db.run(`CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_log(created_at)`)
86
+ db.run(`CREATE INDEX IF NOT EXISTS idx_activity_project ON activity_log(project)`)
87
+
88
+ // Observation links (supersedes/contradicts/supports relationships)
89
+ db.run(`
90
+ CREATE TABLE IF NOT EXISTS observation_links (
91
+ source_id TEXT NOT NULL,
92
+ target_id TEXT NOT NULL,
93
+ link_type TEXT NOT NULL,
94
+ created_at TEXT NOT NULL,
95
+ PRIMARY KEY (source_id, target_id)
96
+ )
97
+ `)
98
+ }