@auto-engineer/server-implementer 1.81.0 → 1.83.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 (32) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +72 -0
  5. package/dist/src/agent/detectShadowsInSlice.specs.d.ts +2 -0
  6. package/dist/src/agent/detectShadowsInSlice.specs.d.ts.map +1 -0
  7. package/dist/src/agent/detectShadowsInSlice.specs.js +41 -0
  8. package/dist/src/agent/detectShadowsInSlice.specs.js.map +1 -0
  9. package/dist/src/agent/runSlice.d.ts +4 -0
  10. package/dist/src/agent/runSlice.d.ts.map +1 -1
  11. package/dist/src/agent/runSlice.js +22 -3
  12. package/dist/src/agent/runSlice.js.map +1 -1
  13. package/dist/src/commands/implement-slice.d.ts.map +1 -1
  14. package/dist/src/commands/implement-slice.js +3 -0
  15. package/dist/src/commands/implement-slice.js.map +1 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/utils/detectImportedTypeShadowing.d.ts +3 -0
  18. package/dist/src/utils/detectImportedTypeShadowing.d.ts.map +1 -0
  19. package/dist/src/utils/detectImportedTypeShadowing.js +44 -0
  20. package/dist/src/utils/detectImportedTypeShadowing.js.map +1 -0
  21. package/dist/src/utils/detectImportedTypeShadowing.specs.d.ts +2 -0
  22. package/dist/src/utils/detectImportedTypeShadowing.specs.d.ts.map +1 -0
  23. package/dist/src/utils/detectImportedTypeShadowing.specs.js +112 -0
  24. package/dist/src/utils/detectImportedTypeShadowing.specs.js.map +1 -0
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/ketchup-plan.md +9 -0
  27. package/package.json +4 -4
  28. package/src/agent/detectShadowsInSlice.specs.ts +62 -0
  29. package/src/agent/runSlice.ts +23 -3
  30. package/src/commands/implement-slice.ts +3 -0
  31. package/src/utils/detectImportedTypeShadowing.specs.ts +129 -0
  32. package/src/utils/detectImportedTypeShadowing.ts +47 -0
@@ -0,0 +1,9 @@
1
+ # Ketchup Plan: Prevent implementer AI from redefining imported types
2
+
3
+ ## TODO
4
+
5
+ ## DONE
6
+
7
+ - [x] Burst 1: Detection utility — detectImportedTypeShadowing with TypeScript AST [depends: none] (afff6f0f)
8
+ - [x] Burst 2: Integrate shadow detection into runSlice.ts and implement-slice.ts [depends: 1] (b47ae539)
9
+ - [x] Burst 3: Clarify Internal State Pattern in projection template + update snapshots [depends: none] (pending)
package/package.json CHANGED
@@ -18,8 +18,8 @@
18
18
  "debug": "^4.3.4",
19
19
  "fast-glob": "^3.3.3",
20
20
  "vite": "^5.4.1",
21
- "@auto-engineer/model-factory": "1.81.0",
22
- "@auto-engineer/message-bus": "1.81.0"
21
+ "@auto-engineer/message-bus": "1.83.0",
22
+ "@auto-engineer/model-factory": "1.83.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/fs-extra": "^11.0.4",
@@ -29,9 +29,9 @@
29
29
  "glob": "^11.0.3",
30
30
  "tsx": "^4.20.3",
31
31
  "typescript": "^5.8.3",
32
- "@auto-engineer/cli": "1.81.0"
32
+ "@auto-engineer/cli": "1.83.0"
33
33
  },
