@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.
Files changed (29) hide show
  1. package/dist/inference/core/specly-converter.d.ts.map +1 -1
  2. package/dist/inference/core/specly-converter.js +20 -0
  3. package/dist/inference/core/specly-converter.js.map +1 -1
  4. package/dist/inference/index.d.ts.map +1 -1
  5. package/dist/inference/index.js +72 -22
  6. package/dist/inference/index.js.map +1 -1
  7. package/dist/inference/logical/generators/controller-generator.d.ts.map +1 -1
  8. package/dist/inference/logical/generators/controller-generator.js +26 -4
  9. package/dist/inference/logical/generators/controller-generator.js.map +1 -1
  10. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +26 -10
  11. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +50 -15
  12. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +27 -7
  13. package/dist/libs/instance-factories/services/templates/mongodb-native/controller-generator.js +59 -23
  14. package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +319 -0
  15. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +192 -28
  16. package/dist/parser/processors/ExecutableProcessor.d.ts.map +1 -1
  17. package/dist/parser/processors/ExecutableProcessor.js +14 -1
  18. package/dist/parser/processors/ExecutableProcessor.js.map +1 -1
  19. package/dist/realize/index.d.ts.map +1 -1
  20. package/dist/realize/index.js +22 -3
  21. package/dist/realize/index.js.map +1 -1
  22. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +48 -12
  23. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +80 -21
  24. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +49 -8
  25. package/libs/instance-factories/services/templates/mongodb-native/__tests__/controller-generator.test.ts +3 -1
  26. package/libs/instance-factories/services/templates/mongodb-native/controller-generator.ts +82 -25
  27. package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +423 -0
  28. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +287 -38
  29. 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
+ }