@specverse/engines 4.1.13 → 4.1.14

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 (34) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +120 -0
  2. package/dist/ai/behavior-ai-service.d.ts +63 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -0
  4. package/dist/ai/behavior-ai-service.js +203 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -0
  6. package/dist/ai/index.d.ts +27 -0
  7. package/dist/ai/index.d.ts.map +1 -1
  8. package/dist/ai/index.js +30 -0
  9. package/dist/ai/index.js.map +1 -1
  10. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +16 -0
  11. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -30
  12. package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
  13. package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
  14. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +25 -9
  15. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
  16. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +141 -0
  17. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +62 -42
  18. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +39 -7
  19. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
  20. package/dist/realize/index.d.ts.map +1 -1
  21. package/dist/realize/index.js +54 -0
  22. package/dist/realize/index.js.map +1 -1
  23. package/libs/instance-factories/cli/templates/commander/command-generator.ts +16 -0
  24. package/libs/instance-factories/communication/event-emitter.yaml +16 -12
  25. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -35
  26. package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
  27. package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
  28. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +32 -11
  29. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
  30. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +211 -0
  31. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +86 -40
  32. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +54 -8
  33. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +166 -85
  34. package/package.json +1 -1
@@ -36,6 +36,22 @@ export default function generateFastifyRoutes(context: TemplateContext): string
36
36
  }
37
37
  }
38
38
 
39
+ // Add custom action endpoints
40
+ if (controller.actions) {
41
+ if (!endpoints) endpoints = [];
42
+ for (const [actionName, action] of Object.entries(controller.actions) as [string, any][]) {
43
+ const params = action.parameters || {};
44
+ const hasIdParam = Object.keys(params).some(p => p === 'id' || p === `${modelName?.charAt(0).toLowerCase()}${modelName?.slice(1)}Id`);
45
+ endpoints.push({
46
+ operation: actionName,
47
+ method: 'POST',
48
+ path: hasIdParam ? `/:id/${actionName}` : `/${actionName}`,
49
+ parameters: params,
50
+ description: action.description || `Custom action: ${actionName}`,
51
+ });
52
+ }
53
+ }
54
+
39
55
  // Generate route handlers for each endpoint
40
56
  if (!endpoints || endpoints.length === 0) {
41
57
  console.warn(`Warning: Controller ${controllerName} has no endpoints. Generating empty routes file.`);
@@ -137,7 +153,7 @@ function generateRouteHandler(
137
153
 
138
154
  const method = endpoint.method?.toLowerCase() || inferHttpMethod(operation);
139
155
  const path = inferPath(operation, endpoint); // Always call inferPath to handle path extraction
140
- const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType);
156
+ const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint);
141
157
 
142
158
  let route = `// ${operation} ${modelName}\n`;
143
159
  route += `fastify.${method}('${path}', {\n`;
@@ -181,7 +197,8 @@ function generateHandlerBody(
181
197
  modelName: string,
182
198
  handlerName: string,
183
199
  isModelController: boolean,
184
- implType: any
200
+ implType: any,
201
+ endpoint?: any
185
202
  ): string {
186
203
  const rawLowerModel = modelName?.toLowerCase() || 'item';
187
204
  // Avoid JavaScript reserved words as variable names
@@ -276,17 +293,23 @@ function generateHandlerBody(
276
293
  });
