@auto-engineer/component-implementer 0.11.15 → 0.11.16

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.
@@ -1,7 +1,7 @@
1
- // noinspection ExceptionCaughtLocallyJS
2
1
  import { defineCommandHandler } from '@auto-engineer/message-bus';
3
2
  import * as fs from 'fs/promises';
4
3
  import * as path from 'path';
4
+ import * as ts from 'typescript';
5
5
  import createDebug from 'debug';
6
6
  import { callAI, loadScheme } from '../agent.js';
7
7
  import { execa } from 'execa';
@@ -27,6 +27,10 @@ export const commandHandler = defineCommandHandler({
27
27
  filePath: { description: 'Component file path', required: true },
28
28
  componentName: { description: 'Name of component to implement', required: true },
29
29
  failures: { description: 'Any failures from previous implementations', required: false },
30
+ aiOptions: {
31
+ description: 'AI generation options',
32
+ required: false,
33
+ },
30
34
  },
31
35
  examples: [
32
36
  '$ auto implement:component --project-dir=./client --ia-scheme-dir=./.context --design-system-path=./design-system.md --component-type=molecule --component-name=SurveyCard',
@@ -43,89 +47,185 @@ export const commandHandler = defineCommandHandler({
43
47
  return result;
44
48
  },
45
49
  });
46
- // eslint-disable-next-line complexity
50
+ async function loadComponentDataForImplementation(iaSchemeDir, componentType, componentName, projectDir, designSystemPath, filePath) {
51
+ const t1 = performance.now();
52
+ const scheme = await loadScheme(iaSchemeDir);
53
+ debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
54
+ if (!scheme)
55
+ throw new Error('IA scheme not found');
56
+ const pluralKey = `${componentType}s`;
57
+ const collection = scheme[pluralKey];
58
+ if (!isValidCollection(collection))
59
+ throw new Error(`Invalid IA schema structure for ${pluralKey}`);
60
+ const items = collection.items;
61
+ const componentDef = items[componentName];
62
+ if (!componentDef)
63
+ throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
64
+ const outPath = path.join(projectDir, '..', filePath);
65
+ const t2 = performance.now();
66
+ let existingScaffold = '';
67
+ try {
68
+ existingScaffold = await fs.readFile(outPath, 'utf-8');
69
+ debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
70
+ }
71
+ catch {
72
+ debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
73
+ }
74
+ const t3 = performance.now();
75
+ const projectConfig = await readProjectContext(projectDir);
76
+ debugProcess(`[3] Loaded project context in ${(performance.now() - t3).toFixed(2)} ms`);
77
+ const t4 = performance.now();
78
+ const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
79
+ debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
80
+ const t5 = performance.now();
81
+ const registry = await buildComponentRegistry(projectDir);
82
+ debugProcess(`[5] Built component registry in ${(performance.now() - t5).toFixed(2)} ms`);
83
+ return {
84
+ scheme: scheme,
85
+ componentDef,
86
+ existingScaffold,
87
+ projectConfig,
88
+ designSystemReference,
89
+ registry,
90
+ outPath,
91
+ };
92
+ }
93
+ async function loadDependencySources(scheme, componentType, componentName, projectDir, registry) {
94
+ const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
95
+ debugProcess(`[6] Resolved ${dependencyList.length} dependencies for ${componentName}`);
96
+ const { primary: primaryDeps } = resolveDependenciesToRegistry(dependencyList, registry);
97
+ const dependencySources = {};
98
+ const allAtoms = Array.from(registry.entries())
99
+ .filter(([_, entry]) => entry.type === 'atoms')
100
+ .map(([name, _]) => name);
101
+ for (const atomName of allAtoms) {
102
+ const atomSource = await readComponentPropsInterface(projectDir, 'atoms', atomName, registry);
103
+ if (atomSource !== null) {
104
+ dependencySources[`atoms/${atomName}`] = atomSource;
105
+ }
106
+ }
107
+ for (const dep of primaryDeps) {
108
+ debugProcess(`[loadDependencySources] Attempting to read dependency ${dep.type}/${dep.name}`);
109
+ const depContent = await readComponentFullImplementation(projectDir, dep.type, dep.name, registry);
110
+ if (depContent != null) {
111
+ debugProcess(`[loadDependencySources] Successfully read full implementation for ${dep.type}/${dep.name}`);
112
+ dependencySources[`${dep.type}/${dep.name}`] = depContent;
113
+ }
114
+ else {
115
+ debugProcess(`[loadDependencySources] Failed to read implementation for ${dep.type}/${dep.name} (returned null)`);
116
+ }
117
+ }
118
+ return dependencySources;
119
+ }
120
+ async function generateCodeWithRetryLoop(params) {
121
+ const { componentName, componentDef, basePrompt, composition, dependencySummary, projectConfig, graphqlFiles, outPath, projectDir, registry, existingScaffold, } = params;
122
+ let attempt = 1;
123
+ let code = '';
124
+ let lastErrors = '';
125
+ let lastImportErrors = [];
126
+ const maxAttempts = 3;
127
+ let currentMaxTokens = params.maxTokens;
128
+ const description = componentDef.description ?? '';
129
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
130
+ while (attempt <= maxAttempts) {
131
+ const genStart = performance.now();
132
+ const prompt = attempt === 1
133
+ ? makeImplementPrompt(basePrompt, graphqlFiles)
134
+ : makeRetryPrompt(componentName, lastErrors, composition, dependencySummary, lastImportErrors, description, existingScaffold, projectConfig['package.json'] ?? '', graphqlFiles);
135
+ const promptPath = `/tmp/prompt-${componentName}-attempt-${attempt}.txt`;
136
+ await fs.writeFile(promptPath, prompt, 'utf-8');
137
+ debugProcess(`[DEBUG] Saved prompt to ${promptPath} (${prompt.length} chars)`);
138
+ const aiRaw = await callAI(prompt, { maxTokens: currentMaxTokens });
139
+ code = extractCodeBlock(aiRaw);
140
+ const isTruncated = detectTruncation(code);
141
+ if (isTruncated) {
142
+ const suggestedMaxTokens = Math.ceil(currentMaxTokens * 1.5);
143
+ debugProcess(`[WARNING] Truncation detected at attempt ${attempt}. Increasing maxTokens: ${currentMaxTokens} → ${suggestedMaxTokens}`);
144
+ currentMaxTokens = suggestedMaxTokens;
145
+ }
146
+ await fs.writeFile(outPath, code, 'utf-8');
147
+ debugProcess(`[6.${attempt}] AI output written (${code.length} chars, truncated: ${isTruncated}) in ${(performance.now() - genStart).toFixed(2)} ms`);
148
+ const importValidation = validateImports(code, registry);
149
+ lastImportErrors = importValidation.errors;
150
+ if (!importValidation.valid) {
151
+ debugProcess(`[WARN] Invalid imports detected: ${importValidation.errors.join('; ')}`);
152
+ }
153
+ const checkStart = performance.now();
154
+ const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
155
+ debugTypeCheck(`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`);
156
+ if (success) {
157
+ return code;
158
+ }
159
+ lastErrors = errors;
160
+ if (attempt === maxAttempts) {
161
+ const wasTruncated = detectTruncation(code);
162
+ const errorMessage = wasTruncated
163
+ ? `Component generation failed after ${attempt} attempts due to output truncation.\n` +
164
+ `Final maxTokens used: ${currentMaxTokens}\n` +
165
+ `Suggestion: Increase aiOptions.maxTokens in your config (try ${Math.ceil(currentMaxTokens * 1.5)} or higher)\n\n` +
166
+ `TypeScript errors:\n${errors}`
167
+ : `Type errors persist after ${attempt} attempts:\n${errors}`;
168
+ throw new Error(errorMessage);
169
+ }
170
+ attempt += 1;
171
+ }
172
+ throw new Error('Unreachable state');
173
+ }
47
174
  async function handleImplementComponentCommandInternal(command) {
48
175
  const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
49
176
  try {
50
177
  const start = performance.now();
51
178
  debugProcess(`Starting ${componentType}:${componentName}`);
52
- const t1 = performance.now();
53
- const scheme = await loadScheme(iaSchemeDir);
54
- debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
55
- if (!scheme)
56
- throw new Error('IA scheme not found');
57
- const pluralKey = `${componentType}s`;
58
- const collection = scheme[pluralKey];
59
- if (!isValidCollection(collection))
60
- throw new Error(`Invalid IA schema structure for ${pluralKey}`);
61
- const items = collection.items;
62
- const componentDef = items[componentName];
63
- if (!componentDef)
64
- throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
65
- const outPath = path.join(projectDir, '..', filePath);
66
- const t2 = performance.now();
67
- let existingScaffold = '';
68
- try {
69
- existingScaffold = await fs.readFile(outPath, 'utf-8');
70
- debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
179
+ const loadData = await loadComponentDataForImplementation(iaSchemeDir, componentType, componentName, projectDir, designSystemPath, filePath);
180
+ const dependencySources = await loadDependencySources(loadData.scheme, componentType, componentName, projectDir, loadData.registry);
181
+ const usageInfo = await extractComponentUsageFromScaffolds(componentName, componentType, projectDir);
182
+ debugProcess(`[extractComponentUsageFromScaffolds] Found ${usageInfo.usageExamples.length} usage examples, requiresChildren: ${usageInfo.requiresChildren}`);
183
+ const basePrompt = makeBasePrompt(componentType, componentName, loadData.componentDef, loadData.existingScaffold, loadData.projectConfig, loadData.designSystemReference, dependencySources, usageInfo);
184
+ const composition = extractComposition(loadData.componentDef);
185
+ const dependencySummary = Object.entries(dependencySources)
186
+ .map(([name, src]) => `### ${name}\n${src}`)
187
+ .join('\n\n') || '(No dependencies found)';
188
+ const hasOwnDataRequirements = hasDataRequirements(loadData.componentDef);
189
+ const hasParentDataRequirements = componentType === 'molecule' ? findParentDataRequirements(loadData.scheme, componentName) : false;
190
+ const needsGraphQLFiles = hasOwnDataRequirements || hasParentDataRequirements;
191
+ let graphqlFiles = {};
192
+ if (needsGraphQLFiles) {
193
+ const t6 = performance.now();
194
+ graphqlFiles = await readGraphQLFiles(projectDir);
195
+ const reason = hasOwnDataRequirements ? 'has own data requirements' : 'parent has data requirements';
196
+ debugProcess(`[6] Loaded GraphQL files for ${componentName} (${reason}) in ${(performance.now() - t6).toFixed(2)} ms`);
71
197
  }
72
- catch {
73
- debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
74
- }
75
- const t3 = performance.now();
76
- const projectConfig = await readAllTopLevelFiles(projectDir);
77
- debugProcess(`[3] Loaded project + gql/graphql files in ${(performance.now() - t3).toFixed(2)} ms`);
78
- const t4 = performance.now();
79
- const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
80
- debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
81
- const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
82
- debugProcess(`[5] Resolved ${dependencyList.length} dependencies for ${componentName}`);
83
- const dependencySources = {};
84
- for (const dep of dependencyList) {
85
- const depSource = await readComponentSource(projectDir, dep.type, dep.name);
86
- if (depSource != null)
87
- dependencySources[`${dep.type}/${dep.name}`] = depSource;
88
- }
89
- const basePrompt = makeBasePrompt(componentType, componentName, componentDef, existingScaffold, projectConfig, designSystemReference, dependencySources);
90
- await fs.mkdir(path.dirname(outPath), { recursive: true });
91
- let attempt = 1;
92
- let code = '';
93
- let lastErrors = '';
94
- const maxAttempts = 3;
95
- while (attempt <= maxAttempts) {
96
- const genStart = performance.now();
97
- const prompt = attempt === 1
98
- ? makeImplementPrompt(basePrompt)
99
- : makeRetryPrompt(basePrompt, componentType, componentName, code, lastErrors);
100
- const aiRaw = await callAI(prompt);
101
- code = extractCodeBlock(aiRaw);
102
- await fs.writeFile(outPath, code, 'utf-8');
103
- debugProcess(`[6.${attempt}] AI output written (${code.length} chars) in ${(performance.now() - genStart).toFixed(2)} ms`);
104
- const checkStart = performance.now();
105
- const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
106
- debugTypeCheck(`[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`);
107
- if (success) {
108
- debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
109
- return {
110
- type: 'ComponentImplemented',
111
- data: {
112
- filePath: outPath,
113
- componentType,
114
- componentName,
115
- composition: extractComposition(componentDef),
116
- specs: extractSpecs(componentDef),
117
- },
118
- timestamp: new Date(),
119
- requestId: command.requestId,
120
- correlationId: command.correlationId,
121
- };
122
- }
123
- lastErrors = errors;
124
- if (attempt === maxAttempts)
125
- throw new Error(`Type errors persist after ${attempt} attempts:\n${errors}`);
126
- attempt += 1;
198
+ else {
199
+ debugProcess(`[6] Skipped GraphQL files for ${componentName} (no data requirements)`);
127
200
  }
128
- throw new Error('Unreachable state');
201
+ await generateCodeWithRetryLoop({
202
+ componentName,
203
+ componentDef: loadData.componentDef,
204
+ basePrompt,
205
+ composition,
206
+ dependencySummary,
207
+ projectConfig: loadData.projectConfig,
208
+ graphqlFiles,
209
+ outPath: loadData.outPath,
210
+ projectDir,
211
+ registry: loadData.registry,
212
+ maxTokens: command.data.aiOptions?.maxTokens ?? 2000,
213
+ existingScaffold: loadData.existingScaffold,
214
+ });
215
+ debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
216
+ return {
217
+ type: 'ComponentImplemented',
218
+ data: {
219
+ filePath: loadData.outPath,
220
+ componentType,
221
+ componentName,
222
+ composition: extractComposition(loadData.componentDef),
223
+ specs: extractSpecs(loadData.componentDef),
224
+ },
225
+ timestamp: new Date(),
226
+ requestId: command.requestId,
227
+ correlationId: command.correlationId,
228
+ };
129
229
  }
130
230
  catch (error) {
131
231
  debug('[Error] Component implementation failed: %O', error);
@@ -143,23 +243,8 @@ async function handleImplementComponentCommandInternal(command) {
143
243
  };
144
244
  }
145
245
  }
146
- // eslint-disable-next-line complexity
147
- async function resolveDependenciesRecursively(scheme, type, name, visited = new Set()) {
148
- const key = `${type}:${name}`;
149
- if (visited.has(key))
150
- return [];
151
- visited.add(key);
152
- const collection = scheme[`${type}s`];
153
- //
154
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
155
- if (!collection || !isValidCollection(collection))
156
- return [];
157
- const def = collection.items[name];
158
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
159
- if (!def || typeof def !== 'object' || !('composition' in def))
160
- return [];
246
+ async function resolveCompositionDependencies(scheme, composition, visited) {
161
247
  const result = [];
162
- const composition = def.composition;
163
248
  for (const [subType, subNames] of Object.entries(composition)) {
164
249
  if (!Array.isArray(subNames))
165
250
  continue;
@@ -171,12 +256,277 @@ async function resolveDependenciesRecursively(scheme, type, name, visited = new
171
256
  }
172
257
  return result;
173
258
  }
174
- async function readComponentSource(projectDir, type, name) {
175
- const file = path.join(projectDir, 'src', 'components', type, `${name}.tsx`);
259
+ async function resolveLayoutDependencies(scheme, layout, visited) {
260
+ const result = [];
261
+ if ('organisms' in layout && Array.isArray(layout.organisms)) {
262
+ for (const organismName of layout.organisms) {
263
+ result.push({ type: 'organisms', name: organismName });
264
+ const nested = await resolveDependenciesRecursively(scheme, 'organisms', organismName, visited);
265
+ result.push(...nested);
266
+ }
267
+ }
268
+ return result;
269
+ }
270
+ function getComponentDefinitionFromScheme(scheme, type, name) {
271
+ const collection = scheme[`${type}s`];
272
+ if (collection === null || collection === undefined || !isValidCollection(collection))
273
+ return null;
274
+ const def = collection.items[name];
275
+ if (def === null || def === undefined || typeof def !== 'object')
276
+ return null;
277
+ return def;
278
+ }
279
+ async function resolveDependenciesRecursively(scheme, type, name, visited = new Set()) {
280
+ const key = `${type}:${name}`;
281
+ if (visited.has(key))
282
+ return [];
283
+ visited.add(key);
284
+ const def = getComponentDefinitionFromScheme(scheme, type, name);
285
+ if (def === null)
286
+ return [];
287
+ const result = [];
288
+ if ('composition' in def) {
289
+ const compositionDeps = await resolveCompositionDependencies(scheme, def.composition, visited);
290
+ result.push(...compositionDeps);
291
+ }
292
+ if ('layout' in def && typeof def.layout === 'object' && def.layout !== null) {
293
+ const layoutDeps = await resolveLayoutDependencies(scheme, def.layout, visited);
294
+ result.push(...layoutDeps);
295
+ }
296
+ return result;
297
+ }
298
+ function hasExportModifier(node) {
299
+ const modifiers = 'modifiers' in node ? node.modifiers : undefined;
300
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
301
+ }
302
+ function extractFromVariableStatement(node, exports) {
303
+ node.declarationList.declarations.forEach((decl) => {
304
+ if (ts.isIdentifier(decl.name)) {
305
+ exports.push(decl.name.text);
306
+ }
307
+ });
308
+ }
309
+ function extractFromExportDeclaration(node, exports) {
310
+ if (node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
311
+ node.exportClause.elements.forEach((element) => {
312
+ exports.push(element.name.text);
313
+ });
314
+ }
315
+ }
316
+ function extractExportedComponentNames(sourceFile) {
317
+ const exports = [];
318
+ ts.forEachChild(sourceFile, (node) => {
319
+ if (ts.isVariableStatement(node) && hasExportModifier(node)) {
320
+ extractFromVariableStatement(node, exports);
321
+ }
322
+ else if (ts.isExportDeclaration(node)) {
323
+ extractFromExportDeclaration(node, exports);
324
+ }
325
+ else if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name !== undefined) {
326
+ exports.push(node.name.text);
327
+ }
328
+ });
329
+ return exports;
330
+ }
331
+ function extractAllExportedTypes(sourceFile) {
332
+ const types = [];
333
+ ts.forEachChild(sourceFile, (node) => {
334
+ if (ts.isInterfaceDeclaration(node)) {
335
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
336
+ if (hasExport) {
337
+ types.push(node.name.text);
338
+ }
339
+ }
340
+ if (ts.isTypeAliasDeclaration(node)) {
341
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
342
+ if (hasExport) {
343
+ types.push(node.name.text);
344
+ }
345
+ }
346
+ });
347
+ return types;
348
+ }
349
+ function extractTypeReferencesFromProps(node, sourceFile) {
350
+ const refs = new Set();
351
+ function visitType(typeNode) {
352
+ if (ts.isTypeReferenceNode(typeNode)) {
353
+ const typeName = typeNode.typeName.getText(sourceFile);
354
+ refs.add(typeName);
355
+ }
356
+ if (ts.isArrayTypeNode(typeNode)) {
357
+ visitType(typeNode.elementType);
358
+ }
359
+ if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
360
+ typeNode.types.forEach(visitType);
361
+ }
362
+ if (ts.isFunctionTypeNode(typeNode)) {
363
+ typeNode.parameters.forEach((param) => {
364
+ if (param.type !== undefined)
365
+ visitType(param.type);
366
+ });
367
+ if (typeNode.type !== undefined)
368
+ visitType(typeNode.type);
369
+ }
370
+ if (ts.isParenthesizedTypeNode(typeNode)) {
371
+ visitType(typeNode.type);
372
+ }
373
+ }
374
+ if (ts.isInterfaceDeclaration(node)) {
375
+ node.members.forEach((member) => {
376
+ if (ts.isPropertySignature(member) && member.type !== undefined) {
377
+ visitType(member.type);
378
+ }
379
+ });
380
+ }
381
+ else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
382
+ visitType(node.type);
383
+ }
384
+ return refs;
385
+ }
386
+ function extractTypeAlias(node, sourceFile) {
387
+ const typeName = node.name.text;
388
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
389
+ const exportPrefix = hasExport ? 'export ' : '';
390
+ const typeValue = node.type?.getText(sourceFile) ?? 'unknown';
391
+ return `${exportPrefix}type ${typeName} = ${typeValue}`;
392
+ }
393
+ function extractEnumDefinition(node, sourceFile) {
394
+ const enumName = node.name.text;
395
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
396
+ const exportPrefix = hasExport ? 'export ' : '';
397
+ const members = node.members
398
+ .map((m) => {
399
+ const name = m.name.getText(sourceFile);
400
+ const value = m.initializer?.getText(sourceFile);
401
+ return value !== undefined ? `${name} = ${value}` : name;
402
+ })
403
+ .join(', ');
404
+ return `${exportPrefix}enum ${enumName} { ${members} }`;
405
+ }
406
+ function extractInterfaceDefinition(node, sourceFile) {
407
+ const interfaceName = node.name.text;
408
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
409
+ const exportPrefix = hasExport ? 'export ' : '';
410
+ const members = node.members
411
+ .map((member) => {
412
+ if (ts.isPropertySignature(member) && member.name !== undefined) {
413
+ const propName = member.name.getText(sourceFile);
414
+ const optional = member.questionToken !== undefined ? '?' : '';
415
+ const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
416
+ return `${propName}${optional}: ${propType}`;
417
+ }
418
+ return '';
419
+ })
420
+ .filter((m) => m !== '')
421
+ .join('; ');
422
+ return `${exportPrefix}interface ${interfaceName} { ${members} }`;
423
+ }
424
+ function extractReferencedTypeDefinitions(sourceFile, typeReferences) {
425
+ const definitions = [];
426
+ ts.forEachChild(sourceFile, (node) => {
427
+ if (ts.isTypeAliasDeclaration(node) && typeReferences.has(node.name.text)) {
428
+ definitions.push(extractTypeAlias(node, sourceFile));
429
+ }
430
+ else if (ts.isEnumDeclaration(node) && typeReferences.has(node.name.text)) {
431
+ definitions.push(extractEnumDefinition(node, sourceFile));
432
+ }
433
+ else if (ts.isInterfaceDeclaration(node) &&
434
+ typeReferences.has(node.name.text) &&
435
+ !node.name.text.includes('Props')) {
436
+ definitions.push(extractInterfaceDefinition(node, sourceFile));
437
+ }
438
+ });
439
+ return definitions.length > 0 ? definitions.join('\n ') : '';
440
+ }
441
+ function flattenPropsInterface(node, sourceFile) {
442
+ const properties = [];
443
+ if (ts.isInterfaceDeclaration(node)) {
444
+ node.members.forEach((member) => {
445
+ if (ts.isPropertySignature(member) && member.name !== undefined) {
446
+ const propName = member.name.getText(sourceFile);
447
+ const optional = member.questionToken ? '?' : '';
448
+ const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
449
+ properties.push(`${propName}${optional}: ${propType}`);
450
+ }
451
+ });
452
+ }
453
+ else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
454
+ return node.type.getText(sourceFile);
455
+ }
456
+ return properties.join('; ');
457
+ }
458
+ function extractPropsAndImport(sourceCode, componentName, type, actualFilename) {
459
+ const sourceFile = ts.createSourceFile(`${componentName}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
460
+ const exportedComponents = extractExportedComponentNames(sourceFile);
461
+ const allExportedTypes = extractAllExportedTypes(sourceFile);
462
+ let propsInline = '';
463
+ let propsNode = null;
464
+ ts.forEachChild(sourceFile, (node) => {
465
+ if (!propsInline) {
466
+ if (ts.isInterfaceDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
467
+ propsInline = flattenPropsInterface(node, sourceFile);
468
+ propsNode = node;
469
+ }
470
+ else if (ts.isTypeAliasDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
471
+ propsInline = flattenPropsInterface(node, sourceFile);
472
+ propsNode = node;
473
+ }
474
+ }
475
+ });
476
+ const typeReferences = propsNode !== null ? extractTypeReferencesFromProps(propsNode, sourceFile) : new Set();
477
+ const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [componentName];
478
+ for (const typeRef of typeReferences) {
479
+ if (allExportedTypes.includes(typeRef) && !toImport.includes(typeRef)) {
480
+ toImport.push(typeRef);
481
+ }
482
+ }
483
+ const typeDefinitions = extractReferencedTypeDefinitions(sourceFile, typeReferences);
484
+ const importStatement = `import { ${toImport.join(', ')} } from '@/components/${type}/${actualFilename}';`;
485
+ const importLine = `**Import**: \`${importStatement}\``;
486
+ const typeDefsLine = typeDefinitions ? `**Type Definitions**:\n ${typeDefinitions}` : '';
487
+ const propsLine = propsInline ? `**Props**: \`${propsInline}\`` : '**Props**: None';
488
+ const parts = [importLine, typeDefsLine, propsLine].filter((p) => p !== '');
489
+ return parts.join('\n');
490
+ }
491
+ async function readComponentPropsInterface(projectDir, _type, name, registry) {
492
+ const entry = registry.get(name);
493
+ if (entry === undefined) {
494
+ debugProcess(`[readComponentPropsInterface] No registry entry found for ${name}`);
495
+ return null;
496
+ }
497
+ const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
498
+ debugProcess(`[readComponentPropsInterface] Reading file: ${file}`);
499
+ try {
500
+ const sourceCode = await fs.readFile(file, 'utf-8');
501
+ const result = extractPropsAndImport(sourceCode, name, entry.type, entry.actualFilename);
502
+ debugProcess(`[readComponentPropsInterface] extractPropsAndImport returned ${result ? result.length : 0} chars`);
503
+ return result;
504
+ }
505
+ catch (error) {
506
+ debugProcess(`[readComponentPropsInterface] Error reading file: ${String(error)}`);
507
+ return null;
508
+ }
509
+ }
510
+ async function readComponentFullImplementation(projectDir, _type, name, registry) {
511
+ const entry = registry.get(name);
512
+ if (entry === undefined) {
513
+ debugProcess(`[readComponentFullImplementation] No registry entry found for ${name}`);
514
+ return null;
515
+ }
516
+ const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
517
+ debugProcess(`[readComponentFullImplementation] Reading file: ${file}`);
176
518
  try {
177
- return await fs.readFile(file, 'utf-8');
519
+ const sourceCode = await fs.readFile(file, 'utf-8');
520
+ const sourceFile = ts.createSourceFile(`${name}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
521
+ const exportedComponents = extractExportedComponentNames(sourceFile);
522
+ const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [name];
523
+ const importStatement = `import { ${toImport.join(', ')} } from '@/components/${entry.type}/${entry.actualFilename}';`;
524
+ const formattedOutput = `**Import**: \`${importStatement}\`\n\n**Implementation**:\n\`\`\`tsx\n${sourceCode}\n\`\`\``;
525
+ debugProcess(`[readComponentFullImplementation] Returning ${formattedOutput.length} chars for ${name}`);
526
+ return formattedOutput;
178
527
  }
179
- catch {
528
+ catch (error) {
529
+ debugProcess(`[readComponentFullImplementation] Error reading file: ${String(error)}`);
180
530
  return null;
181
531
  }
182
532
  }
@@ -186,69 +536,221 @@ function extractCodeBlock(text) {
186
536
  .replace(/```/g, '')
187
537
  .trim();
188
538
  }
189
- async function readAllTopLevelFiles(projectDir) {
190
- debugProcess('[readAllTopLevelFiles] Reading project files from %s', projectDir);
539
+ function detectTruncation(code) {
540
+ const truncationIndicators = [/<\w+[^/>]*$/, /<\/\w*$/, /"[^"]*$/, /'[^']*$/, /`[^`]*$/, /\{[^}]*$/m, /\([^)]*$/m];
541
+ const lastLines = code.split('\n').slice(-5).join('\n');
542
+ return truncationIndicators.some((pattern) => pattern.test(lastLines));
543
+ }
544
+ async function readEssentialFiles(projectDir) {
545
+ debugProcess('[readEssentialFiles] Reading essential config files from %s', projectDir);
191
546
  const start = performance.now();
192
547
  const config = {};
193
- async function readRecursive(currentDir) {
194
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
195
- for (const entry of entries) {
196
- const fullPath = path.join(currentDir, entry.name);
197
- const relativePath = path.relative(projectDir, fullPath);
198
- if (entry.isDirectory()) {
199
- if (['node_modules', 'dist', 'build', '.next', '.turbo'].includes(entry.name))
200
- continue;
201
- await readRecursive(fullPath);
202
- }
203
- else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
204
- try {
205
- config[relativePath] = await fs.readFile(fullPath, 'utf-8');
206
- }
207
- catch (err) {
208
- debugProcess(`Failed to read ${relativePath}: ${err.message}`);
209
- }
210
- }
548
+ const essentialFiles = ['package.json', 'tsconfig.json', 'tailwind.config.js', 'tailwind.config.ts'];
549
+ for (const file of essentialFiles) {
550
+ try {
551
+ config[file] = await fs.readFile(path.join(projectDir, file), 'utf-8');
552
+ debugProcess(`Read essential file: ${file}`);
553
+ }
554
+ catch {
555
+ debugProcess(`Essential file not found: ${file}`);
211
556
  }
212
557
  }
213
- await readRecursive(projectDir);
214
- debugProcess(`[readAllTopLevelFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
558
+ debugProcess(`[readEssentialFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
215
559
  return config;
216
560
  }
561
+ function hasDataRequirements(componentDef) {
562
+ return ('data_requirements' in componentDef &&
563
+ Array.isArray(componentDef.data_requirements) &&
564
+ componentDef.data_requirements.length > 0);
565
+ }
566
+ function checkOrganismForMolecule(moleculeName, organismName, organismDef) {
567
+ if (typeof organismDef !== 'object' || organismDef === null)
568
+ return false;
569
+ const composition = organismDef.composition;
570
+ const includesMolecule = composition?.molecules?.includes(moleculeName) ?? false;
571
+ if (!includesMolecule)
572
+ return false;
573
+ const hasData = hasDataRequirements(organismDef);
574
+ if (hasData) {
575
+ debugProcess(`[findParentDataRequirements] Molecule ${moleculeName} is used by organism ${organismName} which has data_requirements`);
576
+ return true;
577
+ }
578
+ return false;
579
+ }
580
+ function checkPageLayoutOrganisms(layout, organisms, moleculeName) {
581
+ if (layout?.organisms === undefined)
582
+ return false;
583
+ for (const organismName of layout.organisms) {
584
+ const organismDef = organisms.items[organismName];
585
+ if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
586
+ return true;
587
+ }
588
+ }
589
+ return false;
590
+ }
591
+ function checkPagesForMolecule(pages, organisms, moleculeName) {
592
+ if (pages?.items === undefined || organisms?.items === undefined)
593
+ return false;
594
+ const organismsWithItems = { items: organisms.items };
595
+ for (const [, pageDef] of Object.entries(pages.items)) {
596
+ if (typeof pageDef !== 'object' || pageDef === null)
597
+ continue;
598
+ const layout = pageDef.layout;
599
+ if (checkPageLayoutOrganisms(layout, organismsWithItems, moleculeName)) {
600
+ return true;
601
+ }
602
+ }
603
+ return false;
604
+ }
605
+ function findParentDataRequirements(scheme, moleculeName) {
606
+ debugProcess(`[findParentDataRequirements] Checking if molecule ${moleculeName} has parents with data requirements`);
607
+ const organisms = scheme.organisms;
608
+ if (organisms?.items !== undefined) {
609
+ for (const [organismName, organismDef] of Object.entries(organisms.items)) {
610
+ if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
611
+ return true;
612
+ }
613
+ }
614
+ }
615
+ const pages = scheme.pages;
616
+ if (checkPagesForMolecule(pages, organisms, moleculeName)) {
617
+ return true;
618
+ }
619
+ debugProcess(`[findParentDataRequirements] No parents with data_requirements found for molecule ${moleculeName}`);
620
+ return false;
621
+ }
622
+ async function readGraphQLFiles(projectDir) {
623
+ debugProcess('[readGraphQLFiles] Reading GraphQL type definition files from %s', projectDir);
624
+ const start = performance.now();
625
+ const graphqlFiles = {};
626
+ const graphqlFilePaths = ['src/gql/graphql.ts', 'src/graphql/queries.ts', 'src/graphql/mutations.ts'];
627
+ for (const filePath of graphqlFilePaths) {
628
+ try {
629
+ graphqlFiles[filePath] = await fs.readFile(path.join(projectDir, filePath), 'utf-8');
630
+ debugProcess(`Read GraphQL file: ${filePath}`);
631
+ }
632
+ catch {
633
+ debugProcess(`GraphQL file not found: ${filePath}`);
634
+ }
635
+ }
636
+ debugProcess(`[readGraphQLFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
637
+ return graphqlFiles;
638
+ }
639
+ async function readProjectContext(projectDir) {
640
+ return await readEssentialFiles(projectDir);
641
+ }
642
+ async function executeTypeCheck(tsconfigRoot, strict) {
643
+ const args = strict
644
+ ? ['tsc', '--noEmit', '--skipLibCheck', '--strict', '--pretty', 'false']
645
+ : ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'];
646
+ const result = await execa('npx', args, {
647
+ cwd: tsconfigRoot,
648
+ stdio: 'pipe',
649
+ reject: false,
650
+ });
651
+ return (result.stdout ?? '') + (result.stderr ?? '');
652
+ }
653
+ async function runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath) {
654
+ debugTypeCheck(`[runTypeCheckForFile] Running strict GraphQL type check...`);
655
+ const strictOutput = await executeTypeCheck(tsconfigRoot, true);
656
+ const graphqlStrictErrors = filterErrorsForFile(strictOutput, relativeFilePath, normalizedRelative, filePath);
657
+ debugTypeCheck(`[runTypeCheckForFile] GraphQL strict errors length: ${graphqlStrictErrors.length} chars`);
658
+ return graphqlStrictErrors;
659
+ }
217
660
  async function runTypeCheckForFile(projectDir, filePath) {
218
661
  const start = performance.now();
219
662
  try {
220
663
  const tsconfigRoot = await findProjectRoot(projectDir);
221
- const relativeFilePath = path.relative(tsconfigRoot, filePath).replace(/\\/g, '/');
664
+ const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(tsconfigRoot, filePath);
665
+ const relativeFilePath = path.relative(tsconfigRoot, absoluteFilePath).replace(/\\/g, '/');
222
666
  const normalizedRelative = relativeFilePath.replace(/^client\//, '');
223
- const result = await execa('npx', ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'], {
224
- cwd: tsconfigRoot,
225
- stdio: 'pipe',
226
- reject: false,
227
- });
228
- const output = (result.stdout ?? '') + (result.stderr ?? '');
667
+ debugTypeCheck(`[runTypeCheckForFile] tsconfigRoot: ${tsconfigRoot}`);
668
+ debugTypeCheck(`[runTypeCheckForFile] absoluteFilePath: ${absoluteFilePath}`);
669
+ debugTypeCheck(`[runTypeCheckForFile] relativeFilePath: ${relativeFilePath}`);
670
+ debugTypeCheck(`[runTypeCheckForFile] normalizedRelative: ${normalizedRelative}`);
671
+ const isGraphQLFile = await detectGraphQLFile(absoluteFilePath, relativeFilePath);
672
+ debugTypeCheck(`[runTypeCheckForFile] isGraphQLFile: ${isGraphQLFile}`);
673
+ const output = await executeTypeCheck(tsconfigRoot, false);
229
674
  debugTypeCheck(`[runTypeCheckForFile] Finished tsc in ${(performance.now() - start).toFixed(2)} ms`);
230
- if (result.exitCode === 0 && !output.includes('error TS'))
675
+ debugTypeCheck(`[runTypeCheckForFile] Total output length: ${output.length} chars`);
676
+ debugTypeCheck(`[runTypeCheckForFile] Output preview (first 2000 chars):\n${output.substring(0, 2000)}`);
677
+ const filteredErrors = filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath);
678
+ const graphqlStrictErrors = isGraphQLFile
679
+ ? await runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath)
680
+ : '';
681
+ const formattedErrors = formatTypeCheckErrors(filteredErrors, graphqlStrictErrors);
682
+ if (!output.includes('error TS') && formattedErrors.trim().length === 0) {
683
+ debugTypeCheck(`[runTypeCheckForFile] No errors found`);
231
684
  return { success: true, errors: '' };
232
- const filteredErrors = output
233
- .split('\n')
234
- .filter((line) => {
235
- const hasError = line.includes('error TS');
236
- const notNodeModules = !line.includes('node_modules');
237
- const matchesTarget = line.includes(relativeFilePath) ||
238
- line.includes(normalizedRelative) ||
239
- line.includes(path.basename(filePath));
240
- return hasError && notNodeModules && matchesTarget;
241
- })
242
- .join('\n');
243
- if (filteredErrors.trim().length === 0)
685
+ }
686
+ if (formattedErrors.trim().length === 0)
244
687
  return { success: true, errors: '' };
245
- return { success: false, errors: filteredErrors };
688
+ return { success: false, errors: formattedErrors };
246
689
  }
247
690
  catch (err) {
248
691
  const message = err instanceof Error ? err.message : String(err);
249
692
  return { success: false, errors: message };
250
693
  }
251
694
  }
695
+ async function detectGraphQLFile(absoluteFilePath, relativeFilePath) {
696
+ const isInGraphQLDir = relativeFilePath.includes('/graphql/') ||
697
+ relativeFilePath.includes('/gql/') ||
698
+ relativeFilePath.includes('\\graphql\\') ||
699
+ relativeFilePath.includes('\\gql\\');
700
+ if (isInGraphQLDir)
701
+ return true;
702
+ try {
703
+ const content = await fs.readFile(absoluteFilePath, 'utf-8');
704
+ return (content.includes("from '@/gql/graphql'") ||
705
+ content.includes('from "@/gql/graphql"') ||
706
+ content.includes("from '@/graphql/") ||
707
+ content.includes('from "@/graphql/') ||
708
+ content.includes('@apollo/client'));
709
+ }
710
+ catch {
711
+ return false;
712
+ }
713
+ }
714
+ function filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath) {
715
+ const allLines = output.split('\n');
716
+ const errorLines = allLines.filter((line) => line.includes('error TS'));
717
+ debugTypeCheck(`[filterErrorsForFile] Total lines: ${allLines.length}, Error lines: ${errorLines.length}`);
718
+ const filteredErrors = output
719
+ .split('\n')
720
+ .filter((line) => {
721
+ const hasError = line.includes('error TS');
722
+ const notNodeModules = !line.includes('node_modules');
723
+ const matchesTarget = line.includes(relativeFilePath) || line.includes(normalizedRelative) || line.includes(path.basename(filePath));
724
+ if (hasError) {
725
+ debugTypeCheck(`[filterErrorsForFile] Checking error line: ${line.substring(0, 150)}`);
726
+ debugTypeCheck(`[filterErrorsForFile] hasError: ${hasError}, notNodeModules: ${notNodeModules}, matchesTarget: ${matchesTarget}`);
727
+ }
728
+ return hasError && notNodeModules && matchesTarget;
729
+ })
730
+ .join('\n');
731
+ debugTypeCheck(`[filterErrorsForFile] Filtered errors length: ${filteredErrors.length} chars`);
732
+ return filteredErrors;
733
+ }
734
+ function formatTypeCheckErrors(regularErrors, graphqlStrictErrors) {
735
+ let formattedOutput = '';
736
+ if (graphqlStrictErrors.trim().length > 0) {
737
+ formattedOutput += '## GraphQL Schema Type Errors (strict mode)\n\n';
738
+ formattedOutput += 'These errors indicate fields/properties that violate GraphQL schema type contracts.\n';
739
+ formattedOutput += 'Common causes:\n';
740
+ formattedOutput += '- Using fields not defined in the GraphQL schema\n';
741
+ formattedOutput += '- Incorrect property types in mutation variables\n';
742
+ formattedOutput += '- Missing required fields in input types\n\n';
743
+ formattedOutput += graphqlStrictErrors;
744
+ formattedOutput += '\n\n';
745
+ formattedOutput += '**Fix**: Check @/gql/graphql.ts for the exact type definition and valid fields.\n\n';
746
+ formattedOutput += '---\n\n';
747
+ }
748
+ if (regularErrors.trim().length > 0) {
749
+ formattedOutput += '## TypeScript Errors\n\n';
750
+ formattedOutput += regularErrors;
751
+ }
752
+ return formattedOutput.trim();
753
+ }
252
754
  async function findProjectRoot(startDir) {
253
755
  let dir = startDir;
254
756
  while (dir !== path.dirname(dir)) {
@@ -263,30 +765,62 @@ async function findProjectRoot(startDir) {
263
765
  }
264
766
  throw new Error('Could not find project root (no package.json or tsconfig.json found)');
265
767
  }
266
- // eslint-disable-next-line complexity
267
- function makeBasePrompt(componentType, componentName, componentDef, existingScaffold, projectConfig, designSystemReference, dependencySources) {
768
+ async function extractComponentUsageFromScaffolds(componentName, componentType, projectDir) {
769
+ const usageExamples = [];
770
+ let requiresChildren = false;
771
+ const detectedProps = [];
772
+ const searchDirs = [];
773
+ if (componentType === 'molecule') {
774
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'organisms'));
775
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
776
+ }
777
+ else if (componentType === 'organism') {
778
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
779
+ }
780
+ for (const dir of searchDirs) {
781
+ try {
782
+ const files = await fs.readdir(dir);
783
+ for (const file of files) {
784
+ if (!file.endsWith('.tsx'))
785
+ continue;
786
+ const filePath = path.join(dir, file);
787
+ const content = await fs.readFile(filePath, 'utf-8');
788
+ const openTagPattern = new RegExp(`<${componentName}(?:\\s|>)`, 'g');
789
+ if (!openTagPattern.test(content))
790
+ continue;
791
+ const withChildrenPattern = new RegExp(`<${componentName}[^>]*>([\\s\\S]*?)</${componentName}>`, 'g');
792
+ const hasChildren = withChildrenPattern.test(content);
793
+ if (hasChildren) {
794
+ requiresChildren = true;
795
+ }
796
+ const lines = content.split('\n');
797
+ const usageLineIndexes = [];
798
+ lines.forEach((line, idx) => {
799
+ if (line.includes(`<${componentName}`)) {
800
+ usageLineIndexes.push(idx);
801
+ }
802
+ });
803
+ if (usageLineIndexes.length > 0) {
804
+ const lineIdx = usageLineIndexes[0];
805
+ const start = Math.max(0, lineIdx - 2);
806
+ const end = Math.min(lines.length, lineIdx + 8);
807
+ const snippet = lines.slice(start, end).join('\n');
808
+ usageExamples.push({
809
+ file: path.relative(projectDir, filePath),
810
+ snippet,
811
+ });
812
+ }
813
+ }
814
+ }
815
+ catch {
816
+ // ignore read errors
817
+ }
818
+ }
819
+ return { usageExamples, requiresChildren, detectedProps };
820
+ }
821
+ function makeBasePrompt(componentType, componentName, componentDef, existingScaffold, projectConfig, designSystemReference, dependencySources, usageInfo) {
268
822
  const hasScaffold = Boolean(existingScaffold?.trim());
269
- const gqlFiles = {};
270
- const graphqlFiles = {};
271
- const otherFiles = {};
272
- for (const [filePath, content] of Object.entries(projectConfig)) {
273
- const lower = filePath.toLowerCase();
274
- if (lower.includes('src/gql/'))
275
- gqlFiles[filePath] = content;
276
- else if (lower.includes('src/graphql/'))
277
- graphqlFiles[filePath] = content;
278
- else
279
- otherFiles[filePath] = content;
280
- }
281
- const queriesFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('queries.ts'))?.[1] ?? '';
282
- const mutationsFile = Object.entries(graphqlFiles).find(([n]) => n.endsWith('mutations.ts'))?.[1] ?? '';
283
- const gqlSection = Object.entries(gqlFiles)
284
- .map(([p, c]) => `### ${p}\n${c}`)
285
- .join('\n\n') || '(No gql folder found)';
286
- const graphqlSection = Object.entries(graphqlFiles)
287
- .map(([p, c]) => `### ${p}\n${c}`)
288
- .join('\n\n') || '(No graphql folder found)';
289
- const configSection = Object.entries(otherFiles)
823
+ const configSection = Object.entries(projectConfig)
290
824
  .map(([p, c]) => `### ${p}\n${c}`)
291
825
  .join('\n\n') || '(No additional config files)';
292
826
  const designSystemBlock = designSystemReference.trim()
@@ -296,7 +830,7 @@ function makeBasePrompt(componentType, componentName, componentDef, existingScaf
296
830
  .map(([name, src]) => `### ${name}\n${src}`)
297
831
  .join('\n\n') || '(No dependencies found)';
298
832
  return `
299
- # Implementation Brief: ${componentName} (${componentType})
833
+ # Implement ${componentName} (${componentType})
300
834
 
301
835
  You are a senior frontend engineer specializing in **React + TypeScript + Apollo Client**.
302
836
  Your task is to build a visually excellent, type-safe, and production-ready ${componentType} component.
@@ -315,55 +849,81 @@ Your component must:
315
849
 
316
850
  ---
317
851
 
852
+ ## IA Schema
853
+ ${JSON.stringify(componentDef, null, 2)}
854
+
855
+ ---
856
+
318
857
  ## Project Context
319
858
 
320
- **File Path:** src/components/${componentType}/${componentName}.tsx
321
859
  **Purpose:** A reusable UI element connected to the GraphQL layer and design system.
322
860
 
323
- ### IA Schema
324
- ${JSON.stringify(componentDef, null, 2)}
861
+ ## Component Scaffold
325
862
 
326
- ### Existing Scaffold
327
- ${hasScaffold ? existingScaffold : '(No existing scaffold found)'}
863
+ ${hasScaffold
864
+ ? `The scaffold below contains:
865
+ - Import statements for all dependencies (use these exact imports)
866
+ - Type guidance comments showing GraphQL queries/mutations/enums to use
867
+ - Specs describing required functionality
868
+ - Component structure to implement
328
869
 
329
- ### Design System Reference
330
- ${designSystemBlock}
870
+ **CRITICAL**: Follow the import statements and type guidance comments in the scaffold exactly.
331
871
 
332
- ### Related Components (Dependencies)
333
- ${dependencySection}
872
+ ${existingScaffold}`
873
+ : '(No existing scaffold found - create component from scratch)'}
334
874
 
335
- ### GraphQL Context (src/graphql)
336
- ${graphqlSection}
875
+ ---
337
876
 
338
- #### queries.ts
339
- ${queriesFile || '(queries.ts not found)'}
877
+ ## Design System
878
+ ${designSystemBlock}
879
+
880
+ ---
881
+
882
+ ## Available Dependencies
340
883
 
341
- #### mutations.ts
342
- ${mutationsFile || '(mutations.ts not found)'}
884
+ ${dependencySection}
343
885
 
344
- ### GraphQL Codegen (src/gql)
345
- ${gqlSection}
886
+ ---
346
887
 
347
- ### Other Relevant Files
888
+ ## Project Configuration
348
889
  ${configSection}
349
890
 
350
891
  ---
351
892
 
352
- ## Engineering Guidelines
893
+ ## Implementation Rules
353
894
 
354
895
  **Type Safety**
355
- - Explicitly type all props, state, and GraphQL responses.
356
- - Avoid \`any\` prefer discriminated unions, interfaces, and generics.
896
+ - No \`any\` or \`as SomeType\` - type correctly
897
+ - Import types from dependencies - never redefine them locally
898
+ - Type all props, state, and GraphQL responses explicitly
357
899
 
358
- **React Practices**
359
- - Never call setState during render.
360
- - Always use dependency arrays in effects.
361
- - Memoize computed values and callbacks.
362
- - Keep rendering pure and predictable.
900
+ **Imports**
901
+ - Use exact imports from scaffold and dependencies section
902
+ - Pattern: \`@/components/{type}/{component}\`
903
+ - Never use relative paths (\`../\`)
904
+ - Only use packages from package.json shown above
363
905
 
364
- **Error Handling**
365
- - Wrap async operations in try/catch with graceful fallback UI.
366
- - Check for null/undefined using optional chaining (?.) and defaults (??).
906
+ **GraphQL (if applicable)**
907
+ - **CRITICAL**: NEVER use inline gql template literals or import gql from @apollo/client
908
+ - **CRITICAL**: ALWAYS import pre-generated operations from @/graphql/queries or @/graphql/mutations
909
+ - Follow type guidance comments in scaffold for exact query/mutation names
910
+ - Use pattern: \`const { data } = useQuery(QueryName)\` where QueryName is imported from @/graphql/queries
911
+ - Use pattern: \`const [mutate, { loading }] = useMutation(MutationName, { refetchQueries: [...], awaitRefetchQueries: true })\`
912
+ - Access enum values: \`EnumName.Value\` (e.g., \`TodoStateStatus.Pending\`)
913
+ - **CRITICAL**: ALL mutations return \`{ success: Boolean!, error: { type: String!, message: String } }\`
914
+ - **CRITICAL**: ALWAYS check \`data?.mutationName?.success\` before considering mutation successful
915
+ - **CRITICAL**: ALWAYS handle \`data?.mutationName?.error?.message\` when \`success\` is false
916
+ - **CRITICAL**: ALWAYS use \`loading\` state to disable buttons during mutations: \`disabled={loading}\`
917
+ - **CRITICAL**: ALWAYS wrap mutations in try-catch for network errors
918
+ - **CRITICAL**: Use \`awaitRefetchQueries: true\` to prevent race conditions with polling queries
919
+
920
+ **React Best Practices**
921
+ - No setState during render
922
+ - Include dependency arrays in useEffect
923
+ - Use optional chaining (?.) and nullish coalescing (??)
924
+ ${usageInfo.requiresChildren
925
+ ? `\n**CRITICAL**: This component MUST accept \`children?: React.ReactNode\` prop based on parent usage patterns.`
926
+ : ''}
367
927
 
368
928
  **Visual & UX Quality**
369
929
  - Perfect spacing and alignment using Tailwind or the design system tokens.
@@ -380,9 +940,10 @@ ${configSection}
380
940
  - Match button, card, and badge styles with existing components.
381
941
 
382
942
  **Prohibited**
383
- - No placeholder data, TODOs, or pseudo-logic.
384
- - No new external packages.
385
- - No commented-out or partial implementations.
943
+ - No placeholder data or TODOs
944
+ - No new external packages not in package.json
945
+ - No reimplementing dependencies inline
946
+ - No redefining types that dependencies export
386
947
 
387
948
  ---
388
949
 
@@ -404,43 +965,141 @@ ${configSection}
404
965
 
405
966
  ---
406
967
 
407
- **Final Output Requirement:**
408
- Return only the complete \`.tsx\` source code for this component — no markdown fences, commentary, or extra text.
968
+ **Output**: Return ONLY the complete .tsx source code - no markdown fences or commentary.
409
969
  `.trim();
410
970
  }
411
- function makeImplementPrompt(basePrompt) {
412
- return `${basePrompt}
971
+ function makeImplementPrompt(basePrompt, graphqlFiles) {
972
+ const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
973
+ const graphqlSection = hasGraphQLFiles
974
+ ? `
975
+ ## GraphQL Type Definitions (Source of Truth)
976
+
977
+ **CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
978
+
979
+ ${Object.entries(graphqlFiles)
980
+ .map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
981
+ .join('\n\n')}
413
982
 
414
983
  ---
415
984
 
416
- Generate the **complete final implementation** for \`${basePrompt}\`.
417
- Begin directly with import statements and end with the export statement.
985
+ `
986
+ : '';
987
+ return `${basePrompt}
988
+
989
+ ${graphqlSection}---
990
+
991
+ Begin directly with import statements and end with the export statement.
418
992
  Do not include markdown fences, comments, or explanations — only the valid .tsx file content.
419
993
  `.trim();
420
994
  }
421
- function makeRetryPrompt(basePrompt, componentType, componentName, previousCode, previousErrors) {
995
+ function validateComponentImport(componentType, filename, registry) {
996
+ const importPath = `@/components/${componentType}/${filename}`;
997
+ const exists = Array.from(registry.values()).some((entry) => entry.type === componentType && entry.actualFilename === filename);
998
+ if (exists) {
999
+ return null;
1000
+ }
1001
+ const suggestions = Array.from(registry.values())
1002
+ .filter((entry) => entry.actualFilename.includes(filename) || filename.includes(entry.actualFilename))
1003
+ .map((entry) => `@/components/${entry.type}/${entry.actualFilename}`)
1004
+ .slice(0, 3);
1005
+ if (suggestions.length > 0) {
1006
+ return `Import not found: ${importPath}\nDid you mean: ${suggestions.join(', ')}?`;
1007
+ }
1008
+ return `Import not found: ${importPath}`;
1009
+ }
1010
+ function validateNonComponentImport(importPath) {
1011
+ if (importPath.startsWith('@/store/')) {
1012
+ return `Invalid import: ${importPath}\nThis project uses Apollo Client with GraphQL. Check the GraphQL files in context for available queries and mutations.`;
1013
+ }
1014
+ return null;
1015
+ }
1016
+ function validateImports(code, registry) {
1017
+ const errors = [];
1018
+ const componentImportPattern = /import\s+[^'"]*from\s+['"]@\/components\/([^/]+)\/([^'"]+)['"]/g;
1019
+ let match;
1020
+ while ((match = componentImportPattern.exec(code)) !== null) {
1021
+ const error = validateComponentImport(match[1], match[2], registry);
1022
+ if (error !== null) {
1023
+ errors.push(error);
1024
+ }
1025
+ }
1026
+ const allImportPattern = /import\s+[^'"]*from\s+['"](@\/[^'"]+)['"]/g;
1027
+ while ((match = allImportPattern.exec(code)) !== null) {
1028
+ const error = validateNonComponentImport(match[1]);
1029
+ if (error !== null) {
1030
+ errors.push(error);
1031
+ }
1032
+ }
1033
+ return { valid: errors.length === 0, errors };
1034
+ }
1035
+ function makeRetryPrompt(componentName, previousErrors, composition, dependencySummary, importValidationErrors, description, existingScaffold, packageJson, graphqlFiles) {
1036
+ const compositionHint = composition.length > 0 ? `\n**Required Components**: ${composition.join(', ')}\n` : '';
1037
+ const importErrorsHint = importValidationErrors.length > 0
1038
+ ? `\n**Import Errors**:\n${importValidationErrors.map((err) => `- ${err}`).join('\n')}\n`
1039
+ : '';
1040
+ const hasScaffold = Boolean(existingScaffold?.trim());
1041
+ const scaffoldSection = hasScaffold
1042
+ ? `
1043
+ ## Scaffold with Type Guidance
1044
+
1045
+ **CRITICAL**: Follow the type guidance comments in the scaffold for exact GraphQL operation names, enum values, and import patterns.
1046
+
1047
+ ${existingScaffold}
1048
+
1049
+ ---
1050
+ `
1051
+ : '';
1052
+ const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
1053
+ const graphqlSection = hasGraphQLFiles
1054
+ ? `
1055
+ ## GraphQL Type Definitions (Source of Truth)
1056
+
1057
+ **CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
1058
+
1059
+ ${Object.entries(graphqlFiles)
1060
+ .map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
1061
+ .join('\n\n')}
1062
+
1063
+ ---
1064
+ `
1065
+ : '';
422
1066
  return `
423
- ${basePrompt}
1067
+ # Fix TypeScript Errors: ${componentName}
1068
+
1069
+ ${description ? `**Description**: ${description}\n` : ''}
1070
+ ${compositionHint}
1071
+ ${scaffoldSection}
1072
+ ${graphqlSection}
1073
+ ## Project Dependencies (package.json)
1074
+
1075
+ **CRITICAL**: Only import packages that exist in the dependencies below.
1076
+
1077
+ ${packageJson}
424
1078
 
425
1079
  ---
426
1080
 
427
- ### Correction Task
428
- The previously generated ${componentType} component **${componentName}** failed TypeScript validation.
429
- Fix only the issues listed below without altering logic or layout.
1081
+ ## Available Components
1082
+
1083
+ ${dependencySummary}
1084
+
1085
+ ---
1086
+
1087
+ ## Errors to Fix
430
1088
 
431
- **Errors**
432
1089
  ${previousErrors}
433
1090
 
434
- **Previous Code**
435
- ${previousCode}
1091
+ ${importErrorsHint}
1092
+ **Hints**:
1093
+ - Follow scaffold's type guidance comments for exact operation names and enum values
1094
+ ${hasGraphQLFiles ? '- Use the GraphQL Type Definitions section above for exact types, interfaces, and enums' : '- Check @/gql/graphql.ts for GraphQL type definitions'}
1095
+ - **CRITICAL**: NEVER use inline \`gql\` template literals - ALWAYS import from @/graphql/queries or @/graphql/mutations
1096
+ - Use pattern: \`mutate({ variables: { input: InputType } })\`
1097
+ - Import pattern: \`@/components/{type}/{component}\`
1098
+ - Import types from dependencies - never redefine them
436
1099
 
437
1100
  ---
438
1101
 
439
- ### Correction Rules
440
- - Fix only TypeScript or import errors.
441
- - Do not change working logic or structure.
442
- - Keep eslint directives and formatting intact.
443
- - Return the corrected \`.tsx\` file only, with no markdown fences or commentary.
1102
+ **Output**: Return ONLY the corrected ${componentName}.tsx code - no markdown fences or commentary.
444
1103
  `.trim();
445
1104
  }
446
1105
  /* -------------------------------------------------------------------------- */
@@ -467,6 +1126,59 @@ function isValidCollection(collection) {
467
1126
  const items = collection.items;
468
1127
  return typeof items === 'object' && items !== null;
469
1128
  }
1129
+ async function buildComponentRegistry(projectDir) {
1130
+ const registry = new Map();
1131
+ const types = ['atoms', 'molecules', 'organisms'];
1132
+ for (const type of types) {
1133
+ const dir = path.join(projectDir, 'src', 'components', type);
1134
+ try {
1135
+ const files = await fs.readdir(dir);
1136
+ for (const file of files) {
1137
+ if (!file.endsWith('.tsx'))
1138
+ continue;
1139
+ const fullPath = path.join(dir, file);
1140
+ const content = await fs.readFile(fullPath, 'utf-8');
1141
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
1142
+ const exportedTypes = extractAllExportedTypes(sourceFile);
1143
+ const exportedComponents = extractExportedComponentNames(sourceFile);
1144
+ const allExports = [...new Set([...exportedTypes, ...exportedComponents])];
1145
+ if (allExports.length === 0)
1146
+ continue;
1147
+ const actualFilename = file.replace('.tsx', '');
1148
+ for (const exportName of allExports) {
1149
+ registry.set(exportName, {
1150
+ name: exportName,
1151
+ actualFilename,
1152
+ type,
1153
+ exports: allExports,
1154
+ });
1155
+ }
1156
+ }
1157
+ }
1158
+ catch (error) {
1159
+ debugProcess(`[buildComponentRegistry] Could not read ${type} directory: ${String(error)}`);
1160
+ }
1161
+ }
1162
+ debugProcess(`[buildComponentRegistry] Indexed ${registry.size} components`);
1163
+ return registry;
1164
+ }
1165
+ function resolveDependenciesToRegistry(dependencies, registry) {
1166
+ const primary = [];
1167
+ debugProcess(`[resolveDependenciesToRegistry] Processing ${dependencies.length} dependencies: ${dependencies.map((d) => `${d.type}/${d.name}`).join(', ')}`);
1168
+ for (const dep of dependencies) {
1169
+ const entry = registry.get(dep.name);
1170
+ if (entry !== undefined) {
1171
+ debugProcess(`[resolveDependenciesToRegistry] Found registry entry for ${dep.name}`);
1172
+ primary.push(entry);
1173
+ }
1174
+ else {
1175
+ debugProcess(`[resolveDependenciesToRegistry] No registry entry for ${dep.name}`);
1176
+ }
1177
+ }
1178
+ const available = Array.from(registry.values()).filter((entry) => entry.type === 'atoms');
1179
+ debugProcess(`[resolveDependenciesToRegistry] Resolved ${primary.length} primary dependencies`);
1180
+ return { primary, available };
1181
+ }
470
1182
  async function readDesignSystem(providedPath, refs) {
471
1183
  const start = performance.now();
472
1184
  const candidates = [];