@auto-engineer/component-implementer 0.11.14 → 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,8 +1,7 @@
1
- // noinspection ExceptionCaughtLocallyJS
2
-
3
1
  import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
4
2
  import * as fs from 'fs/promises';
5
3
  import * as path from 'path';
4
+ import * as ts from 'typescript';
6
5
  import createDebug from 'debug';
7
6
  import { callAI, loadScheme } from '../agent';
8
7
  import { execa } from 'execa';
@@ -13,6 +12,15 @@ const debugTypeCheck = createDebug('auto:client-implementer:component:typecheck'
13
12
  const debugProcess = createDebug('auto:client-implementer:component:process');
14
13
  const debugResult = createDebug('auto:client-implementer:component:result');
15
14
 
15
+ interface ComponentRegistryEntry {
16
+ name: string;
17
+ actualFilename: string;
18
+ type: 'atoms' | 'molecules' | 'organisms';
19
+ exports: string[];
20
+ }
21
+
22
+ type ComponentRegistry = Map<string, ComponentRegistryEntry>;
23
+
16
24
  export type ImplementComponentCommand = Command<
17
25
  'ImplementComponent',
18
26
  {
@@ -23,6 +31,10 @@ export type ImplementComponentCommand = Command<
23
31
  filePath: string;
24
32
  componentName: string;
25
33
  failures?: string[];
34
+ aiOptions?: {
35
+ temperature?: number;
36
+ maxTokens?: number;
37
+ };
26
38
  }
27
39
  >;
28
40
 
@@ -67,6 +79,10 @@ export const commandHandler = defineCommandHandler<
67
79
  filePath: { description: 'Component file path', required: true },
68
80
  componentName: { description: 'Name of component to implement', required: true },
69
81
  failures: { description: 'Any failures from previous implementations', required: false },
82
+ aiOptions: {
83
+ description: 'AI generation options',
84
+ required: false,
85
+ },
70
86
  },
71
87
  examples: [
72
88
  '$ auto implement:component --project-dir=./client --ia-scheme-dir=./.context --design-system-path=./design-system.md --component-type=molecule --component-name=SurveyCard',
@@ -85,122 +101,317 @@ export const commandHandler = defineCommandHandler<
85
101
  },
86
102
  });
87
103
 
88
- // eslint-disable-next-line complexity
89
- async function handleImplementComponentCommandInternal(
90
- command: ImplementComponentCommand,
91
- ): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
92
- const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
104
+ interface ComponentLoadData {
105
+ scheme: Record<string, unknown>;
106
+ componentDef: Record<string, unknown>;
107
+ existingScaffold: string;
108
+ projectConfig: Record<string, string>;
109
+ designSystemReference: string;
110
+ registry: ComponentRegistry;
111
+ outPath: string;
112
+ }
113
+
114
+ async function loadComponentDataForImplementation(
115
+ iaSchemeDir: string,
116
+ componentType: string,
117
+ componentName: string,
118
+ projectDir: string,
119
+ designSystemPath: string,
120
+ filePath: string,
121
+ ): Promise<ComponentLoadData> {
122
+ const t1 = performance.now();
123
+ const scheme = await loadScheme(iaSchemeDir);
124
+ debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
125
+ if (!scheme) throw new Error('IA scheme not found');
126
+
127
+ const pluralKey = `${componentType}s`;
128
+ const collection = (scheme as Record<string, unknown>)[pluralKey];
129
+ if (!isValidCollection(collection)) throw new Error(`Invalid IA schema structure for ${pluralKey}`);
130
+
131
+ const items = (collection as { items: Record<string, unknown> }).items;
132
+ const componentDef = items[componentName] as Record<string, unknown> | undefined;
133
+ if (!componentDef) throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
93
134
 
135
+ const outPath = path.join(projectDir, '..', filePath);
136
+
137
+ const t2 = performance.now();
138
+ let existingScaffold = '';
94
139
  try {
95
- const start = performance.now();
96
- debugProcess(`Starting ${componentType}:${componentName}`);
140
+ existingScaffold = await fs.readFile(outPath, 'utf-8');
141
+ debugProcess(`[2] Found existing scaffold in ${(performance.now() - t2).toFixed(2)} ms`);
142
+ } catch {
143
+ debugProcess(`[2] No existing scaffold found (${(performance.now() - t2).toFixed(2)} ms)`);
144
+ }
97
145
 
98
- const t1 = performance.now();
99
- const scheme = await loadScheme(iaSchemeDir);
100
- debugProcess(`[1] Loaded IA scheme in ${(performance.now() - t1).toFixed(2)} ms`);
101
- if (!scheme) throw new Error('IA scheme not found');
146
+ const t3 = performance.now();
147
+ const projectConfig = await readProjectContext(projectDir);
148
+ debugProcess(`[3] Loaded project context in ${(performance.now() - t3).toFixed(2)} ms`);
149
+
150
+ const t4 = performance.now();
151
+ const designSystemReference = await readDesignSystem(designSystemPath, { projectDir, iaSchemeDir });
152
+ debugProcess(`[4] Loaded design system reference in ${(performance.now() - t4).toFixed(2)} ms`);
153
+
154
+ const t5 = performance.now();
155
+ const registry = await buildComponentRegistry(projectDir);
156
+ debugProcess(`[5] Built component registry in ${(performance.now() - t5).toFixed(2)} ms`);
157
+
158
+ return {
159
+ scheme: scheme as Record<string, unknown>,
160
+ componentDef,
161
+ existingScaffold,
162
+ projectConfig,
163
+ designSystemReference,
164
+ registry,
165
+ outPath,
166
+ };
167
+ }
102
168
 
