@auto-engineer/component-implementor-react 1.98.0 → 1.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +92 -0
  5. package/dist/src/commands/implement-component.d.ts +19 -0
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +109 -30
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +259 -69
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/extract-exports.d.ts +6 -0
  12. package/dist/src/extract-exports.d.ts.map +1 -0
  13. package/dist/src/extract-exports.js +46 -0
  14. package/dist/src/extract-exports.js.map +1 -0
  15. package/dist/src/generate-story-deterministic.d.ts +30 -0
  16. package/dist/src/generate-story-deterministic.d.ts.map +1 -0
  17. package/dist/src/generate-story-deterministic.js +229 -0
  18. package/dist/src/generate-story-deterministic.js.map +1 -0
  19. package/dist/src/index.d.ts +4 -0
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/pipeline/run-pipeline.d.ts +69 -0
  24. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
  25. package/dist/src/pipeline/run-pipeline.js +78 -0
  26. package/dist/src/pipeline/run-pipeline.js.map +1 -0
  27. package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
  28. package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/run-pipeline.test.js +247 -0
  30. package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
  32. package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/generate-component.js +50 -0
  34. package/dist/src/pipeline/steps/generate-component.js.map +1 -0
  35. package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
  36. package/dist/src/pipeline/steps/generate-component.test.js +106 -0
  37. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
  38. package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
  39. package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
  40. package/dist/src/pipeline/steps/generate-story.js +14 -0
  41. package/dist/src/pipeline/steps/generate-story.js.map +1 -0
  42. package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
  43. package/dist/src/pipeline/steps/generate-story.test.js +41 -0
  44. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
  45. package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
  46. package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
  47. package/dist/src/pipeline/steps/generate-test.js +19 -0
  48. package/dist/src/pipeline/steps/generate-test.js.map +1 -0
  49. package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
  50. package/dist/src/pipeline/steps/generate-test.test.js +60 -0
  51. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
  52. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  53. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  54. package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
  55. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  56. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  57. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  58. package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
  59. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  60. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  61. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  62. package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
  63. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  64. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  65. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  66. package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
  67. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  68. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  69. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  70. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  71. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  72. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  73. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  74. package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
  75. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  76. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  77. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  78. package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
  79. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  80. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  81. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  82. package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
  83. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  84. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  85. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  86. package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
  87. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  88. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  89. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  90. package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
  91. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  92. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  93. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  94. package/dist/src/pipeline/steps/visual-test.js +4 -0
  95. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  96. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  97. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  98. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  99. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  100. package/dist/src/project-context.d.ts +10 -0
  101. package/dist/src/project-context.d.ts.map +1 -0
  102. package/dist/src/project-context.js +178 -0
  103. package/dist/src/project-context.js.map +1 -0
  104. package/dist/src/prompt.d.ts +39 -7
  105. package/dist/src/prompt.d.ts.map +1 -1
  106. package/dist/src/prompt.js +233 -23
  107. package/dist/src/prompt.js.map +1 -1
  108. package/dist/src/prompt.test.js +154 -9
  109. package/dist/src/prompt.test.js.map +1 -1
  110. package/dist/src/scaffold.d.ts +49 -0
  111. package/dist/src/scaffold.d.ts.map +1 -0
  112. package/dist/src/scaffold.js +208 -0
  113. package/dist/src/scaffold.js.map +1 -0
  114. package/dist/src/tools/lint-runner.d.ts +7 -0
  115. package/dist/src/tools/lint-runner.d.ts.map +1 -0
  116. package/dist/src/tools/lint-runner.js +48 -0
  117. package/dist/src/tools/lint-runner.js.map +1 -0
  118. package/dist/src/tools/lint-runner.test.d.ts +2 -0
  119. package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
  120. package/dist/src/tools/lint-runner.test.js +90 -0
  121. package/dist/src/tools/lint-runner.test.js.map +1 -0
  122. package/dist/src/tools/storybook-runner.d.ts +6 -0
  123. package/dist/src/tools/storybook-runner.d.ts.map +1 -0
  124. package/dist/src/tools/storybook-runner.js +25 -0
  125. package/dist/src/tools/storybook-runner.js.map +1 -0
  126. package/dist/src/tools/storybook-runner.test.d.ts +2 -0
  127. package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
  128. package/dist/src/tools/storybook-runner.test.js +43 -0
  129. package/dist/src/tools/storybook-runner.test.js.map +1 -0
  130. package/dist/src/tools/test-runner.d.ts +9 -0
  131. package/dist/src/tools/test-runner.d.ts.map +1 -0
  132. package/dist/src/tools/test-runner.js +74 -0
  133. package/dist/src/tools/test-runner.js.map +1 -0
  134. package/dist/src/tools/test-runner.test.d.ts +2 -0
  135. package/dist/src/tools/test-runner.test.d.ts.map +1 -0
  136. package/dist/src/tools/test-runner.test.js +177 -0
  137. package/dist/src/tools/test-runner.test.js.map +1 -0
  138. package/dist/src/tools/type-checker.d.ts +6 -0
  139. package/dist/src/tools/type-checker.d.ts.map +1 -0
  140. package/dist/src/tools/type-checker.js +36 -0
  141. package/dist/src/tools/type-checker.js.map +1 -0
  142. package/dist/src/tools/type-checker.test.d.ts +2 -0
  143. package/dist/src/tools/type-checker.test.d.ts.map +1 -0
  144. package/dist/src/tools/type-checker.test.js +96 -0
  145. package/dist/src/tools/type-checker.test.js.map +1 -0
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/inputs/model-a/spec-deltas.json +1460 -0
  148. package/inputs/model-b/spec-deltas.json +1424 -0
  149. package/inputs/model-c/spec-deltas.json +1432 -0
  150. package/inputs/model-d/spec-deltas.json +967 -0
  151. package/inputs/model-e/spec-deltas.json +2292 -0
  152. package/ketchup-plan.md +43 -8
  153. package/package.json +3 -3
  154. package/scoring-heuristic.md +138 -0
  155. package/scripts/improve.ts +23 -18
  156. package/src/commands/implement-component.test.ts +309 -76
  157. package/src/commands/implement-component.ts +155 -31
  158. package/src/extract-exports.ts +53 -0
  159. package/src/generate-story-deterministic.ts +267 -0
  160. package/src/index.ts +12 -0
  161. package/src/pipeline/run-pipeline.test.ts +292 -0
  162. package/src/pipeline/run-pipeline.ts +160 -0
  163. package/src/pipeline/steps/generate-component.test.ts +130 -0
  164. package/src/pipeline/steps/generate-component.ts +60 -0
  165. package/src/pipeline/steps/generate-story.test.ts +54 -0
  166. package/src/pipeline/steps/generate-story.ts +17 -0
  167. package/src/pipeline/steps/generate-test.test.ts +75 -0
  168. package/src/pipeline/steps/generate-test.ts +25 -0
  169. package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
  170. package/src/pipeline/steps/lint-fix-loop.ts +59 -0
  171. package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
  172. package/src/pipeline/steps/story-fix-loop.ts +47 -0
  173. package/src/pipeline/steps/storybook-test.test.ts +82 -0
  174. package/src/pipeline/steps/storybook-test.ts +27 -0
  175. package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
  176. package/src/pipeline/steps/test-fix-loop.ts +56 -0
  177. package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
  178. package/src/pipeline/steps/type-fix-loop.ts +55 -0
  179. package/src/pipeline/steps/visual-test.test.ts +10 -0
  180. package/src/pipeline/steps/visual-test.ts +5 -0
  181. package/src/project-context.ts +205 -0
  182. package/src/prompt.test.ts +174 -8
  183. package/src/prompt.ts +301 -23
  184. package/src/scaffold.ts +281 -0
  185. package/src/tools/lint-runner.test.ts +112 -0
  186. package/src/tools/lint-runner.ts +52 -0
  187. package/src/tools/storybook-runner.test.ts +53 -0
  188. package/src/tools/storybook-runner.ts +29 -0
  189. package/src/tools/test-runner.test.ts +213 -0
  190. package/src/tools/test-runner.ts +84 -0
  191. package/src/tools/type-checker.test.ts +120 -0
  192. package/src/tools/type-checker.ts +42 -0
  193. package/vitest.config.ts +9 -1
  194. package/dist/src/generate-component.d.ts +0 -4
  195. package/dist/src/generate-component.d.ts.map +0 -1
  196. package/dist/src/generate-component.js +0 -14
  197. package/dist/src/generate-component.js.map +0 -1
  198. package/dist/src/generate-component.test.d.ts.map +0 -1
  199. package/dist/src/generate-component.test.js +0 -73
  200. package/dist/src/generate-component.test.js.map +0 -1
  201. package/dist/src/generate-story.d.ts +0 -4
  202. package/dist/src/generate-story.d.ts.map +0 -1
  203. package/dist/src/generate-story.js +0 -14
  204. package/dist/src/generate-story.js.map +0 -1
  205. package/dist/src/generate-story.test.d.ts.map +0 -1
  206. package/dist/src/generate-story.test.js +0 -58
  207. package/dist/src/generate-story.test.js.map +0 -1
  208. package/dist/src/generate-test.d.ts +0 -4
  209. package/dist/src/generate-test.d.ts.map +0 -1
  210. package/dist/src/generate-test.js +0 -14
  211. package/dist/src/generate-test.js.map +0 -1
  212. package/dist/src/generate-test.test.d.ts.map +0 -1
  213. package/dist/src/generate-test.test.js +0 -77
  214. package/dist/src/generate-test.test.js.map +0 -1
  215. package/dist/src/reconcile.d.ts +0 -8
  216. package/dist/src/reconcile.d.ts.map +0 -1
  217. package/dist/src/reconcile.js +0 -18
  218. package/dist/src/reconcile.js.map +0 -1
  219. package/dist/src/reconcile.test.d.ts +0 -2
  220. package/dist/src/reconcile.test.d.ts.map +0 -1
  221. package/dist/src/reconcile.test.js +0 -108
  222. package/dist/src/reconcile.test.js.map +0 -1
  223. package/src/generate-component.test.ts +0 -89
  224. package/src/generate-component.ts +0 -16
  225. package/src/generate-story.test.ts +0 -71
  226. package/src/generate-story.ts +0 -16
  227. package/src/generate-test.test.ts +0 -93
  228. package/src/generate-test.ts +0 -16
  229. package/src/reconcile.test.ts +0 -127
  230. package/src/reconcile.ts +0 -27
  231. /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
  232. /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
  233. /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
