@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
@@ -19,7 +19,7 @@ function generateCommand(context) {
19
19
  const alias = flag.alias ? `${flag.alias}, ` : "";
20
20
  const flagType = flag.type?.toLowerCase();
21
21
  const valuePart = flagType === "boolean" ? "" : ` <${flagName.replace(/^--/, "")}>`;
22
- const defaultVal = flag.default !== void 0 ? `, ${JSON.stringify(flag.default)}` : "";
22
+ const defaultVal = flag.default !== void 0 ? `, ${commanderDefault(flag.default, flagType)}` : "";
23
23
  const desc = flag.description || `${flagName} option`;
24
24
  return ` .option('${alias}${flagName}${valuePart}', '${desc}'${defaultVal})`;
25
25
  });
@@ -47,7 +47,16 @@ function generateCommand(context) {
47
47
  if (ENGINE_HANDLERS[key]?.imports) allImportSets.push(ENGINE_HANDLERS[key].imports);
48
48
  }
49
49
  }
50
- const engineImports = deduplicateImports(allImportSets);
50
+ const registerBody = hasSubcommands ? generateCommandWithSubcommands(name, description, subcommands, optionDefs) : generateLeafCommand(name, description, commandStr, optionDefs, actionParams, serviceRef, exitCodes);
51
+ const bodyForUsage = `${serviceImport}
52
+ ${registerBody}
53
+ ${subcommandRegistrations}`;
54
+ const commandOptionsUsed = new RegExp("\\bCommandOptions\\b").test(bodyForUsage);
55
+ const engineImports = deduplicateImports(allImportSets, bodyForUsage);
56
+ const commandOptionsDecl = commandOptionsUsed ? `interface CommandOptions {
57
+ ${optionTypes.length > 0 ? optionTypes.join("\n") : " [key: string]: any;"}
58
+ }
59
+ ` : "";
51
60
  return `/**
52
61
  * ${name} command
53
62
  * ${description}
@@ -58,10 +67,7 @@ import { Command } from 'commander';
58
67
  ${serviceImport}
59
68
  ${engineImports}
60
69
 
61
- interface CommandOptions {
62
- ${optionTypes.length > 0 ? optionTypes.join("\n") : " [key: string]: any;"}
63
- }
64
-
70
+ ${commandOptionsDecl}
65
71
  ${exitCodeComments ? `/**
66
72
  * Exit codes:
67
73
  ${exitCodeComments}
@@ -71,7 +77,7 @@ ${exitCodeComments}
71
77
  * Register the ${name} command on the program.
72
78
  */
73
79
  export function register${capitalize(name)}Command(program: Command): void {
74
- ${hasSubcommands ? generateCommandWithSubcommands(name, description, subcommands, optionDefs) : generateLeafCommand(name, description, commandStr, optionDefs, actionParams, serviceRef, exitCodes)}
80
+ ${registerBody}
75
81
  }
76
82
  ${subcommandRegistrations}
77
83
  `;
@@ -243,6 +249,28 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
243
249
  }
244
250
  }
245
251
 
252
+ // Merge service operation steps from original spec into inferred services
253
+ // Inference generates service shells with preconditions/returns but drops
254
+ // the declarative steps arrays \u2014 we restore them from the parsed AST.
255
+ const origServices = Array.isArray((origComponent as any).services)
256
+ ? (origComponent as any).services
257
+ : Object.values((origComponent as any).services || {});
258
+ const inferredServices = componentData.services || {};
259
+ for (const origSvc of origServices) {
260
+ const svcName = (origSvc as any).name;
261
+ if (!svcName) continue;
262
+ const inferredSvc = inferredServices[svcName];
263
+ if (!inferredSvc) continue;
264
+
265
+ const origOps = (origSvc as any).operations || {};
266
+ const inferredOps = inferredSvc.operations || {};
267
+ for (const [opName, origOp] of Object.entries(origOps) as [string, any][]) {
268
+ if (inferredOps[opName] && origOp.steps) {
269
+ inferredOps[opName].steps = origOp.steps;
270
+ }
271
+ }
272
+ }
273
+
246
274
  // Inject key as name into entity maps, and expand convention-format attributes
247
275
  // Iterate all object-valued sections (not hardcoded \u2014 covers any entity type)
248
276
  const entitySections = Object.keys(componentData).filter(k =>
@@ -311,7 +339,7 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
311
339
  if (!realizeEngine) { console.error('No realize engine found.'); process.exit(1); }
312
340
  await realizeEngine.initialize({ manifestPath: effectiveManifestPath, workingDir: (process.env.SPECVERSE_USER_CWD || process.cwd()) });
313
341
 
314
- const outputDir = options.output || resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), 'generated/code');
342
+ const outputDir = resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), options.output || 'generated/code');
315
343
  console.log('Realizing ' + type + ' from ' + file + '...');
316
344
  await (realizeEngine as any).realizeAll(inferredSpec, outputDir);
317
345
 
@@ -789,10 +817,12 @@ import { EngineRegistry } from '@specverse/entities';`,
789
817
  imports: ``,
