@specverse/engines 4.1.14 → 4.1.16

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 (66) 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/index.d.ts +2 -1
  8. package/dist/inference/index.d.ts.map +1 -1
  9. package/dist/inference/index.js +2 -1
  10. package/dist/inference/index.js.map +1 -1
  11. package/dist/inference/quint-transpiler.d.ts +18 -1
  12. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  13. package/dist/inference/quint-transpiler.js +501 -21
  14. package/dist/inference/quint-transpiler.js.map +1 -1
  15. package/dist/inference/verification.d.ts +78 -0
  16. package/dist/inference/verification.d.ts.map +1 -0
  17. package/dist/inference/verification.js +263 -0
  18. package/dist/inference/verification.js.map +1 -0
  19. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  20. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  21. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  22. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +111 -27
  23. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
  24. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
  25. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  26. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
  27. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
  28. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
  29. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  30. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
  31. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  32. package/dist/realize/index.d.ts.map +1 -1
  33. package/dist/realize/index.js +123 -25
  34. package/dist/realize/index.js.map +1 -1
  35. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  36. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  37. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  38. package/libs/instance-factories/cli/templates/commander/command-generator.ts +134 -27
  39. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
  40. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
  41. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  42. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
  43. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
  44. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
  45. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  46. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
  47. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  48. package/package.json +1 -1
  49. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  50. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  57. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  58. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  59. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  60. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  61. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  62. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  63. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  64. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  65. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  66. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -19,8 +19,8 @@ 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)}` : "";
23
- const desc = flag.description || `${flagName} option`;
22
+ const defaultVal = flag.default !== void 0 ? `, ${commanderDefault(flag.default, flagType)}` : "";
23
+ const desc = escapeSingleQuotes(flag.description || `${flagName} option`);
24
24
  return ` .option('${alias}${flagName}${valuePart}', '${desc}'${defaultVal})`;
25
25
  });
26
26
  const optionTypes = Object.entries(flags).map(([flagName, flag]) => {
@@ -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
  `;
@@ -117,6 +123,27 @@ import type { ParserEngine } from '@specverse/types';`,
117
123
  console.warn('Warnings:');
118
124
  result.warnings.forEach((w: string) => console.warn(' ', w));
119
125
  }
126
+ }
127
+
128
+ // L3 verification \u2014 opt-in via --verify flag. Runs the
129
+ // transpiled Quint guards against the parsed spec. Guards
130
+ // whose state-var dependencies aren't present in this spec
131
+ // are reported as skipped (not failed).
132
+ if (options.verify) {
133
+ try {
134
+ const { verifySpec, formatVerificationResult } = await import('@specverse/engines/inference');
135
+ const verification = await verifySpec(result.ast);
136
+ if (options.json) {
137
+ console.log(JSON.stringify({ valid: true, verification }, null, 2));
138
+ } else {
139
+ console.log('');
140
+ console.log(formatVerificationResult(verification, options.verbose));
141
+ }
142
+ if (verification.failed.length > 0) process.exit(1);
143
+ } catch (ve: any) {
144
+ console.error('Verification error:', ve.message);
145
+ process.exit(1);
146
+ }
120
147
  }`
121
148
  },
122
149
  infer: {
@@ -243,6 +270,28 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
243
270
  }
244
271
  }
245
272
 
273
+ // Merge service operation steps from original spec into inferred services
274
+ // Inference generates service shells with preconditions/returns but drops
275
+ // the declarative steps arrays \u2014 we restore them from the parsed AST.
276
+ const origServices = Array.isArray((origComponent as any).services)
277
+ ? (origComponent as any).services
278
+ : Object.values((origComponent as any).services || {});
279
+ const inferredServices = componentData.services || {};
280
+ for (const origSvc of origServices) {
281
+ const svcName = (origSvc as any).name;
282
+ if (!svcName) continue;
283
+ const inferredSvc = inferredServices[svcName];
284
+ if (!inferredSvc) continue;
285
+
286
+ const origOps = (origSvc as any).operations || {};
287
+ const inferredOps = inferredSvc.operations || {};
288
+ for (const [opName, origOp] of Object.entries(origOps) as [string, any][]) {
289
+ if (inferredOps[opName] && origOp.steps) {
290
+ inferredOps[opName].steps = origOp.steps;
291
+ }
292
+ }
293
+ }
294
+
246
295
  // Inject key as name into entity maps, and expand convention-format attributes
247
296
  // Iterate all object-valued sections (not hardcoded \u2014 covers any entity type)