103
- const pluralKey = `${componentType}s`;
104
- const collection = (scheme as Record<string, unknown>)[pluralKey];
105
- if (!isValidCollection(collection)) throw new Error(`Invalid IA schema structure for ${pluralKey}`);
169
+ async function loadDependencySources(
170
+ scheme: Record<string, unknown>,
171
+ componentType: string,
172
+ componentName: string,
173
+ projectDir: string,
174
+ registry: ComponentRegistry,
175
+ ): Promise<Record<string, string>> {
176
+ const dependencyList = await resolveDependenciesRecursively(scheme, componentType, componentName);
177
+ debugProcess(`[6] Resolved ${dependencyList.length} dependencies for ${componentName}`);
178
+
179
+ const { primary: primaryDeps } = resolveDependenciesToRegistry(dependencyList, registry);
180
+ const dependencySources: Record<string, string> = {};
181
+
182
+ const allAtoms = Array.from(registry.entries())
183
+ .filter(([_, entry]) => entry.type === 'atoms')
184
+ .map(([name, _]) => name);
185
+
186
+ for (const atomName of allAtoms) {
187
+ const atomSource = await readComponentPropsInterface(projectDir, 'atoms', atomName, registry);
188
+ if (atomSource !== null) {
189
+ dependencySources[`atoms/${atomName}`] = atomSource;
190
+ }
191
+ }
106
192
 
107
- const items = (collection as { items: Record<string, unknown> }).items;
108
- const componentDef = items[componentName] as Record<string, unknown> | undefined;
109
- if (!componentDef) throw new Error(`Component ${componentType}:${componentName} not found in IA schema`);
193
+ for (const dep of primaryDeps) {
194
+ debugProcess(`[loadDependencySources] Attempting to read dependency ${dep.type}/${dep.name}`);
195
+ const depContent = await readComponentFullImplementation(projectDir, dep.type, dep.name, registry);
196
+ if (depContent != null) {
197
+ debugProcess(`[loadDependencySources] Successfully read full implementation for ${dep.type}/${dep.name}`);
198
+ dependencySources[`${dep.type}/${dep.name}`] = depContent;
199
+ } else {
200
+ debugProcess(`[loadDependencySources] Failed to read implementation for ${dep.type}/${dep.name} (returned null)`);
201
+ }
202
+ }
110
203
 
111
- const outPath = path.join(projectDir, '..', filePath);
204
+ return dependencySources;
205
+ }
112
206
 
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)`);
207
+ async function generateCodeWithRetryLoop(params: {
208
+ componentName: string;
209
+ componentDef: Record<string, unknown>;
210
+ basePrompt: string;
211
+ composition: string[];
212
+ dependencySummary: string;
213
+ projectConfig: Record<string, string>;
214
+ graphqlFiles: Record<string, string>;
215
+ outPath: string;
216
+ projectDir: string;
217
+ registry: ComponentRegistry;
218
+ maxTokens: number;
219
+ existingScaffold: string;
220
+ }): Promise<string> {
221
+ const {
222
+ componentName,
223
+ componentDef,
224
+ basePrompt,
225
+ composition,
226
+ dependencySummary,
227
+ projectConfig,
228
+ graphqlFiles,
229
+ outPath,
230
+ projectDir,
231
+ registry,
232
+ existingScaffold,
233
+ } = params;
234
+
235
+ let attempt = 1;
236
+ let code = '';
237
+ let lastErrors = '';
238
+ let lastImportErrors: string[] = [];
239
+ const maxAttempts = 3;
240
+ let currentMaxTokens = params.maxTokens;
241
+ const description = (componentDef.description as string) ?? '';
242
+
243
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
244
+
245
+ while (attempt <= maxAttempts) {
246
+ const genStart = performance.now();
247
+
248
+ const prompt =
249
+ attempt === 1
250
+ ? makeImplementPrompt(basePrompt, graphqlFiles)
251
+ : makeRetryPrompt(
252
+ componentName,
253
+ lastErrors,
254
+ composition,
255
+ dependencySummary,
256
+ lastImportErrors,
257
+ description,
258
+ existingScaffold,
259
+ projectConfig['package.json'] ?? '',
260
+ graphqlFiles,
261
+ );
262
+
263
+ const promptPath = `/tmp/prompt-${componentName}-attempt-${attempt}.txt`;
264
+ await fs.writeFile(promptPath, prompt, 'utf-8');
265
+ debugProcess(`[DEBUG] Saved prompt to ${promptPath} (${prompt.length} chars)`);
266
+
267
+ const aiRaw = await callAI(prompt, { maxTokens: currentMaxTokens });
268
+ code = extractCodeBlock(aiRaw);
269
+
270
+ const isTruncated = detectTruncation(code);
271
+ if (isTruncated) {
272
+ const suggestedMaxTokens = Math.ceil(currentMaxTokens * 1.5);
273
+ debugProcess(
274
+ `[WARNING] Truncation detected at attempt ${attempt}. Increasing maxTokens: ${currentMaxTokens} → ${suggestedMaxTokens}`,
275
+ );
276
+ currentMaxTokens = suggestedMaxTokens;
120
277
  }
121
278
 
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`);
279
+ await fs.writeFile(outPath, code, 'utf-8');
280
+ debugProcess(
281
+ `[6.${attempt}] AI output written (${code.length} chars, truncated: ${isTruncated}) in ${(performance.now() - genStart).toFixed(2)} ms`,
282
+ );
125
283
 
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`);
284
+ const importValidation = validateImports(code, registry);
285
+ lastImportErrors = importValidation.errors;
286
+ if (!importValidation.valid) {
287
+ debugProcess(`[WARN] Invalid imports detected: ${importValidation.errors.join('; ')}`);
288
+ }
129
289
 
130
- const dependencyList = await resolveDependenciesRecursively(
131
- scheme as Record<string, unknown>,
132
- componentType,
133
- componentName,
290
+ const checkStart = performance.now();
291
+ const { success, errors } = await runTypeCheckForFile(projectDir, outPath);
292
+ debugTypeCheck(
293
+ `[7.${attempt}] Type check in ${(performance.now() - checkStart).toFixed(2)} ms (success: ${success})`,
134
294
  );
135
295
 
136
- debugProcess(`[5] Resolved ${dependencyList.length} dependencies for ${componentName}`);
296
+ if (success) {
297
+ return code;
298
+ }
137
299
 
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;
300
+ lastErrors = errors;
301
+ if (attempt === maxAttempts) {
302
+ const wasTruncated = detectTruncation(code);
303
+ const errorMessage = wasTruncated
304
+ ? `Component generation failed after ${attempt} attempts due to output truncation.\n` +
305
+ `Final maxTokens used: ${currentMaxTokens}\n` +
306
+ `Suggestion: Increase aiOptions.maxTokens in your config (try ${Math.ceil(currentMaxTokens * 1.5)} or higher)\n\n` +
307
+ `TypeScript errors:\n${errors}`
308
+ : `Type errors persist after ${attempt} attempts:\n${errors}`;
309
+ throw new Error(errorMessage);
142
310
  }
311
+ attempt += 1;
312
+ }
143
313
 
144
- const basePrompt = makeBasePrompt(
314
+ throw new Error('Unreachable state');
315
+ }
316
+
317
+ async function handleImplementComponentCommandInternal(
318
+ command: ImplementComponentCommand,
319
+ ): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
320
+ const { projectDir, iaSchemeDir, designSystemPath, componentType, componentName, filePath } = command.data;
321
+
322
+ try {
323
+ const start = performance.now();
324
+ debugProcess(`Starting ${componentType}:${componentName}`);
325
+
326
+ const loadData = await loadComponentDataForImplementation(
327
+ iaSchemeDir,
145
328
  componentType,
146
329
  componentName,
147
- componentDef,
148
- existingScaffold,
149
- projectConfig,
150
- designSystemReference,
151
- dependencySources,
330
+ projectDir,
331
+ designSystemPath,
332
+ filePath,
152
333
  );
153
334
 
154
- await fs.mkdir(path.dirname(outPath), { recursive: true });
335
+ const dependencySources = await loadDependencySources(
336
+ loadData.scheme,
337
+ componentType,
338
+ componentName,
339
+ projectDir,
340
+ loadData.registry,
341
+ );
155
342
 
156
- let attempt = 1;
157
- let code = '';
158
- let lastErrors = '';
159
- const maxAttempts = 3;
343
+ const usageInfo = await extractComponentUsageFromScaffolds(
344
+ componentName,
345
+ componentType as 'molecule' | 'organism',
346
+ projectDir,
347
+ );
348
+ debugProcess(
349
+ `[extractComponentUsageFromScaffolds] Found ${usageInfo.usageExamples.length} usage examples, requiresChildren: ${usageInfo.requiresChildren}`,
350
+ );
160
351
 
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);
352
+ const basePrompt = makeBasePrompt(
353
+ componentType,
354
+ componentName,
355
+ loadData.componentDef,
356
+ loadData.existingScaffold,
357
+ loadData.projectConfig,
358
+ loadData.designSystemReference,
359
+ dependencySources,
360
+ usageInfo,
361
+ );
167
362
 
168
- const aiRaw = await callAI(prompt);
169
- code = extractCodeBlock(aiRaw);
170
- await fs.writeFile(outPath, code, 'utf-8');
363
+ const composition = extractComposition(loadData.componentDef);
364
+ const dependencySummary =
365
+ Object.entries(dependencySources)
366
+ .map(([name, src]) => `### ${name}\n${src}`)
367
+ .join('\n\n') || '(No dependencies found)';
368
+
369
+ const hasOwnDataRequirements = hasDataRequirements(loadData.componentDef);
370
+ const hasParentDataRequirements =
371
+ componentType === 'molecule' ? findParentDataRequirements(loadData.scheme, componentName) : false;
372
+ const needsGraphQLFiles = hasOwnDataRequirements || hasParentDataRequirements;
373
+
374
+ let graphqlFiles: Record<string, string> = {};
375
+ if (needsGraphQLFiles) {
376
+ const t6 = performance.now();
377
+ graphqlFiles = await readGraphQLFiles(projectDir);
378
+ const reason = hasOwnDataRequirements ? 'has own data requirements' : 'parent has data requirements';
171
379
  debugProcess(
172
- `[6.${attempt}] AI output written (${code.length} chars) in ${(performance.now() - genStart).toFixed(2)} ms`,
380
+ `[6] Loaded GraphQL files for ${componentName} (${reason}) in ${(performance.now() - t6).toFixed(2)} ms`,
173
381
  );
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;
382
+ } else {
383
+ debugProcess(`[6] Skipped GraphQL files for ${componentName} (no data requirements)`);
201
384
  }
202
385
 
