claude-brain 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -0
- package/package.json +2 -1
- package/packs/backend/node.json +173 -0
- package/packs/core/javascript.json +176 -0
- package/packs/core/typescript.json +222 -0
- package/packs/frontend/react.json +254 -0
- package/packs/meta/testing.json +172 -0
- package/src/cli/bin.ts +14 -0
- package/src/cli/commands/chroma.ts +53 -17
- package/src/cli/commands/hooks.ts +214 -0
- package/src/cli/commands/pack.ts +197 -0
- package/src/cli/commands/serve.ts +34 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +85 -2
- package/src/hooks/brain-hook.ts +110 -0
- package/src/hooks/capture.ts +161 -0
- package/src/hooks/deduplicator.ts +72 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/installer.ts +181 -0
- package/src/hooks/passive-classifier.ts +366 -0
- package/src/hooks/queue.ts +122 -0
- package/src/hooks/session-tracker.ts +199 -0
- package/src/hooks/types.ts +47 -0
- package/src/memory/chroma/client.ts +1 -1
- package/src/memory/chroma/index.ts +1 -1
- package/src/memory/chroma/store.ts +29 -9
- package/src/memory/index.ts +1 -0
- package/src/memory/store.ts +1 -0
- package/src/packs/index.ts +9 -0
- package/src/packs/loader.ts +134 -0
- package/src/packs/manager.ts +204 -0
- package/src/packs/ranker.ts +78 -0
- package/src/packs/types.ts +81 -0
- package/src/routing/entity-extractor.ts +410 -0
- package/src/routing/intent-classifier.ts +229 -0
- package/src/routing/response-filter.ts +221 -0
- package/src/routing/router.ts +671 -0
- package/src/server/handlers/call-tool.ts +7 -0
- package/src/server/handlers/list-tools.ts +22 -5
- package/src/server/handlers/tools/brain.ts +85 -0
- package/src/server/handlers/tools/init-project.ts +47 -0
- package/src/server/handlers/tools/schemas.ts +12 -0
- package/src/server/http-api.ts +188 -0
- package/src/tools/registry.ts +9 -0
- package/src/tools/schemas.ts +33 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18: Knowledge Pack Types & Schemas
|
|
3
|
+
* Defines the format for pre-seeded knowledge packs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
/** Valid entry types in a knowledge pack */
|
|
9
|
+
export const PackEntryTypeSchema = z.enum([
|
|
10
|
+
'pattern',
|
|
11
|
+
'anti-pattern',
|
|
12
|
+
'best-practice',
|
|
13
|
+
'common-issue',
|
|
14
|
+
'decision-template'
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export type PackEntryType = z.infer<typeof PackEntryTypeSchema>
|
|
18
|
+
|
|
19
|
+
/** A single entry in a knowledge pack */
|
|
20
|
+
export const PackEntrySchema = z.object({
|
|
21
|
+
type: PackEntryTypeSchema,
|
|
22
|
+
category: z.string().min(1),
|
|
23
|
+
title: z.string().min(1),
|
|
24
|
+
content: z.string().min(1),
|
|
25
|
+
confidence: z.number().min(0).max(1).default(0.85),
|
|
26
|
+
tags: z.array(z.string()).default([]),
|
|
27
|
+
example: z.string().optional()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export type PackEntry = z.infer<typeof PackEntrySchema>
|
|
31
|
+
|
|
32
|
+
/** A complete knowledge pack with metadata */
|
|
33
|
+
export const KnowledgePackSchema = z.object({
|
|
34
|
+
id: z.string().min(1),
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver'),
|
|
37
|
+
stack: z.array(z.string()).min(1),
|
|
38
|
+
description: z.string().min(1),
|
|
39
|
+
author: z.string().default('claude-brain'),
|
|
40
|
+
entries: z.array(PackEntrySchema).min(1),
|
|
41
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export type KnowledgePack = z.infer<typeof KnowledgePackSchema>
|
|
45
|
+
|
|
46
|
+
/** Tracks which packs have been loaded for a project */
|
|
47
|
+
export interface PackManifestEntry {
|
|
48
|
+
packId: string
|
|
49
|
+
version: string
|
|
50
|
+
entriesLoaded: number
|
|
51
|
+
loadedAt: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PackManifest {
|
|
55
|
+
project: string
|
|
56
|
+
packs: PackManifestEntry[]
|
|
57
|
+
lastUpdated: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Result of loading packs for a project */
|
|
61
|
+
export interface PackLoadResult {
|
|
62
|
+
packsLoaded: number
|
|
63
|
+
entriesLoaded: number
|
|
64
|
+
packDetails: Array<{
|
|
65
|
+
packId: string
|
|
66
|
+
name: string
|
|
67
|
+
entriesLoaded: number
|
|
68
|
+
}>
|
|
69
|
+
skipped: Array<{
|
|
70
|
+
packId: string
|
|
71
|
+
reason: string
|
|
72
|
+
}>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Maps pack entry types to the storage pattern_type values */
|
|
76
|
+
export const ENTRY_TYPE_TO_PATTERN_TYPE: Record<string, 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'> = {
|
|
77
|
+
'pattern': 'solution',
|
|
78
|
+
'anti-pattern': 'anti-pattern',
|
|
79
|
+
'best-practice': 'best-practice',
|
|
80
|
+
'common-issue': 'common-issue'
|
|
81
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
export interface BrainExtractedEntities {
|
|
88
|
+
project?: string
|
|
89
|
+
technologies: string[]
|
|
90
|
+
decision?: string
|
|
91
|
+
reasoning?: string
|
|
92
|
+
alternatives?: string
|
|
93
|
+
topic?: string
|
|
94
|
+
completedTask?: string
|
|
95
|
+
nextSteps?: string
|
|
96
|
+
original?: string
|
|
97
|
+
correction?: string
|
|
98
|
+
patternType?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class BrainEntityExtractor {
|
|
102
|
+
/**
|
|
103
|
+
* Extract all entities from a natural language message
|
|
104
|
+
*/
|
|
105
|
+
async extract(message: string, knownProject?: string): Promise<BrainExtractedEntities> {
|
|
106
|
+
const entities: BrainExtractedEntities = {
|
|
107
|
+
technologies: []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use provided project or try to detect from message
|
|
111
|
+
entities.project = knownProject || await this.extractProject(message)
|
|
112
|
+
entities.technologies = this.extractTechnologies(message)
|
|
113
|
+
entities.topic = this.extractTopic(message)
|
|
114
|
+
|
|
115
|
+
// Extract decision components if present
|
|
116
|
+
const decisionParts = this.extractDecision(message)
|
|
117
|
+
if (decisionParts) {
|
|
118
|
+
entities.decision = decisionParts.decision
|
|
119
|
+
entities.reasoning = decisionParts.reasoning
|
|
120
|
+
entities.alternatives = decisionParts.alternatives
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract progress components
|
|
124
|
+
const progress = this.extractProgress(message)
|
|
125
|
+
if (progress) {
|
|
126
|
+
entities.completedTask = progress.completedTask
|
|
127
|
+
entities.nextSteps = progress.nextSteps
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract correction components
|
|
131
|
+
const correction = this.extractCorrection(message)
|
|
132
|
+
if (correction) {
|
|
133
|
+
entities.original = correction.original
|
|
134
|
+
entities.correction = correction.correction
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract pattern type
|
|
138
|
+
entities.patternType = this.extractPatternType(message)
|
|
139
|
+
|
|
140
|
+
return entities
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract project name from message
|
|
145
|
+
*/
|
|
146
|
+
private async extractProject(message: string): Promise<string | undefined> {
|
|
147
|
+
const lower = message.toLowerCase()
|
|
148
|
+
|
|
149
|
+
// Pattern: "in project X", "for project X", "on project X"
|
|
150
|
+
const projectPatterns = [
|
|
151
|
+
/(?:in|for|on)\s+(?:the\s+)?project\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
152
|
+
/project\s*:\s*["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
153
|
+
/(?:in|for|on)\s+["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?\s+project/i,
|
|
154
|
+
// "working on X", "starting X", "resuming X"
|
|
155
|
+
/(?:working\s+on|starting|resuming|picking\s+up)\s+(?:the\s+)?["']?([a-z0-9][a-z0-9-]*[a-z0-9])["']?/i,
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
for (const pattern of projectPatterns) {
|
|
159
|
+
const match = message.match(pattern)
|
|
160
|
+
if (match && match[1]) {
|
|
161
|
+
const candidate = match[1].toLowerCase()
|
|
162
|
+
// Filter out common false positives
|
|
163
|
+
if (!COMMON_TECH.has(candidate) && candidate.length > 1 && candidate.length < 50) {
|
|
164
|
+
return candidate
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try to match against known projects via vault service
|
|
170
|
+
if (isServicesInitialized()) {
|
|
171
|
+
try {
|
|
172
|
+
const vault = getVaultService()
|
|
173
|
+
const projects = await vault.listProjects()
|
|
174
|
+
for (const project of projects) {
|
|
175
|
+
if (lower.includes(project.toLowerCase())) {
|
|
176
|
+
return project
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Vault service not available, skip project matching
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract technology mentions
|
|
189
|
+
*/
|
|
190
|
+
private extractTechnologies(message: string): string[] {
|
|
191
|
+
const lower = message.toLowerCase()
|
|
192
|
+
const words = lower.split(/[\s,;:()[\]{}"'`|/\\]+/)
|
|
193
|
+
const found = new Set<string>()
|
|
194
|
+
|
|
195
|
+
for (const word of words) {
|
|
196
|
+
const cleaned = word.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '')
|
|
197
|
+
if (cleaned.length < 2) continue
|
|
198
|
+
|
|
199
|
+
// Direct match
|
|
200
|
+
if (COMMON_TECH.has(cleaned)) {
|
|
201
|
+
found.add(cleaned)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Alias match
|
|
206
|
+
const alias = TECH_ALIASES[cleaned]
|
|
207
|
+
if (alias) {
|
|
208
|
+
found.add(alias)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check multi-word aliases
|
|
213
|
+
for (const [alias, normalized] of Object.entries(TECH_ALIASES)) {
|
|
214
|
+
if (alias.includes('.') || alias.includes('-')) {
|
|
215
|
+
if (lower.includes(alias)) {
|
|
216
|
+
found.add(normalized)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Array.from(found)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Extract decision, reasoning, and alternatives
|
|
226
|
+
*/
|
|
227
|
+
private extractDecision(message: string): { decision?: string; reasoning?: string; alternatives?: string } | null {
|
|
228
|
+
const lower = message.toLowerCase()
|
|
229
|
+
|
|
230
|
+
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
231
|
+
if (!hasDecision) return null
|
|
232
|
+
|
|
233
|
+
// Find the decision phrase and extract text after it
|
|
234
|
+
let decision: string | undefined
|
|
235
|
+
for (const phrase of DECISION_PHRASES) {
|
|
236
|
+
const index = lower.indexOf(phrase)
|
|
237
|
+
if (index !== -1) {
|
|
238
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
239
|
+
// Take until reasoning or sentence end
|
|
240
|
+
const endIndex = this.findClauseEnd(afterPhrase, REASONING_PHRASES)
|
|
241
|
+
decision = afterPhrase.substring(0, endIndex).trim()
|
|
242
|
+
if (decision.length > 5) break
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Extract reasoning
|
|
247
|
+
let reasoning: string | undefined
|
|
248
|
+
for (const phrase of REASONING_PHRASES) {
|
|
249
|
+
const index = lower.indexOf(phrase)
|
|
250
|
+
if (index !== -1) {
|
|
251
|
+
const afterPhrase = message.substring(index).trim()
|
|
252
|
+
const endIndex = Math.min(afterPhrase.length, 300)
|
|
253
|
+
const sentenceEnd = afterPhrase.search(/\n\n/)
|
|
254
|
+
reasoning = afterPhrase.substring(0, sentenceEnd !== -1 ? sentenceEnd : endIndex).trim()
|
|
255
|
+
if (reasoning.length > 10) break
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Extract alternatives
|
|
260
|
+
let alternatives: string | undefined
|
|
261
|
+
for (const phrase of ALTERNATIVE_PHRASES) {
|
|
262
|
+
const index = lower.indexOf(phrase)
|
|
263
|
+
if (index !== -1) {
|
|
264
|
+
const afterPhrase = message.substring(index + phrase.length).trim()
|
|
265
|
+
const endMatch = afterPhrase.match(/[.!?\n]/)
|
|
266
|
+
const endIndex = endMatch ? endMatch.index! + 1 : Math.min(afterPhrase.length, 150)
|
|
267
|
+
alternatives = afterPhrase.substring(0, endIndex).trim()
|
|
268
|
+
if (alternatives.length > 3) break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!decision && !reasoning) return null
|
|
273
|
+
|
|
274
|
+
return { decision, reasoning, alternatives }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extract progress info: completed task and next steps
|
|
279
|
+
*/
|
|
280
|
+
private extractProgress(message: string): { completedTask?: string; nextSteps?: string } | null {
|
|
281
|
+
const completedPatterns = [
|
|
282
|
+
/(?:finished|completed|done with|implemented|built|created|added|fixed|resolved|shipped)\s+(.+?)(?:\.|,\s*next|$)/i,
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const nextPatterns = [
|
|
286
|
+
/(?: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,
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
let completedTask: string | undefined
|
|
290
|
+
let nextSteps: string | undefined
|
|
291
|
+
|
|
292
|
+
for (const pattern of completedPatterns) {
|
|
293
|
+
const match = message.match(pattern)
|
|
294
|
+
if (match && match[1] && match[1].length > 3) {
|
|
295
|
+
completedTask = match[1].trim()
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const pattern of nextPatterns) {
|
|
301
|
+
const match = message.match(pattern)
|
|
302
|
+
if (match && match[1] && match[1].length > 3) {
|
|
303
|
+
nextSteps = match[1].trim()
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!completedTask) return null
|
|
309
|
+
return { completedTask, nextSteps }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Extract correction: original mistake and what should have been done
|
|
314
|
+
*/
|
|
315
|
+
private extractCorrection(message: string): { original?: string; correction?: string } | null {
|
|
316
|
+
const lower = message.toLowerCase()
|
|
317
|
+
|
|
318
|
+
const correctionIndicators = [
|
|
319
|
+
'the bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
320
|
+
'should have', 'should not have', "shouldn't have",
|
|
321
|
+
'lesson learned', "don't use", 'avoid using', 'never use',
|
|
322
|
+
'the fix is', 'the fix was', 'fixed by', 'solved by'
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
const hasCorrection = correctionIndicators.some(p => lower.includes(p))
|
|
326
|
+
if (!hasCorrection) return null
|
|
327
|
+
|
|
328
|
+
// Extract the original problem
|
|
329
|
+
let original: string | undefined
|
|
330
|
+
const problemPatterns = [
|
|
331
|
+
/(?:the\s+(?:bug|issue|problem|mistake)\s+was)\s+(.+?)(?:\.|,|;|$)/i,
|
|
332
|
+
/(?:should\s+(?:have|not\s+have)|shouldn't\s+have)\s+(.+?)(?:\.|,|;|$)/i
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
for (const pattern of problemPatterns) {
|
|
336
|
+
const match = message.match(pattern)
|
|
337
|
+
if (match && match[1] && match[1].length > 3) {
|
|
338
|
+
original = match[1].trim()
|
|
339
|
+
break
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Extract the correction
|
|
344
|
+
let correction: string | undefined
|
|
345
|
+
const fixPatterns = [
|
|
346
|
+
/(?:the\s+fix\s+(?:is|was)|fixed\s+by|solved\s+by|instead\s+(?:should|use))\s+(.+?)(?:\.|,|;|$)/i,
|
|
347
|
+
/(?:don't\s+use|avoid\s+using|never\s+use)\s+(.+?)(?:\.|,|;|$)/i
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for (const pattern of fixPatterns) {
|
|
351
|
+
const match = message.match(pattern)
|
|
352
|
+
if (match && match[1] && match[1].length > 3) {
|
|
353
|
+
correction = match[1].trim()
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!original && !correction) return null
|
|
359
|
+
return { original: original || message.slice(0, 200), correction }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Extract pattern type from message
|
|
364
|
+
*/
|
|
365
|
+
private extractPatternType(message: string): BrainExtractedEntities['patternType'] {
|
|
366
|
+
const lower = message.toLowerCase()
|
|
367
|
+
if (lower.includes('anti-pattern') || lower.includes('antipattern')) return 'anti-pattern'
|
|
368
|
+
if (lower.includes('best practice') || lower.includes('best-practice')) return 'best-practice'
|
|
369
|
+
if (lower.includes('common issue') || lower.includes('common-issue') || lower.includes('common problem')) return 'common-issue'
|
|
370
|
+
if (lower.includes('pattern') || lower.includes('reusable solution') || lower.includes('reusable approach')) return 'solution'
|
|
371
|
+
return undefined
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Extract topic from message (main subject without noise words)
|
|
376
|
+
*/
|
|
377
|
+
private extractTopic(message: string): string | undefined {
|
|
378
|
+
// For short messages, the whole thing is the topic
|
|
379
|
+
if (message.length < 80) {
|
|
380
|
+
const cleaned = message.replace(/[?.!]+$/, '').trim()
|
|
381
|
+
if (cleaned.length > 3) return cleaned
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// For longer messages, extract the first meaningful clause
|
|
385
|
+
const sentences = message.split(/[.!?\n]+/).filter(s => s.trim().length > 3)
|
|
386
|
+
const first = sentences[0]
|
|
387
|
+
if (first) {
|
|
388
|
+
return first.trim().slice(0, 200)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return undefined
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Find where a clause ends (at a reasoning phrase or sentence boundary)
|
|
396
|
+
*/
|
|
397
|
+
private findClauseEnd(text: string, stopPhrases: string[]): number {
|
|
398
|
+
const lower = text.toLowerCase()
|
|
399
|
+
|
|
400
|
+
const phraseStart = stopPhrases.reduce((min, phrase) => {
|
|
401
|
+
const idx = lower.indexOf(phrase)
|
|
402
|
+
return idx !== -1 ? Math.min(min, idx) : min
|
|
403
|
+
}, text.length)
|
|
404
|
+
|
|
405
|
+
const sentenceEnd = text.search(/[.!?\n]/)
|
|
406
|
+
const end = sentenceEnd !== -1 ? sentenceEnd : text.length
|
|
407
|
+
|
|
408
|
+
return Math.min(phraseStart, end, 200)
|
|
409
|
+
}
|
|
410
|
+
}
|