248
297
  const entitySections = Object.keys(componentData).filter(k =>
@@ -311,7 +360,7 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
311
360
  if (!realizeEngine) { console.error('No realize engine found.'); process.exit(1); }
312
361
  await realizeEngine.initialize({ manifestPath: effectiveManifestPath, workingDir: (process.env.SPECVERSE_USER_CWD || process.cwd()) });
313
362
 
314
- const outputDir = options.output || resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), 'generated/code');
363
+ const outputDir = resolve((process.env.SPECVERSE_USER_CWD || process.cwd()), options.output || 'generated/code');
315
364
  console.log('Realizing ' + type + ' from ' + file + '...');
316
365
  await (realizeEngine as any).realizeAll(inferredSpec, outputDir);
317
366
 
@@ -789,10 +838,12 @@ import { EngineRegistry } from '@specverse/entities';`,
789
838
  imports: ``,
790
839
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
791
840
  const manager = new SessionManager();
792
- const job = await manager.submit(sessionId, requirements, {
793
- jobId: options.jobId,
794
- outputPath: options.output,
795
- operation: options.operation,
841
+ const job = await manager.submit({
842
+ jobId: options.jobId || ('job-' + Date.now()),
843
+ sessionId,
844
+ operation: (options.operation || 'create') as 'create' | 'analyse' | 'materialise' | 'realize',
845
+ requirements,
846
+ output: options.output,
796
847
  });
797
848
  console.log('Job submitted: ' + job.jobId);
798
849
  console.log('Status: ' + job.status);
@@ -808,8 +859,10 @@ import { EngineRegistry } from '@specverse/entities';`,
808
859
  } else {
809
860
  console.log('ID: ' + (status.sessionId || status.jobId));
810
861
  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);
862
+ if (status.submitted) console.log('Submitted: ' + new Date(status.submitted).toLocaleString());
863
+ if (status.started) console.log('Started: ' + new Date(status.started).toLocaleString());
864
+ if (status.completed) console.log('Completed: ' + new Date(status.completed).toLocaleString());
865
+ if (status.duration) console.log('Duration: ' + status.duration);
813
866
  }`
814
867
  },
815
868
  "session.process": {
@@ -817,7 +870,7 @@ import { EngineRegistry } from '@specverse/entities';`,
817
870
  handler: `const { SessionManager } = await import('@specverse/engines/ai');
818
871
  const manager = new SessionManager();
819
872
  console.log('Processing job: ' + jobId);
820
- await manager.process(jobId);
873
+ await manager.processJob(jobId);
821
874
  console.log('Job processed successfully');`
822
875
  }
823
876
  };
