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,454 +1,454 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain Entity Extractor
|
|
3
|
-
* Phase 16: Extracts structured data from natural language messages
|
|
4
|
-
* for the unified brain() tool routing
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getVaultService, isServicesInitialized } from '@/server/services'
|
|
8
|
-
|
|
9
|
-
// Reuse phrase lists from decision detector
|
|
10
|
-
const DECISION_PHRASES = [
|
|
11
|
-
'i recommend',
|
|
12
|
-
'you should use',
|
|
13
|
-
'the best approach',
|
|
14
|
-
'i suggest',
|
|
15
|
-
'better to use',
|
|
16
|
-
'prefer using',
|
|
17
|
-
'go with',
|
|
18
|
-
'choose',
|
|
19
|
-
'instead of',
|
|
20
|
-
'the right choice',
|
|
21
|
-
'decided to',
|
|
22
|
-
"let's use",
|
|
23
|
-
'we will use',
|
|
24
|
-
'the solution is',
|
|
25
|
-
'implement using',
|
|
26
|
-
'going with',
|
|
27
|
-
'switching to',
|
|
28
|
-
'adopting',
|
|
29
|
-
'we chose',
|
|
30
|
-
'the plan is to'
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
const REASONING_PHRASES = [
|
|
34
|
-
'because',
|
|
35
|
-
'since',
|
|
36
|
-
'due to',
|
|
37
|
-
'as it',
|
|
38
|
-
'which provides',
|
|
39
|
-
'this allows',
|
|
40
|
-
'this ensures',
|
|
41
|
-
'given that',
|
|
42
|
-
'considering',
|
|
43
|
-
'the reason is',
|
|
44
|
-
'this way'
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
const ALTERNATIVE_PHRASES = [
|
|
48
|
-
'instead of',
|
|
49
|
-
'rather than',
|
|
50
|
-
'over',
|
|
51
|
-
' vs ',
|
|
52
|
-
'compared to',
|
|
53
|
-
'unlike',
|
|
54
|
-
'as opposed to'
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
// Compact tech dictionary for entity extraction (most common technologies)
|
|
58
|
-
// Full dictionary is in knowledge/entity-extractor.ts — we use a subset for speed
|
|
59
|
-
const COMMON_TECH: Set<string> = new Set([
|
|
60
|
-
'typescript', 'javascript', 'python', 'rust', 'go', 'java', 'ruby', 'php', 'swift', 'kotlin',
|
|
61
|
-
'react', 'vue', 'angular', 'svelte', 'nextjs', 'nuxt', 'remix', 'astro', 'solid',
|
|
62
|
-
'express', 'fastify', 'hono', 'nestjs', 'django', 'flask', 'fastapi', 'rails', 'spring',
|
|
63
|
-
'mongodb', 'redis', 'postgresql', 'postgres', 'mysql', 'sqlite', 'dynamodb', 'firebase', 'supabase',
|
|
64
|
-
'prisma', 'drizzle', 'typeorm', 'sequelize', 'chromadb', 'pinecone',
|
|
65
|
-
'docker', 'kubernetes', 'aws', 'gcp', 'azure', 'vercel', 'netlify',
|
|
66
|
-
'webpack', 'vite', 'esbuild', 'bun', 'deno', 'node', 'npm', 'yarn', 'pnpm',
|
|
67
|
-
'jest', 'vitest', 'cypress', 'playwright',
|
|
68
|
-
'tailwind', 'bootstrap', 'zod', 'trpc', 'graphql', 'rest',
|
|
69
|
-
'jwt', 'oauth', 'openai', 'anthropic', 'langchain',
|
|
70
|
-
'git', 'github', 'gitlab', 'eslint', 'prettier',
|
|
71
|
-
'zustand', 'redux', 'pinia', 'mobx', 'jotai', 'recoil',
|
|
72
|
-
'storybook', 'turborepo', 'nx',
|
|
73
|
-
'microservices', 'serverless', 'monolith', 'ssr', 'ssg', 'spa', 'pwa', 'mcp', 'rag'
|
|
74
|
-
])
|
|
75
|
-
|
|
76
|
-
// Aliases for common tech names
|
|
77
|
-
const TECH_ALIASES: Record<string, string> = {
|
|
78
|
-
'ts': 'typescript', 'js': 'javascript', 'py': 'python',
|
|
79
|
-
'react.js': 'react', 'reactjs': 'react', 'vue.js': 'vue', 'vuejs': 'vue',
|
|
80
|
-
'next.js': 'nextjs', 'nuxt.js': 'nuxt', 'nest.js': 'nestjs',
|
|
81
|
-
'express.js': 'express', 'node.js': 'node', 'nodejs': 'node',
|
|
82
|
-
'mongo': 'mongodb', 'pg': 'postgresql', 'k8s': 'kubernetes',
|
|
83
|
-
'tailwindcss': 'tailwind', 'tailwind-css': 'tailwind',
|
|
84
|
-
'gql': 'graphql', 'golang': 'go',
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Common English words that should never be treated as project names
|
|
88
|
-
const GENERIC_WORDS: Set<string> = new Set([
|
|
89
|
-
'small', 'large', 'big', 'new', 'old', 'good', 'bad', 'best', 'worst',
|
|
90
|
-
'simple', 'complex', 'easy', 'hard', 'fast', 'slow', 'quick', 'all',
|
|
91
|
-
'this', 'that', 'these', 'those', 'some', 'any', 'every', 'each',
|
|
92
|
-
'other', 'another', 'next', 'last', 'first', 'final', 'main', 'side',
|
|
93
|
-
'personal', 'general', 'specific', 'local', 'global', 'internal', 'external',
|
|
94
|
-
'current', 'future', 'past', 'recent', 'data', 'test', 'code', 'file',
|
|
95
|
-
'the', 'for', 'with', 'from', 'into', 'about', 'like', 'just', 'more',
|
|
96
|
-
'production', 'development', 'staging', 'projects', 'apps', 'tools',
|
|
97
|
-
'frontend', 'backend', 'fullstack', 'mobile', 'web', 'desktop', 'cli'
|
|
98
|
-
])
|
|
99
|
-
|
|
100
|
-
export interface BrainExtractedEntities {
|
|
101
|
-
project?: string
|
|
102
|
-
technologies: string[]
|
|
103
|
-
decision?: string
|
|
104
|
-
reasoning?: string
|
|
105
|
-
alternatives?: string
|
|
106
|
-
topic?: string
|
|
107
|
-
completedTask?: string
|
|
108
|
-
nextSteps?: string
|
|
109
|
-
original?: string
|
|
110
|
-
correction?: string
|
|
111
|
-
patternType?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export class BrainEntityExtractor {
|
|
115
|
-
/** SLM Upgrade: Optional inference router for model-based entity extraction */
|
|
116
|
-
private inferenceRouter:
|
|
117
|
-
|
|
118
|
-
/** SLM Upgrade: Set optional inference router for model-based NER */
|
|
119
|
-
setInferenceRouter(router:
|
|
120
|
-
this.inferenceRouter = router
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Extract all entities from a natural language message
|
|
125
|
-
*/
|
|
126
|
-
async extract(message: string, knownProject?: string): Promise<BrainExtractedEntities> {
|
|
127
|
-
const entities: BrainExtractedEntities = {
|
|
128
|
-
technologies: []
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Use provided project or try to detect from message
|
|
132
|
-
entities.project = knownProject || await this.extractProject(message)
|
|
133
|
-
entities.technologies = await this.extractTechnologies(message)
|
|
134
|
-
entities.topic = this.extractTopic(message)
|
|
135
|
-
|
|
136
|
-
// Extract decision components if present
|
|
137
|
-
const decisionParts = this.extractDecision(message)
|
|
138
|
-
if (decisionParts) {
|
|
139
|
-
entities.decision = decisionParts.decision
|
|
140
|
-
entities.reasoning = decisionParts.reasoning
|
|
141
|
-
entities.alternatives = decisionParts.alternatives
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Extract progress components
|
|
145
|
-
const progress = this.extractProgress(message)
|
|
146
|
-
if (progress) {
|
|
147
|
-
entities.completedTask = progress.completedTask
|
|
148
|
-
entities.nextSteps = progress.nextSteps
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Extract correction components
|
|
152
|
-
const correction = this.extractCorrection(message)
|
|
153
|
-
if (correction) {
|
|
154
|
-
entities.original = correction.original
|
|
155
|
-
entities.correction = correction.correction
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Extract pattern type
|
|
159
|
-
entities.patternType = this.extractPatternType(message)
|
|
160
|
-
|
|
161
|
-
return entities
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Extract project name from message
|
|
166
|
-
*/
|
|
167
|
-
private async extractProject(message: string): Promise<string | undefined> {
|
|
168
|
-
const lower = message.toLowerCase()
|
|
169
|
-
|
|
170
|
-
// Pattern: "in project X", "for project X", "on project X"
|
|
171
|
-
const projectPatterns = [
|
|
172
|
-
/(?:in|for|on)\s+(?:the\s+)?project\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
173
|
-
/project\s*:\s*["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
174
|
-
/(?:in|for|on)\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?\s+project/i,
|
|
175
|
-
// "working on X", "starting X", "resuming X"
|
|
176
|
-
/(?:working\s+on|starting|resuming|picking\s+up)\s+(?:the\s+)?["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
for (const pattern of projectPatterns) {
|
|
180
|
-
const match = message.match(pattern)
|
|
181
|
-
if (match && match[1]) {
|
|
182
|
-
const candidate = match[1].toLowerCase()
|
|
183
|
-
// Filter out common false positives: tech names, generic words, adjectives
|
|
184
|
-
if (
|
|
185
|
-
!COMMON_TECH.has(candidate) &&
|
|
186
|
-
!GENERIC_WORDS.has(candidate) &&
|
|
187
|
-
candidate.length > 2 &&
|
|
188
|
-
candidate.length < 50
|
|
189
|
-
) {
|
|
190
|
-
return candidate
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Try to match against known projects via vault service
|
|
196
|
-
if (isServicesInitialized()) {
|
|
197
|
-
try {
|
|
198
|
-
const vault = getVaultService()
|
|
199
|
-
const projects = await vault.listProjects()
|
|
200
|
-
for (const project of projects) {
|
|
201
|
-
if (lower.includes(project.toLowerCase())) {
|
|
202
|
-
return project
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
// Vault service not available, skip project matching
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return undefined
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Extract technology mentions.
|
|
215
|
-
* SLM Upgrade: tries model-based NER first if InferenceRouter is available,
|
|
216
|
-
* merges with dictionary fallback for comprehensive coverage.
|
|
217
|
-
*/
|
|
218
|
-
private async extractTechnologies(message: string): Promise<string[]> {
|
|
219
|
-
const found = new Set<string>()
|
|
220
|
-
|
|
221
|
-
// SLM: Try model-based entity extraction first
|
|
222
|
-
if (this.inferenceRouter) {
|
|
223
|
-
try {
|
|
224
|
-
const modelEntities = await this.inferenceRouter.extractEntities(message)
|
|
225
|
-
for (const entity of modelEntities) {
|
|
226
|
-
if (entity.type === 'technology') {
|
|
227
|
-
found.add(entity.normalizedName)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
// Model failed, fall through to dictionary
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Dictionary fallback (always runs — merges with model results)
|
|
236
|
-
const lower = message.toLowerCase()
|
|
237
|
-
const words = lower.split(/[\s,;:()[\]{}"'`|/\\]+/)
|
|
238
|
-
|
|
239
|
-
for (const word of words) {
|
|
240
|
-
const cleaned = word.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
|
|
241
|
-
if (cleaned.length < 2) continue
|
|
242
|
-
|
|
243
|
-
// Direct match
|
|
244
|
-
if (COMMON_TECH.has(cleaned)) {
|
|
245
|
-
found.add(cleaned)
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Alias match
|
|
250
|
-
const alias = TECH_ALIASES[cleaned]
|
|
251
|
-
if (alias) {
|
|
252
|
-
found.add(alias)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Check multi-word aliases
|
|
257
|
-
for (const [alias, normalized] of Object.entries(TECH_ALIASES)) {
|
|
258
|
-
if (alias.includes('.') || alias.includes('-')) {
|
|
259
|
-
if (lower.includes(alias)) {
|
|
260
|
-
found.add(normalized)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return Array.from(found)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Extract decision, reasoning, and alternatives
|
|
270
|
-
*/
|
|
271
|
-
private extractDecision(message: string): { decision?: string; reasoning?: string; alternatives?: string } | null {
|
|
272
|
-
const lower = message.toLowerCase()
|
|
273
|
-
|
|
274
|
-
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
275
|
-
if (!hasDecision) return null
|
|
276
|
-
|
|
277
|
-
// Find the decision phrase and extract text after it
|
|
278
|
-
let decision: string | undefined
|
|
279
|
-
for (const phrase of DECISION_PHRASES) {
|
|
280
|
-
const index = lower.indexOf(phrase)
|
|
281
|
-
if (index !== -1) {
|
|
282
|
-
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
283
|
-
// Take until reasoning or sentence end
|
|
284
|
-
const endIndex = this.findClauseEnd(afterPhrase, REASONING_PHRASES)
|
|
285
|
-
decision = afterPhrase.substring(0, endIndex).trim()
|
|
286
|
-
if (decision.length > 5) break
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Extract reasoning
|
|
291
|
-
let reasoning: string | undefined
|
|
292
|
-
for (const phrase of REASONING_PHRASES) {
|
|
293
|
-
const index = lower.indexOf(phrase)
|
|
294
|
-
if (index !== -1) {
|
|
295
|
-
const afterPhrase = message.substring(index).trim()
|
|
296
|
-
const endIndex = Math.min(afterPhrase.length, 300)
|
|
297
|
-
const sentenceEnd = afterPhrase.search(/\n\n/)
|
|
298
|
-
reasoning = afterPhrase.substring(0, sentenceEnd !== -1 ? sentenceEnd : endIndex).trim()
|
|
299
|
-
if (reasoning.length > 10) break
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Extract alternatives
|
|
304
|
-
let alternatives: string | undefined
|
|
305
|
-
for (const phrase of ALTERNATIVE_PHRASES) {
|
|
306
|
-
const index = lower.indexOf(phrase)
|
|
307
|
-
if (index !== -1) {
|
|
308
|
-
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
309
|
-
const endMatch = afterPhrase.match(/[.!?\n]/)
|
|
310
|
-
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
311
|
-
alternatives = afterPhrase.substring(0, endIndex).trim()
|
|
312
|
-
if (alternatives.length > 3) break
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!decision && !reasoning) return null
|
|
317
|
-
|
|
318
|
-
return { decision, reasoning, alternatives }
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Extract progress info: completed task and next steps
|
|
323
|
-
*/
|
|
324
|
-
private extractProgress(message: string): { completedTask?: string; nextSteps?: string } | null {
|
|
325
|
-
const completedPatterns = [
|
|
326
|
-
/(?:finished|completed|done with|implemented|built|created|added|fixed|resolved|shipped)\s+(.+?)(?:\.|,\s*next|$)/i,
|
|
327
|
-
]
|
|
328
|
-
|
|
329
|
-
const nextPatterns = [
|
|
330
|
-
/(?:next\s+(?:is|step|up|i'll|we'll|will\s+be)?|then\s+(?:i'll|we'll|will)|moving\s+(?:on\s+)?to)\s+(.+?)(?:\.|$)/i,
|
|
331
|
-
]
|
|
332
|
-
|
|
333
|
-
let completedTask: string | undefined
|
|
334
|
-
let nextSteps: string | undefined
|
|
335
|
-
|
|
336
|
-
for (const pattern of completedPatterns) {
|
|
337
|
-
const match = message.match(pattern)
|
|
338
|
-
if (match && match[1] && match[1].length > 3) {
|
|
339
|
-
completedTask = match[1].trim()
|
|
340
|
-
break
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
for (const pattern of nextPatterns) {
|
|
345
|
-
const match = message.match(pattern)
|
|
346
|
-
if (match && match[1] && match[1].length > 3) {
|
|
347
|
-
nextSteps = match[1].trim()
|
|
348
|
-
break
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!completedTask) return null
|
|
353
|
-
return { completedTask, nextSteps }
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Extract correction: original mistake and what should have been done
|
|
358
|
-
*/
|
|
359
|
-
private extractCorrection(message: string): { original?: string; correction?: string } | null {
|
|
360
|
-
const lower = message.toLowerCase()
|
|
361
|
-
|
|
362
|
-
const correctionIndicators = [
|
|
363
|
-
'the bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
364
|
-
'should have', 'should not have', "shouldn't have",
|
|
365
|
-
'lesson learned', "don't use", 'avoid using', 'never use',
|
|
366
|
-
'the fix is', 'the fix was', 'fixed by', 'solved by'
|
|
367
|
-
]
|
|
368
|
-
|
|
369
|
-
const hasCorrection = correctionIndicators.some(p => lower.includes(p))
|
|
370
|
-
if (!hasCorrection) return null
|
|
371
|
-
|
|
372
|
-
// Extract the original problem
|
|
373
|
-
let original: string | undefined
|
|
374
|
-
const problemPatterns = [
|
|
375
|
-
/(?:the\s+(?:bug|issue|problem|mistake)\s+was)\s+(.+?)(?:\.|,|;|$)/i,
|
|
376
|
-
/(?:should\s+(?:have|not\s+have)|shouldn't\s+have)\s+(.+?)(?:\.|,|;|$)/i
|
|
377
|
-
]
|
|
378
|
-
|
|
379
|
-
for (const pattern of problemPatterns) {
|
|
380
|
-
const match = message.match(pattern)
|
|
381
|
-
if (match && match[1] && match[1].length > 3) {
|
|
382
|
-
original = match[1].trim()
|
|
383
|
-
break
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Extract the correction
|
|
388
|
-
let correction: string | undefined
|
|
389
|
-
const fixPatterns = [
|
|
390
|
-
/(?:the\s+fix\s+(?:is|was)|fixed\s+by|solved\s+by|instead\s+(?:should|use))\s+(.+?)(?:\.|,|;|$)/i,
|
|
391
|
-
/(?:don't\s+use|avoid\s+using|never\s+use)\s+(.+?)(?:\.|,|;|$)/i
|
|
392
|
-
]
|
|
393
|
-
|
|
394
|
-
for (const pattern of fixPatterns) {
|
|
395
|
-
const match = message.match(pattern)
|
|
396
|
-
if (match && match[1] && match[1].length > 3) {
|
|
397
|
-
correction = match[1].trim()
|
|
398
|
-
break
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (!original && !correction) return null
|
|
403
|
-
return { original: original || message.slice(0, 200), correction }
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Extract pattern type from message
|
|
408
|
-
*/
|
|
409
|
-
private extractPatternType(message: string): BrainExtractedEntities['patternType'] {
|
|
410
|
-
const lower = message.toLowerCase()
|
|
411
|
-
if (lower.includes('anti-pattern') || lower.includes('antipattern')) return 'anti-pattern'
|
|
412
|
-
if (lower.includes('best practice') || lower.includes('best-practice')) return 'best-practice'
|
|
413
|
-
if (lower.includes('common issue') || lower.includes('common-issue') || lower.includes('common problem')) return 'common-issue'
|
|
414
|
-
if (lower.includes('pattern') || lower.includes('reusable solution') || lower.includes('reusable approach')) return 'solution'
|
|
415
|
-
return undefined
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Extract topic from message (main subject without noise words)
|
|
420
|
-
*/
|
|
421
|
-
private extractTopic(message: string): string | undefined {
|
|
422
|
-
// For short messages, the whole thing is the topic
|
|
423
|
-
if (message.length < 80) {
|
|
424
|
-
const cleaned = message.replace(/[?.!]+$/, '').trim()
|
|
425
|
-
if (cleaned.length > 3) return cleaned
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// For longer messages, extract the first meaningful clause
|
|
429
|
-
const sentences = message.split(/[.!?\n]+/).filter(s => s.trim().length > 3)
|
|
430
|
-
const first = sentences[0]
|
|
431
|
-
if (first) {
|
|
432
|
-
return first.trim().slice(0, 200)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return undefined
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Find where a clause ends (at a reasoning phrase or sentence boundary)
|
|
440
|
-
*/
|
|
441
|
-
private findClauseEnd(text: string, stopPhrases: string[]): number {
|
|
442
|
-
const lower = text.toLowerCase()
|
|
443
|
-
|
|
444
|
-
const phraseStart = stopPhrases.reduce((min, phrase) => {
|
|
445
|
-
const idx = lower.indexOf(phrase)
|
|
446
|
-
return idx !== -1 ? Math.min(min, idx) : min
|
|
447
|
-
}, text.length)
|
|
448
|
-
|
|
449
|
-
const sentenceEnd = text.search(/[.!?\n]/)
|
|
450
|
-
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
451
|
-
|
|
452
|
-
return Math.min(phraseStart, end, 200)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Brain Entity Extractor
|
|
3
|
+
* Phase 16: Extracts structured data from natural language messages
|
|
4
|
+
* for the unified brain() tool routing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getVaultService, isServicesInitialized } from '@/server/services'
|
|
8
|
+
|
|
9
|
+
// Reuse phrase lists from decision detector
|
|
10
|
+
const DECISION_PHRASES = [
|
|
11
|
+
'i recommend',
|
|
12
|
+
'you should use',
|
|
13
|
+
'the best approach',
|
|
14
|
+
'i suggest',
|
|
15
|
+
'better to use',
|
|
16
|
+
'prefer using',
|
|
17
|
+
'go with',
|
|
18
|
+
'choose',
|
|
19
|
+
'instead of',
|
|
20
|
+
'the right choice',
|
|
21
|
+
'decided to',
|
|
22
|
+
"let's use",
|
|
23
|
+
'we will use',
|
|
24
|
+
'the solution is',
|
|
25
|
+
'implement using',
|
|
26
|
+
'going with',
|
|
27
|
+
'switching to',
|
|
28
|
+
'adopting',
|
|
29
|
+
'we chose',
|
|
30
|
+
'the plan is to'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const REASONING_PHRASES = [
|
|
34
|
+
'because',
|
|
35
|
+
'since',
|
|
36
|
+
'due to',
|
|
37
|
+
'as it',
|
|
38
|
+
'which provides',
|
|
39
|
+
'this allows',
|
|
40
|
+
'this ensures',
|
|
41
|
+
'given that',
|
|
42
|
+
'considering',
|
|
43
|
+
'the reason is',
|
|
44
|
+
'this way'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const ALTERNATIVE_PHRASES = [
|
|
48
|
+
'instead of',
|
|
49
|
+
'rather than',
|
|
50
|
+
'over',
|
|
51
|
+
' vs ',
|
|
52
|
+
'compared to',
|
|
53
|
+
'unlike',
|
|
54
|
+
'as opposed to'
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
// Compact tech dictionary for entity extraction (most common technologies)
|
|
58
|
+
// Full dictionary is in knowledge/entity-extractor.ts — we use a subset for speed
|
|
59
|
+
const COMMON_TECH: Set<string> = new Set([
|
|
60
|
+
'typescript', 'javascript', 'python', 'rust', 'go', 'java', 'ruby', 'php', 'swift', 'kotlin',
|
|
61
|
+
'react', 'vue', 'angular', 'svelte', 'nextjs', 'nuxt', 'remix', 'astro', 'solid',
|
|
62
|
+
'express', 'fastify', 'hono', 'nestjs', 'django', 'flask', 'fastapi', 'rails', 'spring',
|
|
63
|
+
'mongodb', 'redis', 'postgresql', 'postgres', 'mysql', 'sqlite', 'dynamodb', 'firebase', 'supabase',
|
|
64
|
+
'prisma', 'drizzle', 'typeorm', 'sequelize', 'chromadb', 'pinecone',
|
|
65
|
+
'docker', 'kubernetes', 'aws', 'gcp', 'azure', 'vercel', 'netlify',
|
|
66
|
+
'webpack', 'vite', 'esbuild', 'bun', 'deno', 'node', 'npm', 'yarn', 'pnpm',
|
|
67
|
+
'jest', 'vitest', 'cypress', 'playwright',
|
|
68
|
+
'tailwind', 'bootstrap', 'zod', 'trpc', 'graphql', 'rest',
|
|
69
|
+
'jwt', 'oauth', 'openai', 'anthropic', 'langchain',
|
|
70
|
+
'git', 'github', 'gitlab', 'eslint', 'prettier',
|
|
71
|
+
'zustand', 'redux', 'pinia', 'mobx', 'jotai', 'recoil',
|
|
72
|
+
'storybook', 'turborepo', 'nx',
|
|
73
|
+
'microservices', 'serverless', 'monolith', 'ssr', 'ssg', 'spa', 'pwa', 'mcp', 'rag'
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
// Aliases for common tech names
|
|
77
|
+
const TECH_ALIASES: Record<string, string> = {
|
|
78
|
+
'ts': 'typescript', 'js': 'javascript', 'py': 'python',
|
|
79
|
+
'react.js': 'react', 'reactjs': 'react', 'vue.js': 'vue', 'vuejs': 'vue',
|
|
80
|
+
'next.js': 'nextjs', 'nuxt.js': 'nuxt', 'nest.js': 'nestjs',
|
|
81
|
+
'express.js': 'express', 'node.js': 'node', 'nodejs': 'node',
|
|
82
|
+
'mongo': 'mongodb', 'pg': 'postgresql', 'k8s': 'kubernetes',
|
|
83
|
+
'tailwindcss': 'tailwind', 'tailwind-css': 'tailwind',
|
|
84
|
+
'gql': 'graphql', 'golang': 'go',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Common English words that should never be treated as project names
|
|
88
|
+
const GENERIC_WORDS: Set<string> = new Set([
|
|
89
|
+
'small', 'large', 'big', 'new', 'old', 'good', 'bad', 'best', 'worst',
|
|
90
|
+
'simple', 'complex', 'easy', 'hard', 'fast', 'slow', 'quick', 'all',
|
|
91
|
+
'this', 'that', 'these', 'those', 'some', 'any', 'every', 'each',
|
|
92
|
+
'other', 'another', 'next', 'last', 'first', 'final', 'main', 'side',
|
|
93
|
+
'personal', 'general', 'specific', 'local', 'global', 'internal', 'external',
|
|
94
|
+
'current', 'future', 'past', 'recent', 'data', 'test', 'code', 'file',
|
|
95
|
+
'the', 'for', 'with', 'from', 'into', 'about', 'like', 'just', 'more',
|
|
96
|
+
'production', 'development', 'staging', 'projects', 'apps', 'tools',
|
|
97
|
+
'frontend', 'backend', 'fullstack', 'mobile', 'web', 'desktop', 'cli'
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
export interface BrainExtractedEntities {
|
|
101
|
+
project?: string
|
|
102
|
+
technologies: string[]
|
|
103
|
+
decision?: string
|
|
104
|
+
reasoning?: string
|
|
105
|
+
alternatives?: string
|
|
106
|
+
topic?: string
|
|
107
|
+
completedTask?: string
|
|
108
|
+
nextSteps?: string
|
|
109
|
+
original?: string
|
|
110
|
+
correction?: string
|
|
111
|
+
patternType?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class BrainEntityExtractor {
|
|
115
|
+
/** SLM Upgrade: Optional inference router for model-based entity extraction */
|
|
116
|
+
private inferenceRouter: import('@/intelligence/inference-router').InferenceRouter | null = null
|
|
117
|
+
|
|
118
|
+
/** SLM Upgrade: Set optional inference router for model-based NER */
|
|
119
|
+
setInferenceRouter(router: import('@/intelligence/inference-router').InferenceRouter): void {
|
|
120
|
+
this.inferenceRouter = router
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract all entities from a natural language message
|
|
125
|
+
*/
|
|
126
|
+
async extract(message: string, knownProject?: string): Promise<BrainExtractedEntities> {
|
|
127
|
+
const entities: BrainExtractedEntities = {
|
|
128
|
+
technologies: []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Use provided project or try to detect from message
|
|
132
|
+
entities.project = knownProject || await this.extractProject(message)
|
|
133
|
+
entities.technologies = await this.extractTechnologies(message)
|
|
134
|
+
entities.topic = this.extractTopic(message)
|
|
135
|
+
|
|
136
|
+
// Extract decision components if present
|
|
137
|
+
const decisionParts = this.extractDecision(message)
|
|
138
|
+
if (decisionParts) {
|
|
139
|
+
entities.decision = decisionParts.decision
|
|
140
|
+
entities.reasoning = decisionParts.reasoning
|
|
141
|
+
entities.alternatives = decisionParts.alternatives
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract progress components
|
|
145
|
+
const progress = this.extractProgress(message)
|
|
146
|
+
if (progress) {
|
|
147
|
+
entities.completedTask = progress.completedTask
|
|
148
|
+
entities.nextSteps = progress.nextSteps
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Extract correction components
|
|
152
|
+
const correction = this.extractCorrection(message)
|
|
153
|
+
if (correction) {
|
|
154
|
+
entities.original = correction.original
|
|
155
|
+
entities.correction = correction.correction
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract pattern type
|
|
159
|
+
entities.patternType = this.extractPatternType(message)
|
|
160
|
+
|
|
161
|
+
return entities
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract project name from message
|
|
166
|
+
*/
|
|
167
|
+
private async extractProject(message: string): Promise<string | undefined> {
|
|
168
|
+
const lower = message.toLowerCase()
|
|
169
|
+
|
|
170
|
+
// Pattern: "in project X", "for project X", "on project X"
|
|
171
|
+
const projectPatterns = [
|
|
172
|
+
/(?:in|for|on)\s+(?:the\s+)?project\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
173
|
+
/project\s*:\s*["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
174
|
+
/(?:in|for|on)\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?\s+project/i,
|
|
175
|
+
// "working on X", "starting X", "resuming X"
|
|
176
|
+
/(?:working\s+on|starting|resuming|picking\s+up)\s+(?:the\s+)?["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
for (const pattern of projectPatterns) {
|
|
180
|
+
const match = message.match(pattern)
|
|
181
|
+
if (match && match[1]) {
|
|
182
|
+
const candidate = match[1].toLowerCase()
|
|
183
|
+
// Filter out common false positives: tech names, generic words, adjectives
|
|
184
|
+
if (
|
|
185
|
+
!COMMON_TECH.has(candidate) &&
|
|
186
|
+
!GENERIC_WORDS.has(candidate) &&
|
|
187
|
+
candidate.length > 2 &&
|
|
188
|
+
candidate.length < 50
|
|
189
|
+
) {
|
|
190
|
+
return candidate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Try to match against known projects via vault service
|
|
196
|
+
if (isServicesInitialized()) {
|
|
197
|
+
try {
|
|
198
|
+
const vault = getVaultService()
|
|
199
|
+
const projects = await vault.listProjects()
|
|
200
|
+
for (const project of projects) {
|
|
201
|
+
if (lower.includes(project.toLowerCase())) {
|
|
202
|
+
return project
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Vault service not available, skip project matching
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract technology mentions.
|
|
215
|
+
* SLM Upgrade: tries model-based NER first if InferenceRouter is available,
|
|
216
|
+
* merges with dictionary fallback for comprehensive coverage.
|
|
217
|
+
*/
|
|
218
|
+
private async extractTechnologies(message: string): Promise<string[]> {
|
|
219
|
+
const found = new Set<string>()
|
|
220
|
+
|
|
221
|
+
// SLM: Try model-based entity extraction first
|
|
222
|
+
if (this.inferenceRouter) {
|
|
223
|
+
try {
|
|
224
|
+
const modelEntities = await this.inferenceRouter.extractEntities(message)
|
|
225
|
+
for (const entity of modelEntities) {
|
|
226
|
+
if (entity.type === 'technology') {
|
|
227
|
+
found.add(entity.normalizedName)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Model failed, fall through to dictionary
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Dictionary fallback (always runs — merges with model results)
|
|
236
|
+
const lower = message.toLowerCase()
|
|
237
|
+
const words = lower.split(/[\s,;:()[\]{}"'`|/\\]+/)
|
|
238
|
+
|
|
239
|
+
for (const word of words) {
|
|
240
|
+
const cleaned = word.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
|
|
241
|
+
if (cleaned.length < 2) continue
|
|
242
|
+
|
|
243
|
+
// Direct match
|
|
244
|
+
if (COMMON_TECH.has(cleaned)) {
|
|
245
|
+
found.add(cleaned)
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Alias match
|
|
250
|
+
const alias = TECH_ALIASES[cleaned]
|
|
251
|
+
if (alias) {
|
|
252
|
+
found.add(alias)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check multi-word aliases
|
|
257
|
+
for (const [alias, normalized] of Object.entries(TECH_ALIASES)) {
|
|
258
|
+
if (alias.includes('.') || alias.includes('-')) {
|
|
259
|
+
if (lower.includes(alias)) {
|
|
260
|
+
found.add(normalized)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return Array.from(found)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract decision, reasoning, and alternatives
|
|
270
|
+
*/
|
|
271
|
+
private extractDecision(message: string): { decision?: string; reasoning?: string; alternatives?: string } | null {
|
|
272
|
+
const lower = message.toLowerCase()
|
|
273
|
+
|
|
274
|
+
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
275
|
+
if (!hasDecision) return null
|
|
276
|
+
|
|
277
|
+
// Find the decision phrase and extract text after it
|
|
278
|
+
let decision: string | undefined
|
|
279
|
+
for (const phrase of DECISION_PHRASES) {
|
|
280
|
+
const index = lower.indexOf(phrase)
|
|
281
|
+
if (index !== -1) {
|
|
282
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
283
|
+
// Take until reasoning or sentence end
|
|
284
|
+
const endIndex = this.findClauseEnd(afterPhrase, REASONING_PHRASES)
|
|
285
|
+
decision = afterPhrase.substring(0, endIndex).trim()
|
|
286
|
+
if (decision.length > 5) break
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Extract reasoning
|
|
291
|
+
let reasoning: string | undefined
|
|
292
|
+
for (const phrase of REASONING_PHRASES) {
|
|
293
|
+
const index = lower.indexOf(phrase)
|
|
294
|
+
if (index !== -1) {
|
|
295
|
+
const afterPhrase = message.substring(index).trim()
|
|
296
|
+
const endIndex = Math.min(afterPhrase.length, 300)
|
|
297
|
+
const sentenceEnd = afterPhrase.search(/\n\n/)
|
|
298
|
+
reasoning = afterPhrase.substring(0, sentenceEnd !== -1 ? sentenceEnd : endIndex).trim()
|
|
299
|
+
if (reasoning.length > 10) break
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract alternatives
|
|
304
|
+
let alternatives: string | undefined
|
|
305
|
+
for (const phrase of ALTERNATIVE_PHRASES) {
|
|
306
|
+
const index = lower.indexOf(phrase)
|
|
307
|
+
if (index !== -1) {
|
|
308
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
309
|
+
const endMatch = afterPhrase.match(/[.!?\n]/)
|
|
310
|
+
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
311
|
+
alternatives = afterPhrase.substring(0, endIndex).trim()
|
|
312
|
+
if (alternatives.length > 3) break
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!decision && !reasoning) return null
|
|
317
|
+
|
|
318
|
+
return { decision, reasoning, alternatives }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract progress info: completed task and next steps
|
|
323
|
+
*/
|
|
324
|
+
private extractProgress(message: string): { completedTask?: string; nextSteps?: string } | null {
|
|
325
|
+
const completedPatterns = [
|
|
326
|
+
/(?:finished|completed|done with|implemented|built|created|added|fixed|resolved|shipped)\s+(.+?)(?:\.|,\s*next|$)/i,
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
const nextPatterns = [
|
|
330
|
+
/(?:next\s+(?:is|step|up|i'll|we'll|will\s+be)?|then\s+(?:i'll|we'll|will)|moving\s+(?:on\s+)?to)\s+(.+?)(?:\.|$)/i,
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
let completedTask: string | undefined
|
|
334
|
+
let nextSteps: string | undefined
|
|
335
|
+
|
|
336
|
+
for (const pattern of completedPatterns) {
|
|
337
|
+
const match = message.match(pattern)
|
|
338
|
+
if (match && match[1] && match[1].length > 3) {
|
|
339
|
+
completedTask = match[1].trim()
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const pattern of nextPatterns) {
|
|
345
|
+
const match = message.match(pattern)
|
|
346
|
+
if (match && match[1] && match[1].length > 3) {
|
|
347
|
+
nextSteps = match[1].trim()
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!completedTask) return null
|
|
353
|
+
return { completedTask, nextSteps }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Extract correction: original mistake and what should have been done
|
|
358
|
+
*/
|
|
359
|
+
private extractCorrection(message: string): { original?: string; correction?: string } | null {
|
|
360
|
+
const lower = message.toLowerCase()
|
|
361
|
+
|
|
362
|
+
const correctionIndicators = [
|
|
363
|
+
'the bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
364
|
+
'should have', 'should not have', "shouldn't have",
|
|
365
|
+
'lesson learned', "don't use", 'avoid using', 'never use',
|
|
366
|
+
'the fix is', 'the fix was', 'fixed by', 'solved by'
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
const hasCorrection = correctionIndicators.some(p => lower.includes(p))
|
|
370
|
+
if (!hasCorrection) return null
|
|
371
|
+
|
|
372
|
+
// Extract the original problem
|
|
373
|
+
let original: string | undefined
|
|
374
|
+
const problemPatterns = [
|
|
375
|
+
/(?:the\s+(?:bug|issue|problem|mistake)\s+was)\s+(.+?)(?:\.|,|;|$)/i,
|
|
376
|
+
/(?:should\s+(?:have|not\s+have)|shouldn't\s+have)\s+(.+?)(?:\.|,|;|$)/i
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
for (const pattern of problemPatterns) {
|
|
380
|
+
const match = message.match(pattern)
|
|
381
|
+
if (match && match[1] && match[1].length > 3) {
|
|
382
|
+
original = match[1].trim()
|
|
383
|
+
break
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Extract the correction
|
|
388
|
+
let correction: string | undefined
|
|
389
|
+
const fixPatterns = [
|
|
390
|
+
/(?:the\s+fix\s+(?:is|was)|fixed\s+by|solved\s+by|instead\s+(?:should|use))\s+(.+?)(?:\.|,|;|$)/i,
|
|
391
|
+
/(?:don't\s+use|avoid\s+using|never\s+use)\s+(.+?)(?:\.|,|;|$)/i
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
for (const pattern of fixPatterns) {
|
|
395
|
+
const match = message.match(pattern)
|
|
396
|
+
if (match && match[1] && match[1].length > 3) {
|
|
397
|
+
correction = match[1].trim()
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!original && !correction) return null
|
|
403
|
+
return { original: original || message.slice(0, 200), correction }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Extract pattern type from message
|
|
408
|
+
*/
|
|
409
|
+
private extractPatternType(message: string): BrainExtractedEntities['patternType'] {
|
|
410
|
+
const lower = message.toLowerCase()
|
|
411
|
+
if (lower.includes('anti-pattern') || lower.includes('antipattern')) return 'anti-pattern'
|
|
412
|
+
if (lower.includes('best practice') || lower.includes('best-practice')) return 'best-practice'
|
|
413
|
+
if (lower.includes('common issue') || lower.includes('common-issue') || lower.includes('common problem')) return 'common-issue'
|
|
414
|
+
if (lower.includes('pattern') || lower.includes('reusable solution') || lower.includes('reusable approach')) return 'solution'
|
|
415
|
+
return undefined
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Extract topic from message (main subject without noise words)
|
|
420
|
+
*/
|
|
421
|
+
private extractTopic(message: string): string | undefined {
|
|
422
|
+
// For short messages, the whole thing is the topic
|
|
423
|
+
if (message.length < 80) {
|
|
424
|
+
const cleaned = message.replace(/[?.!]+$/, '').trim()
|
|
425
|
+
if (cleaned.length > 3) return cleaned
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// For longer messages, extract the first meaningful clause
|
|
429
|
+
const sentences = message.split(/[.!?\n]+/).filter(s => s.trim().length > 3)
|
|
430
|
+
const first = sentences[0]
|
|
431
|
+
if (first) {
|
|
432
|
+
return first.trim().slice(0, 200)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return undefined
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Find where a clause ends (at a reasoning phrase or sentence boundary)
|
|
440
|
+
*/
|
|
441
|
+
private findClauseEnd(text: string, stopPhrases: string[]): number {
|
|
442
|
+
const lower = text.toLowerCase()
|
|
443
|
+
|
|
444
|
+
const phraseStart = stopPhrases.reduce((min, phrase) => {
|
|
445
|
+
const idx = lower.indexOf(phrase)
|
|
446
|
+
return idx !== -1 ? Math.min(min, idx) : min
|
|
447
|
+
}, text.length)
|
|
448
|
+
|
|
449
|
+
const sentenceEnd = text.search(/[.!?\n]/)
|
|
450
|
+
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
451
|
+
|
|
452
|
+
return Math.min(phraseStart, end, 200)
|
|
453
|
+
}
|
|
454
|
+
}
|