@auto-engineer/server-implementer 1.119.0 → 1.120.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 (49) 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 +29 -0
  5. package/dist/src/agent/runSlice.d.ts.map +1 -1
  6. package/dist/src/agent/runSlice.js +4 -25
  7. package/dist/src/agent/runSlice.js.map +1 -1
  8. package/dist/src/commands/implement-slice.d.ts.map +1 -1
  9. package/dist/src/commands/implement-slice.js +8 -124
  10. package/dist/src/commands/implement-slice.js.map +1 -1
  11. package/dist/src/index.d.ts.map +1 -1
  12. package/dist/src/prompts/systemPrompt.d.ts +1 -1
  13. package/dist/src/prompts/systemPrompt.d.ts.map +1 -1
  14. package/dist/src/prompts/systemPrompt.js +82 -46
  15. package/dist/src/prompts/systemPrompt.js.map +1 -1
  16. package/dist/src/utils/buildContextSections.d.ts +4 -0
  17. package/dist/src/utils/buildContextSections.d.ts.map +1 -0
  18. package/dist/src/utils/buildContextSections.js +21 -0
  19. package/dist/src/utils/buildContextSections.js.map +1 -0
  20. package/dist/src/utils/buildContextSections.specs.d.ts +2 -0
  21. package/dist/src/utils/buildContextSections.specs.d.ts.map +1 -0
  22. package/dist/src/utils/buildContextSections.specs.js +87 -0
  23. package/dist/src/utils/buildContextSections.specs.js.map +1 -0
  24. package/dist/src/utils/loadContextFiles.d.ts +2 -0
  25. package/dist/src/utils/loadContextFiles.d.ts.map +1 -0
  26. package/dist/src/utils/loadContextFiles.js +15 -0
  27. package/dist/src/utils/loadContextFiles.js.map +1 -0
  28. package/dist/src/utils/loadContextFiles.specs.d.ts +2 -0
  29. package/dist/src/utils/loadContextFiles.specs.d.ts.map +1 -0
  30. package/dist/src/utils/loadContextFiles.specs.js +44 -0
  31. package/dist/src/utils/loadContextFiles.specs.js.map +1 -0
  32. package/dist/src/utils/loadSharedContext.d.ts +1 -1
  33. package/dist/src/utils/loadSharedContext.d.ts.map +1 -1
  34. package/dist/src/utils/loadSharedContext.js +5 -5
  35. package/dist/src/utils/loadSharedContext.js.map +1 -1
  36. package/dist/src/utils/loadSharedContext.specs.js +12 -7
  37. package/dist/src/utils/loadSharedContext.specs.js.map +1 -1
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/ketchup-plan.md +7 -2
  40. package/package.json +4 -4
  41. package/src/agent/runSlice.ts +4 -28
  42. package/src/commands/implement-slice.ts +8 -142
  43. package/src/prompts/systemPrompt.ts +82 -46
  44. package/src/utils/buildContextSections.specs.ts +109 -0
  45. package/src/utils/buildContextSections.ts +30 -0
  46. package/src/utils/loadContextFiles.specs.ts +54 -0
  47. package/src/utils/loadContextFiles.ts +15 -0
  48. package/src/utils/loadSharedContext.specs.ts +12 -9
  49. package/src/utils/loadSharedContext.ts +6 -6
package/ketchup-plan.md CHANGED
@@ -1,4 +1,4 @@
1
- # Ketchup Plan: Fix implementer context/prompt gaps
1
+ # Ketchup Plan: Fix Server-Implementer Prompt Engineering
2
2
 
3
3
  ## TODO
4
4
 
@@ -6,10 +6,15 @@
6
6
 
7
7
  ## DONE
8
8
 
9
+ - [x] Burst 6: Update runSlice.ts to use shared utilities (782660f2)
10
+ - [x] Burst 5: Update implement-slice.ts to use shared utilities (b31e9c45)
11
+ - [x] Burst 4: Rewrite systemPrompt.ts with emmett patterns and import rules (e619ec01)
12
+ - [x] Burst 3: Extract shared buildContextSections utility + tests (ad51e08f)
13
+ - [x] Burst 2: Extract shared loadContextFiles utility + tests (987b9f8b)
14
+ - [x] Burst 1: Change loadSharedContext return type to Record<string, string> + update tests (09ebc833)
9
15
  - [x] Burst 10: Add strict array/object typing rules to implementer prompts