277
294
  }`;
278
295
 
279
- default:
280
- // For custom actions, generate a handler that calls the action method
296
+ default: {
297
+ // Custom action pass named parameters from body (and :id from URL if present)
298
+ const paramNames = endpoint?.parameters ? Object.keys(endpoint.parameters) : [];
299
+ const callArgs = paramNames.length > 0
300
+ ? paramNames.map(p => `body.${p}`).join(', ')
301
+ : 'body';
281
302
  return `try {
282
- const result = await handler.${operation}(request.body as any);
283
- return reply.send(result);
303
+ const body = (request.body || {}) as Record<string, any>;
304
+ const result = await handler.${operation}(${callArgs});
305
+ return reply.send(result || { success: true });
284
306
  } catch (error) {
285
307
  return reply.status(400).send({
286
308
  error: 'Failed to execute ${operation}',
287
309
  message: error instanceof Error ? error.message : String(error)
288
310
  });
289
311
  }`;
312
+ }
290
313
  }
291
314
  }
292
315
 
@@ -322,11 +345,9 @@ function inferHttpMethod(operation: string): string {
322
345
  * handles custom service operation paths locally.
323
346
  */
324
347
  function inferPath(operation: string, endpoint: any): string {
325
- // For custom actions, extract relative path from endpoint.path
326
- if (endpoint?.path && endpoint.serviceOperation?.type === 'custom') {
327
- const pathParts = endpoint.path.split('/').filter((p: string) => p);
328
- const lastPart = pathParts[pathParts.length - 1];
329
- return `/${lastPart}`;
348
+ // If endpoint has an explicit path, use it directly
349
+ if (endpoint?.path) {
350
+ return endpoint.path;
330
351
  }
331
352
 
332
353
  return sharedInferPath(operation);
@@ -24,6 +24,16 @@ export default function generateFastifyServer(context: TemplateContext): string
24
24
  return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
25
25
  }).join('\n');
26
26
 
27
+ // Check for events in spec
28
+ const specEvents = spec.events ? Object.keys(spec.events) : [];
29
+ const hasEvents = specEvents.length > 0 || modelNames.length > 0;
30
+
31
+ // Service imports
32
+ const servicesList = spec.services ? Object.keys(spec.services) : [];
33
+ const serviceImports = servicesList.map((name: string) =>
34
+ `import './${name.charAt(0).toLowerCase() + name.slice(1)}.js'; // Initialize ${name} event subscriptions`
35
+ );
36
+
27
37
  return `/**
28
38
  * Fastify Server
29
39
  * Generated from SpecVerse specification
@@ -32,6 +42,8 @@ export default function generateFastifyServer(context: TemplateContext): string
32
42
  import Fastify from 'fastify';
33
43
  import cors from '@fastify/cors';
34
44
  import { PrismaClient } from '@prisma/client';
45
+ ${hasEvents ? `import { eventBus } from './events/eventBus.js';
46
+ import { registerWebSocketBridge } from './events/websocket-bridge.js';` : ''}
35
47
 
36
48
  // Initialize Prisma
37
49
  export const prisma = new PrismaClient();
@@ -56,13 +68,18 @@ fastify.get('/api/spec', async () => embeddedSpec);
56
68
  fastify.get('/api/runtime/info', async () => ({
57
69
  controllers: ${JSON.stringify(modelNames.map((n: string) => `${n}Controller`))},
58
70
  models: ${JSON.stringify(modelNames)},
59
- events: [],
60
- services: []
71
+ events: ${JSON.stringify(specEvents)},
72
+ services: ${JSON.stringify(servicesList)}
61
73
  }));
62
74
 
75
+ ${hasEvents ? `// Event history endpoint
76
+ fastify.get('/api/events', async () => eventBus.getHistory());` : ''}
77
+
63
78
  // Register routes
64
79
  ${routeImports}
65
80
 
81
+ ${serviceImports.length > 0 ? `// Initialize services (registers event subscriptions)\n${serviceImports.join('\n')}` : ''}
82
+
66
83
  async function registerRoutes() {
67
84
  ${routeRegistrations}
68
85
  }