790
818
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
791
819
  const manager = new SessionManager();
792
- const job = await manager.submit(sessionId, requirements, {
793
- jobId: options.jobId,
794
- outputPath: options.output,
795
- operation: options.operation,
820
+ const job = await manager.submit({
821
+ jobId: options.jobId || ('job-' + Date.now()),
822
+ sessionId,
823
+ operation: (options.operation || 'create') as 'create' | 'analyse' | 'materialise' | 'realize',
824
+ requirements,
825
+ output: options.output,
796
826
  });
797
827
  console.log('Job submitted: ' + job.jobId);
798
828
  console.log('Status: ' + job.status);
@@ -808,8 +838,10 @@ import { EngineRegistry } from '@specverse/entities';`,
808
838
  } else {
809
839
  console.log('ID: ' + (status.sessionId || status.jobId));
810
840
  console.log('Status: ' + status.status);
811
- if (status.created) console.log('Created: ' + new Date(status.created).toLocaleString());
812
- if (status.jobsProcessed !== undefined) console.log('Jobs: ' + status.jobsProcessed);
841
+ if (status.submitted) console.log('Submitted: ' + new Date(status.submitted).toLocaleString());
842
+ if (status.started) console.log('Started: ' + new Date(status.started).toLocaleString());
843
+ if (status.completed) console.log('Completed: ' + new Date(status.completed).toLocaleString());
844
+ if (status.duration) console.log('Duration: ' + status.duration);
813
845
  }`
814
846
  },
815
847
  "session.process": {
@@ -817,7 +849,7 @@ import { EngineRegistry } from '@specverse/entities';`,
817
849
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
818
850
  const manager = new SessionManager();
819
851
  console.log('Processing job: ' + jobId);
820
- await manager.process(jobId);
852
+ await manager.processJob(jobId);
821
853
  console.log('Job processed successfully');`
822
854
  }
823
855
  };
