@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
@@ -1,20 +1,26 @@
1
1
  function generateEventBus(context) {
2
2
  const { spec } = context;
3
- const events = spec.events ? Object.keys(spec.events) : [];
3
+ const hasEvents = spec.events && Object.keys(spec.events).length > 0;
4
+ const models = spec.models ? Object.keys(spec.models) : [];
4
5
  return `/**
5
6
  * Event Bus
6
- * In-memory event bus using EventEmitter3
7
+ * Typed in-memory event bus using EventEmitter3
7
8
  * Generated from SpecVerse specification
8
9
  */
9
10
 
10
11
  import EventEmitter from 'eventemitter3';
12
+ ${hasEvents || models.length > 0 ? `import type { EventPayloads, EventName } from './event-types.js';` : ""}
11
13
 
12
- // Event type definitions
13
- ${events.map((event) => `export type ${event}Payload = any; // TODO: Define payload type`).join("\n")}
14
+ // Re-export types for consumers
15
+ ${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ""}
14
16
 
15
- // Event names enum
16
- export enum EventName {
17
- ${events.map((event) => ` ${event} = '${event}',`).join("\n")}
17
+ /**
18
+ * Event history entry
19
+ */
20
+ interface EventHistoryEntry {
21
+ event: string;
22
+ payload: any;
23
+ timestamp: Date;
18
24
  }
19
25
 
20
26
  /**
@@ -22,15 +28,14 @@ ${events.map((event) => ` ${event} = '${event}',`).join("\n")}
22
28
  */
23
29
  class EventBus extends EventEmitter {
24
30
  private static instance: EventBus;
31
+ private history: EventHistoryEntry[] = [];
32
+ private maxHistory = 1000;
25
33
 
26
34
  private constructor() {
27
35
  super();
28
- this.setMaxListeners(100); // Configure max listeners
36
+ this.setMaxListeners(100);
29
37
  }
30
38
 
31
- /**
32
- * Get singleton instance
33
- */
34
39
  public static getInstance(): EventBus {
35
40
  if (!EventBus.instance) {
36
41
  EventBus.instance = new EventBus();
@@ -39,37 +44,33 @@ class EventBus extends EventEmitter {
39
44
  }
40
45
 
41
46
  /**
42
- * Publish an event
47
+ * Publish a typed event
43
48
  */
44
- public publish<T = any>(event: EventName | string, payload: T): void {
45
- console.log(\`[EventBus] Publishing event: \${event}\`, payload);
49
+ public publish<K extends string>(event: K, payload: K extends keyof EventPayloads ? EventPayloads[K] : any): void {
50
+ this.history.push({ event, payload, timestamp: new Date() });
51
+ if (this.history.length > this.maxHistory) this.history.shift();
46
52
  this.emit(event, payload);
47
53
  }
48
54
 
49
55
  /**
50
- * Subscribe to an event
56
+ * Subscribe to a typed event
51
57
  */
52
- public subscribe<T = any>(
53
- event: EventName | string,
54
- handler: (payload: T) => void | Promise<void>
58
+ public subscribe<K extends string>(
59
+ event: K,
60
+ handler: (payload: K extends keyof EventPayloads ? EventPayloads[K] : any) => void | Promise<void>
55
61
  ): () => void {
56
- console.log(\`[EventBus] Subscribing to event: \${event}\`);
57
62
  this.on(event, handler);
58
-
59
- // Return unsubscribe function
60
- return () => {
61
- this.off(event, handler);
62
- };
63
+ return () => { this.off(event, handler); };
63
64
  }
64
65
 
65
66
  /**
66
- * Subscribe to an event (one-time)
67
+ * Get event history
67
68
  */
68
- public subscribeOnce<T = any>(
69
- event: EventName | string,
70
- handler: (payload: T) => void | Promise<void>
71
- ): void {
72
- this.once(event, handler);
69
+ public getHistory(eventName?: string, limit = 50): EventHistoryEntry[] {
70
+ const filtered = eventName
71
+ ? this.history.filter(e => e.event === eventName)
72
+ : this.history;
73
+ return filtered.slice(-limit);
73
74
  }
74
75
  }
75
76
 
@@ -0,0 +1,79 @@
1
+ function tsType(specType) {
2
+ const map = {
3
+ String: "string",
4
+ Text: "string",
5
+ Email: "string",
6
+ URL: "string",
7
+ Integer: "number",
8
+ Float: "number",
9
+ Number: "number",
10
+ Decimal: "number",
11
+ Money: "number",
12
+ Boolean: "boolean",
13
+ Date: "string",
14
+ DateTime: "string",
15
+ Timestamp: "string",
16
+ UUID: "string",
17
+ Object: "Record<string, any>",
18
+ Array: "any[]"
19
+ };
20
+ return map[specType] || "any";
21
+ }
22
+ function generateEventTypes(context) {
23
+ const { spec } = context;
24
+ const events = spec.events && typeof spec.events === "object" ? Object.entries(spec.events) : [];
25
+ if (events.length === 0) {
26
+ return `/**
27
+ * Event Types \u2014 no events defined in specification
28
+ */
29
+ export type EventPayloads = Record<string, any>;
30
+ `;
31
+ }
32
+ const interfaces = [];
33
+ const payloadMapEntries = [];
34
+ for (const [eventName, eventDef] of events) {
35
+ const def = eventDef;
36
+ const attrs = def.attributes || def.payload || {};
37
+ const attrEntries = Array.isArray(attrs) ? attrs.map((a) => [a.name, a]) : Object.entries(attrs);
38
+ const fields = attrEntries.map(([name, attrDef]) => {
39
+ const type = typeof attrDef === "string" ? tsType(attrDef.split(" ")[0]) : tsType(attrDef?.type || "String");
40
+ const required = typeof attrDef === "string" ? attrDef.includes("required") : attrDef?.required;
41
+ return ` ${name}${required ? "" : "?"}: ${type};`;
42
+ });
43
+ interfaces.push(`export interface ${eventName}Payload {
44
+ ${fields.join("\n")}
45
+ }`);
46
+ payloadMapEntries.push(` ${eventName}: ${eventName}Payload;`);
47
+ }
48
+ const models = spec.models ? Object.keys(spec.models) : [];
49
+ for (const model of models) {
50
+ for (const suffix of ["Created", "Updated", "Deleted", "Evolved"]) {
51
+ const name = `${model}${suffix}`;
52
+ if (!payloadMapEntries.some((e) => e.includes(name))) {
53
+ payloadMapEntries.push(` ${name}: { id: string; timestamp: string; [key: string]: any };`);
54
+ }
55
+ }
56
+ }
57
+ return `/**
58
+ * Event Payload Types
59
+ * Generated from SpecVerse specification
60
+ */
61
+
62
+ ${interfaces.join("\n\n")}
63
+
64
+ /**
65
+ * Map of all event names to their payload types
66
+ */
67
+ export type EventPayloads = {
68
+ ${payloadMapEntries.join("\n")}
69
+ };
70
+
71
+ /**
72
+ * All known event names
73
+ */
74
+ export type EventName = keyof EventPayloads;
75
+ `;
76
+ }
77
+ export {
78
+ generateEventTypes as default
79
+ };
@@ -0,0 +1,96 @@
1
+ function generateWebSocketBridge(context) {
2
+ const { spec } = context;
3
+ const models = spec.models ? Object.keys(spec.models) : [];
4
+ const specEvents = spec.events ? Object.keys(spec.events) : [];
5
+ const curedEvents = models.flatMap(
6
+ (m) => ["Created", "Updated", "Deleted", "Evolved"].map((s) => `${m}${s}`)
7
+ );
8
+ const allEvents = [.../* @__PURE__ */ new Set([...specEvents, ...curedEvents])];
9
+ return `/**
10
+ * WebSocket Bridge
11
+ * Bridges the event bus to frontend clients for real-time updates.
12
+ * Generated from SpecVerse specification
13
+ */
14
+
15
+ import type { FastifyInstance } from 'fastify';
16
+ import type { WebSocket } from '@fastify/websocket';
17
+ import { eventBus } from './eventBus.js';
18
+
19
+ interface ClientSubscription {
20
+ ws: WebSocket;
21
+ events: Set<string>;
22
+ }
23
+
24
+ const clients = new Map<WebSocket, ClientSubscription>();
25
+
26
+ // All events this bridge knows about
27
+ const ALL_EVENTS = ${JSON.stringify(allEvents, null, 2)};
28
+
29
+ /**
30
+ * Register WebSocket bridge with Fastify
31
+ */
32
+ export async function registerWebSocketBridge(fastify: FastifyInstance): Promise<void> {
33
+ // Register @fastify/websocket plugin
34
+ const websocketPlugin = await import('@fastify/websocket');
35
+ await fastify.register(websocketPlugin.default || websocketPlugin);
36
+
37
+ // WebSocket route
38
+ fastify.get('/ws', { websocket: true }, (socket: WebSocket) => {
39
+ const subscription: ClientSubscription = { ws: socket, events: new Set() };
40
+ clients.set(socket, subscription);
41
+
42
+ socket.on('message', (raw: Buffer) => {
43
+ try {
44
+ const msg = JSON.parse(raw.toString());
45
+
46
+ if (msg.type === 'subscribe' && msg.event) {
47
+ subscription.events.add(msg.event);
48
+ socket.send(JSON.stringify({ type: 'subscribed', event: msg.event }));
49
+ }
50
+
51
+ if (msg.type === 'unsubscribe' && msg.event) {
52
+ subscription.events.delete(msg.event);
53
+ socket.send(JSON.stringify({ type: 'unsubscribed', event: msg.event }));
54
+ }
55
+
56
+ if (msg.type === 'subscribe-all') {
57
+ ALL_EVENTS.forEach(e => subscription.events.add(e));
58
+ socket.send(JSON.stringify({ type: 'subscribed-all', events: ALL_EVENTS }));
59
+ }
60
+ } catch { /* ignore malformed messages */ }
61
+ });
62
+
63
+ socket.on('close', () => {
64
+ clients.delete(socket);
65
+ });
66
+
67
+ // Send initial connection confirmation
68
+ socket.send(JSON.stringify({
69
+ type: 'connected',
70
+ availableEvents: ALL_EVENTS,
71
+ }));
72
+ });
73
+
74
+ // Subscribe to all events and broadcast to interested clients
75
+ for (const eventName of ALL_EVENTS) {
76
+ eventBus.subscribe(eventName, (payload: any) => {
77
+ const message = JSON.stringify({
78
+ type: 'event',
79
+ event: eventName,
80
+ payload,
81
+ timestamp: new Date().toISOString(),
82
+ });
83
+
84
+ for (const [ws, sub] of clients) {
85
+ if (sub.events.has(eventName) && ws.readyState === 1) {
86
+ try { ws.send(message); } catch { /* client disconnected */ }
87
+ }
88
+ }
89
+ });
90
+ }
91
+ }
92
+ `;
93
+ }
94
+ export {
95
+ generateWebSocketBridge as default
96
+ };
@@ -16,6 +16,20 @@ function generateFastifyRoutes(context) {
16
16
  endpoints = curedToEndpoints(controller.cured, modelName);
17
17
  }
18
18
  }
19
+ if (controller.actions) {
20
+ if (!endpoints) endpoints = [];
21
+ for (const [actionName, action] of Object.entries(controller.actions)) {
22
+ const params = action.parameters || {};
23
+ const hasIdParam = Object.keys(params).some((p) => p === "id" || p === `${modelName?.charAt(0).toLowerCase()}${modelName?.slice(1)}Id`);
24
+ endpoints.push({
25
+ operation: actionName,
26
+ method: "POST",
27
+ path: hasIdParam ? `/:id/${actionName}` : `/${actionName}`,
28
+ parameters: params,
29
+ description: action.description || `Custom action: ${actionName}`
30
+ });
31
+ }
32
+ }
19
33
  if (!endpoints || endpoints.length === 0) {
20
34
  console.warn(`Warning: Controller ${controllerName} has no endpoints. Generating empty routes file.`);
21
35
  }
@@ -76,7 +90,7 @@ function generateRouteHandler(endpoint, modelName, handlerName, isModelControlle
76
90
  }
77
91
  const method = endpoint.method?.toLowerCase() || inferHttpMethod(operation);
78
92
  const path = inferPath(operation, endpoint);
79
- const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType);
93
+ const handler = generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint);
80
94
  let route = `// ${operation} ${modelName}
81
95
  `;
82
96
  route += `fastify.${method}('${path}', {
@@ -111,7 +125,7 @@ function generateRouteHandler(endpoint, modelName, handlerName, isModelControlle
111
125
  route += `});`;
112
126
  return route;
113
127
  }
114
- function generateHandlerBody(operation, modelName, handlerName, isModelController, implType) {
128
+ function generateHandlerBody(operation, modelName, handlerName, isModelController, implType, endpoint) {
115
129
  const rawLowerModel = modelName?.toLowerCase() || "item";
116
130
  const RESERVED_WORDS = /* @__PURE__ */ new Set(["import", "export", "default", "class", "function", "return", "delete", "new", "this", "switch", "case", "break", "continue", "for", "while", "do", "if", "else", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "let", "const", "var", "void", "with", "yield", "async", "await", "enum", "implements", "interface", "package", "private", "protected", "public", "static", "super", "extends"]);
117
131
  const lowerModel = RESERVED_WORDS.has(rawLowerModel) ? `${rawLowerModel}Item` : rawLowerModel;
@@ -199,16 +213,20 @@ function generateHandlerBody(operation, modelName, handlerName, isModelControlle
199
213
  message: error instanceof Error ? error.message : String(error)
200
214
  });
201
215
  }`;
202
- default:
216
+ default: {
217
+ const paramNames = endpoint?.parameters ? Object.keys(endpoint.parameters) : [];
218
+ const callArgs = paramNames.length > 0 ? paramNames.map((p) => `body.${p}`).join(", ") : "body";
203
219
  return `try {
204
- const result = await handler.${operation}(request.body as any);
205
- return reply.send(result);
220
+ const body = (request.body || {}) as Record<string, any>;
221
+ const result = await handler.${operation}(${callArgs});
222
+ return reply.send(result || { success: true });
206
223
  } catch (error) {
207
224
  return reply.status(400).send({
208
225
  error: 'Failed to execute ${operation}',
209
226
  message: error instanceof Error ? error.message : String(error)
210
227
  });
211
228
  }`;
229
+ }
212
230
  }
213
231
  }
214
232
  function inferOperationFromMethodAndPath(method, path) {
@@ -227,10 +245,8 @@ function inferHttpMethod(operation) {
227
245
  return sharedInferHttpMethod(operation);
228
246
  }
229
247
  function inferPath(operation, endpoint) {
230
- if (endpoint?.path && endpoint.serviceOperation?.type === "custom") {
231
- const pathParts = endpoint.path.split("/").filter((p) => p);
232
- const lastPart = pathParts[pathParts.length - 1];
233
- return `/${lastPart}`;
248
+ if (endpoint?.path) {
249
+ return endpoint.path;
234
250
  }
235
251
  return sharedInferPath(operation);
236
252
  }
@@ -10,6 +10,12 @@ function generateFastifyServer(context) {
10
10
  const path = deriveBasePath(name);
11
11
  return ` await fastify.register(${name}Routes, { prefix: '${path}', controllers: { ${name}Controller: new (await import('./controllers/${name}Controller.js')).${name}Controller() } });`;
12
12
  }).join("\n");
13
+ const specEvents = spec.events ? Object.keys(spec.events) : [];
14
+ const hasEvents = specEvents.length > 0 || modelNames.length > 0;
15
+ const servicesList = spec.services ? Object.keys(spec.services) : [];
16
+ const serviceImports = servicesList.map(
17
+ (name) => `import './${name.charAt(0).toLowerCase() + name.slice(1)}.js'; // Initialize ${name} event subscriptions`
18
+ );
13
19
  return `/**
14
20
  * Fastify Server
15
21
  * Generated from SpecVerse specification
@@ -18,6 +24,8 @@ function generateFastifyServer(context) {
18
24
  import Fastify from 'fastify';
19
25
  import cors from '@fastify/cors';
20
26
  import { PrismaClient } from '@prisma/client';
27
+ ${hasEvents ? `import { eventBus } from './events/eventBus.js';
28
+ import { registerWebSocketBridge } from './events/websocket-bridge.js';` : ""}
21
29
 
22
30
  // Initialize Prisma
23
31
  export const prisma = new PrismaClient();
@@ -42,13 +50,19 @@ fastify.get('/api/spec', async () => embeddedSpec);
42
50
  fastify.get('/api/runtime/info', async () => ({
43
51
  controllers: ${JSON.stringify(modelNames.map((n) => `${n}Controller`))},
44
52
  models: ${JSON.stringify(modelNames)},
45
- events: [],
46
- services: []
53
+ events: ${JSON.stringify(specEvents)},
54
+ services: ${JSON.stringify(servicesList)}
47
55
  }));
48
56
 
57
+ ${hasEvents ? `// Event history endpoint
58
+ fastify.get('/api/events', async () => eventBus.getHistory());` : ""}
59
+
49
60
  // Register routes
50
61
  ${routeImports}
51
62
 
63
+ ${serviceImports.length > 0 ? `// Initialize services (registers event subscriptions)
64
+ ${serviceImports.join("\n")}` : ""}
65
+
52
66
  async function registerRoutes() {
53
67
  ${routeRegistrations}
54
68
  }
@@ -57,10 +71,14 @@ ${routeRegistrations}
57
71
  const start = async () => {
58
72
  try {
59
73
  await registerRoutes();
74
+ ${hasEvents ? ` // Register WebSocket bridge for real-time frontend events
75
+ await registerWebSocketBridge(fastify);` : ""}
60
76
  const port = parseInt(process.env.PORT || '3000');
61
77
  await fastify.listen({ port, host: '0.0.0.0' });
62
78
  console.log(\`Server running at http://localhost:\${port}\`);
63
79
  console.log(\`API endpoints: ${modelNames.map((n) => `/api/${n.toLowerCase()}s`).join(", ")}\`);
80
+ ${hasEvents ? ` console.log(\`WebSocket: ws://localhost:\${port}/ws\`);
81
+ console.log(\`Events: ${specEvents.join(", ")}\`);` : ""}
64
82
  } catch (err) {
65
83
  fastify.log.error(err);
66
84
  process.exit(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
+ };