@specverse/engines 6.6.3 → 6.11.2
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/dist/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +20 -0
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +72 -22
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/logical/generators/controller-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/controller-generator.js +26 -4
- package/dist/inference/logical/generators/controller-generator.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +26 -10
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +50 -15
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +27 -7
- package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +59 -23
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +319 -0
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +192 -28
- package/dist/parser/processors/ExecutableProcessor.d.ts.map +1 -1
- package/dist/parser/processors/ExecutableProcessor.js +14 -1
- package/dist/parser/processors/ExecutableProcessor.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +22 -3
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +48 -12
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +80 -21
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +49 -8
- package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-generator.test.ts +3 -1
- package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +82 -25
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +423 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +287 -38
- package/package.json +6 -6
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step Conventions for MongoDB native driver — mirrors the prisma library's
|
|
3
|
+
* pattern set but emits native-driver collection code instead of `prisma.X`.
|
|
4
|
+
*
|
|
5
|
+
* Each convention maps a natural-language step pattern to a generated code
|
|
6
|
+
* block. Patterns are tried in order; first match wins. Unmatched steps
|
|
7
|
+
* fall through to the AI-behaviors layer (a separate concern).
|
|
8
|
+
*
|
|
9
|
+
* The emitted code assumes the controller's surrounding scope provides:
|
|
10
|
+
* - `getCollection(name)` (from `../db/mongoClient.js`)
|
|
11
|
+
* - `byId(id)` filter helper for `_id`-shaped queries
|
|
12
|
+
* - `eventBus` for `publish`
|
|
13
|
+
* These are imported by the mongo-native controller-generator regardless,
|
|
14
|
+
* so step-emitted code compiles inline.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface MongoStepConvention {
|
|
18
|
+
name: string;
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
generateCall: (match: RegExpMatchArray, ctx: MongoStepContext) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MongoStepContext {
|
|
24
|
+
/** The model the action lives on (e.g. 'Player' for PlayerController). */
|
|
25
|
+
modelName: string;
|
|
26
|
+
/** Collection name for the model (lowercased + 's' or storage.collection). */
|
|
27
|
+
collectionName: string;
|
|
28
|
+
serviceName: string;
|
|
29
|
+
operationName: string;
|
|
30
|
+
stepNum: number;
|
|
31
|
+
/** Parameter names from the action (so we know what's in `args`). */
|
|
32
|
+
parameterNames?: string[];
|
|
33
|
+
/** Variables already declared (to avoid redeclaration). */
|
|
34
|
+
declaredVars?: Set<string>;
|
|
35
|
+
/** Named result variable from spec's `as:` clause. */
|
|
36
|
+
resultName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toVar(name: string): string {
|
|
40
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toCollection(modelName: string): string {
|
|
44
|
+
return modelName.toLowerCase() + 's';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Camel-case a step's text into a TS identifier. Identical algorithm to
|
|
48
|
+
* prisma's step-conventions `toMethod` so AI-behavior function names align
|
|
49
|
+
* across the orm-agnostic generator. */
|
|
50
|
+
function toMethod(words: string): string {
|
|
51
|
+
const cleaned = words.trim().replace(/[^A-Za-z0-9\s]+/g, ' ');
|
|
52
|
+
const camel = cleaned.replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toLowerCase());
|
|
53
|
+
const safe = camel.replace(/[^A-Za-z0-9_$]/g, '');
|
|
54
|
+
return safe || 'unnamedStep';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolve a value reference in step text to a TS expression. */
|
|
58
|
+
function resolveValue(rawValue: string, ctx: MongoStepContext): string {
|
|
59
|
+
const value = rawValue.trim().replace(/^['"]|['"]$/g, '');
|
|
60
|
+
if (/^(current\s*time|now|timestamp)$/i.test(value)) return 'new Date().toISOString()';
|
|
61
|
+
if (value === 'true' || value === 'false') return value;
|
|
62
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return value;
|
|
63
|
+
|
|
64
|
+
const declared = ctx.declaredVars || new Set();
|
|
65
|
+
const params = ctx.parameterNames || [];
|
|
66
|
+
|
|
67
|
+
const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
|
|
68
|
+
if (rootMatch) {
|
|
69
|
+
const root = rootMatch[1];
|
|
70
|
+
if (declared.has(root) || params.includes(root)) return value;
|
|
71
|
+
if (params.length > 0) return `args.${value}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback: quoted string literal so generated code stays valid TS
|
|
75
|
+
if (/\s/.test(value)) return `/* TODO: resolve "${value}" */ null`;
|
|
76
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
80
|
+
// --- Find / Lookup by single field ---
|
|
81
|
+
// Matches: "Look up X by Y", "Find X by Y", "Find X by Y or fail with 404"
|
|
82
|
+
{
|
|
83
|
+
name: 'find-by-field',
|
|
84
|
+
pattern: /^(?:look\s+up|find|fetch|get)\s+(\w+)\s+by\s+(\w+)(?:\s+or\s+fail.*)?$/i,
|
|
85
|
+
generateCall: (m, ctx) => {
|
|
86
|
+
const model = m[1];
|
|
87
|
+
const field = m[2];
|
|
88
|
+
const modelVar = toVar(model);
|
|
89
|
+
const collection = toCollection(model);
|
|
90
|
+
const params = ctx.parameterNames || [];
|
|
91
|
+
const declared = ctx.declaredVars || new Set();
|
|
92
|
+
const idVal = field === 'id'
|
|
93
|
+
? (params.includes(`${modelVar}Id`) ? `args.${modelVar}Id` : 'args.id')
|
|
94
|
+
: (params.includes(field) ? `args.${field}` : `args.${field}`);
|
|
95
|
+
|
|
96
|
+
if (declared.has(modelVar)) {
|
|
97
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
|
|
98
|
+
}
|
|
99
|
+
declared.add(modelVar);
|
|
100
|
+
const failOnMissing = /or\s+fail/i.test(m[0]);
|
|
101
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
|
|
102
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
103
|
+
const ${modelVar} = await ${modelVar}_collection.findOne({ ${field}: ${idVal} });${failOnMissing ? `
|
|
104
|
+
if (!${modelVar}) throw new Error('${model} not found');` : ''}`;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// --- Find by composite (two fields) ---
|
|
109
|
+
// Matches: "Look up X by Y and Z", "Find X by Y and Z"
|
|
110
|
+
{
|
|
111
|
+
name: 'find-by-composite',
|
|
112
|
+
pattern: /^(?:look\s+up|find|fetch|get)(?:\s+existing)?\s+(\w+)\s+by\s+(\w+)\s+and\s+(\w+)$/i,
|
|
113
|
+
generateCall: (m, ctx) => {
|
|
114
|
+
const model = m[1];
|
|
115
|
+
const f1 = m[2];
|
|
116
|
+
const f2 = m[3];
|
|
117
|
+
const modelVar = toVar(model);
|
|
118
|
+
const collection = toCollection(model);
|
|
119
|
+
const declared = ctx.declaredVars || new Set();
|
|
120
|
+
if (declared.has(modelVar)) {
|
|
121
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2} (already loaded)`;
|
|
122
|
+
}
|
|
123
|
+
declared.add(modelVar);
|
|
124
|
+
return ` // Step ${ctx.stepNum}: Find ${model} by ${f1} and ${f2}
|
|
125
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
126
|
+
const ${modelVar} = await ${modelVar}_collection.findOne({ ${f1}: args.${f1}, ${f2}: args.${f2} });`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// --- Create model record ---
|
|
131
|
+
// Matches: "Create X", "Create new X with ..."
|
|
132
|
+
{
|
|
133
|
+
name: 'create',
|
|
134
|
+
pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
135
|
+
generateCall: (m, ctx) => {
|
|
136
|
+
const model = m[1];
|
|
137
|
+
const modelVar = toVar(model);
|
|
138
|
+
const collection = toCollection(model);
|
|
139
|
+
const params = ctx.parameterNames || [];
|
|
140
|
+
const declared = ctx.declaredVars || new Set();
|
|
141
|
+
declared.add(modelVar);
|
|
142
|
+
const dataExpr = params.length > 0 ? `{ ${params.join(', ')} }` : 'args';
|
|
143
|
+
return ` // Step ${ctx.stepNum}: Create ${model}
|
|
144
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
145
|
+
const ${modelVar}_inserted = await ${modelVar}_collection.insertOne(${dataExpr});
|
|
146
|
+
const ${modelVar} = { _id: ${modelVar}_inserted.insertedId, ...${dataExpr} };`;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// --- Update specific field on previously-loaded model ---
|
|
151
|
+
// Matches: "Update X field to value". Only fires when X has been
|
|
152
|
+
// declared by a prior matched find — otherwise the model var doesn't
|
|
153
|
+
// exist in scope and tsc fails. Falling through to the unmatched path
|
|
154
|
+
// lets the AI layer handle it.
|
|
155
|
+
{
|
|
156
|
+
name: 'update-field',
|
|
157
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
158
|
+
generateCall: (m, ctx) => {
|
|
159
|
+
const model = m[1];
|
|
160
|
+
const field = m[2];
|
|
161
|
+
const rawValue = m[3];
|
|
162
|
+
const modelVar = toVar(model);
|
|
163
|
+
if (!ctx.declaredVars?.has(modelVar)) return ''; // signal "not really matched"
|
|
164
|
+
const collection = toCollection(model);
|
|
165
|
+
const val = resolveValue(rawValue, ctx);
|
|
166
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} to ${rawValue.trim()}
|
|
167
|
+
{
|
|
168
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
169
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
170
|
+
}`;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// --- Update field timestamp (e.g. "Update device lastLoginAt timestamp") ---
|
|
175
|
+
{
|
|
176
|
+
name: 'update-field-timestamp',
|
|
177
|
+
pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
|
|
178
|
+
generateCall: (m, ctx) => {
|
|
179
|
+
const model = m[1];
|
|
180
|
+
const field = m[2];
|
|
181
|
+
const modelVar = toVar(model);
|
|
182
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
183
|
+
const collection = toCollection(model);
|
|
184
|
+
return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
|
|
185
|
+
{
|
|
186
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
187
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: new Date().toISOString() } });
|
|
188
|
+
}`;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// --- Generic "Update X" (writes args back to record) ---
|
|
193
|
+
{
|
|
194
|
+
name: 'update',
|
|
195
|
+
pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
|
|
196
|
+
generateCall: (m, ctx) => {
|
|
197
|
+
const model = m[1];
|
|
198
|
+
const modelVar = toVar(model);
|
|
199
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
200
|
+
const collection = toCollection(model);
|
|
201
|
+
return ` // Step ${ctx.stepNum}: Update ${model}
|
|
202
|
+
{
|
|
203
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
204
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: args });
|
|
205
|
+
}`;
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// --- Delete model record ---
|
|
210
|
+
{
|
|
211
|
+
name: 'delete',
|
|
212
|
+
pattern: /^delete\s+(\w+)/i,
|
|
213
|
+
generateCall: (m, ctx) => {
|
|
214
|
+
const model = m[1];
|
|
215
|
+
const modelVar = toVar(model);
|
|
216
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
217
|
+
const collection = toCollection(model);
|
|
218
|
+
return ` // Step ${ctx.stepNum}: Delete ${model}
|
|
219
|
+
{
|
|
220
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
221
|
+
await ${modelVar}_collection.deleteOne({ _id: ${modelVar}._id });
|
|
222
|
+
}`;
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// --- Transition to lifecycle state ---
|
|
227
|
+
// Matches: "Transition X to state"
|
|
228
|
+
{
|
|
229
|
+
name: 'transition',
|
|
230
|
+
pattern: /^transition\s+(\w+)\s+to\s+(\w+)/i,
|
|
231
|
+
generateCall: (m, ctx) => {
|
|
232
|
+
const model = m[1];
|
|
233
|
+
const state = m[2];
|
|
234
|
+
const modelVar = toVar(model);
|
|
235
|
+
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
236
|
+
const collection = toCollection(model);
|
|
237
|
+
return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
|
|
238
|
+
if ((${modelVar} as any).status === '${state}') throw new Error('${model} is already ${state}');
|
|
239
|
+
{
|
|
240
|
+
const ${modelVar}_collection = await getCollection('${collection}');
|
|
241
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { status: '${state}' } });
|
|
242
|
+
}`;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// --- Set field to value (on the controller's primary model) ---
|
|
247
|
+
{
|
|
248
|
+
name: 'set',
|
|
249
|
+
pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
|
|
250
|
+
generateCall: (m, ctx) => {
|
|
251
|
+
const field = m[1];
|
|
252
|
+
const rawValue = m[2];
|
|
253
|
+
const modelVar = toVar(ctx.modelName);
|
|
254
|
+
const val = resolveValue(rawValue, ctx);
|
|
255
|
+
return ` // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
|
|
256
|
+
{
|
|
257
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
258
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $set: { ${field}: ${val} } });
|
|
259
|
+
}`;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// --- Increment / Decrement ---
|
|
264
|
+
{
|
|
265
|
+
name: 'increment',
|
|
266
|
+
pattern: /^increment\s+(\w+)\s+by\s+(\w+)/i,
|
|
267
|
+
generateCall: (m, ctx) => {
|
|
268
|
+
const field = m[1];
|
|
269
|
+
const amount = m[2];
|
|
270
|
+
const modelVar = toVar(ctx.modelName);
|
|
271
|
+
return ` // Step ${ctx.stepNum}: Increment ${field} by ${amount}
|
|
272
|
+
{
|
|
273
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
274
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: ${amount} } });
|
|
275
|
+
}`;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'decrement',
|
|
280
|
+
pattern: /^decrement\s+(\w+)\s+by\s+(\w+)/i,
|
|
281
|
+
generateCall: (m, ctx) => {
|
|
282
|
+
const field = m[1];
|
|
283
|
+
const amount = m[2];
|
|
284
|
+
const modelVar = toVar(ctx.modelName);
|
|
285
|
+
return ` // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
|
|
286
|
+
{
|
|
287
|
+
const ${modelVar}_collection = await getCollection(COLLECTION_NAME);
|
|
288
|
+
await ${modelVar}_collection.updateOne({ _id: ${modelVar}._id }, { $inc: { ${field}: -${amount} } });
|
|
289
|
+
}`;
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
// --- Send/Emit/Publish event ---
|
|
294
|
+
// Emits an eventBus.publish call. The payload references the controller's
|
|
295
|
+
// primary model variable IF it was declared by a prior matched step;
|
|
296
|
+
// otherwise the payload is just operation + timestamp (the event still
|
|
297
|
+
// fires, just without a record-level id).
|
|
298
|
+
{
|
|
299
|
+
name: 'send-event',
|
|
300
|
+
pattern: /^(?:send|emit|publish)\s+(\w+)\s+event/i,
|
|
301
|
+
generateCall: (m, ctx) => {
|
|
302
|
+
const event = m[1];
|
|
303
|
+
const modelVar = toVar(ctx.modelName);
|
|
304
|
+
const hasModelVar = ctx.declaredVars?.has(modelVar);
|
|
305
|
+
const payload = hasModelVar
|
|
306
|
+
? `{ ${modelVar}Id: (${modelVar} as any)?._id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() }`
|
|
307
|
+
: `{ operation: '${ctx.operationName}', timestamp: new Date().toISOString() }`;
|
|
308
|
+
return ` // Step ${ctx.stepNum}: Emit ${event} event
|
|
309
|
+
await eventBus.publish('${event}', ${payload} as any);`;
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// --- Call service ---
|
|
314
|
+
{
|
|
315
|
+
name: 'call-service',
|
|
316
|
+
pattern: /^call\s+(\w+)\.(\w+)/i,
|
|
317
|
+
generateCall: (m, ctx) => {
|
|
318
|
+
const service = m[1];
|
|
319
|
+
const method = m[2];
|
|
320
|
+
const args = (ctx.parameterNames || []).join(', ');
|
|
321
|
+
return ` // Step ${ctx.stepNum}: Call ${service}.${method}
|
|
322
|
+
await (${toVar(service)} as any).${method}({ ${args} });`;
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
// --- Return ---
|
|
327
|
+
// Only matches when the returned value is a single declared identifier
|
|
328
|
+
// OR when the natural-language reference resolves to "the model" (e.g.
|
|
329
|
+
// "Return the user", "Return updated game"). Multi-word phrases like
|
|
330
|
+
// "Return state to caller" don't translate to valid JS, so they fall
|
|
331
|
+
// through to the unmatched-step path and emit a `// TODO:` line.
|
|
332
|
+
{
|
|
333
|
+
name: 'return-model',
|
|
334
|
+
pattern: /^return\s+(?:the\s+|updated\s+|created\s+)?(\w+)\s*$/i,
|
|
335
|
+
generateCall: (m, ctx) => {
|
|
336
|
+
const valueRaw = m[1].trim();
|
|
337
|
+
const declared = ctx.declaredVars || new Set();
|
|
338
|
+
const params = ctx.parameterNames || [];
|
|
339
|
+
const modelVar = toVar(ctx.modelName);
|
|
340
|
+
// Resolve to a declared variable, parameter, or the controller's
|
|
341
|
+
// primary model variable. Anything else doesn't have a JS expression
|
|
342
|
+
// — fall through (return unmatched so the TODO path fires).
|
|
343
|
+
if (valueRaw.toLowerCase() === ctx.modelName.toLowerCase() || valueRaw === modelVar) {
|
|
344
|
+
return ` // Step ${ctx.stepNum}: Return ${ctx.modelName}
|
|
345
|
+
return ${modelVar};`;
|
|
346
|
+
}
|
|
347
|
+
if (declared.has(valueRaw)) {
|
|
348
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
349
|
+
return ${valueRaw};`;
|
|
350
|
+
}
|
|
351
|
+
if (params.includes(valueRaw)) {
|
|
352
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw}
|
|
353
|
+
return args.${valueRaw};`;
|
|
354
|
+
}
|
|
355
|
+
// Unresolvable — emit a TODO comment + neutral return so generated
|
|
356
|
+
// code still compiles. Caller can replace.
|
|
357
|
+
return ` // Step ${ctx.stepNum}: Return ${valueRaw} — TODO: resolve binding
|
|
358
|
+
return null;`;
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Match a step against the mongo-native conventions.
|
|
365
|
+
*
|
|
366
|
+
* Matched: returns the inline native-driver code to emit AND advances the
|
|
367
|
+
* convention's bookkeeping (e.g. find/create add the model variable to
|
|
368
|
+
* declaredVars).
|
|
369
|
+
*
|
|
370
|
+
* Unmatched: returns the function name + inputs + resultVar that an
|
|
371
|
+
* AI-behaviors layer should produce for this step. The shape mirrors
|
|
372
|
+
* prisma's step-conventions matchStep so the orm-agnostic
|
|
373
|
+
* AI-behaviors-generator can drive either side without branching.
|
|
374
|
+
*/
|
|
375
|
+
export function matchMongoStep(
|
|
376
|
+
step: string,
|
|
377
|
+
ctx: MongoStepContext,
|
|
378
|
+
): {
|
|
379
|
+
matched: boolean;
|
|
380
|
+
call: string;
|
|
381
|
+
helperMethod?: string;
|
|
382
|
+
functionName?: string;
|
|
383
|
+
inputs?: string[];
|
|
384
|
+
resultVar?: string;
|
|
385
|
+
} {
|
|
386
|
+
for (const convention of MONGO_STEP_CONVENTIONS) {
|
|
387
|
+
const m = step.match(convention.pattern);
|
|
388
|
+
if (m) {
|
|
389
|
+
const call = convention.generateCall(m, ctx);
|
|
390
|
+
// A convention may signal "regex matched but I can't safely emit"
|
|
391
|
+
// by returning the empty string (e.g. update-field needs the model
|
|
392
|
+
// to be declared by a prior find; if it isn't, fall through to AI).
|
|
393
|
+
if (call) {
|
|
394
|
+
return { matched: true, call };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Unmatched — caller is expected to delegate to an AI-generated pure
|
|
400
|
+
// function in `behaviors/<Owner>.ai.ts`. Inputs = action parameters +
|
|
401
|
+
// any variables already declared by prior matched conventions; this
|
|
402
|
+
// matches what the AI-behaviors-generator will produce for the same
|
|
403
|
+
// step (it walks the same conventions in the same order, so its
|
|
404
|
+
// declaredVars set agrees with ours by construction).
|
|
405
|
+
const functionName = toMethod(step);
|
|
406
|
+
const declared = Array.from(ctx.declaredVars || []);
|
|
407
|
+
const paramNames = ctx.parameterNames || [];
|
|
408
|
+
const inputs = [...paramNames, ...declared];
|
|
409
|
+
const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
|
|
410
|
+
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
411
|
+
const inputObj = inputs.length > 0
|
|
412
|
+
? `{ ${inputs.map((n) => paramNames.includes(n) ? `${n}: args.${n}` : n).join(', ')} }`
|
|
413
|
+
: '{}';
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
matched: false,
|
|
417
|
+
call: ` // Step ${ctx.stepNum}: ${step} [AI-generated — pure function]
|
|
418
|
+
const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
|
|
419
|
+
functionName,
|
|
420
|
+
inputs,
|
|
421
|
+
resultVar,
|
|
422
|
+
};
|
|
423
|
+
}
|