@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.
- 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/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +204 -4
- package/dist/inference/quint-transpiler.js.map +1 -1
- 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 +81 -22
- 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 +138 -23
- 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 +99 -22
- 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,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 ? `, ${
|
|
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
|
|
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
|
`;
|
|
@@ -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 =
|
|
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(
|
|
793
|
-
jobId: options.jobId,
|
|
794
|
-
|
|
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.
|
|
812
|
-
if (status.
|
|
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.
|
|
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
|
-
|
|
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 (${
|
|
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 ? `, ${
|
|
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 (${
|
|
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
|
-
|
|
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
|
|
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")}`;
|