@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
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
function generateEventBus(context) {
|
|
2
2
|
const { spec } = context;
|
|
3
|
-
const
|
|
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
|
-
*
|
|
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
|
-
//
|
|
13
|
-
${
|
|
14
|
+
// Re-export types for consumers
|
|
15
|
+
${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ""}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
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
|
|
47
|
+
* Publish a typed event
|
|
43
48
|
*/
|
|
44
|
-
public publish<
|
|
45
|
-
|
|
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
|
|
56
|
+
* Subscribe to a typed event
|
|
51
57
|
*/
|
|
52
|
-
public subscribe<
|
|
53
|
-
event:
|
|
54
|
-
handler: (payload:
|
|
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
|
-
*
|
|
67
|
+
* Get event history
|
|
67
68
|
*/
|
|
68
|
-
public
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
205
|
-
|
|
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
|
|
231
|
-
|
|
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
|
+
};
|