@auto-engineer/component-implementor-react 1.95.0 → 1.96.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 (85) 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 +72 -0
  5. package/dist/src/commands/implement-component.d.ts.map +1 -1
  6. package/dist/src/commands/implement-component.js +13 -16
  7. package/dist/src/commands/implement-component.js.map +1 -1
  8. package/dist/src/commands/implement-component.test.js +14 -5
  9. package/dist/src/commands/implement-component.test.js.map +1 -1
  10. package/dist/src/extract-code-block.d.ts +1 -0
  11. package/dist/src/extract-code-block.d.ts.map +1 -1
  12. package/dist/src/extract-code-block.js +12 -0
  13. package/dist/src/extract-code-block.js.map +1 -1
  14. package/dist/src/extract-code-block.test.js +28 -1
  15. package/dist/src/extract-code-block.test.js.map +1 -1
  16. package/dist/src/generate-component.d.ts +2 -13
  17. package/dist/src/generate-component.d.ts.map +1 -1
  18. package/dist/src/generate-component.js +4 -29
  19. package/dist/src/generate-component.js.map +1 -1
  20. package/dist/src/generate-component.test.js +18 -22
  21. package/dist/src/generate-component.test.js.map +1 -1
  22. package/dist/src/generate-story.d.ts +2 -12
  23. package/dist/src/generate-story.d.ts.map +1 -1
  24. package/dist/src/generate-story.js +4 -25
  25. package/dist/src/generate-story.js.map +1 -1
  26. package/dist/src/generate-story.test.js +17 -21
  27. package/dist/src/generate-story.test.js.map +1 -1
  28. package/dist/src/generate-test.d.ts +2 -12
  29. package/dist/src/generate-test.d.ts.map +1 -1
  30. package/dist/src/generate-test.js +4 -28
  31. package/dist/src/generate-test.js.map +1 -1
  32. package/dist/src/generate-test.test.js +17 -6
  33. package/dist/src/generate-test.test.js.map +1 -1
  34. package/dist/src/prompt.d.ts +64 -0
  35. package/dist/src/prompt.d.ts.map +1 -0
  36. package/dist/src/prompt.js +481 -0
  37. package/dist/src/prompt.js.map +1 -0
  38. package/dist/src/prompt.test.d.ts +2 -0
  39. package/dist/src/prompt.test.d.ts.map +1 -0
  40. package/dist/src/prompt.test.js +136 -0
  41. package/dist/src/prompt.test.js.map +1 -0
  42. package/dist/src/reconcile.d.ts +8 -0
  43. package/dist/src/reconcile.d.ts.map +1 -0
  44. package/dist/src/reconcile.js +18 -0
  45. package/dist/src/reconcile.js.map +1 -0
  46. package/dist/src/reconcile.test.d.ts +2 -0
  47. package/dist/src/reconcile.test.d.ts.map +1 -0
  48. package/dist/src/reconcile.test.js +108 -0
  49. package/dist/src/reconcile.test.js.map +1 -0
  50. package/dist/src/run.d.ts +2 -0
  51. package/dist/src/run.d.ts.map +1 -0
  52. package/dist/src/run.js +86 -0
  53. package/dist/src/run.js.map +1 -0
  54. package/dist/src/spec-contract.d.ts +9 -0
  55. package/dist/src/spec-contract.d.ts.map +1 -0
  56. package/dist/src/spec-contract.js +16 -0
  57. package/dist/src/spec-contract.js.map +1 -0
  58. package/dist/tsconfig.tsbuildinfo +1 -1
  59. package/improvement-prompt.md +208 -0
  60. package/inputs/action-button/spec.json +50 -0
  61. package/inputs/command-palette/spec.json +62 -0
  62. package/inputs/data-card/spec.json +59 -0
  63. package/inputs/editable-data-table/spec.json +70 -0
  64. package/inputs/multi-step-form/spec.json +66 -0
  65. package/inputs/notification-center/spec.json +67 -0
  66. package/inputs/search-input/spec.json +62 -0
  67. package/inputs/status-badge/spec.json +46 -0
  68. package/package.json +4 -3
  69. package/scripts/improve.ts +592 -0
  70. package/src/commands/implement-component.test.ts +14 -5
  71. package/src/commands/implement-component.ts +13 -17
  72. package/src/extract-code-block.test.ts +33 -1
  73. package/src/extract-code-block.ts +13 -0
  74. package/src/generate-component.test.ts +22 -26
  75. package/src/generate-component.ts +5 -46
  76. package/src/generate-story.test.ts +17 -21
  77. package/src/generate-story.ts +5 -40
  78. package/src/generate-test.test.ts +22 -7
  79. package/src/generate-test.ts +5 -44
  80. package/src/prompt.test.ts +163 -0
  81. package/src/prompt.ts +581 -0
  82. package/src/reconcile.test.ts +127 -0
  83. package/src/reconcile.ts +27 -0
  84. package/src/run.ts +106 -0
  85. package/src/spec-contract.ts +22 -0
