@specverse/engines 4.1.12 → 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 (48) 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/inference/index.d.ts.map +1 -1
  11. package/dist/inference/index.js +1 -0
  12. package/dist/inference/index.js.map +1 -1
  13. package/dist/inference/logical/logical-engine.d.ts.map +1 -1
  14. package/dist/inference/logical/logical-engine.js +3 -0
  15. package/dist/inference/logical/logical-engine.js.map +1 -1
  16. package/dist/libs/instance-factories/cli/templates/commander/cli-entry-generator.js +12 -11
  17. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +21 -5
  18. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -30
  19. package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
  20. package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
  21. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +25 -9
  22. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
  23. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +141 -0
  24. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +62 -42
  25. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +39 -7
  26. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
  27. package/dist/parser/processors/AttributeProcessor.d.ts.map +1 -1
  28. package/dist/parser/processors/AttributeProcessor.js +13 -6
  29. package/dist/parser/processors/AttributeProcessor.js.map +1 -1
  30. package/dist/parser/processors/ModelProcessor.d.ts.map +1 -1
  31. package/dist/parser/processors/ModelProcessor.js +7 -0
  32. package/dist/parser/processors/ModelProcessor.js.map +1 -1
  33. package/dist/realize/index.d.ts.map +1 -1
  34. package/dist/realize/index.js +54 -0
  35. package/dist/realize/index.js.map +1 -1
  36. package/libs/instance-factories/cli/templates/commander/cli-entry-generator.ts +12 -11
  37. package/libs/instance-factories/cli/templates/commander/command-generator.ts +21 -5
  38. package/libs/instance-factories/communication/event-emitter.yaml +16 -12
  39. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -35
  40. package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
  41. package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
  42. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +32 -11
  43. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
  44. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +211 -0
  45. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +86 -40
  46. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +54 -8
  47. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +166 -85
  48. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ import { matchStep } from "./step-conventions.js";
