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.
- package/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
|
@@ -1,391 +1,399 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain Response Filter
|
|
3
|
-
* Phase 16 + Phase 19: Filters noise, deduplicates, ranks, and synthesizes results
|
|
4
|
-
*
|
|
5
|
-
* Phase 19 changes:
|
|
6
|
-
* - D2: Lower word overlap threshold 0.85 → 0.75, add ID-based and prefix-based dedup
|
|
7
|
-
* - D3: Split INFRA_NOISE into strong (1 hit = filter) and weak (2+ hits = filter)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface FilterableResult {
|
|
11
|
-
content: string
|
|
12
|
-
score: number
|
|
13
|
-
source: string
|
|
14
|
-
metadata?: Record<string, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface FilteredResult {
|
|
18
|
-
id?: string
|
|
19
|
-
content: string
|
|
20
|
-
score: number
|
|
21
|
-
source: string
|
|
22
|
-
relevanceNote: string
|
|
23
|
-
metadata?: Record<string, unknown>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface BrainResponse {
|
|
27
|
-
action: 'retrieved' | 'stored' | 'analyzed' | 'none'
|
|
28
|
-
summary: string
|
|
29
|
-
content: string
|
|
30
|
-
relevantItems: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TierResults {
|
|
34
|
-
label: string
|
|
35
|
-
results: FilterableResult[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class ResponseFilter {
|
|
39
|
-
// D3: Strong noise — 1 hit is enough to filter (very specific internal terms)
|
|
40
|
-
private readonly STRONG_NOISE = [
|
|
41
|
-
'mcp-server', 'model-context-protocol', 'embedding-service', 'vector-database',
|
|
42
|
-
'mcp tool', 'tool handler', 'precompute engine', 'knowledge graph builder',
|
|
43
|
-
'semantic cache', 'bun:test'
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
// D3: Weak noise — need 2+ hits to filter (terms that could appear in legitimate decisions)
|
|
47
|
-
private readonly WEAK_NOISE = [
|
|
48
|
-
'chromadb', 'minisearch', 'compromise', 'better-sqlite3',
|
|
49
|
-
'pino', '
|
|
50
|
-
'phase 12', 'phase 13', 'phase 14', 'phase 15', 'phase 16',
|
|
51
|
-
'phase 17', 'phase 18', 'phase 19'
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Filter a set of results to remove noise and improve relevance
|
|
56
|
-
*/
|
|
57
|
-
filter(results: FilterableResult[], query: string, project?: string): FilteredResult[] {
|
|
58
|
-
let filtered = results
|
|
59
|
-
|
|
60
|
-
// 1. Remove infrastructure noise (if project !== 'claude-brain')
|
|
61
|
-
if (project !== 'claude-brain') {
|
|
62
|
-
const queryLower = query.toLowerCase()
|
|
63
|
-
filtered = filtered.filter(r => !this.isInfrastructureNoise(r.content, queryLower))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 2. Remove near-duplicates (Phase 19 D2: improved dedup)
|
|
67
|
-
filtered = this.deduplicateResults(filtered)
|
|
68
|
-
|
|
69
|
-
// 3. Apply dynamic threshold (at least 70% of median similarity)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
for
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Brain Response Filter
|
|
3
|
+
* Phase 16 + Phase 19: Filters noise, deduplicates, ranks, and synthesizes results
|
|
4
|
+
*
|
|
5
|
+
* Phase 19 changes:
|
|
6
|
+
* - D2: Lower word overlap threshold 0.85 → 0.75, add ID-based and prefix-based dedup
|
|
7
|
+
* - D3: Split INFRA_NOISE into strong (1 hit = filter) and weak (2+ hits = filter)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface FilterableResult {
|
|
11
|
+
content: string
|
|
12
|
+
score: number
|
|
13
|
+
source: string
|
|
14
|
+
metadata?: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FilteredResult {
|
|
18
|
+
id?: string
|
|
19
|
+
content: string
|
|
20
|
+
score: number
|
|
21
|
+
source: string
|
|
22
|
+
relevanceNote: string
|
|
23
|
+
metadata?: Record<string, unknown>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BrainResponse {
|
|
27
|
+
action: 'retrieved' | 'stored' | 'analyzed' | 'none'
|
|
28
|
+
summary: string
|
|
29
|
+
content: string
|
|
30
|
+
relevantItems: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TierResults {
|
|
34
|
+
label: string
|
|
35
|
+
results: FilterableResult[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ResponseFilter {
|
|
39
|
+
// D3: Strong noise — 1 hit is enough to filter (very specific internal terms)
|
|
40
|
+
private readonly STRONG_NOISE = [
|
|
41
|
+
'mcp-server', 'model-context-protocol', 'embedding-service', 'vector-database',
|
|
42
|
+
'mcp tool', 'tool handler', 'precompute engine', 'knowledge graph builder',
|
|
43
|
+
'semantic cache', 'bun:test'
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
// D3: Weak noise — need 2+ hits to filter (terms that could appear in legitimate decisions)
|
|
47
|
+
private readonly WEAK_NOISE = [
|
|
48
|
+
'chromadb', 'minisearch', 'compromise', 'better-sqlite3',
|
|
49
|
+
'pino', 'claude-brain',
|
|
50
|
+
'phase 12', 'phase 13', 'phase 14', 'phase 15', 'phase 16',
|
|
51
|
+
'phase 17', 'phase 18', 'phase 19'
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Filter a set of results to remove noise and improve relevance
|
|
56
|
+
*/
|
|
57
|
+
filter(results: FilterableResult[], query: string, project?: string): FilteredResult[] {
|
|
58
|
+
let filtered = results
|
|
59
|
+
|
|
60
|
+
// 1. Remove infrastructure noise (if project !== 'claude-brain')
|
|
61
|
+
if (project !== 'claude-brain') {
|
|
62
|
+
const queryLower = query.toLowerCase()
|
|
63
|
+
filtered = filtered.filter(r => !this.isInfrastructureNoise(r.content, queryLower))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Remove near-duplicates (Phase 19 D2: improved dedup)
|
|
67
|
+
filtered = this.deduplicateResults(filtered)
|
|
68
|
+
|
|
69
|
+
// 3. Apply dynamic threshold (at least 70% of median similarity)
|
|
70
|
+
const preThreshold = filtered
|
|
71
|
+
filtered = this.applyDynamicThreshold(filtered)
|
|
72
|
+
|
|
73
|
+
// Safety valve: if threshold killed everything but we had results, keep the best one
|
|
74
|
+
if (filtered.length === 0 && preThreshold.length > 0) {
|
|
75
|
+
const best = preThreshold.reduce((a, b) => a.score > b.score ? a : b)
|
|
76
|
+
filtered = [best]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Sort by score descending
|
|
80
|
+
filtered.sort((a, b) => b.score - a.score)
|
|
81
|
+
|
|
82
|
+
// 5. Limit to 5 results
|
|
83
|
+
filtered = filtered.slice(0, 5)
|
|
84
|
+
|
|
85
|
+
// 6. Add one-line relevance explanation per result (preserve id from metadata)
|
|
86
|
+
return filtered.map(r => ({
|
|
87
|
+
id: (r.metadata as Record<string, unknown> | undefined)?.id as string | undefined || (r.metadata as Record<string, unknown> | undefined)?.decision_id as string | undefined,
|
|
88
|
+
content: r.content,
|
|
89
|
+
score: r.score,
|
|
90
|
+
source: r.source,
|
|
91
|
+
relevanceNote: this.generateRelevanceNote(r, query),
|
|
92
|
+
metadata: r.metadata
|
|
93
|
+
}))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Synthesize results from multiple tiers into a unified BrainResponse
|
|
98
|
+
*/
|
|
99
|
+
synthesize(
|
|
100
|
+
tiers: TierResults[],
|
|
101
|
+
message: string,
|
|
102
|
+
project?: string,
|
|
103
|
+
action: BrainResponse['action'] = 'retrieved'
|
|
104
|
+
): BrainResponse {
|
|
105
|
+
// Combine all results from all tiers
|
|
106
|
+
const allResults: FilterableResult[] = []
|
|
107
|
+
for (const tier of tiers) {
|
|
108
|
+
allResults.push(...tier.results)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (allResults.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
action: 'none',
|
|
114
|
+
summary: 'No relevant information found',
|
|
115
|
+
content: `No results found for: "${message.slice(0, 100)}"`,
|
|
116
|
+
relevantItems: 0
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filter the combined results
|
|
121
|
+
const filtered = this.filter(allResults, message, project)
|
|
122
|
+
|
|
123
|
+
if (filtered.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
action: 'none',
|
|
126
|
+
summary: 'Results filtered out as noise or irrelevant',
|
|
127
|
+
content: `No relevant results after filtering for: "${message.slice(0, 100)}"`,
|
|
128
|
+
relevantItems: 0
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Format filtered results
|
|
133
|
+
const contentParts: string[] = []
|
|
134
|
+
for (const result of filtered) {
|
|
135
|
+
const scoreStr = result.score > 0 ? ` [${Math.round(result.score * 100)}%]` : ''
|
|
136
|
+
// Defensive: ensure content is always a string (prevents [object Object])
|
|
137
|
+
const contentStr = typeof result.content === 'string'
|
|
138
|
+
? result.content
|
|
139
|
+
: JSON.stringify(result.content ?? '')
|
|
140
|
+
contentParts.push(`**${result.source}**${scoreStr}\n${contentStr}`)
|
|
141
|
+
if (result.relevanceNote) {
|
|
142
|
+
contentParts.push(`_${result.relevanceNote}_`)
|
|
143
|
+
}
|
|
144
|
+
contentParts.push('')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const summary = filtered.length === 1
|
|
148
|
+
? `Found 1 relevant result`
|
|
149
|
+
: `Found ${filtered.length} relevant results`
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
action,
|
|
153
|
+
summary,
|
|
154
|
+
content: contentParts.join('\n'),
|
|
155
|
+
relevantItems: filtered.length
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* D3: Check if content is infrastructure/internal noise
|
|
161
|
+
* Strong noise: 1 hit filters (unless the term appears in the query)
|
|
162
|
+
* Weak noise: 2+ hits filters (skipping terms that appear in the query)
|
|
163
|
+
*/
|
|
164
|
+
private isInfrastructureNoise(content: string, queryLower?: string): boolean {
|
|
165
|
+
const lower = content.toLowerCase()
|
|
166
|
+
|
|
167
|
+
// Strong noise: a single hit is enough (but skip terms the user asked about)
|
|
168
|
+
for (const term of this.STRONG_NOISE) {
|
|
169
|
+
if (queryLower && queryLower.includes(term)) continue
|
|
170
|
+
if (lower.includes(term)) {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Weak noise: need 2+ hits (skip terms the user asked about)
|
|
176
|
+
let weakHits = 0
|
|
177
|
+
for (const term of this.WEAK_NOISE) {
|
|
178
|
+
if (queryLower && queryLower.includes(term)) continue
|
|
179
|
+
if (lower.includes(term)) {
|
|
180
|
+
weakHits++
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return weakHits >= 2
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* D2: Improved deduplication
|
|
188
|
+
* - ID-based dedup (if metadata has an ID)
|
|
189
|
+
* - Prefix-based dedup (first 100 chars match)
|
|
190
|
+
* - Word overlap dedup (lowered from 0.85 to 0.75)
|
|
191
|
+
*/
|
|
192
|
+
private deduplicateResults(results: FilterableResult[]): FilterableResult[] {
|
|
193
|
+
const kept: FilterableResult[] = []
|
|
194
|
+
const seenIds = new Set<string>()
|
|
195
|
+
const seenPrefixes = new Set<string>()
|
|
196
|
+
|
|
197
|
+
for (const result of results) {
|
|
198
|
+
// ID-based dedup
|
|
199
|
+
const id = (result.metadata as Record<string, unknown> | undefined)?.id as string | undefined || (result.metadata as Record<string, unknown> | undefined)?.decision_id as string | undefined
|
|
200
|
+
if (id && seenIds.has(id)) continue
|
|
201
|
+
if (id) seenIds.add(id)
|
|
202
|
+
|
|
203
|
+
// Prefix-based dedup (first 100 chars, normalized)
|
|
204
|
+
const prefix = result.content.slice(0, 100).toLowerCase().replace(/\s+/g, ' ').trim()
|
|
205
|
+
if (prefix.length > 20 && seenPrefixes.has(prefix)) continue
|
|
206
|
+
if (prefix.length > 20) seenPrefixes.add(prefix)
|
|
207
|
+
|
|
208
|
+
// Word overlap dedup (D2: lowered to 0.75)
|
|
209
|
+
const isDuplicate = kept.some(existing => {
|
|
210
|
+
const overlap = this.calculateWordOverlap(existing.content, result.content)
|
|
211
|
+
return overlap > 0.75
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!isDuplicate) {
|
|
215
|
+
kept.push(result)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return kept
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Apply dynamic threshold — results must be at least 70% of median score
|
|
224
|
+
*/
|
|
225
|
+
private applyDynamicThreshold(results: FilterableResult[]): FilterableResult[] {
|
|
226
|
+
if (results.length <= 1) return results
|
|
227
|
+
|
|
228
|
+
// Calculate median score
|
|
229
|
+
const scores = results.map(r => r.score).sort((a, b) => a - b)
|
|
230
|
+
const mid = Math.floor(scores.length / 2)
|
|
231
|
+
const left = scores[mid - 1] ?? 0
|
|
232
|
+
const right = scores[mid] ?? 0
|
|
233
|
+
const median = scores.length % 2 === 0
|
|
234
|
+
? (left + right) / 2
|
|
235
|
+
: right
|
|
236
|
+
|
|
237
|
+
const threshold = Math.max(median * 0.7, 0.25)
|
|
238
|
+
|
|
239
|
+
return results.filter(r => r.score >= threshold)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Calculate word overlap between two strings (0-1)
|
|
244
|
+
*/
|
|
245
|
+
private calculateWordOverlap(a: string, b: string): number {
|
|
246
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
247
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
248
|
+
|
|
249
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0
|
|
250
|
+
|
|
251
|
+
let intersection = 0
|
|
252
|
+
for (const word of wordsA) {
|
|
253
|
+
if (wordsB.has(word)) intersection++
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const smaller = Math.min(wordsA.size, wordsB.size)
|
|
257
|
+
return intersection / smaller
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Generate a one-line relevance explanation
|
|
262
|
+
*/
|
|
263
|
+
private generateRelevanceNote(result: FilterableResult, _query: string): string {
|
|
264
|
+
const score = Math.round(result.score * 100)
|
|
265
|
+
if (score >= 90) return `Highly relevant match (${score}%)`
|
|
266
|
+
if (score >= 70) return `Good match (${score}%)`
|
|
267
|
+
if (score >= 50) return `Partial match (${score}%)`
|
|
268
|
+
return `Low relevance match (${score}%)`
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// Phase 27: Progressive Disclosure — Compact Response Formatters
|
|
274
|
+
// =============================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Truncate text to maxLen chars, appending "..." if truncated
|
|
278
|
+
*/
|
|
279
|
+
function truncate(text: string, maxLen: number): string {
|
|
280
|
+
if (!text) return ''
|
|
281
|
+
if (text.length <= maxLen) return text
|
|
282
|
+
return text.substring(0, maxLen).trimEnd() + '...'
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format a date as a human-readable relative time string
|
|
287
|
+
*/
|
|
288
|
+
function formatRelativeTime(dateStr: string | Date | undefined): string {
|
|
289
|
+
if (!dateStr) return 'unknown'
|
|
290
|
+
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
|
|
291
|
+
if (isNaN(date.getTime())) return 'unknown'
|
|
292
|
+
const now = new Date()
|
|
293
|
+
const diffMs = now.getTime() - date.getTime()
|
|
294
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
295
|
+
const diffHours = Math.floor(diffMs / 3600000)
|
|
296
|
+
const diffDays = Math.floor(diffMs / 86400000)
|
|
297
|
+
|
|
298
|
+
if (diffMins < 1) return 'just now'
|
|
299
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`
|
|
300
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
|
|
301
|
+
if (diffDays === 1) return 'yesterday'
|
|
302
|
+
if (diffDays < 7) return `${diffDays} days ago`
|
|
303
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) === 1 ? '' : 's'} ago`
|
|
304
|
+
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) === 1 ? '' : 's'} ago`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Layer 1: Format a single result as a compact one-liner with metadata
|
|
309
|
+
*/
|
|
310
|
+
export function formatCompactResult(result: Record<string, unknown>, index: number): string {
|
|
311
|
+
const meta = (result.metadata || {}) as Record<string, unknown>
|
|
312
|
+
const category = result.category || result.type || meta.category || result.source || 'memory'
|
|
313
|
+
const summary = truncate(String(result.content || result.text || result.decision || ''), 80)
|
|
314
|
+
const project = result.project || meta.project || 'general'
|
|
315
|
+
const timeAgo = formatRelativeTime(
|
|
316
|
+
(result.createdAt || result.created_at || result.timestamp ||
|
|
317
|
+
result.date || meta.created_at || meta.createdAt || meta.date) as string | Date | undefined
|
|
318
|
+
)
|
|
319
|
+
const id = result.id || meta.id || meta.decision_id || 'unknown'
|
|
320
|
+
|
|
321
|
+
return `${index}. [${category}] ${summary}\n Project: ${project} | ${timeAgo}\n ID: ${id}`
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Layer 1: Full compact response with header, compact items, and footer
|
|
326
|
+
*/
|
|
327
|
+
export function formatCompactResponse(results: Record<string, unknown>[], query: string): string {
|
|
328
|
+
if (results.length === 0) {
|
|
329
|
+
return `No memories found for "${query}".`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const header = `Found ${results.length} relevant ${results.length === 1 ? 'memory' : 'memories'} for "${query}":`
|
|
333
|
+
const items = results.map((r, i) => formatCompactResult(r, i + 1)).join('\n\n')
|
|
334
|
+
const footer = `\nUse brain("details {ID}") for full context.`
|
|
335
|
+
|
|
336
|
+
return header + '\n\n' + items + footer
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Layer 2: Full detail view for a single observation/memory
|
|
341
|
+
*/
|
|
342
|
+
export function formatDetailResponse(observation: Record<string, unknown>): string {
|
|
343
|
+
const meta = (observation.metadata || {}) as Record<string, unknown>
|
|
344
|
+
const category = String(observation.category || observation.type || 'Memory')
|
|
345
|
+
const content = String(observation.content || observation.text || observation.decision || '')
|
|
346
|
+
const context = String(observation.context || meta.context || '')
|
|
347
|
+
const reasoning = String(observation.reasoning || meta.reasoning || '')
|
|
348
|
+
const tags = (observation.tags || meta.tags || []) as unknown
|
|
349
|
+
const created = String(observation.createdAt || observation.created_at || observation.timestamp || '')
|
|
350
|
+
|
|
351
|
+
const lines: string[] = []
|
|
352
|
+
lines.push(`${category.charAt(0).toUpperCase() + category.slice(1)}: ${content}`)
|
|
353
|
+
if (context) lines.push(`\nContext: ${context}`)
|
|
354
|
+
if (reasoning) lines.push(`Reasoning: ${reasoning}`)
|
|
355
|
+
if (tags.length > 0) lines.push(`Tags: ${Array.isArray(tags) ? tags.join(', ') : tags}`)
|
|
356
|
+
if (created) lines.push(`Created: ${created}`)
|
|
357
|
+
|
|
358
|
+
return lines.join('\n')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Layer 3: Group observations by day for timeline view
|
|
363
|
+
*/
|
|
364
|
+
export function groupByDay(observations: Record<string, unknown>[]): Map<string, Record<string, unknown>[]> {
|
|
365
|
+
const groups = new Map<string, Record<string, unknown>[]>()
|
|
366
|
+
for (const obs of observations) {
|
|
367
|
+
const date = new Date((obs.createdAt || obs.created_at || obs.timestamp || Date.now()) as string | number)
|
|
368
|
+
const dayKey = isNaN(date.getTime())
|
|
369
|
+
? 'Unknown'
|
|
370
|
+
: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
371
|
+
if (!groups.has(dayKey)) groups.set(dayKey, [])
|
|
372
|
+
groups.get(dayKey)!.push(obs)
|
|
373
|
+
}
|
|
374
|
+
return groups
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Layer 3: Format a timeline from day-grouped observations
|
|
379
|
+
*/
|
|
380
|
+
export function formatTimeline(grouped: Map<string, Record<string, unknown>[]>, project?: string): string {
|
|
381
|
+
if (grouped.size === 0) {
|
|
382
|
+
return project ? `No activity found for "${project}".` : 'No recent activity found.'
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const header = project ? `Timeline for ${project}:` : 'Recent timeline:'
|
|
386
|
+
const sections: string[] = [header, '']
|
|
387
|
+
|
|
388
|
+
for (const [day, observations] of grouped) {
|
|
389
|
+
sections.push(`${day}:`)
|
|
390
|
+
for (const obs of observations) {
|
|
391
|
+
const category = obs.category || obs.type || 'memory'
|
|
392
|
+
const summary = truncate(String(obs.content || obs.text || obs.decision || ''), 60)
|
|
393
|
+
sections.push(` - [${category}] ${summary}`)
|
|
394
|
+
}
|
|
395
|
+
sections.push('')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return sections.join('\n').trim()
|
|
399
|
+
}
|