@auto-engineer/server-implementer 1.118.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.
- 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 +46 -0
- package/dist/src/agent/runSlice.d.ts.map +1 -1
- package/dist/src/agent/runSlice.js +4 -25
- package/dist/src/agent/runSlice.js.map +1 -1
- package/dist/src/commands/implement-slice.d.ts.map +1 -1
- package/dist/src/commands/implement-slice.js +8 -124
- package/dist/src/commands/implement-slice.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/prompts/systemPrompt.d.ts +1 -1
- package/dist/src/prompts/systemPrompt.d.ts.map +1 -1
- package/dist/src/prompts/systemPrompt.js +82 -46
- package/dist/src/prompts/systemPrompt.js.map +1 -1
- package/dist/src/utils/buildContextSections.d.ts +4 -0
- package/dist/src/utils/buildContextSections.d.ts.map +1 -0
- package/dist/src/utils/buildContextSections.js +21 -0
- package/dist/src/utils/buildContextSections.js.map +1 -0
- package/dist/src/utils/buildContextSections.specs.d.ts +2 -0
- package/dist/src/utils/buildContextSections.specs.d.ts.map +1 -0
- package/dist/src/utils/buildContextSections.specs.js +87 -0
- package/dist/src/utils/buildContextSections.specs.js.map +1 -0
- package/dist/src/utils/loadContextFiles.d.ts +2 -0
- package/dist/src/utils/loadContextFiles.d.ts.map +1 -0
- package/dist/src/utils/loadContextFiles.js +15 -0
- package/dist/src/utils/loadContextFiles.js.map +1 -0
- package/dist/src/utils/loadContextFiles.specs.d.ts +2 -0
- package/dist/src/utils/loadContextFiles.specs.d.ts.map +1 -0
- package/dist/src/utils/loadContextFiles.specs.js +44 -0
- package/dist/src/utils/loadContextFiles.specs.js.map +1 -0
- package/dist/src/utils/loadSharedContext.d.ts +1 -1
- package/dist/src/utils/loadSharedContext.d.ts.map +1 -1
- package/dist/src/utils/loadSharedContext.js +5 -5
- package/dist/src/utils/loadSharedContext.js.map +1 -1
- package/dist/src/utils/loadSharedContext.specs.js +12 -7
- package/dist/src/utils/loadSharedContext.specs.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +7 -2
- package/package.json +4 -4
- package/src/agent/runSlice.ts +4 -28
- package/src/commands/implement-slice.ts +8 -142
- package/src/prompts/systemPrompt.ts +82 -46
- package/src/utils/buildContextSections.specs.ts +109 -0
- package/src/utils/buildContextSections.ts +30 -0
- package/src/utils/loadContextFiles.specs.ts +54 -0
- package/src/utils/loadContextFiles.ts +15 -0
- package/src/utils/loadSharedContext.specs.ts +12 -9
- package/src/utils/loadSharedContext.ts +6 -6
package/ketchup-plan.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Ketchup Plan: Fix
|
|
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.
|
|
22
|
-
"@auto-engineer/message-bus": "1.
|
|
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.
|
|
32
|
+
"@auto-engineer/cli": "1.120.0"
|
|
33
33
|
},
|
|
34
|
-
"version": "1.
|
|
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",
|
package/src/agent/runSlice.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
8
|
+
import { SYSTEM_PROMPT } from '../prompts/systemPrompt';
|
|
9
|
+
import { buildContextSections } from '../utils/buildContextSections';
|
|
9
10
|
import { buildShadowWarning } from '../utils/detectImportedTypeShadowing';
|
|
10
|
-
import {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|