@specverse/engines 4.1.14 → 4.1.15

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 (57) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +7 -1
  2. package/dist/ai/behavior-ai-service.d.ts +2 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -1
  4. package/dist/ai/behavior-ai-service.js +2 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -1
  6. package/dist/ai/prompt-loader.js +2 -2
  7. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  8. package/dist/inference/quint-transpiler.js +204 -4
  9. package/dist/inference/quint-transpiler.js.map +1 -1
  10. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  11. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  12. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  13. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +81 -22
  14. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
  15. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
  16. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  17. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
  18. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
  19. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
  20. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  21. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
  22. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  23. package/dist/realize/index.d.ts.map +1 -1
  24. package/dist/realize/index.js +138 -23
  25. package/dist/realize/index.js.map +1 -1
  26. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  27. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  28. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  29. package/libs/instance-factories/cli/templates/commander/command-generator.ts +99 -22
  30. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
  31. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
  32. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  33. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
  34. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
  35. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
  36. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  37. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
  38. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  39. package/package.json +1 -1
  40. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  41. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  42. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  43. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  44. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  45. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  46. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  47. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  48. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  49. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  50. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  57. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -13,6 +13,7 @@ function generatePrismaController(context) {
13
13
  const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
14
14
  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"]);
15
15
  const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
16
+ const prismaDelegate = RESERVED_WORDS.has(rawModelVar) ? `prisma['${rawModelVar}']` : `prisma.${rawModelVar}`;
16
17
  const curedOps = controller.cured || {};
17
18
  const idAttr = (Array.isArray(model.attributes) ? model.attributes : Object.values(model.attributes || {})).find((a) => a.name === "id");
18
19
  const idType = idAttr?.type || "UUID";
@@ -40,11 +41,11 @@ function parseId(id: string): ${needsIntParse ? "number" : "string"} {
40
41
  */
41
42
  export class ${controllerName} {
42
43
  ${generateValidateMethod(model, modelName)}
43
- ${curedOps.create ? generateCreateMethod(model, modelName, modelVar, controller, allModels) : ""}
44
- ${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar) : ""}
45
- ${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ""}
46
- ${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ""}
47
- ${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ""}
44
+ ${curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ""}
45
+ ${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : ""}
46
+ ${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ""}
47
+ ${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) : ""}
48
+ ${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) : ""}
48
49
  ${customActions.code}
49
50
  }
50
51
 
@@ -116,9 +117,7 @@ function generateValidationLogic(model, dataParam = "_data", contextParam = "_co
116
117
  });
117
118
  return validations.join("\n") || "// No validation rules defined";
118
119
  }
119
- function generateCreateMethod(model, modelName, modelVar, controller, allModels) {
120
- const hasEvents = controller.publishes && Array.isArray(controller.publishes);
121
- const createEvent = hasEvents ? controller.publishes.find((e) => e.includes("Created")) : null;
120
+ function generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
122
121
  return `
123
122
  /**
124
123
  * Create a new ${modelName}
@@ -135,29 +134,24 @@ function generateCreateMethod(model, modelName, modelVar, controller, allModels)
135
134
  ${generateFKTransform(model, "prismaData", allModels)}
136
135
 
137
136
  // Create record
138
- const ${modelVar} = await prisma.${modelVar}.create({
137
+ const ${modelVar} = await ${prismaDelegate}.create({
139
138
  data: prismaData${generateIncludeRelationships(model)}
140
139
  });
141
140
 
142
- ${createEvent ? `
143
- // Publish event
144
- eventBus.publish(EventName.${createEvent}, {
145
- ${modelVar},
146
- timestamp: new Date().toISOString()
147
- });
148
- ` : ""}
141
+ // Publish CURED event
142
+ await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
149
143
 
150
144
  return ${modelVar};
151
145
  }
152
146
  `;
153
147
  }
