@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.
- package/assets/prompts/core/standard/v9/behavior.prompt.yaml +120 -0
- package/dist/ai/behavior-ai-service.d.ts +63 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -0
- package/dist/ai/behavior-ai-service.js +203 -0
- package/dist/ai/behavior-ai-service.js.map +1 -0
- package/dist/ai/index.d.ts +27 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +30 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +16 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -30
- package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +25 -9
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +141 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +62 -42
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +39 -7
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +54 -0
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +16 -0
- package/libs/instance-factories/communication/event-emitter.yaml +16 -12
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -35
- package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
- package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +32 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +211 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +86 -40
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +54 -8
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +166 -85
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TemplateContext } from '@specverse/types';
|
|
9
9
|
import { buildTransitionMap, isAutoField } from '@specverse/types/spec-rules';
|
|
10
|
+
import { generateBehaviorWithHelpers, type BehaviorContext, type BehaviorMetadata } from './behavior-generator.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Generate Prisma controller for a model
|
|
@@ -36,6 +37,9 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
36
37
|
const idType = idAttr?.type || 'UUID';
|
|
37
38
|
const needsIntParse = idType === 'Integer' || idType === 'Int' || idType === 'Number';
|
|
38
39
|
|
|
40
|
+
// Generate custom actions and collect AI behavior stubs
|
|
41
|
+
const customActions = generateCustomActions(controller, modelName, modelVar);
|
|
42
|
+
|
|
39
43
|
return `/**
|
|
40
44
|
* ${controllerName}
|
|
41
45
|
* Model-specific business logic for ${modelName}
|
|
@@ -43,7 +47,8 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
43
47
|
*/
|
|
44
48
|
|
|
45
49
|
import { PrismaClient } from '@prisma/client';
|
|
46
|
-
${hasEventPublishing(curedOps, controller) ? `import { eventBus
|
|
50
|
+
${hasEventPublishing(curedOps, controller) ? `import { eventBus } from '../events/eventBus.js';` : ''}
|
|
51
|
+
${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : ''}
|
|
47
52
|
|
|
48
53
|
const prisma = new PrismaClient();
|
|
49
54
|
|
|
@@ -62,13 +67,14 @@ export class ${controllerName} {
|
|
|
62
67
|
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ''}
|
|
63
68
|
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ''}
|
|
64
69
|
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ''}
|
|
65
|
-
${
|
|
70
|
+
${customActions.code}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// Export singleton instance
|
|
69
74
|
export const ${modelVar}Controller = new ${controllerName}();
|
|
70
75
|
export default ${modelVar}Controller;
|
|
71
76
|
`;
|
|
77
|
+
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -368,26 +374,59 @@ function generateDeleteMethod(model: any, modelName: string, modelVar: string, c
|
|
|
368
374
|
/**
|
|
369
375
|
* Generate custom actions defined in controller
|
|
370
376
|
*/
|
|
371
|
-
|
|
377
|
+
interface CustomActionsResult {
|
|
378
|
+
code: string;
|
|
379
|
+
unmatchedSteps: Array<{ step: string; functionName: string; operationName: string }>;
|
|
380
|
+
needsAiBehaviors: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function generateCustomActions(controller: any, modelName: string, modelVar: string): CustomActionsResult {
|
|
372
384
|
if (!controller.actions || Object.keys(controller.actions).length === 0) {
|
|
373
|
-
return '';
|
|
385
|
+
return { code: '', unmatchedSteps: [], needsAiBehaviors: false };
|
|
374
386
|
}
|
|
375
387
|
|
|
376
388
|
const actions: string[] = [];
|
|
389
|
+
const allUnmatchedSteps: Array<{ step: string; functionName: string; operationName: string }> = [];
|
|
377
390
|
|
|
378
391
|
Object.entries(controller.actions).forEach(([actionName, action]: [string, any]) => {
|
|
392
|
+
const behavior: BehaviorMetadata = {
|
|
393
|
+
preconditions: action.requires || action.preconditions || [],
|
|
394
|
+
steps: action.steps || [],
|
|
395
|
+
postconditions: action.ensures || action.postconditions || [],
|
|
396
|
+
sideEffects: action.publishes || action.events || [],
|
|
397
|
+
transactional: action.transactional,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const ctx: BehaviorContext = {
|
|
401
|
+
modelName,
|
|
402
|
+
serviceName: `${modelName}Controller`,
|
|
403
|
+
operationName: actionName,
|
|
404
|
+
prismaModel: modelVar,
|
|
405
|
+
parameterNames: Object.keys(action.parameters || {}),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const result = generateBehaviorWithHelpers(behavior, {}, ctx);
|
|
409
|
+
allUnmatchedSteps.push(...result.unmatchedSteps);
|
|
410
|
+
|
|
379
411
|
actions.push(`
|
|
380
412
|
/**
|
|
381
413
|
* ${actionName}
|
|
382
414
|
* ${action.description || ''}
|
|
383
415
|
*/
|
|
384
416
|
public async ${actionName}(${generateActionParams(action)}): Promise<any> {
|
|
385
|
-
|
|
386
|
-
throw new Error('${actionName} not implemented');
|
|
417
|
+
${result.body}
|
|
387
418
|
}`);
|
|
419
|
+
|
|
420
|
+
if (result.helperMethods.length > 0) {
|
|
421
|
+
actions.push(...result.helperMethods);
|
|
422
|
+
}
|
|
388
423
|
});
|
|
389
424
|
|
|
390
|
-
return
|
|
425
|
+
return {
|
|
426
|
+
code: actions.join('\n'),
|
|
427
|
+
unmatchedSteps: allUnmatchedSteps,
|
|
428
|
+
needsAiBehaviors: allUnmatchedSteps.length > 0,
|
|
429
|
+
};
|
|
391
430
|
}
|
|
392
431
|
|
|
393
432
|
/**
|
|
@@ -509,5 +548,12 @@ ${includes}
|
|
|
509
548
|
* Check if controller publishes events
|
|
510
549
|
*/
|
|
511
550
|
function hasEventPublishing(curedOps: any, controller: any): boolean {
|
|
512
|
-
|
|
551
|
+
if (controller.publishes && Array.isArray(controller.publishes) && controller.publishes.length > 0) return true;
|
|
552
|
+
// Check if any custom action publishes events
|
|
553
|
+
if (controller.actions) {
|
|
554
|
+
for (const action of Object.values(controller.actions) as any[]) {
|
|
555
|
+
if (action.publishes?.length > 0 || action.events?.length > 0 || action.sideEffects?.length > 0) return true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
513
559
|
}
|
|
@@ -18,6 +18,10 @@ export interface StepContext {
|
|
|
18
18
|
serviceName: string;
|
|
19
19
|
operationName: string;
|
|
20
20
|
stepNum: number;
|
|
21
|
+
/** Parameter names from the action (e.g., ['pollId', 'optionId']) */
|
|
22
|
+
parameterNames?: string[];
|
|
23
|
+
/** Variables already declared (to avoid redeclaration) */
|
|
24
|
+
declaredVars?: Set<string>;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
function toVar(name: string): string {
|
|
@@ -28,15 +32,81 @@ function toMethod(words: string): string {
|
|
|
28
32
|
return words.trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, c => c.toLowerCase());
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a value expression in a step to a TypeScript expression.
|
|
37
|
+
*
|
|
38
|
+
* Handles:
|
|
39
|
+
* - "current time", "now", "timestamp" → new Date().toISOString()
|
|
40
|
+
* - References to declared variables (e.g., "discount.amount") → discount.amount
|
|
41
|
+
* - References to operation parameters → parameter name
|
|
42
|
+
* - References to stepNResult → stepNResult
|
|
43
|
+
* - Numbers → numeric literal
|
|
44
|
+
* - Boolean literals → true/false
|
|
45
|
+
* - Everything else → quoted string literal
|
|
46
|
+
*/
|
|
47
|
+
function resolveValue(rawValue: string, ctx: StepContext): string {
|
|
48
|
+
const value = rawValue.trim().replace(/^['"]|['"]$/g, '');
|
|
49
|
+
|
|
50
|
+
// Time expressions
|
|
51
|
+
if (/^(current\s*time|now|timestamp)$/i.test(value)) {
|
|
52
|
+
return 'new Date().toISOString()';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Boolean literals
|
|
56
|
+
if (value === 'true' || value === 'false') return value;
|
|
57
|
+
|
|
58
|
+
// Numeric literal
|
|
59
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
60
|
+
|
|
61
|
+
const declared = ctx.declaredVars || new Set();
|
|
62
|
+
const params = ctx.parameterNames || [];
|
|
63
|
+
|
|
64
|
+
// Reference: "varName" or "varName.field" — check if root matches a declared var or parameter
|
|
65
|
+
const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
|
|
66
|
+
if (rootMatch) {
|
|
67
|
+
const root = rootMatch[1];
|
|
68
|
+
if (declared.has(root) || params.includes(root)) {
|
|
69
|
+
return value; // use as-is (e.g., "discount.amount")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Natural language reference: "calculated amount", "the discount" etc.
|
|
74
|
+
// Try to match to a declared var by tokenising
|
|
75
|
+
const tokens = value.toLowerCase().split(/\s+/);
|
|
76
|
+
for (const declaredName of [...declared, ...params]) {
|
|
77
|
+
const normalized = declaredName.toLowerCase();
|
|
78
|
+
if (tokens.includes(normalized)) return declaredName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// "calculated X" / "computed X" / "the X" — look for a stepNResult with
|
|
82
|
+
// matching field. Since we don't know the AI function's return shape at
|
|
83
|
+
// compile time, emit a typed access with a TODO comment the developer can fix.
|
|
84
|
+
const calcMatch = value.match(/^(?:the\s+|calculated\s+|computed\s+|resulting\s+)(\w+)/i);
|
|
85
|
+
if (calcMatch) {
|
|
86
|
+
const field = calcMatch[1];
|
|
87
|
+
// Find most recent stepNResult
|
|
88
|
+
const stepResults = [...declared].filter(v => /^step\d+Result$/.test(v)).sort().reverse();
|
|
89
|
+
if (stepResults.length > 0) {
|
|
90
|
+
return `(${stepResults[0]} as any).${field} /* TODO: verify field name */`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Looks like natural language (multiple words with spaces) — emit TODO
|
|
95
|
+
if (/\s/.test(value)) {
|
|
96
|
+
return `/* TODO: resolve "${value}" — no matching declared variable */ null`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fallback: quoted string literal
|
|
100
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
101
|
+
}
|
|
102
|
+
|
|
31
103
|
export const STEP_CONVENTIONS: StepConvention[] = [
|
|
32
104
|
// --- Validation ---
|
|
33
105
|
{
|
|
34
106
|
name: 'validate',
|
|
35
107
|
pattern: /^validate\s+(.+)/i,
|
|
36
108
|
generateCall: (m, ctx) => ` // Step ${ctx.stepNum}: Validate ${m[1]}
|
|
37
|
-
|
|
38
|
-
if (!validationResult.valid) {
|
|
39
|
-
throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
|
|
109
|
+
// TODO: Add validation logic for ${m[1]}
|
|
40
110
|
}`,
|
|
41
111
|
},
|
|
42
112
|
|
|
@@ -48,21 +118,9 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
48
118
|
const condition = m[1];
|
|
49
119
|
const methodName = toMethod('check ' + condition);
|
|
50
120
|
return ` // Step ${ctx.stepNum}: Check ${condition}
|
|
51
|
-
const checkResult = await this.${methodName}(params);
|
|
52
|
-
if (!checkResult) {
|
|
53
|
-
throw new Error('Check failed: ${condition}');
|
|
54
|
-
}`;
|
|
55
|
-
},
|
|
56
|
-
generateMethod: (m, ctx) => {
|
|
57
|
-
const condition = m[1];
|
|
58
|
-
const methodName = toMethod('check ' + condition);
|
|
59
|
-
return `
|
|
60
|
-
private async ${methodName}(params: any): Promise<boolean> {
|
|
61
121
|
// TODO: Implement check — ${condition}
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return true;
|
|
65
|
-
}`;
|
|
122
|
+
// const checkResult = ...
|
|
123
|
+
// if (!checkResult) throw new Error('Check failed: ${condition}');`;
|
|
66
124
|
},
|
|
67
125
|
},
|
|
68
126
|
|
|
@@ -73,22 +131,40 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
73
131
|
generateCall: (m, ctx) => {
|
|
74
132
|
const model = m[1];
|
|
75
133
|
const field = m[2];
|
|
134
|
+
const modelVar = toVar(model);
|
|
135
|
+
const params = ctx.parameterNames || [];
|
|
136
|
+
const declared = ctx.declaredVars || new Set();
|
|
137
|
+
|
|
138
|
+
// Find the ID parameter: modelId (e.g., pollId) or field directly
|
|
139
|
+
const idParam = field === 'id'
|
|
140
|
+
? (params.find(p => p === `${modelVar}Id`) || params.find(p => p === 'id') || `${modelVar}Id`)
|
|
141
|
+
: (params.find(p => p === field) || `params.${field}`);
|
|
142
|
+
|
|
143
|
+
// Skip if already declared by precondition
|
|
144
|
+
if (declared.has(modelVar)) {
|
|
145
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
|
|
146
|
+
}
|
|
147
|
+
declared.add(modelVar);
|
|
148
|
+
|
|
76
149
|
return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
|
|
77
|
-
const ${
|
|
78
|
-
if (!${toVar(model)}) {
|
|
79
|
-
throw new Error('${model} not found by ${field}');
|
|
80
|
-
}`;
|
|
150
|
+
const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { ${field}: ${idParam} } });`;
|
|
81
151
|
},
|
|
82
152
|
},
|
|
83
153
|
|
|
84
154
|
// --- Create ---
|
|
85
155
|
{
|
|
86
156
|
name: 'create',
|
|
87
|
-
pattern: /^create\s+(\w+)(?:\s+
|
|
157
|
+
pattern: /^create\s+(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
88
158
|
generateCall: (m, ctx) => {
|
|
89
159
|
const model = m[1];
|
|
160
|
+
const modelVar = toVar(model);
|
|
161
|
+
// Build data from parameter names if available
|
|
162
|
+
const paramNames = ctx.parameterNames || [];
|
|
163
|
+
const dataFields = paramNames.length > 0
|
|
164
|
+
? `{ ${paramNames.join(', ')} }`
|
|
165
|
+
: 'data';
|
|
90
166
|
return ` // Step ${ctx.stepNum}: Create ${model}
|
|
91
|
-
const ${
|
|
167
|
+
const ${modelVar} = await prisma.${modelVar}.create({ data: ${dataFields} });`;
|
|
92
168
|
},
|
|
93
169
|
},
|
|
94
170
|
|
|
@@ -99,12 +175,12 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
99
175
|
generateCall: (m, ctx) => {
|
|
100
176
|
const model = m[1];
|
|
101
177
|
const field = m[2];
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const val =
|
|
105
|
-
return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${
|
|
106
|
-
await
|
|
107
|
-
where: { id:
|
|
178
|
+
const rawValue = m[3];
|
|
179
|
+
const modelVar = toVar(model);
|
|
180
|
+
const val = resolveValue(rawValue, ctx);
|
|
181
|
+
return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${rawValue.trim()}
|
|
182
|
+
await prisma.${modelVar}.update({
|
|
183
|
+
where: { id: ${modelVar}.id },
|
|
108
184
|
data: { ${field}: ${val} },
|
|
109
185
|
});`;
|
|
110
186
|
},
|
|
@@ -116,9 +192,10 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
116
192
|
pattern: /^update\s+(\w+)(?:\s+(.+))?/i,
|
|
117
193
|
generateCall: (m, ctx) => {
|
|
118
194
|
const model = m[1];
|
|
195
|
+
const modelVar = toVar(model);
|
|
119
196
|
return ` // Step ${ctx.stepNum}: Update ${model}
|
|
120
|
-
await
|
|
121
|
-
where: { id:
|
|
197
|
+
await prisma.${modelVar}.update({
|
|
198
|
+
where: { id: ${modelVar}.id },
|
|
122
199
|
data: params,
|
|
123
200
|
});`;
|
|
124
201
|
},
|
|
@@ -130,8 +207,9 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
130
207
|
pattern: /^delete\s+(\w+)/i,
|
|
131
208
|
generateCall: (m, ctx) => {
|
|
132
209
|
const model = m[1];
|
|
210
|
+
const modelVar = toVar(model);
|
|
133
211
|
return ` // Step ${ctx.stepNum}: Delete ${model}
|
|
134
|
-
await
|
|
212
|
+
await prisma.${modelVar}.delete({ where: { id: ${modelVar}.id } });`;
|
|
135
213
|
},
|
|
136
214
|
},
|
|
137
215
|
|
|
@@ -142,11 +220,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
142
220
|
generateCall: (m, ctx) => {
|
|
143
221
|
const model = m[1];
|
|
144
222
|
const state = m[2];
|
|
223
|
+
const modelVar = toVar(model);
|
|
145
224
|
return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
where: { id: params.id },
|
|
225
|
+
if (${modelVar}.status === '${state}') throw new Error('${model} is already ${state}');
|
|
226
|
+
await prisma.${modelVar}.update({
|
|
227
|
+
where: { id: ${modelVar}.id },
|
|
150
228
|
data: { status: '${state}' },
|
|
151
229
|
});`;
|
|
152
230
|
},
|
|
@@ -158,12 +236,12 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
158
236
|
pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
|
|
159
237
|
generateCall: (m, ctx) => {
|
|
160
238
|
const field = m[1];
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const val =
|
|
164
|
-
return ` // Step ${ctx.stepNum}: Set ${field} to ${
|
|
165
|
-
await
|
|
166
|
-
where: { id:
|
|
239
|
+
const rawValue = m[2];
|
|
240
|
+
const modelVar = toVar(ctx.prismaModel);
|
|
241
|
+
const val = resolveValue(rawValue, ctx);
|
|
242
|
+
return ` // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
|
|
243
|
+
await prisma.${modelVar}.update({
|
|
244
|
+
where: { id: ${modelVar}.id },
|
|
167
245
|
data: { ${field}: ${val} },
|
|
168
246
|
});`;
|
|
169
247
|
},
|
|
@@ -176,10 +254,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
176
254
|
generateCall: (m, ctx) => {
|
|
177
255
|
const field = m[1];
|
|
178
256
|
const amount = m[2];
|
|
179
|
-
const
|
|
257
|
+
const modelVar = toVar(ctx.prismaModel);
|
|
258
|
+
const amountVal = /^\d+$/.test(amount) ? amount : amount;
|
|
180
259
|
return ` // Step ${ctx.stepNum}: Increment ${field} by ${amount}
|
|
181
|
-
await
|
|
182
|
-
where: { id:
|
|
260
|
+
await prisma.${modelVar}.update({
|
|
261
|
+
where: { id: ${modelVar}.id },
|
|
183
262
|
data: { ${field}: { increment: ${amountVal} } },
|
|
184
263
|
});`;
|
|
185
264
|
},
|
|
@@ -192,39 +271,19 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
192
271
|
generateCall: (m, ctx) => {
|
|
193
272
|
const field = m[1];
|
|
194
273
|
const amount = m[2];
|
|
195
|
-
const
|
|
274
|
+
const modelVar = toVar(ctx.prismaModel);
|
|
275
|
+
const amountVal = /^\d+$/.test(amount) ? amount : amount;
|
|
196
276
|
return ` // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
throw new Error('Cannot decrement ${field} below zero');
|
|
200
|
-
}
|
|
201
|
-
await this.prisma.${toVar(ctx.prismaModel)}.update({
|
|
202
|
-
where: { id: params.id },
|
|
277
|
+
await prisma.${modelVar}.update({
|
|
278
|
+
where: { id: ${modelVar}.id },
|
|
203
279
|
data: { ${field}: { decrement: ${amountVal} } },
|
|
204
280
|
});`;
|
|
205
281
|
},
|
|
206
282
|
},
|
|
207
283
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
pattern: /^calculate\s+(.+)/i,
|
|
212
|
-
generateCall: (m, ctx) => {
|
|
213
|
-
const metric = m[1];
|
|
214
|
-
const methodName = toMethod('calculate ' + metric);
|
|
215
|
-
return ` // Step ${ctx.stepNum}: Calculate ${metric}
|
|
216
|
-
const ${toVar(metric.replace(/\s+/g, ''))} = await this.${methodName}(params);`;
|
|
217
|
-
},
|
|
218
|
-
generateMethod: (m, ctx) => {
|
|
219
|
-
const metric = m[1];
|
|
220
|
-
const methodName = toMethod('calculate ' + metric);
|
|
221
|
-
return `
|
|
222
|
-
private async ${methodName}(params: any): Promise<number> {
|
|
223
|
-
// TODO: Implement calculation — ${metric}
|
|
224
|
-
return 0;
|
|
225
|
-
}`;
|
|
226
|
-
},
|
|
227
|
-
},
|
|
284
|
+
// NOTE: "calculate X" is intentionally NOT a convention — it falls through to AI
|
|
285
|
+
// behaviors because calculations are domain-specific pure functions that benefit
|
|
286
|
+
// from AI generation with the full input context.
|
|
228
287
|
|
|
229
288
|
// --- Send event ---
|
|
230
289
|
{
|
|
@@ -233,7 +292,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
233
292
|
generateCall: (m, ctx) => {
|
|
234
293
|
const event = m[1];
|
|
235
294
|
return ` // Step ${ctx.stepNum}: Emit ${event} event
|
|
236
|
-
|
|
295
|
+
await eventBus.publish('${event}', { ${toVar(ctx.prismaModel)}Id: ${toVar(ctx.prismaModel)}.id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() });`;
|
|
237
296
|
},
|
|
238
297
|
},
|
|
239
298
|
|
|
@@ -244,7 +303,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
244
303
|
generateCall: (m, ctx) => {
|
|
245
304
|
const type = m[1];
|
|
246
305
|
return ` // Step ${ctx.stepNum}: Send ${type} notification
|
|
247
|
-
|
|
306
|
+
await eventBus.publish('${type}Notification', { ${toVar(ctx.prismaModel)}Id: ${toVar(ctx.prismaModel)}.id, operation: '${ctx.operationName}' });`;
|
|
248
307
|
},
|
|
249
308
|
},
|
|
250
309
|
|
|
@@ -256,7 +315,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
256
315
|
const service = m[1];
|
|
257
316
|
const method = m[2];
|
|
258
317
|
return ` // Step ${ctx.stepNum}: Call ${service}.${method}
|
|
259
|
-
await
|
|
318
|
+
await ${toVar(service)}.${method}({ ${(ctx.parameterNames || []).join(', ')} });`;
|
|
260
319
|
},
|
|
261
320
|
},
|
|
262
321
|
|
|
@@ -280,30 +339,52 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
280
339
|
/**
|
|
281
340
|
* Match a step string against all conventions. Returns the generated call
|
|
282
341
|
* code and optionally a helper method to add to the class.
|
|
342
|
+
*
|
|
343
|
+
* For unmatched steps, the call is wired to a pure AI function that takes
|
|
344
|
+
* all currently-declared variables as a typed input object and returns
|
|
345
|
+
* a result that subsequent steps can use.
|
|
283
346
|
*/
|
|
284
347
|
export function matchStep(
|
|
285
348
|
step: string,
|
|
286
349
|
ctx: StepContext
|
|
287
|
-
): {
|
|
350
|
+
): {
|
|
351
|
+
call: string;
|
|
352
|
+
helperMethod?: string;
|
|
353
|
+
matched: boolean;
|
|
354
|
+
functionName?: string;
|
|
355
|
+
inputs?: string[];
|
|
356
|
+
resultVar?: string;
|
|
357
|
+
} {
|
|
288
358
|
for (const convention of STEP_CONVENTIONS) {
|
|
289
359
|
const match = step.match(convention.pattern);
|
|
290
360
|
if (match) {
|
|
291
361
|
return {
|
|
292
362
|
call: convention.generateCall(match, ctx),
|
|
293
363
|
helperMethod: convention.generateMethod?.(match, ctx),
|
|
364
|
+
matched: true,
|
|
294
365
|
};
|
|
295
366
|
}
|
|
296
367
|
}
|
|
297
368
|
|
|
298
|
-
// No match —
|
|
299
|
-
|
|
369
|
+
// No match — pure AI function call
|
|
370
|
+
// Inputs = all variables declared by previous steps + operation parameters
|
|
371
|
+
const functionName = toMethod(step);
|
|
372
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
373
|
+
const paramNames = ctx.parameterNames || [];
|
|
374
|
+
const inputs = [...paramNames, ...declared];
|
|
375
|
+
|
|
376
|
+
const resultVar = `step${ctx.stepNum}Result`;
|
|
377
|
+
const inputObj = inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}';
|
|
378
|
+
|
|
379
|
+
// Register the result variable so subsequent steps can reference it
|
|
380
|
+
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
381
|
+
|
|
300
382
|
return {
|
|
301
|
-
call: ` // Step ${ctx.stepNum}: ${step}
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}`,
|
|
383
|
+
call: ` // Step ${ctx.stepNum}: ${step} [AI-generated — pure function]
|
|
384
|
+
const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
|
|
385
|
+
matched: false,
|
|
386
|
+
functionName,
|
|
387
|
+
inputs,
|
|
388
|
+
resultVar,
|
|
308
389
|
};
|
|
309
390
|
}
|