tlc-claude-code 1.4.1 → 1.4.4
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/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +3 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +5 -2
- package/server/dashboard/index.html +1336 -779
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createRouter,
|
|
4
|
+
resolveProvider,
|
|
5
|
+
resolveCapability,
|
|
6
|
+
loadConfig,
|
|
7
|
+
DEFAULT_CONFIG,
|
|
8
|
+
} from './model-router.js';
|
|
9
|
+
|
|
10
|
+
// Mock dependencies
|
|
11
|
+
vi.mock('./cli-detector.js', () => ({
|
|
12
|
+
detectAllCLIs: vi.fn(),
|
|
13
|
+
clearCache: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('fs/promises', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
readFile: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
import { detectAllCLIs } from './cli-detector.js';
|
|
23
|
+
import fs from 'fs/promises';
|
|
24
|
+
|
|
25
|
+
describe('model-router', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('createRouter', () => {
|
|
31
|
+
it('creates router with default config', async () => {
|
|
32
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
33
|
+
|
|
34
|
+
const router = await createRouter();
|
|
35
|
+
|
|
36
|
+
expect(router).toBeDefined();
|
|
37
|
+
expect(router.resolveProvider).toBeDefined();
|
|
38
|
+
expect(router.resolveCapability).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('creates router with custom config', async () => {
|
|
42
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
43
|
+
|
|
44
|
+
const config = {
|
|
45
|
+
providers: {
|
|
46
|
+
claude: { type: 'cli', command: 'claude' },
|
|
47
|
+
},
|
|
48
|
+
capabilities: {
|
|
49
|
+
review: { providers: ['claude'] },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const router = await createRouter(config);
|
|
54
|
+
|
|
55
|
+
expect(router).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('detects local CLIs on creation', async () => {
|
|
59
|
+
detectAllCLIs.mockResolvedValue(new Map([
|
|
60
|
+
['claude', { name: 'claude', version: 'v4.0.0' }],
|
|
61
|
+
]));
|
|
62
|
+
|
|
63
|
+
await createRouter();
|
|
64
|
+
|
|
65
|
+
expect(detectAllCLIs).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('resolveProvider', () => {
|
|
70
|
+
it('returns local when CLI detected', async () => {
|
|
71
|
+
detectAllCLIs.mockResolvedValue(new Map([
|
|
72
|
+
['claude', { name: 'claude', version: 'v4.0.0', detected: true }],
|
|
73
|
+
]));
|
|
74
|
+
|
|
75
|
+
const router = await createRouter({
|
|
76
|
+
providers: {
|
|
77
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = router.resolveProvider('claude');
|
|
82
|
+
|
|
83
|
+
expect(result.via).toBe('local');
|
|
84
|
+
expect(result.provider.name).toBe('claude');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns devserver when CLI not detected', async () => {
|
|
88
|
+
detectAllCLIs.mockResolvedValue(new Map()); // No CLIs detected
|
|
89
|
+
|
|
90
|
+
const router = await createRouter({
|
|
91
|
+
providers: {
|
|
92
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
93
|
+
},
|
|
94
|
+
devserver: { url: 'https://devserver.example.com' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = router.resolveProvider('claude');
|
|
98
|
+
|
|
99
|
+
expect(result.via).toBe('devserver');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns devserver for API type', async () => {
|
|
103
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
104
|
+
|
|
105
|
+
const router = await createRouter({
|
|
106
|
+
providers: {
|
|
107
|
+
deepseek: {
|
|
108
|
+
type: 'api',
|
|
109
|
+
baseUrl: 'https://api.deepseek.com',
|
|
110
|
+
capabilities: ['review'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = router.resolveProvider('deepseek');
|
|
116
|
+
|
|
117
|
+
expect(result.via).toBe('devserver');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns null for unknown provider', async () => {
|
|
121
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
122
|
+
|
|
123
|
+
const router = await createRouter();
|
|
124
|
+
|
|
125
|
+
const result = router.resolveProvider('unknown');
|
|
126
|
+
|
|
127
|
+
expect(result).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('resolveCapability', () => {
|
|
132
|
+
it('returns ordered providers for capability', async () => {
|
|
133
|
+
detectAllCLIs.mockResolvedValue(new Map([
|
|
134
|
+
['claude', { name: 'claude' }],
|
|
135
|
+
]));
|
|
136
|
+
|
|
137
|
+
const router = await createRouter({
|
|
138
|
+
providers: {
|
|
139
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
140
|
+
deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com', capabilities: ['review'] },
|
|
141
|
+
},
|
|
142
|
+
capabilities: {
|
|
143
|
+
review: { providers: ['claude', 'deepseek'] },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const providers = router.resolveCapability('review');
|
|
148
|
+
|
|
149
|
+
expect(providers.length).toBe(2);
|
|
150
|
+
expect(providers[0].name).toBe('claude');
|
|
151
|
+
expect(providers[1].name).toBe('deepseek');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('filters by capability', async () => {
|
|
155
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
156
|
+
|
|
157
|
+
const router = await createRouter({
|
|
158
|
+
providers: {
|
|
159
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review', 'code-gen'] },
|
|
160
|
+
gemini: { type: 'cli', command: 'gemini', capabilities: ['design'] },
|
|
161
|
+
},
|
|
162
|
+
capabilities: {
|
|
163
|
+
review: { providers: ['claude'] },
|
|
164
|
+
design: { providers: ['gemini'] },
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const reviewProviders = router.resolveCapability('review');
|
|
169
|
+
const designProviders = router.resolveCapability('design');
|
|
170
|
+
|
|
171
|
+
expect(reviewProviders.length).toBe(1);
|
|
172
|
+
expect(reviewProviders[0].name).toBe('claude');
|
|
173
|
+
|
|
174
|
+
expect(designProviders.length).toBe(1);
|
|
175
|
+
expect(designProviders[0].name).toBe('gemini');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns empty array for unknown capability', async () => {
|
|
179
|
+
detectAllCLIs.mockResolvedValue(new Map());
|
|
180
|
+
|
|
181
|
+
const router = await createRouter();
|
|
182
|
+
|
|
183
|
+
const providers = router.resolveCapability('unknown');
|
|
184
|
+
|
|
185
|
+
expect(providers).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('cascade behavior', () => {
|
|
190
|
+
it('tries local first', async () => {
|
|
191
|
+
detectAllCLIs.mockResolvedValue(new Map([
|
|
192
|
+
['claude', { name: 'claude', detected: true }],
|
|
193
|
+
]));
|
|
194
|
+
|
|
195
|
+
const router = await createRouter({
|
|
196
|
+
providers: {
|
|
197
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
198
|
+
},
|
|
199
|
+
devserver: { url: 'https://devserver.example.com' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = router.resolveProvider('claude');
|
|
203
|
+
|
|
204
|
+
expect(result.via).toBe('local');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('falls back to devserver when local unavailable', async () => {
|
|
208
|
+
detectAllCLIs.mockResolvedValue(new Map()); // No CLIs detected
|
|
209
|
+
|
|
210
|
+
const router = await createRouter({
|
|
211
|
+
providers: {
|
|
212
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
213
|
+
},
|
|
214
|
+
devserver: { url: 'https://devserver.example.com' },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const result = router.resolveProvider('claude');
|
|
218
|
+
|
|
219
|
+
expect(result.via).toBe('devserver');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('loadConfig', () => {
|
|
224
|
+
it('reads from .tlc.json', async () => {
|
|
225
|
+
const config = {
|
|
226
|
+
router: {
|
|
227
|
+
providers: { claude: { type: 'cli', command: 'claude' } },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
fs.readFile.mockResolvedValue(JSON.stringify(config));
|
|
232
|
+
|
|
233
|
+
const loaded = await loadConfig('/project');
|
|
234
|
+
|
|
235
|
+
expect(fs.readFile).toHaveBeenCalledWith('/project/.tlc.json', 'utf8');
|
|
236
|
+
expect(loaded.providers.claude).toBeDefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('uses defaults when file missing', async () => {
|
|
240
|
+
fs.readFile.mockRejectedValue(new Error('ENOENT'));
|
|
241
|
+
|
|
242
|
+
const loaded = await loadConfig('/project');
|
|
243
|
+
|
|
244
|
+
expect(loaded).toEqual(DEFAULT_CONFIG);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('merges with defaults', async () => {
|
|
248
|
+
const config = {
|
|
249
|
+
router: {
|
|
250
|
+
providers: { custom: { type: 'api', baseUrl: 'https://example.com' } },
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
fs.readFile.mockResolvedValue(JSON.stringify(config));
|
|
255
|
+
|
|
256
|
+
const loaded = await loadConfig('/project');
|
|
257
|
+
|
|
258
|
+
// Should have custom provider
|
|
259
|
+
expect(loaded.providers.custom).toBeDefined();
|
|
260
|
+
// Should still have defaults
|
|
261
|
+
expect(loaded.providers.claude).toBeDefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('DEFAULT_CONFIG', () => {
|
|
266
|
+
it('includes claude provider', () => {
|
|
267
|
+
expect(DEFAULT_CONFIG.providers.claude).toBeDefined();
|
|
268
|
+
expect(DEFAULT_CONFIG.providers.claude.type).toBe('cli');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('includes codex provider', () => {
|
|
272
|
+
expect(DEFAULT_CONFIG.providers.codex).toBeDefined();
|
|
273
|
+
expect(DEFAULT_CONFIG.providers.codex.type).toBe('cli');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('includes gemini provider', () => {
|
|
277
|
+
expect(DEFAULT_CONFIG.providers.gemini).toBeDefined();
|
|
278
|
+
expect(DEFAULT_CONFIG.providers.gemini.type).toBe('cli');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('includes deepseek provider', () => {
|
|
282
|
+
expect(DEFAULT_CONFIG.providers.deepseek).toBeDefined();
|
|
283
|
+
expect(DEFAULT_CONFIG.providers.deepseek.type).toBe('api');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('includes review capability', () => {
|
|
287
|
+
expect(DEFAULT_CONFIG.capabilities.review).toBeDefined();
|
|
288
|
+
expect(DEFAULT_CONFIG.capabilities.review.providers).toContain('claude');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('handleUnavailable', () => {
|
|
293
|
+
it('skips unavailable providers', async () => {
|
|
294
|
+
detectAllCLIs.mockResolvedValue(new Map()); // No local CLIs
|
|
295
|
+
|
|
296
|
+
const router = await createRouter({
|
|
297
|
+
providers: {
|
|
298
|
+
claude: { type: 'cli', command: 'claude', capabilities: ['review'] },
|
|
299
|
+
deepseek: { type: 'api', baseUrl: 'https://api.deepseek.com', capabilities: ['review'] },
|
|
300
|
+
},
|
|
301
|
+
capabilities: {
|
|
302
|
+
review: { providers: ['claude', 'deepseek'] },
|
|
303
|
+
},
|
|
304
|
+
devserver: { url: 'https://devserver.example.com' },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const providers = router.resolveCapability('review');
|
|
308
|
+
|
|
309
|
+
// Both should be available (claude via devserver, deepseek via api)
|
|
310
|
+
expect(providers.length).toBe(2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Schemas - Standard JSON schemas for provider outputs
|
|
3
|
+
*
|
|
4
|
+
* Ensures consistent output format across all providers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Built-in schemas for common operations
|
|
11
|
+
*/
|
|
12
|
+
export const BUILTIN_SCHEMAS = {
|
|
13
|
+
'review-result': {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
summary: { type: 'string', description: 'Brief summary of the review' },
|
|
17
|
+
issues: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
severity: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ['critical', 'moderate', 'suggestion'],
|
|
25
|
+
description: 'Issue severity level',
|
|
26
|
+
},
|
|
27
|
+
file: { type: 'string', description: 'File path' },
|
|
28
|
+
line: { type: 'integer', description: 'Line number' },
|
|
29
|
+
title: { type: 'string', description: 'Issue title' },
|
|
30
|
+
description: { type: 'string', description: 'Detailed description' },
|
|
31
|
+
suggestion: { type: 'string', description: 'Suggested fix' },
|
|
32
|
+
},
|
|
33
|
+
required: ['severity', 'file', 'title', 'description'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
score: {
|
|
37
|
+
type: 'integer',
|
|
38
|
+
minimum: 0,
|
|
39
|
+
maximum: 100,
|
|
40
|
+
description: 'Overall code quality score',
|
|
41
|
+
},
|
|
42
|
+
approved: { type: 'boolean', description: 'Whether the code is approved' },
|
|
43
|
+
},
|
|
44
|
+
required: ['summary', 'issues', 'score', 'approved'],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
'design-result': {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
mockups: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
name: { type: 'string', description: 'Mockup name' },
|
|
56
|
+
description: { type: 'string', description: 'Mockup description' },
|
|
57
|
+
imageUrl: { type: 'string', description: 'Generated image URL' },
|
|
58
|
+
components: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
items: { type: 'string' },
|
|
61
|
+
description: 'UI components used',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ['name', 'description'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
rationale: { type: 'string', description: 'Design rationale' },
|
|
68
|
+
alternatives: {
|
|
69
|
+
type: 'array',
|
|
70
|
+
items: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
name: { type: 'string' },
|
|
74
|
+
description: { type: 'string' },
|
|
75
|
+
tradeoffs: { type: 'string' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
description: 'Alternative design approaches',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['mockups', 'rationale'],
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
'code-result': {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
files: {
|
|
88
|
+
type: 'array',
|
|
89
|
+
items: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
path: { type: 'string', description: 'File path' },
|
|
93
|
+
content: { type: 'string', description: 'File content' },
|
|
94
|
+
action: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
enum: ['create', 'modify', 'delete'],
|
|
97
|
+
description: 'Action to take',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
required: ['path', 'content', 'action'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
explanation: { type: 'string', description: 'Explanation of changes' },
|
|
104
|
+
tests: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
name: { type: 'string' },
|
|
110
|
+
description: { type: 'string' },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
description: 'Suggested tests',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['files', 'explanation'],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load a schema from file
|
|
122
|
+
* @param {string} filePath - Path to schema file
|
|
123
|
+
* @returns {Promise<Object>} Parsed schema
|
|
124
|
+
*/
|
|
125
|
+
export async function loadSchema(filePath) {
|
|
126
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate output against a schema
|
|
132
|
+
* @param {any} data - Data to validate
|
|
133
|
+
* @param {Object} schema - JSON schema
|
|
134
|
+
* @returns {Object} Validation result { valid, errors }
|
|
135
|
+
*/
|
|
136
|
+
export function validateOutput(data, schema) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
|
|
139
|
+
function validate(value, schemaNode, path = '') {
|
|
140
|
+
if (!schemaNode) return;
|
|
141
|
+
|
|
142
|
+
// Type validation
|
|
143
|
+
if (schemaNode.type) {
|
|
144
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
145
|
+
const expectedType = schemaNode.type;
|
|
146
|
+
|
|
147
|
+
// Handle integer as number
|
|
148
|
+
if (expectedType === 'integer') {
|
|
149
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
150
|
+
errors.push(`${path}: expected integer, got ${actualType}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
} else if (expectedType !== actualType) {
|
|
154
|
+
errors.push(`${path}: expected ${expectedType}, got ${actualType}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Enum validation
|
|
160
|
+
if (schemaNode.enum && !schemaNode.enum.includes(value)) {
|
|
161
|
+
errors.push(`${path}: value must be one of: ${schemaNode.enum.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Number constraints
|
|
165
|
+
if (typeof value === 'number') {
|
|
166
|
+
if (schemaNode.minimum !== undefined && value < schemaNode.minimum) {
|
|
167
|
+
errors.push(`${path}: value must be >= ${schemaNode.minimum}`);
|
|
168
|
+
}
|
|
169
|
+
if (schemaNode.maximum !== undefined && value > schemaNode.maximum) {
|
|
170
|
+
errors.push(`${path}: value must be <= ${schemaNode.maximum}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Object validation
|
|
175
|
+
if (schemaNode.type === 'object' && typeof value === 'object' && value !== null) {
|
|
176
|
+
// Required fields
|
|
177
|
+
if (schemaNode.required) {
|
|
178
|
+
for (const field of schemaNode.required) {
|
|
179
|
+
if (!(field in value)) {
|
|
180
|
+
errors.push(`${path}.${field}: required field missing`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate properties
|
|
186
|
+
if (schemaNode.properties) {
|
|
187
|
+
for (const [key, propSchema] of Object.entries(schemaNode.properties)) {
|
|
188
|
+
if (key in value) {
|
|
189
|
+
validate(value[key], propSchema, `${path}.${key}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Array validation
|
|
196
|
+
if (schemaNode.type === 'array' && Array.isArray(value)) {
|
|
197
|
+
if (schemaNode.items) {
|
|
198
|
+
value.forEach((item, index) => {
|
|
199
|
+
validate(item, schemaNode.items, `${path}[${index}]`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
validate(data, schema, 'root');
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
valid: errors.length === 0,
|
|
209
|
+
errors,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Convert schema to human-readable prompt instructions
|
|
215
|
+
* @param {Object} schema - JSON schema
|
|
216
|
+
* @returns {string} Text instructions
|
|
217
|
+
*/
|
|
218
|
+
export function schemaToPromptInstructions(schema) {
|
|
219
|
+
const lines = ['Your response must be valid JSON matching this structure:'];
|
|
220
|
+
|
|
221
|
+
function describeSchema(node, indent = 0) {
|
|
222
|
+
const prefix = ' '.repeat(indent);
|
|
223
|
+
|
|
224
|
+
if (node.type === 'object' && node.properties) {
|
|
225
|
+
lines.push(`${prefix}{`);
|
|
226
|
+
|
|
227
|
+
for (const [key, prop] of Object.entries(node.properties)) {
|
|
228
|
+
const required = node.required?.includes(key) ? ' (required)' : '';
|
|
229
|
+
const type = prop.type || 'any';
|
|
230
|
+
|
|
231
|
+
if (prop.enum) {
|
|
232
|
+
lines.push(`${prefix} "${key}": one of [${prop.enum.join(', ')}]${required}`);
|
|
233
|
+
} else if (type === 'object' && prop.properties) {
|
|
234
|
+
lines.push(`${prefix} "${key}": {${required}`);
|
|
235
|
+
describeSchema(prop, indent + 2);
|
|
236
|
+
lines.push(`${prefix} }`);
|
|
237
|
+
} else if (type === 'array') {
|
|
238
|
+
lines.push(`${prefix} "${key}": array of ${prop.items?.type || 'items'}${required}`);
|
|
239
|
+
} else {
|
|
240
|
+
lines.push(`${prefix} "${key}": ${type}${required}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
lines.push(`${prefix}}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
describeSchema(schema);
|
|
249
|
+
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build a prompt that includes schema instructions
|
|
255
|
+
* @param {string} prompt - Original prompt
|
|
256
|
+
* @param {Object} schema - JSON schema
|
|
257
|
+
* @returns {string} Enhanced prompt
|
|
258
|
+
*/
|
|
259
|
+
export function buildPromptWithSchema(prompt, schema) {
|
|
260
|
+
if (!schema) return prompt;
|
|
261
|
+
|
|
262
|
+
const instructions = schemaToPromptInstructions(schema);
|
|
263
|
+
|
|
264
|
+
return `${prompt}
|
|
265
|
+
|
|
266
|
+
${instructions}
|
|
267
|
+
|
|
268
|
+
Respond ONLY with valid JSON matching the above structure.`;
|
|
269
|
+
}
|