@@ -71,10 +88,14 @@ ${routeRegistrations}
71
88
  const start = async () => {
72
89
  try {
73
90
  await registerRoutes();
91
+ ${hasEvents ? ` // Register WebSocket bridge for real-time frontend events
92
+ await registerWebSocketBridge(fastify);` : ''}
74
93
  const port = parseInt(process.env.PORT || '3000');
75
94
  await fastify.listen({ port, host: '0.0.0.0' });
76
95
  console.log(\`Server running at http://localhost:\${port}\`);
77
96
  console.log(\`API endpoints: ${modelNames.map((n: string) => `/api/${n.toLowerCase()}s`).join(', ')}\`);
97
+ ${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
98
+ console.log(\`Events: ${specEvents.join(', ')}\`);` : ''}
78
99
  } catch (err) {
79
100
  fastify.log.error(err);
80
101
  process.exit(1);
@@ -0,0 +1,211 @@
1
+ /**
2
+ * AI Behaviors Generator
3
+ *
4
+ * Generates files for behavior steps that couldn't be matched
5
+ * by convention patterns. Tries to use the configured AI engine to
6
+ * generate real implementations; falls back to stubs if AI is unavailable.
7
+ *
8
+ * - Clearly separated from convention-generated code (.ai.ts suffix)
9
+ * - Each function header documents the spec step + generation method
10
+ * - Human reviewable and editable
11
+ * - Regeneratable via `specverse ai regenerate` (TODO)
12
+ * - Falls back to stub if AI unavailable or fails
13
+ *
14
+ * Level separation:
15
+ * L1/L2: Convention code → inline in controller (trusted, deterministic)
16
+ * L3: Quint guards → guards.ts (formally verified)
17
+ * L4: AI-generated → behaviors/*.ai.ts (needs human review)
18
+ */
19
+
20
+ import type { TemplateContext } from '@specverse/types';
21
+ import { matchStep, type StepContext } from './step-conventions.js';
22
+
23
+ /**
24
+ * Generate AI behaviors file for a controller's unmatched steps.
25
+ * Returns empty string if all steps matched conventions (no AI file needed).
26
+ *
27
+ * This is async — it calls the AI engine to generate function bodies.
28
+ */
29
+ export default async function generateAiBehaviors(context: TemplateContext): Promise<string> {
30
+ const { controller, model } = context;
31
+ if (!controller?.actions) return '';
32
+
33
+ const modelName = model?.name || controller.model || 'Model';
34
+ const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
35
+
36
+ // Find unmatched steps across all actions.
37
+ // We simulate step execution through matchStep() so declaredVars accumulates
38
+ // exactly as it does in the controller generator — that way the inputs array
39
+ // for each unmatched step matches what will actually be passed at runtime.
40
+ const unmatchedFunctions: Array<{
41
+ functionName: string;
42
+ step: string;
43
+ operationName: string;
44
+ parameterNames: string[];
45
+ inputs: string[];
46
+ }> = [];
47
+
48
+ for (const [actionName, action] of Object.entries(controller.actions) as [string, any][]) {
49
+ const steps = action.steps || [];
50
+ const parameterNames = Object.keys(action.parameters || {});
51
+ const preconditions = action.requires || action.preconditions || [];
52
+
53
+ // Simulate precondition variable declarations
54
+ const declaredVars = new Set<string>();
55
+ for (const pc of preconditions) {
56
+ const match = pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i);
57
+ if (match) {
58
+ const entity = match[1];
59
+ declaredVars.add(entity.charAt(0).toLowerCase() + entity.slice(1));
60
+ }
61
+ }
62
+
63
+ for (let i = 0; i < steps.length; i++) {
64
+ const step = steps[i];
65
+ if (typeof step !== 'string') continue;
66
+
67
+ const ctx: StepContext = {
68
+ modelName,
69
+ prismaModel: modelVar,
70
+ serviceName: `${modelName}Controller`,
71
+ operationName: actionName,
72
+ stepNum: i + 1,
73
+ parameterNames,
74
+ declaredVars,
75
+ };
76
+
77
+ const result = matchStep(step, ctx);
78
+ if (!result.matched && result.functionName) {
79
+ // Avoid duplicate function definitions, but capture the widest set of inputs
80
+ const existing = unmatchedFunctions.find(f => f.functionName === result.functionName);
81
+ if (!existing) {
82
+ unmatchedFunctions.push({
83
+ functionName: result.functionName,
84
+ step,
85
+ operationName: actionName,
86
+ parameterNames,
87
+ inputs: result.inputs || [],
88
+ });
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ if (unmatchedFunctions.length === 0) return '';
95
+
96
+ // Start a Claude session for this controller — all unmatched functions share
97
+ // the conversation context, so Claude builds understanding of the spec
98
+ // across calls (mirrors app-demo's session-based AI service)
99
+ let aiService: any = null;
100
+ try {
101
+ const { BehaviorAIService } = await import('@specverse/engines/ai');
102
+ aiService = new BehaviorAIService();
103
+ if (!aiService.isAvailable) {
104
+ aiService = null;
105
+ } else {
106
+ aiService.startSession(`${modelName}Controller`);
107
+ }
108
+ } catch {
109
+ aiService = null;
110
+ }
111
+
112
+ const availableModels = (context.spec?.models ? Object.keys(context.spec.models) : []) as string[];
113
+
114
+ // Generate function bodies sequentially — same Claude session for all
115
+ const functions: string[] = [];
116
+ for (const { functionName, step, operationName, parameterNames, inputs } of unmatchedFunctions) {
117
+ // Pure function signature: all inputs as a typed destructured object
118
+ const signature = inputs.length > 0
119
+ ? `input: { ${inputs.map(n => `${n}: any`).join('; ')} }`
120
+ : 'input: Record<string, never>';
121
+
122
+ // Inside the body, destructure for readability
123
+ const destructure = inputs.length > 0
124
+ ? ` const { ${inputs.join(', ')} } = input;`
125
+ : '';
126
+
127
+ let body: string | null = null;
128
+ let source = 'STUB';
129
+
130
+ if (aiService) {
131
+ try {
132
+ body = await aiService.generateBehavior({
133
+ step,
134
+ modelName,
135
+ operationName,
136
+ functionName,
137
+ parameterNames: inputs, // the actual inputs to the pure function
138
+ availableModels,
139
+ spec: context.spec,
140
+ });
141
+ if (body) source = 'AI-GENERATED';
142
+ } catch {
143
+ // Fall through to stub
144
+ }
145
+ }
146
+
147
+ if (!body) {
148
+ body = ` throw new Error('Not implemented: ${functionName} — see behaviors/${modelName}Controller.ai.ts');`;
149
+ } else {
150
+ // Indent body to match function scope
151
+ body = body.split('\n').map(line => line ? ' ' + line : line).join('\n');
152
+ }
153
+
154
+ const inputsDoc = inputs.length > 0
155
+ ? ` * Inputs: ${inputs.join(', ')}\n`
156
+ : '';
157
+
158
+ functions.push(`/**
159
+ * ${functionName}
160
+ *
161
+ * Spec step: "${step}"
162
+ * Called by: ${modelName}Controller.${operationName}()
163
+ ${inputsDoc} * Source: ${source}
164
+ * Generated: ${new Date().toISOString().split('T')[0]}
165
+ *
166
+ * PURE FUNCTION — no database access, no event publishing, no external services.
167
+ * All data comes in via \`input\`; all effects happen in the calling controller.
168
+ * ${source === 'AI-GENERATED'
169
+ ? 'AI-generated implementation. Review and test before deploying.'
170
+ : 'STUB — Claude CLI unavailable. Install Claude Code or implement manually.'}
171
+ */
172
+ export async function ${functionName}(${signature}): Promise<any> {
173
+ ${destructure ? destructure + '\n' : ''}${body}
174
+ }`);
175
+ }
176
+
177
+ // End the session
178
+ if (aiService?.endSession) aiService.endSession();
179
+
180
+ return `/**
181
+ * ${modelName}Controller — AI-Generated Behaviors
182
+ *
183
+ * ⚠️ THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
184
+ *
185
+ * These functions could not be generated from convention patterns.
186
+ * They are called by ${modelName}Controller when executing custom actions.
187
+ *
188
+ * Options for each function:
189
+ * - Implement manually (recommended for business-critical logic)
190
+ * - Use AI generation: specverse ai generate <function>
191
+ * - Refactor the spec step to use a convention pattern
192
+ *
193
+ * Convention patterns that ARE auto-generated (no AI needed):
194
+ * "Find {Model} by {field}" → prisma.model.findUniqueOrThrow(...)
195
+ * "Create {Model}" → prisma.model.create(...)
196
+ * "Update {Model} {field} to {value}" → prisma.model.update(...)
197
+ * "Delete {Model}" → prisma.model.delete(...)
198
+ * "Transition {Model} to {state}" → prisma.model.update({ status: ... })
199
+ * "Count {Model}s per {Group}" → prisma.model.groupBy(...)
200
+ * See step-conventions.ts for the full list.
201
+ *
202
+ * Generated: ${new Date().toISOString().split('T')[0]}
203
+ */
204
+
205
+ import { PrismaClient } from '@prisma/client';
206
+
207
+ const prisma = new PrismaClient();
208
+
209
+ ${functions.join('\n\n')}
210
+ `;
211
+ }
@@ -24,6 +24,8 @@ export interface BehaviorContext {
24
24
  serviceName: string;
25
25
  operationName: string;
26
26
  prismaModel?: string;
27
+ /** Parameter names from the action definition (e.g., ['pollId', 'optionId', 'voterName']) */
28
+ parameterNames?: string[];
27
29
  }
28
30
 
29
31
  export interface BehaviorMetadata {
@@ -43,6 +45,14 @@ export interface OperationMetadata {
43
45
  export interface BehaviorResult {
44
46
  body: string;
45
47
  helperMethods: string[];
48
+ /** Steps that couldn't be matched by conventions — need AI generation */
49
+ unmatchedSteps: Array<{
50
+ step: string;
51
+ functionName: string;
52
+ operationName: string;
53
+ /** Names of variables passed as the typed input object */
54
+ inputs: string[];
55
+ }>;
46
56
  }
47
57
 
48
58
  /**
@@ -71,15 +81,18 @@ export function generateBehaviorWithHelpers(
71
81
  const parts: string[] = [];
72
82
  const helperMethods: string[] = [];
73
83
 
84
+ // Track variables declared by preconditions so steps can reuse them
85
+ const preconditionDeclared = new Set<string>();
86
+
74
87
  // Precondition guards
75
88
  const preconditions = generatePreconditionChecks(
76
- behavior.preconditions || [], context
89
+ behavior.preconditions || [], context, preconditionDeclared
77
90
  );
78
91
  if (preconditions) parts.push(preconditions);
79
92
 
80
93
  // Main logic (from steps or inferred from operation semantics)
81
- const { code, helpers } = generateStepLogic(
82
- behavior.steps || [], context
94
+ const { code, helpers, unmatchedSteps } = generateStepLogic(
95
+ behavior.steps || [], context, preconditionDeclared
83
96
  );
84
97
  parts.push(code);
85
98
  helperMethods.push(...helpers);
@@ -92,7 +105,7 @@ export function generateBehaviorWithHelpers(
92
105
 
93
106
  // Event publishing
94
107
  const events = generateEventPublishing(
95
- behavior.sideEffects || [], context.operationName
108
+ behavior.sideEffects || [], context.operationName, context.parameterNames
96
109
  );
97
110
  if (events) parts.push(events);
98
111
 
@@ -103,39 +116,56 @@ export function generateBehaviorWithHelpers(
103
116
  body = generateTransactionWrapper(body, context);
104
117
  }
105
118
 
106
- return { body, helperMethods };
119
+ return { body, helperMethods, unmatchedSteps };
107
120
  }
108
121
 
109
122
  /**
110
123
  * Generate precondition checks from natural-language strings.
124
+ * Tracks declared variables to avoid duplicates and reuses fetched entities.
111
125
  */
112
126
  export function generatePreconditionChecks(
113
127
  preconditions: string[],
114
- context: BehaviorContext
128
+ context: BehaviorContext,
129
+ declared?: Set<string>
115
130
  ): string {
116
131
  if (preconditions.length === 0) return '';
117
132
 
118
- const checks = preconditions.map(pc => matchPreconditionPattern(pc, context));
133
+ if (!declared) declared = new Set<string>();
134
+ const checks = preconditions.map(pc => matchPreconditionPattern(pc, context, declared));
119
135
  return ` // === PRECONDITIONS ===\n${checks.join('\n')}`;
120
136
  }
121
137
 
138
+ /** Find the parameter name that refers to a model (e.g., 'pollId' for 'Poll') */
139
+ function findIdParam(modelName: string, paramNames: string[]): string {
140
+ const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
141
+ // Check for modelId (e.g., pollId)
142
+ const idParam = paramNames.find(p => p === `${modelVar}Id`);
143
+ if (idParam) return idParam;
144
+ // Check for just 'id'
145
+ if (paramNames.includes('id')) return 'id';
146
+ return `${modelVar}Id`;
147
+ }
148
+
122
149
  function matchPreconditionPattern(
123
150
  precondition: string,
124
- context: BehaviorContext
151
+ context: BehaviorContext,
152
+ declared: Set<string>
125
153
  ): string {
126
154
  const pc = precondition.toLowerCase();
127
- const prismaModel = context.prismaModel || context.modelName;
155
+ const params = context.parameterNames || [];
128
156
 
129
157
  // Pattern: "{Model} exists"
130
158
  const existsMatch = precondition.match(/^(\w+)\s+exists/i);
131
159
  if (existsMatch) {
132
160
  const entity = existsMatch[1];
133
161
  const entityVar = entity.charAt(0).toLowerCase() + entity.slice(1);
134
- return ` // Guard: ${precondition}
135
- const ${entityVar} = await prisma.${entityVar}.findUnique({ where: { id: params.id } });
136
- if (!${entityVar}) {
137
- throw new Error('Precondition failed: ${precondition}');
138
- }`;
162
+ const idParam = findIdParam(entity, params);
163
+ if (declared.has(entityVar)) {
164
+ // Already fetched — just check it
165
+ return ` // Guard: ${precondition} (already fetched)`;
166
+ }
167
+ declared.add(entityVar);
168
+ return ` const ${entityVar} = await prisma.${entityVar}.findUniqueOrThrow({ where: { id: ${idParam} } });`;
139
169
  }
140
170
 
141
171
  // Pattern: "{field} is not empty" / "{field} is required"
@@ -143,31 +173,22 @@ function matchPreconditionPattern(
143
173
  const fieldMatch = precondition.match(/^(\w+)\s+is/i);
144
174
  if (fieldMatch) {
145
175
  const field = fieldMatch[1];
146
- return ` // Guard: ${precondition}
147
- if (!params.${field}) {
148
- throw new Error('Precondition failed: ${precondition}');
149
- }`;
176
+ const paramRef = params.includes(field) ? field : `params.${field}`;
177
+ return ` if (!${paramRef}) throw new Error('${field} is required');`;
150
178
  }
151
179
  }
152
180
 
153
181
  // Pattern: "{field} is valid"
154
182
  if (pc.includes('is valid')) {
155
- return ` // Guard: ${precondition}
156
- const validation = this.validate(params, { operation: '${context.operationName}' });
157
- if (!validation.valid) {
158
- throw new Error('Precondition failed: ${precondition} — ' + validation.errors.join(', '));
159
- }`;
183
+ return ` // TODO: Implement validation: ${precondition}`;
160
184
  }
161
185
 
162
186
  // Pattern: "{X} matches {Y}" / "{X} equals {Y}"
163
187
  const matchesMatch = precondition.match(/^(\w+)\s+(?:matches|equals)\s+(.+)/i);
164
188
  if (matchesMatch) {
165
- const left = matchesMatch[1];
166
- const right = matchesMatch[2];
167
- return ` // Guard: ${precondition}
168
- if (params.${left.charAt(0).toLowerCase() + left.slice(1)} !== params.${right.charAt(0).toLowerCase() + right.slice(1)}) {
169
- throw new Error('Precondition failed: ${precondition}');
170
- }`;
189
+ const left = matchesMatch[1].charAt(0).toLowerCase() + matchesMatch[1].slice(1);
190
+ const right = matchesMatch[2].charAt(0).toLowerCase() + matchesMatch[2].slice(1);
191
+ return ` if (${left} !== ${right}) throw new Error('${matchesMatch[1]} must match ${matchesMatch[2]}');`;
171
192
  }
172
193
 
173
194
  // Pattern: "{Model} is {state}"
@@ -176,16 +197,19 @@ function matchPreconditionPattern(
176
197
  const model = stateMatch[1];
177
198
  const state = stateMatch[2];
178
199
  const modelVar = model.charAt(0).toLowerCase() + model.slice(1);
179
- return ` // Guard: ${precondition}
180
- const ${modelVar}State = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: params.id } });
181
- if (${modelVar}State.status !== '${state}') {
182
- throw new Error('Precondition failed: ${precondition} (current: ' + ${modelVar}State.status + ')');
183
- }`;
200
+ const idParam = findIdParam(model, params);
201
+
202
+ // If already declared, reuse; otherwise fetch
203
+ if (!declared.has(modelVar)) {
204
+ declared.add(modelVar);
205
+ return ` const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: ${idParam} } });
206
+ if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
207
+ }
208
+ return ` if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
184
209
  }
185
210
 
186
211
  // Unrecognized
187
- return ` // Guard: ${precondition}
188
- // TODO: Implement precondition check`;
212
+ return ` // TODO: Implement precondition: ${precondition}`;
189
213
  }
190
214
 
191
215
  /**
@@ -193,9 +217,14 @@ function matchPreconditionPattern(
193
217
  */
194
218
  function generateStepLogic(
195
219
  steps: string[],
196
- context: BehaviorContext
197
- ): { code: string; helpers: string[] } {
220
+ context: BehaviorContext,
221
+ preconditionDeclared?: Set<string>
222
+ ): { code: string; helpers: string[]; unmatchedSteps: Array<{ step: string; functionName: string; operationName: string; inputs: string[] }> } {
198
223
  const helpers: string[] = [];
224
+ const unmatchedSteps: Array<{ step: string; functionName: string; operationName: string; inputs: string[] }> = [];
225
+
226
+ // Shared declared variables across steps — carried through from preconditions
227
+ const declaredVars = preconditionDeclared || new Set<string>();
199
228
 
200
229
  if (steps && steps.length > 0) {
201
230
  const stepCode = steps.map((step, i) => {
@@ -209,18 +238,29 @@ function generateStepLogic(
209
238
  serviceName: context.serviceName,
210
239
  operationName: context.operationName,
211
240
  stepNum: i + 1,
241
+ parameterNames: context.parameterNames,
242
+ declaredVars,
212
243
  };
213
244
 
214
245
  const result = matchStep(step, ctx);
215
246
  if (result.helperMethod) {
216
247
  helpers.push(result.helperMethod);
217
248
  }
249
+ if (!result.matched && result.functionName) {
250
+ unmatchedSteps.push({
251
+ step,
252
+ functionName: result.functionName,
253
+ operationName: context.operationName,
254
+ inputs: result.inputs || [],
255
+ });
256
+ }
218
257
  return result.call;
219
258
  });
220
259
 
221
260
  return {
222
261
  code: ` // === EXECUTE ===\n${stepCode.join('\n\n')}`,
223
262
  helpers,
263
+ unmatchedSteps,
224
264
  };
225
265
  }
226
266
 
@@ -228,6 +268,7 @@ function generateStepLogic(
228
268
  return {
229
269
  code: ` // === EXECUTE ===\n${inferLogicFromOperationName(context)}`,
230
270
  helpers,
271
+ unmatchedSteps,
231
272
  };
232
273
  }
233
274
 
@@ -280,12 +321,17 @@ ${checks.join('\n')}
280
321
  */
281
322
  export function generateEventPublishing(
282
323
  sideEffects: string[],
283
- operationName: string
324
+ operationName: string,
325
+ parameterNames?: string[]
284
326
  ): string {
285
327
  if (!sideEffects || sideEffects.length === 0) return '';
286
328
 
329
+ const paramFields = parameterNames?.length
330
+ ? parameterNames.join(', ') + ', '
331
+ : '';
332
+
287
333
  const publishes = sideEffects.map(event =>
288
- ` this.emit('${event}', { operation: '${operationName}', timestamp: new Date().toISOString() });`
334
+ ` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() });`
289
335
  );
290
336
 
291
337
  return ` // === EVENTS ===\n${publishes.join('\n')}`;