@auto-engineer/component-implementer 0.10.5 → 0.11.10
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 +8 -0
- package/dist/src/agent-cli.js +1 -1
- package/dist/src/agent.d.ts +1 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/commands/implement-component.d.ts +17 -2
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +407 -64
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/index.d.ts +15 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/commands/implement-component.ts +473 -79
package/package.json
CHANGED
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"openai": "^5.7.0",
|
|
20
20
|
"vite": "^5.4.1",
|
|
21
21
|
"zod": "^3.25.67",
|
|
22
|
-
"@auto-engineer/
|
|
23
|
-
"@auto-engineer/
|
|
22
|
+
"@auto-engineer/message-bus": "0.11.10",
|
|
23
|
+
"@auto-engineer/ai-gateway": "0.11.10"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"playwright": "^1.54.1",
|
|
27
|
-
"@auto-engineer/cli": "0.11.
|
|
27
|
+
"@auto-engineer/cli": "0.11.10"
|
|
28
28
|
},
|
|
29
|
-
"version": "0.10
|
|
29
|
+
"version": "0.11.10",
|
|
30
30
|
"scripts": {
|
|
31
31
|
"start": "tsx src/index.ts",
|
|
32
32
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts && cp src/*.html dist/src/ 2>/dev/null || true",
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
2
|
+
|
|
1
3
|
import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
|
|
2
4
|
import * as fs from 'fs/promises';
|
|
3
5
|
import * as path from 'path';
|
|
4
6
|
import createDebug from 'debug';
|
|
5
|
-
import { callAI,
|
|
7
|
+
import { callAI, loadScheme } from '../agent';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import { performance } from 'perf_hooks';
|
|
6
10
|
|
|
7
|
-
const debug = createDebug('
|
|
11
|
+
const debug = createDebug('auto:client-implementer:component');
|
|
12
|
+
const debugTypeCheck = createDebug('auto:client-implementer:component:typecheck');
|
|
13
|
+
const debugProcess = createDebug('auto:client-implementer:component:process');
|
|
14
|
+
const debugResult = createDebug('auto:client-implementer:component:result');
|
|
8
15
|
|
|
9
16
|
export type ImplementComponentCommand = Command<
|
|
10
17
|
'ImplementComponent',
|
|
@@ -13,6 +20,7 @@ export type ImplementComponentCommand = Command<
|
|
|
13
20
|
iaSchemeDir: string;
|
|
14
21
|
designSystemPath: string;
|
|
15
22
|
componentType: 'atom' | 'molecule' | 'organism' | 'page';
|
|
23
|
+
filePath: string;
|
|
16
24
|
componentName: string;
|
|
17
25
|
failures?: string[];
|
|
18
26
|
}
|
|
@@ -35,6 +43,7 @@ export type ComponentImplementationFailedEvent = Event<
|
|
|
35
43
|
error: string;
|
|
36
44
|
componentType: string;
|
|
37
45
|
componentName: string;
|
|
46
|
+
filePath: string;
|
|
38
47
|
}
|
|
39
48
|
>;
|
|
40
49
|
|
|
@@ -55,11 +64,9 @@ export const commandHandler = defineCommandHandler<
|
|
|
55
64
|
description: 'Type of component: atom|molecule|organism|page',
|
|
56
65
|
required: true,
|
|
57
66
|
},
|
|
67
|
+
filePath: { description: 'Component file path', required: true },
|
|
58
68
|
componentName: { description: 'Name of component to implement', required: true },
|
|
59
|
-
failures: {
|
|
60
|
-
description: 'Any failures from previous implementations',
|
|
61
|
-
required: false,
|
|
62
|
-
},
|
|
69
|
+
failures: { description: 'Any failures from previous implementations', required: false },
|
|
63
70
|
},
|
|
64
71
|
examples: [
|
|
65
72
|
'$ auto implement:component --project-dir=./client --ia-scheme-dir=./.context --design-system-path=./design-system.md --component-type=molecule --component-name=SurveyCard',
|
|
@@ -70,78 +77,139 @@ export const commandHandler = defineCommandHandler<
|
|
|
70
77
|
): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> => {
|
|
71
78
|
const result = await handleImplementComponentCommandInternal(command);
|
|
72
79
|
if (result.type === 'ComponentImplemented') {
|
|
73
|
-
debug(
|
|
74
|
-
'Component implemented: %s/%s at %s',
|
|
75
|
-
result.data.componentType,
|
|
76
|
-
result.data.componentName,
|
|
77
|
-
result.data.filePath,
|
|
78
|
-
);
|
|
80
|
+
debug('Component implemented successfully: %s/%s', result.data.componentType, result.data.componentName);
|
|
79
81
|
} else {
|
|
80
|
-
debug('
|
|
82
|
+
debug('Component implementation failed: %s', result.data.error);
|
|
81
83
|
}
|
|
82
84
|
return result;
|
|
83
85
|
},
|
|
84
86
|
});
|
|
85
87
|
|
|
88
|
+
// eslint-disable-next-line complexity
|
|
86
89
|
async function handleImplementComponentCommandInternal(
|
|
87
90
|
command: ImplementComponentCommand,
|
|
88
91
|
): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
|
|
89
|
-
const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName,
|
|
92
|
+
const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
|
|
90
93
|
|
|
91
94
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
fs.readFile(userPreferencesFile, 'utf-8'),
|
|
95
|
-
fs.readFile(designSystemPath, 'utf-8'),
|
|
96
|
-
]);
|
|
95
|
+
const start = performance.now();
|
|
96
|
+
debugProcess(`Starting ${componentType}:${componentName}`);
|
|
97
97
|
|
|
98
|
+
const t1 = performance.now();
|
|
98
99
|
const scheme = await loadScheme(iaSchemeDir);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
100
|
+
debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
|
|
101
|
+
if (!scheme) throw new Error('IA scheme not found');
|
|
102
102
|
|
|
103
103
|
const pluralKey = `${componentType}s`;
|
|
104
104
|
const collection = (scheme as Record<string, unknown>)[pluralKey];
|
|
105
|
-
if (!isValidCollection(collection)) {
|
|
106
|
-
throw new Error(`Invalid IA schema structure for ${pluralKey}`);
|
|
107
|
-
}
|
|
105
|
+
if (!isValidCollection(collection)) throw new Error(`Invalid IA schema structure for ${pluralKey}`);
|
|
108
106
|
|
|
109
107
|
const items = (collection as { items: Record<string, unknown> }).items;
|
|
110
108
|
const componentDef = items[componentName] as Record<string, unknown> | undefined;
|
|
111
|
-
if (!componentDef) {
|
|
112
|
-
|
|
109
|
+
if (!componentDef) throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
|
|
110
|
+
|
|
111
|
+
const outPath = path.join(projectDir, '..', filePath);
|
|
112
|
+
|
|
113
|
+
const t2 = performance.now();
|
|
114
|
+
let existingScaffold = '';
|
|
115
|
+
try {
|
|
116
|
+
existingScaffold = await fs.readFile(outPath, 'utf-8');
|
|
117
|
+
debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
|
|
118
|
+
} catch {
|
|
119
|
+
debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
const
|
|
122
|
+
const t3 = performance.now();
|
|
123
|
+
const projectConfig = await readAllTopLevelFiles(projectDir);
|
|
124
|
+
debugProcess(`[3] Loaded project + gql/graphql files in ${(performance.now() - t3).toFixed(2)} ms`);
|
|
116
125
|
|
|
117
|
-
const
|
|
126
|
+
const t4 = performance.now();
|
|
127
|
+
const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
|
|
128
|
+
debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
|
|
118
129
|
|
|
119
|
-
const
|
|
130
|
+
const dependencyList = await resolveDependenciesRecursively(
|
|
131
|
+
scheme as Record<string, unknown>,
|
|
132
|
+
componentType,
|
|
133
|
+
componentName,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
debugProcess(`[5] Resolved ${dependencyList.length} dependencies for ${componentName}`);
|
|
137
|
+
|
|
138
|
+
const dependencySources: Record<string, string> = {};
|
|
139
|
+
for (const dep of dependencyList) {
|
|
140
|
+
const depSource = await readComponentSource(projectDir, dep.type, dep.name);
|
|
141
|
+
if (depSource != null) dependencySources[`${dep.type}/${dep.name}`] = depSource;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const basePrompt = makeBasePrompt(
|
|
145
|
+
componentType,
|
|
146
|
+
componentName,
|
|
147
|
+
componentDef,
|
|
148
|
+
existingScaffold,
|
|
149
|
+
projectConfig,
|
|
150
|
+
designSystemReference,
|
|
151
|
+
dependencySources,
|
|
152
|
+
);
|
|
120
153
|
|
|
121
|
-
const outPath = path.join(projectDir, 'src/components', `${componentType}s`, `${componentName}.tsx`);
|
|
122
154
|
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
123
|
-
await fs.writeFile(outPath, code, 'utf-8');
|
|
124
155
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
let attempt = 1;
|
|
157
|
+
let code = '';
|
|
158
|
+
let lastErrors = '';
|
|
159
|
+
const maxAttempts = 3;
|
|
160
|
+
|
|
161
|
+
while (attempt <= maxAttempts) {
|
|
162
|
+
const genStart = performance.now();
|
|
163
|
+
const prompt =
|
|
164
|
+
attempt === 1
|
|
165
|
+
? makeImplementPrompt(basePrompt)
|
|
166
|
+
: makeRetryPrompt(basePrompt, componentType, componentName, code, lastErrors);
|
|
167
|
+
|
|
168
|
+
const aiRaw = await callAI(prompt);
|
|
169
|
+
code = extractCodeBlock(aiRaw);
|
|
170
|
+
await fs.writeFile(outPath, code, 'utf-8');
|
|
171
|
+
debugProcess(
|
|
172
|
+
`[6.${attempt}] AI output written (${code.length} chars) in ${(performance.now() - genStart).toFixed(2)} ms`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const checkStart = performance.now();
|
|
176
|
+
const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
|
|
177
|
+
debugTypeCheck(
|
|
178
|
+
`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (success) {
|
|
182
|
+
debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
|
|
183
|
+
return {
|
|
184
|
+
type: 'ComponentImplemented',
|
|
185
|
+
data: {
|
|
186
|
+
filePath: outPath,
|
|
187
|
+
componentType,
|
|
188
|
+
componentName,
|
|
189
|
+
composition: extractComposition(componentDef),
|
|
190
|
+
specs: extractSpecs(componentDef),
|
|
191
|
+
},
|
|
192
|
+
timestamp: new Date(),
|
|
193
|
+
requestId: command.requestId,
|
|
194
|
+
correlationId: command.correlationId,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
lastErrors = errors;
|
|
199
|
+
if (attempt === maxAttempts) throw new Error(`Type errors persist after ${attempt} attempts:\n${errors}`);
|
|
200
|
+
attempt += 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw new Error('Unreachable state');
|
|
138
204
|
} catch (error: unknown) {
|
|
205
|
+
debug('[Error] Component implementation failed: %O', error);
|
|
139
206
|
return {
|
|
140
207
|
type: 'ComponentImplementationFailed',
|
|
141
208
|
data: {
|
|
142
209
|
error: error instanceof Error ? error.message : String(error),
|
|
143
210
|
componentType,
|
|
144
211
|
componentName,
|
|
212
|
+
filePath,
|
|
145
213
|
},
|
|
146
214
|
timestamp: new Date(),
|
|
147
215
|
requestId: command.requestId,
|
|
@@ -150,67 +218,393 @@ async function handleImplementComponentCommandInternal(
|
|
|
150
218
|
}
|
|
151
219
|
}
|
|
152
220
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
221
|
+
// eslint-disable-next-line complexity
|
|
222
|
+
async function resolveDependenciesRecursively(
|
|
223
|
+
scheme: Record<string, unknown>,
|
|
224
|
+
type: string,
|
|
225
|
+
name: string,
|
|
226
|
+
visited: Set<string> = new Set(),
|
|
227
|
+
): Promise<{ type: string; name: string }[]> {
|
|
228
|
+
const key = `${type}:${name}`;
|
|
229
|
+
if (visited.has(key)) return [];
|
|
230
|
+
visited.add(key);
|
|
231
|
+
|
|
232
|
+
const collection = scheme[`${type}s`];
|
|
233
|
+
//
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
235
|
+
if (!collection || !isValidCollection(collection)) return [];
|
|
236
|
+
|
|
237
|
+
const def = collection.items[name];
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
239
|
+
if (!def || typeof def !== 'object' || !('composition' in def)) return [];
|
|
240
|
+
|
|
241
|
+
const result: { type: string; name: string }[] = [];
|
|
242
|
+
|
|
243
|
+
const composition = def.composition as Record<string, string[]>;
|
|
244
|
+
for (const [subType, subNames] of Object.entries(composition)) {
|
|
245
|
+
if (!Array.isArray(subNames)) continue;
|
|
246
|
+
for (const subName of subNames) {
|
|
247
|
+
result.push({ type: subType, name: subName });
|
|
248
|
+
const nested = await resolveDependenciesRecursively(scheme, subType, subName, visited);
|
|
249
|
+
result.push(...nested);
|
|
158
250
|
}
|
|
159
|
-
|
|
160
|
-
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function readComponentSource(projectDir: string, type: string, name: string): Promise<string | null> {
|
|
256
|
+
const file = path.join(projectDir, 'src', 'components', type, `${name}.tsx`);
|
|
257
|
+
try {
|
|
258
|
+
return await fs.readFile(file, 'utf-8');
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function extractCodeBlock(text: string): string {
|
|
265
|
+
return text
|
|
266
|
+
.replace(/```(?:tsx|ts|typescript)?/g, '')
|
|
267
|
+
.replace(/```/g, '')
|
|
268
|
+
.trim();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function readAllTopLevelFiles(projectDir: string): Promise<Record<string, string>> {
|
|
272
|
+
debugProcess('[readAllTopLevelFiles] Reading project files from %s', projectDir);
|
|
273
|
+
const start = performance.now();
|
|
274
|
+
const config: Record<string, string> = {};
|
|
275
|
+
|
|
276
|
+
async function readRecursive(currentDir: string) {
|
|
277
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
280
|
+
const relativePath = path.relative(projectDir, fullPath);
|
|
281
|
+
if (entry.isDirectory()) {
|
|
282
|
+
if (['node_modules', 'dist', 'build', '.next', '.turbo'].includes(entry.name)) continue;
|
|
283
|
+
await readRecursive(fullPath);
|
|
284
|
+
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
|
|
285
|
+
try {
|
|
286
|
+
config[relativePath] = await fs.readFile(fullPath, 'utf-8');
|
|
287
|
+
} catch (err) {
|
|
288
|
+
debugProcess(`Failed to read ${relativePath}: ${(err as Error).message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
161
291
|
}
|
|
162
292
|
}
|
|
163
|
-
|
|
293
|
+
|
|
294
|
+
await readRecursive(projectDir);
|
|
295
|
+
debugProcess(`[readAllTopLevelFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
|
|
296
|
+
return config;
|
|
164
297
|
}
|
|
165
298
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
299
|
+
async function runTypeCheckForFile(
|
|
300
|
+
projectDir: string,
|
|
301
|
+
filePath: string,
|
|
302
|
+
): Promise<{ success: boolean; errors: string }> {
|
|
303
|
+
const start = performance.now();
|
|
304
|
+
try {
|
|
305
|
+
const tsconfigRoot = await findProjectRoot(projectDir);
|
|
306
|
+
const relativeFilePath = path.relative(tsconfigRoot, filePath).replace(/\\/g, '/');
|
|
307
|
+
const normalizedRelative = relativeFilePath.replace(/^client\//, '');
|
|
308
|
+
const result = await execa('npx', ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'], {
|
|
309
|
+
cwd: tsconfigRoot,
|
|
310
|
+
stdio: 'pipe',
|
|
311
|
+
reject: false,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const output = (result.stdout ?? '') + (result.stderr ?? '');
|
|
315
|
+
debugTypeCheck(`[runTypeCheckForFile] Finished tsc in ${(performance.now() - start).toFixed(2)} ms`);
|
|
316
|
+
|
|
317
|
+
if (result.exitCode === 0 && !output.includes('error TS')) return { success: true, errors: '' };
|
|
318
|
+
|
|
319
|
+
const filteredErrors = output
|
|
320
|
+
.split('\n')
|
|
321
|
+
.filter((line) => {
|
|
322
|
+
const hasError = line.includes('error TS');
|
|
323
|
+
const notNodeModules = !line.includes('node_modules');
|
|
324
|
+
const matchesTarget =
|
|
325
|
+
line.includes(relativeFilePath) ||
|
|
326
|
+
line.includes(normalizedRelative) ||
|
|
327
|
+
line.includes(path.basename(filePath));
|
|
328
|
+
return hasError && notNodeModules && matchesTarget;
|
|
329
|
+
})
|
|
330
|
+
.join('\n');
|
|
331
|
+
|
|
332
|
+
if (filteredErrors.trim().length === 0) return { success: true, errors: '' };
|
|
333
|
+
return { success: false, errors: filteredErrors };
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
+
return { success: false, errors: message };
|
|
169
337
|
}
|
|
170
|
-
return [];
|
|
171
338
|
}
|
|
172
339
|
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
340
|
+
async function findProjectRoot(startDir: string): Promise<string> {
|
|
341
|
+
let dir = startDir;
|
|
342
|
+
while (dir !== path.dirname(dir)) {
|
|
343
|
+
try {
|
|
344
|
+
await fs.access(path.join(dir, 'package.json'));
|
|
345
|
+
await fs.access(path.join(dir, 'tsconfig.json'));
|
|
346
|
+
return dir;
|
|
347
|
+
} catch {
|
|
348
|
+
dir = path.dirname(dir);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw new Error('Could not find project root (no package.json or tsconfig.json found)');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// eslint-disable-next-line complexity
|
|
355
|
+
function makeBasePrompt(
|
|
180
356
|
componentType: string,
|
|
181
357
|
componentName: string,
|
|
182
358
|
componentDef: Record<string, unknown>,
|
|
359
|
+
existingScaffold: string,
|
|
360
|
+
projectConfig: Record<string, string>,
|
|
361
|
+
designSystemReference: string,
|
|
362
|
+
dependencySources: Record<string, string>,
|
|
183
363
|
): string {
|
|
364
|
+
const hasScaffold = Boolean(existingScaffold?.trim());
|
|
365
|
+
|
|
366
|
+
const gqlFiles: Record<string, string> = {};
|
|
367
|
+
const graphqlFiles: Record<string, string> = {};
|
|
368
|
+
const otherFiles: Record<string, string> = {};
|
|
369
|
+
|
|
370
|
+
for (const [filePath, content] of Object.entries(projectConfig)) {
|
|
371
|
+
const lower = filePath.toLowerCase();
|
|
372
|
+
if (lower.includes('src/gql/')) gqlFiles[filePath] = content;
|
|
373
|
+
else if (lower.includes('src/graphql/')) graphqlFiles[filePath] = content;
|
|
374
|
+
else otherFiles[filePath] = content;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const queriesFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('queries.ts'))?.[1] ?? '';
|
|
378
|
+
const mutationsFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('mutations.ts'))?.[1] ?? '';
|
|
379
|
+
|
|
380
|
+
const gqlSection =
|
|
381
|
+
Object.entries(gqlFiles)
|
|
382
|
+
.map(([p, c]) => `### ${p}\n${c}`)
|
|
383
|
+
.join('\n\n') || '(No gql folder found)';
|
|
384
|
+
|
|
385
|
+
const graphqlSection =
|
|
386
|
+
Object.entries(graphqlFiles)
|
|
387
|
+
.map(([p, c]) => `### ${p}\n${c}`)
|
|
388
|
+
.join('\n\n') || '(No graphql folder found)';
|
|
389
|
+
|
|
390
|
+
const configSection =
|
|
391
|
+
Object.entries(otherFiles)
|
|
392
|
+
.map(([p, c]) => `### ${p}\n${c}`)
|
|
393
|
+
.join('\n\n') || '(No additional config files)';
|
|
394
|
+
|
|
395
|
+
const designSystemBlock = designSystemReference.trim()
|
|
396
|
+
? designSystemReference
|
|
397
|
+
: '(No design system content provided)';
|
|
398
|
+
|
|
399
|
+
const dependencySection =
|
|
400
|
+
Object.entries(dependencySources)
|
|
401
|
+
.map(([name, src]) => `### ${name}\n${src}`)
|
|
402
|
+
.join('\n\n') || '(No dependencies found)';
|
|
403
|
+
|
|
184
404
|
return `
|
|
185
|
-
|
|
186
|
-
|
|
405
|
+
# Implementation Brief: ${componentName} (${componentType})
|
|
406
|
+
|
|
407
|
+
You are a senior frontend engineer specializing in **React + TypeScript + Apollo Client**.
|
|
408
|
+
Your task is to build a visually excellent, type-safe, and production-ready ${componentType} component.
|
|
409
|
+
The goal is to deliver elegant, minimal, and robust code that integrates perfectly with the existing system.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Objective
|
|
414
|
+
Implement **${componentName}** as defined in the IA schema and design system.
|
|
415
|
+
Your component must:
|
|
416
|
+
- Compile cleanly with no TypeScript errors.
|
|
417
|
+
- Follow established design tokens, colors, and spacing.
|
|
418
|
+
- Be visually polished, responsive, and accessible.
|
|
419
|
+
- Reuse existing atoms/molecules/organisms wherever possible.
|
|
420
|
+
- Use valid imports only — no new dependencies or mock data.
|
|
187
421
|
|
|
188
|
-
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Project Context
|
|
425
|
+
|
|
426
|
+
**File Path:** src/components/${componentType}/${componentName}.tsx
|
|
427
|
+
**Purpose:** A reusable UI element connected to the GraphQL layer and design system.
|
|
428
|
+
|
|
429
|
+
### IA Schema
|
|
189
430
|
${JSON.stringify(componentDef, null, 2)}
|
|
190
431
|
|
|
191
|
-
|
|
192
|
-
${
|
|
432
|
+
### Existing Scaffold
|
|
433
|
+
${hasScaffold ? existingScaffold : '(No existing scaffold found)'}
|
|
434
|
+
|
|
435
|
+
### Design System Reference
|
|
436
|
+
${designSystemBlock}
|
|
437
|
+
|
|
438
|
+
### Related Components (Dependencies)
|
|
439
|
+
${dependencySection}
|
|
440
|
+
|
|
441
|
+
### GraphQL Context (src/graphql)
|
|
442
|
+
${graphqlSection}
|
|
443
|
+
|
|
444
|
+
#### queries.ts
|
|
445
|
+
${queriesFile || '(queries.ts not found)'}
|
|
446
|
+
|
|
447
|
+
#### mutations.ts
|
|
448
|
+
${mutationsFile || '(mutations.ts not found)'}
|
|
193
449
|
|
|
194
|
-
|
|
195
|
-
${
|
|
450
|
+
### GraphQL Codegen (src/gql)
|
|
451
|
+
${gqlSection}
|
|
196
452
|
|
|
197
|
-
|
|
198
|
-
${
|
|
453
|
+
### Other Relevant Files
|
|
454
|
+
${configSection}
|
|
199
455
|
|
|
200
|
-
|
|
201
|
-
${ctx.theme}
|
|
456
|
+
---
|
|
202
457
|
|
|
203
|
-
|
|
204
|
-
|
|
458
|
+
## Engineering Guidelines
|
|
459
|
+
|
|
460
|
+
**Type Safety**
|
|
461
|
+
- Explicitly type all props, state, and GraphQL responses.
|
|
462
|
+
- Avoid \`any\` — prefer discriminated unions, interfaces, and generics.
|
|
463
|
+
|
|
464
|
+
**React Practices**
|
|
465
|
+
- Never call setState during render.
|
|
466
|
+
- Always use dependency arrays in effects.
|
|
467
|
+
- Memoize computed values and callbacks.
|
|
468
|
+
- Keep rendering pure and predictable.
|
|
469
|
+
|
|
470
|
+
**Error Handling**
|
|
471
|
+
- Wrap async operations in try/catch with graceful fallback UI.
|
|
472
|
+
- Check for null/undefined using optional chaining (?.) and defaults (??).
|
|
473
|
+
|
|
474
|
+
**Visual & UX Quality**
|
|
475
|
+
- Perfect spacing and alignment using Tailwind or the design system tokens.
|
|
476
|
+
- Add subtle hover, focus, and loading states.
|
|
477
|
+
- Use accessible HTML semantics and ARIA attributes.
|
|
478
|
+
- Animate with Framer Motion when appropriate.
|
|
479
|
+
|
|
480
|
+
**Performance**
|
|
481
|
+
- Prevent unnecessary re-renders with React.memo and stable references.
|
|
482
|
+
- Avoid redundant state and computations.
|
|
483
|
+
|
|
484
|
+
**Consistency**
|
|
485
|
+
- Follow established color, typography, and spacing scales.
|
|
486
|
+
- Match button, card, and badge styles with existing components.
|
|
487
|
+
|
|
488
|
+
**Prohibited**
|
|
489
|
+
- No placeholder data, TODOs, or pseudo-logic.
|
|
490
|
+
- No new external packages.
|
|
491
|
+
- No commented-out or partial implementations.
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Visual Quality Checklist
|
|
496
|
+
- Consistent vertical rhythm and alignment.
|
|
497
|
+
- Smooth hover and transition states.
|
|
498
|
+
- Responsive design that looks intentional at all breakpoints.
|
|
499
|
+
- Uses design tokens and existing components wherever possible.
|
|
500
|
+
- Clear visual hierarchy and accessible structure.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Validation Checklist
|
|
505
|
+
- Compiles cleanly with \`tsc --noEmit\`.
|
|
506
|
+
- Imports exist and resolve correctly.
|
|
507
|
+
- Component matches the design system and IA schema.
|
|
508
|
+
- No unused props, variables, or imports.
|
|
509
|
+
- Visually and functionally complete.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
**Final Output Requirement:**
|
|
514
|
+
Return only the complete \`.tsx\` source code for this component — no markdown fences, commentary, or extra text.
|
|
515
|
+
`.trim();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function makeImplementPrompt(basePrompt: string): string {
|
|
519
|
+
return `${basePrompt}
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
Generate the **complete final implementation** for \`${basePrompt}\`.
|
|
524
|
+
Begin directly with import statements and end with the export statement.
|
|
525
|
+
Do not include markdown fences, comments, or explanations — only the valid .tsx file content.
|
|
526
|
+
`.trim();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function makeRetryPrompt(
|
|
530
|
+
basePrompt: string,
|
|
531
|
+
componentType: string,
|
|
532
|
+
componentName: string,
|
|
533
|
+
previousCode: string,
|
|
534
|
+
previousErrors: string,
|
|
535
|
+
): string {
|
|
536
|
+
return `
|
|
537
|
+
${basePrompt}
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### Correction Task
|
|
542
|
+
The previously generated ${componentType} component **${componentName}** failed TypeScript validation.
|
|
543
|
+
Fix only the issues listed below without altering logic or layout.
|
|
544
|
+
|
|
545
|
+
**Errors**
|
|
546
|
+
${previousErrors}
|
|
547
|
+
|
|
548
|
+
**Previous Code**
|
|
549
|
+
${previousCode}
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
### Correction Rules
|
|
554
|
+
- Fix only TypeScript or import errors.
|
|
555
|
+
- Do not change working logic or structure.
|
|
556
|
+
- Keep eslint directives and formatting intact.
|
|
557
|
+
- Return the corrected \`.tsx\` file only, with no markdown fences or commentary.
|
|
558
|
+
`.trim();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* -------------------------------------------------------------------------- */
|
|
562
|
+
|
|
563
|
+
function extractComposition(componentDef: Record<string, unknown>): string[] {
|
|
564
|
+
if ('composition' in componentDef && Boolean(componentDef.composition)) {
|
|
565
|
+
const comp = componentDef.composition as Record<string, unknown>;
|
|
566
|
+
if ('atoms' in comp && Array.isArray(comp.atoms)) return comp.atoms as string[];
|
|
567
|
+
if ('molecules' in comp && Array.isArray(comp.molecules)) return comp.molecules as string[];
|
|
568
|
+
}
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function extractSpecs(componentDef: Record<string, unknown>): string[] {
|
|
573
|
+
return Array.isArray(componentDef.specs) ? (componentDef.specs as string[]) : [];
|
|
205
574
|
}
|
|
206
575
|
|
|
207
576
|
function isValidCollection(collection: unknown): collection is { items: Record<string, unknown> } {
|
|
208
577
|
if (collection === null || collection === undefined) return false;
|
|
209
578
|
if (typeof collection !== 'object') return false;
|
|
210
579
|
if (!('items' in collection)) return false;
|
|
211
|
-
|
|
212
580
|
const items = (collection as { items: unknown }).items;
|
|
213
581
|
return typeof items === 'object' && items !== null;
|
|
214
582
|
}
|
|
215
583
|
|
|
216
|
-
|
|
584
|
+
async function readDesignSystem(
|
|
585
|
+
providedPath: string,
|
|
586
|
+
refs: { projectDir: string; iaSchemeDir: string },
|
|
587
|
+
): Promise<string> {
|
|
588
|
+
const start = performance.now();
|
|
589
|
+
const candidates: string[] = [];
|
|
590
|
+
if (providedPath) {
|
|
591
|
+
candidates.push(providedPath);
|
|
592
|
+
if (!path.isAbsolute(providedPath)) {
|
|
593
|
+
candidates.push(path.resolve(refs.projectDir, providedPath));
|
|
594
|
+
candidates.push(path.resolve(refs.iaSchemeDir, providedPath));
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const candidate of candidates) {
|
|
599
|
+
try {
|
|
600
|
+
const content = await fs.readFile(candidate, 'utf-8');
|
|
601
|
+
debugProcess(`[readDesignSystem] Loaded from ${candidate} in ${(performance.now() - start).toFixed(2)} ms`);
|
|
602
|
+
return content;
|
|
603
|
+
} catch {
|
|
604
|
+
debugProcess(`[readDesignSystem] Could not read design system from %s`, candidate);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
debugProcess(`[readDesignSystem] Design system not found, elapsed ${(performance.now() - start).toFixed(2)} ms`);
|
|
609
|
+
return '';
|
|
610
|
+
}
|