@@ -827,11 +880,12 @@ function generateLeafCommand(name, description, commandStr, optionDefs, actionPa
827
880
  const result = await service.execute(${actionParams.includes(":") ? "{ " + actionParams.split(",").map((p) => p.trim().split(":")[0].trim()).join(", ") + ", ...options }" : "options"});
828
881
  console.log(result);` : `console.log('Executing ${name}...');
829
882
  // TODO: Wire to service`;
830
- return `const cmd = program
883
+ const finalActionParams = renameUnusedActionParams(actionParams, handler);
884
+ return `program
831
885
  .command('${commandStr}')
832
- .description('${description}')
886
+ .description('${escapeSingleQuotes(description)}')
833
887
  ${optionDefs.join("\n")}
834
- .action(async (${actionParams}) => {
888
+ .action(async (${finalActionParams}) => {
835
889
  try {
836
890
  ${handler}
837
891
  } catch (error: any) {
@@ -840,6 +894,27 @@ ${optionDefs.join("\n")}
840
894
  }
841
895
  });`;
842
896
  }
897
+ function escapeSingleQuotes(s) {
898
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
899
+ }
900
+ function commanderDefault(value, flagType) {
901
+ if (flagType === "boolean") return JSON.stringify(Boolean(value));
902
+ if (typeof value === "number") return JSON.stringify(String(value));
903
+ return JSON.stringify(value);
904
+ }
905
+ function renameUnusedActionParams(actionParams, handler) {
906
+ if (!actionParams.trim()) return actionParams;
907
+ return actionParams.split(",").map((param) => {
908
+ const trimmed = param.trim();
909
+ const nameMatch = trimmed.match(/^(\w+)/);
910
+ if (!nameMatch) return param;
911
+ const name = nameMatch[1];
912
+ if (name.startsWith("_")) return param;
913
+ const re = new RegExp(`\\b${name}\\b`);
914
+ if (re.test(handler)) return param;
915
+ return param.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
916
+ }).join(", ");
917
+ }
843
918
  function generateCommandWithSubcommands(name, description, subcommands, optionDefs) {
844
919
  const subcmdRegistrations = Object.entries(subcommands).map(([subName, subDef]) => {
845
920
  const subDesc = subDef.description || "";
@@ -851,20 +926,22 @@ function generateCommandWithSubcommands(name, description, subcommands, optionDe
851
926
  const alias = flag.alias ? `${flag.alias}, ` : "";
852
927
  const flagType = flag.type?.toLowerCase();
853
928
  const valuePart = flagType === "boolean" ? "" : ` <${flagName.replace(/^--/, "")}>`;
854
- const defaultVal = flag.default !== void 0 ? `, ${JSON.stringify(flag.default)}` : "";
855
- return ` .option('${alias}${flagName}${valuePart}', '${flag.description || flagName}'${defaultVal})`;
929
+ const defaultVal = flag.default !== void 0 ? `, ${commanderDefault(flag.default, flagType)}` : "";
930
+ const desc = escapeSingleQuotes(flag.description || flagName);
931
+ return ` .option('${alias}${flagName}${valuePart}', '${desc}'${defaultVal})`;
856
932
  });
857
933
  const handlerKey = `${name}.${subName}`;
858
934
  const engineHandler = ENGINE_HANDLERS[handlerKey];
859
935
  const subArgTypes = Object.entries(subArgs).filter(([_, a]) => a.positional).map(([n, a]) => `${n}: ${mapArgTypeToTS(a.type)}`);
860
936
  const subActionParams = subArgTypes.length > 0 ? subArgTypes.join(", ") + ", options: any" : "options: any";
861
937
  const handler = engineHandler ? engineHandler.handler : `console.log('${name} ${subName}: not yet implemented via engine');`;
938
+ const finalSubActionParams = renameUnusedActionParams(subActionParams, handler);
862
939
  return `
863
940
  cmd
864
941
  .command('${subCmdStr}')
865
- .description('${subDesc}')
942
+ .description('${escapeSingleQuotes(subDesc)}')
866
943
  ${subOptionDefs.join("\n")}
867
- .action(async (${subActionParams}) => {
944
+ .action(async (${finalSubActionParams}) => {
868
945
  try {
869
946
  ${handler}
870
947
  } catch (error: any) {
@@ -875,7 +952,7 @@ ${subOptionDefs.join("\n")}
875
952
  });
876
953
  return `const cmd = program
877
954
  .command('${name}')
878
- .description('${description}');
955
+ .description('${escapeSingleQuotes(description)}');
879
956
  ${subcmdRegistrations.join("\n")}`;
880
957
  }
881
958
  function generateSubcommandRegistrations(_parentName, _subcommands) {
@@ -899,10 +976,14 @@ function mapArgTypeToTS(type) {
899
976
  function capitalize(str) {
900
977
  return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
901
978
  }
902
- function deduplicateImports(importBlocks) {
979
+ function deduplicateImports(importBlocks, usedIn) {
903
980
  const seen = /* @__PURE__ */ new Map();
904
981
  const typeImports = /* @__PURE__ */ new Map();
905
982
  const rawLines = /* @__PURE__ */ new Set();
983
+ const isUsed = (name) => {
984
+ if (!usedIn) return true;
985
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(usedIn);
986
+ };
906
987
  for (const block of importBlocks) {
907
988
  for (const line of block.split("\n").map((l) => l.trim()).filter((l) => l)) {
908
989
  const namedMatch = line.match(/^import\s+\{\s*(.+?)\s*\}\s+from\s+'(.+?)';?$/);
@@ -926,11 +1007,14 @@ function deduplicateImports(importBlocks) {
926
1007
  }
927
1008
  const result = [];
928
1009
  for (const [mod, names] of seen.entries()) {
929
- result.push(`import { ${[...names].join(", ")} } from '${mod}';`);
1010
+ const used = [...names].filter(isUsed);
1011
+ if (used.length > 0) {
1012
+ result.push(`import { ${used.join(", ")} } from '${mod}';`);
1013
+ }
930
1014
  }
931
1015
  for (const [mod, names] of typeImports.entries()) {
932
1016
  const regularNames = seen.get(mod) || /* @__PURE__ */ new Set();
933
- const typeOnly = [...names].filter((n) => !regularNames.has(n));
1017
+ const typeOnly = [...names].filter((n) => !regularNames.has(n)).filter(isUsed);
934
1018
  if (typeOnly.length > 0) {
935
1019
  result.push(`import type { ${typeOnly.join(", ")} } from '${mod}';`);
936
1020
  }
@@ -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")}`;