@@ -0,0 +1,127 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { reconcile } from './reconcile';
3
+
4
+ vi.mock('ai', () => ({
5
+ generateText: vi.fn(),
6
+ }));
7
+
8
+ vi.mock('@auto-engineer/model-factory', () => ({
9
+ createModelFromEnv: vi.fn(() => 'mock-model'),
10
+ }));
11
+
12
+ import { generateText } from 'ai';
13
+
14
+ const specDeltas = {
15
+ structure: ['renders a Button element'],
16
+ rendering: ['shows loading spinner when loading=true'],
17
+ interaction: ['calls onClick when clicked'],
18
+ styling: ['applies primary variant by default'],
19
+ };
20
+
21
+ describe('reconcile', () => {
22
+ afterEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+
26
+ it('passes system prompt with methodology, rules, and checklist', async () => {
27
+ const mockGenerateText = vi.mocked(generateText);
28
+ mockGenerateText.mockResolvedValue({
29
+ text: '```tsx\nc\n```\n```tsx\ns\n```',
30
+ } as Awaited<ReturnType<typeof generateText>>);
31
+
32
+ await reconcile({
33
+ componentName: 'Button',
34
+ specDeltas,
35
+ componentCode: 'c',
36
+ testCode: 't',
37
+ storyCode: 's',
38
+ });
39
+
40
+ const system = mockGenerateText.mock.calls[0][0].system as string;
41
+ expect(system).toContain('staff frontend engineer');
42
+ expect(system).toContain('integration review');
43
+ expect(system).toContain('METHODOLOGY');
44
+ expect(system).toContain('RULES');
45
+ expect(system).toContain('QUALITY CHECKLIST');
46
+ expect(system).toContain('Tests are immutable');
47
+ });
48
+
49
+ it('sends component, test, and story code in the user prompt', async () => {
50
+ const mockGenerateText = vi.mocked(generateText);
51
+ mockGenerateText.mockResolvedValue({
52
+ text: '```tsx\nfixed component\n```\n\n```tsx\nfixed story\n```',
53
+ } as Awaited<ReturnType<typeof generateText>>);
54
+
55
+ await reconcile({
56
+ componentName: 'Button',
57
+ specDeltas,
58
+ componentCode: 'original component',
59
+ testCode: 'test code here',
60
+ storyCode: 'original story',
61
+ });
62
+
63
+ const prompt = mockGenerateText.mock.calls[0][0].prompt as string;
64
+ expect(prompt).toContain('## Component Code');
65
+ expect(prompt).toContain('original component');
66
+ expect(prompt).toContain('## Test File (source of truth');
67
+ expect(prompt).toContain('test code here');
68
+ expect(prompt).toContain('## Story File');
69
+ expect(prompt).toContain('original story');
70
+ });
71
+
72
+ it('parses two code blocks and returns component + story', async () => {
73
+ const mockGenerateText = vi.mocked(generateText);
74
+ mockGenerateText.mockResolvedValue({
75
+ text: '```tsx\nfixed component code\n```\n\n```tsx\nfixed story code\n```',
76
+ } as Awaited<ReturnType<typeof generateText>>);
77
+
78
+ const result = await reconcile({
79
+ componentName: 'Button',
80
+ specDeltas,
81
+ componentCode: 'original component',
82
+ testCode: 'test code',
83
+ storyCode: 'original story',
84
+ });
85
+
86
+ expect(result.componentCode).toBe('fixed component code');
87
+ expect(result.storyCode).toBe('fixed story code');
88
+ });
89
+
90
+ it('falls back to original code when blocks are missing', async () => {
91
+ const mockGenerateText = vi.mocked(generateText);
92
+ mockGenerateText.mockResolvedValue({
93
+ text: 'no code blocks at all',
94
+ } as Awaited<ReturnType<typeof generateText>>);
95
+
96
+ const result = await reconcile({
97
+ componentName: 'Button',
98
+ specDeltas,
99
+ componentCode: 'original component',
100
+ testCode: 'test code',
101
+ storyCode: 'original story',
102
+ });
103
+
104
+ expect(result.componentCode).toBe('no code blocks at all');
105
+ expect(result.storyCode).toBe('original story');
106
+ });
107
+
108
+ it('includes existing component in prompt when provided', async () => {
109
+ const mockGenerateText = vi.mocked(generateText);
110
+ mockGenerateText.mockResolvedValue({
111
+ text: '```tsx\nfixed\n```\n```tsx\nstory\n```',
112
+ } as Awaited<ReturnType<typeof generateText>>);
113
+
114
+ await reconcile({
115
+ componentName: 'Button',
116
+ specDeltas,
117
+ componentCode: 'new component',
118
+ testCode: 'test code',
119
+ storyCode: 'story code',
120
+ existingComponent: 'old component',
121
+ });
122
+
123
+ const prompt = mockGenerateText.mock.calls[0][0].prompt as string;
124
+ expect(prompt).toContain('## Existing Component');
125
+ expect(prompt).toContain('old component');
126
+ });
127
+ });
@@ -0,0 +1,27 @@
1
+ import { createModelFromEnv } from '@auto-engineer/model-factory';
2
+ import { generateText } from 'ai';
3
+ import { extractCodeBlocks } from './extract-code-block';
4
+ import { buildReconcilerPrompt, type ReconcilerPromptInput } from './prompt';
5
+
6
+ export type ReconcileInput = ReconcilerPromptInput;
7
+
8
+ export type ReconcileOutput = {
9
+ componentCode: string;
10
+ storyCode: string;
11
+ };
12
+
13
+ export async function reconcile(input: ReconcileInput): Promise<ReconcileOutput> {
14
+ const { system, prompt } = buildReconcilerPrompt(input);
15
+ const { text } = await generateText({
16
+ model: createModelFromEnv(),
17
+ system,
18
+ prompt,
19
+ });
20
+
21
+ const blocks = extractCodeBlocks(text);
22
+
23
+ return {
24
+ componentCode: blocks[0] ?? input.componentCode,
25
+ storyCode: blocks[1] ?? input.storyCode,
26
+ };
27
+ }
package/src/run.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { mkdir, readFile, rm, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { config } from 'dotenv';
4
+ import { handleImplementComponent, type ImplementComponentCommand } from './commands/implement-component';
5
+
6
+ // Load .env from package root
7
+ config({ path: path.resolve(import.meta.dirname, '..', '.env') });
8
+
9
+ const pkgDir = path.resolve(import.meta.dirname, '..');
10
+
11
+ function pascalCase(id: string): string {
12
+ return id
13
+ .split('-')
14
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
15
+ .join('');
16
+ }
17
+
18
+ /**
19
+ * Fix empty file paths for new components that weren't in the components DB.
20
+ * Derives sensible paths from the componentId.
21
+ */
22
+ function patchJobPayload(payload: Record<string, unknown>): void {
23
+ const componentId = payload.componentId as string;
24
+ const name = pascalCase(componentId);
25
+ const componentFile = `src/components/${name}.tsx`;
26
+ const storyFile = `src/components/${name}.stories.tsx`;
27
+
28
+ // Fix empty files.create
29
+ const files = payload.files as { create?: string[]; modify?: string[] };
30
+ if (files.create && files.create.length > 0 && files.create[0] === '') {
31
+ files.create = [componentFile];
32
+ }
33
+ if (!files.create || files.create.length === 0) {
34
+ files.create = [componentFile];
35
+ }
36
+
37
+ // Fix empty storybookPath
38
+ if (!payload.storybookPath || payload.storybookPath === '.stories') {
39
+ payload.storybookPath = storyFile;
40
+ }
41
+ }
42
+
43
+ async function main() {
44
+ const jobGraphPath = path.join(pkgDir, 'job-graph.json');
45
+ const raw = await readFile(jobGraphPath, 'utf-8');
46
+ const jobs = JSON.parse(raw) as Array<{
47
+ id: string;
48
+ dependsOn: string[];
49
+ target: string;
50
+ payload: Record<string, unknown>;
51
+ }>;
52
+
53
+ console.log(`[run] Loaded ${jobs.length} jobs from job-graph.json`);
54
+
55
+ // Target dir — where component files get written
56
+ const targetDir = path.join(pkgDir, 'output');
57
+
58
+ // Guard: if 'output' exists as a file, remove it so mkdir succeeds
59
+ try {
60
+ const s = await stat(targetDir);
61
+ if (!s.isDirectory()) {
62
+ await rm(targetDir);
63
+ }
64
+ } catch {
65
+ // doesn't exist yet — fine
66
+ }
67
+
68
+ await mkdir(path.join(targetDir, 'src/components'), { recursive: true });
69
+
70
+ for (const job of jobs) {
71
+ const componentId = job.payload.componentId as string;
72
+ console.log(`\n[run] ── ${componentId} ──`);
73
+
74
+ // Patch empty paths for new components
75
+ patchJobPayload(job.payload);
76
+
77
+ const command: ImplementComponentCommand = {
78
+ type: 'ImplementComponent',
79
+ data: {
80
+ targetDir,
81
+ job: job as ImplementComponentCommand['data']['job'],
82
+ },
83
+ timestamp: new Date(),
84
+ requestId: `run-${componentId}`,
85
+ correlationId: `run-${Date.now()}`,
86
+ };
87
+
88
+ const result = await handleImplementComponent(command);
89
+
90
+ if (result.type === 'ComponentImplemented') {
91
+ console.log(`[run] ✓ ${result.data.name}`);
92
+ console.log(`[run] component: ${result.data.componentPath}`);
93
+ console.log(`[run] test: ${result.data.testPath}`);
94
+ console.log(`[run] story: ${result.data.storyPath}`);
95
+ } else {
96
+ console.log(`[run] ✗ ${result.data.name}: ${result.data.error}`);
97
+ }
98
+ }
99
+
100
+ console.log('\n[run] Done');
101
+ }
102
+
103
+ main().catch((err) => {
104
+ console.error('[run] Fatal error:', err);
105
+ process.exit(1);
106
+ });
@@ -0,0 +1,22 @@
1
+ export type SpecDeltas = {
2
+ structure: string[];
3
+ rendering: string[];
4
+ interaction: string[];
5
+ styling: string[];
6
+ };
7
+
8
+ export function buildSpecSection(heading: string, items: string[]): string {
9
+ if (items.length === 0) return '';
10
+ return `## ${heading}\n${items.map((i) => `- ${i}`).join('\n')}`;
11
+ }
12
+
13
+ export function buildSpecText(specDeltas: SpecDeltas): string {
14
+ return [
15
+ buildSpecSection('Structure', specDeltas.structure),
16
+ buildSpecSection('Rendering', specDeltas.rendering),
17
+ buildSpecSection('Interaction', specDeltas.interaction),
18
+ buildSpecSection('Styling', specDeltas.styling),
19
+ ]
20
+ .filter(Boolean)
21
+ .join('\n\n');
22
+ }