2
+ async function generateAiBehaviors(context) {
3
+ const { controller, model } = context;
4
+ if (!controller?.actions) return "";
5
+ const modelName = model?.name || controller.model || "Model";
6
+ const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
7
+ const unmatchedFunctions = [];
8
+ for (const [actionName, action] of Object.entries(controller.actions)) {
9
+ const steps = action.steps || [];
10
+ const parameterNames = Object.keys(action.parameters || {});
11
+ const preconditions = action.requires || action.preconditions || [];
12
+ const declaredVars = /* @__PURE__ */ new Set();
13
+ for (const pc of preconditions) {
14
+ const match = pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i);
15
+ if (match) {
16
+ const entity = match[1];
17
+ declaredVars.add(entity.charAt(0).toLowerCase() + entity.slice(1));
18
+ }
19
+ }
20
+ for (let i = 0; i < steps.length; i++) {
21
+ const step = steps[i];
22
+ if (typeof step !== "string") continue;
23
+ const ctx = {
24
+ modelName,
25
+ prismaModel: modelVar,
26
+ serviceName: `${modelName}Controller`,
27
+ operationName: actionName,
28
+ stepNum: i + 1,
29
+ parameterNames,
30
+ declaredVars
31
+ };
32
+ const result = matchStep(step, ctx);
33
+ if (!result.matched && result.functionName) {
34
+ const existing = unmatchedFunctions.find((f) => f.functionName === result.functionName);
35
+ if (!existing) {
36
+ unmatchedFunctions.push({
37
+ functionName: result.functionName,
38
+ step,
39
+ operationName: actionName,
40
+ parameterNames,
41
+ inputs: result.inputs || []
42
+ });
43
+ }
44
+ }
45
+ }
46
+ }
47
+ if (unmatchedFunctions.length === 0) return "";
48
+ let aiService = null;
49
+ try {
50
+ const { BehaviorAIService } = await import("@specverse/engines/ai");
51
+ aiService = new BehaviorAIService();
52
+ if (!aiService.isAvailable) {
53
+ aiService = null;
54
+ } else {
55
+ aiService.startSession(`${modelName}Controller`);
56
+ }
57
+ } catch {
58
+ aiService = null;
59
+ }
60
+ const availableModels = context.spec?.models ? Object.keys(context.spec.models) : [];
61
+ const functions = [];
62
+ for (const { functionName, step, operationName, parameterNames, inputs } of unmatchedFunctions) {
63
+ const signature = inputs.length > 0 ? `input: { ${inputs.map((n) => `${n}: any`).join("; ")} }` : "input: Record<string, never>";
64
+ const destructure = inputs.length > 0 ? ` const { ${inputs.join(", ")} } = input;` : "";
65
+ let body = null;
66
+ let source = "STUB";
67
+ if (aiService) {
68
+ try {
69
+ body = await aiService.generateBehavior({
70
+ step,
71
+ modelName,
72
+ operationName,
73
+ functionName,
74
+ parameterNames: inputs,
75
+ // the actual inputs to the pure function
76
+ availableModels,
77
+ spec: context.spec
78
+ });
79
+ if (body) source = "AI-GENERATED";
80
+ } catch {
81
+ }
82
+ }
83
+ if (!body) {
84
+ body = ` throw new Error('Not implemented: ${functionName} \u2014 see behaviors/${modelName}Controller.ai.ts');`;
85
+ } else {
86
+ body = body.split("\n").map((line) => line ? " " + line : line).join("\n");
87
+ }
88
+ const inputsDoc = inputs.length > 0 ? ` * Inputs: ${inputs.join(", ")}
89
+ ` : "";
90
+ functions.push(`/**
91
+ * ${functionName}
92
+ *
93
+ * Spec step: "${step}"
94
+ * Called by: ${modelName}Controller.${operationName}()
95
+ ${inputsDoc} * Source: ${source}
96
+ * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
97
+ *
98
+ * PURE FUNCTION \u2014 no database access, no event publishing, no external services.
99
+ * All data comes in via \`input\`; all effects happen in the calling controller.
100
+ * ${source === "AI-GENERATED" ? "AI-generated implementation. Review and test before deploying." : "STUB \u2014 Claude CLI unavailable. Install Claude Code or implement manually."}
101
+ */
102
+ export async function ${functionName}(${signature}): Promise<any> {
103
+ ${destructure ? destructure + "\n" : ""}${body}
104
+ }`);
105
+ }
106
+ if (aiService?.endSession) aiService.endSession();
107
+ return `/**
108
+ * ${modelName}Controller \u2014 AI-Generated Behaviors
109
+ *
110
+ * \u26A0\uFE0F THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
111
+ *
112
+ * These functions could not be generated from convention patterns.
113
+ * They are called by ${modelName}Controller when executing custom actions.
114
+ *
115
+ * Options for each function:
116
+ * - Implement manually (recommended for business-critical logic)
117
+ * - Use AI generation: specverse ai generate <function>
118
+ * - Refactor the spec step to use a convention pattern
119
+ *
120
+ * Convention patterns that ARE auto-generated (no AI needed):
121
+ * "Find {Model} by {field}" \u2192 prisma.model.findUniqueOrThrow(...)
122
+ * "Create {Model}" \u2192 prisma.model.create(...)
123
+ * "Update {Model} {field} to {value}" \u2192 prisma.model.update(...)
124
+ * "Delete {Model}" \u2192 prisma.model.delete(...)
125
+ * "Transition {Model} to {state}" \u2192 prisma.model.update({ status: ... })
126
+ * "Count {Model}s per {Group}" \u2192 prisma.model.groupBy(...)
127
+ * See step-conventions.ts for the full list.
128
+ *
129
+ * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
130
+ */
131
+
132
+ import { PrismaClient } from '@prisma/client';
133
+
134
+ const prisma = new PrismaClient();
135
+
136
+ ${functions.join("\n\n")}
137
+ `;
138
+ }
139
+ export {
140
+ generateAiBehaviors as default
141
+ };
@@ -6,14 +6,17 @@ function generateBehaviorBody(behavior, opMeta, context) {
6
6
  function generateBehaviorWithHelpers(behavior, opMeta, context) {
7
7
  const parts = [];
8
8
  const helperMethods = [];
9
+ const preconditionDeclared = /* @__PURE__ */ new Set();
9
10
  const preconditions = generatePreconditionChecks(
10
11
  behavior.preconditions || [],
11
- context
12
+ context,
13
+ preconditionDeclared
12
14
  );
13
15
  if (preconditions) parts.push(preconditions);
14
- const { code, helpers } = generateStepLogic(
16
+ const { code, helpers, unmatchedSteps } = generateStepLogic(
15
17
  behavior.steps || [],
16
- context
18
+ context,
19
+ preconditionDeclared
17
20
  );
18
21
  parts.push(code);
19
22
  helperMethods.push(...helpers);
@@ -23,76 +26,80 @@ function generateBehaviorWithHelpers(behavior, opMeta, context) {
23
26
  if (postconditions) parts.push(postconditions);
24
27
  const events = generateEventPublishing(
25
28
  behavior.sideEffects || [],
26
- context.operationName
29
+ context.operationName,
30
+ context.parameterNames
27
31
  );
28
32
  if (events) parts.push(events);
29
33
  let body = parts.join("\n\n");
30
34
  if (behavior.transactional) {
31
35
  body = generateTransactionWrapper(body, context);
32
36
  }
33
- return { body, helperMethods };
37
+ return { body, helperMethods, unmatchedSteps };
34
38
  }
35
- function generatePreconditionChecks(preconditions, context) {
39
+ function generatePreconditionChecks(preconditions, context, declared) {
36
40
  if (preconditions.length === 0) return "";
37
- const checks = preconditions.map((pc) => matchPreconditionPattern(pc, context));
41
+ if (!declared) declared = /* @__PURE__ */ new Set();
42
+ const checks = preconditions.map((pc) => matchPreconditionPattern(pc, context, declared));
38
43
  return ` // === PRECONDITIONS ===
39
44
  ${checks.join("\n")}`;
40
45
  }
41
- function matchPreconditionPattern(precondition, context) {
46
+ function findIdParam(modelName, paramNames) {
47
+ const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
48
+ const idParam = paramNames.find((p) => p === `${modelVar}Id`);
49
+ if (idParam) return idParam;
50
+ if (paramNames.includes("id")) return "id";
51
+ return `${modelVar}Id`;
52
+ }
53
+ function matchPreconditionPattern(precondition, context, declared) {
42
54
  const pc = precondition.toLowerCase();
43
- const prismaModel = context.prismaModel || context.modelName;
55
+ const params = context.parameterNames || [];
44
56
  const existsMatch = precondition.match(/^(\w+)\s+exists/i);
45
57
  if (existsMatch) {
46
58
  const entity = existsMatch[1];
47
59
  const entityVar = entity.charAt(0).toLowerCase() + entity.slice(1);
48
- return ` // Guard: ${precondition}
49
- const ${entityVar} = await prisma.${entityVar}.findUnique({ where: { id: params.id } });
50
- if (!${entityVar}) {
51
- throw new Error('Precondition failed: ${precondition}');
52
- }`;
60
+ const idParam = findIdParam(entity, params);
61
+ if (declared.has(entityVar)) {
62
+ return ` // Guard: ${precondition} (already fetched)`;
63
+ }
64
+ declared.add(entityVar);
65
+ return ` const ${entityVar} = await prisma.${entityVar}.findUniqueOrThrow({ where: { id: ${idParam} } });`;
53
66
  }
54
67
  if (pc.includes("is not empty") || pc.includes("is required")) {
55
68
  const fieldMatch = precondition.match(/^(\w+)\s+is/i);
56
69
  if (fieldMatch) {
57
70
  const field = fieldMatch[1];
58
- return ` // Guard: ${precondition}
59
- if (!params.${field}) {
60
- throw new Error('Precondition failed: ${precondition}');
61
- }`;
71
+ const paramRef = params.includes(field) ? field : `params.${field}`;
72
+ return ` if (!${paramRef}) throw new Error('${field} is required');`;
62
73
  }
63
74
  }
64
75
  if (pc.includes("is valid")) {
65
- return ` // Guard: ${precondition}
66
- const validation = this.validate(params, { operation: '${context.operationName}' });
67
- if (!validation.valid) {
68
- throw new Error('Precondition failed: ${precondition} \u2014 ' + validation.errors.join(', '));
69
- }`;
76
+ return ` // TODO: Implement validation: ${precondition}`;
70
77
  }
71
78
  const matchesMatch = precondition.match(/^(\w+)\s+(?:matches|equals)\s+(.+)/i);
72
79
  if (matchesMatch) {
73
- const left = matchesMatch[1];
74
- const right = matchesMatch[2];
75
- return ` // Guard: ${precondition}
76
- if (params.${left.charAt(0).toLowerCase() + left.slice(1)} !== params.${right.charAt(0).toLowerCase() + right.slice(1)}) {
77
- throw new Error('Precondition failed: ${precondition}');
78
- }`;
80
+ const left = matchesMatch[1].charAt(0).toLowerCase() + matchesMatch[1].slice(1);
81
+ const right = matchesMatch[2].charAt(0).toLowerCase() + matchesMatch[2].slice(1);
82
+ return ` if (${left} !== ${right}) throw new Error('${matchesMatch[1]} must match ${matchesMatch[2]}');`;
79
83
  }
80
84
  const stateMatch = precondition.match(/^(\w+)\s+is\s+(\w+)$/i);
81
85
  if (stateMatch) {
82
86
  const model = stateMatch[1];
83
87
  const state = stateMatch[2];
84
88
  const modelVar = model.charAt(0).toLowerCase() + model.slice(1);
85
- return ` // Guard: ${precondition}
86
- const ${modelVar}State = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: params.id } });
87
- if (${modelVar}State.status !== '${state}') {
88
- throw new Error('Precondition failed: ${precondition} (current: ' + ${modelVar}State.status + ')');
89
- }`;
89
+ const idParam = findIdParam(model, params);
90
+ if (!declared.has(modelVar)) {
91
+ declared.add(modelVar);
92
+ return ` const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: ${idParam} } });
93
+ if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
94
+ }
95
+ return ` if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
90
96
  }
91
- return ` // Guard: ${precondition}
92
- // TODO: Implement precondition check`;
97
+ return ` // TODO: Implement precondition: ${precondition}`;
93
98
  }
94
- function generateStepLogic(steps, context) {
99
+ function generateStepLogic(steps, context, preconditionDeclared) {
95
100
  const helpers = [];
101
+ const unmatchedSteps = [];
102
+ const declaredVars = preconditionDeclared || /* @__PURE__ */ new Set();
96
103
  if (steps && steps.length > 0) {
97
104
  const stepCode = steps.map((step, i) => {
98
105
  if (typeof step !== "string") {
@@ -103,24 +110,36 @@ function generateStepLogic(steps, context) {
103
110
  prismaModel: context.prismaModel || context.modelName,
104
111
  serviceName: context.serviceName,
105
112
  operationName: context.operationName,
106
- stepNum: i + 1
113
+ stepNum: i + 1,
114
+ parameterNames: context.parameterNames,
115
+ declaredVars
107
116
  };
108
117
  const result = matchStep(step, ctx);
109
118
  if (result.helperMethod) {
110
119
  helpers.push(result.helperMethod);
111
120
  }
121
+ if (!result.matched && result.functionName) {
122
+ unmatchedSteps.push({
123
+ step,
124
+ functionName: result.functionName,
125
+ operationName: context.operationName,
126
+ inputs: result.inputs || []
127
+ });
128
+ }
112
129
  return result.call;
113
130
  });
114
131
  return {
115
132
  code: ` // === EXECUTE ===
116
133
  ${stepCode.join("\n\n")}`,
117
- helpers
134
+ helpers,
135
+ unmatchedSteps
118
136
  };
119
137
  }
120
138
  return {
121
139
  code: ` // === EXECUTE ===
122
140
  ${inferLogicFromOperationName(context)}`,
123
- helpers
141
+ helpers,
142
+ unmatchedSteps
124
143
  };
125
144
  }
126
145
  function inferLogicFromOperationName(context) {
@@ -154,10 +173,11 @@ function generatePostconditionVerification(postconditions) {
154
173
  ${checks.join("\n")}
155
174
  }`;
156
175
  }
157
- function generateEventPublishing(sideEffects, operationName) {
176
+ function generateEventPublishing(sideEffects, operationName, parameterNames) {
158
177
  if (!sideEffects || sideEffects.length === 0) return "";
178
+ const paramFields = parameterNames?.length ? parameterNames.join(", ") + ", " : "";
159
179
  const publishes = sideEffects.map(
160
- (event) => ` this.emit('${event}', { operation: '${operationName}', timestamp: new Date().toISOString() });`
180
+ (event) => ` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() });`
161
181
  );
162
182
  return ` // === EVENTS ===
163
183
  ${publishes.join("\n")}`;
@@ -1,4 +1,5 @@
1
1
  import { buildTransitionMap, isAutoField } from "@specverse/types/spec-rules";
2
+ import { generateBehaviorWithHelpers } from "./behavior-generator.js";
2
3
  function generatePrismaController(context) {
3
4
  const { controller, model, spec, models: allModels } = context;
4
5
  if (!controller) {
@@ -16,6 +17,7 @@ function generatePrismaController(context) {
16
17
  const idAttr = (Array.isArray(model.attributes) ? model.attributes : Object.values(model.attributes || {})).find((a) => a.name === "id");
17
18
  const idType = idAttr?.type || "UUID";
18
19
  const needsIntParse = idType === "Integer" || idType === "Int" || idType === "Number";
20
+ const customActions = generateCustomActions(controller, modelName, modelVar);
19
21
  return `/**
20
22
  * ${controllerName}
21
23
  * Model-specific business logic for ${modelName}
@@ -23,7 +25,8 @@ function generatePrismaController(context) {
23
25
  */
24
26
 
25
27
  import { PrismaClient } from '@prisma/client';
26
- ${hasEventPublishing(curedOps, controller) ? `import { eventBus, EventName } from '../events/eventBus.js';` : ""}
28
+ ${hasEventPublishing(curedOps, controller) ? `import { eventBus } from '../events/eventBus.js';` : ""}
29
+ ${customActions.needsAiBehaviors ? `import * as aiBehaviors from '../behaviors/${modelName}Controller.ai.js';` : ""}
27
30
 
28
31
  const prisma = new PrismaClient();
29
32
 
@@ -42,7 +45,7 @@ export class ${controllerName} {
42
45
  ${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ""}
43
46
  ${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ""}
44
47
  ${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ""}
45
- ${generateCustomActions(controller, modelName, modelVar)}
48
+ ${customActions.code}
46
49
  }
47
50
 
48
51
  // Export singleton instance
@@ -298,21 +301,44 @@ function generateDeleteMethod(model, modelName, modelVar, controller) {
298
301
  }
299
302
  function generateCustomActions(controller, modelName, modelVar) {
300
303
  if (!controller.actions || Object.keys(controller.actions).length === 0) {
301
- return "";
304
+ return { code: "", unmatchedSteps: [], needsAiBehaviors: false };
302
305
  }
303
306
  const actions = [];
307
+ const allUnmatchedSteps = [];
304
308
  Object.entries(controller.actions).forEach(([actionName, action]) => {
309
+ const behavior = {
310
+ preconditions: action.requires || action.preconditions || [],
311
+ steps: action.steps || [],
312
+ postconditions: action.ensures || action.postconditions || [],
313
+ sideEffects: action.publishes || action.events || [],
314
+ transactional: action.transactional
315
+ };
316
+ const ctx = {
317
+ modelName,
318
+ serviceName: `${modelName}Controller`,
319
+ operationName: actionName,
320
+ prismaModel: modelVar,
321
+ parameterNames: Object.keys(action.parameters || {})
322
+ };
323
+ const result = generateBehaviorWithHelpers(behavior, {}, ctx);
324
+ allUnmatchedSteps.push(...result.unmatchedSteps);
305
325
  actions.push(`
306
326
  /**
307
327
  * ${actionName}
308
328
  * ${action.description || ""}
309
329
  */
310
330
  public async ${actionName}(${generateActionParams(action)}): Promise<any> {
311
- // TODO: Implement ${actionName} logic
312
- throw new Error('${actionName} not implemented');
331
+ ${result.body}
313
332
  }`);
333
+ if (result.helperMethods.length > 0) {
334
+ actions.push(...result.helperMethods);
335
+ }
314
336
  });
315
- return actions.join("\n");
337
+ return {
338
+ code: actions.join("\n"),
339
+ unmatchedSteps: allUnmatchedSteps,
340
+ needsAiBehaviors: allUnmatchedSteps.length > 0
341
+ };
316
342
  }
317
343
  function generateActionParams(action) {
318
344
  if (!action.parameters || Object.keys(action.parameters).length === 0) {
@@ -391,7 +417,13 @@ ${includes}
391
417
  }`;
392
418
  }
393
419
  function hasEventPublishing(curedOps, controller) {
394
- return controller.publishes && Array.isArray(controller.publishes) && controller.publishes.length > 0;
420
+ if (controller.publishes && Array.isArray(controller.publishes) && controller.publishes.length > 0) return true;
421
+ if (controller.actions) {
422
+ for (const action of Object.values(controller.actions)) {
423
+ if (action.publishes?.length > 0 || action.events?.length > 0 || action.sideEffects?.length > 0) return true;
424
+ }
425
+ }
426
+ return false;
395
427
  }
396
428
  export {
397
429
  generatePrismaController as default