203
- throw new Error('Unreachable state');
386
+ await generateCodeWithRetryLoop({
387
+ componentName,
388
+ componentDef: loadData.componentDef,
389
+ basePrompt,
390
+ composition,
391
+ dependencySummary,
392
+ projectConfig: loadData.projectConfig,
393
+ graphqlFiles,
394
+ outPath: loadData.outPath,
395
+ projectDir,
396
+ registry: loadData.registry,
397
+ maxTokens: command.data.aiOptions?.maxTokens ?? 2000,
398
+ existingScaffold: loadData.existingScaffold,
399
+ });
400
+
401
+ debugResult(`[✓] Implementation succeeded in ${(performance.now() - start).toFixed(2)} ms total`);
402
+ return {
403
+ type: 'ComponentImplemented',
404
+ data: {
405
+ filePath: loadData.outPath,
406
+ componentType,
407
+ componentName,
408
+ composition: extractComposition(loadData.componentDef),
409
+ specs: extractSpecs(loadData.componentDef),
410
+ },
411
+ timestamp: new Date(),
412
+ requestId: command.requestId,
413
+ correlationId: command.correlationId,
414
+ };
204
415
  } catch (error: unknown) {
205
416
  debug('[Error] Component implementation failed: %O', error);
206
417
  return {
@@ -218,7 +429,53 @@ async function handleImplementComponentCommandInternal(
218
429
  }
219
430
  }
220
431
 
221
- // eslint-disable-next-line complexity
432
+ async function resolveCompositionDependencies(
433
+ scheme: Record<string, unknown>,
434
+ composition: Record<string, string[]>,
435
+ visited: Set<string>,
436
+ ): Promise<{ type: string; name: string }[]> {
437
+ const result: { type: string; name: string }[] = [];
438
+ for (const [subType, subNames] of Object.entries(composition)) {
439
+ if (!Array.isArray(subNames)) continue;
440
+ for (const subName of subNames) {
441
+ result.push({ type: subType, name: subName });
442
+ const nested = await resolveDependenciesRecursively(scheme, subType, subName, visited);
443
+ result.push(...nested);
444
+ }
445
+ }
446
+ return result;
447
+ }
448
+
449
+ async function resolveLayoutDependencies(
450
+ scheme: Record<string, unknown>,
451
+ layout: Record<string, unknown>,
452
+ visited: Set<string>,
453
+ ): Promise<{ type: string; name: string }[]> {
454
+ const result: { type: string; name: string }[] = [];
455
+ if ('organisms' in layout && Array.isArray(layout.organisms)) {
456
+ for (const organismName of layout.organisms as string[]) {
457
+ result.push({ type: 'organisms', name: organismName });
458
+ const nested = await resolveDependenciesRecursively(scheme, 'organisms', organismName, visited);
459
+ result.push(...nested);
460
+ }
461
+ }
462
+ return result;
463
+ }
464
+
465
+ function getComponentDefinitionFromScheme(
466
+ scheme: Record<string, unknown>,
467
+ type: string,
468
+ name: string,
469
+ ): Record<string, unknown> | null {
470
+ const collection = scheme[`${type}s`];
471
+ if (collection === null || collection === undefined || !isValidCollection(collection)) return null;
472
+
473
+ const def = collection.items[name];
474
+ if (def === null || def === undefined || typeof def !== 'object') return null;
475
+
476
+ return def as Record<string, unknown>;
477
+ }
478
+
222
479
  async function resolveDependenciesRecursively(
223
480
  scheme: Record<string, unknown>,
224
481
  type: string,
@@ -229,34 +486,310 @@ async function resolveDependenciesRecursively(
229
486
  if (visited.has(key)) return [];
230
487
  visited.add(key);
231
488
 
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 [];
489
+ const def = getComponentDefinitionFromScheme(scheme, type, name);
490
+ if (def === null) return [];
240
491
 
241
492
  const result: { type: string; name: string }[] = [];
242
493
 
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);
250
- }
494
+ if ('composition' in def) {
495
+ const compositionDeps = await resolveCompositionDependencies(
496
+ scheme,
497
+ def.composition as Record<string, string[]>,
498
+ visited,
499
+ );
500
+ result.push(...compositionDeps);
501
+ }
502
+
503
+ if ('layout' in def && typeof def.layout === 'object' && def.layout !== null) {
504
+ const layoutDeps = await resolveLayoutDependencies(scheme, def.layout as Record<string, unknown>, visited);
505
+ result.push(...layoutDeps);
251
506
  }
507
+
252
508
  return result;
253
509
  }
254
510
 
