@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +72 -0
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +13 -16
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/commands/implement-component.test.js +14 -5
- package/dist/src/commands/implement-component.test.js.map +1 -1
- package/dist/src/extract-code-block.d.ts +1 -0
- package/dist/src/extract-code-block.d.ts.map +1 -1
- package/dist/src/extract-code-block.js +12 -0
- package/dist/src/extract-code-block.js.map +1 -1
- package/dist/src/extract-code-block.test.js +28 -1
- package/dist/src/extract-code-block.test.js.map +1 -1
- package/dist/src/generate-component.d.ts +2 -13
- package/dist/src/generate-component.d.ts.map +1 -1
- package/dist/src/generate-component.js +4 -29
- package/dist/src/generate-component.js.map +1 -1
- package/dist/src/generate-component.test.js +18 -22
- package/dist/src/generate-component.test.js.map +1 -1
- package/dist/src/generate-story.d.ts +2 -12
- package/dist/src/generate-story.d.ts.map +1 -1
- package/dist/src/generate-story.js +4 -25
- package/dist/src/generate-story.js.map +1 -1
- package/dist/src/generate-story.test.js +17 -21
- package/dist/src/generate-story.test.js.map +1 -1
- package/dist/src/generate-test.d.ts +2 -12
- package/dist/src/generate-test.d.ts.map +1 -1
- package/dist/src/generate-test.js +4 -28
- package/dist/src/generate-test.js.map +1 -1
- package/dist/src/generate-test.test.js +17 -6
- package/dist/src/generate-test.test.js.map +1 -1
- package/dist/src/prompt.d.ts +64 -0
- package/dist/src/prompt.d.ts.map +1 -0
- package/dist/src/prompt.js +481 -0
- package/dist/src/prompt.js.map +1 -0
- package/dist/src/prompt.test.d.ts +2 -0
- package/dist/src/prompt.test.d.ts.map +1 -0
- package/dist/src/prompt.test.js +136 -0
- package/dist/src/prompt.test.js.map +1 -0
- package/dist/src/reconcile.d.ts +8 -0
- package/dist/src/reconcile.d.ts.map +1 -0
- package/dist/src/reconcile.js +18 -0
- package/dist/src/reconcile.js.map +1 -0
- package/dist/src/reconcile.test.d.ts +2 -0
- package/dist/src/reconcile.test.d.ts.map +1 -0
- package/dist/src/reconcile.test.js +108 -0
- package/dist/src/reconcile.test.js.map +1 -0
- package/dist/src/run.d.ts +2 -0
- package/dist/src/run.d.ts.map +1 -0
- package/dist/src/run.js +86 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/spec-contract.d.ts +9 -0
- package/dist/src/spec-contract.d.ts.map +1 -0
- package/dist/src/spec-contract.js +16 -0
- package/dist/src/spec-contract.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/improvement-prompt.md +208 -0
- package/inputs/action-button/spec.json +50 -0
- package/inputs/command-palette/spec.json +62 -0
- package/inputs/data-card/spec.json +59 -0
- package/inputs/editable-data-table/spec.json +70 -0
- package/inputs/multi-step-form/spec.json +66 -0
- package/inputs/notification-center/spec.json +67 -0
- package/inputs/search-input/spec.json +62 -0
- package/inputs/status-badge/spec.json +46 -0
- package/package.json +4 -3
- package/scripts/improve.ts +592 -0
- package/src/commands/implement-component.test.ts +14 -5
- package/src/commands/implement-component.ts +13 -17
- package/src/extract-code-block.test.ts +33 -1
- package/src/extract-code-block.ts +13 -0
- package/src/generate-component.test.ts +22 -26
- package/src/generate-component.ts +5 -46
- package/src/generate-story.test.ts +17 -21
- package/src/generate-story.ts +5 -40
- package/src/generate-test.test.ts +22 -7
- package/src/generate-test.ts +5 -44
- package/src/prompt.test.ts +163 -0
- package/src/prompt.ts +581 -0
- package/src/reconcile.test.ts +127 -0
- package/src/reconcile.ts +27 -0
- package/src/run.ts +106 -0
- 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
|
+
});
|
package/src/reconcile.ts
ADDED
|
@@ -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
|
+
}
|