@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
@@ -0,0 +1,292 @@
1
+ import type { LanguageModel } from 'ai';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ vi.mock('./steps/generate-test', () => ({
5
+ generateTestStep: vi.fn(async () => ({ success: true })),
6
+ }));
7
+ vi.mock('./steps/generate-component', () => ({
8
+ generateComponentStep: vi.fn(async () => ({ success: true })),
9
+ }));
10
+ vi.mock('./steps/type-fix-loop', () => ({
11
+ typeFixLoop: vi.fn(async () => ({ success: true })),
12
+ }));
13
+ vi.mock('./steps/test-fix-loop', () => ({
14
+ testFixLoop: vi.fn(async () => ({ success: true })),
15
+ }));
16
+ vi.mock('./steps/lint-fix-loop', () => ({
17
+ lintFixLoop: vi.fn(async () => ({ success: true })),
18
+ }));
19
+ vi.mock('./steps/generate-story', () => ({
20
+ generateStoryStep: vi.fn(async () => ({ success: true })),
21
+ }));
22
+ vi.mock('./steps/story-fix-loop', () => ({
23
+ storyFixLoop: vi.fn(async () => ({ success: true })),
24
+ }));
25
+ vi.mock('./steps/storybook-test', () => ({
26
+ storybookTestStep: vi.fn(async () => ({ success: true })),
27
+ }));
28
+ vi.mock('./steps/visual-test', () => ({
29
+ visualTestStep: vi.fn(async () => ({ success: true })),
30
+ }));
31
+
32
+ import {
33
+ buildPipelineSteps,
34
+ type PipelineConfig,
35
+ type PipelineContext,
36
+ type PipelineModels,
37
+ type PipelineStep,
38
+ runPipeline,
39
+ } from './run-pipeline';
40
+ import { generateComponentStep } from './steps/generate-component';
41
+ import { generateStoryStep } from './steps/generate-story';
42
+ import { generateTestStep } from './steps/generate-test';
43
+ import { lintFixLoop } from './steps/lint-fix-loop';
44
+ import { storyFixLoop } from './steps/story-fix-loop';
45
+ import { storybookTestStep } from './steps/storybook-test';
46
+ import { testFixLoop } from './steps/test-fix-loop';
47
+ import { typeFixLoop } from './steps/type-fix-loop';
48
+ import { visualTestStep } from './steps/visual-test';
49
+
50
+ afterEach(() => {
51
+ vi.clearAllMocks();
52
+ });
53
+
54
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
55
+ return {
56
+ componentName: 'MyButton',
57
+ componentPath: '/project/src/MyButton.tsx',
58
+ testPath: '/project/src/MyButton.test.tsx',
59
+ storyPath: '/project/src/MyButton.stories.tsx',
60
+ componentImportPath: '@/components/ui/MyButton',
61
+ targetDir: '/project',
62
+ specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
63
+ projectSection: '',
64
+ composes: [],
65
+ isModify: false,
66
+ llmCalls: 0,
67
+ fixIterations: 0,
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ function successStep(name: string): PipelineStep {
73
+ return { name, run: async () => ({ success: true }) };
74
+ }
75
+
76
+ function failStep(name: string, error: string): PipelineStep {
77
+ return { name, run: async () => ({ success: false, error }) };
78
+ }
79
+
80
+ describe('runPipeline', () => {
81
+ it('runs all steps in sequence and returns success', async () => {
82
+ const executionOrder: string[] = [];
83
+ const steps: PipelineStep[] = [
84
+ {
85
+ name: 'Step A',
86
+ run: async () => {
87
+ executionOrder.push('A');
88
+ return { success: true };
89
+ },
90
+ },
91
+ {
92
+ name: 'Step B',
93
+ run: async () => {
94
+ executionOrder.push('B');
95
+ return { success: true };
96
+ },
97
+ },
98
+ {
99
+ name: 'Step C',
100
+ run: async () => {
101
+ executionOrder.push('C');
102
+ return { success: true };
103
+ },
104
+ },
105
+ ];
106
+
107
+ const result = await runPipeline(steps, makeCtx());
108
+
109
+ expect(result).toEqual({ success: true, llmCalls: 0, fixIterations: 0 });
110
+ expect(executionOrder).toEqual(['A', 'B', 'C']);
111
+ });
112
+
113
+ it('stops at first failing step and returns failure', async () => {
114
+ const executionOrder: string[] = [];
115
+ const steps: PipelineStep[] = [
116
+ {
117
+ name: 'Step A',
118
+ run: async () => {
119
+ executionOrder.push('A');
120
+ return { success: true };
121
+ },
122
+ },
123
+ failStep('Step B', 'something broke'),
124
+ {
125
+ name: 'Step C',
126
+ run: async () => {
127
+ executionOrder.push('C');
128
+ return { success: true };
129
+ },
130
+ },
131
+ ];
132
+
133
+ const result = await runPipeline(steps, makeCtx());
134
+
135
+ expect(result).toEqual({
136
+ success: false,
137
+ error: 'Step "Step B" failed: something broke',
138
+ llmCalls: 0,
139
+ fixIterations: 0,
140
+ });
141
+ expect(executionOrder).toEqual(['A']);
142
+ });
143
+
144
+ it('returns success with empty steps', async () => {
145
+ const result = await runPipeline([], makeCtx());
146
+
147
+ expect(result).toEqual({ success: true, llmCalls: 0, fixIterations: 0 });
148
+ });
149
+
150
+ it('reports accumulated llmCalls and fixIterations from context', async () => {
151
+ const steps: PipelineStep[] = [{ name: 'Step A', run: async () => ({ success: true }) }];
152
+ const ctx = makeCtx({ llmCalls: 5, fixIterations: 3 });
153
+
154
+ const result = await runPipeline(steps, ctx);
155
+
156
+ expect(result).toEqual({ success: true, llmCalls: 5, fixIterations: 3 });
157
+ });
158
+
159
+ it('reports context metrics on failure', async () => {
160
+ const steps: PipelineStep[] = [failStep('Broken', 'oops')];
161
+ const ctx = makeCtx({ llmCalls: 2, fixIterations: 1 });
162
+
163
+ const result = await runPipeline(steps, ctx);
164
+
165
+ expect(result).toEqual({
166
+ success: false,
167
+ error: 'Step "Broken" failed: oops',
168
+ llmCalls: 2,
169
+ fixIterations: 1,
170
+ });
171
+ });
172
+ });
173
+
174
+ const fakeModel = {} as LanguageModel;
175
+
176
+ function makeModels(): PipelineModels {
177
+ return {
178
+ generateTest: fakeModel,
179
+ generateComponent: fakeModel,
180
+ typeFixer: fakeModel,
181
+ testFixer: fakeModel,
182
+ lintFixer: fakeModel,
183
+ storyFixer: fakeModel,
184
+ };
185
+ }
186
+
187
+ function makeConfig(overrides: Partial<PipelineConfig> = {}): PipelineConfig {
188
+ return {
189
+ models: {
190
+ generateTest: '',
191
+ generateComponent: '',
192
+ typeFixer: '',
193
+ testFixer: '',
194
+ lintFixer: '',
195
+ storyFixer: '',
196
+ },
197
+ maxTypeFixIterations: 3,
198
+ maxTestFixIterations: 3,
199
+ maxLintFixIterations: 2,
200
+ maxStoryFixIterations: 2,
201
+ enableStorybookTest: false,
202
+ enableVisualTest: false,
203
+ ...overrides,
204
+ };
205
+ }
206
+
207
+ describe('buildPipelineSteps', () => {
208
+ it('returns 7 core steps by default', () => {
209
+ const steps = buildPipelineSteps(makeModels(), makeConfig(), makeCtx());
210
+
211
+ expect(steps.map((s) => s.name)).toEqual([
212
+ 'Generate Test',
213
+ 'Generate Component',
214
+ 'Type Fix Loop',
215
+ 'Test Fix Loop',
216
+ 'Lint Fix Loop',
217
+ 'Generate Story',
218
+ 'Story Fix Loop',
219
+ ]);
220
+ });
221
+
222
+ it('adds Storybook Test step when enabled', () => {
223
+ const steps = buildPipelineSteps(makeModels(), makeConfig({ enableStorybookTest: true }), makeCtx());
224
+
225
+ expect(steps.map((s) => s.name)).toEqual([
226
+ 'Generate Test',
227
+ 'Generate Component',
228
+ 'Type Fix Loop',
229
+ 'Test Fix Loop',
230
+ 'Lint Fix Loop',
231
+ 'Generate Story',
232
+ 'Story Fix Loop',
233
+ 'Storybook Test',
234
+ ]);
235
+ });
236
+
237
+ it('adds Visual Test step when enabled', () => {
238
+ const steps = buildPipelineSteps(makeModels(), makeConfig({ enableVisualTest: true }), makeCtx());
239
+
240
+ expect(steps.map((s) => s.name)).toEqual([
241
+ 'Generate Test',
242
+ 'Generate Component',
243
+ 'Type Fix Loop',
244
+ 'Test Fix Loop',
245
+ 'Lint Fix Loop',
246
+ 'Generate Story',
247
+ 'Story Fix Loop',
248
+ 'Visual Test',
249
+ ]);
250
+ });
251
+
252
+ it('adds both optional steps when both enabled', () => {
253
+ const steps = buildPipelineSteps(
254
+ makeModels(),
255
+ makeConfig({ enableStorybookTest: true, enableVisualTest: true }),
256
+ makeCtx(),
257
+ );
258
+
259
+ expect(steps.map((s) => s.name)).toEqual([
260
+ 'Generate Test',
261
+ 'Generate Component',
262
+ 'Type Fix Loop',
263
+ 'Test Fix Loop',
264
+ 'Lint Fix Loop',
265
+ 'Generate Story',
266
+ 'Story Fix Loop',
267
+ 'Storybook Test',
268
+ 'Visual Test',
269
+ ]);
270
+ });
271
+
272
+ it('wires each step to its corresponding function', async () => {
273
+ const models = makeModels();
274
+ const config = makeConfig({ enableStorybookTest: true, enableVisualTest: true });
275
+ const ctx = makeCtx();
276
+ const steps = buildPipelineSteps(models, config, ctx);
277
+
278
+ for (const step of steps) {
279
+ await step.run();
280
+ }
281
+
282
+ expect(generateTestStep).toHaveBeenCalledWith(models.generateTest, ctx);
283
+ expect(generateComponentStep).toHaveBeenCalledWith(models.generateComponent, ctx);
284
+ expect(typeFixLoop).toHaveBeenCalledWith(models.typeFixer, ctx, 3);
285
+ expect(testFixLoop).toHaveBeenCalledWith(models.testFixer, ctx, 3);
286
+ expect(lintFixLoop).toHaveBeenCalledWith(models.lintFixer, ctx, 2);
287
+ expect(generateStoryStep).toHaveBeenCalledWith(ctx);
288
+ expect(storyFixLoop).toHaveBeenCalledWith(models.storyFixer, ctx, 2);
289
+ expect(storybookTestStep).toHaveBeenCalledWith(ctx, true);
290
+ expect(visualTestStep).toHaveBeenCalled();
291
+ });
292
+ });
@@ -0,0 +1,160 @@
1
+ import type { LanguageModel } from 'ai';
2
+ import createDebug from 'debug';
3
+ import type { DeterministicStoryProp, StoryVariant } from '../generate-story-deterministic';
4
+ import type { SpecDeltas } from '../spec-contract';
5
+ import { generateComponentStep } from './steps/generate-component';
6
+ import { generateStoryStep } from './steps/generate-story';
7
+ import { generateTestStep } from './steps/generate-test';
8
+ import { lintFixLoop } from './steps/lint-fix-loop';
9
+ import { storyFixLoop } from './steps/story-fix-loop';
10
+ import { storybookTestStep } from './steps/storybook-test';
11
+ import { testFixLoop } from './steps/test-fix-loop';
12
+ import { typeFixLoop } from './steps/type-fix-loop';
13
+ import { visualTestStep } from './steps/visual-test';
14
+
15
+ const debug = createDebug('auto:component-implementor-react:pipeline');
16
+
17
+ export type StepResult = {
18
+ success: boolean;
19
+ error?: string;
20
+ };
21
+
22
+ export type PipelineStep = {
23
+ name: string;
24
+ run: () => Promise<StepResult>;
25
+ };
26
+
27
+ export type PipelineContext = {
28
+ componentName: string;
29
+ componentPath: string;
30
+ testPath: string;
31
+ storyPath: string;
32
+ componentImportPath: string;
33
+ targetDir: string;
34
+ specDeltas: SpecDeltas;
35
+ projectSection: string;
36
+ props?: DeterministicStoryProp[];
37
+ composes: Array<{ id: string; path: string }>;
38
+ existingComponent?: string;
39
+ existingTest?: string;
40
+ isModify: boolean;
41
+ storyVariants?: StoryVariant[];
42
+ testCode?: string;
43
+ componentCode?: string;
44
+ storyCode?: string;
45
+ llmCalls: number;
46
+ fixIterations: number;
47
+ };
48
+
49
+ export type ModelConfig = {
50
+ generateTest: string;
51
+ generateComponent: string;
52
+ typeFixer: string;
53
+ testFixer: string;
54
+ lintFixer: string;
55
+ storyFixer: string;
56
+ };
57
+
58
+ export type PipelineConfig = {
59
+ models: ModelConfig;
60
+ maxTypeFixIterations: number;
61
+ maxTestFixIterations: number;
62
+ maxLintFixIterations: number;
63
+ maxStoryFixIterations: number;
64
+ enableStorybookTest: boolean;
65
+ enableVisualTest: boolean;
66
+ };
67
+
68
+ export type PipelineModels = {
69
+ generateTest: LanguageModel;
70
+ generateComponent: LanguageModel;
71
+ typeFixer: LanguageModel;
72
+ testFixer: LanguageModel;
73
+ lintFixer: LanguageModel;
74
+ storyFixer: LanguageModel;
75
+ };
76
+
77
+ export type PipelineResult = {
78
+ success: boolean;
79
+ error?: string;
80
+ llmCalls: number;
81
+ fixIterations: number;
82
+ };
83
+
84
+ export function buildPipelineSteps(
85
+ models: PipelineModels,
86
+ config: PipelineConfig,
87
+ ctx: PipelineContext,
88
+ ): PipelineStep[] {
89
+ const steps: PipelineStep[] = [
90
+ {
91
+ name: 'Generate Test',
92
+ run: () => generateTestStep(models.generateTest, ctx),
93
+ },
94
+ {
95
+ name: 'Generate Component',
96
+ run: () => generateComponentStep(models.generateComponent, ctx),
97
+ },
98
+ {
99
+ name: 'Type Fix Loop',
100
+ run: () => typeFixLoop(models.typeFixer, ctx, config.maxTypeFixIterations),
101
+ },
102
+ {
103
+ name: 'Test Fix Loop',
104
+ run: () => testFixLoop(models.testFixer, ctx, config.maxTestFixIterations),
105
+ },
106
+ {
107
+ name: 'Lint Fix Loop',
108
+ run: () => lintFixLoop(models.lintFixer, ctx, config.maxLintFixIterations),
109
+ },
110
+ {
111
+ name: 'Generate Story',
112
+ run: () => generateStoryStep(ctx),
113
+ },
114
+ {
115
+ name: 'Story Fix Loop',
116
+ run: () => storyFixLoop(models.storyFixer, ctx, config.maxStoryFixIterations),
117
+ },
118
+ ];
119
+
120
+ if (config.enableStorybookTest) {
121
+ steps.push({
122
+ name: 'Storybook Test',
123
+ run: () => storybookTestStep(ctx, true),
124
+ });
125
+ }
126
+
127
+ if (config.enableVisualTest) {
128
+ steps.push({
129
+ name: 'Visual Test',
130
+ run: () => visualTestStep(),
131
+ });
132
+ }
133
+
134
+ return steps;
135
+ }
136
+
137
+ export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext): Promise<PipelineResult> {
138
+ for (const step of steps) {
139
+ debug('Running step: %s', step.name);
140
+ const result = await step.run();
141
+
142
+ if (!result.success) {
143
+ debug('Step failed: %s — %s', step.name, result.error);
144
+ return {
145
+ success: false,
146
+ error: `Step "${step.name}" failed: ${result.error}`,
147
+ llmCalls: ctx.llmCalls,
148
+ fixIterations: ctx.fixIterations,
149
+ };
150
+ }
151
+
152
+ debug('Step passed: %s', step.name);
153
+ }
154
+
155
+ return {
156
+ success: true,
157
+ llmCalls: ctx.llmCalls,
158
+ fixIterations: ctx.fixIterations,
159
+ };
160
+ }
@@ -0,0 +1,130 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('ai', () => ({
4
+ generateText: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('node:fs/promises', () => ({
8
+ readFile: vi.fn(),
9
+ writeFile: vi.fn(),
10
+ }));
11
+
12
+ import { readFile, writeFile } from 'node:fs/promises';
13
+ import { generateText } from 'ai';
14
+ import type { PipelineContext } from '../run-pipeline';
15
+ import { generateComponentStep } from './generate-component';
16
+
17
+ afterEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
22
+ return {
23
+ componentName: 'MyButton',
24
+ componentPath: '/project/src/MyButton.tsx',
25
+ testPath: '/project/src/MyButton.test.tsx',
26
+ storyPath: '/project/src/MyButton.stories.tsx',
27
+ componentImportPath: '@/components/ui/MyButton',
28
+ targetDir: '/project',
29
+ specDeltas: {
30
+ structure: ['renders a button'],
31
+ rendering: [],
32
+ interaction: [],
33
+ styling: [],
34
+ },
35
+ projectSection: '',
36
+ composes: [],
37
+ isModify: false,
38
+ llmCalls: 0,
39
+ fixIterations: 0,
40
+ testCode: 'import { render } from "@testing-library/react";',
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ describe('generateComponentStep', () => {
46
+ it('generates component code and writes to componentPath', async () => {
47
+ vi.mocked(generateText).mockResolvedValue({
48
+ text: '```tsx\nexport function MyButton() { return <button>Click</button> }\n```',
49
+ } as Awaited<ReturnType<typeof generateText>>);
50
+ vi.mocked(writeFile).mockResolvedValue(undefined);
51
+
52
+ const ctx = makeCtx();
53
+ const result = await generateComponentStep('mock-model' as never, ctx);
54
+
55
+ expect(result).toEqual({ success: true });
56
+ expect(generateText).toHaveBeenCalledTimes(1);
57
+ expect(writeFile).toHaveBeenCalledWith(
58
+ '/project/src/MyButton.tsx',
59
+ 'export function MyButton() { return <button>Click</button> }',
60
+ 'utf-8',
61
+ );
62
+ });
63
+
64
+ it('includes test code in prompt when available', async () => {
65
+ vi.mocked(generateText).mockResolvedValue({
66
+ text: 'component code',
67
+ } as Awaited<ReturnType<typeof generateText>>);
68
+ vi.mocked(writeFile).mockResolvedValue(undefined);
69
+
70
+ const ctx = makeCtx({ testCode: 'test source code' });
71
+ await generateComponentStep('mock-model' as never, ctx);
72
+
73
+ const call = vi.mocked(generateText).mock.calls[0][0];
74
+ expect(call.prompt).toContain('## Test File');
75
+ expect(call.prompt).toContain('test source code');
76
+ });
77
+
78
+ it('resolves composed component exports and falls back on missing files', async () => {
79
+ vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
80
+ vi.mocked(generateText).mockResolvedValue({
81
+ text: 'component code',
82
+ } as Awaited<ReturnType<typeof generateText>>);
83
+ vi.mocked(writeFile).mockResolvedValue(undefined);
84
+
85
+ const ctx = makeCtx({
86
+ composes: [{ id: 'card', path: 'src/components/Card' }],
87
+ });
88
+ const result = await generateComponentStep('mock-model' as never, ctx);
89
+
90
+ expect(result).toEqual({ success: true });
91
+ });
92
+
93
+ it('resolves composed component exports when file exists', async () => {
94
+ vi.mocked(readFile).mockResolvedValue('export function Card() {}' as never);
95
+ vi.mocked(generateText).mockResolvedValue({
96
+ text: 'component code',
97
+ } as Awaited<ReturnType<typeof generateText>>);
98
+ vi.mocked(writeFile).mockResolvedValue(undefined);
99
+
100
+ const ctx = makeCtx({
101
+ composes: [{ id: 'card', path: 'src/components/Card' }],
102
+ });
103
+ await generateComponentStep('mock-model' as never, ctx);
104
+
105
+ expect(readFile).toHaveBeenCalled();
106
+ });
107
+
108
+ it('generates scaffold section when props are provided', async () => {
109
+ vi.mocked(generateText).mockResolvedValue({
110
+ text: 'component with scaffold',
111
+ } as Awaited<ReturnType<typeof generateText>>);
112
+ vi.mocked(writeFile).mockResolvedValue(undefined);
113
+
114
+ const ctx = makeCtx({
115
+ props: [
116
+ {
117
+ name: 'label',
118
+ type: 'string',
119
+ required: true,
120
+ description: 'Button label',
121
+ category: 'data',
122
+ },
123
+ ],
124
+ });
125
+ await generateComponentStep('mock-model' as never, ctx);
126
+
127
+ const call = vi.mocked(generateText).mock.calls[0][0];
128
+ expect(call.prompt).toContain('## Component Scaffold');
129
+ });
130
+ });
@@ -0,0 +1,60 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { LanguageModel } from 'ai';
4
+ import { generateText } from 'ai';
5
+ import { extractCodeBlock } from '../../extract-code-block';
6
+ import { extractExportedNames } from '../../extract-exports';
7
+ import { buildComponentPrompt } from '../../prompt';
8
+ import { generateScaffold, type ScaffoldComposedComponent } from '../../scaffold';
9
+ import type { PipelineContext, StepResult } from '../run-pipeline';
10
+
11
+ export async function generateComponentStep(model: LanguageModel, ctx: PipelineContext): Promise<StepResult> {
12
+ const resolvedComposes: ScaffoldComposedComponent[] = await Promise.all(
13
+ ctx.composes.map(async (c) => {
14
+ const candidates = [
15
+ path.join(ctx.targetDir, c.path),
16
+ path.join(ctx.targetDir, `${c.path}.tsx`),
17
+ path.join(ctx.targetDir, `${c.path}.ts`),
18
+ ];
19
+ for (const candidate of candidates) {
20
+ try {
21
+ const content = await readFile(candidate, 'utf-8');
22
+ const exports = extractExportedNames(content);
23
+ return { ...c, exports };
24
+ } catch {}
25
+ }
26
+ return c;
27
+ }),
28
+ );
29
+
30
+ let scaffoldSection = '';
31
+ if (ctx.props && ctx.props.length > 0) {
32
+ const scaffold = generateScaffold({
33
+ componentName: ctx.componentName,
34
+ props: ctx.props.map((p) => ({ ...p, category: p.category })),
35
+ composes: resolvedComposes,
36
+ specDeltas: ctx.specDeltas,
37
+ });
38
+ scaffoldSection = `\n\n## Component Scaffold\n\n\`\`\`tsx\n${scaffold.scaffoldedFile}\n\`\`\`\n\nUse the props type exactly as shown. Replace \`__BODY__\` with the full component body.`;
39
+ }
40
+
41
+ const { system, prompt: basePrompt } = buildComponentPrompt({
42
+ componentName: ctx.componentName,
43
+ specDeltas: ctx.specDeltas,
44
+ existingComponent: ctx.existingComponent,
45
+ projectSection: ctx.projectSection,
46
+ testCode: ctx.testCode,
47
+ });
48
+
49
+ const prompt = scaffoldSection ? basePrompt + scaffoldSection : basePrompt;
50
+
51
+ const { text } = await generateText({ model, system, prompt });
52
+ ctx.llmCalls++;
53
+
54
+ const componentCode = extractCodeBlock(text);
55
+ ctx.componentCode = componentCode;
56
+
57
+ await writeFile(ctx.componentPath, componentCode, 'utf-8');
58
+
59
+ return { success: true };
60
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('node:fs/promises', () => ({
4
+ writeFile: vi.fn(),
5
+ }));
6
+
7
+ import { writeFile } from 'node:fs/promises';
8
+ import type { PipelineContext } from '../run-pipeline';
9
+ import { generateStoryStep } from './generate-story';
10
+
11
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
12
+ return {
13
+ componentName: 'MyButton',
14
+ componentPath: '/project/src/MyButton.tsx',
15
+ testPath: '/project/src/MyButton.test.tsx',
16
+ storyPath: '/project/src/MyButton.stories.tsx',
17
+ componentImportPath: '@/components/ui/MyButton',
18
+ targetDir: '/project',
19
+ specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
20
+ projectSection: '',
21
+ composes: [],
22
+ isModify: false,
23
+ llmCalls: 0,
24
+ fixIterations: 0,
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe('generateStoryStep', () => {
30
+ it('generates story deterministically and writes to storyPath', async () => {
31
+ vi.mocked(writeFile).mockResolvedValue(undefined);
32
+
33
+ const ctx = makeCtx();
34
+ const result = await generateStoryStep(ctx);
35
+
36
+ expect(result).toEqual({ success: true });
37
+ expect(ctx.storyCode).toContain('export default meta');
38
+ expect(ctx.storyCode).toContain('MyButton');
39
+ expect(writeFile).toHaveBeenCalledWith(
40
+ '/project/src/MyButton.stories.tsx',
41
+ expect.stringContaining('export default meta'),
42
+ 'utf-8',
43
+ );
44
+ });
45
+
46
+ it('does not increment llmCalls since no LLM is used', async () => {
47
+ vi.mocked(writeFile).mockResolvedValue(undefined);
48
+
49
+ const ctx = makeCtx();
50
+ await generateStoryStep(ctx);
51
+
52
+ expect(ctx.llmCalls).toBe(0);
53
+ });
54
+ });