34
- "version": "1.81.0",
34
+ "version": "1.83.0",
35
35
  "scripts": {
36
36
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
37
37
  "test": "vitest run --reporter=dot",
@@ -0,0 +1,62 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { detectShadowsInSlice } from './runSlice';
6
+
7
+ describe('detectShadowsInSlice', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(path.join(tmpdir(), 'shadow-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true });
16
+ });
17
+
18
+ it('returns empty results for clean files', async () => {
19
+ await writeFile(path.join(tempDir, 'projection.ts'), `import type { MyState } from './state';\nconst x = 1;\n`);
20
+
21
+ const result = await detectShadowsInSlice(tempDir);
22
+
23
+ expect(result).toEqual({ errors: '', failedFiles: [] });
24
+ });
25
+
26
+ it('detects shadow in a file and returns error with file path', async () => {
27
+ await writeFile(
28
+ path.join(tempDir, 'projection.ts'),
29
+ `import type { MyState } from './state';\ntype MyState = { name: string };\n`,
30
+ );
31
+
32
+ const result = await detectShadowsInSlice(tempDir);
33
+
34
+ expect(result).toEqual({
35
+ errors:
36
+ 'CONTRACT VIOLATION in projection.ts: Types [MyState] are imported but redefined locally. Remove local definitions and use the imports.',
37
+ failedFiles: [path.join(tempDir, 'projection.ts')],
38
+ });
39
+ });
40
+
41
+ it('ignores spec files', async () => {
42
+ await writeFile(
43
+ path.join(tempDir, 'projection.specs.ts'),
44
+ `import type { MyState } from './state';\ntype MyState = { name: string };\n`,
45
+ );
46
+
47
+ const result = await detectShadowsInSlice(tempDir);
48
+
49
+ expect(result).toEqual({ errors: '', failedFiles: [] });
50
+ });
51
+
52
+ it('reports multiple files with shadows', async () => {
53
+ await writeFile(path.join(tempDir, 'a.ts'), `import type { Alpha } from './types';\ntype Alpha = { x: number };\n`);
54
+ await writeFile(path.join(tempDir, 'b.ts'), `import type { Beta } from './types';\ninterface Beta { y: string }\n`);
55
+
56
+ const result = await detectShadowsInSlice(tempDir);
57
+
58
+ expect(result.failedFiles).toEqual([path.join(tempDir, 'a.ts'), path.join(tempDir, 'b.ts')]);
59
+ expect(result.errors).toContain('CONTRACT VIOLATION in a.ts');
60
+ expect(result.errors).toContain('CONTRACT VIOLATION in b.ts');
61
+ });
62
+ });
@@ -5,6 +5,7 @@ import { generateText } from 'ai';
5
5
  import { execa } from 'execa';
6
6
  import fg from 'fast-glob';
7
7
  import { SYSTEM_PROMPT } from '../prompts/systemPrompt';
8
+ import { buildShadowWarning } from '../utils/detectImportedTypeShadowing';
8
9
  import { extractCodeBlock } from '../utils/extractCodeBlock';
9
10
  import { runTests } from './runTests';
10
11
 
@@ -185,15 +186,34 @@ export async function runTestsAndTypecheck(sliceDir: string): Promise<TestAndTyp
185
186
  const rootDir = await findProjectRoot(sliceDir);
186
187
  const testResult = await runTests(sliceDir, rootDir);
187
188
  const typecheckResult = await runTypecheck(sliceDir, rootDir);
189
+ const shadowResult = await detectShadowsInSlice(sliceDir);
190
+ const failedTypecheckFiles = [...typecheckResult.failedTypecheckFiles, ...shadowResult.failedFiles];
191
+ const typecheckErrors = [typecheckResult.typecheckErrors, shadowResult.errors].filter(Boolean).join('\n');
192
+ const hasErrors = !testResult.success || !typecheckResult.success || shadowResult.errors.length > 0;
188
193
  return {
189
- success: testResult.success && typecheckResult.success,
194
+ success: !hasErrors,
190
195
  failedTestFiles: testResult.failedTestFiles,
191
- failedTypecheckFiles: typecheckResult.failedTypecheckFiles,
196
+ failedTypecheckFiles,
192
197
  testErrors: testResult.testErrors,
193
- typecheckErrors: typecheckResult.typecheckErrors,
198
+ typecheckErrors,
194
199
  };
195
200
  }
196
201
 
