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,263 +1,263 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trend Detection
|
|
3
|
-
* Detect technology, pattern, and concern trends across decisions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Logger } from 'pino'
|
|
7
|
-
import type { CollectionManager } from '@/memory/chroma/collection-manager'
|
|
8
|
-
|
|
9
|
-
export interface TrendItem {
|
|
10
|
-
term: string
|
|
11
|
-
occurrences: number
|
|
12
|
-
firstSeen: string
|
|
13
|
-
lastSeen: string
|
|
14
|
-
projects: string[]
|
|
15
|
-
trend: 'rising' | 'stable' | 'declining'
|
|
16
|
-
momentum: number // -1 to 1, positive = rising
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface TrendAnalysis {
|
|
20
|
-
project?: string
|
|
21
|
-
period: string
|
|
22
|
-
topTrends: TrendItem[]
|
|
23
|
-
emergingTopics: TrendItem[]
|
|
24
|
-
decliningTopics: TrendItem[]
|
|
25
|
-
totalDecisionsAnalyzed: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class TrendDetector {
|
|
29
|
-
private logger: Logger
|
|
30
|
-
private collections: CollectionManager
|
|
31
|
-
|
|
32
|
-
// Common stop words to exclude from trend analysis
|
|
33
|
-
private stopWords = new Set([
|
|
34
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
35
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
36
|
-
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
|
|
37
|
-
'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
|
|
38
|
-
'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above',
|
|
39
|
-
'below', 'between', 'out', 'off', 'over', 'under', 'again', 'further',
|
|
40
|
-
'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all',
|
|
41
|
-
'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
|
|
42
|
-
'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
|
|
43
|
-
'just', 'because', 'but', 'and', 'or', 'if', 'while', 'this', 'that',
|
|
44
|
-
'these', 'those', 'it', 'its', 'we', 'our', 'use', 'using', 'which',
|
|
45
|
-
'what', 'about', 'also', 'make', 'like', 'well', 'back', 'even',
|
|
46
|
-
'want', 'give', 'day', 'take', 'come', 'made', 'get', 'set', 'new',
|
|
47
|
-
'way', 'work', 'project', 'decision', 'decided', 'recommend', 'instead'
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
constructor(logger: Logger, collections: CollectionManager) {
|
|
51
|
-
this.logger = logger.child({ component: 'trend-detector' })
|
|
52
|
-
this.collections = collections
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Detect trends across decisions
|
|
57
|
-
*/
|
|
58
|
-
async detectTrends(options: {
|
|
59
|
-
project?: string
|
|
60
|
-
periodDays?: number
|
|
61
|
-
minOccurrences?: number
|
|
62
|
-
limit?: number
|
|
63
|
-
} = {}): Promise<TrendAnalysis> {
|
|
64
|
-
const {
|
|
65
|
-
project,
|
|
66
|
-
periodDays = 90,
|
|
67
|
-
minOccurrences = 2,
|
|
68
|
-
limit = 20
|
|
69
|
-
} = options
|
|
70
|
-
|
|
71
|
-
// Fetch all decisions within the period
|
|
72
|
-
const endDate = new Date()
|
|
73
|
-
const startDate = new Date(endDate)
|
|
74
|
-
startDate.setDate(startDate.getDate() - periodDays)
|
|
75
|
-
|
|
76
|
-
const midDate = new Date(startDate.getTime() + (endDate.getTime() - startDate.getTime()) / 2)
|
|
77
|
-
|
|
78
|
-
const allDecisions = await this.fetchAllDecisions(project)
|
|
79
|
-
|
|
80
|
-
// Split decisions into two halves for trend detection
|
|
81
|
-
const firstHalf: Array<{ content: string; date: string; project: string }> = []
|
|
82
|
-
const secondHalf: Array<{ content: string; date: string; project: string }> = []
|
|
83
|
-
const withinPeriod: Array<{ content: string; date: string; project: string }> = []
|
|
84
|
-
|
|
85
|
-
for (const d of allDecisions) {
|
|
86
|
-
const date = new Date(d.date)
|
|
87
|
-
if (date >= startDate && date <= endDate) {
|
|
88
|
-
withinPeriod.push(d)
|
|
89
|
-
if (date < midDate) {
|
|
90
|
-
firstHalf.push(d)
|
|
91
|
-
} else {
|
|
92
|
-
secondHalf.push(d)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Extract terms from each half
|
|
98
|
-
const firstTerms = this.extractTermFrequency(firstHalf)
|
|
99
|
-
const secondTerms = this.extractTermFrequency(secondHalf)
|
|
100
|
-
const allTerms = this.extractTermFrequency(withinPeriod)
|
|
101
|
-
|
|
102
|
-
// Build trend items
|
|
103
|
-
const trendItems: TrendItem[] = []
|
|
104
|
-
const allTermKeys = new Set([...firstTerms.keys(), ...secondTerms.keys()])
|
|
105
|
-
|
|
106
|
-
for (const term of allTermKeys) {
|
|
107
|
-
const total = allTerms.get(term)
|
|
108
|
-
if (!total || total.count < minOccurrences) continue
|
|
109
|
-
|
|
110
|
-
const firstCount = firstTerms.get(term)?.count || 0
|
|
111
|
-
const secondCount = secondTerms.get(term)?.count || 0
|
|
112
|
-
|
|
113
|
-
// Calculate momentum
|
|
114
|
-
const totalCount = firstCount + secondCount
|
|
115
|
-
const momentum = totalCount > 0 ? (secondCount - firstCount) / totalCount : 0
|
|
116
|
-
|
|
117
|
-
let trend: 'rising' | 'stable' | 'declining'
|
|
118
|
-
if (momentum > 0.2) trend = 'rising'
|
|
119
|
-
else if (momentum < -0.2) trend = 'declining'
|
|
120
|
-
else trend = 'stable'
|
|
121
|
-
|
|
122
|
-
trendItems.push({
|
|
123
|
-
term,
|
|
124
|
-
occurrences: total.count,
|
|
125
|
-
firstSeen: total.firstSeen,
|
|
126
|
-
lastSeen: total.lastSeen,
|
|
127
|
-
projects: total.projects,
|
|
128
|
-
trend,
|
|
129
|
-
momentum
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Sort by occurrences
|
|
134
|
-
trendItems.sort((a, b) => b.occurrences - a.occurrences)
|
|
135
|
-
|
|
136
|
-
const topTrends = trendItems.slice(0, limit)
|
|
137
|
-
const emergingTopics = trendItems
|
|
138
|
-
.filter(t => t.trend === 'rising')
|
|
139
|
-
.sort((a, b) => b.momentum - a.momentum)
|
|
140
|
-
.slice(0, limit)
|
|
141
|
-
const decliningTopics = trendItems
|
|
142
|
-
.filter(t => t.trend === 'declining')
|
|
143
|
-
.sort((a, b) => a.momentum - b.momentum)
|
|
144
|
-
.slice(0, limit)
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
project,
|
|
148
|
-
period: `${periodDays} days`,
|
|
149
|
-
topTrends,
|
|
150
|
-
emergingTopics,
|
|
151
|
-
decliningTopics,
|
|
152
|
-
totalDecisionsAnalyzed: withinPeriod.length
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private async fetchAllDecisions(project?: string): Promise<Array<{
|
|
157
|
-
content: string
|
|
158
|
-
date: string
|
|
159
|
-
project: string
|
|
160
|
-
}>> {
|
|
161
|
-
try {
|
|
162
|
-
const collection = await this.collections.getDecisions()
|
|
163
|
-
|
|
164
|
-
const where
|
|
165
|
-
|
|
166
|
-
const results = await collection.get({
|
|
167
|
-
where,
|
|
168
|
-
include: ['documents', 'metadatas']
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
return results.ids.map((_, i) => ({
|
|
172
|
-
content: (results.documents![i] as string) || '',
|
|
173
|
-
date: (results.metadatas![i] as
|
|
174
|
-
project: (results.metadatas![i] as
|
|
175
|
-
}))
|
|
176
|
-
} catch (error) {
|
|
177
|
-
this.logger.warn({ error }, 'Failed to fetch decisions for trend analysis')
|
|
178
|
-
return []
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private extractTermFrequency(
|
|
183
|
-
items: Array<{ content: string; date: string; project: string }>
|
|
184
|
-
): Map<string, { count: number; firstSeen: string; lastSeen: string; projects: string[] }> {
|
|
185
|
-
const terms = new Map<string, { count: number; firstSeen: string; lastSeen: string; projects: Set<string> }>()
|
|
186
|
-
|
|
187
|
-
for (const item of items) {
|
|
188
|
-
const words = this.tokenize(item.content)
|
|
189
|
-
|
|
190
|
-
// Extract meaningful terms (bigrams + unigrams)
|
|
191
|
-
const seen = new Set<string>()
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i < words.length; i++) {
|
|
194
|
-
const word = words[i]!
|
|
195
|
-
if (word.length < 3 || this.stopWords.has(word)) continue
|
|
196
|
-
|
|
197
|
-
if (!seen.has(word)) {
|
|
198
|
-
seen.add(word)
|
|
199
|
-
const existing = terms.get(word)
|
|
200
|
-
if (existing) {
|
|
201
|
-
existing.count++
|
|
202
|
-
if (item.date < existing.firstSeen) existing.firstSeen = item.date
|
|
203
|
-
if (item.date > existing.lastSeen) existing.lastSeen = item.date
|
|
204
|
-
existing.projects.add(item.project)
|
|
205
|
-
} else {
|
|
206
|
-
terms.set(word, {
|
|
207
|
-
count: 1,
|
|
208
|
-
firstSeen: item.date,
|
|
209
|
-
lastSeen: item.date,
|
|
210
|
-
projects: new Set([item.project])
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Bigrams
|
|
216
|
-
if (i < words.length - 1) {
|
|
217
|
-
const next = words[i + 1]!
|
|
218
|
-
if (next.length >= 3 && !this.stopWords.has(next)) {
|
|
219
|
-
const bigram = `${word} ${next}`
|
|
220
|
-
if (!seen.has(bigram)) {
|
|
221
|
-
seen.add(bigram)
|
|
222
|
-
const existing = terms.get(bigram)
|
|
223
|
-
if (existing) {
|
|
224
|
-
existing.count++
|
|
225
|
-
if (item.date < existing.firstSeen) existing.firstSeen = item.date
|
|
226
|
-
if (item.date > existing.lastSeen) existing.lastSeen = item.date
|
|
227
|
-
existing.projects.add(item.project)
|
|
228
|
-
} else {
|
|
229
|
-
terms.set(bigram, {
|
|
230
|
-
count: 1,
|
|
231
|
-
firstSeen: item.date,
|
|
232
|
-
lastSeen: item.date,
|
|
233
|
-
projects: new Set([item.project])
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Convert to output format
|
|
243
|
-
const result = new Map<string, { count: number; firstSeen: string; lastSeen: string; projects: string[] }>()
|
|
244
|
-
for (const [term, data] of terms) {
|
|
245
|
-
result.set(term, {
|
|
246
|
-
count: data.count,
|
|
247
|
-
firstSeen: data.firstSeen,
|
|
248
|
-
lastSeen: data.lastSeen,
|
|
249
|
-
projects: Array.from(data.projects)
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return result
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private tokenize(text: string): string[] {
|
|
257
|
-
return text
|
|
258
|
-
.toLowerCase()
|
|
259
|
-
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
260
|
-
.split(/\s+/)
|
|
261
|
-
.filter(w => w.length > 0)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Trend Detection
|
|
3
|
+
* Detect technology, pattern, and concern trends across decisions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { CollectionManager } from '@/memory/chroma/collection-manager'
|
|
8
|
+
|
|
9
|
+
export interface TrendItem {
|
|
10
|
+
term: string
|
|
11
|
+
occurrences: number
|
|
12
|
+
firstSeen: string
|
|
13
|
+
lastSeen: string
|
|
14
|
+
projects: string[]
|
|
15
|
+
trend: 'rising' | 'stable' | 'declining'
|
|
16
|
+
momentum: number // -1 to 1, positive = rising
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TrendAnalysis {
|
|
20
|
+
project?: string
|
|
21
|
+
period: string
|
|
22
|
+
topTrends: TrendItem[]
|
|
23
|
+
emergingTopics: TrendItem[]
|
|
24
|
+
decliningTopics: TrendItem[]
|
|
25
|
+
totalDecisionsAnalyzed: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class TrendDetector {
|
|
29
|
+
private logger: Logger
|
|
30
|
+
private collections: CollectionManager
|
|
31
|
+
|
|
32
|
+
// Common stop words to exclude from trend analysis
|
|
33
|
+
private stopWords = new Set([
|
|
34
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
35
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
36
|
+
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
|
|
37
|
+
'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
|
|
38
|
+
'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above',
|
|
39
|
+
'below', 'between', 'out', 'off', 'over', 'under', 'again', 'further',
|
|
40
|
+
'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all',
|
|
41
|
+
'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
|
|
42
|
+
'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very',
|
|
43
|
+
'just', 'because', 'but', 'and', 'or', 'if', 'while', 'this', 'that',
|
|
44
|
+
'these', 'those', 'it', 'its', 'we', 'our', 'use', 'using', 'which',
|
|
45
|
+
'what', 'about', 'also', 'make', 'like', 'well', 'back', 'even',
|
|
46
|
+
'want', 'give', 'day', 'take', 'come', 'made', 'get', 'set', 'new',
|
|
47
|
+
'way', 'work', 'project', 'decision', 'decided', 'recommend', 'instead'
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
constructor(logger: Logger, collections: CollectionManager) {
|
|
51
|
+
this.logger = logger.child({ component: 'trend-detector' })
|
|
52
|
+
this.collections = collections
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect trends across decisions
|
|
57
|
+
*/
|
|
58
|
+
async detectTrends(options: {
|
|
59
|
+
project?: string
|
|
60
|
+
periodDays?: number
|
|
61
|
+
minOccurrences?: number
|
|
62
|
+
limit?: number
|
|
63
|
+
} = {}): Promise<TrendAnalysis> {
|
|
64
|
+
const {
|
|
65
|
+
project,
|
|
66
|
+
periodDays = 90,
|
|
67
|
+
minOccurrences = 2,
|
|
68
|
+
limit = 20
|
|
69
|
+
} = options
|
|
70
|
+
|
|
71
|
+
// Fetch all decisions within the period
|
|
72
|
+
const endDate = new Date()
|
|
73
|
+
const startDate = new Date(endDate)
|
|
74
|
+
startDate.setDate(startDate.getDate() - periodDays)
|
|
75
|
+
|
|
76
|
+
const midDate = new Date(startDate.getTime() + (endDate.getTime() - startDate.getTime()) / 2)
|
|
77
|
+
|
|
78
|
+
const allDecisions = await this.fetchAllDecisions(project)
|
|
79
|
+
|
|
80
|
+
// Split decisions into two halves for trend detection
|
|
81
|
+
const firstHalf: Array<{ content: string; date: string; project: string }> = []
|
|
82
|
+
const secondHalf: Array<{ content: string; date: string; project: string }> = []
|
|
83
|
+
const withinPeriod: Array<{ content: string; date: string; project: string }> = []
|
|
84
|
+
|
|
85
|
+
for (const d of allDecisions) {
|
|
86
|
+
const date = new Date(d.date)
|
|
87
|
+
if (date >= startDate && date <= endDate) {
|
|
88
|
+
withinPeriod.push(d)
|
|
89
|
+
if (date < midDate) {
|
|
90
|
+
firstHalf.push(d)
|
|
91
|
+
} else {
|
|
92
|
+
secondHalf.push(d)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract terms from each half
|
|
98
|
+
const firstTerms = this.extractTermFrequency(firstHalf)
|
|
99
|
+
const secondTerms = this.extractTermFrequency(secondHalf)
|
|
100
|
+
const allTerms = this.extractTermFrequency(withinPeriod)
|
|
101
|
+
|
|
102
|
+
// Build trend items
|
|
103
|
+
const trendItems: TrendItem[] = []
|
|
104
|
+
const allTermKeys = new Set([...firstTerms.keys(), ...secondTerms.keys()])
|
|
105
|
+
|
|
106
|
+
for (const term of allTermKeys) {
|
|
107
|
+
const total = allTerms.get(term)
|
|
108
|
+
if (!total || total.count < minOccurrences) continue
|
|
109
|
+
|
|
110
|
+
const firstCount = firstTerms.get(term)?.count || 0
|
|
111
|
+
const secondCount = secondTerms.get(term)?.count || 0
|
|
112
|
+
|
|
113
|
+
// Calculate momentum
|
|
114
|
+
const totalCount = firstCount + secondCount
|
|
115
|
+
const momentum = totalCount > 0 ? (secondCount - firstCount) / totalCount : 0
|
|
116
|
+
|
|
117
|
+
let trend: 'rising' | 'stable' | 'declining'
|
|
118
|
+
if (momentum > 0.2) trend = 'rising'
|
|
119
|
+
else if (momentum < -0.2) trend = 'declining'
|
|
120
|
+
else trend = 'stable'
|
|
121
|
+
|
|
122
|
+
trendItems.push({
|
|
123
|
+
term,
|
|
124
|
+
occurrences: total.count,
|
|
125
|
+
firstSeen: total.firstSeen,
|
|
126
|
+
lastSeen: total.lastSeen,
|
|
127
|
+
projects: total.projects,
|
|
128
|
+
trend,
|
|
129
|
+
momentum
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Sort by occurrences
|
|
134
|
+
trendItems.sort((a, b) => b.occurrences - a.occurrences)
|
|
135
|
+
|
|
136
|
+
const topTrends = trendItems.slice(0, limit)
|
|
137
|
+
const emergingTopics = trendItems
|
|
138
|
+
.filter(t => t.trend === 'rising')
|
|
139
|
+
.sort((a, b) => b.momentum - a.momentum)
|
|
140
|
+
.slice(0, limit)
|
|
141
|
+
const decliningTopics = trendItems
|
|
142
|
+
.filter(t => t.trend === 'declining')
|
|
143
|
+
.sort((a, b) => a.momentum - b.momentum)
|
|
144
|
+
.slice(0, limit)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
project,
|
|
148
|
+
period: `${periodDays} days`,
|
|
149
|
+
topTrends,
|
|
150
|
+
emergingTopics,
|
|
151
|
+
decliningTopics,
|
|
152
|
+
totalDecisionsAnalyzed: withinPeriod.length
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async fetchAllDecisions(project?: string): Promise<Array<{
|
|
157
|
+
content: string
|
|
158
|
+
date: string
|
|
159
|
+
project: string
|
|
160
|
+
}>> {
|
|
161
|
+
try {
|
|
162
|
+
const collection = await this.collections.getDecisions()
|
|
163
|
+
|
|
164
|
+
const where = project ? { project: { $eq: project } } : undefined
|
|
165
|
+
|
|
166
|
+
const results = await collection.get({
|
|
167
|
+
where,
|
|
168
|
+
include: ['documents', 'metadatas']
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return results.ids.map((_, i) => ({
|
|
172
|
+
content: (results.documents![i] as string) || '',
|
|
173
|
+
date: (results.metadatas![i] as Record<string, unknown>)?.created_at as string || new Date().toISOString(),
|
|
174
|
+
project: (results.metadatas![i] as Record<string, unknown>)?.project as string || ''
|
|
175
|
+
}))
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.logger.warn({ error }, 'Failed to fetch decisions for trend analysis')
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private extractTermFrequency(
|
|
183
|
+
items: Array<{ content: string; date: string; project: string }>
|
|
184
|
+
): Map<string, { count: number; firstSeen: string; lastSeen: string; projects: string[] }> {
|
|
185
|
+
const terms = new Map<string, { count: number; firstSeen: string; lastSeen: string; projects: Set<string> }>()
|
|
186
|
+
|
|
187
|
+
for (const item of items) {
|
|
188
|
+
const words = this.tokenize(item.content)
|
|
189
|
+
|
|
190
|
+
// Extract meaningful terms (bigrams + unigrams)
|
|
191
|
+
const seen = new Set<string>()
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < words.length; i++) {
|
|
194
|
+
const word = words[i]!
|
|
195
|
+
if (word.length < 3 || this.stopWords.has(word)) continue
|
|
196
|
+
|
|
197
|
+
if (!seen.has(word)) {
|
|
198
|
+
seen.add(word)
|
|
199
|
+
const existing = terms.get(word)
|
|
200
|
+
if (existing) {
|
|
201
|
+
existing.count++
|
|
202
|
+
if (item.date < existing.firstSeen) existing.firstSeen = item.date
|
|
203
|
+
if (item.date > existing.lastSeen) existing.lastSeen = item.date
|
|
204
|
+
existing.projects.add(item.project)
|
|
205
|
+
} else {
|
|
206
|
+
terms.set(word, {
|
|
207
|
+
count: 1,
|
|
208
|
+
firstSeen: item.date,
|
|
209
|
+
lastSeen: item.date,
|
|
210
|
+
projects: new Set([item.project])
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Bigrams
|
|
216
|
+
if (i < words.length - 1) {
|
|
217
|
+
const next = words[i + 1]!
|
|
218
|
+
if (next.length >= 3 && !this.stopWords.has(next)) {
|
|
219
|
+
const bigram = `${word} ${next}`
|
|
220
|
+
if (!seen.has(bigram)) {
|
|
221
|
+
seen.add(bigram)
|
|
222
|
+
const existing = terms.get(bigram)
|
|
223
|
+
if (existing) {
|
|
224
|
+
existing.count++
|
|
225
|
+
if (item.date < existing.firstSeen) existing.firstSeen = item.date
|
|
226
|
+
if (item.date > existing.lastSeen) existing.lastSeen = item.date
|
|
227
|
+
existing.projects.add(item.project)
|
|
228
|
+
} else {
|
|
229
|
+
terms.set(bigram, {
|
|
230
|
+
count: 1,
|
|
231
|
+
firstSeen: item.date,
|
|
232
|
+
lastSeen: item.date,
|
|
233
|
+
projects: new Set([item.project])
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Convert to output format
|
|
243
|
+
const result = new Map<string, { count: number; firstSeen: string; lastSeen: string; projects: string[] }>()
|
|
244
|
+
for (const [term, data] of terms) {
|
|
245
|
+
result.set(term, {
|
|
246
|
+
count: data.count,
|
|
247
|
+
firstSeen: data.firstSeen,
|
|
248
|
+
lastSeen: data.lastSeen,
|
|
249
|
+
projects: Array.from(data.projects)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private tokenize(text: string): string[] {
|
|
257
|
+
return text
|
|
258
|
+
.toLowerCase()
|
|
259
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
260
|
+
.split(/\s+/)
|
|
261
|
+
.filter(w => w.length > 0)
|
|
262
|
+
}
|
|
263
|
+
}
|