@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.
- package/assets/prompts/core/standard/v9/behavior.prompt.yaml +7 -1
- package/dist/ai/behavior-ai-service.d.ts +2 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -1
- package/dist/ai/behavior-ai-service.js +2 -0
- package/dist/ai/behavior-ai-service.js.map +1 -1
- package/dist/ai/prompt-loader.js +2 -2
- package/dist/inference/index.d.ts +2 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +18 -1
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +501 -21
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/inference/verification.d.ts +78 -0
- package/dist/inference/verification.d.ts.map +1 -0
- package/dist/inference/verification.js +263 -0
- package/dist/inference/verification.js.map +1 -0
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +111 -27
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +123 -25
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +134 -27
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
- package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
- 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 ? `, ${
|
|
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
|
|
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
|
-
|
|
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
|
-
${
|
|
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 =
|
|
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(
|
|
793
|
-
jobId: options.jobId,
|
|
794
|
-
|
|
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.
|
|
812
|
-
if (status.
|
|
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.
|
|
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
|
-
|
|
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 (${
|
|
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 ? `, ${
|
|
855
|
-
|
|
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 (${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
22
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
161
|
+
availableModels: availableModels2,
|
|
162
|
+
spec,
|
|
163
|
+
returnType
|
|
164
|
+
// Pass declared return type to Claude
|
|
78
165
|
});
|
|
79
|
-
if (body)
|
|
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: ${
|
|
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
|
|
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
|
-
* ${
|
|
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 ${
|
|
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((
|
|
105
|
-
|
|
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(
|
|
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")}`;
|