255
- async function readComponentSource(projectDir: string, type: string, name: string): Promise<string | null> {
256
- const file = path.join(projectDir, 'src', 'components', type, `${name}.tsx`);
511
+ function hasExportModifier(node: ts.Node): boolean {
512
+ const modifiers = 'modifiers' in node ? (node.modifiers as readonly ts.Modifier[] | undefined) : undefined;
513
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
514
+ }
515
+
516
+ function extractFromVariableStatement(node: ts.VariableStatement, exports: string[]): void {
517
+ node.declarationList.declarations.forEach((decl) => {
518
+ if (ts.isIdentifier(decl.name)) {
519
+ exports.push(decl.name.text);
520
+ }
521
+ });
522
+ }
523
+
524
+ function extractFromExportDeclaration(node: ts.ExportDeclaration, exports: string[]): void {
525
+ if (node.exportClause !== undefined && ts.isNamedExports(node.exportClause)) {
526
+ node.exportClause.elements.forEach((element) => {
527
+ exports.push(element.name.text);
528
+ });
529
+ }
530
+ }
531
+
532
+ function extractExportedComponentNames(sourceFile: ts.SourceFile): string[] {
533
+ const exports: string[] = [];
534
+
535
+ ts.forEachChild(sourceFile, (node) => {
536
+ if (ts.isVariableStatement(node) && hasExportModifier(node)) {
537
+ extractFromVariableStatement(node, exports);
538
+ } else if (ts.isExportDeclaration(node)) {
539
+ extractFromExportDeclaration(node, exports);
540
+ } else if (ts.isFunctionDeclaration(node) && hasExportModifier(node) && node.name !== undefined) {
541
+ exports.push(node.name.text);
542
+ }
543
+ });
544
+
545
+ return exports;
546
+ }
547
+
548
+ function extractAllExportedTypes(sourceFile: ts.SourceFile): string[] {
549
+ const types: string[] = [];
550
+
551
+ ts.forEachChild(sourceFile, (node) => {
552
+ if (ts.isInterfaceDeclaration(node)) {
553
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
554
+ if (hasExport) {
555
+ types.push(node.name.text);
556
+ }
557
+ }
558
+ if (ts.isTypeAliasDeclaration(node)) {
559
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
560
+ if (hasExport) {
561
+ types.push(node.name.text);
562
+ }
563
+ }
564
+ });
565
+
566
+ return types;
567
+ }
568
+
569
+ function extractTypeReferencesFromProps(
570
+ node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
571
+ sourceFile: ts.SourceFile,
572
+ ): Set<string> {
573
+ const refs = new Set<string>();
574
+
575
+ function visitType(typeNode: ts.TypeNode): void {
576
+ if (ts.isTypeReferenceNode(typeNode)) {
577
+ const typeName = typeNode.typeName.getText(sourceFile);
578
+ refs.add(typeName);
579
+ }
580
+ if (ts.isArrayTypeNode(typeNode)) {
581
+ visitType(typeNode.elementType);
582
+ }
583
+ if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
584
+ typeNode.types.forEach(visitType);
585
+ }
586
+ if (ts.isFunctionTypeNode(typeNode)) {
587
+ typeNode.parameters.forEach((param) => {
588
+ if (param.type !== undefined) visitType(param.type);
589
+ });
590
+ if (typeNode.type !== undefined) visitType(typeNode.type);
591
+ }
592
+ if (ts.isParenthesizedTypeNode(typeNode)) {
593
+ visitType(typeNode.type);
594
+ }
595
+ }
596
+
597
+ if (ts.isInterfaceDeclaration(node)) {
598
+ node.members.forEach((member) => {
599
+ if (ts.isPropertySignature(member) && member.type !== undefined) {
600
+ visitType(member.type);
601
+ }
602
+ });
603
+ } else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
604
+ visitType(node.type);
605
+ }
606
+
607
+ return refs;
608
+ }
609
+
610
+ function extractTypeAlias(node: ts.TypeAliasDeclaration, sourceFile: ts.SourceFile): string {
611
+ const typeName = node.name.text;
612
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
613
+ const exportPrefix = hasExport ? 'export ' : '';
614
+ const typeValue = node.type?.getText(sourceFile) ?? 'unknown';
615
+ return `${exportPrefix}type ${typeName} = ${typeValue}`;
616
+ }
617
+
618
+ function extractEnumDefinition(node: ts.EnumDeclaration, sourceFile: ts.SourceFile): string {
619
+ const enumName = node.name.text;
620
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
621
+ const exportPrefix = hasExport ? 'export ' : '';
622
+ const members = node.members
623
+ .map((m) => {
624
+ const name = m.name.getText(sourceFile);
625
+ const value = m.initializer?.getText(sourceFile);
626
+ return value !== undefined ? `${name} = ${value}` : name;
627
+ })
628
+ .join(', ');
629
+ return `${exportPrefix}enum ${enumName} { ${members} }`;
630
+ }
631
+
632
+ function extractInterfaceDefinition(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): string {
633
+ const interfaceName = node.name.text;
634
+ const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
635
+ const exportPrefix = hasExport ? 'export ' : '';
636
+ const members = node.members
637
+ .map((member) => {
638
+ if (ts.isPropertySignature(member) && member.name !== undefined) {
639
+ const propName = member.name.getText(sourceFile);
640
+ const optional = member.questionToken !== undefined ? '?' : '';
641
+ const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
642
+ return `${propName}${optional}: ${propType}`;
643
+ }
644
+ return '';
645
+ })
646
+ .filter((m) => m !== '')
647
+ .join('; ');
648
+ return `${exportPrefix}interface ${interfaceName} { ${members} }`;
649
+ }
650
+
651
+ function extractReferencedTypeDefinitions(sourceFile: ts.SourceFile, typeReferences: Set<string>): string {
652
+ const definitions: string[] = [];
653
+
654
+ ts.forEachChild(sourceFile, (node) => {
655
+ if (ts.isTypeAliasDeclaration(node) && typeReferences.has(node.name.text)) {
656
+ definitions.push(extractTypeAlias(node, sourceFile));
657
+ } else if (ts.isEnumDeclaration(node) && typeReferences.has(node.name.text)) {
658
+ definitions.push(extractEnumDefinition(node, sourceFile));
659
+ } else if (
660
+ ts.isInterfaceDeclaration(node) &&
661
+ typeReferences.has(node.name.text) &&
662
+ !node.name.text.includes('Props')
663
+ ) {
664
+ definitions.push(extractInterfaceDefinition(node, sourceFile));
665
+ }
666
+ });
667
+
668
+ return definitions.length > 0 ? definitions.join('\n ') : '';
669
+ }
670
+
671
+ function flattenPropsInterface(
672
+ node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
673
+ sourceFile: ts.SourceFile,
674
+ ): string {
675
+ const properties: string[] = [];
676
+
677
+ if (ts.isInterfaceDeclaration(node)) {
678
+ node.members.forEach((member) => {
679
+ if (ts.isPropertySignature(member) && member.name !== undefined) {
680
+ const propName = member.name.getText(sourceFile);
681
+ const optional = member.questionToken ? '?' : '';
682
+ const propType = member.type !== undefined ? member.type.getText(sourceFile) : 'any';
683
+ properties.push(`${propName}${optional}: ${propType}`);
684
+ }
685
+ });
686
+ } else if (ts.isTypeAliasDeclaration(node) && node.type !== undefined) {
687
+ return node.type.getText(sourceFile);
688
+ }
689
+
690
+ return properties.join('; ');
691
+ }
692
+
693
+ function extractPropsAndImport(
694
+ sourceCode: string,
695
+ componentName: string,
696
+ type: string,
697
+ actualFilename: string,
698
+ ): string {
699
+ const sourceFile = ts.createSourceFile(`${componentName}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
700
+
701
+ const exportedComponents = extractExportedComponentNames(sourceFile);
702
+ const allExportedTypes = extractAllExportedTypes(sourceFile);
703
+
704
+ let propsInline = '';
705
+ let propsNode: ts.InterfaceDeclaration | ts.TypeAliasDeclaration | null = null;
706
+
707
+ ts.forEachChild(sourceFile, (node) => {
708
+ if (!propsInline) {
709
+ if (ts.isInterfaceDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
710
+ propsInline = flattenPropsInterface(node, sourceFile);
711
+ propsNode = node;
712
+ } else if (ts.isTypeAliasDeclaration(node) && node.name.text.includes(`${componentName}Props`)) {
713
+ propsInline = flattenPropsInterface(node, sourceFile);
714
+ propsNode = node;
715
+ }
716
+ }
717
+ });
718
+
719
+ const typeReferences = propsNode !== null ? extractTypeReferencesFromProps(propsNode, sourceFile) : new Set<string>();
720
+
721
+ const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [componentName];
722
+
723
+ for (const typeRef of typeReferences) {
724
+ if (allExportedTypes.includes(typeRef) && !toImport.includes(typeRef)) {
725
+ toImport.push(typeRef);
726
+ }
727
+ }
728
+
729
+ const typeDefinitions = extractReferencedTypeDefinitions(sourceFile, typeReferences);
730
+
731
+ const importStatement = `import { ${toImport.join(', ')} } from '@/components/${type}/${actualFilename}';`;
732
+
733
+ const importLine = `**Import**: \`${importStatement}\``;
734
+ const typeDefsLine = typeDefinitions ? `**Type Definitions**:\n ${typeDefinitions}` : '';
735
+ const propsLine = propsInline ? `**Props**: \`${propsInline}\`` : '**Props**: None';
736
+
737
+ const parts = [importLine, typeDefsLine, propsLine].filter((p) => p !== '');
738
+ return parts.join('\n');
739
+ }
740
+
741
+ async function readComponentPropsInterface(
742
+ projectDir: string,
743
+ _type: string,
744
+ name: string,
745
+ registry: ComponentRegistry,
746
+ ): Promise<string | null> {
747
+ const entry = registry.get(name);
748
+ if (entry === undefined) {
749
+ debugProcess(`[readComponentPropsInterface] No registry entry found for ${name}`);
750
+ return null;
751
+ }
752
+
753
+ const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
754
+ debugProcess(`[readComponentPropsInterface] Reading file: ${file}`);
257
755
  try {
258
- return await fs.readFile(file, 'utf-8');
259
- } catch {
756
+ const sourceCode = await fs.readFile(file, 'utf-8');
757
+ const result = extractPropsAndImport(sourceCode, name, entry.type, entry.actualFilename);
758
+ debugProcess(`[readComponentPropsInterface] extractPropsAndImport returned ${result ? result.length : 0} chars`);
759
+ return result;
760
+ } catch (error) {
761
+ debugProcess(`[readComponentPropsInterface] Error reading file: ${String(error)}`);
762
+ return null;
763
+ }
764
+ }
765
+
766
+ async function readComponentFullImplementation(
767
+ projectDir: string,
768
+ _type: string,
769
+ name: string,
770
+ registry: ComponentRegistry,
771
+ ): Promise<string | null> {
772
+ const entry = registry.get(name);
773
+ if (entry === undefined) {
774
+ debugProcess(`[readComponentFullImplementation] No registry entry found for ${name}`);
775
+ return null;
776
+ }
777
+
778
+ const file = path.join(projectDir, 'src', 'components', entry.type, `${entry.actualFilename}.tsx`);
779
+ debugProcess(`[readComponentFullImplementation] Reading file: ${file}`);
780
+ try {
781
+ const sourceCode = await fs.readFile(file, 'utf-8');
782
+ const sourceFile = ts.createSourceFile(`${name}.tsx`, sourceCode, ts.ScriptTarget.Latest, true);
783
+ const exportedComponents = extractExportedComponentNames(sourceFile);
784
+ const toImport = exportedComponents.length > 0 ? [...exportedComponents] : [name];
785
+ const importStatement = `import { ${toImport.join(', ')} } from '@/components/${entry.type}/${entry.actualFilename}';`;
786
+
787
+ const formattedOutput = `**Import**: \`${importStatement}\`\n\n**Implementation**:\n\`\`\`tsx\n${sourceCode}\n\`\`\``;
788
+
789
+ debugProcess(`[readComponentFullImplementation] Returning ${formattedOutput.length} chars for ${name}`);
790
+ return formattedOutput;
791
+ } catch (error) {
792
+ debugProcess(`[readComponentFullImplementation] Error reading file: ${String(error)}`);
260
793
  return null;
261
794
  }
262
795
  }
@@ -268,34 +801,172 @@ function extractCodeBlock(text: string): string {
268
801
  .trim();
269
802
  }
270
803
 
271
- async function readAllTopLevelFiles(projectDir: string): Promise<Record<string, string>> {
272
- debugProcess('[readAllTopLevelFiles] Reading project files from %s', projectDir);
804
+ function detectTruncation(code: string): boolean {
805
+ const truncationIndicators = [/<\w+[^/>]*$/, /<\/\w*$/, /"[^"]*$/, /'[^']*$/, /`[^`]*$/, /\{[^}]*$/m, /\([^)]*$/m];
806
+
807
+ const lastLines = code.split('\n').slice(-5).join('\n');
808
+
809
+ return truncationIndicators.some((pattern) => pattern.test(lastLines));
810
+ }
811
+
812
+ async function readEssentialFiles(projectDir: string): Promise<Record<string, string>> {
813
+ debugProcess('[readEssentialFiles] Reading essential config files from %s', projectDir);
273
814
  const start = performance.now();
274
815
  const config: Record<string, string> = {};
275
816
 
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
- }
817
+ const essentialFiles = ['package.json', 'tsconfig.json', 'tailwind.config.js', 'tailwind.config.ts'];
818
+
819
+ for (const file of essentialFiles) {
820
+ try {
821
+ config[file] = await fs.readFile(path.join(projectDir, file), 'utf-8');
822
+ debugProcess(`Read essential file: ${file}`);
823
+ } catch {
824
+ debugProcess(`Essential file not found: ${file}`);
291
825
  }
292
826
  }