202
+ export async function detectShadowsInSlice(sliceDir: string): Promise<{ errors: string; failedFiles: string[] }> {
203
+ const files = await fg(['*.ts'], { cwd: sliceDir, ignore: ['*.spec.ts', '*.specs.ts', '*.test.ts'] });
204
+ const errors: string[] = [];
205
+ const failedFiles: string[] = [];
206
+ for (const file of files) {
207
+ const content = await readFile(path.join(sliceDir, file), 'utf-8');
208
+ const warning = buildShadowWarning(content, file);
209
+ if (warning.length > 0) {
210
+ errors.push(warning);
211
+ failedFiles.push(path.join(sliceDir, file));
212
+ }
213
+ }
214
+ return { errors: errors.join('\n'), failedFiles };
215
+ }
216
+
197
217
  async function retryFailedTests(sliceDir: string, flow: string, result: TestAndTypecheckResult) {
198
218
  let contextFiles = await loadContextFiles(sliceDir);
199
219
  for (let attempt = 1; attempt <= 5; attempt++) {
@@ -6,6 +6,7 @@ import { createModelFromEnv } from '@auto-engineer/model-factory';
6
6
  import { generateText } from 'ai';
7
7
  import createDebug from 'debug';
8
8
  import fg from 'fast-glob';
9
+ import { buildShadowWarning } from '../utils/detectImportedTypeShadowing';
9
10
 
10
11
  const debug = createDebug('auto:server-implementer:slice');
11
12
  const debugHandler = createDebug('auto:server-implementer:slice:handler');
@@ -259,6 +260,7 @@ function buildRetryPrompt(targetFile: string, context: Record<string, string>, p
259
260
  const sliceFiles = Object.entries(context).filter(
260
261
  ([name]) => name !== targetFile && name !== 'domain-shared-types.ts',
261
262
  );
263
+ const shadowWarning = buildShadowWarning(context[targetFile], targetFile);
262
264
 
263
265
  return `
264
266
  ${SYSTEM_PROMPT}
@@ -267,6 +269,7 @@ ${SYSTEM_PROMPT}
267
269
  The previous implementation needs adjustment based on this feedback:
268
270
 
269
271
  ${previousOutputs}
272
+ ${shadowWarning.length > 0 ? `\n🚨 ${shadowWarning}\n` : ''}
270
273
 
271
274
  📄 File to update: ${targetFile}
272
275
 
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildShadowWarning, detectImportedTypeShadowing } from './detectImportedTypeShadowing';
3
+
4
+ describe('detectImportedTypeShadowing', () => {
5
+ it('returns empty array when no shadows exist', () => {
6
+ const code = `
7
+ import type { MyState } from './state';
8
+ const x = 1;
9
+ `;
10
+ expect(detectImportedTypeShadowing(code)).toEqual([]);
11
+ });
12
+
13
+ it('detects import type shadowed by local type alias', () => {
14
+ const code = `
15
+ import type { MyState } from './state';
16
+ type MyState = { name: string };
17
+ `;
18
+ expect(detectImportedTypeShadowing(code)).toEqual(['MyState']);
19
+ });
20
+
21
+ it('detects import type shadowed by local interface', () => {
22
+ const code = `
23
+ import type { MyState } from './state';
24
+ interface MyState { name: string }
25
+ `;
26
+ expect(detectImportedTypeShadowing(code)).toEqual(['MyState']);
27
+ });
28
+
29
+ it('does not flag interface with different name extending imported type', () => {
30
+ const code = `
31
+ import type { MyState } from './state';
32
+ interface InternalMyState extends MyState { extra: number }
33
+ `;
34
+ expect(detectImportedTypeShadowing(code)).toEqual([]);
35
+ });
36
+
37
+ it('returns only shadowed names when multiple types imported', () => {
38
+ const code = `
39
+ import type { Alpha, Beta, Gamma } from './types';
40
+ type Beta = { value: number };
41
+ `;
42
+ expect(detectImportedTypeShadowing(code)).toEqual(['Beta']);
43
+ });
44
+
45
+ it('handles multi-line imports', () => {
46
+ const code = `
47
+ import type {
48
+ Alpha,
49
+ Beta,
50
+ } from './types';
51
+ type Alpha = { x: number };
52
+ `;
53
+ expect(detectImportedTypeShadowing(code)).toEqual(['Alpha']);
54
+ });
55
+
56
+ it('uses local name when import has alias', () => {
57
+ const code = `
58
+ import type { Original as Renamed } from './types';
59
+ type Renamed = { x: number };
60
+ `;
61
+ expect(detectImportedTypeShadowing(code)).toEqual(['Renamed']);
62
+ });
63
+
64
+ it('collects only per-specifier type imports from mixed import', () => {
65
+ const code = `
66
+ import { type TypeOnly, ValueImport } from './module';
67
+ type TypeOnly = { x: number };
68
+ type ValueImport = { y: number };
69
+ `;
70
+ expect(detectImportedTypeShadowing(code)).toEqual(['TypeOnly']);
71
+ });
72
+
73
+ it('ignores type names inside comments', () => {
74
+ const code = `
75
+ import type { MyState } from './state';
76
+ // type MyState = { fake: true };
77
+ /* type MyState = { also: false }; */
78
+ `;
79
+ expect(detectImportedTypeShadowing(code)).toEqual([]);
80
+ });
81
+
82
+ it('ignores type names inside string literals', () => {
83
+ const code = `
84
+ import type { MyState } from './state';
85
+ const s = "type MyState = { name: string }";
86
+ `;
87
+ expect(detectImportedTypeShadowing(code)).toEqual([]);
88
+ });
89
+
90
+ it('returns empty array when there are no imports', () => {
91
+ const code = `
92
+ type LocalOnly = { x: number };
93
+ interface AnotherLocal { y: string }
94
+ `;
95
+ expect(detectImportedTypeShadowing(code)).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe('buildShadowWarning', () => {
100
+ it('returns empty string for clean code', () => {
101
+ const code = `
102
+ import type { MyState } from './state';
103
+ const x = 1;
104
+ `;
105
+ expect(buildShadowWarning(code)).toBe('');
106
+ });
107
+
108
+ it('returns contract-violation string for shadowed code', () => {
109
+ const code = `
110
+ import type { MyState } from './state';
111
+ type MyState = { name: string };
112
+ `;
113
+ const result = buildShadowWarning(code, 'projection.ts');
114
+ expect(result).toBe(
115
+ 'CONTRACT VIOLATION in projection.ts: Types [MyState] are imported but redefined locally. Remove local definitions and use the imports.',
116
+ );
117
+ });
118
+
119
+ it('returns contract-violation without file path when not provided', () => {
120
+ const code = `
121
+ import type { MyState } from './state';
122
+ type MyState = { name: string };
123
+ `;
124
+ const result = buildShadowWarning(code);
125
+ expect(result).toBe(
126
+ 'CONTRACT VIOLATION: Types [MyState] are imported but redefined locally. Remove local definitions and use the imports.',
127
+ );
128
+ });
129
+ });
@@ -0,0 +1,47 @@
1
+ import ts from 'typescript';
2
+
3
+ export function detectImportedTypeShadowing(code: string): string[] {
4
+ const sourceFile = ts.createSourceFile('check.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
5
+
6
+ const importedTypeNames = new Set<string>();
7
+ const localTypeNames = new Set<string>();
8
+
9
+ for (const statement of sourceFile.statements) {
10
+ if (ts.isImportDeclaration(statement)) {
11
+ const clause = statement.importClause;
12
+ if (clause === undefined) continue;
13
+ const namedBindings = clause.namedBindings;
14
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings)) continue;
15
+ const isWholeImportTypeOnly = clause.isTypeOnly;
16
+ for (const element of namedBindings.elements) {
17
+ if (isWholeImportTypeOnly || element.isTypeOnly) {
18
+ const localName = element.name.text;
19
+ importedTypeNames.add(localName);
20
+ }
21
+ }
22
+ }
23
+
24
+ if (ts.isTypeAliasDeclaration(statement)) {
25
+ localTypeNames.add(statement.name.text);
26
+ }
27
+
28
+ if (ts.isInterfaceDeclaration(statement)) {
29
+ localTypeNames.add(statement.name.text);
30
+ }
31
+ }
32
+
33
+ const shadows: string[] = [];
34
+ for (const name of importedTypeNames) {
35
+ if (localTypeNames.has(name)) {
36
+ shadows.push(name);
37
+ }
38
+ }
39
+ return shadows;
40
+ }
41
+
42
+ export function buildShadowWarning(code: string, filePath?: string): string {
43
+ const shadows = detectImportedTypeShadowing(code);
44
+ if (shadows.length === 0) return '';
45
+ const location = filePath !== undefined ? ` in ${filePath}` : '';
46
+ return `CONTRACT VIOLATION${location}: Types [${shadows.join(', ')}] are imported but redefined locally. Remove local definitions and use the imports.`;
47
+ }