@auto-engineer/component-implementor-react 1.110.2 → 1.110.3

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 (96) 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 +19 -0
  5. package/dist/src/commands/implement-component.d.ts +5 -6
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +37 -9
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +41 -54
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/pipeline/run-pipeline.d.ts +25 -5
  12. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -1
  13. package/dist/src/pipeline/run-pipeline.js +47 -17
  14. package/dist/src/pipeline/run-pipeline.js.map +1 -1
  15. package/dist/src/pipeline/run-pipeline.test.js +129 -29
  16. package/dist/src/pipeline/run-pipeline.test.js.map +1 -1
  17. package/dist/src/pipeline/steps/generate-component.test.js +5 -1
  18. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -1
  19. package/dist/src/pipeline/steps/generate-story.test.js +5 -1
  20. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -1
  21. package/dist/src/pipeline/steps/generate-test.test.js +5 -1
  22. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -1
  23. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  24. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  25. package/dist/src/pipeline/steps/lint-fix-loop.js +46 -0
  26. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  27. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  28. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/steps/lint-fix-loop.test.js +123 -0
  30. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  32. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/story-fix-loop.js +35 -0
  34. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  35. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  36. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  37. package/dist/src/pipeline/steps/story-fix-loop.test.js +98 -0
  38. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  39. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  40. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  41. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  42. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  43. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  44. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  45. package/dist/src/pipeline/steps/storybook-test.test.js +70 -0
  46. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  47. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  48. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  49. package/dist/src/pipeline/steps/test-fix-loop.js +45 -0
  50. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  51. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  52. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  53. package/dist/src/pipeline/steps/test-fix-loop.test.js +172 -0
  54. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  55. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  56. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  57. package/dist/src/pipeline/steps/type-fix-loop.js +44 -0
  58. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  59. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  60. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  61. package/dist/src/pipeline/steps/type-fix-loop.test.js +116 -0
  62. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  63. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  64. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  65. package/dist/src/pipeline/steps/visual-test.js +4 -0
  66. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  67. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  68. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  69. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  70. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +3 -3
  73. package/src/commands/implement-component.test.ts +47 -57
  74. package/src/commands/implement-component.ts +51 -14
  75. package/src/pipeline/run-pipeline.test.ts +137 -32
  76. package/src/pipeline/run-pipeline.ts +74 -22
  77. package/src/pipeline/steps/generate-component.test.ts +5 -1
  78. package/src/pipeline/steps/generate-story.test.ts +5 -1
  79. package/src/pipeline/steps/generate-test.test.ts +5 -1
  80. package/src/pipeline/steps/lint-fix-loop.test.ts +159 -0
  81. package/src/pipeline/steps/lint-fix-loop.ts +60 -0
  82. package/src/pipeline/steps/story-fix-loop.test.ts +127 -0
  83. package/src/pipeline/steps/story-fix-loop.ts +48 -0
  84. package/src/pipeline/steps/storybook-test.test.ts +86 -0
  85. package/src/pipeline/steps/storybook-test.ts +27 -0
  86. package/src/pipeline/steps/test-fix-loop.test.ts +205 -0
  87. package/src/pipeline/steps/test-fix-loop.ts +57 -0
  88. package/src/pipeline/steps/type-fix-loop.test.ts +149 -0
  89. package/src/pipeline/steps/type-fix-loop.ts +56 -0
  90. package/src/pipeline/steps/visual-test.test.ts +10 -0
  91. package/src/pipeline/steps/visual-test.ts +5 -0
  92. package/dist/src/pipeline/steps/fix-from-feedback.d.ts +0 -4
  93. package/dist/src/pipeline/steps/fix-from-feedback.d.ts.map +0 -1
  94. package/dist/src/pipeline/steps/fix-from-feedback.js +0 -94
  95. package/dist/src/pipeline/steps/fix-from-feedback.js.map +0 -1
  96. package/src/pipeline/steps/fix-from-feedback.ts +0 -105
@@ -2,10 +2,15 @@ import type { LanguageModel } from 'ai';
2
2
  import createDebug from 'debug';