293
827
 
294
- await readRecursive(projectDir);
295
- debugProcess(`[readAllTopLevelFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
828
+ debugProcess(`[readEssentialFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
296
829
  return config;
297
830
  }
298
831
 
832
+ function hasDataRequirements(componentDef: Record<string, unknown>): boolean {
833
+ return (
834
+ 'data_requirements' in componentDef &&
835
+ Array.isArray(componentDef.data_requirements) &&
836
+ componentDef.data_requirements.length > 0
837
+ );
838
+ }
839
+
840
+ function checkOrganismForMolecule(moleculeName: string, organismName: string, organismDef: unknown): boolean {
841
+ if (typeof organismDef !== 'object' || organismDef === null) return false;
842
+
843
+ const composition = (organismDef as Record<string, unknown>).composition as { molecules?: string[] } | undefined;
844
+
845
+ const includesMolecule = composition?.molecules?.includes(moleculeName) ?? false;
846
+ if (!includesMolecule) return false;
847
+
848
+ const hasData = hasDataRequirements(organismDef as Record<string, unknown>);
849
+ if (hasData) {
850
+ debugProcess(
851
+ `[findParentDataRequirements] Molecule ${moleculeName} is used by organism ${organismName} which has data_requirements`,
852
+ );
853
+ return true;
854
+ }
855
+
856
+ return false;
857
+ }
858
+
859
+ function checkPageLayoutOrganisms(
860
+ layout: { organisms?: string[] } | undefined,
861
+ organisms: { items: Record<string, unknown> },
862
+ moleculeName: string,
863
+ ): boolean {
864
+ if (layout?.organisms === undefined) return false;
865
+
866
+ for (const organismName of layout.organisms) {
867
+ const organismDef = organisms.items[organismName];
868
+ if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
869
+ return true;
870
+ }
871
+ }
872
+
873
+ return false;
874
+ }
875
+
876
+ function checkPagesForMolecule(
877
+ pages: { items?: Record<string, unknown> } | undefined,
878
+ organisms: { items?: Record<string, unknown> } | undefined,
879
+ moleculeName: string,
880
+ ): boolean {
881
+ if (pages?.items === undefined || organisms?.items === undefined) return false;
882
+
883
+ const organismsWithItems = { items: organisms.items };
884
+
885
+ for (const [, pageDef] of Object.entries(pages.items)) {
886
+ if (typeof pageDef !== 'object' || pageDef === null) continue;
887
+
888
+ const layout = (pageDef as Record<string, unknown>).layout as { organisms?: string[] } | undefined;
889
+ if (checkPageLayoutOrganisms(layout, organismsWithItems, moleculeName)) {
890
+ return true;
891
+ }
892
+ }
893
+
894
+ return false;
895
+ }
896
+
897
+ function findParentDataRequirements(scheme: Record<string, unknown>, moleculeName: string): boolean {
898
+ debugProcess(`[findParentDataRequirements] Checking if molecule ${moleculeName} has parents with data requirements`);
899
+
900
+ const organisms = scheme.organisms as { items?: Record<string, unknown> } | undefined;
901
+
902
+ if (organisms?.items !== undefined) {
903
+ for (const [organismName, organismDef] of Object.entries(organisms.items)) {
904
+ if (checkOrganismForMolecule(moleculeName, organismName, organismDef)) {
905
+ return true;
906
+ }
907
+ }
908
+ }
909
+
910
+ const pages = scheme.pages as { items?: Record<string, unknown> } | undefined;
911
+ if (checkPagesForMolecule(pages, organisms, moleculeName)) {
912
+ return true;
913
+ }
914
+
915
+ debugProcess(`[findParentDataRequirements] No parents with data_requirements found for molecule ${moleculeName}`);
916
+ return false;
917
+ }
918
+
919
+ async function readGraphQLFiles(projectDir: string): Promise<Record<string, string>> {
920
+ debugProcess('[readGraphQLFiles] Reading GraphQL type definition files from %s', projectDir);
921
+ const start = performance.now();
922
+ const graphqlFiles: Record<string, string> = {};
923
+
924
+ const graphqlFilePaths = ['src/gql/graphql.ts', 'src/graphql/queries.ts', 'src/graphql/mutations.ts'];
925
+
926
+ for (const filePath of graphqlFilePaths) {
927
+ try {
928
+ graphqlFiles[filePath] = await fs.readFile(path.join(projectDir, filePath), 'utf-8');
929
+ debugProcess(`Read GraphQL file: ${filePath}`);
930
+ } catch {
931
+ debugProcess(`GraphQL file not found: ${filePath}`);
932
+ }
933
+ }
934
+
935
+ debugProcess(`[readGraphQLFiles] Completed in ${(performance.now() - start).toFixed(2)} ms`);
936
+ return graphqlFiles;
937
+ }
938
+
939
+ async function readProjectContext(projectDir: string): Promise<Record<string, string>> {
940
+ return await readEssentialFiles(projectDir);
941
+ }
942
+
943
+ async function executeTypeCheck(tsconfigRoot: string, strict: boolean): Promise<string> {
944
+ const args = strict
945
+ ? ['tsc', '--noEmit', '--skipLibCheck', '--strict', '--pretty', 'false']
946
+ : ['tsc', '--noEmit', '--skipLibCheck', '--pretty', 'false'];
947
+
948
+ const result = await execa('npx', args, {
949
+ cwd: tsconfigRoot,
950
+ stdio: 'pipe',
951
+ reject: false,
952
+ });
953
+
954
+ return (result.stdout ?? '') + (result.stderr ?? '');
955
+ }
956
+
957
+ async function runGraphQLStrictCheck(
958
+ tsconfigRoot: string,
959
+ relativeFilePath: string,
960
+ normalizedRelative: string,
961
+ filePath: string,
962
+ ): Promise<string> {
963
+ debugTypeCheck(`[runTypeCheckForFile] Running strict GraphQL type check...`);
964
+ const strictOutput = await executeTypeCheck(tsconfigRoot, true);
965
+ const graphqlStrictErrors = filterErrorsForFile(strictOutput, relativeFilePath, normalizedRelative, filePath);
966
+ debugTypeCheck(`[runTypeCheckForFile] GraphQL strict errors length: ${graphqlStrictErrors.length} chars`);
967
+ return graphqlStrictErrors;
968
+ }
969
+
299
970
  async function runTypeCheckForFile(
300
971
  projectDir: string,
301
972
  filePath: string,
@@ -303,40 +974,124 @@ async function runTypeCheckForFile(
303
974
  const start = performance.now();
304
975
  try {
305
976
  const tsconfigRoot = await findProjectRoot(projectDir);
306
- const relativeFilePath = path.relative(tsconfigRoot, filePath).replace(/\\/g, '/');
977
+ const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(tsconfigRoot, filePath);
978
+ const relativeFilePath = path.relative(tsconfigRoot, absoluteFilePath).replace(/\\/g, '/');
307
979
  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
980
 
314
- const output = (result.stdout ?? '') + (result.stderr ?? '');
981
+ debugTypeCheck(`[runTypeCheckForFile] tsconfigRoot: ${tsconfigRoot}`);
982
+ debugTypeCheck(`[runTypeCheckForFile] absoluteFilePath: ${absoluteFilePath}`);
983
+ debugTypeCheck(`[runTypeCheckForFile] relativeFilePath: ${relativeFilePath}`);
984
+ debugTypeCheck(`[runTypeCheckForFile] normalizedRelative: ${normalizedRelative}`);
985
+
986
+ const isGraphQLFile = await detectGraphQLFile(absoluteFilePath, relativeFilePath);
987
+ debugTypeCheck(`[runTypeCheckForFile] isGraphQLFile: ${isGraphQLFile}`);
988
+
989
+ const output = await executeTypeCheck(tsconfigRoot, false);
315
990
  debugTypeCheck(`[runTypeCheckForFile] Finished tsc in ${(performance.now() - start).toFixed(2)} ms`);
991
+ debugTypeCheck(`[runTypeCheckForFile] Total output length: ${output.length} chars`);
992
+ debugTypeCheck(`[runTypeCheckForFile] Output preview (first 2000 chars):\n${output.substring(0, 2000)}`);
993
+
994
+ const filteredErrors = filterErrorsForFile(output, relativeFilePath, normalizedRelative, filePath);
995
+
996
+ const graphqlStrictErrors = isGraphQLFile
997
+ ? await runGraphQLStrictCheck(tsconfigRoot, relativeFilePath, normalizedRelative, filePath)
998
+ : '';
999
+
1000
+ const formattedErrors = formatTypeCheckErrors(filteredErrors, graphqlStrictErrors);
316
1001
 
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 };
1002
+ if (!output.includes('error TS') && formattedErrors.trim().length === 0) {
1003
+ debugTypeCheck(`[runTypeCheckForFile] No errors found`);
1004
+ return { success: true, errors: '' };
1005
+ }
1006
+
1007
+ if (formattedErrors.trim().length === 0) return { success: true, errors: '' };
1008
+ return { success: false, errors: formattedErrors };
334
1009
  } catch (err) {
335
1010
  const message = err instanceof Error ? err.message : String(err);
336
1011
  return { success: false, errors: message };
337
1012
  }
338
1013
  }
