claude-brain 0.30.2 → 0.30.3

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 (236) hide show
  1. package/README.md +241 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -29
  5. package/package.json +7 -3
  6. package/packs/backend/node.json +173 -173
  7. package/packs/core/javascript.json +176 -176
  8. package/packs/core/typescript.json +222 -222
  9. package/packs/frontend/react.json +254 -254
  10. package/packs/meta/testing.json +172 -172
  11. package/scripts/postinstall.mjs +531 -531
  12. package/src/automation/decision-detector.ts +452 -452
  13. package/src/automation/phase12-manager.ts +456 -456
  14. package/src/automation/proactive-recall.ts +373 -373
  15. package/src/automation/project-detector.ts +310 -310
  16. package/src/automation/repo-scanner.ts +210 -205
  17. package/src/cli/auto-setup.ts +75 -75
  18. package/src/cli/auto-start.ts +266 -266
  19. package/src/cli/bin.ts +264 -264
  20. package/src/cli/commands/autostart.ts +90 -90
  21. package/src/cli/commands/chroma.ts +578 -577
  22. package/src/cli/commands/export-training.ts +70 -70
  23. package/src/cli/commands/export.ts +130 -130
  24. package/src/cli/commands/git-hook.ts +183 -183
  25. package/src/cli/commands/hooks.ts +217 -217
  26. package/src/cli/commands/init.ts +123 -123
  27. package/src/cli/commands/install-mcp.ts +122 -111
  28. package/src/cli/commands/models.ts +979 -979
  29. package/src/cli/commands/pack.ts +200 -200
  30. package/src/cli/commands/refresh.ts +344 -339
  31. package/src/cli/commands/reindex.ts +120 -120
  32. package/src/cli/commands/serve.ts +466 -463
  33. package/src/cli/commands/start.ts +44 -44
  34. package/src/cli/commands/status.ts +220 -203
  35. package/src/cli/commands/uninstall-mcp.ts +45 -41
  36. package/src/cli/commands/update.ts +130 -124
  37. package/src/cli/migrate-chroma.ts +106 -106
  38. package/src/cli/ui/animations.ts +80 -80
  39. package/src/cli/ui/components.ts +82 -82
  40. package/src/cli/ui/index.ts +4 -4
  41. package/src/cli/ui/logo.ts +36 -36
  42. package/src/cli/ui/theme.ts +55 -55
  43. package/src/code-intelligence/indexer.ts +352 -352
  44. package/src/code-intelligence/linker.ts +178 -178
  45. package/src/code-intelligence/parser.ts +484 -484
  46. package/src/code-intelligence/query.ts +291 -291
  47. package/src/code-intelligence/schema.ts +83 -83
  48. package/src/code-intelligence/types.ts +95 -95
  49. package/src/config/defaults.ts +52 -52
  50. package/src/config/home.ts +56 -56
  51. package/src/config/index.ts +5 -5
  52. package/src/config/loader.ts +192 -192
  53. package/src/config/schema.ts +446 -415
  54. package/src/config/validator.ts +182 -182
  55. package/src/context/assembler.ts +407 -400
  56. package/src/context/index.ts +79 -79
  57. package/src/context/progress-tracker.ts +174 -174
  58. package/src/context/standards-manager.ts +287 -287
  59. package/src/context/validator.ts +58 -58
  60. package/src/diagnostics/index.ts +122 -121
  61. package/src/health/index.ts +233 -232
  62. package/src/hooks/brain-hook.ts +134 -131
  63. package/src/hooks/capture.ts +168 -168
  64. package/src/hooks/claude-code-mastery.md +112 -112
  65. package/src/hooks/context-hook.ts +260 -245
  66. package/src/hooks/deduplicator.ts +72 -72
  67. package/src/hooks/git-capture.ts +109 -109
  68. package/src/hooks/git-hook-installer.ts +211 -207
  69. package/src/hooks/index.ts +20 -20
  70. package/src/hooks/installer.ts +306 -288
  71. package/src/hooks/interceptor-hook.ts +204 -201
  72. package/src/hooks/passive-classifier.ts +397 -397
  73. package/src/hooks/queue.ts +160 -129
  74. package/src/hooks/session-tracker.ts +312 -312
  75. package/src/hooks/types.ts +52 -52
  76. package/src/index.ts +7 -7
  77. package/src/intelligence/cross-project/generalizer.ts +283 -283
  78. package/src/intelligence/cross-project/index.ts +7 -7
  79. package/src/intelligence/hf-downloader.ts +222 -222
  80. package/src/intelligence/hf-manifest.json +78 -78
  81. package/src/intelligence/index.ts +24 -24
  82. package/src/intelligence/inference-router.ts +762 -762
  83. package/src/intelligence/model-manager.ts +263 -245
  84. package/src/intelligence/optimization/index.ts +10 -10
  85. package/src/intelligence/optimization/precompute.ts +202 -202
  86. package/src/intelligence/optimization/semantic-cache.ts +213 -207
  87. package/src/intelligence/prediction/index.ts +7 -7
  88. package/src/intelligence/prediction/recommender.ts +276 -268
  89. package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
  90. package/src/intelligence/reasoning/index.ts +7 -7
  91. package/src/intelligence/temporal/evolution.ts +193 -197
  92. package/src/intelligence/temporal/index.ts +16 -16
  93. package/src/intelligence/temporal/query-processor.ts +190 -190
  94. package/src/intelligence/temporal/timeline.ts +272 -259
  95. package/src/intelligence/temporal/trends.ts +263 -263
  96. package/src/intelligence/tokenizer.ts +118 -118
  97. package/src/knowledge/entity-extractor.ts +447 -443
  98. package/src/knowledge/graph/builder.ts +185 -185
  99. package/src/knowledge/graph/linker.ts +201 -201
  100. package/src/knowledge/graph/memory-graph.ts +359 -359
  101. package/src/knowledge/graph/schema.ts +99 -99
  102. package/src/knowledge/graph/search.ts +166 -166
  103. package/src/knowledge/relationship-extractor.ts +108 -108
  104. package/src/memory/chroma/client.ts +211 -192
  105. package/src/memory/chroma/collection-manager.ts +92 -92
  106. package/src/memory/chroma/config.ts +57 -57
  107. package/src/memory/chroma/embeddings.ts +177 -175
  108. package/src/memory/chroma/index.ts +82 -82
  109. package/src/memory/chroma/migration.ts +270 -270
  110. package/src/memory/chroma/schemas.ts +69 -69
  111. package/src/memory/chroma/search.ts +319 -315
  112. package/src/memory/chroma/store.ts +755 -747
  113. package/src/memory/compression.ts +121 -121
  114. package/src/memory/consolidation/archiver.ts +162 -165
  115. package/src/memory/consolidation/merger.ts +182 -186
  116. package/src/memory/consolidation/scorer.ts +136 -136
  117. package/src/memory/database.ts +9 -0
  118. package/src/memory/dual-write.ts +145 -0
  119. package/src/memory/embeddings.ts +226 -226
  120. package/src/memory/episodic/detector.ts +108 -108
  121. package/src/memory/episodic/manager.ts +347 -351
  122. package/src/memory/episodic/summarizer.ts +179 -179
  123. package/src/memory/episodic/types.ts +52 -52
  124. package/src/memory/fts5-search.ts +692 -633
  125. package/src/memory/index.ts +943 -1060
  126. package/src/memory/migrations/add-fts5.ts +118 -108
  127. package/src/memory/patterns.ts +438 -438
  128. package/src/memory/pruning.ts +60 -60
  129. package/src/memory/schema.ts +88 -88
  130. package/src/memory/store.ts +911 -787
  131. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  132. package/src/packs/index.ts +9 -9
  133. package/src/packs/loader.ts +134 -134
  134. package/src/packs/manager.ts +204 -204
  135. package/src/packs/ranker.ts +78 -78
  136. package/src/packs/types.ts +81 -81
  137. package/src/phase12/index.ts +5 -5
  138. package/src/retrieval/bm25/index.ts +300 -297
  139. package/src/retrieval/bm25/tokenizer.ts +184 -184
  140. package/src/retrieval/feedback/adaptive.ts +221 -221
  141. package/src/retrieval/feedback/index.ts +16 -16
  142. package/src/retrieval/feedback/metrics.ts +221 -221
  143. package/src/retrieval/feedback/store.ts +283 -283
  144. package/src/retrieval/fusion/index.ts +194 -194
  145. package/src/retrieval/fusion/rrf.ts +165 -165
  146. package/src/retrieval/index.ts +12 -12
  147. package/src/retrieval/pipeline.ts +375 -375
  148. package/src/retrieval/query/expander.ts +203 -203
  149. package/src/retrieval/query/index.ts +27 -27
  150. package/src/retrieval/query/intent-classifier.ts +252 -252
  151. package/src/retrieval/query/temporal-parser.ts +295 -295
  152. package/src/retrieval/reranker/index.ts +189 -188
  153. package/src/retrieval/reranker/model.ts +99 -95
  154. package/src/retrieval/service.ts +125 -125
  155. package/src/retrieval/types.ts +162 -162
  156. package/src/routing/entity-extractor.ts +454 -454
  157. package/src/routing/handlers/exploration-handler.ts +369 -0
  158. package/src/routing/handlers/index.ts +19 -0
  159. package/src/routing/handlers/memory-handler.ts +273 -0
  160. package/src/routing/handlers/mutation-handler.ts +241 -0
  161. package/src/routing/handlers/recall-handler.ts +642 -0
  162. package/src/routing/handlers/shared.ts +515 -0
  163. package/src/routing/handlers/types.ts +48 -0
  164. package/src/routing/intent-classifier.ts +552 -552
  165. package/src/routing/response-filter.ts +399 -391
  166. package/src/routing/router.ts +245 -2193
  167. package/src/routing/search-engine.ts +521 -514
  168. package/src/routing/types.ts +104 -94
  169. package/src/scripts/health-check.ts +118 -118
  170. package/src/scripts/setup.ts +122 -122
  171. package/src/server/auto-updater.ts +283 -276
  172. package/src/server/handlers/call-tool.ts +159 -159
  173. package/src/server/handlers/list-tools.ts +35 -35
  174. package/src/server/handlers/tools/auto-remember.ts +165 -165
  175. package/src/server/handlers/tools/brain.ts +86 -86
  176. package/src/server/handlers/tools/create-project.ts +135 -135
  177. package/src/server/handlers/tools/get-code-standards.ts +123 -123
  178. package/src/server/handlers/tools/get-corrections.ts +152 -152
  179. package/src/server/handlers/tools/get-patterns.ts +156 -156
  180. package/src/server/handlers/tools/get-project-context.ts +75 -75
  181. package/src/server/handlers/tools/index.ts +30 -30
  182. package/src/server/handlers/tools/init-project.ts +756 -756
  183. package/src/server/handlers/tools/list-projects.ts +126 -126
  184. package/src/server/handlers/tools/recall-similar.ts +87 -87
  185. package/src/server/handlers/tools/recognize-pattern.ts +132 -132
  186. package/src/server/handlers/tools/record-correction.ts +131 -131
  187. package/src/server/handlers/tools/remember-decision.ts +168 -168
  188. package/src/server/handlers/tools/schemas.ts +179 -179
  189. package/src/server/handlers/tools/search-code.ts +122 -122
  190. package/src/server/handlers/tools/smart-context.ts +146 -146
  191. package/src/server/handlers/tools/update-progress.ts +131 -131
  192. package/src/server/http-api.ts +215 -1229
  193. package/src/server/mcp-proxy.ts +85 -84
  194. package/src/server/mcp-server.ts +285 -284
  195. package/src/server/middleware/auth.ts +39 -0
  196. package/src/server/middleware/error-handler.ts +37 -0
  197. package/src/server/middleware/rate-limit.ts +53 -0
  198. package/src/server/middleware/validate.ts +42 -0
  199. package/src/server/pid-manager.ts +137 -136
  200. package/src/server/providers/resources.ts +581 -581
  201. package/src/server/routes/code.ts +228 -0
  202. package/src/server/routes/context.ts +26 -0
  203. package/src/server/routes/health.ts +19 -0
  204. package/src/server/routes/helpers.ts +100 -0
  205. package/src/server/routes/hooks.ts +197 -0
  206. package/src/server/routes/mcp.ts +47 -0
  207. package/src/server/routes/memory.ts +397 -0
  208. package/src/server/routes/models.ts +96 -0
  209. package/src/server/routes/projects.ts +89 -0
  210. package/src/server/routes/types.ts +21 -0
  211. package/src/server/schemas/api-schemas.ts +202 -0
  212. package/src/server/services.ts +720 -720
  213. package/src/server/utils/memory-indicator.ts +84 -84
  214. package/src/server/utils/response-formatter.ts +129 -129
  215. package/src/server/web-viewer.ts +1145 -1115
  216. package/src/setup/index.ts +38 -38
  217. package/src/tools/registry.ts +115 -115
  218. package/src/tools/schemas.ts +666 -666
  219. package/src/tools/types.ts +412 -412
  220. package/src/training/data-store.ts +320 -298
  221. package/src/training/retrain-pipeline.ts +399 -394
  222. package/src/utils/error-handler.ts +136 -136
  223. package/src/utils/index.ts +58 -58
  224. package/src/utils/kill-port.ts +55 -53
  225. package/src/utils/phase12-helper.ts +56 -56
  226. package/src/utils/safe-path.ts +43 -0
  227. package/src/utils/timing.ts +47 -47
  228. package/src/utils/transaction.ts +63 -63
  229. package/src/vault/index.ts +4 -3
  230. package/src/vault/paths.ts +106 -106
  231. package/src/vault/query.ts +4 -1
  232. package/src/vault/reader.ts +44 -1
  233. package/src/vault/watcher.ts +24 -1
  234. package/src/vault/writer.ts +487 -413
  235. package/skills/persistent-memory/SKILL.md +0 -148
  236. package/skills/persistent-memory/references/tool-reference.md +0 -90
