prjct-cli 1.7.5 → 1.9.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/CHANGELOG.md +205 -1
- package/bin/prjct.ts +14 -0
- package/core/__tests__/agentic/command-context.test.ts +281 -0
- package/core/__tests__/agentic/domain-classifier.test.ts +330 -0
- package/core/__tests__/agentic/response-validator.test.ts +263 -0
- package/core/__tests__/agentic/smart-context.test.ts +3 -3
- package/core/__tests__/domain/fibonacci.test.ts +113 -0
- package/core/__tests__/infrastructure/performance-tracker.test.ts +328 -0
- package/core/__tests__/schemas/model.test.ts +272 -0
- package/core/agentic/command-classifier.ts +141 -0
- package/core/agentic/command-context.ts +168 -0
- package/core/agentic/domain-classifier.ts +525 -0
- package/core/agentic/index.ts +1 -0
- package/core/agentic/orchestrator-executor.ts +43 -199
- package/core/agentic/prompt-builder.ts +50 -55
- package/core/agentic/response-validator.ts +98 -0
- package/core/agentic/smart-context.ts +60 -144
- package/core/commands/command-data.ts +17 -0
- package/core/commands/commands.ts +9 -0
- package/core/commands/performance.ts +114 -0
- package/core/commands/register.ts +6 -0
- package/core/commands/workflow.ts +87 -4
- package/core/config/command-context.config.json +66 -0
- package/core/domain/fibonacci.ts +128 -0
- package/core/index.ts +25 -1
- package/core/infrastructure/ai-provider.ts +35 -0
- package/core/infrastructure/performance-tracker.ts +326 -0
- package/core/schemas/analysis.ts +4 -0
- package/core/schemas/classification.ts +91 -0
- package/core/schemas/command-context.ts +29 -0
- package/core/schemas/index.ts +6 -0
- package/core/schemas/llm-output.ts +170 -0
- package/core/schemas/model.ts +153 -0
- package/core/schemas/performance.ts +128 -0
- package/core/schemas/state.ts +9 -0
- package/core/storage/state-storage.ts +21 -0
- package/core/types/config.ts +2 -0
- package/core/types/provider.ts +12 -0
- package/dist/bin/prjct.mjs +3184 -1945
- package/dist/core/infrastructure/command-installer.js +78 -7
- package/dist/core/infrastructure/setup.js +78 -7
- package/package.json +1 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Classifier
|
|
3
|
+
*
|
|
4
|
+
* LLM-based task domain classification with caching and fallback chain.
|
|
5
|
+
* Replaces hardcoded keyword substring matching that caused false positives
|
|
6
|
+
* (e.g., "author" matching "auth" → backend).
|
|
7
|
+
*
|
|
8
|
+
* Fallback chain:
|
|
9
|
+
* 1. Cache lookup (file-based, 1hr TTL)
|
|
10
|
+
* 2. Confirmed patterns (from successful task completions)
|
|
11
|
+
* 3. LLM call (Claude haiku, ~200 tokens)
|
|
12
|
+
* 4. Improved heuristic (word-boundary matching, no substring traps)
|
|
13
|
+
*
|
|
14
|
+
* @see PRJ-299
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createHash } from 'node:crypto'
|
|
18
|
+
import fs from 'node:fs/promises'
|
|
19
|
+
import path from 'node:path'
|
|
20
|
+
import {
|
|
21
|
+
type ClassificationCache,
|
|
22
|
+
type ClassificationDomain,
|
|
23
|
+
DEFAULT_CLASSIFICATION_CACHE,
|
|
24
|
+
GENERAL_CLASSIFICATION,
|
|
25
|
+
type TaskClassification,
|
|
26
|
+
TaskClassificationSchema,
|
|
27
|
+
} from '../schemas/classification'
|
|
28
|
+
import { renderSchemaForPrompt } from '../schemas/llm-output'
|
|
29
|
+
import { getErrorMessage, isNotFoundError } from '../types/fs'
|
|
30
|
+
|
|
31
|
+
const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Project Context (passed to classifier)
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export interface ProjectContext {
|
|
38
|
+
/** Domains detected during sync */
|
|
39
|
+
domains: {
|
|
40
|
+
hasFrontend: boolean
|
|
41
|
+
hasBackend: boolean
|
|
42
|
+
hasDatabase: boolean
|
|
43
|
+
hasTesting: boolean
|
|
44
|
+
hasDocker: boolean
|
|
45
|
+
}
|
|
46
|
+
/** Available agent names (without .md extension) */
|
|
47
|
+
agents: string[]
|
|
48
|
+
/** Project stack info */
|
|
49
|
+
stack?: { language?: string; framework?: string }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Hashing
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
function hashDescription(description: string): string {
|
|
57
|
+
return createHash('sha256').update(description.toLowerCase().trim()).digest('hex').slice(0, 16)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Cache Layer
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
async function loadCache(globalPath: string): Promise<ClassificationCache> {
|
|
65
|
+
try {
|
|
66
|
+
const cachePath = path.join(globalPath, 'storage', 'classification-cache.json')
|
|
67
|
+
const content = await fs.readFile(cachePath, 'utf-8')
|
|
68
|
+
return JSON.parse(content)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (isNotFoundError(error)) return DEFAULT_CLASSIFICATION_CACHE
|
|
71
|
+
console.warn('[classifier] Failed to load cache:', getErrorMessage(error))
|
|
72
|
+
return DEFAULT_CLASSIFICATION_CACHE
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function saveCache(globalPath: string, cache: ClassificationCache): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
const cachePath = path.join(globalPath, 'storage', 'classification-cache.json')
|
|
79
|
+
await fs.writeFile(cachePath, JSON.stringify(cache, null, 2))
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn('[classifier] Failed to save cache:', getErrorMessage(error))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function lookupCache(
|
|
86
|
+
cache: ClassificationCache,
|
|
87
|
+
hash: string,
|
|
88
|
+
projectId: string
|
|
89
|
+
): TaskClassification | null {
|
|
90
|
+
const entry = cache.entries[hash]
|
|
91
|
+
if (!entry) return null
|
|
92
|
+
if (entry.projectId !== projectId) return null
|
|
93
|
+
|
|
94
|
+
// Check TTL
|
|
95
|
+
const age = Date.now() - new Date(entry.classifiedAt).getTime()
|
|
96
|
+
if (age > CACHE_TTL_MS) return null
|
|
97
|
+
|
|
98
|
+
return entry.classification
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Confirmed Patterns (from successful task completions)
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
function lookupPatterns(cache: ClassificationCache, hash: string): TaskClassification | null {
|
|
106
|
+
const pattern = cache.confirmedPatterns.find((p) => p.descriptionHash === hash)
|
|
107
|
+
return pattern?.classification ?? null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// LLM Classification (Claude Haiku via raw API)
|
|
112
|
+
// =============================================================================
|
|
113
|
+
|
|
114
|
+
async function classifyWithLLM(
|
|
115
|
+
description: string,
|
|
116
|
+
context: ProjectContext
|
|
117
|
+
): Promise<TaskClassification | null> {
|
|
118
|
+
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
119
|
+
if (!apiKey) return null
|
|
120
|
+
|
|
121
|
+
const availableDomains = buildAvailableDomains(context)
|
|
122
|
+
const schemaBlock = renderSchemaForPrompt('classification') || ''
|
|
123
|
+
|
|
124
|
+
const prompt = `Classify this software engineering task into a domain.
|
|
125
|
+
|
|
126
|
+
Task: "${description}"
|
|
127
|
+
|
|
128
|
+
Available domains in this project: ${availableDomains.join(', ')}
|
|
129
|
+
Available agents: ${context.agents.join(', ') || 'none'}
|
|
130
|
+
Stack: ${context.stack?.language || 'unknown'} / ${context.stack?.framework || 'unknown'}
|
|
131
|
+
|
|
132
|
+
${schemaBlock}`
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'x-api-key': apiKey,
|
|
140
|
+
'anthropic-version': '2023-06-01',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
model: 'claude-haiku-4-5-20251001',
|
|
144
|
+
max_tokens: 200,
|
|
145
|
+
messages: [{ role: 'user', content: prompt }],
|
|
146
|
+
}),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (!response.ok) return null
|
|
150
|
+
|
|
151
|
+
const data = (await response.json()) as {
|
|
152
|
+
content: Array<{ type: string; text?: string }>
|
|
153
|
+
}
|
|
154
|
+
const text = data.content?.[0]?.text
|
|
155
|
+
if (!text) return null
|
|
156
|
+
|
|
157
|
+
const parsed = JSON.parse(text)
|
|
158
|
+
|
|
159
|
+
// Validate with Zod schema (PRJ-264)
|
|
160
|
+
const result = TaskClassificationSchema.safeParse(parsed)
|
|
161
|
+
if (result.success) {
|
|
162
|
+
return result.data
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Re-prompt: coerce what we can from partial response
|
|
166
|
+
return {
|
|
167
|
+
primaryDomain: availableDomains.includes(parsed.primaryDomain)
|
|
168
|
+
? parsed.primaryDomain
|
|
169
|
+
: 'general',
|
|
170
|
+
secondaryDomains: (parsed.secondaryDomains || []).filter((d: string) =>
|
|
171
|
+
availableDomains.includes(d as ClassificationDomain)
|
|
172
|
+
),
|
|
173
|
+
confidence: Math.min(1, Math.max(0, parsed.confidence || 0.5)),
|
|
174
|
+
filePatterns: Array.isArray(parsed.filePatterns) ? parsed.filePatterns : [],
|
|
175
|
+
relevantAgents: Array.isArray(parsed.relevantAgents) ? parsed.relevantAgents : [],
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Improved Heuristic (word-boundary matching, no substring traps)
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
/** Word-boundary keyword map — avoids substring false positives */
|
|
187
|
+
const DOMAIN_PATTERNS: Record<ClassificationDomain, RegExp[]> = {
|
|
188
|
+
frontend: [
|
|
189
|
+
/\bui\b/i,
|
|
190
|
+
/\bcomponents?\b/i,
|
|
191
|
+
/\breact\b/i,
|
|
192
|
+
/\bvue\b/i,
|
|
193
|
+
/\bangular\b/i,
|
|
194
|
+
/\bsvelte\b/i,
|
|
195
|
+
/\bnext\.?js\b/i,
|
|
196
|
+
/\bnuxt\b/i,
|
|
197
|
+
/\bcss\b/i,
|
|
198
|
+
/\bscss\b/i,
|
|
199
|
+
/\bstyles?\b/i,
|
|
200
|
+
/\bbuttons?\b/i,
|
|
201
|
+
/\bforms?\b/i,
|
|
202
|
+
/\bmodals?\b/i,
|
|
203
|
+
/\blayout\b/i,
|
|
204
|
+
/\bresponsive\b/i,
|
|
205
|
+
/\banimation\b/i,
|
|
206
|
+
/\bdom\b/i,
|
|
207
|
+
/\bhtml\b/i,
|
|
208
|
+
/\bfrontend\b/i,
|
|
209
|
+
/\bclient[- ]side\b/i,
|
|
210
|
+
/\bbrowser\b/i,
|
|
211
|
+
/\bjsx\b/i,
|
|
212
|
+
/\btsx\b/i,
|
|
213
|
+
/\bhooks?\b/i,
|
|
214
|
+
/\bredux\b/i,
|
|
215
|
+
/\bzustand\b/i,
|
|
216
|
+
/\btailwind\b/i,
|
|
217
|
+
/\bdashboard\b/i,
|
|
218
|
+
/\bpage\b/i,
|
|
219
|
+
/\bnavigation\b/i,
|
|
220
|
+
/\bsidebar\b/i,
|
|
221
|
+
/\bheader\b/i,
|
|
222
|
+
/\bfooter\b/i,
|
|
223
|
+
/\bwidget\b/i,
|
|
224
|
+
/\btooltip\b/i,
|
|
225
|
+
/\bdropdown\b/i,
|
|
226
|
+
/\bcarousel\b/i,
|
|
227
|
+
/\bprofile\s+page\b/i,
|
|
228
|
+
/\bdisplay\b/i,
|
|
229
|
+
],
|
|
230
|
+
backend: [
|
|
231
|
+
/\bapi\b/i,
|
|
232
|
+
/\bendpoints?\b/i,
|
|
233
|
+
/\bserver\b/i,
|
|
234
|
+
/\broutes?\b/i,
|
|
235
|
+
/\bhandlers?\b/i,
|
|
236
|
+
/\bcontrollers?\b/i,
|
|
237
|
+
/\bservices?\b/i,
|
|
238
|
+
/\bmiddleware\b/i,
|
|
239
|
+
/\bauth\b/i,
|
|
240
|
+
/\bauthentication\b/i,
|
|
241
|
+
/\bauthorization\b/i,
|
|
242
|
+
/\bjwt\b/i,
|
|
243
|
+
/\boauth\b/i,
|
|
244
|
+
/\brest\b/i,
|
|
245
|
+
/\bgraphql\b/i,
|
|
246
|
+
/\btrpc\b/i,
|
|
247
|
+
/\bexpress\b/i,
|
|
248
|
+
/\bfastify\b/i,
|
|
249
|
+
/\bhono\b/i,
|
|
250
|
+
/\bnest\.?js\b/i,
|
|
251
|
+
/\bvalidation\b/i,
|
|
252
|
+
/\bbusiness\s+logic\b/i,
|
|
253
|
+
/\bcron\b/i,
|
|
254
|
+
/\bwebhook\b/i,
|
|
255
|
+
/\bworker\b/i,
|
|
256
|
+
/\bqueue\b/i,
|
|
257
|
+
/\bcache\b/i,
|
|
258
|
+
],
|
|
259
|
+
database: [
|
|
260
|
+
/\bdatabase\b/i,
|
|
261
|
+
/\bdb\b/i,
|
|
262
|
+
/\bsql\b/i,
|
|
263
|
+
/\bquery\b/i,
|
|
264
|
+
/\btables?\b/i,
|
|
265
|
+
/\bschema\b/i,
|
|
266
|
+
/\bmigrations?\b/i,
|
|
267
|
+
/\bpostgres\b/i,
|
|
268
|
+
/\bmysql\b/i,
|
|
269
|
+
/\bsqlite\b/i,
|
|
270
|
+
/\bmongo\b/i,
|
|
271
|
+
/\bredis\b/i,
|
|
272
|
+
/\bprisma\b/i,
|
|
273
|
+
/\bdrizzle\b/i,
|
|
274
|
+
/\borm\b/i,
|
|
275
|
+
/\bentity\b/i,
|
|
276
|
+
/\brepository\b/i,
|
|
277
|
+
/\bdata\s+layer\b/i,
|
|
278
|
+
/\bpersist\b/i,
|
|
279
|
+
/\bindex(?:es|ing)?\b/i,
|
|
280
|
+
/\bconnection\s+pool\b/i,
|
|
281
|
+
],
|
|
282
|
+
devops: [
|
|
283
|
+
/\bdocker\b/i,
|
|
284
|
+
/\bkubernetes\b/i,
|
|
285
|
+
/\bk8s\b/i,
|
|
286
|
+
/\bci\b/i,
|
|
287
|
+
/\bcd\b/i,
|
|
288
|
+
/\bpipeline\b/i,
|
|
289
|
+
/\bdeploy\b/i,
|
|
290
|
+
/\bgithub\s+actions\b/i,
|
|
291
|
+
/\bvercel\b/i,
|
|
292
|
+
/\baws\b/i,
|
|
293
|
+
/\bgcp\b/i,
|
|
294
|
+
/\bazure\b/i,
|
|
295
|
+
/\bterraform\b/i,
|
|
296
|
+
/\bnginx\b/i,
|
|
297
|
+
/\bcaddy\b/i,
|
|
298
|
+
/\binfrastructure\b/i,
|
|
299
|
+
/\bmonitoring\b/i,
|
|
300
|
+
/\blogging\b/i,
|
|
301
|
+
/\bcontainer\b/i,
|
|
302
|
+
/\bhelm\b/i,
|
|
303
|
+
],
|
|
304
|
+
testing: [
|
|
305
|
+
/\btests?\b/i,
|
|
306
|
+
/\bspec\b/i,
|
|
307
|
+
/\bunit\s+tests?\b/i,
|
|
308
|
+
/\bintegration\s+tests?\b/i,
|
|
309
|
+
/\be2e\b/i,
|
|
310
|
+
/\bjest\b/i,
|
|
311
|
+
/\bvitest\b/i,
|
|
312
|
+
/\bplaywright\b/i,
|
|
313
|
+
/\bcypress\b/i,
|
|
314
|
+
/\bmocha\b/i,
|
|
315
|
+
/\bmocks?\b/i,
|
|
316
|
+
/\bstubs?\b/i,
|
|
317
|
+
/\bfixtures?\b/i,
|
|
318
|
+
/\bcoverage\b/i,
|
|
319
|
+
/\bassertions?\b/i,
|
|
320
|
+
],
|
|
321
|
+
docs: [
|
|
322
|
+
/\bdocument(?:ation)?\b/i,
|
|
323
|
+
/\bdocs\b/i,
|
|
324
|
+
/\breadme\b/i,
|
|
325
|
+
/\bchangelog\b/i,
|
|
326
|
+
/\bjsdoc\b/i,
|
|
327
|
+
/\btutorial\b/i,
|
|
328
|
+
/\bguide\b/i,
|
|
329
|
+
/\bmarkdown\b/i,
|
|
330
|
+
],
|
|
331
|
+
uxui: [
|
|
332
|
+
/\bdesign\b/i,
|
|
333
|
+
/\bux\b/i,
|
|
334
|
+
/\buser\s+experience\b/i,
|
|
335
|
+
/\baccessibility\b/i,
|
|
336
|
+
/\ba11y\b/i,
|
|
337
|
+
/\bwcag\b/i,
|
|
338
|
+
/\bfigma\b/i,
|
|
339
|
+
/\bprototype\b/i,
|
|
340
|
+
/\bwireframe\b/i,
|
|
341
|
+
/\busability\b/i,
|
|
342
|
+
],
|
|
343
|
+
general: [],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Improved heuristic classifier using word-boundary regex.
|
|
348
|
+
* Avoids substring traps like "author" matching "auth".
|
|
349
|
+
*/
|
|
350
|
+
function classifyWithHeuristic(description: string, context: ProjectContext): TaskClassification {
|
|
351
|
+
const availableDomains = buildAvailableDomains(context)
|
|
352
|
+
const scores = new Map<ClassificationDomain, number>()
|
|
353
|
+
|
|
354
|
+
for (const [domain, patterns] of Object.entries(DOMAIN_PATTERNS)) {
|
|
355
|
+
if (domain === 'general') continue
|
|
356
|
+
// Only score domains that exist in this project
|
|
357
|
+
if (!availableDomains.includes(domain as ClassificationDomain)) continue
|
|
358
|
+
|
|
359
|
+
let score = 0
|
|
360
|
+
for (const pattern of patterns) {
|
|
361
|
+
const matches = description.match(new RegExp(pattern, 'gi'))
|
|
362
|
+
if (matches) {
|
|
363
|
+
// Multi-word patterns score higher
|
|
364
|
+
score += pattern.source.includes('\\s') ? 3 : 1
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (score > 0) {
|
|
368
|
+
scores.set(domain as ClassificationDomain, score)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (scores.size === 0) {
|
|
373
|
+
return GENERAL_CLASSIFICATION
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const sorted = Array.from(scores.entries()).sort((a, b) => b[1] - a[1])
|
|
377
|
+
const primary = sorted[0][0]
|
|
378
|
+
const primaryScore = sorted[0][1]
|
|
379
|
+
const secondary = sorted.slice(1, 3).map(([d]) => d)
|
|
380
|
+
|
|
381
|
+
const totalScore = sorted.reduce((sum, [, s]) => sum + s, 0)
|
|
382
|
+
const confidence = Math.min(0.85, primaryScore / totalScore + 0.2)
|
|
383
|
+
|
|
384
|
+
// Map domain to file patterns
|
|
385
|
+
const filePatterns = getFilePatterns(primary)
|
|
386
|
+
|
|
387
|
+
// Map domain to agent names
|
|
388
|
+
const relevantAgents = context.agents.filter(
|
|
389
|
+
(a) => a === primary || a.includes(primary) || primary.includes(a.replace('.md', ''))
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
primaryDomain: primary,
|
|
394
|
+
secondaryDomains: secondary,
|
|
395
|
+
confidence,
|
|
396
|
+
filePatterns,
|
|
397
|
+
relevantAgents,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// =============================================================================
|
|
402
|
+
// Helpers
|
|
403
|
+
// =============================================================================
|
|
404
|
+
|
|
405
|
+
function buildAvailableDomains(context: ProjectContext): ClassificationDomain[] {
|
|
406
|
+
const domains: ClassificationDomain[] = []
|
|
407
|
+
if (context.domains.hasFrontend) domains.push('frontend')
|
|
408
|
+
if (context.domains.hasBackend) domains.push('backend')
|
|
409
|
+
if (context.domains.hasDatabase) domains.push('database')
|
|
410
|
+
if (context.domains.hasTesting) domains.push('testing')
|
|
411
|
+
if (context.domains.hasDocker) domains.push('devops')
|
|
412
|
+
// Always include these as possibilities
|
|
413
|
+
domains.push('docs', 'uxui', 'general')
|
|
414
|
+
return domains
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getFilePatterns(domain: ClassificationDomain): string[] {
|
|
418
|
+
const patterns: Record<ClassificationDomain, string[]> = {
|
|
419
|
+
frontend: ['src/components/**', 'src/pages/**', 'src/hooks/**', '**/*.tsx', '**/*.jsx'],
|
|
420
|
+
backend: ['src/api/**', 'src/routes/**', 'src/services/**', 'src/handlers/**'],
|
|
421
|
+
database: ['src/models/**', 'src/schemas/**', '**/*.sql', 'prisma/**'],
|
|
422
|
+
devops: ['.github/**', 'docker/**', 'deploy/**', 'infra/**', '**/*.yml', '**/*.yaml'],
|
|
423
|
+
testing: ['**/*.test.*', '**/*.spec.*', 'tests/**', '__tests__/**', 'e2e/**'],
|
|
424
|
+
docs: ['docs/**', '**/*.md', '**/*.mdx'],
|
|
425
|
+
uxui: ['src/components/**', 'src/styles/**', '**/*.css'],
|
|
426
|
+
general: ['**/*.ts', '**/*.js'],
|
|
427
|
+
}
|
|
428
|
+
return patterns[domain] || patterns.general
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// =============================================================================
|
|
432
|
+
// Main Classifier
|
|
433
|
+
// =============================================================================
|
|
434
|
+
|
|
435
|
+
export class DomainClassifier {
|
|
436
|
+
/**
|
|
437
|
+
* Classify a task description into a domain.
|
|
438
|
+
*
|
|
439
|
+
* Fallback chain:
|
|
440
|
+
* 1. Cache lookup (1hr TTL)
|
|
441
|
+
* 2. Confirmed patterns (from completed tasks)
|
|
442
|
+
* 3. LLM call (Claude haiku)
|
|
443
|
+
* 4. Improved heuristic (word-boundary matching)
|
|
444
|
+
*/
|
|
445
|
+
async classify(
|
|
446
|
+
description: string,
|
|
447
|
+
projectId: string,
|
|
448
|
+
globalPath: string,
|
|
449
|
+
context: ProjectContext
|
|
450
|
+
): Promise<{
|
|
451
|
+
classification: TaskClassification
|
|
452
|
+
source: 'cache' | 'history' | 'llm' | 'heuristic'
|
|
453
|
+
}> {
|
|
454
|
+
const hash = hashDescription(description)
|
|
455
|
+
const cache = await loadCache(globalPath)
|
|
456
|
+
|
|
457
|
+
// 1. Cache lookup
|
|
458
|
+
const cached = lookupCache(cache, hash, projectId)
|
|
459
|
+
if (cached) {
|
|
460
|
+
return { classification: cached, source: 'cache' }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 2. Confirmed patterns
|
|
464
|
+
const pattern = lookupPatterns(cache, hash)
|
|
465
|
+
if (pattern) {
|
|
466
|
+
return { classification: pattern, source: 'history' }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 3. LLM call
|
|
470
|
+
const llmResult = await classifyWithLLM(description, context)
|
|
471
|
+
if (llmResult) {
|
|
472
|
+
// Cache the LLM result
|
|
473
|
+
cache.entries[hash] = {
|
|
474
|
+
classification: llmResult,
|
|
475
|
+
classifiedAt: new Date().toISOString(),
|
|
476
|
+
source: 'llm',
|
|
477
|
+
descriptionHash: hash,
|
|
478
|
+
projectId,
|
|
479
|
+
}
|
|
480
|
+
await saveCache(globalPath, cache)
|
|
481
|
+
return { classification: llmResult, source: 'llm' }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 4. Heuristic fallback
|
|
485
|
+
const heuristicResult = classifyWithHeuristic(description, context)
|
|
486
|
+
// Cache heuristic results too
|
|
487
|
+
cache.entries[hash] = {
|
|
488
|
+
classification: heuristicResult,
|
|
489
|
+
classifiedAt: new Date().toISOString(),
|
|
490
|
+
source: 'heuristic',
|
|
491
|
+
descriptionHash: hash,
|
|
492
|
+
projectId,
|
|
493
|
+
}
|
|
494
|
+
await saveCache(globalPath, cache)
|
|
495
|
+
return { classification: heuristicResult, source: 'heuristic' }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Persist a classification as a confirmed pattern after successful task completion.
|
|
500
|
+
*/
|
|
501
|
+
async confirmClassification(
|
|
502
|
+
description: string,
|
|
503
|
+
classification: TaskClassification,
|
|
504
|
+
globalPath: string
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
const hash = hashDescription(description)
|
|
507
|
+
const cache = await loadCache(globalPath)
|
|
508
|
+
|
|
509
|
+
// Check if already confirmed
|
|
510
|
+
if (cache.confirmedPatterns.some((p) => p.descriptionHash === hash)) return
|
|
511
|
+
|
|
512
|
+
cache.confirmedPatterns.push({
|
|
513
|
+
descriptionHash: hash,
|
|
514
|
+
classification,
|
|
515
|
+
confirmedAt: new Date().toISOString(),
|
|
516
|
+
taskDescription: description,
|
|
517
|
+
})
|
|
518
|
+
await saveCache(globalPath, cache)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Singleton
|
|
523
|
+
const domainClassifier = new DomainClassifier()
|
|
524
|
+
export default domainClassifier
|
|
525
|
+
export { hashDescription, classifyWithHeuristic, DOMAIN_PATTERNS }
|
package/core/agentic/index.ts
CHANGED
|
@@ -123,6 +123,7 @@ export {
|
|
|
123
123
|
// ============ Context ============
|
|
124
124
|
// Context building and prompt generation
|
|
125
125
|
export { default as contextBuilder } from './context-builder'
|
|
126
|
+
export { DomainClassifier, default as domainClassifier } from './domain-classifier'
|
|
126
127
|
export {
|
|
127
128
|
default as groundTruth,
|
|
128
129
|
escapeRegex,
|