@@ -1,12 +1,17 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { type Command, defineCommandHandler, type Event } from '@auto-engineer/message-bus';
5
+ import { createModelFromEnv } from '@auto-engineer/model-factory';
5
6
  import createDebug from 'debug';
6
- import { generateComponentFile } from '../generate-component';
7
- import { generateStoryFile } from '../generate-story';
8
- import { generateTestFile } from '../generate-test';
9
- import { reconcile } from '../reconcile';
7
+ import {
8
+ buildPipelineSteps,
9
+ type PipelineConfig,
10
+ type PipelineContext,
11
+ type PipelineModels,
12
+ runPipeline,
13
+ } from '../pipeline/run-pipeline';
14
+ import { buildFullProjectSection } from '../project-context';
10
15
 
11
16
  const debug = createDebug('auto:component-implementor-react:command');
12
17
 
@@ -18,6 +23,22 @@ type ComponentJobPayload = {
18
23
  styling: string[];
19
24
  storybookPath: string;
20
25
  files: { create?: string[]; modify?: string[] };
26
+ composes: { id: string; path: string }[];
27
+ props?: Array<{
28
+ name: string;
29
+ type: string;
30
+ required: boolean;
31
+ default?: string;
32
+ description: string;
33
+ category: 'data' | 'callback' | 'slot' | 'visual' | 'state' | 'config';
34
+ }>;
35
+ storyVariants?: Array<{
36
+ name: string;
37
+ description: string;
38
+ args: Record<string, unknown>;
39
+ needsPlayFunction?: boolean;
40
+ playDescription?: string;
41
+ }>;
21
42
  };
