claude-brain 0.3.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 (200) hide show
  1. package/README.md +157 -0
  2. package/VERSION +1 -0
  3. package/assets/CLAUDE.md +307 -0
  4. package/bunfig.toml +8 -0
  5. package/package.json +74 -0
  6. package/src/automation/auto-context.ts +240 -0
  7. package/src/automation/decision-detector.ts +452 -0
  8. package/src/automation/index.ts +11 -0
  9. package/src/automation/proactive-recall.ts +373 -0
  10. package/src/automation/project-detector.ts +297 -0
  11. package/src/cli/auto-setup.ts +74 -0
  12. package/src/cli/bin.ts +110 -0
  13. package/src/cli/commands/install-mcp.ts +50 -0
  14. package/src/cli/commands/serve.ts +129 -0
  15. package/src/cli/diagnose.ts +4 -0
  16. package/src/cli/health-check.ts +4 -0
  17. package/src/cli/migrate-chroma.ts +106 -0
  18. package/src/cli/setup.ts +4 -0
  19. package/src/config/defaults.ts +47 -0
  20. package/src/config/home.ts +55 -0
  21. package/src/config/index.ts +7 -0
  22. package/src/config/loader.ts +166 -0
  23. package/src/config/migration.ts +76 -0
  24. package/src/config/schema.ts +257 -0
  25. package/src/config/validator.ts +184 -0
  26. package/src/config/watcher.ts +86 -0
  27. package/src/context/assembler.ts +398 -0
  28. package/src/context/cache-manager.ts +101 -0
  29. package/src/context/formatter.ts +84 -0
  30. package/src/context/hierarchy.ts +85 -0
  31. package/src/context/index.ts +83 -0
  32. package/src/context/progress-tracker.ts +174 -0
  33. package/src/context/standards-manager.ts +267 -0
  34. package/src/context/types.ts +252 -0
  35. package/src/context/validator.ts +58 -0
  36. package/src/cross-project/affinity.ts +162 -0
  37. package/src/cross-project/generalizer.ts +283 -0
  38. package/src/cross-project/index.ts +13 -0
  39. package/src/cross-project/transfer.ts +201 -0
  40. package/src/diagnostics/index.ts +123 -0
  41. package/src/health/index.ts +229 -0
  42. package/src/index.ts +7 -0
  43. package/src/knowledge/entity-extractor.ts +416 -0
  44. package/src/knowledge/graph/builder.ts +159 -0
  45. package/src/knowledge/graph/linker.ts +201 -0
  46. package/src/knowledge/graph/memory-graph.ts +359 -0
  47. package/src/knowledge/graph/schema.ts +99 -0
  48. package/src/knowledge/graph/search.ts +168 -0
  49. package/src/knowledge/relationship-extractor.ts +108 -0
  50. package/src/memory/chroma/client.ts +169 -0
  51. package/src/memory/chroma/collection-manager.ts +94 -0
  52. package/src/memory/chroma/config.ts +46 -0
  53. package/src/memory/chroma/embeddings.ts +153 -0
  54. package/src/memory/chroma/index.ts +82 -0
  55. package/src/memory/chroma/migration.ts +270 -0
  56. package/src/memory/chroma/schemas.ts +69 -0
  57. package/src/memory/chroma/search.ts +315 -0
  58. package/src/memory/chroma/store.ts +694 -0
  59. package/src/memory/consolidation/archiver.ts +164 -0
  60. package/src/memory/consolidation/merger.ts +186 -0
  61. package/src/memory/consolidation/scorer.ts +138 -0
  62. package/src/memory/context-builder.ts +236 -0
  63. package/src/memory/database.ts +169 -0
  64. package/src/memory/embedding-utils.ts +156 -0
  65. package/src/memory/embeddings.ts +226 -0
  66. package/src/memory/episodic/detector.ts +108 -0
  67. package/src/memory/episodic/manager.ts +334 -0
  68. package/src/memory/episodic/summarizer.ts +179 -0
  69. package/src/memory/episodic/types.ts +52 -0
  70. package/src/memory/index.ts +395 -0
  71. package/src/memory/knowledge-extractor.ts +455 -0
  72. package/src/memory/learning.ts +378 -0
  73. package/src/memory/patterns.ts +396 -0
  74. package/src/memory/schema.ts +56 -0
  75. package/src/memory/search.ts +309 -0
  76. package/src/memory/store.ts +344 -0
  77. package/src/memory/types.ts +121 -0
  78. package/src/optimization/index.ts +10 -0
  79. package/src/optimization/precompute.ts +202 -0
  80. package/src/optimization/semantic-cache.ts +207 -0
  81. package/src/orchestrator/coordinator.ts +272 -0
  82. package/src/orchestrator/decision-logger.ts +228 -0
  83. package/src/orchestrator/event-emitter.ts +198 -0
  84. package/src/orchestrator/event-queue.ts +184 -0
  85. package/src/orchestrator/handlers/base-handler.ts +70 -0
  86. package/src/orchestrator/handlers/context-handler.ts +73 -0
  87. package/src/orchestrator/handlers/decision-handler.ts +204 -0
  88. package/src/orchestrator/handlers/index.ts +10 -0
  89. package/src/orchestrator/handlers/status-handler.ts +131 -0
  90. package/src/orchestrator/handlers/task-handler.ts +171 -0
  91. package/src/orchestrator/index.ts +275 -0
  92. package/src/orchestrator/task-parser.ts +284 -0
  93. package/src/orchestrator/types.ts +98 -0
  94. package/src/phase12/index.ts +456 -0
  95. package/src/prediction/context-anticipator.ts +198 -0
  96. package/src/prediction/decision-predictor.ts +184 -0
  97. package/src/prediction/index.ts +13 -0
  98. package/src/prediction/recommender.ts +268 -0
  99. package/src/reasoning/chain-retrieval.ts +247 -0
  100. package/src/reasoning/counterfactual.ts +248 -0
  101. package/src/reasoning/index.ts +13 -0
  102. package/src/reasoning/synthesizer.ts +169 -0
  103. package/src/retrieval/bm25/index.ts +300 -0
  104. package/src/retrieval/bm25/tokenizer.ts +184 -0
  105. package/src/retrieval/feedback/adaptive.ts +223 -0
  106. package/src/retrieval/feedback/index.ts +16 -0
  107. package/src/retrieval/feedback/metrics.ts +223 -0
  108. package/src/retrieval/feedback/store.ts +283 -0
  109. package/src/retrieval/fusion/index.ts +194 -0
  110. package/src/retrieval/fusion/rrf.ts +163 -0
  111. package/src/retrieval/index.ts +12 -0
  112. package/src/retrieval/pipeline.ts +375 -0
  113. package/src/retrieval/query/expander.ts +198 -0
  114. package/src/retrieval/query/index.ts +27 -0
  115. package/src/retrieval/query/intent-classifier.ts +236 -0
  116. package/src/retrieval/query/temporal-parser.ts +295 -0
  117. package/src/retrieval/reranker/index.ts +188 -0
  118. package/src/retrieval/reranker/model.ts +95 -0
  119. package/src/retrieval/service.ts +125 -0
  120. package/src/retrieval/types.ts +162 -0
  121. package/src/scripts/health-check.ts +118 -0
  122. package/src/scripts/setup.ts +122 -0
  123. package/src/server/handlers/call-tool.ts +194 -0
  124. package/src/server/handlers/index.ts +9 -0
  125. package/src/server/handlers/list-tools.ts +18 -0
  126. package/src/server/handlers/tools/analyze-decision-evolution.ts +71 -0
  127. package/src/server/handlers/tools/auto-remember.ts +200 -0
  128. package/src/server/handlers/tools/create-project.ts +135 -0
  129. package/src/server/handlers/tools/detect-trends.ts +80 -0
  130. package/src/server/handlers/tools/find-cross-project-patterns.ts +73 -0
  131. package/src/server/handlers/tools/get-activity-log.ts +194 -0
  132. package/src/server/handlers/tools/get-code-standards.ts +124 -0
  133. package/src/server/handlers/tools/get-corrections.ts +154 -0
  134. package/src/server/handlers/tools/get-decision-timeline.ts +86 -0
  135. package/src/server/handlers/tools/get-episode.ts +93 -0
  136. package/src/server/handlers/tools/get-patterns.ts +158 -0
  137. package/src/server/handlers/tools/get-phase12-status.ts +63 -0
  138. package/src/server/handlers/tools/get-project-context.ts +75 -0
  139. package/src/server/handlers/tools/get-recommendations.ts +65 -0
  140. package/src/server/handlers/tools/index.ts +33 -0
  141. package/src/server/handlers/tools/init-project.ts +710 -0
  142. package/src/server/handlers/tools/list-episodes.ts +80 -0
  143. package/src/server/handlers/tools/list-projects.ts +125 -0
  144. package/src/server/handlers/tools/rate-memory.ts +95 -0
  145. package/src/server/handlers/tools/recall-similar.ts +87 -0
  146. package/src/server/handlers/tools/recognize-pattern.ts +126 -0
  147. package/src/server/handlers/tools/record-correction.ts +125 -0
  148. package/src/server/handlers/tools/remember-decision.ts +153 -0
  149. package/src/server/handlers/tools/schemas.ts +241 -0
  150. package/src/server/handlers/tools/search-knowledge-graph.ts +89 -0
  151. package/src/server/handlers/tools/smart-context.ts +124 -0
  152. package/src/server/handlers/tools/update-progress.ts +114 -0
  153. package/src/server/handlers/tools/what-if-analysis.ts +73 -0
  154. package/src/server/http-api.ts +474 -0
  155. package/src/server/index.ts +40 -0
  156. package/src/server/mcp-server.ts +283 -0
  157. package/src/server/providers/index.ts +7 -0
  158. package/src/server/providers/prompts.ts +327 -0
  159. package/src/server/providers/resources.ts +427 -0
  160. package/src/server/services.ts +388 -0
  161. package/src/server/types.ts +39 -0
  162. package/src/server/utils/error-handler.ts +155 -0
  163. package/src/server/utils/index.ts +13 -0
  164. package/src/server/utils/memory-indicator.ts +83 -0
  165. package/src/server/utils/request-context.ts +122 -0
  166. package/src/server/utils/response-formatter.ts +124 -0
  167. package/src/server/utils/validators.ts +210 -0
  168. package/src/setup/index.ts +22 -0
  169. package/src/setup/wizard.ts +321 -0
  170. package/src/temporal/evolution.ts +197 -0
  171. package/src/temporal/index.ts +16 -0
  172. package/src/temporal/query-processor.ts +190 -0
  173. package/src/temporal/timeline.ts +259 -0
  174. package/src/temporal/trends.ts +263 -0
  175. package/src/tools/index.ts +24 -0
  176. package/src/tools/registry.ts +106 -0
  177. package/src/tools/schemas.test.ts +30 -0
  178. package/src/tools/schemas.ts +907 -0
  179. package/src/tools/types.ts +412 -0
  180. package/src/utils/circuit-breaker.ts +130 -0
  181. package/src/utils/cleanup.ts +34 -0
  182. package/src/utils/error-handler.ts +132 -0
  183. package/src/utils/error-messages.ts +60 -0
  184. package/src/utils/fallback.ts +45 -0
  185. package/src/utils/index.ts +54 -0
  186. package/src/utils/logger-utils.ts +80 -0
  187. package/src/utils/logger.ts +88 -0
  188. package/src/utils/phase12-helper.ts +56 -0
  189. package/src/utils/retry.ts +94 -0
  190. package/src/utils/transaction.ts +63 -0
  191. package/src/vault/frontmatter.ts +264 -0
  192. package/src/vault/index.ts +318 -0
  193. package/src/vault/paths.ts +106 -0
  194. package/src/vault/query.ts +422 -0
  195. package/src/vault/reader.ts +264 -0
  196. package/src/vault/templates.ts +186 -0
  197. package/src/vault/types.ts +73 -0
  198. package/src/vault/watcher.ts +277 -0
  199. package/src/vault/writer.ts +393 -0
  200. package/tsconfig.json +30 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Feedback Module Exports