10
16
  - [x] Burst 9: Add discriminated union narrowing guidance to prompts (f43a84c9)
11
17
  - [x] Burst 8: Load full shared directory into implementer context (e8f42abf)
12
-
13
18
  - [x] Burst 7: Add implementer prompt guardrails for phantom enums and hardcoded projections (14d8cbda)
14
19
  - [x] Burst 6: Fix `_state` reference in decide.ts.ejs instruction comment (dff8f329)
15
20
  - [x] Burst 5: Fix singleton projection instructions in projection.ts.ejs (3271cfe4)
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.119.0",
22
- "@auto-engineer/message-bus": "1.119.0"
21
+ "@auto-engineer/model-factory": "1.120.0",
22
+ "@auto-engineer/message-bus": "1.120.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.119.0"
32
+ "@auto-engineer/cli": "1.120.0"
33
33
  },
34
- "version": "1.119.0",
34
+ "version": "1.120.0",
35
35
  "scripts": {
36
36
  "build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
37
37
  "test": "vitest run --reporter=dot",
@@ -5,9 +5,10 @@ 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 { buildContextSections } from '../utils/buildContextSections';
8
9
  import { buildShadowWarning } from '../utils/detectImportedTypeShadowing';
9
10
  import { extractCodeBlock } from '../utils/extractCodeBlock';
10
- import { loadSharedContext } from '../utils/loadSharedContext';
11
+ import { loadContextFiles } from '../utils/loadContextFiles';
11
12
  import { runTests } from './runTests';
12
13
 
13
14
  type TestAndTypecheckResult = {
@@ -102,22 +103,6 @@ async function retryFailedFiles(
102
103
  return result;
103
104
  }
104
105
 
105
- async function loadContextFiles(sliceDir: string): Promise<Record<string, string>> {
106
- const files = await fg(['*.ts'], { cwd: sliceDir });
107
- const context: Record<string, string> = {};
108
- for (const file of files) {
109
- const absPath = path.join(sliceDir, file);
110
- context[file] = await readFile(absPath, 'utf-8');
111
- }
112
-
113
- const sharedContent = await loadSharedContext(sliceDir);
114
- if (sharedContent) {
115
- context['domain-shared-types.ts'] = sharedContent;
116
- }
117
-
118
- return context;
119
- }
120
-
121
106
  function findFilesToImplement(contextFiles: Record<string, string>) {
122
107
  return Object.entries(contextFiles).filter(
123
108
  ([, content]) => content.includes('TODO:') || content.includes('IMPLEMENTATION INSTRUCTIONS'),
@@ -133,12 +118,7 @@ ${SYSTEM_PROMPT}
133
118
 
134
119
  ${context[targetFile]}
135
120
 
136
- ---
137
- 🧠 Other files in the same slice:
138
- ${Object.entries(context)
139
- .filter(([name]) => name !== targetFile)
140
- .map(([name, content]) => `// File: ${name}\n${content}`)
141
- .join('\n\n')}
121
+ ${buildContextSections(targetFile, context)}
142
122
 
143
123
  ---
144
124
  Return only the whole updated file of ${targetFile}. Do not remove existing imports or types that are still referenced or required in the file. The file returned has to be production ready.
@@ -161,11 +141,7 @@ The previous implementation of ${targetFile} caused test or type-check failures.
161
141
 
162
142
  ${context[targetFile]}
163
143
 
164
- 🧠 Other files in the same slice:
165
- ${Object.entries(context)
166
- .filter(([name]) => name !== targetFile)
167
- .map(([name, content]) => `// File: ${name}\n${content}`)
168
- .join('\n\n')}
144
+ ${buildContextSections(targetFile, context)}
169
145
 
170
146
  🧪 Test errors:
171
147
  ${testErrors || 'None'}
@@ -1,13 +1,14 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { readFile, writeFile } from 'node:fs/promises';
2
+ import { writeFile } 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
5
  import { createModelFromEnv } from '@auto-engineer/model-factory';
6
6
  import { generateText } from 'ai';
7
7
  import createDebug from 'debug';
8
- import fg from 'fast-glob';
8
+ import { SYSTEM_PROMPT } from '../prompts/systemPrompt';
9
+ import { buildContextSections } from '../utils/buildContextSections';
9
10
  import { buildShadowWarning } from '../utils/detectImportedTypeShadowing';
10
- import { loadSharedContext } from '../utils/loadSharedContext';
11
+ import { loadContextFiles } from '../utils/loadContextFiles';
11
12
 
12
13
  const debug = createDebug('auto:server-implementer:slice');
13
14
  const debugHandler = createDebug('auto:server-implementer:slice:handler');
@@ -102,22 +103,6 @@ function extractCodeBlock(text: string): string {
102
103
  .trim();
103
104
  }
104
105
 
105
- async function loadContextFiles(sliceDir: string): Promise<Record<string, string>> {
106
- const files = await fg(['*.ts', '!*.specs.ts'], { cwd: sliceDir });
107
- const context: Record<string, string> = {};
108
- for (const file of files) {
109
- const absPath = path.join(sliceDir, file);
110
- context[file] = await readFile(absPath, 'utf-8');
111
- }
112
-
113
- const sharedContent = await loadSharedContext(sliceDir);
114
- if (sharedContent) {
115
- context['domain-shared-types.ts'] = sharedContent;
116
- }
117
-
118
- return context;
119
- }
120
-
121
106
  const IMPLEMENTATION_MARKER = '// @auto-implement';
122
107
 
123
108
  function hasImplementationMarker(content: string): boolean {
@@ -141,96 +126,7 @@ function findFilesToImplement(contextFiles: Record<string, string>): Array<[stri
141
126
  return Object.entries(contextFiles).filter(([, content]) => hasImplementationMarker(content));
142
127
  }
143
128
 
144
- const SYSTEM_PROMPT = `
145
- You are a software engineer implementing missing logic in a sliced event-driven TypeScript server. Each slice contains partially scaffolded code, and your task is to complete the logic following implementation instructions embedded in each file.
146
-
147
- Project Characteristics:
148
- - Architecture: sliced event-sourced CQRS (Command, Query, Reaction slices)
149
- - Language: TypeScript with type-graphql and @event-driven-io/emmett
150
- - Each slice has scaffolded files with implementation instructions clearly marked with comments (e.g., '## IMPLEMENTATION INSTRUCTIONS ##') or TODOs.
151
- - Tests (e.g., *.specs.ts) must pass.
152
- - Type errors are not allowed.
153
- - The domain uses shared enums defined in domain/shared/types.ts for type-safe values. When a field type is an enum (e.g., Status), you MUST use enum constants (e.g., Status.IN_PROGRESS) instead of string literals (e.g., 'in_progress').
154
-
155
- Your Goal:
156
- - Read the implementation instructions from the provided file.
157
- - Generate only the code needed to fulfill the instructions, nothing extra and provide back the whole file without the instructions.
158
- - Maintain immutability and adhere to functional best practices.
159
- - Use only the types and domain constructs already present in the slice.
160
- - CRITICAL: When a field has an enum type (e.g., status: Status), you MUST use the enum constant (e.g., Status.IN_PROGRESS) NOT a string literal (e.g., 'in_progress'). Check domain-shared-types.ts for the exact enum constant names.
161
- - Do not remove existing imports or types that are still referenced or required in the file.
162
- - Preserve index signatures like [key: string]: unknown as they are required for TypeScript compatibility.
163
- - Return the entire updated file, not just the modified parts and remove any TODO comments or instructions after implementing the logic
164
-
165
- Key rules:
166
- - Never modify code outside the TODO or instruction areas.
167
- - Ensure the code is production-ready and type-safe.
168
- - Follow the slice type conventions:
169
- - **Command slice**: validate command, inspect state, emit events, never mutate state. Uses graphql mutations.
170
- - **Reaction slice**: respond to events with commands.
171
- - **Query slice**: maintain projections based on events, do not emit or throw. Uses graphql queries.
172
- - All code must be TypeScript compliant and follow functional patterns.
173
- - If a test exists, make it pass.
174
- - Keep implementations minimal and idiomatic.
175
- - CRITICAL: When assigning values to enum-typed fields, use the enum constant name from domain-shared-types.ts. For example, if Status enum has IN_PROGRESS = 'in_progress', use Status.IN_PROGRESS not 'in_progress'.
176
- - Derive all output values from inputs (event.data, command.data, state) or generate at runtime. Never reproduce literal values from test examples.
177
- - Always annotate array literals with explicit types: \`const items: SomeType[] = []\`, never \`const items = []\`.
178
- - When mapping objects to a typed array, include ALL required fields in each object literal. TypeScript requires every field — omitting one and assigning it later causes a type error.
179
-
180
- Avoid:
181
- - Adding new dependencies.
182
- - Refactoring unrelated code.
183
- - Changing the structure of already scaffolded files unless instructed.
184
- - Using string literals for enum-typed fields. ALWAYS use the enum constant from domain-shared-types.ts (e.g., if status field type is Status and Status enum defines IN_PROGRESS = 'in_progress', use Status.IN_PROGRESS not the string 'in_progress').
185
- - Importing enum types that do not appear in domain-shared-types.ts. If a field type is plain \`string\` (not an enum type), use a string literal.
186
-
187
- You will receive:
188
- - The path of the file to implement.
189
- - The current contents of the file, with instruction comments.
190
- - Other relevant files from the same slice (e.g., types, test, state, etc.).
191
- - Shared domain types including enum definitions (domain-shared-types.ts).
192
-
193
- You must:
194
- - Return the entire updated file (no commentary and remove all implementation instructions).
195
- - Ensure the output is valid TypeScript.
196
- - Use enum constants from domain-shared-types.ts when appropriate.
197
- `;
198
-
199
- function extractEnumExamples(sharedTypesContent: string): string {
200
- const enumMatches = sharedTypesContent.matchAll(/export enum (\w+) \{([^}]+)\}/g);
201
- const examples: string[] = [];
202
-
203
- for (const match of enumMatches) {
204
- const enumName = match[1];
205
- const enumBody = match[2];
206
- const firstConstant = enumBody.match(/\s*(\w+)\s*=\s*['"]([^'"]+)['"]/);
207
-
208
- if (firstConstant !== null) {
209
- const constantName = firstConstant[1];
210
- const constantValue = firstConstant[2];
211
- examples.push(` - ${enumName}.${constantName} (NOT '${constantValue}')`);
212
- }
213
- }
214
-
215
- if (examples.length === 0) {
216
- return '';
217
- }
218
-
219
- return `
220
- 📌 CRITICAL: Enum Usage Examples from Your Context:
221
- ${examples.join('\n')}
222
-
223
- Pattern: Always use EnumName.CONSTANT_NAME for enum-typed fields.
224
- Never use string literals like 'pending' or 'Pending' when an enum constant exists.
225
- `;
226
- }
227
-
228
129
  function buildInitialPrompt(targetFile: string, context: Record<string, string>): string {
229
- const sharedTypes = context['domain-shared-types.ts'];
230
- const sliceFiles = Object.entries(context).filter(
231
- ([name]) => name !== targetFile && name !== 'domain-shared-types.ts',
232
- );
233
-
234
130
  return `
235
131
  ${SYSTEM_PROMPT}
236
132
 
@@ -239,31 +135,14 @@ ${SYSTEM_PROMPT}
239
135
 
240
136
  ${context[targetFile]}
241
137
 
242
- ${
243
- sharedTypes !== undefined
244
- ? `---
245
- 📦 Shared domain types (available via import from '../../../shared'):
246
- ${sharedTypes}
247
-
248
- ${extractEnumExamples(sharedTypes)}
249
- IMPORTANT: Use enum constants (e.g., Status.PENDING) instead of string literals (e.g., 'pending') when working with enum types.
250
-
251
- `
252
- : ''
253
- }---
254
- 🧠 Other files in the same slice:
255
- ${sliceFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n')}
138
+ ${buildContextSections(targetFile, context)}
256
139
 
257
140
  ---
258
- Return only the whole updated file of ${targetFile}. Do not remove existing imports or types that are still referenced or required in the file. The file returned has to be production ready. Remember to use enum constants from domain-shared-types.ts instead of string literals.
141
+ Return only the whole updated file of ${targetFile}. Do not remove existing imports or types that are still referenced or required in the file. The file returned has to be production ready.
259
142
  `.trim();
260
143
  }
261
144
 
262
145
  function buildRetryPrompt(targetFile: string, context: Record<string, string>, previousOutputs: string): string {
263
- const sharedTypes = context['domain-shared-types.ts'];
264
- const sliceFiles = Object.entries(context).filter(
265
- ([name]) => name !== targetFile && name !== 'domain-shared-types.ts',
266
- );
267
146
  const shadowWarning = buildShadowWarning(context[targetFile], targetFile);
268
147
 
269
148
  return `
@@ -279,23 +158,10 @@ ${shadowWarning.length > 0 ? `\n🚨 ${shadowWarning}\n` : ''}
279
158
 
280
159
  ${context[targetFile]}
281
160
 
282
- ${
283
- sharedTypes !== undefined
284
- ? `---
285
- 📦 Shared domain types (available via import from '../../../shared'):
286
- ${sharedTypes}
287
-
288
- ${extractEnumExamples(sharedTypes)}
289
- IMPORTANT: Use enum constants (e.g., Status.PENDING) instead of string literals (e.g., 'pending') when working with enum types.
290
-
291
- `
292
- : ''
293
- }---
294
- 🧠 Other files in the same slice:
295
- ${sliceFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n')}
161
+ ${buildContextSections(targetFile, context)}
296
162
 
297
163
  ---
298
- Return only the corrected full contents of ${targetFile}, no commentary, no markdown. Remember to use enum constants from domain-shared-types.ts instead of string literals.
164
+ Return only the corrected full contents of ${targetFile}, no commentary, no markdown.
299
165
  `.trim();
300
166
  }
301
167
 
@@ -1,48 +1,84 @@
1
1
  export const SYSTEM_PROMPT = `
2
- You are a software engineer implementing missing logic in a sliced event-driven TypeScript server. Each slice contains partially scaffolded code, and your task is to complete the logic following implementation instructions embedded in each file.
3
-
4
- Project Characteristics:
5
- - Architecture: sliced event-sourced CQRS (Command, Query, Reaction slices)
6
- - Language: TypeScript with type-graphql and Emmett
7
- - Each slice has scaffolded files with implementation instructions clearly marked with comments (e.g., '## IMPLEMENTATION INSTRUCTIONS ##') or TODOs.
8
- - Tests (e.g., *.specs.ts) must pass.
9
- - Type errors are not allowed.
10
-
11
- Your Goal:
12
- - Read the implementation instructions from the provided file.
13
- - Generate only the code needed to fulfill the instructions, nothing extra and provide back the whole file without the instructions.
14
- - Maintain immutability and adhere to functional best practices.
15
- - Use only the types and domain constructs already present in the slice.
16
- - Do not remove existing imports or types that are still referenced or required in the file.
17
- - Return the entire updated file, not just the modified parts and remove any TODO comments or instructions after implementing the logic
18
-
19
- Key rules:
20
- - Never modify code outside the TODO or instruction areas.
21
- - Ensure the code is production-ready and type-safe.
22
- - Follow the slice type conventions:
23
- - **Command slice**: validate command, inspect state, emit events, never mutate state. Uses graphql mutations.
24
- - **Reaction slice**: respond to events with commands.
25
- - **Query slice**: maintain projections based on events, do not emit or throw. Uses graphql queries.
26
- - All code must be TypeScript compliant and follow functional patterns.
27
- - If a test exists, make it pass.
28
- - Keep implementations minimal and idiomatic.
29
- - Derive all output values from inputs (event.data, command.data, state) or generate at runtime. Never reproduce literal values from test examples.
30
- - Always annotate array literals with explicit types: \`const items: SomeType[] = []\`, never \`const items = []\`.
31
- - When mapping objects to a typed array, include ALL required fields in each object literal. TypeScript requires every field — omitting one and assigning it later causes a type error.
32
-
33
- Avoid:
34
- - Adding new dependencies.
35
- - Refactoring unrelated code.
36
- - Changing the structure of already scaffolded files unless instructed.
37
- - Inventing or importing types (e.g. enums) not already present in the provided files. If a field type is \`string\`, use a string literal.
38
- - Hardcoding constant values (IDs, numbers, strings, arrays) that should be dynamically generated or computed from inputs.
39
-
40
- You will receive:
41
- - The path of the file to implement.
42
- - The current contents of the file, with instruction comments.
43
- - Other relevant files from the same slice (e.g., types, test, state, etc.).
44
-
45
- You must:
46
- - Return the entire updated file (no commentary and remove all implementation instructions).
47
- - Ensure the output is valid TypeScript.
2
+ ## 1. ROLE & CONTEXT
3
+
4
+ You are a software engineer implementing @auto-implement files in a sliced event-driven TypeScript server.
5
+
6
+ - Architecture: event-sourced CQRS with @event-driven-io/emmett
7
+ - Language: TypeScript with type-graphql
8
+ - Sliced structure: Command, Reaction, Query slices
9
+ - Each slice has scaffolded files with implementation instructions marked with comments or TODOs
10
+
11
+ ## 2. TASK
12
+
13
+ - Complete the logic in the target file following embedded instructions
14
+ - Return the entire updated file, production-ready
15
+ - Remove all TODO/instruction comments after implementing
16
+
17
+ ## 3. EMMETT FRAMEWORK PATTERNS
18
+
19
+ decide(command, state) returns only { type, data }:
20
+ CORRECT:
21
+ return { type: 'OrderPlaced', data: { orderId: command.data.id } };
22
+ WRONG framework adds kind/metadata internally:
23
+ return { type: 'OrderPlaced', data: {...}, kind: 'Event', metadata: {...} };
24
+
25
+ evolve(state, event) returns new state derived from event data:
26
+ CORRECT:
27
+ return { ...state, status: event.data.status, updatedAt: event.data.timestamp };
28
+
29
+ react(context) returns void queries eventStore, sends commands:
30
+ CORRECT:
31
+ const { state } = await eventStore.aggregateStream(streamId, { evolve, initialState });
32
+ await commandSender.send({ type: 'ProcessItem', kind: 'Command', data: {...} });
33
+
34
+ Projection evolve(document, event) returns updated document.
35
+
36
+ ## 4. IMPORT RULES
37
+
38
+ - The provided context files are the COMPLETE set of available modules.
39
+ If a module or type does not appear in any provided file, it does not exist.
40
+ - Only import from: sibling files (./), shared directory ('../../../shared'),
41
+ or packages already imported in the scaffolded code.
42
+ - Never create imports to modules not shown in the provided context.
43
+ - If you need a type that doesn't exist in any provided file,
44
+ use inline types or primitive values instead.
45
+
46
+ ## 5. TEST SPECIFICATIONS GUIDANCE
47
+
48
+ - .specs.ts files show expected behavior via Given/When/Then patterns
49
+ - given([events]) = initial state, when({input}) = trigger, then({output}) = expected result
50
+ - Match the exact field names, types, and value derivations in assertions
51
+ - Pay attention to which values come from state vs. command/event data
52
+ - If a spec queries eventStore before testing, the implementation must too
53
+ - Derive values from inputs — never hardcode test literals
54
+
55
+ ## 6. CONSTRAINTS
56
+
57
+ - Type safety: no type errors, annotate array literals with explicit types
58
+ - Enums: if shared types define enums, use constants (e.g., Status.ACTIVE) not string literals ('active')
59
+ - Immutability: functional patterns, never mutate state
60
+ - Include ALL required fields in object literals
61
+ - Preserve index signatures ([key: string]: unknown)
62
+ - Do not remove existing imports still referenced
63
+ - When mapping objects to a typed array, include ALL required fields in each object literal
64
+
65
+ ## 7. SLICE CONVENTIONS
66
+
67
+ - Command slice: validate command, inspect state, emit events, never mutate state. Uses graphql mutations.
68
+ - Reaction slice: respond to events with commands.
69
+ - Query slice: maintain projections based on events, do not emit or throw. Uses graphql queries.
70
+
71
+ ## 8. AVOID
72
+
73
+ - Adding dependencies
74
+ - Refactoring unrelated code
75
+ - Inventing types, enums, or modules not present in provided files
76
+ - String literals for enum-typed fields
77
+ - Hardcoding values that should be computed from inputs
78
+ - Reproducing literal values from test examples
79
+
80
+ ## 9. OUTPUT
81
+
82
+ Return only the entire updated file. No commentary, no markdown fences.
83
+ Ensure the output is valid TypeScript.
48
84
  `;
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildContextSections } from './buildContextSections';
3
+
4
+ describe('buildContextSections', () => {
5
+ it('separates shared files, slice files, and spec files into labeled sections', () => {
6
+ const context: Record<string, string> = {
7
+ 'decide.ts': 'export function decide() {}',
8
+ 'state.ts': 'export type State = {}',
9
+ 'decide.specs.ts': 'it("works", () => {})',
10
+ 'shared/types.ts': 'export type Foo = string;',
11
+ };
12
+
13
+ const result = buildContextSections('decide.ts', context);
14
+
15
+ expect(result).toBe(
16
+ [
17
+ '---',
18
+ "📦 Shared domain types (importable from '../../../shared'):",
19
+ '// File: shared/types.ts',
20
+ 'export type Foo = string;',
21
+ '',
22
+ '---',
23
+ '🧠 Other files in the same slice:',
24
+ '// File: state.ts',
25
+ 'export type State = {}',
26
+ '',
27
+ '---',
28
+ '🧪 Test specifications (READ-ONLY reference):',
29
+ '// File: decide.specs.ts',
30
+ 'it("works", () => {})',
31
+ ].join('\n'),
32
+ );
33
+ });
34
+
35
+ it('omits shared section when no shared files exist', () => {
36
+ const context: Record<string, string> = {
37
+ 'decide.ts': 'export function decide() {}',
38
+ 'state.ts': 'export type State = {}',
39
+ };
40
+
41
+ const result = buildContextSections('decide.ts', context);
42
+
43
+ expect(result).toBe(
44
+ ['---', '🧠 Other files in the same slice:', '// File: state.ts', 'export type State = {}'].join('\n'),
45
+ );
46
+ });
47
+
48
+ it('omits spec section when no spec files exist', () => {
49
+ const context: Record<string, string> = {
50
+ 'decide.ts': 'export function decide() {}',
51
+ 'evolve.ts': 'export function evolve() {}',
52
+ 'shared/types.ts': 'export type Foo = string;',
53
+ };
54
+
55
+ const result = buildContextSections('decide.ts', context);
56
+
57
+ expect(result).toBe(
58
+ [
59
+ '---',
60
+ "📦 Shared domain types (importable from '../../../shared'):",
61
+ '// File: shared/types.ts',
62
+ 'export type Foo = string;',
63
+ '',
64
+ '---',
65
+ '🧠 Other files in the same slice:',
66
+ '// File: evolve.ts',
67
+ 'export function evolve() {}',
68
+ ].join('\n'),
69
+ );
70
+ });
71
+
72
+ it('appends enum examples to shared section when provided', () => {
73
+ const context: Record<string, string> = {
74
+ 'decide.ts': 'export function decide() {}',
75
+ 'evolve.ts': 'export function evolve() {}',
76
+ 'shared/types.ts': 'export enum Status { ACTIVE = "active" }',
77
+ };
78
+
79
+ const result = buildContextSections('decide.ts', context, { enumExamples: 'Use Status.ACTIVE not "active"' });
80
+
81
+ expect(result).toBe(
82
+ [
83
+ '---',
84
+ "📦 Shared domain types (importable from '../../../shared'):",
85
+ '// File: shared/types.ts',
86
+ 'export enum Status { ACTIVE = "active" }',
87
+ '',
88
+ 'Use Status.ACTIVE not "active"',
89
+ '',
90
+ '---',
91
+ '🧠 Other files in the same slice:',
92
+ '// File: evolve.ts',
93
+ 'export function evolve() {}',
94
+ ].join('\n'),
95
+ );
96
+ });
97
+
98
+ it('excludes target file from slice files section', () => {
99
+ const context: Record<string, string> = {
100
+ 'decide.ts': 'export function decide() {}',
101
+ 'evolve.ts': 'export function evolve() {}',
102
+ };
103
+
104
+ const result = buildContextSections('decide.ts', context);
105
+
106
+ expect(result).not.toContain('decide.ts');
107
+ expect(result).toContain('// File: evolve.ts');
108
+ });
109
+ });
@@ -0,0 +1,30 @@
1
+ export function buildContextSections(
2
+ targetFile: string,
3
+ context: Record<string, string>,
4
+ options?: { enumExamples?: string },
5
+ ): string {
6
+ const sharedFiles = Object.entries(context).filter(([name]) => name.startsWith('shared/'));
7
+ const specs = Object.entries(context).filter(([name]) => name.endsWith('.specs.ts'));
8
+ const sliceFiles = Object.entries(context).filter(
9
+ ([name]) => name !== targetFile && !name.startsWith('shared/') && !name.endsWith('.specs.ts'),
10
+ );
11
+
12
+ let result = '';
13
+
14
+ if (sharedFiles.length > 0) {
15
+ result += `---\n📦 Shared domain types (importable from '../../../shared'):\n`;
16
+ result += sharedFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n');
17
+ if (options?.enumExamples) result += `\n\n${options.enumExamples}`;
18
+ result += '\n\n';
19
+ }
20
+
21
+ result += `---\n🧠 Other files in the same slice:\n`;
22
+ result += sliceFiles.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n');
23
+
24
+ if (specs.length > 0) {
25
+ result += `\n\n---\n🧪 Test specifications (READ-ONLY reference):\n`;
26
+ result += specs.map(([name, content]) => `// File: ${name}\n${content}`).join('\n\n');
27
+ }
28
+
29
+ return result;
30
+ }
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { loadContextFiles } from './loadContextFiles';
5
+
6
+ describe('loadContextFiles', () => {
7
+ let tmpDir: string;
8
+ let sliceDir: string;
9
+ let sharedDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tmpDir = await fs.mkdtemp(path.join(import.meta.dirname, '.tmp-'));
13
+ sharedDir = path.join(tmpDir, 'shared');
14
+ sliceDir = path.join(tmpDir, 'src', 'slices', 'some-slice');
15
+ await fs.mkdir(sliceDir, { recursive: true });
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await fs.rm(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ it('loads all .ts files from slice directory including specs', async () => {
23
+ await fs.writeFile(path.join(sliceDir, 'decide.ts'), 'export function decide() {}');
24
+ await fs.writeFile(path.join(sliceDir, 'state.ts'), 'export type State = {}');
25
+ await fs.writeFile(path.join(sliceDir, 'decide.specs.ts'), 'it("works", () => {})');
26
+
27
+ const result = await loadContextFiles(sliceDir);
28
+
29
+ expect(result).toEqual({
30
+ 'decide.ts': 'export function decide() {}',
31
+ 'state.ts': 'export type State = {}',
32
+ 'decide.specs.ts': 'it("works", () => {})',
33
+ });
34
+ });
35
+
36
+ it('integrates shared context files with real filenames', async () => {
37
+ await fs.mkdir(sharedDir, { recursive: true });
38
+ await fs.writeFile(path.join(sliceDir, 'decide.ts'), 'export function decide() {}');
39
+ await fs.writeFile(path.join(sharedDir, 'types.ts'), 'export type Foo = string;');
40
+
41
+ const result = await loadContextFiles(sliceDir);
42
+
43
+ expect(result).toEqual({
44
+ 'decide.ts': 'export function decide() {}',
45
+ 'shared/types.ts': 'export type Foo = string;',
46
+ });
47
+ });
48
+
49
+ it('returns empty record for empty slice directory', async () => {
50
+ const result = await loadContextFiles(sliceDir);
51
+
52
+ expect(result).toEqual({});
53
+ });
54
+ });
@@ -0,0 +1,15 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { loadSharedContext } from './loadSharedContext';
5
+
6
+ export async function loadContextFiles(sliceDir: string): Promise<Record<string, string>> {
7
+ const files = await fg(['*.ts'], { cwd: sliceDir });
8
+ const context: Record<string, string> = {};
9
+ for (const file of files) {
10
+ context[file] = await readFile(path.join(sliceDir, file), 'utf-8');
11
+ }
12
+ const sharedFiles = await loadSharedContext(sliceDir);
13
+ Object.assign(context, sharedFiles);
14
+ return context;
15
+ }