339
1014
 
1015
+ async function detectGraphQLFile(absoluteFilePath: string, relativeFilePath: string): Promise<boolean> {
1016
+ const isInGraphQLDir =
1017
+ relativeFilePath.includes('/graphql/') ||
1018
+ relativeFilePath.includes('/gql/') ||
1019
+ relativeFilePath.includes('\\graphql\\') ||
1020
+ relativeFilePath.includes('\\gql\\');
1021
+
1022
+ if (isInGraphQLDir) return true;
1023
+
1024
+ try {
1025
+ const content = await fs.readFile(absoluteFilePath, 'utf-8');
1026
+ return (
1027
+ content.includes("from '@/gql/graphql'") ||
1028
+ content.includes('from "@/gql/graphql"') ||
1029
+ content.includes("from '@/graphql/") ||
1030
+ content.includes('from "@/graphql/') ||
1031
+ content.includes('@apollo/client')
1032
+ );
1033
+ } catch {
1034
+ return false;
1035
+ }
1036
+ }
1037
+
1038
+ function filterErrorsForFile(
1039
+ output: string,
1040
+ relativeFilePath: string,
1041
+ normalizedRelative: string,
1042
+ filePath: string,
1043
+ ): string {
1044
+ const allLines = output.split('\n');
1045
+ const errorLines = allLines.filter((line) => line.includes('error TS'));
1046
+ debugTypeCheck(`[filterErrorsForFile] Total lines: ${allLines.length}, Error lines: ${errorLines.length}`);
1047
+
1048
+ const filteredErrors = output
1049
+ .split('\n')
1050
+ .filter((line) => {
1051
+ const hasError = line.includes('error TS');
1052
+ const notNodeModules = !line.includes('node_modules');
1053
+ const matchesTarget =
1054
+ line.includes(relativeFilePath) || line.includes(normalizedRelative) || line.includes(path.basename(filePath));
1055
+
1056
+ if (hasError) {
1057
+ debugTypeCheck(`[filterErrorsForFile] Checking error line: ${line.substring(0, 150)}`);
1058
+ debugTypeCheck(
1059
+ `[filterErrorsForFile] hasError: ${hasError}, notNodeModules: ${notNodeModules}, matchesTarget: ${matchesTarget}`,
1060
+ );
1061
+ }
1062
+
1063
+ return hasError && notNodeModules && matchesTarget;
1064
+ })
1065
+ .join('\n');
1066
+
1067
+ debugTypeCheck(`[filterErrorsForFile] Filtered errors length: ${filteredErrors.length} chars`);
1068
+ return filteredErrors;
1069
+ }
1070
+
1071
+ function formatTypeCheckErrors(regularErrors: string, graphqlStrictErrors: string): string {
1072
+ let formattedOutput = '';
1073
+
1074
+ if (graphqlStrictErrors.trim().length > 0) {
1075
+ formattedOutput += '## GraphQL Schema Type Errors (strict mode)\n\n';
1076
+ formattedOutput += 'These errors indicate fields/properties that violate GraphQL schema type contracts.\n';
1077
+ formattedOutput += 'Common causes:\n';
1078
+ formattedOutput += '- Using fields not defined in the GraphQL schema\n';
1079
+ formattedOutput += '- Incorrect property types in mutation variables\n';
1080
+ formattedOutput += '- Missing required fields in input types\n\n';
1081
+ formattedOutput += graphqlStrictErrors;
1082
+ formattedOutput += '\n\n';
1083
+ formattedOutput += '**Fix**: Check @/gql/graphql.ts for the exact type definition and valid fields.\n\n';
1084
+ formattedOutput += '---\n\n';
1085
+ }
1086
+
1087
+ if (regularErrors.trim().length > 0) {
1088
+ formattedOutput += '## TypeScript Errors\n\n';
1089
+ formattedOutput += regularErrors;
1090
+ }
1091
+
1092
+ return formattedOutput.trim();
1093
+ }
1094
+
340
1095
  async function findProjectRoot(startDir: string): Promise<string> {
341
1096
  let dir = startDir;
342
1097
  while (dir !== path.dirname(dir)) {
@@ -351,7 +1106,75 @@ async function findProjectRoot(startDir: string): Promise<string> {
351
1106
  throw new Error('Could not find project root (no package.json or tsconfig.json found)');
352
1107
  }
353
1108
 
354
- // eslint-disable-next-line complexity
1109
+ interface ComponentUsageInfo {
1110
+ usageExamples: Array<{ file: string; snippet: string }>;
1111
+ requiresChildren: boolean;
1112
+ detectedProps: string[];
1113
+ }
1114
+
1115
+ async function extractComponentUsageFromScaffolds(
1116
+ componentName: string,
1117
+ componentType: 'molecule' | 'organism',
1118
+ projectDir: string,
1119
+ ): Promise<ComponentUsageInfo> {
1120
+ const usageExamples: Array<{ file: string; snippet: string }> = [];
1121
+ let requiresChildren = false;
1122
+ const detectedProps: string[] = [];
1123
+
1124
+ const searchDirs: string[] = [];
1125
+ if (componentType === 'molecule') {
1126
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'organisms'));
1127
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
1128
+ } else if (componentType === 'organism') {
1129
+ searchDirs.push(path.join(projectDir, 'src', 'components', 'pages'));
1130
+ }
1131
+
1132
+ for (const dir of searchDirs) {
1133
+ try {
1134
+ const files = await fs.readdir(dir);
1135
+ for (const file of files) {
1136
+ if (!file.endsWith('.tsx')) continue;
1137
+
1138
+ const filePath = path.join(dir, file);
1139
+ const content = await fs.readFile(filePath, 'utf-8');
1140
+
1141
+ const openTagPattern = new RegExp(`<${componentName}(?:\\s|>)`, 'g');
1142
+ if (!openTagPattern.test(content)) continue;
1143
+
1144
+ const withChildrenPattern = new RegExp(`<${componentName}[^>]*>([\\s\\S]*?)</${componentName}>`, 'g');
1145
+
1146
+ const hasChildren = withChildrenPattern.test(content);
1147
+ if (hasChildren) {
1148
+ requiresChildren = true;
1149
+ }
1150
+
1151
+ const lines = content.split('\n');
1152
+ const usageLineIndexes: number[] = [];
1153
+ lines.forEach((line, idx) => {
1154
+ if (line.includes(`<${componentName}`)) {
1155
+ usageLineIndexes.push(idx);
1156
+ }
1157
+ });
1158
+
1159
+ if (usageLineIndexes.length > 0) {
1160
+ const lineIdx = usageLineIndexes[0];
1161
+ const start = Math.max(0, lineIdx - 2);
1162
+ const end = Math.min(lines.length, lineIdx + 8);
1163
+ const snippet = lines.slice(start, end).join('\n');
1164
+ usageExamples.push({
1165
+ file: path.relative(projectDir, filePath),
1166
+ snippet,
1167
+ });
1168
+ }
1169
+ }
1170
+ } catch {
1171
+ // ignore read errors
1172
+ }
1173
+ }
1174
+
1175
+ return { usageExamples, requiresChildren, detectedProps };
1176
+ }
1177
+
355
1178
  function makeBasePrompt(
356
1179
  componentType: string,
357
1180
  componentName: string,
@@ -360,35 +1183,12 @@ function makeBasePrompt(
360
1183
  projectConfig: Record<string, string>,
361
1184
  designSystemReference: string,
362
1185
  dependencySources: Record<string, string>,
1186
+ usageInfo: ComponentUsageInfo,
363
1187
  ): string {
364
1188
  const hasScaffold = Boolean(existingScaffold?.trim());
365
1189
 
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
1190
  const configSection =
391
- Object.entries(otherFiles)
1191
+ Object.entries(projectConfig)
392
1192
  .map(([p, c]) => `### ${p}\n${c}`)
393
1193
  .join('\n\n') || '(No additional config files)';
394
1194
 
@@ -402,7 +1202,7 @@ function makeBasePrompt(
402
1202
  .join('\n\n') || '(No dependencies found)';
403
1203
 
404
1204
  return `
405
- # Implementation Brief: ${componentName} (${componentType})
1205
+ # Implement ${componentName} (${componentType})
406
1206
 
407
1207
  You are a senior frontend engineer specializing in **React + TypeScript + Apollo Client**.
408
1208
  Your task is to build a visually excellent, type-safe, and production-ready ${componentType} component.
@@ -421,55 +1221,85 @@ Your component must:
421
1221
 
422
1222
  ---
423
1223
 
1224
+ ## IA Schema
1225
+ ${JSON.stringify(componentDef, null, 2)}
1226
+
1227
+ ---
1228
+
424
1229
  ## Project Context
425
1230
 
426
- **File Path:** src/components/${componentType}/${componentName}.tsx
427
1231
  **Purpose:** A reusable UI element connected to the GraphQL layer and design system.
428
1232
 
429
- ### IA Schema
430
- ${JSON.stringify(componentDef, null, 2)}
1233
+ ## Component Scaffold
431
1234
 
432
- ### Existing Scaffold
433
- ${hasScaffold ? existingScaffold : '(No existing scaffold found)'}
1235
+ ${
1236
+ hasScaffold
1237
+ ? `The scaffold below contains:
1238
+ - Import statements for all dependencies (use these exact imports)
1239
+ - Type guidance comments showing GraphQL queries/mutations/enums to use
1240
+ - Specs describing required functionality
1241
+ - Component structure to implement
434
1242
 
435
- ### Design System Reference
436
- ${designSystemBlock}
1243
+ **CRITICAL**: Follow the import statements and type guidance comments in the scaffold exactly.
437
1244
 
438
- ### Related Components (Dependencies)
439
- ${dependencySection}
1245
+ ${existingScaffold}`
1246
+ : '(No existing scaffold found - create component from scratch)'
1247
+ }
440
1248
 
441
- ### GraphQL Context (src/graphql)
442
- ${graphqlSection}
1249
+ ---
443
1250
 
444
- #### queries.ts
445
- ${queriesFile || '(queries.ts not found)'}
1251
+ ## Design System
1252
+ ${designSystemBlock}
446
1253
 
447
- #### mutations.ts
448
- ${mutationsFile || '(mutations.ts not found)'}
1254
+ ---
449
1255
 
450
- ### GraphQL Codegen (src/gql)
451
- ${gqlSection}
1256
+ ## Available Dependencies
452
1257
 
453
- ### Other Relevant Files
454
- ${configSection}
1258
+ ${dependencySection}
455
1259
 
456
1260
  ---
457
1261
 
458
- ## Engineering Guidelines
1262
+ ## Project Configuration
1263
+ ${configSection}
459
1264
 
460
- **Type Safety**
461
- - Explicitly type all props, state, and GraphQL responses.
462
- - Avoid \`any\` — prefer discriminated unions, interfaces, and generics.
1265
+ ---
463
1266
 
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.
1267
+ ## Implementation Rules
469
1268
 
470
- **Error Handling**
471
- - Wrap async operations in try/catch with graceful fallback UI.
472
- - Check for null/undefined using optional chaining (?.) and defaults (??).
1269
+ **Type Safety**
1270
+ - No \`any\` or \`as SomeType\` - type correctly
1271
+ - Import types from dependencies - never redefine them locally
1272
+ - Type all props, state, and GraphQL responses explicitly
1273
+
1274
+ **Imports**
1275
+ - Use exact imports from scaffold and dependencies section
1276
+ - Pattern: \`@/components/{type}/{component}\`
1277
+ - Never use relative paths (\`../\`)
1278
+ - Only use packages from package.json shown above
1279
+
1280
+ **GraphQL (if applicable)**
1281
+ - **CRITICAL**: NEVER use inline gql template literals or import gql from @apollo/client
1282
+ - **CRITICAL**: ALWAYS import pre-generated operations from @/graphql/queries or @/graphql/mutations
1283
+ - Follow type guidance comments in scaffold for exact query/mutation names
1284
+ - Use pattern: \`const { data } = useQuery(QueryName)\` where QueryName is imported from @/graphql/queries
1285
+ - Use pattern: \`const [mutate, { loading }] = useMutation(MutationName, { refetchQueries: [...], awaitRefetchQueries: true })\`
1286
+ - Access enum values: \`EnumName.Value\` (e.g., \`TodoStateStatus.Pending\`)
1287
+ - **CRITICAL**: ALL mutations return \`{ success: Boolean!, error: { type: String!, message: String } }\`
1288
+ - **CRITICAL**: ALWAYS check \`data?.mutationName?.success\` before considering mutation successful
1289
+ - **CRITICAL**: ALWAYS handle \`data?.mutationName?.error?.message\` when \`success\` is false
1290
+ - **CRITICAL**: ALWAYS use \`loading\` state to disable buttons during mutations: \`disabled={loading}\`
1291
+ - **CRITICAL**: ALWAYS wrap mutations in try-catch for network errors
1292
+ - **CRITICAL**: Use \`awaitRefetchQueries: true\` to prevent race conditions with polling queries
1293
+
1294
+ **React Best Practices**
1295
+ - No setState during render
1296
+ - Include dependency arrays in useEffect
1297
+ - Use optional chaining (?.) and nullish coalescing (??)
1298
+ ${
1299
+ usageInfo.requiresChildren
1300
+ ? `\n**CRITICAL**: This component MUST accept \`children?: React.ReactNode\` prop based on parent usage patterns.`
1301
+ : ''
1302
+ }
473
1303
 
474
1304
  **Visual & UX Quality**
475
1305
  - Perfect spacing and alignment using Tailwind or the design system tokens.
@@ -486,9 +1316,10 @@ ${configSection}
486
1316
  - Match button, card, and badge styles with existing components.
487
1317
 
488
1318
  **Prohibited**
489
- - No placeholder data, TODOs, or pseudo-logic.
490
- - No new external packages.
491
- - No commented-out or partial implementations.
1319
+ - No placeholder data or TODOs
1320
+ - No new external packages not in package.json
1321
+ - No reimplementing dependencies inline
1322
+ - No redefining types that dependencies export
492
1323
 
493
1324
  ---
494
1325
 
@@ -510,51 +1341,170 @@ ${configSection}
510
1341
 
511
1342
  ---
512
1343
 
513
- **Final Output Requirement:**
514
- Return only the complete \`.tsx\` source code for this component — no markdown fences, commentary, or extra text.
1344
+ **Output**: Return ONLY the complete .tsx source code - no markdown fences or commentary.
515
1345
  `.trim();
516
1346
  }
517
1347
 
518
- function makeImplementPrompt(basePrompt: string): string {
519
- return `${basePrompt}
1348
+ function makeImplementPrompt(basePrompt: string, graphqlFiles?: Record<string, string>): string {
1349
+ const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
1350
+ const graphqlSection = hasGraphQLFiles
1351
+ ? `
1352
+ ## GraphQL Type Definitions (Source of Truth)
1353
+
1354
+ **CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
1355
+
1356
+ ${Object.entries(graphqlFiles)
1357
+ .map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
1358
+ .join('\n\n')}
520
1359
 
521
1360
  ---
522
1361
 
523
- Generate the **complete final implementation** for \`${basePrompt}\`.
524
- Begin directly with import statements and end with the export statement.
1362
+ `
1363
+ : '';
1364
+
1365
+ return `${basePrompt}
1366
+
1367
+ ${graphqlSection}---
1368
+
1369
+ Begin directly with import statements and end with the export statement.
525
1370
  Do not include markdown fences, comments, or explanations — only the valid .tsx file content.
526
1371
  `.trim();
527
1372
  }
528
1373
 
1374
+ function validateComponentImport(componentType: string, filename: string, registry: ComponentRegistry): string | null {
1375
+ const importPath = `@/components/${componentType}/${filename}`;
1376
+ const exists = Array.from(registry.values()).some(
1377
+ (entry) => entry.type === componentType && entry.actualFilename === filename,
1378
+ );
1379
+
1380
+ if (exists) {
1381
+ return null;
1382
+ }
1383
+
1384
+ const suggestions = Array.from(registry.values())
1385
+ .filter((entry) => entry.actualFilename.includes(filename) || filename.includes(entry.actualFilename))
1386
+ .map((entry) => `@/components/${entry.type}/${entry.actualFilename}`)
1387
+ .slice(0, 3);
1388
+
1389
+ if (suggestions.length > 0) {
1390
+ return `Import not found: ${importPath}\nDid you mean: ${suggestions.join(', ')}?`;
1391
+ }
1392
+ return `Import not found: ${importPath}`;
1393
+ }
1394
+
1395
+ function validateNonComponentImport(importPath: string): string | null {
1396
+ if (importPath.startsWith('@/store/')) {
1397
+ return `Invalid import: ${importPath}\nThis project uses Apollo Client with GraphQL. Check the GraphQL files in context for available queries and mutations.`;
1398
+ }
1399
+ return null;
1400
+ }
1401
+
1402
+ function validateImports(code: string, registry: ComponentRegistry): { valid: boolean; errors: string[] } {
1403
+ const errors: string[] = [];
1404
+
1405
+ const componentImportPattern = /import\s+[^'"]*from\s+['"]@\/components\/([^/]+)\/([^'"]+)['"]/g;
1406
+ let match;
1407
+ while ((match = componentImportPattern.exec(code)) !== null) {
1408
+ const error = validateComponentImport(match[1], match[2], registry);
1409
+ if (error !== null) {
1410
+ errors.push(error);
1411
+ }
1412
+ }
1413
+
1414
+ const allImportPattern = /import\s+[^'"]*from\s+['"](@\/[^'"]+)['"]/g;
1415
+ while ((match = allImportPattern.exec(code)) !== null) {
1416
+ const error = validateNonComponentImport(match[1]);
1417
+ if (error !== null) {
1418
+ errors.push(error);
1419
+ }
1420
+ }
1421
+
1422
+ return { valid: errors.length === 0, errors };
1423
+ }
1424
+
529
1425
  function makeRetryPrompt(
530
- basePrompt: string,
531
- componentType: string,
532
1426
  componentName: string,
533
- previousCode: string,
534
1427
  previousErrors: string,
1428
+ composition: string[],
1429
+ dependencySummary: string,
1430
+ importValidationErrors: string[],
1431
+ description: string,
1432
+ existingScaffold: string,
1433
+ packageJson: string,
1434
+ graphqlFiles?: Record<string, string>,
535
1435
  ): string {
1436
+ const compositionHint = composition.length > 0 ? `\n**Required Components**: ${composition.join(', ')}\n` : '';
1437
+
1438
+ const importErrorsHint =
1439
+ importValidationErrors.length > 0
1440
+ ? `\n**Import Errors**:\n${importValidationErrors.map((err) => `- ${err}`).join('\n')}\n`
1441
+ : '';
1442
+
1443
+ const hasScaffold = Boolean(existingScaffold?.trim());
1444
+ const scaffoldSection = hasScaffold
1445
+ ? `
1446
+ ## Scaffold with Type Guidance
1447
+
1448
+ **CRITICAL**: Follow the type guidance comments in the scaffold for exact GraphQL operation names, enum values, and import patterns.
1449
+
1450
+ ${existingScaffold}
1451
+
1452
+ ---
1453
+ `
1454
+ : '';
1455
+
1456
+ const hasGraphQLFiles = graphqlFiles !== undefined && Object.keys(graphqlFiles).length > 0;
1457
+ const graphqlSection = hasGraphQLFiles
1458
+ ? `
1459
+ ## GraphQL Type Definitions (Source of Truth)
1460
+
1461
+ **CRITICAL**: Use these exact TypeScript definitions for GraphQL types, queries, and mutations.
1462
+
1463
+ ${Object.entries(graphqlFiles)
1464
+ .map(([filePath, content]) => `### ${filePath}\n\`\`\`typescript\n${content}\n\`\`\``)
1465
+ .join('\n\n')}
1466
+
1467
+ ---
1468
+ `
1469
+ : '';
1470
+
536
1471
  return `
537
- ${basePrompt}
1472
+ # Fix TypeScript Errors: ${componentName}
1473
+
1474
+ ${description ? `**Description**: ${description}\n` : ''}
1475
+ ${compositionHint}
1476
+ ${scaffoldSection}
1477
+ ${graphqlSection}
1478
+ ## Project Dependencies (package.json)
1479
+
1480
+ **CRITICAL**: Only import packages that exist in the dependencies below.
1481
+
1482
+ ${packageJson}
538
1483
 
539
1484
  ---
540
1485
 
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.
1486
+ ## Available Components
1487
+
1488
+ ${dependencySummary}
1489
+
1490
+ ---
1491
+
1492
+ ## Errors to Fix
544
1493
 
545
- **Errors**
546
1494
  ${previousErrors}
547
1495
 
548
- **Previous Code**
549
- ${previousCode}
1496
+ ${importErrorsHint}
1497
+ **Hints**:
1498
+ - Follow scaffold's type guidance comments for exact operation names and enum values
1499
+ ${hasGraphQLFiles ? '- Use the GraphQL Type Definitions section above for exact types, interfaces, and enums' : '- Check @/gql/graphql.ts for GraphQL type definitions'}
1500
+ - **CRITICAL**: NEVER use inline \`gql\` template literals - ALWAYS import from @/graphql/queries or @/graphql/mutations
1501
+ - Use pattern: \`mutate({ variables: { input: InputType } })\`
1502
+ - Import pattern: \`@/components/{type}/{component}\`
1503
+ - Import types from dependencies - never redefine them
550
1504
 
551
1505
  ---
552
1506
 
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.
1507
+ **Output**: Return ONLY the corrected ${componentName}.tsx code - no markdown fences or commentary.
558
1508
  `.trim();
559
1509
  }