@@ -827,11 +859,12 @@ function generateLeafCommand(name, description, commandStr, optionDefs, actionPa
827
859
  const result = await service.execute(${actionParams.includes(":") ? "{ " + actionParams.split(",").map((p) => p.trim().split(":")[0].trim()).join(", ") + ", ...options }" : "options"});
828
860
  console.log(result);` : `console.log('Executing ${name}...');
829
861
  // TODO: Wire to service`;
830
- return `const cmd = program
862
+ const finalActionParams = renameUnusedActionParams(actionParams, handler);
863
+ return `program
831
864
  .command('${commandStr}')
832
865
  .description('${description}')
833
866
  ${optionDefs.join("\n")}
834
- .action(async (${actionParams}) => {
867
+ .action(async (${finalActionParams}) => {
835
868
  try {
836
869
  ${handler}
837
870
  } catch (error: any) {
@@ -840,6 +873,24 @@ ${optionDefs.join("\n")}
840
873
  }
841
874
  });`;
842
875
  }
876
+ function commanderDefault(value, flagType) {
877
+ if (flagType === "boolean") return JSON.stringify(Boolean(value));
878
+ if (typeof value === "number") return JSON.stringify(String(value));
879
+ return JSON.stringify(value);
880
+ }
881
+ function renameUnusedActionParams(actionParams, handler) {
882
+ if (!actionParams.trim()) return actionParams;
883
+ return actionParams.split(",").map((param) => {
884
+ const trimmed = param.trim();
885
+ const nameMatch = trimmed.match(/^(\w+)/);
886
+ if (!nameMatch) return param;
887
+ const name = nameMatch[1];
888
+ if (name.startsWith("_")) return param;
889
+ const re = new RegExp(`\\b${name}\\b`);
890
+ if (re.test(handler)) return param;
891
+ return param.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
892
+ }).join(", ");
893
+ }
843
894
  function generateCommandWithSubcommands(name, description, subcommands, optionDefs) {
844
895
  const subcmdRegistrations = Object.entries(subcommands).map(([subName, subDef]) => {
845
896
  const subDesc = subDef.description || "";
@@ -851,7 +902,7 @@ function generateCommandWithSubcommands(name, description, subcommands, optionDe
851
902
  const alias = flag.alias ? `${flag.alias}, ` : "";
852
903
  const flagType = flag.type?.toLowerCase();
853
904
  const valuePart = flagType === "boolean" ? "" : ` <${flagName.replace(/^--/, "")}>`;
854
- const defaultVal = flag.default !== void 0 ? `, ${JSON.stringify(flag.default)}` : "";
905
+ const defaultVal = flag.default !== void 0 ? `, ${commanderDefault(flag.default, flagType)}` : "";
855
906
  return ` .option('${alias}${flagName}${valuePart}', '${flag.description || flagName}'${defaultVal})`;
856
907
  });
857
908
  const handlerKey = `${name}.${subName}`;
@@ -859,12 +910,13 @@ function generateCommandWithSubcommands(name, description, subcommands, optionDe
859
910
  const subArgTypes = Object.entries(subArgs).filter(([_, a]) => a.positional).map(([n, a]) => `${n}: ${mapArgTypeToTS(a.type)}`);
860
911
  const subActionParams = subArgTypes.length > 0 ? subArgTypes.join(", ") + ", options: any" : "options: any";
861
912
  const handler = engineHandler ? engineHandler.handler : `console.log('${name} ${subName}: not yet implemented via engine');`;
913
+ const finalSubActionParams = renameUnusedActionParams(subActionParams, handler);
862
914
  return `
863
915
  cmd
864
916
  .command('${subCmdStr}')
865
917
  .description('${subDesc}')
866
918
  ${subOptionDefs.join("\n")}
867
- .action(async (${subActionParams}) => {
919
+ .action(async (${finalSubActionParams}) => {
868
920
  try {
869
921
  ${handler}
870
922
  } catch (error: any) {
@@ -899,10 +951,14 @@ function mapArgTypeToTS(type) {
899
951
  function capitalize(str) {
900
952
  return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
901
953
  }
902
- function deduplicateImports(importBlocks) {
954
+ function deduplicateImports(importBlocks, usedIn) {
903
955
  const seen = /* @__PURE__ */ new Map();
904
956
  const typeImports = /* @__PURE__ */ new Map();
905
957
  const rawLines = /* @__PURE__ */ new Set();
958
+ const isUsed = (name) => {
959
+ if (!usedIn) return true;
960
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(usedIn);
961
+ };
906
962
  for (const block of importBlocks) {
907
963
  for (const line of block.split("\n").map((l) => l.trim()).filter((l) => l)) {
908
964
  const namedMatch = line.match(/^import\s+\{\s*(.+?)\s*\}\s+from\s+'(.+?)';?$/);
@@ -926,11 +982,14 @@ function deduplicateImports(importBlocks) {
926
982
  }
927
983
  const result = [];
928
984
  for (const [mod, names] of seen.entries()) {
929
- result.push(`import { ${[...names].join(", ")} } from '${mod}';`);
985
+ const used = [...names].filter(isUsed);
986
+ if (used.length > 0) {
987
+ result.push(`import { ${used.join(", ")} } from '${mod}';`);
988
+ }
930
989
  }
931
990
  for (const [mod, names] of typeImports.entries()) {
932
991
  const regularNames = seen.get(mod) || /* @__PURE__ */ new Set();
933
- const typeOnly = [...names].filter((n) => !regularNames.has(n));
992
+ const typeOnly = [...names].filter((n) => !regularNames.has(n)).filter(isUsed);
934
993
  if (typeOnly.length > 0) {
935
994
  result.push(`import type { ${typeOnly.join(", ")} } from '${mod}';`);
936
995
  }
@@ -8,8 +8,8 @@ function generateEventBus(context) {
8
8
  * Generated from SpecVerse specification
9
9
  */
10
10
 
11
- import EventEmitter from 'eventemitter3';
12
- ${hasEvents || models.length > 0 ? `import type { EventPayloads, EventName } from './event-types.js';` : ""}
11
+ import { EventEmitter } from 'eventemitter3';
12
+ ${hasEvents || models.length > 0 ? `import type { EventPayloads } from './event-types.js';` : ""}
13
13
 
14
14
  // Re-export types for consumers
15
15
  ${hasEvents || models.length > 0 ? `export type { EventPayloads, EventName } from './event-types.js';` : ""}
@@ -33,7 +33,6 @@ class EventBus extends EventEmitter {
33
33
 
34
34
  private constructor() {
35
35
  super();
36
- this.setMaxListeners(100);
37
36
  }
38
37
 
39
38
  public static getInstance(): EventBus {
@@ -214,10 +214,30 @@ function generateHandlerBody(operation, modelName, handlerName, isModelControlle
214
214
  });
215
215
  }`;
216
216
  default: {
217
- const paramNames = endpoint?.parameters ? Object.keys(endpoint.parameters) : [];
217
+ const params = endpoint?.parameters || {};
218
+ const paramNames = Object.keys(params);
218
219
  const callArgs = paramNames.length > 0 ? paramNames.map((p) => `body.${p}`).join(", ") : "body";
220
+ const validations = [];
221
+ for (const [pName, pDef] of Object.entries(params)) {
222
+ const required = typeof pDef === "string" ? pDef.includes("required") : pDef?.required;
223
+ const typeStr = typeof pDef === "string" ? pDef.split(" ")[0] : pDef?.type || "String";
224
+ if (required) {
225
+ validations.push(` if (body.${pName} === undefined || body.${pName} === null) errors.push({ field: '${pName}', message: '${pName} is required' });`);
226
+ }
227
+ if (typeStr === "UUID" || typeStr === "String" || typeStr === "Email") {
228
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'string') errors.push({ field: '${pName}', message: '${pName} must be a string' });`);
229
+ } else if (typeStr === "Integer" || typeStr === "Number" || typeStr === "Float") {
230
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'number') errors.push({ field: '${pName}', message: '${pName} must be a number' });`);
231
+ } else if (typeStr === "Boolean") {
232
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'boolean') errors.push({ field: '${pName}', message: '${pName} must be a boolean' });`);
233
+ }
234
+ }
235
+ const validationBlock = validations.length > 0 ? ` const errors: Array<{ field: string; message: string }> = [];
236
+ ${validations.join("\n")}
237
+ if (errors.length > 0) return reply.status(400).send({ error: 'Validation failed', details: errors });` : "";
219
238
  return `try {
220
239
  const body = (request.body || {}) as Record<string, any>;
240
+ ${validationBlock}
221
241
  const result = await handler.${operation}(${callArgs});
222
242
  return reply.send(result || { success: true });
223
243
  } catch (error) {
@@ -1,8 +1,16 @@
1
1
  function generateTsConfig(context) {
2
2
  const { manifest, spec } = context;
3
- const mergedOptions = extractTsConfigOptions(context.implementationTypes || []);
3
+ const implementationTypes = context.implementationTypes || [];
4
+ const isMonorepo = !implementationTypes.some((it) => {
5
+ const cfg = it?.configuration || it?.instanceFactory?.configuration || {};
6
+ return cfg.outputStructure === "standalone";
7
+ });
8
+ if (isMonorepo) {
9
+ return "";
10
+ }
11
+ const mergedOptions = extractTsConfigOptions(implementationTypes);
4
12
  const hasViews = spec && (spec.views || Array.isArray(spec.views) && spec.views.length > 0);
5
- const usesReact = hasViews && (context.implementationTypes || []).some(
13
+ const usesReact = hasViews && implementationTypes.some(
6
14
  (implType) => implType.capabilities?.provides?.includes("ui.components") && implType.technology?.framework === "react"
7
15
  );
8
16
  const tsconfig = {
@@ -1,4 +1,48 @@
1
1
  import { matchStep } from "./step-conventions.js";
2
+ import { createHash } from "crypto";
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ async function validateTypeScript(code) {
6
+ try {
7
+ const esbuild = await import("esbuild");
8
+ await esbuild.transform(code, {
9
+ loader: "ts",
10
+ format: "esm",
11
+ target: "es2022"
12
+ });
13
+ return null;
14
+ } catch (err) {
15
+ const msg = err?.errors?.[0]?.text || err?.message || "unknown syntax error";
16
+ return msg;
17
+ }
18
+ }
19
+ const PROMPT_VERSION = "9.1.0";
20
+ function cacheKey(step, modelName, operationName, functionName, inputs) {
21
+ const payload = JSON.stringify({ step, modelName, operationName, functionName, inputs: [...inputs].sort(), v: PROMPT_VERSION });
22
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
23
+ }
24
+ function cacheDir() {
25
+ const cwd = process.env.SPECVERSE_USER_CWD || process.cwd();
26
+ return join(cwd, ".specverse", "ai-cache");
27
+ }
28
+ function cacheRead(key) {
29
+ const path = join(cacheDir(), `${key}.ts`);
30
+ if (!existsSync(path)) return null;
31
+ try {
32
+ return readFileSync(path, "utf8");
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function cacheWrite(key, body) {
38
+ const dir = cacheDir();
39
+ try {
40
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
41
+ writeFileSync(join(dir, `${key}.ts`), body, "utf8");
42
+ } catch (err) {
43
+ console.warn(` [ai-cache] write failed: ${err?.message || err}`);
44
+ }
45
+ }
2
46
  async function generateAiBehaviors(context) {
3
47
  const { controller, model } = context;
4
48
  if (!controller?.actions) return "";
@@ -18,8 +62,11 @@ async function generateAiBehaviors(context) {
18
62
  }
19
63
  }
20
64
  for (let i = 0; i < steps.length; i++) {
21
- const step = steps[i];
22
- if (typeof step !== "string") continue;
65
+ const stepInput = steps[i];
66
+ const stepText = typeof stepInput === "string" ? stepInput : stepInput?.step;
67
+ const stepAs = typeof stepInput === "object" ? stepInput?.as : void 0;
68
+ const stepReturns = typeof stepInput === "object" ? stepInput?.returns : void 0;
69
+ if (typeof stepText !== "string") continue;
23
70
  const ctx = {
24
71
  modelName,
25
72
  prismaModel: modelVar,
@@ -27,24 +74,39 @@ async function generateAiBehaviors(context) {
27
74
  operationName: actionName,
28
75
  stepNum: i + 1,
29
76
  parameterNames,
30
- declaredVars
77
+ declaredVars,
78
+ resultName: stepAs
31
79
  };
32
- const result = matchStep(step, ctx);
80
+ const result = matchStep(stepText, ctx);
33
81
  if (!result.matched && result.functionName) {
34
82
  const existing = unmatchedFunctions.find((f) => f.functionName === result.functionName);
35
83
  if (!existing) {
36
84
  unmatchedFunctions.push({
37
85
  functionName: result.functionName,
38
- step,
86
+ step: stepText,
39
87
  operationName: actionName,
40
88
  parameterNames,
41
- inputs: result.inputs || []
89
+ inputs: result.inputs || [],
90
+ returns: stepReturns
42
91
  });
43
92
  }
44
93
  }
45
94
  }
46
95
  }
47
96
  if (unmatchedFunctions.length === 0) return "";
97
+ return generateAiBehaviorsFile({
98
+ ownerName: `${modelName}Controller`,
99
+ unmatchedFunctions: unmatchedFunctions.map((f) => ({
100
+ ...f,
101
+ modelName
102
+ })),
103
+ availableModels,
104
+ spec: context.spec
105
+ });
106
+ }
107
+ async function generateAiBehaviorsFile(opts) {
108
+ const { ownerName, unmatchedFunctions, availableModels: availableModels2, spec } = opts;
109
+ if (unmatchedFunctions.length === 0) return "";
48
110
  let aiService = null;
49
111
  try {
50
112
  const { BehaviorAIService } = await import("@specverse/engines/ai");
@@ -52,19 +114,42 @@ async function generateAiBehaviors(context) {
52
114
  if (!aiService.isAvailable) {
53
115
  aiService = null;
54
116
  } else {
55
- aiService.startSession(`${modelName}Controller`);
117
+ aiService.startSession(ownerName);
56
118
  }
57
119
  } catch {
58
120
  aiService = null;
59
121
  }
60
- const availableModels = context.spec?.models ? Object.keys(context.spec.models) : [];
61
122
  const functions = [];
62
- for (const { functionName, step, operationName, parameterNames, inputs } of unmatchedFunctions) {
123
+ let cacheHits = 0;
124
+ let cacheMisses = 0;
125
+ for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
63
126
  const signature = inputs.length > 0 ? `input: { ${inputs.map((n) => `${n}: any`).join("; ")} }` : "input: Record<string, never>";
64
127
  const destructure = inputs.length > 0 ? ` const { ${inputs.join(", ")} } = input;` : "";
65
- let body = null;
66
- let source = "STUB";
67
- if (aiService) {
128
+ let returnType = "any";
129
+ if (typeof returns === "string") {
130
+ returnType = returns;
131
+ } else if (returns && typeof returns === "object") {
132
+ const fields = Object.entries(returns).map(([k, v]) => `${k}: ${v}`).join("; ");
133
+ returnType = `{ ${fields} }`;
134
+ }
135
+ const key = cacheKey(step, modelName, operationName, functionName, inputs);
136
+ let body = cacheRead(key);
137
+ let source = body ? "AI-CACHED" : "STUB";
138
+ if (body) {
139
+ const testCode = `export async function ${functionName}(input: any): Promise<any> {
140
+ ${body}
141
+ }`;
142
+ const validationError = await validateTypeScript(testCode);
143
+ if (validationError) {
144
+ console.warn(` [ai-validate] cached ${functionName} failed validation: ${validationError}`);
145
+ body = null;
146
+ source = "STUB";
147
+ } else {
148
+ cacheHits++;
149
+ }
150
+ }
151
+ if (!body && aiService) {
152
+ cacheMisses++;
68
153
  try {
69
154
  body = await aiService.generateBehavior({
70
155
  step,
@@ -73,10 +158,27 @@ async function generateAiBehaviors(context) {
73
158
  functionName,
74
159
  parameterNames: inputs,
75
160
  // the actual inputs to the pure function
76
- availableModels,
77
- spec: context.spec
161
+ availableModels: availableModels2,
162
+ spec,
163
+ returnType
164
+ // Pass declared return type to Claude
78
165
  });
79
- if (body) source = "AI-GENERATED";
166
+ if (body) {
167
+ const testCode = `export async function ${functionName}(input: any): Promise<any> {
168
+ ${body}
169
+ }`;
170
+ const validationError = await validateTypeScript(testCode);
171
+ if (validationError) {
172
+ console.warn(` [ai-validate] ${functionName} has syntax error: ${validationError}`);
173
+ body = `// AI-generated code failed validation: ${validationError}
174
+ // Step: ${step}
175
+ throw new Error('AI behavior has invalid syntax \u2014 see comment above');`;
176
+ source = "AI-INVALID";
177
+ } else {
178
+ source = "AI-GENERATED";
179
+ cacheWrite(key, body);
180
+ }
181
+ }
80
182
  } catch {
81
183
  }
82
184
  }
@@ -86,31 +188,36 @@ async function generateAiBehaviors(context) {
86
188
  body = body.split("\n").map((line) => line ? " " + line : line).join("\n");
87
189
  }
88
190
  const inputsDoc = inputs.length > 0 ? ` * Inputs: ${inputs.join(", ")}
191
+ ` : "";
192
+ const returnsDoc = returnType !== "any" ? ` * Returns: ${returnType}
89
193
  ` : "";
90
194
  functions.push(`/**
91
195
  * ${functionName}
92
196
  *
93
197
  * Spec step: "${step}"
94
- * Called by: ${modelName}Controller.${operationName}()
95
- ${inputsDoc} * Source: ${source}
198
+ * Called by: ${ownerName}.${operationName}()
199
+ ${inputsDoc}${returnsDoc} * Source: ${source}
96
200
  * Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
97
201
  *
98
202
  * PURE FUNCTION \u2014 no database access, no event publishing, no external services.
99
203
  * 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."}
204
+ * ${source === "AI-GENERATED" ? "AI-generated implementation. Review and test before deploying." : source === "AI-CACHED" ? "Restored from AI cache (.specverse/ai-cache/). Delete cache entry to regenerate." : source === "AI-INVALID" ? "AI returned code with syntax errors \u2014 function throws at runtime. Fix or regenerate." : "STUB \u2014 Claude CLI unavailable. Install Claude Code or implement manually."}
101
205
  */
102
- export async function ${functionName}(${signature}): Promise<any> {
206
+ export async function ${functionName}(${signature}): Promise<${returnType}> {
103
207
  ${destructure ? destructure + "\n" : ""}${body}
104
208
  }`);
105
209
  }
106
210
  if (aiService?.endSession) aiService.endSession();
211
+ if (cacheHits > 0 || cacheMisses > 0) {
212
+ console.log(` AI cache: ${cacheHits} hit(s), ${cacheMisses} miss(es) for ${ownerName}`);
213
+ }
107
214
  return `/**
108
- * ${modelName}Controller \u2014 AI-Generated Behaviors
215
+ * ${ownerName} \u2014 AI-Generated Behaviors
109
216
  *
110
217
  * \u26A0\uFE0F THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
111
218
  *
112
219
  * These functions could not be generated from convention patterns.
113
- * They are called by ${modelName}Controller when executing custom actions.
220
+ * They are called by ${ownerName} when executing operations.
114
221
  *
115
222
  * Options for each function:
116
223
  * - Implement manually (recommended for business-critical logic)
@@ -137,5 +244,6 @@ ${functions.join("\n\n")}
137
244
  `;
138
245
  }
139
246
  export {
140
- generateAiBehaviors as default
247
+ generateAiBehaviors as default,
248
+ generateAiBehaviorsFile
141
249
  };
@@ -101,8 +101,11 @@ function generateStepLogic(steps, context, preconditionDeclared) {
101
101
  const unmatchedSteps = [];
102
102
  const declaredVars = preconditionDeclared || /* @__PURE__ */ new Set();
103
103
  if (steps && steps.length > 0) {
104
- const stepCode = steps.map((step, i) => {
105
- if (typeof step !== "string") {
104
+ const stepCode = steps.map((stepInput, i) => {
105
+ const stepText = typeof stepInput === "string" ? stepInput : stepInput.step;
106
+ const resultName = typeof stepInput === "object" ? stepInput.as : void 0;
107
+ const returns = typeof stepInput === "object" ? stepInput.returns : void 0;
108
+ if (typeof stepText !== "string") {
106
109
  return ` // Step ${i + 1}: Complex operation \u2014 see expanded definition`;
107
110
  }
108
111
  const ctx = {
@@ -112,18 +115,22 @@ function generateStepLogic(steps, context, preconditionDeclared) {
112
115
  operationName: context.operationName,
113
116
  stepNum: i + 1,
114
117
  parameterNames: context.parameterNames,
115
- declaredVars
118
+ declaredVars,
119
+ resultName
120
+ // Pass through the named result
116
121
  };
117
- const result = matchStep(step, ctx);
122
+ const result = matchStep(stepText, ctx);
118
123
  if (result.helperMethod) {
119
124
  helpers.push(result.helperMethod);
120
125
  }
121
126
  if (!result.matched && result.functionName) {
122
127
  unmatchedSteps.push({
123
- step,
128
+ step: stepText,
124
129
  functionName: result.functionName,
125
130
  operationName: context.operationName,
126
- inputs: result.inputs || []
131
+ inputs: result.inputs || [],
132
+ returns,
133
+ resultName: resultName || `step${i + 1}Result`
127
134
  });
128
135
  }
129
136
  return result.call;
@@ -177,7 +184,7 @@ function generateEventPublishing(sideEffects, operationName, parameterNames) {
177
184
  if (!sideEffects || sideEffects.length === 0) return "";
178
185
  const paramFields = parameterNames?.length ? parameterNames.join(", ") + ", " : "";
179
186
  const publishes = sideEffects.map(
180
- (event) => ` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() });`
187
+ (event) => ` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() } as any);`
181
188
  );
182
189
  return ` // === EVENTS ===
183
190
  ${publishes.join("\n")}`;