3
+ */
4
+
5
+ export { FeedbackStore } from './store'
6
+ export {
7
+ FeedbackMetrics,
8
+ calculateMRR,
9
+ calculatePrecisionAtK,
10
+ calculateNDCG,
11
+ calculateAllMetrics,
12
+ calculateAverageRating,
13
+ calculatePositiveRate,
14
+ calculateUsageRate
15
+ } from './metrics'
16
+ export { AdaptiveLearner, type AdaptiveLearnerConfig } from './adaptive'
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Retrieval Metrics
3
+ * Evaluation metrics for retrieval quality: MRR, Precision@K, NDCG
4
+ */
5
+
6
+ import type { MemoryFeedback, RetrievalMetrics } from '../types'
7
+
8
+ /**
9
+ * Calculate Mean Reciprocal Rank (MRR)
10
+ *
11
+ * MRR measures where the first relevant result appears in the ranking.
12
+ * MRR = (1/|Q|) * sum(1/rank_i) for each query
13
+ *
14
+ * @param feedbackByQuery - Feedback grouped by query
15
+ * @param relevanceThreshold - Minimum rating to consider relevant (default: 4)
16
+ */
17
+ export function calculateMRR(
18
+ feedbackByQuery: Map<string, MemoryFeedback[]>,
19
+ relevanceThreshold: number = 4
20
+ ): number {
21
+ if (feedbackByQuery.size === 0) return 0
22
+
23
+ let sumReciprocal = 0
24
+ let queryCount = 0
25
+
26
+ for (const [, feedbacks] of feedbackByQuery) {
27
+ // Sort by implicit rank (assuming feedback order matches retrieval order)
28
+ const sorted = [...feedbacks]
29
+
30
+ // Find first relevant result
31
+ let foundRelevant = false
32
+ for (let rank = 0; rank < sorted.length; rank++) {
33
+ if (sorted[rank].rating >= relevanceThreshold) {
34
+ sumReciprocal += 1 / (rank + 1)
35
+ foundRelevant = true
36
+ break
37
+ }
38
+ }
39
+
40
+ if (sorted.length > 0) {
41
+ queryCount++
42
+ }
43
+ }
44
+
45
+ return queryCount > 0 ? sumReciprocal / queryCount : 0
46
+ }
47
+
48
+ /**
49
+ * Calculate Precision at K
50
+ *
51
+ * Precision@K = (# relevant in top K) / K
52
+ *
53
+ * @param feedbackByQuery - Feedback grouped by query
54
+ * @param k - Number of top results to consider
55
+ * @param relevanceThreshold - Minimum rating to consider relevant
56
+ */
57
+ export function calculatePrecisionAtK(
58
+ feedbackByQuery: Map<string, MemoryFeedback[]>,
59
+ k: number,
60
+ relevanceThreshold: number = 4
61
+ ): number {
62
+ if (feedbackByQuery.size === 0) return 0
63
+
64
+ let sumPrecision = 0
65
+ let queryCount = 0
66
+
67
+ for (const [, feedbacks] of feedbackByQuery) {
68
+ const topK = feedbacks.slice(0, k)
69
+ if (topK.length === 0) continue
70
+
71
+ const relevantCount = topK.filter(f => f.rating >= relevanceThreshold).length
72
+ sumPrecision += relevantCount / k
73
+
74
+ queryCount++
75
+ }
76
+
77
+ return queryCount > 0 ? sumPrecision / queryCount : 0
78
+ }
79
+
80
+ /**
81
+ * Calculate Normalized Discounted Cumulative Gain (NDCG)
82
+ *
83
+ * NDCG measures ranking quality, giving higher weight to relevant results at the top.
84
+ * DCG = sum(rating_i / log2(i + 1))
85
+ * NDCG = DCG / IDCG (ideal DCG)
86
+ *
87
+ * @param feedbackByQuery - Feedback grouped by query
88
+ * @param k - Number of top results to consider (optional, default all)
89
+ */
90
+ export function calculateNDCG(
91
+ feedbackByQuery: Map<string, MemoryFeedback[]>,
92
+ k?: number
93
+ ): number {
94
+ if (feedbackByQuery.size === 0) return 0
95
+
96
+ let sumNDCG = 0
97
+ let queryCount = 0
98
+
99
+ for (const [, feedbacks] of feedbackByQuery) {
100
+ if (feedbacks.length === 0) continue
101
+
102
+ const results = k ? feedbacks.slice(0, k) : feedbacks
103
+
104
+ // Calculate DCG
105
+ let dcg = 0
106
+ for (let i = 0; i < results.length; i++) {
107
+ // Use rating as relevance score (1-5)
108
+ const relevance = results[i].rating
109
+ dcg += relevance / Math.log2(i + 2) // +2 because log2(1) = 0
110
+ }
111
+
112
+ // Calculate ideal DCG (sort by rating descending)
113
+ const idealResults = [...results].sort((a, b) => b.rating - a.rating)
114
+ let idcg = 0
115
+ for (let i = 0; i < idealResults.length; i++) {
116
+ const relevance = idealResults[i].rating
117
+ idcg += relevance / Math.log2(i + 2)
118
+ }
119
+
120
+ // Normalize
121
+ if (idcg > 0) {
122
+ sumNDCG += dcg / idcg
123
+ }
124
+
125
+ queryCount++
126
+ }
127
+
128
+ return queryCount > 0 ? sumNDCG / queryCount : 0
129
+ }
130
+
131
+ /**
132
+ * Calculate all retrieval metrics from feedback
133
+ */
134
+ export function calculateAllMetrics(
135
+ feedback: MemoryFeedback[],
136
+ kValues: number[] = [3, 5, 10]
137
+ ): RetrievalMetrics {
138
+ // Group feedback by query
139
+ const feedbackByQuery = new Map<string, MemoryFeedback[]>()
140
+
141
+ for (const f of feedback) {
142
+ const existing = feedbackByQuery.get(f.queryId) || []
143
+ existing.push(f)
144
+ feedbackByQuery.set(f.queryId, existing)
145
+ }
146
+
147
+ // Calculate precision at each K
148
+ const precisionAtK: Record<number, number> = {}
149
+ for (const k of kValues) {
150
+ precisionAtK[k] = calculatePrecisionAtK(feedbackByQuery, k)
151
+ }
152
+
153
+ return {
154
+ mrr: calculateMRR(feedbackByQuery),
155
+ precisionAtK,
156
+ ndcg: calculateNDCG(feedbackByQuery),
157
+ queryCount: feedbackByQuery.size,
158
+ avgLatencyMs: 0 // Not tracked in feedback
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Calculate average rating from feedback
164
+ */
165
+ export function calculateAverageRating(feedback: MemoryFeedback[]): number {
166
+ if (feedback.length === 0) return 0
167
+ const sum = feedback.reduce((acc, f) => acc + f.rating, 0)
168
+ return sum / feedback.length
169
+ }
170
+
171
+ /**
172
+ * Calculate positive feedback rate (rating >= 4)
173
+ */
174
+ export function calculatePositiveRate(
175
+ feedback: MemoryFeedback[],
176
+ threshold: number = 4
177
+ ): number {
178
+ if (feedback.length === 0) return 0
179
+ const positive = feedback.filter(f => f.rating >= threshold).length
180
+ return positive / feedback.length
181
+ }
182
+
183
+ /**
184
+ * Calculate usage rate (was_used = true)
185
+ */
186
+ export function calculateUsageRate(feedback: MemoryFeedback[]): number {
187
+ if (feedback.length === 0) return 0
188
+ const used = feedback.filter(f => f.wasUsed).length
189
+ return used / feedback.length
190
+ }
191
+
192
+ export class FeedbackMetrics {
193
+ calculateMRR(feedback: Map<string, MemoryFeedback[]>): number {
194
+ return calculateMRR(feedback)
195
+ }
196
+
197
+ calculatePrecisionAtK(
198
+ feedback: Map<string, MemoryFeedback[]>,
199
+ k: number
200
+ ): number {
201
+ return calculatePrecisionAtK(feedback, k)
202
+ }
203
+
204
+ calculateNDCG(feedback: Map<string, MemoryFeedback[]>): number {
205
+ return calculateNDCG(feedback)
206
+ }
207
+
208
+ calculateAll(feedback: MemoryFeedback[]): RetrievalMetrics {
209
+ return calculateAllMetrics(feedback)
210
+ }
211
+
212
+ calculateAverageRating(feedback: MemoryFeedback[]): number {
213
+ return calculateAverageRating(feedback)
214
+ }
215
+
216
+ calculatePositiveRate(feedback: MemoryFeedback[]): number {
217
+ return calculatePositiveRate(feedback)
218
+ }
219
+
220
+ calculateUsageRate(feedback: MemoryFeedback[]): number {
221
+ return calculateUsageRate(feedback)
222
+ }
223
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Feedback Store
3
+ * Stores and retrieves memory feedback for adaptive learning
4
+ */
5
+
6
+ import type { Logger } from 'pino'
7
+ import type { CollectionManager } from '@/memory/chroma/collection-manager'
8
+ import type { EmbeddingProvider } from '@/memory/chroma/embeddings'
9
+ import type { MemoryFeedback, RateMemoryInput } from '../types'
10
+
11
+ export class FeedbackStore {
12
+ private logger: Logger
13
+ private collections: CollectionManager
14
+ private embeddings: EmbeddingProvider
15
+
16
+ constructor(
17
+ logger: Logger,
18
+ collections: CollectionManager,
19
+ embeddings: EmbeddingProvider
20
+ ) {
21
+ this.logger = logger.child({ component: 'feedback-store' })
22
+ this.collections = collections
23
+ this.embeddings = embeddings
24
+ }
25
+
26
+ /**
27
+ * Store feedback for a memory retrieval
28
+ */
29
+ async storeFeedback(input: RateMemoryInput): Promise<string> {
30
+ const feedbackId = `feedback_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
31
+ const queryId = `query_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
32
+ const timestamp = new Date().toISOString()
33
+
34
+ this.logger.debug({
35
+ feedbackId,
36
+ memoryId: input.memory_id,
37
+ rating: input.rating
38
+ }, 'Storing feedback')
39
+
40
+ try {
41
+ const collection = await this.collections.getFeedback()
42
+
43
+ // Generate embedding for the query to enable similarity search
44
+ const embedding = await this.embeddings.generate(input.query)
45
+
46
+ // Determine collection type from memory_id prefix
47
+ const collectionType = this.inferCollectionType(input.memory_id)
48
+
49
+ await collection.add({
50
+ ids: [feedbackId],
51
+ embeddings: [embedding],
52
+ documents: [input.query],
53
+ metadatas: [{
54
+ memory_id: input.memory_id,
55
+ query_id: queryId,
56
+ query: input.query,
57
+ rating: input.rating,
58
+ was_used: input.was_used ?? true,
59
+ feedback_text: input.feedback_text || '',
60
+ project: input.project_name || '',
61
+ created_at: timestamp,
62
+ collection_type: collectionType
63
+ }]
64
+ })
65
+
66
+ this.logger.info({
67
+ feedbackId,
68
+ memoryId: input.memory_id,
69
+ rating: input.rating
70
+ }, 'Feedback stored successfully')
71
+
72
+ return feedbackId
73
+
74
+ } catch (error) {
75
+ this.logger.error({ error, input }, 'Failed to store feedback')
76
+ throw error
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get feedback for a specific memory
82
+ */
83
+ async getFeedbackForMemory(memoryId: string): Promise<MemoryFeedback[]> {
84
+ try {
85
+ const collection = await this.collections.getFeedback()
86
+
87
+ const results = await collection.get({
88
+ where: { memory_id: { $eq: memoryId } },
89
+ include: ['documents', 'metadatas']
90
+ })
91
+
92
+ if (!results.ids.length) {
93
+ return []
94
+ }
95
+
96
+ return results.ids.map((id, index) => ({
97
+ id,
98
+ memoryId: results.metadatas?.[index]?.memory_id as string || '',
99
+ queryId: results.metadatas?.[index]?.query_id as string || '',
100
+ query: results.documents?.[index] || '',
101
+ rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
102
+ wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
103
+ feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
104
+ project: results.metadatas?.[index]?.project as string | undefined,
105
+ timestamp: results.metadatas?.[index]?.created_at as string || '',
106
+ collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
107
+ }))
108
+
109
+ } catch (error) {
110
+ this.logger.error({ error, memoryId }, 'Failed to get feedback for memory')
111
+ return []
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get all feedback for a project
117
+ */
118
+ async getFeedbackForProject(project: string, limit: number = 100): Promise<MemoryFeedback[]> {
119
+ try {
120
+ const collection = await this.collections.getFeedback()
121
+
122
+ const results = await collection.get({
123
+ where: { project: { $eq: project } },
124
+ include: ['documents', 'metadatas'],
125
+ limit
126
+ })
127
+
128
+ if (!results.ids.length) {
129
+ return []
130
+ }
131
+
132
+ return results.ids.map((id, index) => ({
133
+ id,
134
+ memoryId: results.metadatas?.[index]?.memory_id as string || '',
135
+ queryId: results.metadatas?.[index]?.query_id as string || '',
136
+ query: results.documents?.[index] || '',
137
+ rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
138
+ wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
139
+ feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
140
+ project: results.metadatas?.[index]?.project as string | undefined,
141
+ timestamp: results.metadatas?.[index]?.created_at as string || '',
142
+ collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
143
+ }))
144
+
145
+ } catch (error) {
146
+ this.logger.error({ error, project }, 'Failed to get feedback for project')
147
+ return []
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get recent feedback (for adaptive learning)
153
+ */
154
+ async getRecentFeedback(limit: number = 100): Promise<MemoryFeedback[]> {
155
+ try {
156
+ const collection = await this.collections.getFeedback()
157
+
158
+ // Get all feedback - ChromaDB doesn't support ordering, so we'll sort in memory
159
+ const results = await collection.get({
160
+ include: ['documents', 'metadatas'],
161
+ limit: limit * 2 // Get more to allow for sorting
162
+ })
163
+
164
+ if (!results.ids.length) {
165
+ return []
166
+ }
167
+
168
+ const feedback: MemoryFeedback[] = results.ids.map((id, index) => ({
169
+ id,
170
+ memoryId: results.metadatas?.[index]?.memory_id as string || '',
171
+ queryId: results.metadatas?.[index]?.query_id as string || '',
172
+ query: results.documents?.[index] || '',
173
+ rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
174
+ wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
175
+ feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
176
+ project: results.metadatas?.[index]?.project as string | undefined,
177
+ timestamp: results.metadatas?.[index]?.created_at as string || '',
178
+ collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
179
+ }))
180
+
181
+ // Sort by timestamp descending and take limit
182
+ return feedback
183
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
184
+ .slice(0, limit)
185
+
186
+ } catch (error) {
187
+ this.logger.error({ error }, 'Failed to get recent feedback')
188
+ return []
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get feedback statistics
194
+ */
195
+ async getStats(): Promise<{
196
+ totalFeedback: number
197
+ avgRating: number
198
+ positiveRate: number
199
+ usageRate: number
200
+ byCollection: Record<string, { count: number; avgRating: number }>
201
+ }> {
202
+ try {
203
+ const collection = await this.collections.getFeedback()
204
+ const totalCount = await collection.count()
205
+
206
+ if (totalCount === 0) {
207
+ return {
208
+ totalFeedback: 0,
209
+ avgRating: 0,
210
+ positiveRate: 0,
211
+ usageRate: 0,
212
+ byCollection: {}
213
+ }
214
+ }
215
+
216
+ const results = await collection.get({
217
+ include: ['metadatas'],
218
+ limit: 1000 // Sample for stats
219
+ })
220
+
221
+ let totalRating = 0
222
+ let positiveCount = 0
223
+ let usedCount = 0
224
+ const byCollection: Record<string, { count: number; totalRating: number }> = {}
225
+
226
+ for (const metadata of results.metadatas || []) {
227
+ const rating = metadata?.rating as number
228
+ const wasUsed = metadata?.was_used as boolean
229
+ const collectionType = (metadata?.collection_type as string) || 'unknown'
230
+
231
+ totalRating += rating
232
+ if (rating >= 4) positiveCount++
233
+ if (wasUsed) usedCount++
234
+
235
+ if (!byCollection[collectionType]) {
236
+ byCollection[collectionType] = { count: 0, totalRating: 0 }
237
+ }
238
+ byCollection[collectionType].count++
239
+ byCollection[collectionType].totalRating += rating
240
+ }
241
+
242
+ const count = results.ids.length
243
+
244
+ return {
245
+ totalFeedback: totalCount,
246
+ avgRating: count > 0 ? totalRating / count : 0,
247
+ positiveRate: count > 0 ? positiveCount / count : 0,
248
+ usageRate: count > 0 ? usedCount / count : 0,
249
+ byCollection: Object.fromEntries(
250
+ Object.entries(byCollection).map(([key, value]) => [
251
+ key,
252
+ {
253
+ count: value.count,
254
+ avgRating: value.count > 0 ? value.totalRating / value.count : 0
255
+ }
256
+ ])
257
+ )
258
+ }
259
+
260
+ } catch (error) {
261
+ this.logger.error({ error }, 'Failed to get feedback stats')
262
+ return {
263
+ totalFeedback: 0,
264
+ avgRating: 0,
265
+ positiveRate: 0,
266
+ usageRate: 0,
267
+ byCollection: {}
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Infer collection type from memory ID prefix
274
+ */
275
+ private inferCollectionType(memoryId: string): 'decisions' | 'memories' | 'patterns' | 'corrections' {
276
+ if (memoryId.startsWith('dec_')) return 'decisions'
277
+ if (memoryId.startsWith('mem_')) return 'memories'
278
+ if (memoryId.startsWith('pat_')) return 'patterns'
279
+ if (memoryId.startsWith('cor_')) return 'corrections'
280
+ // Default to decisions if unknown
281
+ return 'decisions'
282
+ }
283
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Fusion Strategies
3
+ * Different methods for combining dense and sparse search results
4
+ */
5
+
6
+ import type { HybridSearchResult } from '../types'
7
+ import { fuseWithRRF, fuseMultipleWithRRF, calculateRRFScore } from './rrf'
8
+
9
+ export interface FusionConfig {
10
+ /** Fusion method to use */
11
+ method: 'rrf' | 'linear' | 'max'
12
+ /** RRF k parameter (for RRF method) */
13
+ rrfK?: number
14
+ /** Dense weight (for linear method) */
15
+ denseWeight?: number
16
+ /** Sparse weight (for linear method) */
17
+ sparseWeight?: number
18
+ }
19
+
20
+ const DEFAULT_CONFIG: FusionConfig = {
21
+ method: 'rrf',
22
+ rrfK: 60,
23
+ denseWeight: 0.7,
24
+ sparseWeight: 0.3
25
+ }
26
+
27
+ /**
28
+ * RRF Fusion - Reciprocal Rank Fusion
29
+ */
30
+ export class RRFFusion {
31
+ private k: number
32
+
33
+ constructor(k: number = 60) {
34
+ this.k = k
35
+ }
36
+
37
+ fuse(
38
+ denseResults: HybridSearchResult[],
39
+ sparseResults: HybridSearchResult[]
40
+ ): HybridSearchResult[] {
41
+ return fuseWithRRF(denseResults, sparseResults, { k: this.k })
42
+ }
43
+
44
+ fuseMultiple(rankedLists: HybridSearchResult[][]): HybridSearchResult[] {
45
+ return fuseMultipleWithRRF(rankedLists, { k: this.k })
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Linear Fusion - Weighted combination of scores
51
+ */
52
+ export class LinearFusion {
53
+ private denseWeight: number
54
+ private sparseWeight: number
55
+
56
+ constructor(denseWeight: number = 0.7, sparseWeight: number = 0.3) {
57
+ this.denseWeight = denseWeight
58
+ this.sparseWeight = sparseWeight
59
+ }
60
+
61
+ fuse(
62
+ denseResults: HybridSearchResult[],
63
+ sparseResults: HybridSearchResult[]
64
+ ): HybridSearchResult[] {
65
+ // Create map for merging
66
+ const merged = new Map<string, HybridSearchResult>()
67
+
68
+ // Add dense results
69
+ for (const result of denseResults) {
70
+ merged.set(result.id, {
71
+ ...result,
72
+ scores: {
73
+ ...result.scores,
74
+ fusion: result.scores.dense * this.denseWeight,
75
+ final: result.scores.dense * this.denseWeight
76
+ },
77
+ provenance: 'dense'
78
+ })
79
+ }
80
+
81
+ // Add/merge sparse results
82
+ for (const result of sparseResults) {
83
+ const existing = merged.get(result.id)
84
+ if (existing) {
85
+ const combinedScore =
86
+ existing.scores.fusion + result.scores.sparse * this.sparseWeight
87
+ merged.set(result.id, {
88
+ ...existing,
89
+ scores: {
90
+ ...existing.scores,
91
+ sparse: result.scores.sparse,
92
+ fusion: combinedScore,
93
+ final: combinedScore
94
+ },
95
+ provenance: 'both'
96
+ })
97
+ } else {
98
+ merged.set(result.id, {
99
+ ...result,
100
+ scores: {
101
+ ...result.scores,
102
+ fusion: result.scores.sparse * this.sparseWeight,
103
+ final: result.scores.sparse * this.sparseWeight
104
+ },
105
+ provenance: 'sparse'
106
+ })
107
+ }
108
+ }
109
+
110
+ // Sort by fusion score
111
+ const results = Array.from(merged.values())
112
+ results.sort((a, b) => b.scores.fusion - a.scores.fusion)
113
+
114
+ return results
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Max Fusion - Take maximum score from either list
120
+ */
121
+ export class MaxFusion {
122
+ fuse(
123
+ denseResults: HybridSearchResult[],
124
+ sparseResults: HybridSearchResult[]
125
+ ): HybridSearchResult[] {
126
+ // Create map for merging
127
+ const merged = new Map<string, HybridSearchResult>()
128
+
129
+ // Add dense results
130
+ for (const result of denseResults) {
131
+ merged.set(result.id, {
132
+ ...result,
133
+ scores: {
134
+ ...result.scores,
135
+ fusion: result.scores.dense,
136
+ final: result.scores.dense
137
+ },
138
+ provenance: 'dense'
139
+ })
140
+ }
141
+
142
+ // Add/merge sparse results
143
+ for (const result of sparseResults) {
144
+ const existing = merged.get(result.id)
145
+ if (existing) {
146
+ const maxScore = Math.max(existing.scores.dense || 0, result.scores.sparse)
147
+ merged.set(result.id, {
148
+ ...existing,
149
+ scores: {
150
+ ...existing.scores,
151
+ sparse: result.scores.sparse,
152
+ fusion: maxScore,
153
+ final: maxScore
154
+ },
155
+ provenance: 'both'
156
+ })
157
+ } else {
158
+ merged.set(result.id, {
159
+ ...result,
160
+ scores: {
161
+ ...result.scores,
162
+ fusion: result.scores.sparse,
163
+ final: result.scores.sparse
164
+ },
165
+ provenance: 'sparse'
166
+ })
167
+ }
168
+ }
169
+
170
+ // Sort by fusion score
171
+ const results = Array.from(merged.values())
172
+ results.sort((a, b) => b.scores.fusion - a.scores.fusion)
173
+
174
+ return results
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Create a fusion strategy based on config
180
+ */
181
+ export function createFusionStrategy(config: FusionConfig = DEFAULT_CONFIG) {
182
+ switch (config.method) {
183
+ case 'rrf':
184
+ return new RRFFusion(config.rrfK)
185
+ case 'linear':
186
+ return new LinearFusion(config.denseWeight, config.sparseWeight)
187
+ case 'max':
188
+ return new MaxFusion()
189
+ default:
190
+ return new RRFFusion(config.rrfK)
191
+ }
192
+ }
193
+
194
+ export { fuseWithRRF, fuseMultipleWithRRF, calculateRRFScore } from './rrf'