@@ -1,747 +1,755 @@
1
- import { randomUUID } from 'crypto'
2
- import type { Logger } from 'pino'
3
- import type { CollectionManager } from './collection-manager'
4
- import type { MemoryMetadata } from './schemas'
5
- import type { EmbeddingProvider } from './embeddings'
6
- import type { SearchResult } from './search'
7
-
8
- /**
9
- * Sanitize metadata for ChromaDB v3.x compatibility.
10
- * Strips undefined/null values (ChromaDB only accepts string, number, boolean).
11
- */
12
- function sanitizeMetadata(metadata: Record<string, any>): Record<string, string | number | boolean> {
13
- const clean: Record<string, string | number | boolean> = {}
14
- for (const [key, value] of Object.entries(metadata)) {
15
- if (value === undefined || value === null) continue
16
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
17
- clean[key] = value
18
- } else if (Array.isArray(value)) {
19
- clean[key] = JSON.stringify(value)
20
- } else {
21
- clean[key] = String(value)
22
- }
23
- }
24
- return clean
25
- }
26
-
27
- export interface StoreDecisionInput {
28
- id?: string
29
- project: string
30
- context: string
31
- decision: string
32
- reasoning: string
33
- alternatives?: string
34
- outcome?: string
35
- tags?: string[]
36
- }
37
-
38
- export interface StoreMemoryInput {
39
- project: string
40
- content: string
41
- type?: 'fact' | 'preference' | 'constraint' | 'goal' | 'general'
42
- source?: string
43
- confidence?: number
44
- metadata?: Record<string, any>
45
- }
46
-
47
- export interface StoredDecision {
48
- id: string
49
- decision: string
50
- metadata: Record<string, any>
51
- distance?: number
52
- }
53
-
54
- export interface StoredMemory {
55
- id: string
56
- content: string
57
- metadata: Record<string, any>
58
- distance?: number
59
- }
60
-
61
- export type DecisionStoredCallback = (input: StoreDecisionInput & { id: string }) => void
62
-
63
- export class ChromaMemoryStore {
64
- private logger: Logger
65
- private collections: CollectionManager
66
- private embeddings?: EmbeddingProvider
67
- private onDecisionStored: DecisionStoredCallback[] = []
68
-
69
- constructor(logger: Logger, collections: CollectionManager, embeddings?: EmbeddingProvider) {
70
- this.logger = logger.child({ component: 'chroma-memory-store' })
71
- this.collections = collections
72
- this.embeddings = embeddings
73
- }
74
-
75
- addDecisionStoredListener(callback: DecisionStoredCallback): () => void {
76
- this.onDecisionStored.push(callback)
77
- return () => {
78
- const idx = this.onDecisionStored.indexOf(callback)
79
- if (idx >= 0) this.onDecisionStored.splice(idx, 1)
80
- }
81
- }
82
-
83
- /**
84
- * Check for duplicate decisions by searching for similar content
85
- * Returns existing results if similarity >= threshold
86
- */
87
- async searchForDuplicates(
88
- decision: string,
89
- project: string,
90
- similarityThreshold: number = 0.9
91
- ): Promise<{ id: string; similarity: number }[]> {
92
- try {
93
- const collection = await this.collections.getDecisions()
94
-
95
- let results: any
96
-
97
- if (this.embeddings) {
98
- const embedding = await this.embeddings.generate(decision)
99
- results = await collection.query({
100
- queryEmbeddings: [embedding],
101
- nResults: 3,
102
- where: { project: { $eq: project } },
103
- include: ['documents', 'metadatas', 'distances']
104
- })
105
- } else {
106
- results = await collection.query({
107
- queryTexts: [decision],
108
- nResults: 3,
109
- where: { project: { $eq: project } },
110
- include: ['documents', 'metadatas', 'distances']
111
- })
112
- }
113
-
114
- if (!results.ids || results.ids[0]?.length === 0) {
115
- return []
116
- }
117
-
118
- const duplicates: { id: string; similarity: number }[] = []
119
- const ids = results.ids[0] || []
120
- const distances = results.distances?.[0] || []
121
-
122
- for (let i = 0; i < ids.length; i++) {
123
- const similarity = 1 - (distances[i] || 0)
124
- if (similarity >= similarityThreshold) {
125
- duplicates.push({ id: ids[i], similarity })
126
- }
127
- }
128
-
129
- return duplicates
130
- } catch (error) {
131
- this.logger.warn({ error }, 'Failed to check for duplicates, proceeding with store')
132
- return []
133
- }
134
- }
135
-
136
- async storeDecision(input: StoreDecisionInput): Promise<string> {
137
- // Check for duplicate decisions (similarity > 0.9)
138
- const existing = await this.searchForDuplicates(input.decision, input.project, 0.9)
139
- const firstDuplicate = existing[0]
140
- if (firstDuplicate) {
141
- this.logger.info({ existingId: firstDuplicate.id, similarity: firstDuplicate.similarity }, 'Skipping duplicate decision')
142
- return firstDuplicate.id
143
- }
144
-
145
- const id = input.id || randomUUID()
146
- const now = new Date().toISOString()
147
-
148
- const metadata: Record<string, any> = {
149
- project: input.project,
150
- context: input.context,
151
- reasoning: input.reasoning,
152
- alternatives: input.alternatives || '',
153
- outcome: input.outcome || '',
154
- tags: (input.tags || []).join(','),
155
- created_at: now,
156
- updated_at: now,
157
- source: 'manual',
158
- decision: input.decision // Include the decision text in metadata
159
- }
160
-
161
- try {
162
- const collection = await this.collections.getDecisions()
163
-
164
- const embeddings = this.embeddings
165
- ? [await this.embeddings.generate(input.decision)]
166
- : undefined
167
-
168
- await collection.add({
169
- ids: [id],
170
- documents: [input.decision],
171
- metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
172
- ...(embeddings ? { embeddings } : {})
173
- })
174
-
175
- // ALSO store in memories collection for unified semantic search
176
- try {
177
- const memoriesCollection = await this.collections.getMemories()
178
- const memoryContent = `Decision: ${input.decision}\nContext: ${input.context}\nReasoning: ${input.reasoning}`
179
- const memoryMetadata = {
180
- project: input.project,
181
- type: 'decision',
182
- source: 'remember_decision',
183
- confidence: 1.0,
184
- created_at: now,
185
- updated_at: now,
186
- decision_id: id,
187
- // Phase 19: Include decision fields so memories collection results
188
- // can surface decision content without cross-collection lookup
189
- decision: input.decision,
190
- reasoning: input.reasoning,
191
- context: input.context
192
- }
193
-
194
- await memoriesCollection.add({
195
- ids: [id], // Use same ID for cross-reference
196
- documents: [memoryContent],
197
- metadatas: [sanitizeMetadata(memoryMetadata)],
198
- ...(embeddings ? { embeddings } : {})
199
- })
200
-
201
- this.logger.debug({ id }, 'Decision also stored in memories collection')
202
- } catch (memError) {
203
- this.logger.warn({ error: memError, id }, 'Failed to store decision in memories collection')
204
- }
205
-
206
- this.logger.info(
207
- { id, project: input.project },
208
- 'Decision stored in ChromaDB'
209
- )
210
-
211
- // Notify listeners (e.g., knowledge graph builder)
212
- for (const callback of this.onDecisionStored) {
213
- try {
214
- callback({ ...input, id })
215
- } catch (callbackError) {
216
- this.logger.warn({ error: callbackError }, 'Decision stored callback failed')
217
- }
218
- }
219
-
220
- return id
221
-
222
- } catch (error) {
223
- this.logger.error({ error, input }, 'Failed to store decision')
224
- throw error
225
- }
226
- }
227
-
228
- async storePattern(input: {
229
- id?: string
230
- project: string
231
- pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
232
- description: string
233
- example?: string
234
- confidence: number
235
- context?: string
236
- source?: string
237
- }): Promise<string> {
238
- const id = input.id || randomUUID()
239
- const now = new Date().toISOString()
240
-
241
- const metadata: Record<string, any> = {
242
- project: input.project,
243
- pattern_type: input.pattern_type,
244
- description: input.description,
245
- example: input.example || '',
246
- confidence: input.confidence,
247
- context: input.context || '',
248
- created_at: now,
249
- updated_at: now,
250
- source: input.source || 'manual'
251
- }
252
-
253
- try {
254
- const collection = await this.collections.getPatterns()
255
-
256
- const embeddings = this.embeddings
257
- ? [await this.embeddings.generate(input.description)]
258
- : undefined
259
-
260
- await collection.add({
261
- ids: [id],
262
- documents: [input.description],
263
- metadatas: [sanitizeMetadata(metadata)],
264
- ...(embeddings ? { embeddings } : {})
265
- })
266
-
267
- // ALSO store in memories collection for unified semantic search
268
- try {
269
- const memoriesCollection = await this.collections.getMemories()
270
- const memoryContent = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
271
- const memoryMetadata = {
272
- project: input.project,
273
- type: 'pattern',
274
- source: 'recognize_pattern',
275
- confidence: input.confidence,
276
- created_at: now,
277
- updated_at: now,
278
- pattern_id: id,
279
- pattern_type: input.pattern_type
280
- }
281
-
282
- await memoriesCollection.add({
283
- ids: [id],
284
- documents: [memoryContent],
285
- metadatas: [sanitizeMetadata(memoryMetadata)],
286
- ...(embeddings ? { embeddings } : {})
287
- })
288
-
289
- this.logger.debug({ id }, 'Pattern also stored in memories collection')
290
- } catch (memError) {
291
- this.logger.warn({ error: memError, id }, 'Failed to store pattern in memories collection')
292
- }
293
-
294
- this.logger.info({ id, pattern_type: input.pattern_type }, 'Pattern stored in ChromaDB')
295
-
296
- return id
297
-
298
- } catch (error) {
299
- this.logger.error({ error, input }, 'Failed to store pattern')
300
- throw error
301
- }
302
- }
303
-
304
- async storeCorrection(input: {
305
- id?: string
306
- project: string
307
- original: string
308
- correction: string
309
- reasoning: string
310
- context?: string
311
- confidence: number
312
- }): Promise<string> {
313
- const id = input.id || randomUUID()
314
- const now = new Date().toISOString()
315
-
316
- const metadata: Record<string, any> = {
317
- project: input.project,
318
- original: input.original,
319
- correction: input.correction,
320
- reasoning: input.reasoning,
321
- context: input.context || '',
322
- confidence: input.confidence,
323
- created_at: now,
324
- updated_at: now,
325
- source: 'manual'
326
- }
327
-
328
- try {
329
- const collection = await this.collections.getCorrections()
330
-
331
- const embeddings = this.embeddings
332
- ? [await this.embeddings.generate(input.correction)]
333
- : undefined
334
-
335
- await collection.add({
336
- ids: [id],
337
- documents: [input.correction],
338
- metadatas: [sanitizeMetadata(metadata)],
339
- ...(embeddings ? { embeddings } : {})
340
- })
341
-
342
- // ALSO store in memories collection for unified semantic search
343
- try {
344
- const memoriesCollection = await this.collections.getMemories()
345
- const memoryContent = `Correction: ${input.correction}\nOriginal: ${input.original}\nReasoning: ${input.reasoning}${input.context ? `\nContext: ${input.context}` : ''}`
346
- const memoryMetadata = {
347
- project: input.project,
348
- type: 'correction',
349
- source: 'record_correction',
350
- confidence: input.confidence,
351
- created_at: now,
352
- updated_at: now,
353
- correction_id: id
354
- }
355
-
356
- await memoriesCollection.add({
357
- ids: [id],
358
- documents: [memoryContent],
359
- metadatas: [sanitizeMetadata(memoryMetadata)],
360
- ...(embeddings ? { embeddings } : {})
361
- })
362
-
363
- this.logger.debug({ id }, 'Correction also stored in memories collection')
364
- } catch (memError) {
365
- this.logger.warn({ error: memError, id }, 'Failed to store correction in memories collection')
366
- }
367
-
368
- this.logger.info({ id, project: input.project }, 'Correction stored in ChromaDB')
369
-
370
- return id
371
-
372
- } catch (error) {
373
- this.logger.error({ error, input }, 'Failed to store correction')
374
- throw error
375
- }
376
- }
377
-
378
- async storeMemory(input: StoreMemoryInput): Promise<string> {
379
- const id = randomUUID()
380
- const now = new Date().toISOString()
381
-
382
- const metadata: MemoryMetadata = {
383
- project: input.project,
384
- type: input.type || 'general',
385
- source: input.source || 'unknown',
386
- confidence: input.confidence || 1.0,
387
- created_at: now,
388
- updated_at: now
389
- }
390
-
391
- try {
392
- const collection = await this.collections.getMemories()
393
-
394
- const embeddings = this.embeddings
395
- ? [await this.embeddings.generate(input.content)]
396
- : undefined
397
-
398
- await collection.add({
399
- ids: [id],
400
- documents: [input.content],
401
- metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
402
- ...(embeddings ? { embeddings } : {})
403
- })
404
-
405
- this.logger.info(
406
- { id, project: input.project, type: input.type },
407
- 'Memory stored in ChromaDB'
408
- )
409
-
410
- return id
411
-
412
- } catch (error) {
413
- this.logger.error({ error, input }, 'Failed to store memory')
414
- throw error
415
- }
416
- }
417
-
418
- async upsertDecision(id: string, input: StoreDecisionInput): Promise<void> {
419
- const now = new Date().toISOString()
420
-
421
- const metadata: Record<string, any> = {
422
- project: input.project,
423
- context: input.context,
424
- reasoning: input.reasoning,
425
- alternatives: input.alternatives || '',
426
- outcome: input.outcome || '',
427
- tags: (input.tags || []).join(','),
428
- created_at: now,
429
- updated_at: now,
430
- source: 'manual'
431
- }
432
-
433
- try {
434
- const collection = await this.collections.getDecisions()
435
-
436
- const embeddings = this.embeddings
437
- ? [await this.embeddings.generate(input.decision)]
438
- : undefined
439
-
440
- await collection.upsert({
441
- ids: [id],
442
- documents: [input.decision],
443
- metadatas: [sanitizeMetadata(metadata as Record<string, any>)],
444
- ...(embeddings ? { embeddings } : {})
445
- })
446
-
447
- this.logger.info({ id }, 'Decision upserted')
448
-
449
- } catch (error) {
450
- this.logger.error({ error, id, errorMessage: error instanceof Error ? error.message : String(error) }, 'Failed to upsert decision')
451
- throw error
452
- }
453
- }
454
-
455
- async getDecision(id: string): Promise<StoredDecision | null> {
456
- try {
457
- const collection = await this.collections.getDecisions()
458
-
459
- const result = await collection.get({
460
- ids: [id],
461
- include: ['documents', 'metadatas']
462
- })
463
-
464
- if (result.ids.length === 0) {
465
- return null
466
- }
467
-
468
- return {
469
- id: result.ids[0]!,
470
- decision: result.documents![0] as string,
471
- metadata: result.metadatas![0] as Record<string, any>
472
- }
473
-
474
- } catch (error) {
475
- this.logger.error({ error, id }, 'Failed to get decision')
476
- throw error
477
- }
478
- }
479
-
480
- async getDecisionsByProject(project: string): Promise<StoredDecision[]> {
481
- try {
482
- const collection = await this.collections.getDecisions()
483
-
484
- const result = await collection.get({
485
- where: { project },
486
- include: ['documents', 'metadatas']
487
- })
488
-
489
- return result.ids.map((id, i) => ({
490
- id,
491
- decision: result.documents![i] as string,
492
- metadata: result.metadatas![i] as Record<string, any>
493
- }))
494
-
495
- } catch (error) {
496
- this.logger.error({ error, project }, 'Failed to get decisions by project')
497
- throw error
498
- }
499
- }
500
-
501
- async deleteDecision(id: string): Promise<void> {
502
- try {
503
- // Delete from decisions collection
504
- const collection = await this.collections.getDecisions()
505
- await collection.delete({ ids: [id] })
506
-
507
- // Verify deletion succeeded
508
- try {
509
- const verify = await collection.get({ ids: [id] })
510
- if (verify.ids.length > 0) {
511
- this.logger.warn({ id }, 'Decision still exists after delete — retrying')
512
- await collection.delete({ ids: [id] })
513
- }
514
- } catch {
515
- // Verification query failed, assume delete succeeded
516
- }
517
-
518
- // ALSO delete from memories collection (dual storage uses same ID)
519
- try {
520
- const memoriesCollection = await this.collections.getMemories()
521
- await memoriesCollection.delete({ ids: [id] })
522
- this.logger.debug({ id }, 'Decision also deleted from memories collection')
523
- } catch {
524
- // Memories collection entry may not exist, that's ok
525
- }
526
-
527
- this.logger.info({ id }, 'Decision deleted from all collections')
528
-
529
- } catch (error) {
530
- this.logger.error({ error, id }, 'Failed to delete decision')
531
- throw error
532
- }
533
- }
534
-
535
- async getAllDecisions(): Promise<StoredDecision[]> {
536
- try {
537
- const collection = await this.collections.getDecisions()
538
-
539
- const result = await collection.get({
540
- include: ['documents', 'metadatas']
541
- })
542
-
543
- return result.ids.map((id, i) => ({
544
- id,
545
- decision: result.documents![i] as string,
546
- metadata: result.metadatas![i] as Record<string, any>
547
- }))
548
-
549
- } catch (error) {
550
- this.logger.error({ error }, 'Failed to get all decisions')
551
- throw error
552
- }
553
- }
554
-
555
- async getPatternsByProject(project: string, options?: {
556
- pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
557
- limit?: number
558
- }): Promise<any[]> {
559
- try {
560
- const collection = await this.collections.getPatterns()
561
-
562
- // Build where clause - handle single vs multiple conditions
563
- let where: any
564
- if (options?.pattern_type) {
565
- // Multiple conditions: use $and operator
566
- where = {
567
- $and: [
568
- { project: { $eq: project } },
569
- { pattern_type: { $eq: options.pattern_type } }
570
- ]
571
- }
572
- } else {
573
- // Single condition
574
- where = { project: { $eq: project } }
575
- }
576
-
577
- const result = await collection.get({
578
- where,
579
- include: ['documents', 'metadatas'],
580
- ...(options?.limit ? { limit: options.limit } : {})
581
- })
582
-
583
- return result.ids.map((id, i) => ({
584
- id,
585
- description: result.documents![i] as string,
586
- metadata: result.metadatas![i] as Record<string, any>
587
- }))
588
-
589
- } catch (error) {
590
- this.logger.error({ error, project }, 'Failed to get patterns by project')
591
- throw error
592
- }
593
- }
594
-
595
- async getCorrectionsByProject(project: string, limit: number = 10): Promise<any[]> {
596
- try {
597
- const collection = await this.collections.getCorrections()
598
-
599
- const result = await collection.get({
600
- where: { project },
601
- include: ['documents', 'metadatas'],
602
- limit
603
- })
604
-
605
- return result.ids.map((id, i) => ({
606
- id,
607
- correction: result.documents![i] as string,
608
- metadata: result.metadatas![i] as Record<string, any>
609
- }))
610
-
611
- } catch (error) {
612
- this.logger.error({ error, project }, 'Failed to get corrections by project')
613
- throw error
614
- }
615
- }
616
-
617
- async searchPatterns(
618
- query: string,
619
- options: {
620
- project?: string
621
- pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
622
- limit?: number
623
- minSimilarity?: number
624
- } = {}
625
- ): Promise<SearchResult[]> {
626
- const {
627
- project,
628
- pattern_type,
629
- limit = 10,
630
- minSimilarity = 0.5
631
- } = options
632
-
633
- try {
634
- const collection = await this.collections.getPatterns()
635
-
636
- const where: any = {}
637
- if (project) {
638
- where.project = project
639
- }
640
- if (pattern_type) {
641
- where.pattern_type = pattern_type
642
- }
643
-
644
- let results: any
645
-
646
- if (this.embeddings) {
647
- const embedding = await this.embeddings.generate(query)
648
- results = await collection.query({
649
- queryEmbeddings: [embedding],
650
- nResults: limit,
651
- where: Object.keys(where).length > 0 ? where : undefined,
652
- include: ['documents', 'metadatas', 'distances']
653
- })
654
- } else {
655
- results = await collection.query({
656
- queryTexts: [query],
657
- nResults: limit,
658
- where: Object.keys(where).length > 0 ? where : undefined,
659
- include: ['documents', 'metadatas', 'distances']
660
- })
661
- }
662
-
663
- return this.processResults(results, minSimilarity)
664
-
665
- } catch (error) {
666
- this.logger.error({ error, query }, 'Pattern search failed')
667
- throw error
668
- }
669
- }
670
-
671
- async searchCorrections(
672
- query: string,
673
- options: {
674
- project?: string
675
- limit?: number
676
- minSimilarity?: number
677
- } = {}
678
- ): Promise<SearchResult[]> {
679
- const {
680
- project,
681
- limit = 10,
682
- minSimilarity = 0.5
683
- } = options
684
-
685
- try {
686
- const collection = await this.collections.getCorrections()
687
-
688
- const where: any = project ? { project } : {}
689
-
690
- let results: any
691
-
692
- if (this.embeddings) {
693
- const embedding = await this.embeddings.generate(query)
694
- results = await collection.query({
695
- queryEmbeddings: [embedding],
696
- nResults: limit,
697
- where: Object.keys(where).length > 0 ? where : undefined,
698
- include: ['documents', 'metadatas', 'distances']
699
- })
700
- } else {
701
- results = await collection.query({
702
- queryTexts: [query],
703
- nResults: limit,
704
- where: Object.keys(where).length > 0 ? where : undefined,
705
- include: ['documents', 'metadatas', 'distances']
706
- })
707
- }
708
-
709
- return this.processResults(results, minSimilarity)
710
-
711
- } catch (error) {
712
- this.logger.error({ error, query }, 'Correction search failed')
713
- throw error
714
- }
715
- }
716
-
717
- private processResults(
718
- results: any,
719
- minSimilarity: number
720
- ): SearchResult[] {
721
- if (!results.ids || results.ids.length === 0) {
722
- return []
723
- }
724
-
725
- const processed: SearchResult[] = []
726
-
727
- const ids = results.ids[0] || []
728
- const documents = results.documents?.[0] || []
729
- const metadatas = results.metadatas?.[0] || []
730
- const distances = results.distances?.[0] || []
731
-
732
- for (let i = 0; i < ids.length; i++) {
733
- const similarity = 1 - (distances[i] || 0)
734
-
735
- if (similarity >= minSimilarity) {
736
- processed.push({
737
- id: ids[i],
738
- content: documents[i],
739
- metadata: metadatas[i],
740
- similarity
741
- })
742
- }
743
- }
744
-
745
- return processed.sort((a, b) => b.similarity - a.similarity)
746
- }
747
- }
1
+ import { randomUUID } from 'crypto'
2
+ import type { Logger } from 'pino'
3
+ import type { CollectionManager } from './collection-manager'
4
+ import type { MemoryMetadata } from './schemas'
5
+ import type { EmbeddingProvider } from './embeddings'
6
+ import type { SearchResult } from './search'
7
+
8
+ /** ChromaDB query result shape */
9
+ interface ChromaQueryResult {
10
+ ids: string[][]
11
+ documents?: (string | null)[][]
12
+ metadatas?: (Record<string, unknown> | null)[][]
13
+ distances?: number[][]
14
+ }
15
+
16
+ /**
17
+ * Sanitize metadata for ChromaDB v3.x compatibility.
18
+ * Strips undefined/null values (ChromaDB only accepts string, number, boolean).
19
+ */
20
+ function sanitizeMetadata(metadata: Record<string, unknown>): Record<string, string | number | boolean> {
21
+ const clean: Record<string, string | number | boolean> = {}
22
+ for (const [key, value] of Object.entries(metadata)) {
23
+ if (value === undefined || value === null) continue
24
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
25
+ clean[key] = value
26
+ } else if (Array.isArray(value)) {
27
+ clean[key] = JSON.stringify(value)
28
+ } else {
29
+ clean[key] = String(value)
30
+ }
31
+ }
32
+ return clean
33
+ }
34
+
35
+ export interface StoreDecisionInput {
36
+ id?: string
37
+ project: string
38
+ context: string
39
+ decision: string
40
+ reasoning: string
41
+ alternatives?: string
42
+ outcome?: string
43
+ tags?: string[]
44
+ }
45
+
46
+ export interface StoreMemoryInput {
47
+ project: string
48
+ content: string
49
+ type?: 'fact' | 'preference' | 'constraint' | 'goal' | 'general'
50
+ source?: string
51
+ confidence?: number
52
+ metadata?: Record<string, unknown>
53
+ }
54
+
55
+ export interface StoredDecision {
56
+ id: string
57
+ decision: string
58
+ metadata: Record<string, unknown>
59
+ distance?: number
60
+ }
61
+
62
+ export interface StoredMemory {
63
+ id: string
64
+ content: string
65
+ metadata: Record<string, unknown>
66
+ distance?: number
67
+ }
68
+
69
+ export type DecisionStoredCallback = (input: StoreDecisionInput & { id: string }) => void
70
+
71
+ export class ChromaMemoryStore {
72
+ private logger: Logger
73
+ private collections: CollectionManager
74
+ private embeddings?: EmbeddingProvider
75
+ private onDecisionStored: DecisionStoredCallback[] = []
76
+
77
+ constructor(logger: Logger, collections: CollectionManager, embeddings?: EmbeddingProvider) {
78
+ this.logger = logger.child({ component: 'chroma-memory-store' })
79
+ this.collections = collections
80
+ this.embeddings = embeddings
81
+ }
82
+
83
+ addDecisionStoredListener(callback: DecisionStoredCallback): () => void {
84
+ this.onDecisionStored.push(callback)
85
+ return () => {
86
+ const idx = this.onDecisionStored.indexOf(callback)
87
+ if (idx >= 0) this.onDecisionStored.splice(idx, 1)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check for duplicate decisions by searching for similar content
93
+ * Returns existing results if similarity >= threshold
94
+ */
95
+ async searchForDuplicates(
96
+ decision: string,
97
+ project: string,
98
+ similarityThreshold: number = 0.9
99
+ ): Promise<{ id: string; similarity: number }[]> {
100
+ try {
101
+ const collection = await this.collections.getDecisions()
102
+
103
+ let results: ChromaQueryResult
104
+
105
+ if (this.embeddings) {
106
+ const embedding = await this.embeddings.generate(decision)
107
+ results = await collection.query({
108
+ queryEmbeddings: [embedding],
109
+ nResults: 3,
110
+ where: { project: { $eq: project } },
111
+ include: ['documents', 'metadatas', 'distances']
112
+ }) as unknown as ChromaQueryResult
113
+ } else {
114
+ results = await collection.query({
115
+ queryTexts: [decision],
116
+ nResults: 3,
117
+ where: { project: { $eq: project } },
118
+ include: ['documents', 'metadatas', 'distances']
119
+ })
120
+ }
121
+
122
+ if (!results.ids || results.ids[0]?.length === 0) {
123
+ return []
124
+ }
125
+
126
+ const duplicates: { id: string; similarity: number }[] = []
127
+ const ids = results.ids[0] || []
128
+ const distances = results.distances?.[0] || []
129
+
130
+ for (let i = 0; i < ids.length; i++) {
131
+ const similarity = 1 - (distances[i] || 0)
132
+ if (similarity >= similarityThreshold) {
133
+ duplicates.push({ id: ids[i], similarity })
134
+ }
135
+ }
136
+
137
+ return duplicates
138
+ } catch (error) {
139
+ this.logger.warn({ error }, 'Failed to check for duplicates, proceeding with store')
140
+ return []
141
+ }
142
+ }
143
+
144
+ async storeDecision(input: StoreDecisionInput): Promise<string> {
145
+ // Check for duplicate decisions (similarity > 0.9)
146
+ const existing = await this.searchForDuplicates(input.decision, input.project, 0.9)
147
+ const firstDuplicate = existing[0]
148
+ if (firstDuplicate) {
149
+ this.logger.info({ existingId: firstDuplicate.id, similarity: firstDuplicate.similarity }, 'Skipping duplicate decision')
150
+ return firstDuplicate.id
151
+ }
152
+
153
+ const id = input.id || randomUUID()
154
+ const now = new Date().toISOString()
155
+
156
+ const metadata: Record<string, unknown> = {
157
+ project: input.project,
158
+ context: input.context,
159
+ reasoning: input.reasoning,
160
+ alternatives: input.alternatives || '',
161
+ outcome: input.outcome || '',
162
+ tags: (input.tags || []).join(','),
163
+ created_at: now,
164
+ updated_at: now,
165
+ source: 'manual',
166
+ decision: input.decision // Include the decision text in metadata
167
+ }
168
+
169
+ try {
170
+ const collection = await this.collections.getDecisions()
171
+
172
+ const embeddings = this.embeddings
173
+ ? [await this.embeddings.generate(input.decision)]
174
+ : undefined
175
+
176
+ await collection.add({
177
+ ids: [id],
178
+ documents: [input.decision],
179
+ metadatas: [sanitizeMetadata(metadata as Record<string, unknown>)],
180
+ ...(embeddings ? { embeddings } : {})
181
+ })
182
+
183
+ // ALSO store in memories collection for unified semantic search
184
+ try {
185
+ const memoriesCollection = await this.collections.getMemories()
186
+ const memoryContent = `Decision: ${input.decision}\nContext: ${input.context}\nReasoning: ${input.reasoning}`
187
+ const memoryMetadata = {
188
+ project: input.project,
189
+ type: 'decision',
190
+ source: 'remember_decision',
191
+ confidence: 1.0,
192
+ created_at: now,
193
+ updated_at: now,
194
+ decision_id: id,
195
+ // Phase 19: Include decision fields so memories collection results
196
+ // can surface decision content without cross-collection lookup
197
+ decision: input.decision,
198
+ reasoning: input.reasoning,
199
+ context: input.context
200
+ }
201
+
202
+ await memoriesCollection.add({
203
+ ids: [id], // Use same ID for cross-reference
204
+ documents: [memoryContent],
205
+ metadatas: [sanitizeMetadata(memoryMetadata)],
206
+ ...(embeddings ? { embeddings } : {})
207
+ })
208
+
209
+ this.logger.debug({ id }, 'Decision also stored in memories collection')
210
+ } catch (memError) {
211
+ this.logger.warn({ error: memError, id }, 'Failed to store decision in memories collection')
212
+ }
213
+
214
+ this.logger.info(
215
+ { id, project: input.project },
216
+ 'Decision stored in ChromaDB'
217
+ )
218
+
219
+ // Notify listeners (e.g., knowledge graph builder)
220
+ for (const callback of this.onDecisionStored) {
221
+ try {
222
+ callback({ ...input, id })
223
+ } catch (callbackError) {
224
+ this.logger.warn({ error: callbackError }, 'Decision stored callback failed')
225
+ }
226
+ }
227
+
228
+ return id
229
+
230
+ } catch (error) {
231
+ this.logger.error({ error, input }, 'Failed to store decision')
232
+ throw error
233
+ }
234
+ }
235
+
236
+ async storePattern(input: {
237
+ id?: string
238
+ project: string
239
+ pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
240
+ description: string
241
+ example?: string
242
+ confidence: number
243
+ context?: string
244
+ source?: string
245
+ }): Promise<string> {
246
+ const id = input.id || randomUUID()
247
+ const now = new Date().toISOString()
248
+
249
+ const metadata: Record<string, unknown> = {
250
+ project: input.project,
251
+ pattern_type: input.pattern_type,
252
+ description: input.description,
253
+ example: input.example || '',
254
+ confidence: input.confidence,
255
+ context: input.context || '',
256
+ created_at: now,
257
+ updated_at: now,
258
+ source: input.source || 'manual'
259
+ }
260
+
261
+ try {
262
+ const collection = await this.collections.getPatterns()
263
+
264
+ const embeddings = this.embeddings
265
+ ? [await this.embeddings.generate(input.description)]
266
+ : undefined
267
+
268
+ await collection.add({
269
+ ids: [id],
270
+ documents: [input.description],
271
+ metadatas: [sanitizeMetadata(metadata)],
272
+ ...(embeddings ? { embeddings } : {})
273
+ })
274
+
275
+ // ALSO store in memories collection for unified semantic search
276
+ try {
277
+ const memoriesCollection = await this.collections.getMemories()
278
+ const memoryContent = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
279
+ const memoryMetadata = {
280
+ project: input.project,
281
+ type: 'pattern',
282
+ source: 'recognize_pattern',
283
+ confidence: input.confidence,
284
+ created_at: now,
285
+ updated_at: now,
286
+ pattern_id: id,
287
+ pattern_type: input.pattern_type
288
+ }
289
+
290
+ await memoriesCollection.add({
291
+ ids: [id],
292
+ documents: [memoryContent],
293
+ metadatas: [sanitizeMetadata(memoryMetadata)],
294
+ ...(embeddings ? { embeddings } : {})
295
+ })
296
+
297
+ this.logger.debug({ id }, 'Pattern also stored in memories collection')
298
+ } catch (memError) {
299
+ this.logger.warn({ error: memError, id }, 'Failed to store pattern in memories collection')
300
+ }
301
+
302
+ this.logger.info({ id, pattern_type: input.pattern_type }, 'Pattern stored in ChromaDB')
303
+
304
+ return id
305
+
306
+ } catch (error) {
307
+ this.logger.error({ error, input }, 'Failed to store pattern')
308
+ throw error
309
+ }
310
+ }
311
+
312
+ async storeCorrection(input: {
313
+ id?: string
314
+ project: string
315
+ original: string
316
+ correction: string
317
+ reasoning: string
318
+ context?: string
319
+ confidence: number
320
+ }): Promise<string> {
321
+ const id = input.id || randomUUID()
322
+ const now = new Date().toISOString()
323
+
324
+ const metadata: Record<string, unknown> = {
325
+ project: input.project,
326
+ original: input.original,
327
+ correction: input.correction,
328
+ reasoning: input.reasoning,
329
+ context: input.context || '',
330
+ confidence: input.confidence,
331
+ created_at: now,
332
+ updated_at: now,
333
+ source: 'manual'
334
+ }
335
+
336
+ try {
337
+ const collection = await this.collections.getCorrections()
338
+
339
+ const embeddings = this.embeddings
340
+ ? [await this.embeddings.generate(input.correction)]
341
+ : undefined
342
+
343
+ await collection.add({
344
+ ids: [id],
345
+ documents: [input.correction],
346
+ metadatas: [sanitizeMetadata(metadata)],
347
+ ...(embeddings ? { embeddings } : {})
348
+ })
349
+
350
+ // ALSO store in memories collection for unified semantic search
351
+ try {
352
+ const memoriesCollection = await this.collections.getMemories()
353
+ const memoryContent = `Correction: ${input.correction}\nOriginal: ${input.original}\nReasoning: ${input.reasoning}${input.context ? `\nContext: ${input.context}` : ''}`
354
+ const memoryMetadata = {
355
+ project: input.project,
356
+ type: 'correction',
357
+ source: 'record_correction',
358
+ confidence: input.confidence,
359
+ created_at: now,
360
+ updated_at: now,
361
+ correction_id: id
362
+ }
363
+
364
+ await memoriesCollection.add({
365
+ ids: [id],
366
+ documents: [memoryContent],
367
+ metadatas: [sanitizeMetadata(memoryMetadata)],
368
+ ...(embeddings ? { embeddings } : {})
369
+ })
370
+
371
+ this.logger.debug({ id }, 'Correction also stored in memories collection')
372
+ } catch (memError) {
373
+ this.logger.warn({ error: memError, id }, 'Failed to store correction in memories collection')
374
+ }
375
+
376
+ this.logger.info({ id, project: input.project }, 'Correction stored in ChromaDB')
377
+
378
+ return id
379
+
380
+ } catch (error) {
381
+ this.logger.error({ error, input }, 'Failed to store correction')
382
+ throw error
383
+ }
384
+ }
385
+
386
+ async storeMemory(input: StoreMemoryInput): Promise<string> {
387
+ const id = randomUUID()
388
+ const now = new Date().toISOString()
389
+
390
+ const metadata: MemoryMetadata = {
391
+ project: input.project,
392
+ type: input.type || 'general',
393
+ source: input.source || 'unknown',
394
+ confidence: input.confidence || 1.0,
395
+ created_at: now,
396
+ updated_at: now
397
+ }
398
+
399
+ try {
400
+ const collection = await this.collections.getMemories()
401
+
402
+ const embeddings = this.embeddings
403
+ ? [await this.embeddings.generate(input.content)]
404
+ : undefined
405
+
406
+ await collection.add({
407
+ ids: [id],
408
+ documents: [input.content],
409
+ metadatas: [sanitizeMetadata(metadata as Record<string, unknown>)],
410
+ ...(embeddings ? { embeddings } : {})
411
+ })
412
+
413
+ this.logger.info(
414
+ { id, project: input.project, type: input.type },
415
+ 'Memory stored in ChromaDB'
416
+ )
417
+
418
+ return id
419
+
420
+ } catch (error) {
421
+ this.logger.error({ error, input }, 'Failed to store memory')
422
+ throw error
423
+ }
424
+ }
425
+
426
+ async upsertDecision(id: string, input: StoreDecisionInput): Promise<void> {
427
+ const now = new Date().toISOString()
428
+
429
+ const metadata: Record<string, unknown> = {
430
+ project: input.project,
431
+ context: input.context,
432
+ reasoning: input.reasoning,
433
+ alternatives: input.alternatives || '',
434
+ outcome: input.outcome || '',
435
+ tags: (input.tags || []).join(','),
436
+ created_at: now,
437
+ updated_at: now,
438
+ source: 'manual'
439
+ }
440
+
441
+ try {
442
+ const collection = await this.collections.getDecisions()
443
+
444
+ const embeddings = this.embeddings
445
+ ? [await this.embeddings.generate(input.decision)]
446
+ : undefined
447
+
448
+ await collection.upsert({
449
+ ids: [id],
450
+ documents: [input.decision],
451
+ metadatas: [sanitizeMetadata(metadata as Record<string, unknown>)],
452
+ ...(embeddings ? { embeddings } : {})
453
+ })
454
+
455
+ this.logger.info({ id }, 'Decision upserted')
456
+
457
+ } catch (error) {
458
+ this.logger.error({ error, id, errorMessage: error instanceof Error ? error.message : String(error) }, 'Failed to upsert decision')
459
+ throw error
460
+ }
461
+ }
462
+
463
+ async getDecision(id: string): Promise<StoredDecision | null> {
464
+ try {
465
+ const collection = await this.collections.getDecisions()
466
+
467
+ const result = await collection.get({
468
+ ids: [id],
469
+ include: ['documents', 'metadatas']
470
+ })
471
+
472
+ if (result.ids.length === 0) {
473
+ return null
474
+ }
475
+
476
+ return {
477
+ id: result.ids[0]!,
478
+ decision: result.documents![0] as string,
479
+ metadata: result.metadatas![0] as Record<string, unknown>
480
+ }
481
+
482
+ } catch (error) {
483
+ this.logger.error({ error, id }, 'Failed to get decision')
484
+ throw error
485
+ }
486
+ }
487
+
488
+ async getDecisionsByProject(project: string): Promise<StoredDecision[]> {
489
+ try {
490
+ const collection = await this.collections.getDecisions()
491
+
492
+ const result = await collection.get({
493
+ where: { project },
494
+ include: ['documents', 'metadatas']
495
+ })
496
+
497
+ return result.ids.map((id, i) => ({
498
+ id,
499
+ decision: result.documents![i] as string,
500
+ metadata: result.metadatas![i] as Record<string, unknown>
501
+ }))
502
+
503
+ } catch (error) {
504
+ this.logger.error({ error, project }, 'Failed to get decisions by project')
505
+ throw error
506
+ }
507
+ }
508
+
509
+ async deleteDecision(id: string): Promise<void> {
510
+ try {
511
+ // Delete from decisions collection
512
+ const collection = await this.collections.getDecisions()
513
+ await collection.delete({ ids: [id] })
514
+
515
+ // Verify deletion succeeded
516
+ try {
517
+ const verify = await collection.get({ ids: [id] })
518
+ if (verify.ids.length > 0) {
519
+ this.logger.warn({ id }, 'Decision still exists after delete — retrying')
520
+ await collection.delete({ ids: [id] })
521
+ }
522
+ } catch {
523
+ // Verification query failed, assume delete succeeded
524
+ }
525
+
526
+ // ALSO delete from memories collection (dual storage uses same ID)
527
+ try {
528
+ const memoriesCollection = await this.collections.getMemories()
529
+ await memoriesCollection.delete({ ids: [id] })
530
+ this.logger.debug({ id }, 'Decision also deleted from memories collection')
531
+ } catch {
532
+ // Memories collection entry may not exist, that's ok
533
+ }
534
+
535
+ this.logger.info({ id }, 'Decision deleted from all collections')
536
+
537
+ } catch (error) {
538
+ this.logger.error({ error, id }, 'Failed to delete decision')
539
+ throw error
540
+ }
541
+ }
542
+
543
+ async getAllDecisions(): Promise<StoredDecision[]> {
544
+ try {
545
+ const collection = await this.collections.getDecisions()
546
+
547
+ const result = await collection.get({
548
+ include: ['documents', 'metadatas']
549
+ })
550
+
551
+ return result.ids.map((id, i) => ({
552
+ id,
553
+ decision: result.documents![i] as string,
554
+ metadata: result.metadatas![i] as Record<string, unknown>
555
+ }))
556
+
557
+ } catch (error) {
558
+ this.logger.error({ error }, 'Failed to get all decisions')
559
+ throw error
560
+ }
561
+ }
562
+
563
+ async getPatternsByProject(project: string, options?: {
564
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
565
+ limit?: number
566
+ }): Promise<{ id: string; description: string; metadata: Record<string, unknown> }[]> {
567
+ try {
568
+ const collection = await this.collections.getPatterns()
569
+
570
+ // Build where clause - handle single vs multiple conditions
571
+ let where: Record<string, unknown>
572
+ if (options?.pattern_type) {
573
+ // Multiple conditions: use $and operator
574
+ where = {
575
+ $and: [
576
+ { project: { $eq: project } },
577
+ { pattern_type: { $eq: options.pattern_type } }
578
+ ]
579
+ }
580
+ } else {
581
+ // Single condition
582
+ where = { project: { $eq: project } }
583
+ }
584
+
585
+ const result = await collection.get({
586
+ where,
587
+ include: ['documents', 'metadatas'],
588
+ ...(options?.limit ? { limit: options.limit } : {})
589
+ })
590
+
591
+ return result.ids.map((id, i) => ({
592
+ id,
593
+ description: result.documents![i] as string,
594
+ metadata: result.metadatas![i] as Record<string, unknown>
595
+ }))
596
+
597
+ } catch (error) {
598
+ this.logger.error({ error, project }, 'Failed to get patterns by project')
599
+ throw error
600
+ }
601
+ }
602
+
603
+ async getCorrectionsByProject(project: string, limit: number = 10): Promise<{ id: string; correction: string; metadata: Record<string, unknown> }[]> {
604
+ try {
605
+ const collection = await this.collections.getCorrections()
606
+
607
+ const result = await collection.get({
608
+ where: { project },
609
+ include: ['documents', 'metadatas'],
610
+ limit
611
+ })
612
+
613
+ return result.ids.map((id, i) => ({
614
+ id,
615
+ correction: result.documents![i] as string,
616
+ metadata: result.metadatas![i] as Record<string, unknown>
617
+ }))
618
+
619
+ } catch (error) {
620
+ this.logger.error({ error, project }, 'Failed to get corrections by project')
621
+ throw error
622
+ }
623
+ }
624
+
625
+ async searchPatterns(
626
+ query: string,
627
+ options: {
628
+ project?: string
629
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
630
+ limit?: number
631
+ minSimilarity?: number
632
+ } = {}
633
+ ): Promise<SearchResult[]> {
634
+ const {
635
+ project,
636
+ pattern_type,
637
+ limit = 10,
638
+ minSimilarity = 0.5
639
+ } = options
640
+
641
+ try {
642
+ const collection = await this.collections.getPatterns()
643
+
644
+ const where: Record<string, unknown> = {}
645
+ if (project) {
646
+ where.project = project
647
+ }
648
+ if (pattern_type) {
649
+ where.pattern_type = pattern_type
650
+ }
651
+
652
+ let results: ChromaQueryResult
653
+
654
+ if (this.embeddings) {
655
+ const embedding = await this.embeddings.generate(query)
656
+ results = await collection.query({
657
+ queryEmbeddings: [embedding],
658
+ nResults: limit,
659
+ where: Object.keys(where).length > 0 ? where : undefined,
660
+ include: ['documents', 'metadatas', 'distances']
661
+ })
662
+ } else {
663
+ results = await collection.query({
664
+ queryTexts: [query],
665
+ nResults: limit,
666
+ where: Object.keys(where).length > 0 ? where : undefined,
667
+ include: ['documents', 'metadatas', 'distances']
668
+ })
669
+ }
670
+
671
+ return this.processResults(results, minSimilarity)
672
+
673
+ } catch (error) {
674
+ this.logger.error({ error, query }, 'Pattern search failed')
675
+ throw error
676
+ }
677
+ }
678
+
679
+ async searchCorrections(
680
+ query: string,
681
+ options: {
682
+ project?: string
683
+ limit?: number
684
+ minSimilarity?: number
685
+ } = {}
686
+ ): Promise<SearchResult[]> {
687
+ const {
688
+ project,
689
+ limit = 10,
690
+ minSimilarity = 0.5
691
+ } = options
692
+
693
+ try {
694
+ const collection = await this.collections.getCorrections()
695
+
696
+ const where: Record<string, unknown> = project ? { project } : {}
697
+
698
+ let results: ChromaQueryResult
699
+
700
+ if (this.embeddings) {
701
+ const embedding = await this.embeddings.generate(query)
702
+ results = await collection.query({
703
+ queryEmbeddings: [embedding],
704
+ nResults: limit,
705
+ where: Object.keys(where).length > 0 ? where : undefined,
706
+ include: ['documents', 'metadatas', 'distances']
707
+ })
708
+ } else {
709
+ results = await collection.query({
710
+ queryTexts: [query],
711
+ nResults: limit,
712
+ where: Object.keys(where).length > 0 ? where : undefined,
713
+ include: ['documents', 'metadatas', 'distances']
714
+ })
715
+ }
716
+
717
+ return this.processResults(results, minSimilarity)
718
+
719
+ } catch (error) {
720
+ this.logger.error({ error, query }, 'Correction search failed')
721
+ throw error
722
+ }
723
+ }
724
+
725
+ private processResults(
726
+ results: ChromaQueryResult,
727
+ minSimilarity: number
728
+ ): SearchResult[] {
729
+ if (!results.ids || results.ids.length === 0) {
730
+ return []
731
+ }
732
+
733
+ const processed: SearchResult[] = []
734
+
735
+ const ids = results.ids[0] || []
736
+ const documents = results.documents?.[0] || []
737
+ const metadatas = results.metadatas?.[0] || []
738
+ const distances = results.distances?.[0] || []
739
+
740
+ for (let i = 0; i < ids.length; i++) {
741
+ const similarity = 1 - (distances[i] || 0)
742
+
743
+ if (similarity >= minSimilarity) {
744
+ processed.push({
745
+ id: ids[i],
746
+ content: documents[i],
747
+ metadata: metadatas[i],
748
+ similarity
749
+ })
750
+ }
751
+ }
752
+
753
+ return processed.sort((a, b) => b.similarity - a.similarity)
754
+ }
755
+ }