154
- function generateRetrieveMethod(model, modelName, modelVar) {
148
+ function generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) {
155
149
  return `
156
150
  /**
157
151
  * Retrieve ${modelName} by ID
158
152
  */
159
153
  public async retrieve(id: string): Promise<any> {
160
- const ${modelVar} = await prisma.${modelVar}.findUnique({
154
+ const ${modelVar} = await ${prismaDelegate}.findUnique({
161
155
  where: { id: parseId(id) }${generateIncludeRelationships(model)}
162
156
  });
163
157
 
@@ -172,16 +166,14 @@ function generateRetrieveMethod(model, modelName, modelVar) {
172
166
  * Retrieve all ${modelName}s
173
167
  */
174
168
  public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
175
- return await prisma.${modelVar}.findMany({
169
+ return await ${prismaDelegate}.findMany({
176
170
  skip: options.skip,
177
171
  take: options.take${generateIncludeRelationships(model)}
178
172
  });
179
173
  }
180
174
  `;
181
175
  }
182
- function generateUpdateMethod(model, modelName, modelVar, controller, allModels) {
183
- const hasEvents = controller.publishes && Array.isArray(controller.publishes);
184
- const updateEvent = hasEvents ? controller.publishes.find((e) => e.includes("Updated")) : null;
176
+ function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
185
177
  return `
186
178
  /**
187
179
  * Update ${modelName}
@@ -206,24 +198,19 @@ function generateUpdateMethod(model, modelName, modelVar, controller, allModels)
206
198
  ${generateFKTransform(model, "updateData", allModels)}
207
199
 
208
200
  // Update record
209
- const ${modelVar} = await prisma.${modelVar}.update({
201
+ const ${modelVar} = await ${prismaDelegate}.update({
210
202
  where: { id: parseId(id) },
211
203
  data: updateData${generateIncludeRelationships(model)}
212
204
  });
213
205
 
214
- ${updateEvent ? `
215
- // Publish event
216
- eventBus.publish(EventName.${updateEvent}, {
217
- ${modelVar},
218
- timestamp: new Date().toISOString()
219
- });
220
- ` : ""}
206
+ // Publish CURED event
207
+ await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
221
208
 
222
209
  return ${modelVar};
223
210
  }
224
211
  `;
225
212
  }
226
- function generateEvolveMethod(model, modelName, modelVar, controller) {
213
+ function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) {
227
214
  const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
228
215
  const lifecycle = lifecycles[0];
229
216
  const lifecycleName = lifecycle?.name || "status";
@@ -242,7 +229,7 @@ function generateEvolveMethod(model, modelName, modelVar, controller) {
242
229
  }
243
230
 
244
231
  // Get current record to check lifecycle state
245
- const current = await prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
232
+ const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
246
233
  if (!current) {
247
234
  throw new Error('${modelName} not found');
248
235
  }
@@ -261,41 +248,35 @@ function generateEvolveMethod(model, modelName, modelVar, controller) {
261
248
  ` : ""}
262
249
 
263
250
  // Update record
264
- const ${modelVar} = await prisma.${modelVar}.update({
251
+ const ${modelVar} = await ${prismaDelegate}.update({
265
252
  where: { id: parseId(id) },
266
253
  data${generateIncludeRelationships(model)}
267
254
  });
268
255
 
256
+ // Publish CURED event
257
+ await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
258
+
269
259
  return ${modelVar};
270
260
  }
271
261
  `;
272
262
  }
273
- function generateDeleteMethod(model, modelName, modelVar, controller) {
274
- const hasEvents = controller.publishes && Array.isArray(controller.publishes);
275
- const deleteEvent = hasEvents ? controller.publishes.find((e) => e.includes("Deleted")) : null;
263
+ function generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) {
276
264
  return `
277
265
  /**
278
266
  * Delete ${modelName}
279
267
  */
280
268
  public async delete(id: string): Promise<void> {
281
- ${deleteEvent ? `
282
269
  // Get record before deletion for event
283
- const ${modelVar} = await prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
284
- ` : ""}
270
+ const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
285
271
 
286
- await prisma.${modelVar}.delete({
272
+ await ${prismaDelegate}.delete({
287
273
  where: { id: parseId(id) }
288
274
  });
289
275
 
290
- ${deleteEvent ? `
291
- // Publish event
276
+ // Publish CURED event
292
277
  if (${modelVar}) {
293
- eventBus.publish(EventName.${deleteEvent}, {
294
- ${modelVar},
295
- timestamp: new Date().toISOString()
296
- });
278
+ await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
297
279
  }
298
- ` : ""}
299
280
  }
300
281
  `;
301
282
  }
@@ -417,13 +398,7 @@ ${includes}
417
398
  }`;
418
399
  }
419
400
  function hasEventPublishing(curedOps, controller) {
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;
401
+ return true;
427
402
  }
428
403
  export {
429
404
  generatePrismaController as default
@@ -6,23 +6,27 @@ function generatePrismaService(context) {
6
6
  }
7
7
  const serviceName = service.name;
8
8
  const hasEvents = service.publishes && service.publishes.length > 0 || service.subscribes && service.subscribes.length > 0;
9
+ const operationsCode = generateOperationsWithHelpers(service);
10
+ const hasAiBehaviors = /\baiBehaviors\./.test(operationsCode);
11
+ const usesPrisma = /\bprisma\b/.test(operationsCode);
9
12
  return `/**
10
13
  * ${serviceName}
11
14
  * Abstract business logic service
12
15
  * ${service.description || ""}
13
16
  */