560
1510
 
@@ -581,6 +1531,77 @@ function isValidCollection(collection: unknown): collection is { items: Record<s
581
1531
  return typeof items === 'object' && items !== null;
582
1532
  }
583
1533
 
1534
+ async function buildComponentRegistry(projectDir: string): Promise<ComponentRegistry> {
1535
+ const registry: ComponentRegistry = new Map();
1536
+ const types: Array<'atoms' | 'molecules' | 'organisms'> = ['atoms', 'molecules', 'organisms'];
1537
+
1538
+ for (const type of types) {
1539
+ const dir = path.join(projectDir, 'src', 'components', type);
1540
+ try {
1541
+ const files = await fs.readdir(dir);
1542
+
1543
+ for (const file of files) {
1544
+ if (!file.endsWith('.tsx')) continue;
1545
+
1546
+ const fullPath = path.join(dir, file);
1547
+ const content = await fs.readFile(fullPath, 'utf-8');
1548
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
1549
+
1550
+ const exportedTypes = extractAllExportedTypes(sourceFile);
1551
+ const exportedComponents = extractExportedComponentNames(sourceFile);
1552
+ const allExports = [...new Set([...exportedTypes, ...exportedComponents])];
1553
+
1554
+ if (allExports.length === 0) continue;
1555
+
1556
+ const actualFilename = file.replace('.tsx', '');
1557
+
1558
+ for (const exportName of allExports) {
1559
+ registry.set(exportName, {
1560
+ name: exportName,
1561
+ actualFilename,
1562
+ type,
1563
+ exports: allExports,
1564
+ });
1565
+ }
1566
+ }
1567
+ } catch (error) {
1568
+ debugProcess(`[buildComponentRegistry] Could not read ${type} directory: ${String(error)}`);
1569
+ }
1570
+ }
1571
+
1572
+ debugProcess(`[buildComponentRegistry] Indexed ${registry.size} components`);
1573
+ return registry;
1574
+ }
1575
+
1576
+ function resolveDependenciesToRegistry(
1577
+ dependencies: Array<{ type: string; name: string }>,
1578
+ registry: ComponentRegistry,
1579
+ ): {
1580
+ primary: ComponentRegistryEntry[];
1581
+ available: ComponentRegistryEntry[];
1582
+ } {
1583
+ const primary: ComponentRegistryEntry[] = [];
1584
+
1585
+ debugProcess(
1586
+ `[resolveDependenciesToRegistry] Processing ${dependencies.length} dependencies: ${dependencies.map((d) => `${d.type}/${d.name}`).join(', ')}`,
1587
+ );
1588
+
1589
+ for (const dep of dependencies) {
1590
+ const entry = registry.get(dep.name);
1591
+ if (entry !== undefined) {
1592
+ debugProcess(`[resolveDependenciesToRegistry] Found registry entry for ${dep.name}`);
1593
+ primary.push(entry);
1594
+ } else {
1595
+ debugProcess(`[resolveDependenciesToRegistry] No registry entry for ${dep.name}`);
1596
+ }
1597
+ }
1598
+
1599
+ const available = Array.from(registry.values()).filter((entry) => entry.type === 'atoms');
1600
+
1601
+ debugProcess(`[resolveDependenciesToRegistry] Resolved ${primary.length} primary dependencies`);
1602
+ return { primary, available };
1603
+ }
1604
+
584
1605
  async function readDesignSystem(
585
1606
  providedPath: string,
586
1607
  refs: { projectDir: string; iaSchemeDir: string },