3
3
  import type { ScaffoldProp } from '../scaffold';
4
4
  import type { SpecDeltas } from '../spec-contract';
5
- import { fixFromFeedbackStep } from './steps/fix-from-feedback';
6
5
  import { generateComponentStep } from './steps/generate-component';
7
6
  import { generateStoryStep } from './steps/generate-story';
8
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';
9
14
 
10
15
  const debug = createDebug('auto:component-implementor-react:pipeline');
11
16
 
@@ -37,55 +42,60 @@ export type PipelineContext = {
37
42
  componentCode?: string;
38
43
  storyCode?: string;
39
44
  llmCalls: number;
40
- errorFeedback?: string;
41
- attemptNumber: number;
45
+ fixIterations: number;
46
+ typeFixIterations: number;
47
+ testFixIterations: number;
48
+ lintFixIterations: number;
49
+ storyFixIterations: number;
42
50
  };
43
51
 
44
52
  export type ModelConfig = {
45
53
  generateTest: string;
46
54
  generateComponent: string;
47
55
  generateStory: string;
48
- fixer: string;
56
+ typeFixer: string;
57
+ testFixer: string;
58
+ lintFixer: string;
59
+ storyFixer: string;
49
60
  };
50
61
 
51
62
  export type PipelineConfig = {
52
63
  models: ModelConfig;
64
+ maxTypeFixIterations: number;
65
+ maxTestFixIterations: number;
66
+ maxLintFixIterations: number;
67
+ maxStoryFixIterations: number;
68
+ enableStorybookTest: boolean;
69
+ enableVisualTest: boolean;
53
70
  };
54
71
 
55
72
  export type PipelineModels = {
56
73
  generateTest: LanguageModel;
57
74
  generateComponent: LanguageModel;
58
75
  generateStory: LanguageModel;
59
- fixer: LanguageModel;
76
+ typeFixer: LanguageModel;
77
+ testFixer: LanguageModel;
78
+ lintFixer: LanguageModel;
79
+ storyFixer: LanguageModel;
60
80
  };
61
81
 
62
82
  export type PipelineResult = {
63
83
  success: boolean;
64
84
  error?: string;
65
85
  llmCalls: number;
86
+ fixIterations: number;
87
+ typeFixIterations: number;
88
+ testFixIterations: number;
89
+ lintFixIterations: number;
90
+ storyFixIterations: number;
66
91
  };
67
92
 
