@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,17 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { generateStoryDeterministic } from '../../generate-story-deterministic';
3
+ import type { PipelineContext, StepResult } from '../run-pipeline';
4
+
5
+ export async function generateStoryStep(ctx: PipelineContext): Promise<StepResult> {
6
+ const storyCode = generateStoryDeterministic({
7
+ componentName: ctx.componentName,
8
+ componentImportPath: ctx.componentImportPath,
9
+ props: ctx.props,
10
+ storyVariants: ctx.storyVariants,
11
+ });
12
+
13
+ ctx.storyCode = storyCode;
14
+ await writeFile(ctx.storyPath, storyCode, 'utf-8');
15
+
16
+ return { success: true };
17
+ }
@@ -0,0 +1,75 @@
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
+ writeFile: vi.fn(),
9
+ }));
10
+
11
+ import { writeFile } from 'node:fs/promises';
12
+ import { generateText } from 'ai';
13
+ import type { PipelineContext } from '../run-pipeline';
14
+ import { generateTestStep } from './generate-test';
15
+
16
+ afterEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
21
+ return {
22
+ componentName: 'MyButton',
23
+ componentPath: '/project/src/MyButton.tsx',
24
+ testPath: '/project/src/MyButton.test.tsx',
25
+ storyPath: '/project/src/MyButton.stories.tsx',
26
+ componentImportPath: '@/components/ui/MyButton',
27
+ targetDir: '/project',
28
+ specDeltas: {
29
+ structure: ['renders a button'],
30
+ rendering: [],
31
+ interaction: [],
32
+ styling: [],
33
+ },
34
+ projectSection: '',
35
+ composes: [],
36
+ isModify: false,
37
+ llmCalls: 0,
38
+ fixIterations: 0,
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ describe('generateTestStep', () => {
44
+ it('generates test code and writes to testPath', async () => {
45
+ vi.mocked(generateText).mockResolvedValue({
46
+ text: '```tsx\nimport { render } from "@testing-library/react";\n```',
47
+ } as Awaited<ReturnType<typeof generateText>>);
48
+ vi.mocked(writeFile).mockResolvedValue(undefined);
49
+
50
+ const ctx = makeCtx();
51
+ const result = await generateTestStep('mock-model' as never, ctx);
52
+
53
+ expect(result).toEqual({ success: true });
54
+ expect(generateText).toHaveBeenCalledTimes(1);
55
+ expect(writeFile).toHaveBeenCalledWith(
56
+ '/project/src/MyButton.test.tsx',
57
+ 'import { render } from "@testing-library/react";',
58
+ 'utf-8',
59
+ );
60
+ });
61
+
62
+ it('passes existing component to prompt when in modify mode', async () => {
63
+ vi.mocked(generateText).mockResolvedValue({
64
+ text: 'test code',
65
+ } as Awaited<ReturnType<typeof generateText>>);
66
+ vi.mocked(writeFile).mockResolvedValue(undefined);
67
+
68
+ const ctx = makeCtx({ existingComponent: 'export function MyButton() {}', isModify: true });
69
+ await generateTestStep('mock-model' as never, ctx);
70
+
71
+ const call = vi.mocked(generateText).mock.calls[0][0];
72
+ expect(call.prompt).toContain('## Existing Component');
73
+ expect(call.prompt).toContain('export function MyButton() {}');
74
+ });
75
+ });
@@ -0,0 +1,25 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import type { LanguageModel } from 'ai';
3
+ import { generateText } from 'ai';
4
+ import { extractCodeBlock } from '../../extract-code-block';
5
+ import { buildTestPrompt } from '../../prompt';
6
+ import type { PipelineContext, StepResult } from '../run-pipeline';
7
+
8
+ export async function generateTestStep(model: LanguageModel, ctx: PipelineContext): Promise<StepResult> {
9
+ const { system, prompt } = buildTestPrompt({
10
+ componentName: ctx.componentName,
11
+ specDeltas: ctx.specDeltas,
12
+ existingComponent: ctx.existingComponent,
13
+ projectSection: ctx.projectSection,
14
+ });
15
+
16
+ const { text } = await generateText({ model, system, prompt });
17
+ ctx.llmCalls++;
18
+
19
+ const testCode = extractCodeBlock(text);
20
+ ctx.testCode = testCode;
21
+
22
+ await writeFile(ctx.testPath, testCode, 'utf-8');
23
+
24
+ return { success: true };
25
+ }
@@ -0,0 +1,155 @@
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
+ vi.mock('../../tools/lint-runner', () => ({
13
+ runLint: vi.fn(),
14
+ runLintFix: vi.fn(),
15
+ }));
16
+
17
+ import { readFile, writeFile } from 'node:fs/promises';
18
+ import { generateText } from 'ai';
19
+ import { runLint, runLintFix } from '../../tools/lint-runner';
20
+ import type { PipelineContext } from '../run-pipeline';
21
+ import { lintFixLoop } from './lint-fix-loop';
22
+
23
+ afterEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
28
+ return {
29
+ componentName: 'MyButton',
30
+ componentPath: '/project/src/MyButton.tsx',
31
+ testPath: '/project/src/MyButton.test.tsx',
32
+ storyPath: '/project/src/MyButton.stories.tsx',
33
+ componentImportPath: '@/components/ui/MyButton',
34
+ targetDir: '/project',
35
+ specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
36
+ projectSection: '',
37
+ composes: [],
38
+ isModify: false,
39
+ llmCalls: 0,
40
+ fixIterations: 0,
41
+ componentCode: 'original component',
42
+ testCode: 'original test',
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ describe('lintFixLoop', () => {
48
+ it('runs auto-fix first, then returns success if lint passes', async () => {
49
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
50
+ vi.mocked(readFile).mockResolvedValue('auto-fixed code' as never);
51
+ vi.mocked(runLint).mockReturnValue({ passed: true, errors: [] });
52
+
53
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
54
+
55
+ expect(result).toEqual({ success: true });
56
+ expect(runLintFix).toHaveBeenCalledWith(
57
+ ['/project/src/MyButton.tsx', '/project/src/MyButton.test.tsx'],
58
+ '/project',
59
+ );
60
+ expect(generateText).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('calls LLM for remaining lint errors after auto-fix', async () => {
64
+ vi.mocked(runLintFix).mockReturnValue({ passed: false, errors: ['unfixable'] });
65
+ vi.mocked(readFile).mockResolvedValue('auto-fixed partial' as never);
66
+ vi.mocked(runLint)
67
+ .mockReturnValueOnce({ passed: false, errors: ['remaining error'] })
68
+ .mockReturnValueOnce({ passed: true, errors: [] });
69
+
70
+ vi.mocked(generateText).mockResolvedValue({
71
+ text: '```tsx\nlint-fixed component\n```\n```tsx\nlint-fixed test\n```',
72
+ } as Awaited<ReturnType<typeof generateText>>);
73
+ vi.mocked(writeFile).mockResolvedValue(undefined);
74
+
75
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
76
+
77
+ expect(result).toEqual({ success: true });
78
+ expect(generateText).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ it('returns failure after exhausting max iterations', async () => {
82
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
83
+ vi.mocked(readFile).mockResolvedValue('code' as never);
84
+ vi.mocked(runLint).mockReturnValue({ passed: false, errors: ['persistent lint error'] });
85
+
86
+ vi.mocked(generateText).mockResolvedValue({
87
+ text: '```tsx\nstill bad\n```\n```tsx\nstill bad test\n```',
88
+ } as Awaited<ReturnType<typeof generateText>>);
89
+ vi.mocked(writeFile).mockResolvedValue(undefined);
90
+
91
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
92
+
93
+ expect(result).toEqual({
94
+ success: false,
95
+ error: expect.stringContaining('Lint errors remain after 1 iterations'),
96
+ });
97
+ });
98
+
99
+ it('handles single code block response by updating only component', async () => {
100
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
101
+ vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
102
+ vi.mocked(runLint)
103
+ .mockReturnValueOnce({ passed: false, errors: ['lint error'] })
104
+ .mockReturnValueOnce({ passed: true, errors: [] });
105
+
106
+ vi.mocked(generateText).mockResolvedValue({
107
+ text: '```tsx\nfixed component only\n```',
108
+ } as Awaited<ReturnType<typeof generateText>>);
109
+ vi.mocked(writeFile).mockResolvedValue(undefined);
110
+
111
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
112
+
113
+ expect(result).toEqual({ success: true });
114
+ expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
115
+ expect(writeFile).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it('returns success when final check passes after last iteration', async () => {
119
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
120
+ vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
121
+ vi.mocked(runLint)
122
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
123
+ .mockReturnValueOnce({ passed: true, errors: [] });
124
+
125
+ vi.mocked(generateText).mockResolvedValue({
126
+ text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
127
+ } as Awaited<ReturnType<typeof generateText>>);
128
+ vi.mocked(writeFile).mockResolvedValue(undefined);
129
+
130
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
131
+
132
+ expect(result).toEqual({ success: true });
133
+ });
134
+
135
+ it('uses empty string fallback when componentCode and testCode are undefined after readFile', async () => {
136
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
137
+ vi.mocked(readFile).mockResolvedValue(undefined as never);
138
+ vi.mocked(runLint)
139
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
140
+ .mockReturnValueOnce({ passed: true, errors: [] });
141
+
142
+ vi.mocked(generateText).mockResolvedValue({
143
+ text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
144
+ } as Awaited<ReturnType<typeof generateText>>);
145
+ vi.mocked(writeFile).mockResolvedValue(undefined);
146
+
147
+ const result = await lintFixLoop(
148
+ 'mock-model' as never,
149
+ makeCtx({ componentCode: undefined, testCode: undefined }),
150
+ 1,
151
+ );
152
+
153
+ expect(result).toEqual({ success: true });
154
+ });
155
+ });
@@ -0,0 +1,59 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import type { LanguageModel } from 'ai';
3
+ import { generateText } from 'ai';
4
+ import { extractCodeBlocks } from '../../extract-code-block';
5
+ import { buildLintFixPrompt } from '../../prompt';
6
+ import { runLint, runLintFix } from '../../tools/lint-runner';
7
+ import type { PipelineContext, StepResult } from '../run-pipeline';
8
+
9
+ export async function lintFixLoop(
10
+ model: LanguageModel,
11
+ ctx: PipelineContext,
12
+ maxIterations: number,
13
+ ): Promise<StepResult> {
14
+ const filePaths = [ctx.componentPath, ctx.testPath];
15
+
16
+ runLintFix(filePaths, ctx.targetDir);
17
+
18
+ ctx.componentCode = await readFile(ctx.componentPath, 'utf-8');
19
+ ctx.testCode = await readFile(ctx.testPath, 'utf-8');
20
+
21
+ for (let i = 0; i < maxIterations; i++) {
22
+ const result = runLint(filePaths, ctx.targetDir);
23
+
24
+ if (result.passed) {
25
+ return { success: true };
26
+ }
27
+
28
+ const { system, prompt } = buildLintFixPrompt({
29
+ componentCode: ctx.componentCode ?? '',
30
+ testCode: ctx.testCode ?? '',
31
+ errors: result.errors,
32
+ });
33
+
34
+ const { text } = await generateText({ model, system, prompt });
35
+ ctx.llmCalls++;
36
+ ctx.fixIterations++;
37
+
38
+ const blocks = extractCodeBlocks(text);
39
+ if (blocks.length >= 2) {
40
+ ctx.componentCode = blocks[0];
41
+ ctx.testCode = blocks[1];
42
+ await writeFile(ctx.componentPath, blocks[0], 'utf-8');
43
+ await writeFile(ctx.testPath, blocks[1], 'utf-8');
44
+ } else if (blocks.length === 1) {
45
+ ctx.componentCode = blocks[0];
46
+ await writeFile(ctx.componentPath, blocks[0], 'utf-8');
47
+ }
48
+ }
49
+
50
+ const finalResult = runLint(filePaths, ctx.targetDir);
51
+ if (finalResult.passed) {
52
+ return { success: true };
53
+ }
54
+
55
+ return {
56
+ success: false,
57
+ error: `Lint errors remain after ${maxIterations} iterations: ${finalResult.errors.slice(0, 3).join('; ')}`,
58
+ };
59
+ }
@@ -0,0 +1,123 @@
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
+ vi.mock('../../tools/type-checker', () => ({
13
+ runTypeCheck: vi.fn(),
14
+ }));
15
+
16
+ import { readFile, writeFile } from 'node:fs/promises';
17
+ import { generateText } from 'ai';
18
+ import { runTypeCheck } from '../../tools/type-checker';
19
+ import type { PipelineContext } from '../run-pipeline';
20
+ import { storyFixLoop } from './story-fix-loop';
21
+
22
+ afterEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+
26
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
27
+ return {
28
+ componentName: 'MyButton',
29
+ componentPath: '/project/src/MyButton.tsx',
30
+ testPath: '/project/src/MyButton.test.tsx',
31
+ storyPath: '/project/src/MyButton.stories.tsx',
32
+ componentImportPath: '@/components/ui/MyButton',
33
+ targetDir: '/project',
34
+ specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
35
+ projectSection: '',
36
+ composes: [],
37
+ isModify: false,
38
+ llmCalls: 0,
39
+ fixIterations: 0,
40
+ storyCode: 'original story',
41
+ componentCode: 'original component',
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ describe('storyFixLoop', () => {
47
+ it('returns success immediately when story type-checks', async () => {
48
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
49
+
50
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
51
+
52
+ expect(result).toEqual({ success: true });
53
+ expect(generateText).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it('calls LLM to fix story type errors and writes fixed story', async () => {
57
+ vi.mocked(runTypeCheck)
58
+ .mockReturnValueOnce({ passed: false, errors: ['error in story'] })
59
+ .mockReturnValueOnce({ passed: true, errors: [] });
60
+
61
+ vi.mocked(generateText).mockResolvedValue({
62
+ text: '```tsx\nfixed story code\n```',
63
+ } as Awaited<ReturnType<typeof generateText>>);
64
+ vi.mocked(writeFile).mockResolvedValue(undefined);
65
+
66
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
67
+
68
+ expect(result).toEqual({ success: true });
69
+ expect(generateText).toHaveBeenCalledTimes(1);
70
+ expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', 'fixed story code', 'utf-8');
71
+ });
72
+
73
+ it('returns failure after exhausting max iterations', async () => {
74
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: false, errors: ['persistent story error'] });
75
+
76
+ vi.mocked(generateText).mockResolvedValue({
77
+ text: '```tsx\nstill broken story\n```',
78
+ } as Awaited<ReturnType<typeof generateText>>);
79
+ vi.mocked(writeFile).mockResolvedValue(undefined);
80
+ vi.mocked(readFile).mockResolvedValue('still broken story' as never);
81
+
82
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
83
+
84
+ expect(result).toEqual({
85
+ success: false,
86
+ error: expect.stringContaining('Story type errors remain after 1 iterations'),
87
+ });
88
+ });
89
+
90
+ it('returns success when final check passes after last iteration fix', async () => {
91
+ vi.mocked(runTypeCheck)
92
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
93
+ .mockReturnValueOnce({ passed: true, errors: [] });
94
+
95
+ vi.mocked(generateText).mockResolvedValue({
96
+ text: '```tsx\nfixed story\n```',
97
+ } as Awaited<ReturnType<typeof generateText>>);
98
+ vi.mocked(writeFile).mockResolvedValue(undefined);
99
+
100
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
101
+
102
+ expect(result).toEqual({ success: true });
103
+ });
104
+
105
+ it('uses empty string fallback when storyCode and componentCode are undefined', async () => {
106
+ vi.mocked(runTypeCheck)
107
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
108
+ .mockReturnValueOnce({ passed: true, errors: [] });
109
+
110
+ vi.mocked(generateText).mockResolvedValue({
111
+ text: '```tsx\nfixed story\n```',
112
+ } as Awaited<ReturnType<typeof generateText>>);
113
+ vi.mocked(writeFile).mockResolvedValue(undefined);
114
+
115
+ const result = await storyFixLoop(
116
+ 'mock-model' as never,
117
+ makeCtx({ storyCode: undefined, componentCode: undefined }),
118
+ 1,
119
+ );
120
+
121
+ expect(result).toEqual({ success: true });
122
+ });
123
+ });
@@ -0,0 +1,47 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import type { LanguageModel } from 'ai';
3
+ import { generateText } from 'ai';
4
+ import { extractCodeBlock } from '../../extract-code-block';
5
+ import { buildStoryFixPrompt } from '../../prompt';
6
+ import { runTypeCheck } from '../../tools/type-checker';
7
+ import type { PipelineContext, StepResult } from '../run-pipeline';
8
+
9
+ export async function storyFixLoop(
10
+ model: LanguageModel,
11
+ ctx: PipelineContext,
12
+ maxIterations: number,
13
+ ): Promise<StepResult> {
14
+ for (let i = 0; i < maxIterations; i++) {
15
+ const result = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
16
+
17
+ if (result.passed) {
18
+ return { success: true };
19
+ }
20
+
21
+ const { system, prompt } = buildStoryFixPrompt({
22
+ storyCode: ctx.storyCode ?? '',
23
+ componentCode: ctx.componentCode ?? '',
24
+ errors: result.errors,
25
+ });
26
+
27
+ const { text } = await generateText({ model, system, prompt });
28
+ ctx.llmCalls++;
29
+ ctx.fixIterations++;
30
+
31
+ const fixedStory = extractCodeBlock(text);
32
+ ctx.storyCode = fixedStory;
33
+ await writeFile(ctx.storyPath, fixedStory, 'utf-8');
34
+ }
35
+
36
+ const finalResult = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
37
+ if (finalResult.passed) {
38
+ return { success: true };
39
+ }
40
+
41
+ ctx.storyCode = await readFile(ctx.storyPath, 'utf-8');
42
+
43
+ return {
44
+ success: false,
45
+ error: `Story type errors remain after ${maxIterations} iterations: ${finalResult.errors.join('; ')}`,
46
+ };
47
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../../tools/type-checker', () => ({
4
+ runTypeCheck: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../../tools/storybook-runner', () => ({
8
+ runStorybookTest: vi.fn(),
9
+ }));
10
+
11
+ import { runStorybookTest } from '../../tools/storybook-runner';
12
+ import { runTypeCheck } from '../../tools/type-checker';
13
+ import type { PipelineContext } from '../run-pipeline';
14
+ import { storybookTestStep } from './storybook-test';
15
+
16
+ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
17
+ return {
18
+ componentName: 'MyButton',
19
+ componentPath: '/project/src/MyButton.tsx',
20
+ testPath: '/project/src/MyButton.test.tsx',
21
+ storyPath: '/project/src/MyButton.stories.tsx',
22
+ componentImportPath: '@/components/ui/MyButton',
23
+ targetDir: '/project',
24
+ specDeltas: { structure: [], rendering: [], interaction: [], styling: [] },
25
+ projectSection: '',
26
+ composes: [],
27
+ isModify: false,
28
+ llmCalls: 0,
29
+ fixIterations: 0,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe('storybookTestStep', () => {
35
+ it('returns success when type check passes and storybook cli disabled', async () => {
36
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
37
+
38
+ const result = await storybookTestStep(makeCtx(), false);
39
+
40
+ expect(result).toEqual({ success: true });
41
+ expect(runStorybookTest).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('returns failure when story type check fails', async () => {
45
+ vi.mocked(runTypeCheck).mockReturnValue({
46
+ passed: false,
47
+ errors: ['error in story'],
48
+ });
49
+
50
+ const result = await storybookTestStep(makeCtx(), false);
51
+
52
+ expect(result).toEqual({
53
+ success: false,
54
+ error: expect.stringContaining('Story type errors'),
55
+ });
56
+ });
57
+
58
+ it('runs storybook CLI test when enabled and type check passes', async () => {
59
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
60
+ vi.mocked(runStorybookTest).mockReturnValue({ passed: true, errors: [] });
61
+
62
+ const result = await storybookTestStep(makeCtx(), true);
63
+
64
+ expect(result).toEqual({ success: true });
65
+ expect(runStorybookTest).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', '/project');
66
+ });
67
+
68
+ it('returns failure when storybook CLI test fails', async () => {
69
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
70
+ vi.mocked(runStorybookTest).mockReturnValue({
71
+ passed: false,
72
+ errors: ['Story failed'],
73
+ });
74
+
75
+ const result = await storybookTestStep(makeCtx(), true);
76
+
77
+ expect(result).toEqual({
78
+ success: false,
79
+ error: expect.stringContaining('Storybook test failures'),
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,27 @@
1
+ import { runStorybookTest } from '../../tools/storybook-runner';
2
+ import { runTypeCheck } from '../../tools/type-checker';
3
+ import type { PipelineContext, StepResult } from '../run-pipeline';
4
+
5
+ export async function storybookTestStep(ctx: PipelineContext, enableStorybookCli: boolean): Promise<StepResult> {
6
+ const typeResult = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
7
+
8
+ if (!typeResult.passed) {
9
+ return {
10
+ success: false,
11
+ error: `Story type errors: ${typeResult.errors.join('; ')}`,
12
+ };
13
+ }
14
+
15
+ if (enableStorybookCli) {
16
+ const testResult = runStorybookTest(ctx.storyPath, ctx.targetDir);
17
+
18
+ if (!testResult.passed) {
19
+ return {
20
+ success: false,
21
+ error: `Storybook test failures: ${testResult.errors.join('; ')}`,
22
+ };
23
+ }
24
+ }
25
+
26
+ return { success: true };
27
+ }