22
43
 
23
44
  type ComponentJob = {
@@ -63,6 +84,18 @@ function pascalCase(id: string): string {
63
84
  .join('');
64
85
  }
65
86
 
87
+ function filePathToImportAlias(filePath: string): string {
88
+ let rel = filePath;
89
+ if (rel.startsWith('./')) {
90
+ rel = rel.slice(2);
91
+ }
92
+ if (rel.startsWith('src/')) {
93
+ rel = rel.slice(4);
94
+ }
95
+ rel = rel.replace(/\.tsx?$/, '');
96
+ return `@/${rel}`;
97
+ }
98
+
66
99
  function deriveFilePaths(
67
100
  targetDir: string,
68
101
  payload: ComponentJobPayload,
@@ -81,6 +114,65 @@ function deriveFilePaths(
81
114
  return { componentPath, testPath, storyPath, componentName };
82
115
  }
83
116
 
117
+ function buildPipelineConfig(): PipelineConfig {
118
+ return {
119
+ models: {
120
+ generateTest:
121
+ process.env.STEP_GENERATE_TEST_MODEL ??
122
+ process.env.IMPL_MODEL ??
123
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
124
+ '',
125
+ generateComponent:
126
+ process.env.STEP_GENERATE_COMPONENT_MODEL ??
127
+ process.env.IMPL_MODEL ??
128
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
129
+ '',
130
+ typeFixer:
131
+ process.env.STEP_TYPE_FIXER_MODEL ??
132
+ process.env.IMPL_FIXER_MODEL ??
133
+ process.env.IMPL_MODEL ??
134
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
135
+ '',
136
+ testFixer:
137
+ process.env.STEP_TEST_FIXER_MODEL ??
138
+ process.env.IMPL_FIXER_MODEL ??
139
+ process.env.IMPL_MODEL ??
140
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
141
+ '',
142
+ lintFixer:
143
+ process.env.STEP_LINT_FIXER_MODEL ??
144
+ process.env.IMPL_FIXER_MODEL ??
145
+ process.env.IMPL_MODEL ??
146
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
147
+ '',
148
+ storyFixer:
149
+ process.env.STEP_STORY_FIXER_MODEL ??
150
+ process.env.IMPL_FIXER_MODEL ??
151
+ process.env.IMPL_MODEL ??
152
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
153
+ '',
154
+ },
155
+ maxTypeFixIterations: 3,
156
+ maxTestFixIterations: 3,
157
+ maxLintFixIterations: 2,
158
+ maxStoryFixIterations: 2,
159
+ enableStorybookTest: process.env.ENABLE_STORYBOOK_TEST === 'true',
160
+ enableVisualTest: process.env.ENABLE_VISUAL_TEST === 'true',
161
+ };
162
+ }
163
+
164
+ function createPipelineModels(): PipelineModels {
165
+ const model = createModelFromEnv();
166
+ return {
167
+ generateTest: model,
168
+ generateComponent: model,
169
+ typeFixer: model,
170
+ testFixer: model,
171
+ lintFixer: model,
172
+ storyFixer: model,
173
+ };
174
+ }
175
+
84
176
  export async function handleImplementComponent(
85
177
  command: ImplementComponentCommand,
86
178
  ): Promise<ComponentImplementedEvent | ComponentImplementationFailedEvent> {
@@ -88,16 +180,12 @@ export async function handleImplementComponent(
88
180
  const { payload } = job;
89
181
  const { componentPath, testPath, storyPath, componentName } = deriveFilePaths(targetDir, payload);
90
182
  const isModify = (payload.files.modify?.length ?? 0) > 0;
183
+ const rawPath = payload.files.modify?.[0] ?? payload.files.create?.[0] ?? '';
184
+ const componentImportPath = filePathToImportAlias(rawPath);
91
185
 
92
- debug('Implementing component: %s', componentName);
186
+ debug('Implementing component: %s (mode: %s)', componentName, isModify ? 'modify' : 'create');
93
187
 
94
188
  try {
95
- let existingComponent: string | undefined;
96
- if (isModify && existsSync(componentPath)) {
97
- existingComponent = await readFile(componentPath, 'utf-8');
98
- debug('Read existing component: %d chars', existingComponent.length);
99
- }
100
-
101
189
  const specDeltas = {
102
190
  structure: payload.structure,
103
191
  rendering: payload.rendering,
@@ -105,31 +193,67 @@ export async function handleImplementComponent(
105
193
  styling: payload.styling,
106
194
  };
107
195
 
108
- debug('Running Frontend, Tester, and Story agents in parallel...');
109
- const [componentCode, testCode, storyCode] = await Promise.all([
110
- generateComponentFile({ componentName, specDeltas, existingComponent }),
111
- generateTestFile({ componentName, specDeltas, existingComponent }),
112
- generateStoryFile({ componentName, specDeltas }),
113
- ]);
196
+ debug('Building project context...');
197
+ const projectSection = await buildFullProjectSection(targetDir, payload.composes);
198
+
199
+ let existingComponent: string | undefined;
200
+ let existingTest: string | undefined;
201
+
202
+ if (isModify && existsSync(componentPath)) {
203
+ existingComponent = await readFile(componentPath, 'utf-8');
204
+ existingTest = existsSync(testPath) ? await readFile(testPath, 'utf-8') : undefined;
205
+ }
114
206
 
115
- debug('Reconciling component and story against tests...');
116
- const reconciled = await reconcile({
207
+ await mkdir(path.dirname(testPath), { recursive: true });
208
+
209
+ const ctx: PipelineContext = {
117
210
  componentName,
211
+ componentPath,
212
+ testPath,
213
+ storyPath,
214
+ componentImportPath,
215
+ targetDir,
118
216
  specDeltas,
119
- componentCode,
120
- testCode,
121
- storyCode,
217
+ projectSection,
218
+ props: payload.props,
219
+ composes: payload.composes,
122
220
  existingComponent,
123
- });
221
+ existingTest,
222
+ isModify,
223
+ storyVariants: payload.storyVariants,
224
+ llmCalls: 0,
225
+ fixIterations: 0,
226
+ };
124
227
 
125
- await mkdir(path.dirname(testPath), { recursive: true });
126
- await Promise.all([
127
- writeFile(testPath, testCode, 'utf-8'),
128
- writeFile(componentPath, reconciled.componentCode, 'utf-8'),
129
- writeFile(storyPath, reconciled.storyCode, 'utf-8'),
130
- ]);
228
+ const config = buildPipelineConfig();
229
+ const models = createPipelineModels();
230
+
231
+ debug('Running TDD pipeline...');
232
+ const steps = buildPipelineSteps(models, config, ctx);
233
+ const result = await runPipeline(steps, ctx);
131
234
 
132
- debug('Wrote 3 files for %s', componentName);
235
+ if (!result.success) {
236
+ debug(
237
+ 'Pipeline failed: %s (LLM calls: %d, fix iterations: %d)',
238
+ result.error,
239
+ result.llmCalls,
240
+ result.fixIterations,
241
+ );
242
+ return {
243
+ type: 'ComponentImplementationFailed',
244
+ data: { error: result.error ?? 'Pipeline failed', name: componentName },
245
+ timestamp: new Date(),
246
+ requestId: command.requestId,
247
+ correlationId: command.correlationId,
248
+ };
249
+ }
250
+
251
+ debug(
252
+ 'Pipeline succeeded for %s (LLM calls: %d, fix iterations: %d)',
253
+ componentName,
254
+ result.llmCalls,
255
+ result.fixIterations,
256
+ );
133
257
 
134
258
  return {
135
259
  type: 'ComponentImplemented',
@@ -0,0 +1,53 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // Extract exported names from a TypeScript/TSX file using the TS AST
3
+ // ═══════════════════════════════════════════════════════════════════
4
+
5
+ import ts from 'typescript';
6
+
7
+ /**
8
+ * Parse a TypeScript/TSX source string and return all exported value names
9
+ * (functions, constants, classes) — excludes type-only exports.
10
+ */
11
+ export function extractExportedNames(content: string): string[] {
12
+ const sourceFile = ts.createSourceFile('temp.tsx', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
13
+
14
+ const names: string[] = [];
15
+
16
+ ts.forEachChild(sourceFile, (node) => {
17
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
18
+ const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
19
+
20
+ // export function Foo() { ... }
21
+ if (ts.isFunctionDeclaration(node) && isExported && node.name) {
22
+ names.push(node.name.text);
23
+ }
24
+
25
+ // export const Foo = ...
26
+ if (ts.isVariableStatement(node) && isExported) {
27
+ for (const decl of node.declarationList.declarations) {
28
+ if (ts.isIdentifier(decl.name)) {
29
+ names.push(decl.name.text);
30
+ }
31
+ }
32
+ }
33
+
34
+ // export class Foo { ... }
35
+ if (ts.isClassDeclaration(node) && isExported && node.name) {
36
+ names.push(node.name.text);
37
+ }
38
+
39
+ // export { Foo, Bar, Baz }
40
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
41
+ // Skip type-only re-exports: export type { Foo }
42
+ if (node.isTypeOnly) return;
43
+
44
+ for (const spec of node.exportClause.elements) {
45
+ // Skip individual type-only exports: export { type Foo, Bar }
46
+ if (spec.isTypeOnly) continue;
47
+ names.push(spec.name.text);
48
+ }
49
+ }
50
+ });
51
+
52
+ return names;
53
+ }
@@ -0,0 +1,267 @@
1
+ // ═══════════════════════════════════════════════════════════════════
2
+ // Deterministic story generator — no LLM call, pure template logic
3
+ // ═══════════════════════════════════════════════════════════════════
4
+
5
+ export interface DeterministicStoryProp {
6
+ name: string;
7
+ type: string;
8
+ required: boolean;
9
+ default?: string;
10
+ description: string;
11
+ category: 'data' | 'callback' | 'slot' | 'visual' | 'state' | 'config';
12
+ }
13
+
14
+ export interface StoryVariant {
15
+ name: string;
16
+ description: string;
17
+ args: Record<string, unknown>;
18
+ needsPlayFunction?: boolean;
19
+ playDescription?: string;
20
+ }
21
+
22
+ export interface DeterministicStoryInput {
23
+ componentName: string;
24
+ componentImportPath: string;
25
+ props?: DeterministicStoryProp[];
26
+ storyVariants?: StoryVariant[];
27
+ }
28
+
29
+ /**
30
+ * Generates a Storybook CSF3 story file deterministically (no LLM call).
31
+ *
32
+ * Uses structured prop data and optional story variants to emit a complete
33
+ * `.stories.tsx` file string. When no variants are provided, a Default
34
+ * story is synthesized from the props with sensible placeholder values.
35
+ */
36
+ export function generateStoryDeterministic(input: DeterministicStoryInput): string {
37
+ const { componentName, componentImportPath, props, storyVariants } = input;
38
+
39
+ const lines: string[] = [];
40
+
41
+ // ── Imports ──────────────────────────────────────────────────────
42
+ lines.push(`import type { Meta, StoryObj } from '@storybook/react-vite';`);
43
+ lines.push(`import { ${componentName} } from '${componentImportPath}';`);
44
+ lines.push('');
45
+
46
+ // ── Meta ─────────────────────────────────────────────────────────
47
+ lines.push(`const meta: Meta<typeof ${componentName}> = {`);
48
+ lines.push(` title: 'UI Components/${componentName}',`);
49
+ lines.push(` component: ${componentName},`);
50
+ lines.push(`};`);
51
+ lines.push(`export default meta;`);
52
+ lines.push('');
53
+
54
+ // ── Story type alias ─────────────────────────────────────────────
55
+ lines.push(`type Story = StoryObj<typeof ${componentName}>;`);
56
+
57
+ // ── Stories ──────────────────────────────────────────────────────
58
+ if (storyVariants && storyVariants.length > 0) {
59
+ for (const variant of storyVariants) {
60
+ lines.push('');
61
+ lines.push(`export const ${variant.name}: Story = {`);
62
+ const argsBlock = serializeArgs(variant.args);
63
+ if (argsBlock) {
64
+ lines.push(` args: {`);
65
+ lines.push(argsBlock);
66
+ lines.push(` },`);
67
+ }
68
+ lines.push(`};`);
69
+ }
70
+ } else {
71
+ // Derive a Default story from props
72
+ const defaultArgs = deriveDefaultArgs(props ?? []);
73
+ lines.push('');
74
+ lines.push(`export const Default: Story = {`);
75
+ const argsBlock = serializeArgs(defaultArgs);
76
+ if (argsBlock) {
77
+ lines.push(` args: {`);
78
+ lines.push(argsBlock);
79
+ lines.push(` },`);
80
+ }
81
+ lines.push(`};`);
82
+ }
83
+
84
+ lines.push('');
85
+ return lines.join('\n');
86
+ }
87
+
88
+ // ═══════════════════════════════════════════════════════════════════
89
+ // Internals
90
+ // ═══════════════════════════════════════════════════════════════════
91
+
92
+ /**
93
+ * Derives sensible default arg values from structured prop definitions.
94
+ *
95
+ * - `callback` props are omitted (Storybook auto-detects them via argTypes).
96
+ * - `slot` props are omitted (ReactNode cannot be serialized to args).
97
+ * - `data` props get placeholder values based on their TypeScript type.
98
+ * - `visual` props use the declared default if present, otherwise a placeholder.
99
+ * - `state` props default to `false`.
100
+ * - `config` props use the declared default if present, otherwise a placeholder.
101
+ */
102
+ function deriveDefaultArgs(props: DeterministicStoryProp[]): Record<string, unknown> {
103
+ const args: Record<string, unknown> = {};
104
+
105
+ for (const prop of props) {
106
+ // Skip categories that cannot or should not be serialized in args
107
+ if (prop.category === 'callback' || prop.category === 'slot') {
108
+ continue;
109
+ }
110
+
111
+ if (prop.category === 'state') {
112
+ args[prop.name] = false;
113
+ continue;
114
+ }
115
+
116
+ // If a default is declared, try to use it
117
+ if (prop.default !== undefined) {
118
+ const parsed = parseDefaultValue(prop.default);
119
+ if (parsed !== undefined) {
120
+ args[prop.name] = parsed;
121
+ continue;
122
+ }
123
+ }
124
+
125
+ // Otherwise generate a placeholder based on the TypeScript type string
126
+ args[prop.name] = placeholderForType(prop.type);
127
+ }
128
+
129
+ return args;
130
+ }
131
+
132
+ /**
133
+ * Attempts to parse a default value string into a JS primitive.
134
+ * Returns `undefined` when the string cannot be meaningfully parsed
135
+ * (e.g. an arrow function or complex expression).
136
+ */
137
+ function parseDefaultValue(raw: string): unknown {
138
+ const trimmed = raw.trim();
139
+ if (trimmed === 'true') return true;
140
+ if (trimmed === 'false') return false;
141
+ if (trimmed === 'null') return null;
142
+ if (trimmed === 'undefined') return undefined;
143
+
144
+ // Quoted string
145
+ const stringMatch = trimmed.match(/^['"](.*)['"]$/);
146
+ if (stringMatch) return stringMatch[1];
147
+
148
+ // Numeric
149
+ const num = Number(trimmed);
150
+ if (!Number.isNaN(num) && trimmed !== '') return num;
151
+
152
+ // Array or object literal — attempt JSON parse
153
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
154
+ try {
155
+ return JSON.parse(trimmed);
156
+ } catch {
157
+ return undefined;
158
+ }
159
+ }
160
+
161
+ // Anything else (functions, expressions) → skip
162
+ return undefined;
163
+ }
164
+
165
+ /**
166
+ * Returns a sensible placeholder value for a given TypeScript type string.
167
+ */
168
+ function placeholderForType(typeStr: string): unknown {
169
+ const t = typeStr.trim();
170
+
171
+ if (t === 'string') return 'Example';
172
+ if (t === 'number') return 42;
173
+ if (t === 'boolean') return true;
174
+
175
+ // Union of string literals — pick the first literal
176
+ const literalUnion = t.match(/^['"]([^'"]+)['"]/);
177
+ if (literalUnion) return literalUnion[1];
178
+
179
+ // Array types
180
+ if (t.endsWith('[]') || t.startsWith('Array<')) return [];
181
+
182
+ // Generic object / Record
183
+ if (t === 'object' || t.startsWith('Record<') || t.startsWith('{')) return {};
184
+
185
+ // Fallback
186
+ return undefined;
187
+ }
188
+
189
+ /**
190
+ * Serializes an args record into indented TypeScript object body lines.
191
+ * Returns an empty string when the args record is empty (caller should
192
+ * omit the `args` block entirely).
193
+ *
194
+ * Each value is formatted as a valid TypeScript literal:
195
+ * - strings → quoted
196
+ * - numbers / booleans → as-is
197
+ * - arrays / plain objects → JSON.stringify
198
+ * - undefined, null, functions → skipped
199
+ */
200
+ function serializeArgs(args: Record<string, unknown>): string {
201
+ const entries: string[] = [];
202
+
203
+ for (const [key, value] of Object.entries(args)) {
204
+ const formatted = formatArgValue(value);
205
+ if (formatted === null) continue;
206
+ entries.push(` ${key}: ${formatted},`);
207
+ }
208
+
209
+ return entries.join('\n');
210
+ }
211
+
212
+ function formatArgValue(value: unknown): string | null {
213
+ if (value === undefined) return null;
214
+ if (value === null) return null;
215
+ if (typeof value === 'function') return null;
216
+
217
+ if (typeof value === 'string') {
218
+ // Callback strings like "() => {}" — emit as raw JS, not quoted
219
+ if (/^\s*\(/.test(value) || /^\s*function\s*\(/.test(value)) {
220
+ return value;
221
+ }
222
+ return JSON.stringify(value);
223
+ }
224
+ if (typeof value === 'number') return String(value);
225
+ if (typeof value === 'boolean') return String(value);
226
+
227
+ // Arrays and plain objects — deep-fix stringified JSON values first
228
+ if (Array.isArray(value) || typeof value === 'object') {
229
+ const fixed = deepParseStringifiedJson(value);
230
+ return JSON.stringify(fixed);
231
+ }
232
+
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Recursively walks a value and parses any string that looks like
238
+ * stringified JSON back into an actual object/array. This fixes
239
+ * FEA output where nested objects are accidentally double-stringified.
240
+ */
241
+ function deepParseStringifiedJson(value: unknown): unknown {
242
+ if (typeof value === 'string') {
243
+ const trimmed = value.trim();
244
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
245
+ try {
246
+ return deepParseStringifiedJson(JSON.parse(trimmed));
247
+ } catch {
248
+ return value;
249
+ }
250
+ }
251
+ return value;
252
+ }
253
+
254
+ if (Array.isArray(value)) {
255
+ return value.map(deepParseStringifiedJson);
256
+ }
257
+
258
+ if (value !== null && typeof value === 'object') {
259
+ const result: Record<string, unknown> = {};
260
+ for (const [k, v] of Object.entries(value)) {
261
+ result[k] = deepParseStringifiedJson(v);
262
+ }
263
+ return result;
264
+ }
265
+
266
+ return value;
267
+ }
package/src/index.ts CHANGED
@@ -9,3 +9,15 @@ export type {
9
9
  ImplementComponentEvents,
10
10
  } from './commands/implement-component';
11
11
  export { handleImplementComponent } from './commands/implement-component';
12
+ export { generateStoryDeterministic } from './generate-story-deterministic';
13
+ export type {
14
+ ModelConfig,
15
+ PipelineConfig,
16
+ PipelineContext,
17
+ PipelineModels,
18
+ PipelineResult,
19
+ PipelineStep,
20
+ StepResult,
21
+ } from './pipeline/run-pipeline';
22
+ export { buildPipelineSteps, runPipeline } from './pipeline/run-pipeline';
23
+ export { generateScaffold } from './scaffold';