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,452 +1,452 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decision Detector
|
|
3
|
-
* Phase 12: Advanced Memory & Intelligent Automation
|
|
4
|
-
*
|
|
5
|
-
* Detects when Claude makes decisions and automatically saves them
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Logger } from 'pino'
|
|
9
|
-
import type { MemoryManager } from '@/memory'
|
|
10
|
-
|
|
11
|
-
export interface DetectedDecision {
|
|
12
|
-
context: string
|
|
13
|
-
decision: string
|
|
14
|
-
reasoning: string
|
|
15
|
-
confidence: number
|
|
16
|
-
alternatives?: string
|
|
17
|
-
tags?: string[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DetectionResult {
|
|
21
|
-
detected: boolean
|
|
22
|
-
decision?: DetectedDecision
|
|
23
|
-
saved: boolean
|
|
24
|
-
decisionId?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class DecisionDetector {
|
|
28
|
-
private logger: Logger
|
|
29
|
-
private memory: MemoryManager
|
|
30
|
-
|
|
31
|
-
// Decision-indicating phrases
|
|
32
|
-
private readonly DECISION_PHRASES = [
|
|
33
|
-
'i recommend',
|
|
34
|
-
'you should use',
|
|
35
|
-
'the best approach',
|
|
36
|
-
'i suggest',
|
|
37
|
-
'better to use',
|
|
38
|
-
'prefer using',
|
|
39
|
-
'go with',
|
|
40
|
-
'choose',
|
|
41
|
-
'instead of',
|
|
42
|
-
'the right choice',
|
|
43
|
-
'decided to',
|
|
44
|
-
"let's use",
|
|
45
|
-
'we will use',
|
|
46
|
-
'the solution is',
|
|
47
|
-
'implement using'
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
// Reasoning indicators
|
|
51
|
-
private readonly REASONING_PHRASES = [
|
|
52
|
-
'because',
|
|
53
|
-
'since',
|
|
54
|
-
'due to',
|
|
55
|
-
'as it',
|
|
56
|
-
'which provides',
|
|
57
|
-
'this allows',
|
|
58
|
-
'this ensures',
|
|
59
|
-
'given that',
|
|
60
|
-
'considering',
|
|
61
|
-
'the reason is',
|
|
62
|
-
'this way'
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
// Alternative indicators
|
|
66
|
-
private readonly ALTERNATIVE_PHRASES = [
|
|
67
|
-
'instead of',
|
|
68
|
-
'rather than',
|
|
69
|
-
'over',
|
|
70
|
-
' vs ',
|
|
71
|
-
'compared to',
|
|
72
|
-
'unlike',
|
|
73
|
-
'as opposed to'
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
constructor(logger: Logger, memory: MemoryManager) {
|
|
77
|
-
this.logger = logger.child({ component: 'decision-detector' })
|
|
78
|
-
this.memory = memory
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Detect decision from text
|
|
83
|
-
*/
|
|
84
|
-
detectDecision(text: string, _project?: string): DetectedDecision | null {
|
|
85
|
-
const textLower = text.toLowerCase()
|
|
86
|
-
|
|
87
|
-
// Check for decision phrases
|
|
88
|
-
const hasDecisionPhrase = this.DECISION_PHRASES.some(phrase =>
|
|
89
|
-
textLower.includes(phrase)
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if (!hasDecisionPhrase) {
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Extract decision components
|
|
97
|
-
const context = this.extractContext(text)
|
|
98
|
-
const decision = this.extractDecision(text)
|
|
99
|
-
const reasoning = this.extractReasoning(text)
|
|
100
|
-
const alternatives = this.extractAlternatives(text)
|
|
101
|
-
const tags = this.extractTags(text)
|
|
102
|
-
|
|
103
|
-
// Calculate confidence
|
|
104
|
-
const confidence = this.calculateConfidence(text, {
|
|
105
|
-
hasContext: !!context && context.length > 10,
|
|
106
|
-
hasReasoning: !!reasoning && reasoning.length > 10,
|
|
107
|
-
hasAlternatives: !!alternatives
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
if (confidence < 0.5) {
|
|
111
|
-
this.logger.debug(
|
|
112
|
-
{ confidence },
|
|
113
|
-
'Low confidence decision detection - ignoring'
|
|
114
|
-
)
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
context,
|
|
120
|
-
decision,
|
|
121
|
-
reasoning,
|
|
122
|
-
confidence,
|
|
123
|
-
alternatives,
|
|
124
|
-
tags
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Extract context from text
|
|
130
|
-
*/
|
|
131
|
-
private extractContext(text: string): string {
|
|
132
|
-
const sentences = text.split(/[.!?]+/).filter(s => s.trim())
|
|
133
|
-
|
|
134
|
-
// Find sentence with decision phrase
|
|
135
|
-
for (let i = 0; i < sentences.length; i++) {
|
|
136
|
-
const sentence = (sentences[i] ?? "").toLowerCase()
|
|
137
|
-
|
|
138
|
-
if (this.DECISION_PHRASES.some(p => sentence.includes(p))) {
|
|
139
|
-
// Context is likely in previous sentences
|
|
140
|
-
if (i > 0) {
|
|
141
|
-
// Take up to 2 previous sentences for context
|
|
142
|
-
const contextSentences = sentences.slice(Math.max(0, i - 2), i)
|
|
143
|
-
return contextSentences.map(s => s.trim()).join('. ')
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Fallback: use first sentence
|
|
149
|
-
return sentences[0]?.trim() || 'General decision'
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Extract the actual decision
|
|
154
|
-
*/
|
|
155
|
-
private extractDecision(text: string): string {
|
|
156
|
-
const textLower = text.toLowerCase()
|
|
157
|
-
|
|
158
|
-
for (const phrase of this.DECISION_PHRASES) {
|
|
159
|
-
const index = textLower.indexOf(phrase)
|
|
160
|
-
|
|
161
|
-
if (index !== -1) {
|
|
162
|
-
// Extract text after the phrase
|
|
163
|
-
const afterPhrase = text.substring(index + phrase.length).trim()
|
|
164
|
-
|
|
165
|
-
// Get until next sentence or reasoning
|
|
166
|
-
const endIndex = this.findDecisionEnd(afterPhrase)
|
|
167
|
-
const decision = afterPhrase.substring(0, endIndex).trim()
|
|
168
|
-
|
|
169
|
-
// Clean up the decision
|
|
170
|
-
if (decision.length > 5) {
|
|
171
|
-
return decision.charAt(0).toUpperCase() + decision.slice(1)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return 'Decision detected but could not extract details'
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Find where decision ends
|
|
181
|
-
*/
|
|
182
|
-
private findDecisionEnd(text: string): number {
|
|
183
|
-
const textLower = text.toLowerCase()
|
|
184
|
-
|
|
185
|
-
// Decision ends at reasoning or new sentence
|
|
186
|
-
const reasoningStart = this.REASONING_PHRASES.reduce((min, phrase) => {
|
|
187
|
-
const index = textLower.indexOf(phrase)
|
|
188
|
-
return index !== -1 ? Math.min(min, index) : min
|
|
189
|
-
}, text.length)
|
|
190
|
-
|
|
191
|
-
const sentenceEnd = text.search(/[.!?\n]/)
|
|
192
|
-
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
193
|
-
|
|
194
|
-
return Math.min(reasoningStart, end, 200) // Cap at 200 chars
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Extract reasoning
|
|
199
|
-
*/
|
|
200
|
-
private extractReasoning(text: string): string {
|
|
201
|
-
const textLower = text.toLowerCase()
|
|
202
|
-
|
|
203
|
-
for (const phrase of this.REASONING_PHRASES) {
|
|
204
|
-
const index = textLower.indexOf(phrase)
|
|
205
|
-
|
|
206
|
-
if (index !== -1) {
|
|
207
|
-
// Extract text after reasoning phrase
|
|
208
|
-
const afterPhrase = text.substring(index).trim()
|
|
209
|
-
|
|
210
|
-
// Get reasoning - take multiple sentences if they continue the thought
|
|
211
|
-
// Look for double newline or paragraph break as end marker
|
|
212
|
-
let endIndex = afterPhrase.search(/\n\n/)
|
|
213
|
-
|
|
214
|
-
// If no paragraph break, take up to 3 sentences or 300 chars, whichever is shorter
|
|
215
|
-
if (endIndex === -1) {
|
|
216
|
-
const sentences = afterPhrase.split(/[.!?]/)
|
|
217
|
-
if (sentences.length > 3) {
|
|
218
|
-
// Take first 3 sentences
|
|
219
|
-
const firstThree = sentences.slice(0, 3).join('. ')
|
|
220
|
-
endIndex = Math.min(firstThree.length, 300)
|
|
221
|
-
} else {
|
|
222
|
-
// Take all text up to 300 chars
|
|
223
|
-
endIndex = Math.min(afterPhrase.length, 300)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const reasoning = afterPhrase.substring(0, endIndex).trim()
|
|
228
|
-
|
|
229
|
-
if (reasoning.length > 10) {
|
|
230
|
-
return reasoning
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return ''
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Extract alternatives
|
|
240
|
-
*/
|
|
241
|
-
private extractAlternatives(text: string): string | undefined {
|
|
242
|
-
const textLower = text.toLowerCase()
|
|
243
|
-
|
|
244
|
-
// Look for patterns like "We considered X", "Alternatives include X, Y", etc.
|
|
245
|
-
const alternativeIntros = [
|
|
246
|
-
'we considered',
|
|
247
|
-
'we also looked at',
|
|
248
|
-
'we evaluated',
|
|
249
|
-
'alternatives considered',
|
|
250
|
-
'alternatives include',
|
|
251
|
-
'other options'
|
|
252
|
-
]
|
|
253
|
-
|
|
254
|
-
for (const intro of alternativeIntros) {
|
|
255
|
-
const introIndex = textLower.indexOf(intro)
|
|
256
|
-
if (introIndex !== -1) {
|
|
257
|
-
const afterIntro = text.substring(introIndex + intro.length).trim()
|
|
258
|
-
|
|
259
|
-
// Extract until we hit a sentence ending that starts a new thought
|
|
260
|
-
// Look for: period followed by capital letter, or double newline
|
|
261
|
-
const endMatch = afterIntro.match(/\.\s+[A-Z]|\n\n/)
|
|
262
|
-
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterIntro.length, 200)
|
|
263
|
-
|
|
264
|
-
const alternatives = afterIntro.substring(0, endIndex).trim()
|
|
265
|
-
|
|
266
|
-
if (alternatives.length > 10) {
|
|
267
|
-
return alternatives
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Fallback: look for comparison phrases
|
|
273
|
-
for (const phrase of this.ALTERNATIVE_PHRASES) {
|
|
274
|
-
const index = textLower.indexOf(phrase)
|
|
275
|
-
|
|
276
|
-
if (index !== -1) {
|
|
277
|
-
const afterPhrase = text.substring(index + phrase.length).trim()
|
|
278
|
-
|
|
279
|
-
// Don't stop at commas - take until sentence end or new thought
|
|
280
|
-
const endMatch = afterPhrase.match(/\.\s+[A-Z]|\n\n|[.!?]$/)
|
|
281
|
-
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
282
|
-
|
|
283
|
-
const alternative = afterPhrase.substring(0, endIndex).trim()
|
|
284
|
-
|
|
285
|
-
if (alternative.length > 3) {
|
|
286
|
-
return alternative
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return undefined
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Extract tags from decision
|
|
296
|
-
*/
|
|
297
|
-
private extractTags(text: string): string[] {
|
|
298
|
-
const tags: string[] = ['auto-detected']
|
|
299
|
-
const textLower = text.toLowerCase()
|
|
300
|
-
|
|
301
|
-
// Technology tags
|
|
302
|
-
const techPatterns = [
|
|
303
|
-
{ pattern: /typescript|ts\b/i, tag: 'typescript' },
|
|
304
|
-
{ pattern: /javascript|js\b/i, tag: 'javascript' },
|
|
305
|
-
{ pattern: /react/i, tag: 'react' },
|
|
306
|
-
{ pattern: /node\.?js/i, tag: 'nodejs' },
|
|
307
|
-
{ pattern: /python/i, tag: 'python' },
|
|
308
|
-
{ pattern: /database|sql|postgres|mysql|mongodb/i, tag: 'database' },
|
|
309
|
-
{ pattern: /api|rest|graphql/i, tag: 'api' },
|
|
310
|
-
{ pattern: /authentication|auth|login/i, tag: 'authentication' },
|
|
311
|
-
{ pattern: /security/i, tag: 'security' },
|
|
312
|
-
{ pattern: /performance|optimization/i, tag: 'performance' },
|
|
313
|
-
{ pattern: /testing|test|spec/i, tag: 'testing' },
|
|
314
|
-
{ pattern: /architecture|design/i, tag: 'architecture' }
|
|
315
|
-
]
|
|
316
|
-
|
|
317
|
-
for (const { pattern, tag } of techPatterns) {
|
|
318
|
-
if (pattern.test(textLower)) {
|
|
319
|
-
tags.push(tag)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return [...new Set(tags)]
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Calculate confidence score
|
|
328
|
-
*/
|
|
329
|
-
private calculateConfidence(
|
|
330
|
-
text: string,
|
|
331
|
-
features: {
|
|
332
|
-
hasContext: boolean
|
|
333
|
-
hasReasoning: boolean
|
|
334
|
-
hasAlternatives: boolean
|
|
335
|
-
}
|
|
336
|
-
): number {
|
|
337
|
-
let confidence = 0.5 // Base confidence
|
|
338
|
-
|
|
339
|
-
if (features.hasContext) confidence += 0.15
|
|
340
|
-
if (features.hasReasoning) confidence += 0.25
|
|
341
|
-
if (features.hasAlternatives) confidence += 0.1
|
|
342
|
-
|
|
343
|
-
// Bonus for strong decision language
|
|
344
|
-
const strongPhrases = ['definitely', 'clearly', 'must', 'strongly recommend']
|
|
345
|
-
const hasStrong = strongPhrases.some(p => text.toLowerCase().includes(p))
|
|
346
|
-
if (hasStrong) confidence += 0.1
|
|
347
|
-
|
|
348
|
-
// Length bonus (longer = more detailed = more confident)
|
|
349
|
-
if (text.length > 200) confidence += 0.05
|
|
350
|
-
if (text.length > 500) confidence += 0.05
|
|
351
|
-
|
|
352
|
-
return Math.min(confidence, 1.0)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Process text and optionally auto-save decision
|
|
357
|
-
*/
|
|
358
|
-
async processText(
|
|
359
|
-
text: string,
|
|
360
|
-
project: string,
|
|
361
|
-
autoSave: boolean = true
|
|
362
|
-
): Promise<DetectionResult> {
|
|
363
|
-
const decision = this.detectDecision(text, project)
|
|
364
|
-
|
|
365
|
-
if (!decision) {
|
|
366
|
-
return { detected: false, saved: false }
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!autoSave || decision.confidence < 0.7) {
|
|
370
|
-
return {
|
|
371
|
-
detected: true,
|
|
372
|
-
decision,
|
|
373
|
-
saved: false
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Auto-save the decision
|
|
378
|
-
const decisionId = await this.autoSaveDecision(decision, project)
|
|
379
|
-
|
|
380
|
-
return {
|
|
381
|
-
detected: true,
|
|
382
|
-
decision,
|
|
383
|
-
saved: !!decisionId,
|
|
384
|
-
decisionId: decisionId || undefined
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Auto-save decision if confidence is high enough
|
|
390
|
-
*/
|
|
391
|
-
async autoSaveDecision(
|
|
392
|
-
detected: DetectedDecision,
|
|
393
|
-
project: string
|
|
394
|
-
): Promise<string | null> {
|
|
395
|
-
if (detected.confidence < 0.7) {
|
|
396
|
-
this.logger.debug(
|
|
397
|
-
{ confidence: detected.confidence },
|
|
398
|
-
'Confidence too low for auto-save'
|
|
399
|
-
)
|
|
400
|
-
return null
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
const decisionId = await this.memory.rememberDecision(
|
|
405
|
-
project,
|
|
406
|
-
detected.context,
|
|
407
|
-
detected.decision,
|
|
408
|
-
detected.reasoning,
|
|
409
|
-
{
|
|
410
|
-
alternatives: detected.alternatives,
|
|
411
|
-
tags: detected.tags
|
|
412
|
-
}
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
this.logger.info(
|
|
416
|
-
{ project, confidence: detected.confidence, decisionId },
|
|
417
|
-
'Decision auto-saved'
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
return decisionId
|
|
421
|
-
|
|
422
|
-
} catch (error) {
|
|
423
|
-
this.logger.error({ error }, 'Failed to auto-save decision')
|
|
424
|
-
return null
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Batch process multiple texts
|
|
430
|
-
*/
|
|
431
|
-
async processBatch(
|
|
432
|
-
texts: Array<{ text: string; project: string }>,
|
|
433
|
-
autoSave: boolean = true
|
|
434
|
-
): Promise<DetectionResult[]> {
|
|
435
|
-
const results: DetectionResult[] = []
|
|
436
|
-
|
|
437
|
-
for (const { text, project } of texts) {
|
|
438
|
-
const result = await this.processText(text, project, autoSave)
|
|
439
|
-
results.push(result)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return results
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Check if text likely contains a decision
|
|
447
|
-
*/
|
|
448
|
-
quickCheck(text: string): boolean {
|
|
449
|
-
const textLower = text.toLowerCase()
|
|
450
|
-
return this.DECISION_PHRASES.some(phrase => textLower.includes(phrase))
|
|
451
|
-
}
|
|
452
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Decision Detector
|
|
3
|
+
* Phase 12: Advanced Memory & Intelligent Automation
|
|
4
|
+
*
|
|
5
|
+
* Detects when Claude makes decisions and automatically saves them
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Logger } from 'pino'
|
|
9
|
+
import type { MemoryManager } from '@/memory'
|
|
10
|
+
|
|
11
|
+
export interface DetectedDecision {
|
|
12
|
+
context: string
|
|
13
|
+
decision: string
|
|
14
|
+
reasoning: string
|
|
15
|
+
confidence: number
|
|
16
|
+
alternatives?: string
|
|
17
|
+
tags?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DetectionResult {
|
|
21
|
+
detected: boolean
|
|
22
|
+
decision?: DetectedDecision
|
|
23
|
+
saved: boolean
|
|
24
|
+
decisionId?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DecisionDetector {
|
|
28
|
+
private logger: Logger
|
|
29
|
+
private memory: MemoryManager
|
|
30
|
+
|
|
31
|
+
// Decision-indicating phrases
|
|
32
|
+
private readonly DECISION_PHRASES = [
|
|
33
|
+
'i recommend',
|
|
34
|
+
'you should use',
|
|
35
|
+
'the best approach',
|
|
36
|
+
'i suggest',
|
|
37
|
+
'better to use',
|
|
38
|
+
'prefer using',
|
|
39
|
+
'go with',
|
|
40
|
+
'choose',
|
|
41
|
+
'instead of',
|
|
42
|
+
'the right choice',
|
|
43
|
+
'decided to',
|
|
44
|
+
"let's use",
|
|
45
|
+
'we will use',
|
|
46
|
+
'the solution is',
|
|
47
|
+
'implement using'
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
// Reasoning indicators
|
|
51
|
+
private readonly REASONING_PHRASES = [
|
|
52
|
+
'because',
|
|
53
|
+
'since',
|
|
54
|
+
'due to',
|
|
55
|
+
'as it',
|
|
56
|
+
'which provides',
|
|
57
|
+
'this allows',
|
|
58
|
+
'this ensures',
|
|
59
|
+
'given that',
|
|
60
|
+
'considering',
|
|
61
|
+
'the reason is',
|
|
62
|
+
'this way'
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
// Alternative indicators
|
|
66
|
+
private readonly ALTERNATIVE_PHRASES = [
|
|
67
|
+
'instead of',
|
|
68
|
+
'rather than',
|
|
69
|
+
'over',
|
|
70
|
+
' vs ',
|
|
71
|
+
'compared to',
|
|
72
|
+
'unlike',
|
|
73
|
+
'as opposed to'
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
constructor(logger: Logger, memory: MemoryManager) {
|
|
77
|
+
this.logger = logger.child({ component: 'decision-detector' })
|
|
78
|
+
this.memory = memory
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect decision from text
|
|
83
|
+
*/
|
|
84
|
+
detectDecision(text: string, _project?: string): DetectedDecision | null {
|
|
85
|
+
const textLower = text.toLowerCase()
|
|
86
|
+
|
|
87
|
+
// Check for decision phrases
|
|
88
|
+
const hasDecisionPhrase = this.DECISION_PHRASES.some(phrase =>
|
|
89
|
+
textLower.includes(phrase)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (!hasDecisionPhrase) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract decision components
|
|
97
|
+
const context = this.extractContext(text)
|
|
98
|
+
const decision = this.extractDecision(text)
|
|
99
|
+
const reasoning = this.extractReasoning(text)
|
|
100
|
+
const alternatives = this.extractAlternatives(text)
|
|
101
|
+
const tags = this.extractTags(text)
|
|
102
|
+
|
|
103
|
+
// Calculate confidence
|
|
104
|
+
const confidence = this.calculateConfidence(text, {
|
|
105
|
+
hasContext: !!context && context.length > 10,
|
|
106
|
+
hasReasoning: !!reasoning && reasoning.length > 10,
|
|
107
|
+
hasAlternatives: !!alternatives
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (confidence < 0.5) {
|
|
111
|
+
this.logger.debug(
|
|
112
|
+
{ confidence },
|
|
113
|
+
'Low confidence decision detection - ignoring'
|
|
114
|
+
)
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
context,
|
|
120
|
+
decision,
|
|
121
|
+
reasoning,
|
|
122
|
+
confidence,
|
|
123
|
+
alternatives,
|
|
124
|
+
tags
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract context from text
|
|
130
|
+
*/
|
|
131
|
+
private extractContext(text: string): string {
|
|
132
|
+
const sentences = text.split(/[.!?]+/).filter(s => s.trim())
|
|
133
|
+
|
|
134
|
+
// Find sentence with decision phrase
|
|
135
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
136
|
+
const sentence = (sentences[i] ?? "").toLowerCase()
|
|
137
|
+
|
|
138
|
+
if (this.DECISION_PHRASES.some(p => sentence.includes(p))) {
|
|
139
|
+
// Context is likely in previous sentences
|
|
140
|
+
if (i > 0) {
|
|
141
|
+
// Take up to 2 previous sentences for context
|
|
142
|
+
const contextSentences = sentences.slice(Math.max(0, i - 2), i)
|
|
143
|
+
return contextSentences.map(s => s.trim()).join('. ')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: use first sentence
|
|
149
|
+
return sentences[0]?.trim() || 'General decision'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract the actual decision
|
|
154
|
+
*/
|
|
155
|
+
private extractDecision(text: string): string {
|
|
156
|
+
const textLower = text.toLowerCase()
|
|
157
|
+
|
|
158
|
+
for (const phrase of this.DECISION_PHRASES) {
|
|
159
|
+
const index = textLower.indexOf(phrase)
|
|
160
|
+
|
|
161
|
+
if (index !== -1) {
|
|
162
|
+
// Extract text after the phrase
|
|
163
|
+
const afterPhrase = text.substring(index + phrase.length).trim()
|
|
164
|
+
|
|
165
|
+
// Get until next sentence or reasoning
|
|
166
|
+
const endIndex = this.findDecisionEnd(afterPhrase)
|
|
167
|
+
const decision = afterPhrase.substring(0, endIndex).trim()
|
|
168
|
+
|
|
169
|
+
// Clean up the decision
|
|
170
|
+
if (decision.length > 5) {
|
|
171
|
+
return decision.charAt(0).toUpperCase() + decision.slice(1)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return 'Decision detected but could not extract details'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find where decision ends
|
|
181
|
+
*/
|
|
182
|
+
private findDecisionEnd(text: string): number {
|
|
183
|
+
const textLower = text.toLowerCase()
|
|
184
|
+
|
|
185
|
+
// Decision ends at reasoning or new sentence
|
|
186
|
+
const reasoningStart = this.REASONING_PHRASES.reduce((min, phrase) => {
|
|
187
|
+
const index = textLower.indexOf(phrase)
|
|
188
|
+
return index !== -1 ? Math.min(min, index) : min
|
|
189
|
+
}, text.length)
|
|
190
|
+
|
|
191
|
+
const sentenceEnd = text.search(/[.!?\n]/)
|
|
192
|
+
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
193
|
+
|
|
194
|
+
return Math.min(reasoningStart, end, 200) // Cap at 200 chars
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract reasoning
|
|
199
|
+
*/
|
|
200
|
+
private extractReasoning(text: string): string {
|
|
201
|
+
const textLower = text.toLowerCase()
|
|
202
|
+
|
|
203
|
+
for (const phrase of this.REASONING_PHRASES) {
|
|
204
|
+
const index = textLower.indexOf(phrase)
|
|
205
|
+
|
|
206
|
+
if (index !== -1) {
|
|
207
|
+
// Extract text after reasoning phrase
|
|
208
|
+
const afterPhrase = text.substring(index).trim()
|
|
209
|
+
|
|
210
|
+
// Get reasoning - take multiple sentences if they continue the thought
|
|
211
|
+
// Look for double newline or paragraph break as end marker
|
|
212
|
+
let endIndex = afterPhrase.search(/\n\n/)
|
|
213
|
+
|
|
214
|
+
// If no paragraph break, take up to 3 sentences or 300 chars, whichever is shorter
|
|
215
|
+
if (endIndex === -1) {
|
|
216
|
+
const sentences = afterPhrase.split(/[.!?]/)
|
|
217
|
+
if (sentences.length > 3) {
|
|
218
|
+
// Take first 3 sentences
|
|
219
|
+
const firstThree = sentences.slice(0, 3).join('. ')
|
|
220
|
+
endIndex = Math.min(firstThree.length, 300)
|
|
221
|
+
} else {
|
|
222
|
+
// Take all text up to 300 chars
|
|
223
|
+
endIndex = Math.min(afterPhrase.length, 300)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const reasoning = afterPhrase.substring(0, endIndex).trim()
|
|
228
|
+
|
|
229
|
+
if (reasoning.length > 10) {
|
|
230
|
+
return reasoning
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ''
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extract alternatives
|
|
240
|
+
*/
|
|
241
|
+
private extractAlternatives(text: string): string | undefined {
|
|
242
|
+
const textLower = text.toLowerCase()
|
|
243
|
+
|
|
244
|
+
// Look for patterns like "We considered X", "Alternatives include X, Y", etc.
|
|
245
|
+
const alternativeIntros = [
|
|
246
|
+
'we considered',
|
|
247
|
+
'we also looked at',
|
|
248
|
+
'we evaluated',
|
|
249
|
+
'alternatives considered',
|
|
250
|
+
'alternatives include',
|
|
251
|
+
'other options'
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for (const intro of alternativeIntros) {
|
|
255
|
+
const introIndex = textLower.indexOf(intro)
|
|
256
|
+
if (introIndex !== -1) {
|
|
257
|
+
const afterIntro = text.substring(introIndex + intro.length).trim()
|
|
258
|
+
|
|
259
|
+
// Extract until we hit a sentence ending that starts a new thought
|
|
260
|
+
// Look for: period followed by capital letter, or double newline
|
|
261
|
+
const endMatch = afterIntro.match(/\.\s+[A-Z]|\n\n/)
|
|
262
|
+
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterIntro.length, 200)
|
|
263
|
+
|
|
264
|
+
const alternatives = afterIntro.substring(0, endIndex).trim()
|
|
265
|
+
|
|
266
|
+
if (alternatives.length > 10) {
|
|
267
|
+
return alternatives
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fallback: look for comparison phrases
|
|
273
|
+
for (const phrase of this.ALTERNATIVE_PHRASES) {
|
|
274
|
+
const index = textLower.indexOf(phrase)
|
|
275
|
+
|
|
276
|
+
if (index !== -1) {
|
|
277
|
+
const afterPhrase = text.substring(index + phrase.length).trim()
|
|
278
|
+
|
|
279
|
+
// Don't stop at commas - take until sentence end or new thought
|
|
280
|
+
const endMatch = afterPhrase.match(/\.\s+[A-Z]|\n\n|[.!?]$/)
|
|
281
|
+
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
282
|
+
|
|
283
|
+
const alternative = afterPhrase.substring(0, endIndex).trim()
|
|
284
|
+
|
|
285
|
+
if (alternative.length > 3) {
|
|
286
|
+
return alternative
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Extract tags from decision
|
|
296
|
+
*/
|
|
297
|
+
private extractTags(text: string): string[] {
|
|
298
|
+
const tags: string[] = ['auto-detected']
|
|
299
|
+
const textLower = text.toLowerCase()
|
|
300
|
+
|
|
301
|
+
// Technology tags
|
|
302
|
+
const techPatterns = [
|
|
303
|
+
{ pattern: /typescript|ts\b/i, tag: 'typescript' },
|
|
304
|
+
{ pattern: /javascript|js\b/i, tag: 'javascript' },
|
|
305
|
+
{ pattern: /react/i, tag: 'react' },
|
|
306
|
+
{ pattern: /node\.?js/i, tag: 'nodejs' },
|
|
307
|
+
{ pattern: /python/i, tag: 'python' },
|
|
308
|
+
{ pattern: /database|sql|postgres|mysql|mongodb/i, tag: 'database' },
|
|
309
|
+
{ pattern: /api|rest|graphql/i, tag: 'api' },
|
|
310
|
+
{ pattern: /authentication|auth|login/i, tag: 'authentication' },
|
|
311
|
+
{ pattern: /security/i, tag: 'security' },
|
|
312
|
+
{ pattern: /performance|optimization/i, tag: 'performance' },
|
|
313
|
+
{ pattern: /testing|test|spec/i, tag: 'testing' },
|
|
314
|
+
{ pattern: /architecture|design/i, tag: 'architecture' }
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
for (const { pattern, tag } of techPatterns) {
|
|
318
|
+
if (pattern.test(textLower)) {
|
|
319
|
+
tags.push(tag)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return [...new Set(tags)]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Calculate confidence score
|
|
328
|
+
*/
|
|
329
|
+
private calculateConfidence(
|
|
330
|
+
text: string,
|
|
331
|
+
features: {
|
|
332
|
+
hasContext: boolean
|
|
333
|
+
hasReasoning: boolean
|
|
334
|
+
hasAlternatives: boolean
|
|
335
|
+
}
|
|
336
|
+
): number {
|
|
337
|
+
let confidence = 0.5 // Base confidence
|
|
338
|
+
|
|
339
|
+
if (features.hasContext) confidence += 0.15
|
|
340
|
+
if (features.hasReasoning) confidence += 0.25
|
|
341
|
+
if (features.hasAlternatives) confidence += 0.1
|
|
342
|
+
|
|
343
|
+
// Bonus for strong decision language
|
|
344
|
+
const strongPhrases = ['definitely', 'clearly', 'must', 'strongly recommend']
|
|
345
|
+
const hasStrong = strongPhrases.some(p => text.toLowerCase().includes(p))
|
|
346
|
+
if (hasStrong) confidence += 0.1
|
|
347
|
+
|
|
348
|
+
// Length bonus (longer = more detailed = more confident)
|
|
349
|
+
if (text.length > 200) confidence += 0.05
|
|
350
|
+
if (text.length > 500) confidence += 0.05
|
|
351
|
+
|
|
352
|
+
return Math.min(confidence, 1.0)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Process text and optionally auto-save decision
|
|
357
|
+
*/
|
|
358
|
+
async processText(
|
|
359
|
+
text: string,
|
|
360
|
+
project: string,
|
|
361
|
+
autoSave: boolean = true
|
|
362
|
+
): Promise<DetectionResult> {
|
|
363
|
+
const decision = this.detectDecision(text, project)
|
|
364
|
+
|
|
365
|
+
if (!decision) {
|
|
366
|
+
return { detected: false, saved: false }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!autoSave || decision.confidence < 0.7) {
|
|
370
|
+
return {
|
|
371
|
+
detected: true,
|
|
372
|
+
decision,
|
|
373
|
+
saved: false
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Auto-save the decision
|
|
378
|
+
const decisionId = await this.autoSaveDecision(decision, project)
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
detected: true,
|
|
382
|
+
decision,
|
|
383
|
+
saved: !!decisionId,
|
|
384
|
+
decisionId: decisionId || undefined
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Auto-save decision if confidence is high enough
|
|
390
|
+
*/
|
|
391
|
+
async autoSaveDecision(
|
|
392
|
+
detected: DetectedDecision,
|
|
393
|
+
project: string
|
|
394
|
+
): Promise<string | null> {
|
|
395
|
+
if (detected.confidence < 0.7) {
|
|
396
|
+
this.logger.debug(
|
|
397
|
+
{ confidence: detected.confidence },
|
|
398
|
+
'Confidence too low for auto-save'
|
|
399
|
+
)
|
|
400
|
+
return null
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const decisionId = await this.memory.rememberDecision(
|
|
405
|
+
project,
|
|
406
|
+
detected.context,
|
|
407
|
+
detected.decision,
|
|
408
|
+
detected.reasoning,
|
|
409
|
+
{
|
|
410
|
+
alternatives: detected.alternatives,
|
|
411
|
+
tags: detected.tags
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
this.logger.info(
|
|
416
|
+
{ project, confidence: detected.confidence, decisionId },
|
|
417
|
+
'Decision auto-saved'
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return decisionId
|
|
421
|
+
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.logger.error({ error }, 'Failed to auto-save decision')
|
|
424
|
+
return null
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Batch process multiple texts
|
|
430
|
+
*/
|
|
431
|
+
async processBatch(
|
|
432
|
+
texts: Array<{ text: string; project: string }>,
|
|
433
|
+
autoSave: boolean = true
|
|
434
|
+
): Promise<DetectionResult[]> {
|
|
435
|
+
const results: DetectionResult[] = []
|
|
436
|
+
|
|
437
|
+
for (const { text, project } of texts) {
|
|
438
|
+
const result = await this.processText(text, project, autoSave)
|
|
439
|
+
results.push(result)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return results
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check if text likely contains a decision
|
|
447
|
+
*/
|
|
448
|
+
quickCheck(text: string): boolean {
|
|
449
|
+
const textLower = text.toLowerCase()
|
|
450
|
+
return this.DECISION_PHRASES.some(phrase => textLower.includes(phrase))
|
|
451
|
+
}
|
|
452
|
+
}
|