68
93
  export function buildPipelineSteps(
69
94
  models: PipelineModels,
70
- _config: PipelineConfig,
95
+ config: PipelineConfig,
71
96
  ctx: PipelineContext,
72
97
  ): PipelineStep[] {
73
- // On retry (attemptNumber > 0), fix from feedback then regenerate story
74
- if (ctx.attemptNumber > 0 && ctx.errorFeedback) {
75
- return [
76
- {
77
- name: 'Fix From Feedback',
78
- run: () => fixFromFeedbackStep(models.fixer, ctx),
79
- },
80
- {
81
- name: 'Generate Story',
82
- run: () => generateStoryStep(models.generateStory, ctx),
83
- },
84
- ];
85
- }
86
-
87
- // First attempt: generate everything
88
- return [
98
+ const steps: PipelineStep[] = [
89
99
  {
90
100
  name: 'Generate Test',
91
101
  run: () => generateTestStep(models.generateTest, ctx),
@@ -94,11 +104,43 @@ export function buildPipelineSteps(
94
104
  name: 'Generate Component',
95
105
  run: () => generateComponentStep(models.generateComponent, ctx),
96
106
  },
107
+ {
108
+ name: 'Type Fix Loop',
109
+ run: () => typeFixLoop(models.typeFixer, ctx, config.maxTypeFixIterations),
110
+ },
111
+ {
112
+ name: 'Test Fix Loop',
113
+ run: () => testFixLoop(models.testFixer, ctx, config.maxTestFixIterations),
114
+ },
115
+ {
116
+ name: 'Lint Fix Loop',
117
+ run: () => lintFixLoop(models.lintFixer, ctx, config.maxLintFixIterations),
118
+ },
97
119
  {
98
120
  name: 'Generate Story',
99
121
  run: () => generateStoryStep(models.generateStory, ctx),
100
122
  },
123
+ {
124
+ name: 'Story Fix Loop',
125
+ run: () => storyFixLoop(models.storyFixer, ctx, config.maxStoryFixIterations),
126
+ },
101
127
  ];
128
+
129
+ if (config.enableStorybookTest) {
130
+ steps.push({
131
+ name: 'Storybook Test',
132
+ run: () => storybookTestStep(ctx, true),
133
+ });
134
+ }
135
+
136
+ if (config.enableVisualTest) {
137
+ steps.push({
138
+ name: 'Visual Test',
139
+ run: () => visualTestStep(),
140
+ });
141
+ }
142
+
143
+ return steps;
102
144
  }
103
145
 
104
146
  export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext): Promise<PipelineResult> {
@@ -112,6 +154,11 @@ export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext):
112
154
  success: false,
113
155
  error: `Step "${step.name}" failed: ${result.error}`,
114
156
  llmCalls: ctx.llmCalls,
157
+ fixIterations: ctx.fixIterations,
158
+ typeFixIterations: ctx.typeFixIterations,
159
+ testFixIterations: ctx.testFixIterations,
160
+ lintFixIterations: ctx.lintFixIterations,
161
+ storyFixIterations: ctx.storyFixIterations,
115
162
  };
116
163
  }
117
164
 
@@ -121,5 +168,10 @@ export async function runPipeline(steps: PipelineStep[], ctx: PipelineContext):
121
168
  return {
122
169
  success: true,
123
170
  llmCalls: ctx.llmCalls,
171
+ fixIterations: ctx.fixIterations,
172
+ typeFixIterations: ctx.typeFixIterations,
173
+ testFixIterations: ctx.testFixIterations,
174
+ lintFixIterations: ctx.lintFixIterations,
175
+ storyFixIterations: ctx.storyFixIterations,
124
176
  };
125
177
  }
@@ -36,7 +36,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
36
36
  composes: [],
37
37
  isModify: false,
38
38
  llmCalls: 0,
39
- attemptNumber: 0,
39
+ fixIterations: 0,
40
+ typeFixIterations: 0,
41
+ testFixIterations: 0,
42
+ lintFixIterations: 0,
43
+ storyFixIterations: 0,
40
44
  testCode: 'import { render } from "@testing-library/react";',
41
45
  ...overrides,
42
46
  };
@@ -28,7 +28,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
28
28
  composes: [],
29
29
  isModify: false,
30
30
  llmCalls: 0,
31
- attemptNumber: 0,
31
+ fixIterations: 0,
32
+ typeFixIterations: 0,
33
+ testFixIterations: 0,
34
+ lintFixIterations: 0,
35
+ storyFixIterations: 0,
32
36
  ...overrides,
33
37
  };
34
38
  }
@@ -35,7 +35,11 @@ function makeCtx(overrides: Partial<PipelineContext> = {}): PipelineContext {
35
35
  composes: [],
36
36
  isModify: false,
37
37
  llmCalls: 0,
38
- attemptNumber: 0,
38
+ fixIterations: 0,
39
+ typeFixIterations: 0,
40
+ testFixIterations: 0,
41
+ lintFixIterations: 0,
42
+ storyFixIterations: 0,
39
43
  ...overrides,
40
44
  };
41
45
  }
