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
|
@@ -32,149 +32,11 @@ import type {
|
|
|
32
32
|
RealCodebaseContext,
|
|
33
33
|
} from '../types'
|
|
34
34
|
import { getErrorMessage, isNotFoundError } from '../types/fs'
|
|
35
|
+
import domainClassifier, { type ProjectContext } from './domain-classifier'
|
|
35
36
|
import { parseFrontmatter } from './template-loader'
|
|
36
37
|
|
|
37
38
|
const execAsync = promisify(execCallback)
|
|
38
39
|
|
|
39
|
-
// =============================================================================
|
|
40
|
-
// Domain Detection Keywords
|
|
41
|
-
// =============================================================================
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Keywords that indicate a domain is involved in the task
|
|
45
|
-
* These are hints, not absolute rules - context matters
|
|
46
|
-
*/
|
|
47
|
-
const DOMAIN_KEYWORDS: Record<string, string[]> = {
|
|
48
|
-
database: [
|
|
49
|
-
'database',
|
|
50
|
-
'db',
|
|
51
|
-
'sql',
|
|
52
|
-
'query',
|
|
53
|
-
'table',
|
|
54
|
-
'schema',
|
|
55
|
-
'migration',
|
|
56
|
-
'postgres',
|
|
57
|
-
'mysql',
|
|
58
|
-
'sqlite',
|
|
59
|
-
'mongo',
|
|
60
|
-
'redis',
|
|
61
|
-
'prisma',
|
|
62
|
-
'drizzle',
|
|
63
|
-
'orm',
|
|
64
|
-
'model',
|
|
65
|
-
'entity',
|
|
66
|
-
'repository',
|
|
67
|
-
'data layer',
|
|
68
|
-
'persist',
|
|
69
|
-
],
|
|
70
|
-
backend: [
|
|
71
|
-
'api',
|
|
72
|
-
'endpoint',
|
|
73
|
-
'route',
|
|
74
|
-
'server',
|
|
75
|
-
'controller',
|
|
76
|
-
'service',
|
|
77
|
-
'middleware',
|
|
78
|
-
'auth',
|
|
79
|
-
'authentication',
|
|
80
|
-
'authorization',
|
|
81
|
-
'jwt',
|
|
82
|
-
'oauth',
|
|
83
|
-
'rest',
|
|
84
|
-
'graphql',
|
|
85
|
-
'trpc',
|
|
86
|
-
'express',
|
|
87
|
-
'fastify',
|
|
88
|
-
'hono',
|
|
89
|
-
'nest',
|
|
90
|
-
'validation',
|
|
91
|
-
'business logic',
|
|
92
|
-
],
|
|
93
|
-
frontend: [
|
|
94
|
-
'ui',
|
|
95
|
-
'component',
|
|
96
|
-
'page',
|
|
97
|
-
'form',
|
|
98
|
-
'button',
|
|
99
|
-
'input',
|
|
100
|
-
'modal',
|
|
101
|
-
'dialog',
|
|
102
|
-
'react',
|
|
103
|
-
'vue',
|
|
104
|
-
'svelte',
|
|
105
|
-
'angular',
|
|
106
|
-
'next',
|
|
107
|
-
'nuxt',
|
|
108
|
-
'solid',
|
|
109
|
-
'css',
|
|
110
|
-
'style',
|
|
111
|
-
'tailwind',
|
|
112
|
-
'layout',
|
|
113
|
-
'responsive',
|
|
114
|
-
'animation',
|
|
115
|
-
'hook',
|
|
116
|
-
'state',
|
|
117
|
-
'context',
|
|
118
|
-
'redux',
|
|
119
|
-
'zustand',
|
|
120
|
-
'jotai',
|
|
121
|
-
],
|
|
122
|
-
testing: [
|
|
123
|
-
'test',
|
|
124
|
-
'spec',
|
|
125
|
-
'unit',
|
|
126
|
-
'integration',
|
|
127
|
-
'e2e',
|
|
128
|
-
'jest',
|
|
129
|
-
'vitest',
|
|
130
|
-
'playwright',
|
|
131
|
-
'cypress',
|
|
132
|
-
'mocha',
|
|
133
|
-
'chai',
|
|
134
|
-
'mock',
|
|
135
|
-
'stub',
|
|
136
|
-
'fixture',
|
|
137
|
-
'coverage',
|
|
138
|
-
'assertion',
|
|
139
|
-
],
|
|
140
|
-
devops: [
|
|
141
|
-
'docker',
|
|
142
|
-
'kubernetes',
|
|
143
|
-
'k8s',
|
|
144
|
-
'ci',
|
|
145
|
-
'cd',
|
|
146
|
-
'pipeline',
|
|
147
|
-
'deploy',
|
|
148
|
-
'github actions',
|
|
149
|
-
'vercel',
|
|
150
|
-
'aws',
|
|
151
|
-
'gcp',
|
|
152
|
-
'azure',
|
|
153
|
-
'terraform',
|
|
154
|
-
'nginx',
|
|
155
|
-
'caddy',
|
|
156
|
-
'env',
|
|
157
|
-
'environment',
|
|
158
|
-
'config',
|
|
159
|
-
'secret',
|
|
160
|
-
],
|
|
161
|
-
uxui: [
|
|
162
|
-
'design',
|
|
163
|
-
'ux',
|
|
164
|
-
'user experience',
|
|
165
|
-
'accessibility',
|
|
166
|
-
'a11y',
|
|
167
|
-
'color',
|
|
168
|
-
'typography',
|
|
169
|
-
'spacing',
|
|
170
|
-
'prototype',
|
|
171
|
-
'wireframe',
|
|
172
|
-
'figma',
|
|
173
|
-
'user flow',
|
|
174
|
-
'interaction',
|
|
175
|
-
],
|
|
176
|
-
}
|
|
177
|
-
|
|
178
40
|
/**
|
|
179
41
|
* Domain dependency order - earlier domains should complete first
|
|
180
42
|
*/
|
|
@@ -353,84 +215,66 @@ export class OrchestratorExecutor {
|
|
|
353
215
|
}
|
|
354
216
|
|
|
355
217
|
/**
|
|
356
|
-
* Detect which domains are relevant for this task
|
|
218
|
+
* Detect which domains are relevant for this task.
|
|
357
219
|
*
|
|
358
|
-
* Uses
|
|
359
|
-
*
|
|
360
|
-
* - Task description keywords
|
|
361
|
-
* - Project technology stack
|
|
362
|
-
* - Available agents
|
|
220
|
+
* Uses LLM-based classification with fallback chain (PRJ-299):
|
|
221
|
+
* cache → confirmed patterns → LLM → heuristic
|
|
363
222
|
*/
|
|
364
223
|
async detectDomains(
|
|
365
224
|
taskDescription: string,
|
|
366
225
|
projectId: string,
|
|
367
226
|
repoAnalysis: { ecosystem: string; technologies?: string[] } | null
|
|
368
227
|
): Promise<{ domains: string[]; primary: string }> {
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
228
|
+
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
229
|
+
const availableAgents = await this.getAvailableAgentNames(globalPath)
|
|
230
|
+
|
|
231
|
+
// Load state.json for project domain info
|
|
232
|
+
let projectDomains = {
|
|
233
|
+
hasFrontend: false,
|
|
234
|
+
hasBackend: true,
|
|
235
|
+
hasDatabase: false,
|
|
236
|
+
hasTesting: false,
|
|
237
|
+
hasDocker: false,
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const statePath = path.join(globalPath, 'storage', 'state.json')
|
|
241
|
+
const stateContent = await fs.readFile(statePath, 'utf-8')
|
|
242
|
+
const state = JSON.parse(stateContent)
|
|
243
|
+
if (state.domains) {
|
|
244
|
+
projectDomains = state.domains
|
|
383
245
|
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Use defaults
|
|
384
248
|
}
|
|
385
249
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (/react|vue|svelte|angular|next|nuxt/.test(techStr)) {
|
|
392
|
-
const current = detectedDomains.get('frontend') || 0
|
|
393
|
-
if (current > 0) detectedDomains.set('frontend', current + 2)
|
|
394
|
-
}
|
|
250
|
+
const context: ProjectContext = {
|
|
251
|
+
domains: projectDomains,
|
|
252
|
+
agents: availableAgents,
|
|
253
|
+
stack: repoAnalysis ? { language: repoAnalysis.ecosystem } : undefined,
|
|
254
|
+
}
|
|
395
255
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
256
|
+
const { classification } = await domainClassifier.classify(
|
|
257
|
+
taskDescription,
|
|
258
|
+
projectId,
|
|
259
|
+
globalPath,
|
|
260
|
+
context
|
|
261
|
+
)
|
|
401
262
|
|
|
402
|
-
|
|
403
|
-
if (/prisma|drizzle|mongoose|typeorm|sequelize/.test(techStr)) {
|
|
404
|
-
const current = detectedDomains.get('database') || 0
|
|
405
|
-
if (current > 0) detectedDomains.set('database', current + 2)
|
|
406
|
-
}
|
|
407
|
-
}
|
|
263
|
+
const domains = [classification.primaryDomain, ...classification.secondaryDomains]
|
|
408
264
|
|
|
409
|
-
//
|
|
410
|
-
const
|
|
411
|
-
|
|
265
|
+
// Filter to domains that have corresponding agents
|
|
266
|
+
const validDomains = domains.filter((domain) =>
|
|
267
|
+
availableAgents.some(
|
|
268
|
+
(agent) =>
|
|
269
|
+
agent === domain || agent.includes(domain) || domain.includes(agent.replace('.md', ''))
|
|
270
|
+
)
|
|
271
|
+
)
|
|
412
272
|
|
|
413
|
-
// Only include domains that have corresponding agents
|
|
414
|
-
const validDomains = Array.from(detectedDomains.entries())
|
|
415
|
-
.filter(([domain]) => {
|
|
416
|
-
// Check if agent exists for this domain
|
|
417
|
-
return availableAgents.some(
|
|
418
|
-
(agent) =>
|
|
419
|
-
agent === domain || agent.includes(domain) || domain.includes(agent.replace('.md', ''))
|
|
420
|
-
)
|
|
421
|
-
})
|
|
422
|
-
.sort((a, b) => b[1] - a[1]) // Sort by score descending
|
|
423
|
-
.map(([domain]) => domain)
|
|
424
|
-
|
|
425
|
-
// If no domains detected, default to 'general'
|
|
426
273
|
if (validDomains.length === 0) {
|
|
427
274
|
return { domains: ['general'], primary: 'general' }
|
|
428
275
|
}
|
|
429
276
|
|
|
430
|
-
|
|
431
|
-
const primary = validDomains[0]
|
|
432
|
-
|
|
433
|
-
return { domains: validDomains, primary }
|
|
277
|
+
return { domains: validDomains, primary: validDomains[0] }
|
|
434
278
|
}
|
|
435
279
|
|
|
436
280
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs/promises'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { outcomeAnalyzer } from '../outcomes'
|
|
15
|
+
import type { CommandContextEntry } from '../schemas/command-context'
|
|
15
16
|
import { queueStorage, stateStorage } from '../storage'
|
|
16
17
|
import type {
|
|
17
18
|
LearnedPatterns,
|
|
@@ -28,6 +29,7 @@ import type {
|
|
|
28
29
|
import { getErrorMessage, isNotFoundError } from '../types/fs'
|
|
29
30
|
import { fileExists } from '../utils/fs-helpers'
|
|
30
31
|
import { PACKAGE_ROOT } from '../utils/version'
|
|
32
|
+
import { loadCommandContextConfig, resolveCommandContextFull } from './command-context'
|
|
31
33
|
import {
|
|
32
34
|
DEFAULT_BUDGETS,
|
|
33
35
|
filterSkillsByDomains,
|
|
@@ -144,18 +146,14 @@ class PromptBuilder {
|
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* Get additional modules needed for SMART commands (PRJ-94)
|
|
147
|
-
*
|
|
149
|
+
* Now config-driven via command-context.config.json (PRJ-298)
|
|
148
150
|
*/
|
|
149
|
-
getModulesForCommand(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
work: ['CLAUDE-intelligence.md', 'CLAUDE-storage.md'],
|
|
156
|
-
spec: ['CLAUDE-intelligence.md'],
|
|
157
|
-
}
|
|
158
|
-
return smartCommands[commandName] || []
|
|
151
|
+
getModulesForCommand(_commandName: string, commandContext?: CommandContextEntry): string[] {
|
|
152
|
+
if (commandContext) {
|
|
153
|
+
return commandContext.modules
|
|
154
|
+
}
|
|
155
|
+
// Fallback if called without config (shouldn't happen after PRJ-298)
|
|
156
|
+
return []
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
/**
|
|
@@ -411,21 +409,20 @@ class PromptBuilder {
|
|
|
411
409
|
// Store context for use in helper methods
|
|
412
410
|
this._currentContext = context
|
|
413
411
|
|
|
414
|
-
//
|
|
412
|
+
// PRJ-298: Config-driven command context (replaces 4 hardcoded lists)
|
|
415
413
|
const commandName = template.frontmatter?.name?.replace('p:', '') || ''
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const needsAgent = agentCommands.includes(commandName)
|
|
414
|
+
let commandContext: CommandContextEntry
|
|
415
|
+
try {
|
|
416
|
+
const config = await loadCommandContextConfig()
|
|
417
|
+
const resolved = resolveCommandContextFull(config, commandName, template)
|
|
418
|
+
commandContext = resolved.entry
|
|
419
|
+
} catch {
|
|
420
|
+
// Fallback: sensible defaults if config fails to load
|
|
421
|
+
commandContext = { agents: true, patterns: true, checklist: false, modules: [] }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Agent assignment (config-driven)
|
|
425
|
+
const needsAgent = commandContext.agents
|
|
429
426
|
|
|
430
427
|
if (agent && needsAgent) {
|
|
431
428
|
parts.push(`# AGENT: ${agent.name}\n`)
|
|
@@ -591,21 +588,8 @@ class PromptBuilder {
|
|
|
591
588
|
)
|
|
592
589
|
}
|
|
593
590
|
|
|
594
|
-
// OPTIMIZED: Only include patterns for code-modifying commands
|
|
595
|
-
const
|
|
596
|
-
'now',
|
|
597
|
-
'build',
|
|
598
|
-
'feature',
|
|
599
|
-
'design',
|
|
600
|
-
'cleanup',
|
|
601
|
-
'fix',
|
|
602
|
-
'bug',
|
|
603
|
-
'test',
|
|
604
|
-
'init',
|
|
605
|
-
'spec',
|
|
606
|
-
'work',
|
|
607
|
-
]
|
|
608
|
-
const needsPatterns = codeCommands.includes(commandName)
|
|
591
|
+
// OPTIMIZED: Only include patterns for code-modifying commands (config-driven, PRJ-298)
|
|
592
|
+
const needsPatterns = commandContext.patterns
|
|
609
593
|
|
|
610
594
|
// Include code patterns analysis for code-modifying commands
|
|
611
595
|
const codePatternsContent = state?.codePatterns || ''
|
|
@@ -636,8 +620,8 @@ class PromptBuilder {
|
|
|
636
620
|
// CRITICAL: Compressed rules
|
|
637
621
|
parts.push(this.buildCriticalRules())
|
|
638
622
|
|
|
639
|
-
// PRJ-94: Inject additional modules for SMART commands
|
|
640
|
-
const additionalModules = this.getModulesForCommand(commandName)
|
|
623
|
+
// PRJ-94/PRJ-298: Inject additional modules for SMART commands (config-driven)
|
|
624
|
+
const additionalModules = this.getModulesForCommand(commandName, commandContext)
|
|
641
625
|
if (additionalModules.length > 0) {
|
|
642
626
|
for (const moduleName of additionalModules) {
|
|
643
627
|
const moduleContent = await this.loadModule(moduleName)
|
|
@@ -698,19 +682,8 @@ class PromptBuilder {
|
|
|
698
682
|
)
|
|
699
683
|
}
|
|
700
684
|
|
|
701
|
-
// P4.1: Quality Checklists
|
|
702
|
-
|
|
703
|
-
'now',
|
|
704
|
-
'build',
|
|
705
|
-
'feature',
|
|
706
|
-
'design',
|
|
707
|
-
'fix',
|
|
708
|
-
'bug',
|
|
709
|
-
'cleanup',
|
|
710
|
-
'spec',
|
|
711
|
-
'work',
|
|
712
|
-
]
|
|
713
|
-
if (checklistCommands.includes(commandName)) {
|
|
685
|
+
// P4.1: Quality Checklists (config-driven, PRJ-298)
|
|
686
|
+
if (commandContext.checklist) {
|
|
714
687
|
const routing = await this.loadChecklistRouting()
|
|
715
688
|
const checklists = await this.loadChecklists()
|
|
716
689
|
|
|
@@ -725,6 +698,16 @@ class PromptBuilder {
|
|
|
725
698
|
}
|
|
726
699
|
}
|
|
727
700
|
|
|
701
|
+
// PRJ-264: Output schema injection for structured responses
|
|
702
|
+
const schemaType = this.getSchemaTypeForCommand(commandName)
|
|
703
|
+
if (schemaType) {
|
|
704
|
+
const { renderSchemaForPrompt } = await import('../schemas/llm-output')
|
|
705
|
+
const schemaBlock = renderSchemaForPrompt(schemaType)
|
|
706
|
+
if (schemaBlock) {
|
|
707
|
+
parts.push(`\n${schemaBlock}\n`)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
728
711
|
// Simple execution directive
|
|
729
712
|
parts.push('\nEXECUTE: Follow flow. Use tools. Decide.\n')
|
|
730
713
|
|
|
@@ -802,6 +785,18 @@ class PromptBuilder {
|
|
|
802
785
|
return result || null
|
|
803
786
|
}
|
|
804
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Map command names to their expected output schema type.
|
|
790
|
+
* Returns null for commands that don't need structured output.
|
|
791
|
+
*/
|
|
792
|
+
private getSchemaTypeForCommand(commandName: string): string | null {
|
|
793
|
+
const schemaMap: Record<string, string> = {
|
|
794
|
+
task: 'subtaskBreakdown',
|
|
795
|
+
bug: 'classification',
|
|
796
|
+
}
|
|
797
|
+
return schemaMap[commandName] ?? null
|
|
798
|
+
}
|
|
799
|
+
|
|
805
800
|
/**
|
|
806
801
|
* Build critical anti-hallucination rules section
|
|
807
802
|
*/
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Validator
|
|
3
|
+
*
|
|
4
|
+
* Validates LLM responses against Zod schemas.
|
|
5
|
+
* Provides structured error handling with re-prompt support.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Parse raw text as JSON
|
|
9
|
+
* 2. Validate against Zod schema
|
|
10
|
+
* 3. On success: return typed data
|
|
11
|
+
* 4. On failure: return validation errors for re-prompt or fallback
|
|
12
|
+
*
|
|
13
|
+
* @see PRJ-264
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { z } from 'zod'
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export interface ValidationSuccess<T> {
|
|
23
|
+
success: true
|
|
24
|
+
data: T
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidationFailure {
|
|
28
|
+
success: false
|
|
29
|
+
error: string
|
|
30
|
+
/** Raw parsed JSON (may be partial) */
|
|
31
|
+
rawParsed: unknown
|
|
32
|
+
/** Zod validation issues */
|
|
33
|
+
issues: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Core Validation
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate a raw LLM response string against a Zod schema.
|
|
44
|
+
*
|
|
45
|
+
* Handles:
|
|
46
|
+
* - JSON parse errors (LLM returned non-JSON)
|
|
47
|
+
* - Markdown-wrapped JSON (```json ... ```)
|
|
48
|
+
* - Schema validation errors (wrong fields, types)
|
|
49
|
+
*/
|
|
50
|
+
export function validateLLMResponse<T>(raw: string, schema: z.ZodType<T>): ValidationResult<T> {
|
|
51
|
+
// Strip markdown code fences if present
|
|
52
|
+
let jsonText = raw.trim()
|
|
53
|
+
const fenceMatch = jsonText.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/)
|
|
54
|
+
if (fenceMatch) {
|
|
55
|
+
jsonText = fenceMatch[1].trim()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Attempt JSON parse
|
|
59
|
+
let parsed: unknown
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(jsonText)
|
|
62
|
+
} catch {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'Response is not valid JSON',
|
|
66
|
+
rawParsed: null,
|
|
67
|
+
issues: [`JSON parse error: expected JSON, got: ${jsonText.slice(0, 100)}...`],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate against schema
|
|
72
|
+
const result = schema.safeParse(parsed)
|
|
73
|
+
if (result.success) {
|
|
74
|
+
return { success: true, data: result.data }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Extract readable error messages
|
|
78
|
+
const issues = result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: `Schema validation failed: ${issues.join('; ')}`,
|
|
83
|
+
rawParsed: parsed,
|
|
84
|
+
issues,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a re-prompt message when validation fails.
|
|
90
|
+
* Includes the original error so the LLM can fix its response.
|
|
91
|
+
*/
|
|
92
|
+
export function buildReprompt(failure: ValidationFailure, schemaExample: string): string {
|
|
93
|
+
return `Your previous response was not valid. Errors:
|
|
94
|
+
${failure.issues.map((i) => `- ${i}`).join('\n')}
|
|
95
|
+
|
|
96
|
+
Return ONLY valid JSON matching this exact format (no markdown, no explanation):
|
|
97
|
+
${schemaExample}`
|
|
98
|
+
}
|