14
-
15
- import { PrismaClient } from '@prisma/client';
16
- ${hasEvents ? `import { eventBus, EventName } from '../events/eventBus.js';` : ""}
17
-
18
- const prisma = new PrismaClient();
17
+ ${usesPrisma ? `
18
+ import { PrismaClient } from '@prisma/client';` : ""}
19
+ ${hasEvents ? `import { eventBus } from '../events/eventBus.js';` : ""}
20
+ ${hasAiBehaviors ? `import * as aiBehaviors from '../behaviors/${serviceName}.ai.js';` : ""}
21
+ ${usesPrisma ? `
22
+ const prisma = new PrismaClient();` : ""}
19
23
 
20
24
  /**
21
25
  * ${serviceName} class
22
26
  */
23
27
  export class ${serviceName} {
24
28
  ${generateConstructor(service)}
25
- ${generateOperationsWithHelpers(service)}
29
+ ${operationsCode}
26
30
  ${generateEventSubscriptions(service)}
27
31
  }
28
32
 
@@ -97,8 +101,10 @@ function generateOperations(service) {
97
101
  return operations.join("\n");
98
102
  }
99
103
  function generateOperation(operationName, operation, service) {
100
- const params = generateOperationParams(operation);
104
+ const rawParams = generateOperationParams(operation);
101
105
  const hasPublish = service.publishes && service.publishes.length > 0;
106
+ const body = generateOperationLogic(operation, service);
107
+ const params = renameUnusedParams(rawParams, body);
102
108
  return `
103
109
  /**
104
110
  * ${operationName}
@@ -106,7 +112,7 @@ function generateOperation(operationName, operation, service) {
106
112
  */
107
113
  public async ${operationName}(${params}): Promise<any> {
108
114
  try {
109
- ${generateOperationLogic(operation, service)}
115
+ ${body}
110
116
 
111
117
  ${hasPublish ? `
112
118
  // Publish event (example)
@@ -125,6 +131,19 @@ function generateOperation(operationName, operation, service) {
125
131
  }
126
132
  `;
127
133
  }
134
+ function renameUnusedParams(paramsString, body) {
135
+ if (!paramsString.trim()) return paramsString;
136
+ return paramsString.split(",").map((segment) => {
137
+ const trimmed = segment.trim();
138
+ const nameMatch = trimmed.match(/^(\w+)/);
139
+ if (!nameMatch) return segment;
140
+ const name = nameMatch[1];
141
+ if (name.startsWith("_")) return segment;
142
+ const re = new RegExp(`\\b${name}\\b`);
143
+ if (re.test(body)) return segment;
144
+ return segment.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
145
+ }).join(", ");
146
+ }
128
147
  function generateOperationParams(operation) {
129
148
  if (!operation.parameters || Object.keys(operation.parameters).length === 0) {
130
149
  return "params: any = {}";
@@ -149,17 +168,19 @@ function generateOperationLogic(operation, service) {
149
168
  }
150
169
  if (impl.preconditions?.length || impl.postconditions?.length || impl.steps?.length || impl.transactional) {
151
170
  const modelName = inferModelFromServiceName(service.name);
171
+ const parameterNames = Object.keys(operation.parameters || {});
152
172
  const context = {
153
173
  modelName,
154
174
  serviceName: service.name,
155
175
  operationName: operation.name || "execute",
156
- prismaModel: modelName
176
+ prismaModel: modelName,
177
+ parameterNames
157
178
  };
158
179
  const behavior = {
159
180
  preconditions: impl.preconditions || [],
160
181
  postconditions: impl.postconditions || [],
161
182
  sideEffects: impl.sideEffects || [],
162
- steps: impl.steps || [],
183
+ steps: impl.steps || operation.steps || [],
163
184
  transactional: impl.transactional || false
164
185
  };
165
186
  const opMeta = {
@@ -263,7 +263,7 @@ function matchStep(step, ctx) {
263
263
  const declared = Array.from(ctx.declaredVars || []);
264
264
  const paramNames = ctx.parameterNames || [];
265
265
  const inputs = [...paramNames, ...declared];
266
- const resultVar = `step${ctx.stepNum}Result`;
266
+ const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
267
267
  const inputObj = inputs.length > 0 ? `{ ${inputs.join(", ")} }` : "{}";
268
268
  if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
269
269
  return {
@@ -16,27 +16,57 @@ function generateReactComponent(context) {
16
16
  const hasMany = getHasManyRelationships(model);
17
17
  const lifecycle = getLifecycle(model);
18
18
  const classified = classifyAttrs(attrs, lifecycle);
19
+ let body;
19
20
  switch (viewType) {
20
21
  case "list":
21
- return generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
22
+ body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
23
+ break;
22
24
  case "detail":
23
- return generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
25
+ body = generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
26
+ break;
24
27
  case "form":
25
- return generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
28
+ body = generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
29
+ break;
26
30
  case "dashboard":
27
- return generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
31
+ body = generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
32
+ break;
28
33
  case "board":
29
34
  case "workflow":
30
- return generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
35
+ body = generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
36
+ break;
31
37
  case "timeline":
32
- return generateTimelineView(componentName, modelName, lower, plural, api, view);
38
+ body = generateTimelineView(componentName, modelName, lower, plural, api, view);
39
+ break;
33
40
  case "calendar":
34
- return generateCalendarView(componentName, modelName, lower, plural, api, view, model);
41
+ body = generateCalendarView(componentName, modelName, lower, plural, api, view, model);
42
+ break;
35
43
  case "analytics":
36
- return generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
44
+ body = generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
45
+ break;
37
46
  default:
38
- return generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
47
+ body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
39
48
  }
49
+ return stripUnusedImports(body);
50
+ }
51
+ function stripUnusedImports(source) {
52
+ const lines = source.split("\n");
53
+ const out = [];
54
+ let i = 0;
55
+ while (i < lines.length) {
56
+ const line = lines[i];
57
+ const match = line.match(/^import\s+\{\s*([^}]+?)\s*\}\s+from\s+(['"][^'"]+['"]);?\s*$/);
58
+ if (!match) break;
59
+ const names = match[1].split(",").map((s) => s.trim()).filter(Boolean);
60
+ const from = match[2];
61
+ const rest = lines.slice(i + 1).join("\n");
62
+ const used = names.filter((n) => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(rest));
63
+ if (used.length === 0) {
64
+ } else {
65
+ out.push(`import { ${used.join(", ")} } from ${from};`);
66
+ }
67
+ i++;
68
+ }
69
+ return [...out, ...lines.slice(i)].join("\n");
40
70
  }
41
71
  function getModelAttributes(model) {
42
72
  if (!model?.attributes) return [];
@@ -351,7 +381,7 @@ ${lifecycle.states.map((s) => ` <option value="${s}">${s.replace(/[_-]/
351
381
  </select>`;
352
382
  }
353
383
  const t = type.toLowerCase();
354
- if (t === "boolean") return ` <input type="checkbox" name="${n}" checked={!!form.${n}} onChange={e => setForm(f => ({...f, ${n}: e.target.checked}))} className="h-4 w-4 text-blue-600 rounded" />`;
384
+ if (t === "boolean") return ` <input type="checkbox" name="${n}" checked={!!form.${n}} onChange={e => setForm((f: any) => ({...f, ${n}: e.target.checked}))} className="h-4 w-4 text-blue-600 rounded" />`;
355
385
  if (t.includes("date") || t.includes("timestamp")) return ` <input type="datetime-local" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
356
386
  if (t === "integer" || t === "number" || t === "money" || t === "decimal" || t === "float") return ` <input type="number" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
357
387
  if (n.toLowerCase().includes("description") || n.toLowerCase().includes("content") || n.toLowerCase().includes("body")) return ` <textarea name="${n}" value={form.${n} || ''} onChange={handleChange} rows={4} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AASnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA4d9F;;;OAGG;YACW,UAAU;IAwDxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AASnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAklB9F;;;OAGG;YACW,UAAU;IAwDxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -69,6 +69,9 @@ class SpecVerseRealizeEngine {
69
69
  const errors = [];
70
70
  const allModels = Object.values(spec.models || {});
71
71
  const writeOutput = (output) => {
72
+ // A generator can signal "skip this file" by returning an empty string.
73
+ if (output.code === '')
74
+ return;
72
75
  const dir = dirname(output.filePath);
73
76
  if (!existsSync(dir))
74
77
  mkdirSync(dir, { recursive: true });
@@ -203,17 +206,116 @@ class SpecVerseRealizeEngine {
203
206
  // 3. Services
204
207
  const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
205
208
  if (ctrlResolved?.instanceFactory?.codeTemplates?.services) {
209
+ // Load helpers for pre-scanning unmatched steps + generating AI behavior files
210
+ let scanUnmatched = null;
211
+ let generateAiFile = null;
212
+ try {
213
+ const scanPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'step-conventions.ts');
214
+ const scanResolved = resolveGenPath(scanPath);
215
+ if (scanResolved) {
216
+ const mod = await import(scanResolved);
217
+ scanUnmatched = mod.matchStep;
218
+ }
219
+ const aiGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'ai-behaviors-generator.ts');
220
+ const aiResolved = resolveGenPath(aiGenPath);
221
+ if (aiResolved) {
222
+ const mod = await import(aiResolved);
223
+ generateAiFile = mod.generateAiBehaviorsFile;
224
+ }
225
+ }
226
+ catch { /* non-fatal */ }
227
+ /**
228
+ * Pre-scan a service's operations to determine which steps don't match
229
+ * conventions and will need AI-generated behavior functions. Runs the
230
+ * same matchStep logic the service-generator uses, but in this pipeline's
231
+ * module context so we don't need cross-module state.
232
+ */
233
+ const scanServiceUnmatched = (service) => {
234
+ if (!scanUnmatched)
235
+ return [];
236
+ const operations = service.operations || {};
237
+ const entries = Array.isArray(operations)
238
+ ? operations.map((op) => [op.name, op])
239
+ : Object.entries(operations);
240
+ const unmatched = [];
241
+ const seenFunctions = new Set();
242
+ for (const [opName, operation] of entries) {
243
+ const steps = operation.steps || operation.implementation?.steps || [];
244
+ const parameterNames = Object.keys(operation.parameters || {});
245
+ const declaredVars = new Set();
246
+ // Simulate precondition variable declarations
247
+ const preconditions = operation.requires || operation.preconditions || [];
248
+ for (const pc of preconditions) {
249
+ const m = typeof pc === 'string' ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
250
+ if (m)
251
+ declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
252
+ }
253
+ for (let i = 0; i < steps.length; i++) {
254
+ const stepInput = steps[i];
255
+ const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
256
+ const stepAs = typeof stepInput === 'object' ? stepInput?.as : undefined;
257
+ const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
258
+ if (typeof stepText !== 'string')
259
+ continue;
260
+ const ctx = {
261
+ modelName: service.name.replace(/Service$/, ''),
262
+ prismaModel: service.name.replace(/Service$/, '').charAt(0).toLowerCase() + service.name.replace(/Service$/, '').slice(1),
263
+ serviceName: service.name,
264
+ operationName: opName,
265
+ stepNum: i + 1,
266
+ parameterNames,
267
+ declaredVars,
268
+ resultName: stepAs,
269
+ };
270
+ const result = scanUnmatched(stepText, ctx);
271
+ if (!result.matched && result.functionName && !seenFunctions.has(result.functionName)) {
272
+ seenFunctions.add(result.functionName);
273
+ unmatched.push({
274
+ step: stepText,
275
+ functionName: result.functionName,
276
+ operationName: opName,
277
+ parameterNames,
278
+ inputs: result.inputs || [],
279
+ returns: stepReturns,
280
+ modelName: service.name, // AI file uses service name as owner
281
+ });
282
+ }
283
+ }
284
+ }
285
+ return unmatched;
286
+ };
287
+ let svcAiCount = 0;
206
288
  for (const service of servicesList) {
207
289
  try {
208
290
  const svcName = service.name || 'Service';
209
291
  const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'services', { spec, service: { name: svcName, ...service } }, { outputDir });
210
292
  writeOutput(output);
293
+ // Pre-scan this service for unmatched steps and generate AI behaviors file if any
294
+ if (generateAiFile) {
295
+ const unmatched = scanServiceUnmatched(service);
296
+ if (unmatched.length > 0) {
297
+ const aiFile = await generateAiFile({
298
+ ownerName: svcName,
299
+ unmatchedFunctions: unmatched,
300
+ availableModels: allModels.map((m) => m.name),
301
+ spec,
302
+ });
303
+ if (aiFile) {
304
+ const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${svcName}.ai.ts`);
305
+ const dir = dirname(filePath);
306
+ if (!existsSync(dir))
307
+ mkdirSync(dir, { recursive: true });
308
+ writeFileSync(filePath, aiFile);
309
+ svcAiCount++;
310
+ }
311
+ }
312
+ }
211
313
  }
212
314
  catch (e) {
213
315
  errors.push(`Service: ${e.message}`);
214
316
  }
215
317
  }
216
- console.log(` ✅ Services: ${servicesList.length} service(s)`);
318
+ console.log(` ✅ Services: ${servicesList.length} service(s)${svcAiCount > 0 ? ` (${svcAiCount} AI behaviors file${svcAiCount > 1 ? 's' : ''})` : ''}`);
217
319
  }
218
320
  // 4. Routes (per model) — use spec controllers for endpoint data
219
321
  const routeResolved = tryResolve('api.rest');
@@ -374,7 +476,16 @@ class SpecVerseRealizeEngine {
374
476
  }
375
477
  if (frontendFiles.length)
376
478
  console.log(` ✅ Frontend application: ${frontendFiles.join(', ')}`);
377
- // 9a. Generate shared view utilities and Tailwind config
479
+ // 9a. Generate shared view utilities and Tailwind config.
480
+ // The frontend factory may declare outputStructure=standalone with
481
+ // frontendDir="." — in that case views/lib live at the output root
482
+ // instead of under `frontend/`.
483
+ const frontendCfg = frontendResolved?.configuration || frontendResolved?.instanceFactory?.configuration || {};
484
+ const frontendOutputStructure = frontendCfg.outputStructure || 'monorepo';
485
+ const frontendRelDir = frontendOutputStructure === 'standalone'
486
+ ? '.'
487
+ : (frontendCfg.frontendDir || 'frontend');
488
+ const frontendDir = frontendRelDir === '.' ? outputDir : join(outputDir, frontendRelDir);
378
489
  try {
379
490
  const sharedUtilsGen = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'views', 'templates', 'react', 'shared-utils-generator.ts');
380
491
  const sharedUtilsResolved = resolveGenPath(sharedUtilsGen);
@@ -382,7 +493,7 @@ class SpecVerseRealizeEngine {
382
493
  const genPath = sharedUtilsResolved;
383
494
  const { default: generateSharedUtils } = await import(genPath);
384
495
  const result = generateSharedUtils({ spec });
385
- const libDir = join(outputDir, 'frontend', 'src', 'lib');
496
+ const libDir = join(frontendDir, 'src', 'lib');
386
497
  if (!existsSync(libDir))
387
498
  mkdirSync(libDir, { recursive: true });
388
499
  for (const file of result.files) {
@@ -390,7 +501,6 @@ class SpecVerseRealizeEngine {
390
501
  }
391
502
  }
392
503
  // Tailwind config
393
- const frontendDir = join(outputDir, 'frontend');
394
504
  writeFileSync(join(frontendDir, 'tailwind.config.js'), `import path from 'path';
395
505
  import { createRequire } from 'module';
396
506
  const require = createRequire(import.meta.url);
@@ -419,26 +529,31 @@ export default {
419
529
  errors.push(`SharedUtils: ${e.message}`);
420
530
  }
421
531
  }
422
- // 9b. Runtime guards — transpiled from Quint specifications
423
- try {
424
- const { transpileEntityGuards, generateGuardsModule } = await import('../inference/index.js');
425
- const { createRequire } = await import('module');
426
- const req = createRequire(import.meta.url);
427
- const entitiesPkg = req.resolve('@specverse/entities/package.json');
428
- const entitiesSrc = join(dirname(entitiesPkg), 'src');
429
- const guards = transpileEntityGuards(entitiesSrc);
430
- if (guards.length > 0) {
431
- const guardsCode = generateGuardsModule(guards);
432
- const guardsPath = join(outputDir, 'backend', 'src', 'guards.ts');
433
- const guardsDir = dirname(guardsPath);
434
- if (!existsSync(guardsDir))
435
- mkdirSync(guardsDir, { recursive: true });
436
- writeFileSync(guardsPath, guardsCode);
437
- console.log(` ✅ Runtime guards: ${guards.length} guards (${guards.filter((g) => g.kind === 'function').length} functions, ${guards.filter((g) => g.kind === 'invariant').length} invariants) from Quint specs`);
532
+ // 9b. Runtime guards — transpiled from Quint specifications.
533
+ // Only emit when the manifest declares a backend; a frontend-only
534
+ // layout has no `backend/src/` to write into.
535
+ const hasBackendForGuards = !!(tryResolve('service.controller') || tryResolve('api.rest') || tryResolve('orm.schema'));
536
+ if (hasBackendForGuards) {
537
+ try {
538
+ const { transpileEntityGuards, generateGuardsModule } = await import('../inference/index.js');
539
+ const { createRequire } = await import('module');
540
+ const req = createRequire(import.meta.url);
541
+ const entitiesPkg = req.resolve('@specverse/entities/package.json');
542
+ const entitiesSrc = join(dirname(entitiesPkg), 'src');
543
+ const guards = transpileEntityGuards(entitiesSrc);
544
+ if (guards.length > 0) {
545
+ const guardsCode = generateGuardsModule(guards);
546
+ const guardsPath = join(outputDir, 'backend', 'src', 'guards.ts');
547
+ const guardsDir = dirname(guardsPath);
548
+ if (!existsSync(guardsDir))
549
+ mkdirSync(guardsDir, { recursive: true });
550
+ writeFileSync(guardsPath, guardsCode);
551
+ console.log(` ✅ Runtime guards: ${guards.length} guards (${guards.filter((g) => g.kind === 'function').length} functions, ${guards.filter((g) => g.kind === 'invariant').length} invariants) from Quint specs`);
552
+ }
553
+ }
554
+ catch (e) {
555
+ errors.push(`Guards: ${e.message}`);
438
556
  }
439
- }
440
- catch (e) {
441
- errors.push(`Guards: ${e.message}`);
442
557
  }
443
558
  // 10. CLI commands (Commander.js)
444
559
  try {