@@ -0,0 +1,159 @@
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
+ typeFixIterations: 0,
42
+ testFixIterations: 0,
43
+ lintFixIterations: 0,
44
+ storyFixIterations: 0,
45
+ componentCode: 'original component',
46
+ testCode: 'original test',
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ describe('lintFixLoop', () => {
52
+ it('runs auto-fix first, then returns success if lint passes', async () => {
53
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
54
+ vi.mocked(readFile).mockResolvedValue('auto-fixed code' as never);
55
+ vi.mocked(runLint).mockReturnValue({ passed: true, errors: [] });
56
+
57
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
58
+
59
+ expect(result).toEqual({ success: true });
60
+ expect(runLintFix).toHaveBeenCalledWith(
61
+ ['/project/src/MyButton.tsx', '/project/src/MyButton.test.tsx'],
62
+ '/project',
63
+ );
64
+ expect(generateText).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('calls LLM for remaining lint errors after auto-fix', async () => {
68
+ vi.mocked(runLintFix).mockReturnValue({ passed: false, errors: ['unfixable'] });
69
+ vi.mocked(readFile).mockResolvedValue('auto-fixed partial' as never);
70
+ vi.mocked(runLint)
71
+ .mockReturnValueOnce({ passed: false, errors: ['remaining error'] })
72
+ .mockReturnValueOnce({ passed: true, errors: [] });
73
+
74
+ vi.mocked(generateText).mockResolvedValue({
75
+ text: '```tsx\nlint-fixed component\n```\n```tsx\nlint-fixed test\n```',
76
+ } as Awaited<ReturnType<typeof generateText>>);
77
+ vi.mocked(writeFile).mockResolvedValue(undefined);
78
+
79
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
80
+
81
+ expect(result).toEqual({ success: true });
82
+ expect(generateText).toHaveBeenCalledTimes(1);
83
+ });
84
+
85
+ it('returns failure after exhausting max iterations', async () => {
86
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
87
+ vi.mocked(readFile).mockResolvedValue('code' as never);
88
+ vi.mocked(runLint).mockReturnValue({ passed: false, errors: ['persistent lint error'] });
89
+
90
+ vi.mocked(generateText).mockResolvedValue({
91
+ text: '```tsx\nstill bad\n```\n```tsx\nstill bad test\n```',
92
+ } as Awaited<ReturnType<typeof generateText>>);
93
+ vi.mocked(writeFile).mockResolvedValue(undefined);
94
+
95
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
96
+
97
+ expect(result).toEqual({
98
+ success: false,
99
+ error: expect.stringContaining('Lint errors remain after 1 iterations'),
100
+ });
101
+ });
102
+
103
+ it('handles single code block response by updating only component', async () => {
104
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
105
+ vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
106
+ vi.mocked(runLint)
107
+ .mockReturnValueOnce({ passed: false, errors: ['lint error'] })
108
+ .mockReturnValueOnce({ passed: true, errors: [] });
109
+
110
+ vi.mocked(generateText).mockResolvedValue({
111
+ text: '```tsx\nfixed component only\n```',
112
+ } as Awaited<ReturnType<typeof generateText>>);
113
+ vi.mocked(writeFile).mockResolvedValue(undefined);
114
+
115
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 2);
116
+
117
+ expect(result).toEqual({ success: true });
118
+ expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.tsx', 'fixed component only', 'utf-8');
119
+ expect(writeFile).toHaveBeenCalledTimes(1);
120
+ });
121
+
122
+ it('returns success when final check passes after last iteration', async () => {
123
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
124
+ vi.mocked(readFile).mockResolvedValue('auto-fixed' as never);
125
+ vi.mocked(runLint)
126
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
127
+ .mockReturnValueOnce({ passed: true, errors: [] });
128
+
129
+ vi.mocked(generateText).mockResolvedValue({
130
+ text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
131
+ } as Awaited<ReturnType<typeof generateText>>);
132
+ vi.mocked(writeFile).mockResolvedValue(undefined);
133
+
134
+ const result = await lintFixLoop('mock-model' as never, makeCtx(), 1);
135
+
136
+ expect(result).toEqual({ success: true });
137
+ });
138
+
139
+ it('uses empty string fallback when componentCode and testCode are undefined after readFile', async () => {
140
+ vi.mocked(runLintFix).mockReturnValue({ passed: true, errors: [] });
141
+ vi.mocked(readFile).mockResolvedValue(undefined as never);
142
+ vi.mocked(runLint)
143
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
144
+ .mockReturnValueOnce({ passed: true, errors: [] });
145
+
146
+ vi.mocked(generateText).mockResolvedValue({
147
+ text: '```tsx\nfixed\n```\n```tsx\nfixed test\n```',
148
+ } as Awaited<ReturnType<typeof generateText>>);
149
+ vi.mocked(writeFile).mockResolvedValue(undefined);
150
+
151
+ const result = await lintFixLoop(
152
+ 'mock-model' as never,
153
+ makeCtx({ componentCode: undefined, testCode: undefined }),
154
+ 1,
155
+ );
156
+
157
+ expect(result).toEqual({ success: true });
158
+ });
159
+ });
@@ -0,0 +1,60 @@
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
+ ctx.lintFixIterations++;
38
+
39
+ const blocks = extractCodeBlocks(text);
40
+ if (blocks.length >= 2) {
41
+ ctx.componentCode = blocks[0];
42
+ ctx.testCode = blocks[1];
43
+ await writeFile(ctx.componentPath, blocks[0], 'utf-8');
44
+ await writeFile(ctx.testPath, blocks[1], 'utf-8');
45
+ } else if (blocks.length === 1) {
46
+ ctx.componentCode = blocks[0];
47
+ await writeFile(ctx.componentPath, blocks[0], 'utf-8');
48
+ }
49
+ }
50
+
51
+ const finalResult = runLint(filePaths, ctx.targetDir);
52
+ if (finalResult.passed) {
53
+ return { success: true };
54
+ }
55
+
56
+ return {
57
+ success: false,
58
+ error: `Lint errors remain after ${maxIterations} iterations: ${finalResult.errors.slice(0, 3).join('; ')}`,
59
+ };
60
+ }
@@ -0,0 +1,127 @@
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
+ typeFixIterations: 0,
41
+ testFixIterations: 0,
42
+ lintFixIterations: 0,
43
+ storyFixIterations: 0,
44
+ storyCode: 'original story',
45
+ componentCode: 'original component',
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ describe('storyFixLoop', () => {
51
+ it('returns success immediately when story type-checks', async () => {
52
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
53
+
54
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
55
+
56
+ expect(result).toEqual({ success: true });
57
+ expect(generateText).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it('calls LLM to fix story type errors and writes fixed story', async () => {
61
+ vi.mocked(runTypeCheck)
62
+ .mockReturnValueOnce({ passed: false, errors: ['error in story'] })
63
+ .mockReturnValueOnce({ passed: true, errors: [] });
64
+
65
+ vi.mocked(generateText).mockResolvedValue({
66
+ text: '```tsx\nfixed story code\n```',
67
+ } as Awaited<ReturnType<typeof generateText>>);
68
+ vi.mocked(writeFile).mockResolvedValue(undefined);
69
+
70
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 2);
71
+
72
+ expect(result).toEqual({ success: true });
73
+ expect(generateText).toHaveBeenCalledTimes(1);
74
+ expect(writeFile).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', 'fixed story code', 'utf-8');
75
+ });
76
+
77
+ it('returns failure after exhausting max iterations', async () => {
78
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: false, errors: ['persistent story error'] });
79
+
80
+ vi.mocked(generateText).mockResolvedValue({
81
+ text: '```tsx\nstill broken story\n```',
82
+ } as Awaited<ReturnType<typeof generateText>>);
83
+ vi.mocked(writeFile).mockResolvedValue(undefined);
84
+ vi.mocked(readFile).mockResolvedValue('still broken story' as never);
85
+
86
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
87
+
88
+ expect(result).toEqual({
89
+ success: false,
90
+ error: expect.stringContaining('Story type errors remain after 1 iterations'),
91
+ });
92
+ });
93
+
94
+ it('returns success when final check passes after last iteration fix', async () => {
95
+ vi.mocked(runTypeCheck)
96
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
97
+ .mockReturnValueOnce({ passed: true, errors: [] });
98
+
99
+ vi.mocked(generateText).mockResolvedValue({
100
+ text: '```tsx\nfixed story\n```',
101
+ } as Awaited<ReturnType<typeof generateText>>);
102
+ vi.mocked(writeFile).mockResolvedValue(undefined);
103
+
104
+ const result = await storyFixLoop('mock-model' as never, makeCtx(), 1);
105
+
106
+ expect(result).toEqual({ success: true });
107
+ });
108
+
109
+ it('uses empty string fallback when storyCode and componentCode are undefined', async () => {
110
+ vi.mocked(runTypeCheck)
111
+ .mockReturnValueOnce({ passed: false, errors: ['error'] })
112
+ .mockReturnValueOnce({ passed: true, errors: [] });
113
+
114
+ vi.mocked(generateText).mockResolvedValue({
115
+ text: '```tsx\nfixed story\n```',
116
+ } as Awaited<ReturnType<typeof generateText>>);
117
+ vi.mocked(writeFile).mockResolvedValue(undefined);
118
+
119
+ const result = await storyFixLoop(
120
+ 'mock-model' as never,
121
+ makeCtx({ storyCode: undefined, componentCode: undefined }),
122
+ 1,
123
+ );
124
+
125
+ expect(result).toEqual({ success: true });
126
+ });
127
+ });
@@ -0,0 +1,48 @@
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
+ ctx.storyFixIterations++;
31
+
32
+ const fixedStory = extractCodeBlock(text);
33
+ ctx.storyCode = fixedStory;
34
+ await writeFile(ctx.storyPath, fixedStory, 'utf-8');
35
+ }
36
+
37
+ const finalResult = runTypeCheck(ctx.targetDir, [ctx.storyPath]);
38
+ if (finalResult.passed) {
39
+ return { success: true };
40
+ }
41
+
42
+ ctx.storyCode = await readFile(ctx.storyPath, 'utf-8');
43
+
44
+ return {
45
+ success: false,
46
+ error: `Story type errors remain after ${maxIterations} iterations: ${finalResult.errors.join('; ')}`,
47
+ };
48
+ }
@@ -0,0 +1,86 @@
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
+ typeFixIterations: 0,
31
+ testFixIterations: 0,
32
+ lintFixIterations: 0,
33
+ storyFixIterations: 0,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ describe('storybookTestStep', () => {
39
+ it('returns success when type check passes and storybook cli disabled', async () => {
40
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
41
+
42
+ const result = await storybookTestStep(makeCtx(), false);
43
+
44
+ expect(result).toEqual({ success: true });
45
+ expect(runStorybookTest).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it('returns failure when story type check fails', async () => {
49
+ vi.mocked(runTypeCheck).mockReturnValue({
50
+ passed: false,
51
+ errors: ['error in story'],
52
+ });
53
+
54
+ const result = await storybookTestStep(makeCtx(), false);
55
+
56
+ expect(result).toEqual({
57
+ success: false,
58
+ error: expect.stringContaining('Story type errors'),
59
+ });
60
+ });
61
+
62
+ it('runs storybook CLI test when enabled and type check passes', async () => {
63
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
64
+ vi.mocked(runStorybookTest).mockReturnValue({ passed: true, errors: [] });
65
+
66
+ const result = await storybookTestStep(makeCtx(), true);
67
+
68
+ expect(result).toEqual({ success: true });
69
+ expect(runStorybookTest).toHaveBeenCalledWith('/project/src/MyButton.stories.tsx', '/project');
70
+ });
71
+
72
+ it('returns failure when storybook CLI test fails', async () => {
73
+ vi.mocked(runTypeCheck).mockReturnValue({ passed: true, errors: [] });
74
+ vi.mocked(runStorybookTest).mockReturnValue({
75
+ passed: false,
76
+ errors: ['Story failed'],
77
+ });
78
+
79
+ const result = await storybookTestStep(makeCtx(), true);
80
+
81
+ expect(result).toEqual({
82
+ success: false,
83
+ error: expect.stringContaining('Storybook test failures'),
84
+ });
85
+ });
86
+ });