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