@specverse/engines 4.1.13 → 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 +126 -0
- package/dist/ai/behavior-ai-service.d.ts +65 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -0
- package/dist/ai/behavior-ai-service.js +205 -0
- package/dist/ai/behavior-ai-service.js.map +1 -0
- package/dist/ai/index.d.ts +27 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +30 -0
- package/dist/ai/index.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 +97 -22
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
- package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
- 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 +249 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -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 +101 -84
- 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 +192 -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 +115 -22
- package/libs/instance-factories/communication/event-emitter.yaml +16 -12
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
- package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
- package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
- 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
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
}
|
|
46
|
+
async function generateAiBehaviors(context) {
|
|
47
|
+
const { controller, model } = context;
|
|
48
|
+
if (!controller?.actions) return "";
|
|
49
|
+
const modelName = model?.name || controller.model || "Model";
|
|
50
|
+
const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
51
|
+
const unmatchedFunctions = [];
|
|
52
|
+
for (const [actionName, action] of Object.entries(controller.actions)) {
|
|
53
|
+
const steps = action.steps || [];
|
|
54
|
+
const parameterNames = Object.keys(action.parameters || {});
|
|
55
|
+
const preconditions = action.requires || action.preconditions || [];
|
|
56
|
+
const declaredVars = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const pc of preconditions) {
|
|
58
|
+
const match = pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i);
|
|
59
|
+
if (match) {
|
|
60
|
+
const entity = match[1];
|
|
61
|
+
declaredVars.add(entity.charAt(0).toLowerCase() + entity.slice(1));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < steps.length; i++) {
|
|
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;
|
|
70
|
+
const ctx = {
|
|
71
|
+
modelName,
|
|
72
|
+
prismaModel: modelVar,
|
|
73
|
+
serviceName: `${modelName}Controller`,
|
|
74
|
+
operationName: actionName,
|
|
75
|
+
stepNum: i + 1,
|
|
76
|
+
parameterNames,
|
|
77
|
+
declaredVars,
|
|
78
|
+
resultName: stepAs
|
|
79
|
+
};
|
|
80
|
+
const result = matchStep(stepText, ctx);
|
|
81
|
+
if (!result.matched && result.functionName) {
|
|
82
|
+
const existing = unmatchedFunctions.find((f) => f.functionName === result.functionName);
|
|
83
|
+
if (!existing) {
|
|
84
|
+
unmatchedFunctions.push({
|
|
85
|
+
functionName: result.functionName,
|
|
86
|
+
step: stepText,
|
|
87
|
+
operationName: actionName,
|
|
88
|
+
parameterNames,
|
|
89
|
+
inputs: result.inputs || [],
|
|
90
|
+
returns: stepReturns
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
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 "";
|
|
110
|
+
let aiService = null;
|
|
111
|
+
try {
|
|
112
|
+
const { BehaviorAIService } = await import("@specverse/engines/ai");
|
|
113
|
+
aiService = new BehaviorAIService();
|
|
114
|
+
if (!aiService.isAvailable) {
|
|
115
|
+
aiService = null;
|
|
116
|
+
} else {
|
|
117
|
+
aiService.startSession(ownerName);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
aiService = null;
|
|
121
|
+
}
|
|
122
|
+
const functions = [];
|
|
123
|
+
let cacheHits = 0;
|
|
124
|
+
let cacheMisses = 0;
|
|
125
|
+
for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
|
|
126
|
+
const signature = inputs.length > 0 ? `input: { ${inputs.map((n) => `${n}: any`).join("; ")} }` : "input: Record<string, never>";
|
|
127
|
+
const destructure = inputs.length > 0 ? ` const { ${inputs.join(", ")} } = input;` : "";
|
|
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++;
|
|
153
|
+
try {
|
|
154
|
+
body = await aiService.generateBehavior({
|
|
155
|
+
step,
|
|
156
|
+
modelName,
|
|
157
|
+
operationName,
|
|
158
|
+
functionName,
|
|
159
|
+
parameterNames: inputs,
|
|
160
|
+
// the actual inputs to the pure function
|
|
161
|
+
availableModels: availableModels2,
|
|
162
|
+
spec,
|
|
163
|
+
returnType
|
|
164
|
+
// Pass declared return type to Claude
|
|
165
|
+
});
|
|
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
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!body) {
|
|
186
|
+
body = ` throw new Error('Not implemented: ${functionName} \u2014 see behaviors/${modelName}Controller.ai.ts');`;
|
|
187
|
+
} else {
|
|
188
|
+
body = body.split("\n").map((line) => line ? " " + line : line).join("\n");
|
|
189
|
+
}
|
|
190
|
+
const inputsDoc = inputs.length > 0 ? ` * Inputs: ${inputs.join(", ")}
|
|
191
|
+
` : "";
|
|
192
|
+
const returnsDoc = returnType !== "any" ? ` * Returns: ${returnType}
|
|
193
|
+
` : "";
|
|
194
|
+
functions.push(`/**
|
|
195
|
+
* ${functionName}
|
|
196
|
+
*
|
|
197
|
+
* Spec step: "${step}"
|
|
198
|
+
* Called by: ${ownerName}.${operationName}()
|
|
199
|
+
${inputsDoc}${returnsDoc} * Source: ${source}
|
|
200
|
+
* Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
201
|
+
*
|
|
202
|
+
* PURE FUNCTION \u2014 no database access, no event publishing, no external services.
|
|
203
|
+
* All data comes in via \`input\`; all effects happen in the calling controller.
|
|
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."}
|
|
205
|
+
*/
|
|
206
|
+
export async function ${functionName}(${signature}): Promise<${returnType}> {
|
|
207
|
+
${destructure ? destructure + "\n" : ""}${body}
|
|
208
|
+
}`);
|
|
209
|
+
}
|
|
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
|
+
}
|
|
214
|
+
return `/**
|
|
215
|
+
* ${ownerName} \u2014 AI-Generated Behaviors
|
|
216
|
+
*
|
|
217
|
+
* \u26A0\uFE0F THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
|
|
218
|
+
*
|
|
219
|
+
* These functions could not be generated from convention patterns.
|
|
220
|
+
* They are called by ${ownerName} when executing operations.
|
|
221
|
+
*
|
|
222
|
+
* Options for each function:
|
|
223
|
+
* - Implement manually (recommended for business-critical logic)
|
|
224
|
+
* - Use AI generation: specverse ai generate <function>
|
|
225
|
+
* - Refactor the spec step to use a convention pattern
|
|
226
|
+
*
|
|
227
|
+
* Convention patterns that ARE auto-generated (no AI needed):
|
|
228
|
+
* "Find {Model} by {field}" \u2192 prisma.model.findUniqueOrThrow(...)
|
|
229
|
+
* "Create {Model}" \u2192 prisma.model.create(...)
|
|
230
|
+
* "Update {Model} {field} to {value}" \u2192 prisma.model.update(...)
|
|
231
|
+
* "Delete {Model}" \u2192 prisma.model.delete(...)
|
|
232
|
+
* "Transition {Model} to {state}" \u2192 prisma.model.update({ status: ... })
|
|
233
|
+
* "Count {Model}s per {Group}" \u2192 prisma.model.groupBy(...)
|
|
234
|
+
* See step-conventions.ts for the full list.
|
|
235
|
+
*
|
|
236
|
+
* Generated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
import { PrismaClient } from '@prisma/client';
|
|
240
|
+
|
|
241
|
+
const prisma = new PrismaClient();
|
|
242
|
+
|
|
243
|
+
${functions.join("\n\n")}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
generateAiBehaviors as default,
|
|
248
|
+
generateAiBehaviorsFile
|
|
249
|
+
};
|
|
@@ -6,14 +6,17 @@ function generateBehaviorBody(behavior, opMeta, context) {
|
|
|
6
6
|
function generateBehaviorWithHelpers(behavior, opMeta, context) {
|
|
7
7
|
const parts = [];
|
|
8
8
|
const helperMethods = [];
|
|
9
|
+
const preconditionDeclared = /* @__PURE__ */ new Set();
|
|
9
10
|
const preconditions = generatePreconditionChecks(
|
|
10
11
|
behavior.preconditions || [],
|
|
11
|
-
context
|
|
12
|
+
context,
|
|
13
|
+
preconditionDeclared
|
|
12
14
|
);
|
|
13
15
|
if (preconditions) parts.push(preconditions);
|
|
14
|
-
const { code, helpers } = generateStepLogic(
|
|
16
|
+
const { code, helpers, unmatchedSteps } = generateStepLogic(
|
|
15
17
|
behavior.steps || [],
|
|
16
|
-
context
|
|
18
|
+
context,
|
|
19
|
+
preconditionDeclared
|
|
17
20
|
);
|
|
18
21
|
parts.push(code);
|
|
19
22
|
helperMethods.push(...helpers);
|
|
@@ -23,79 +26,86 @@ function generateBehaviorWithHelpers(behavior, opMeta, context) {
|
|
|
23
26
|
if (postconditions) parts.push(postconditions);
|
|
24
27
|
const events = generateEventPublishing(
|
|
25
28
|
behavior.sideEffects || [],
|
|
26
|
-
context.operationName
|
|
29
|
+
context.operationName,
|
|
30
|
+
context.parameterNames
|
|
27
31
|
);
|
|
28
32
|
if (events) parts.push(events);
|
|
29
33
|
let body = parts.join("\n\n");
|
|
30
34
|
if (behavior.transactional) {
|
|
31
35
|
body = generateTransactionWrapper(body, context);
|
|
32
36
|
}
|
|
33
|
-
return { body, helperMethods };
|
|
37
|
+
return { body, helperMethods, unmatchedSteps };
|
|
34
38
|
}
|
|
35
|
-
function generatePreconditionChecks(preconditions, context) {
|
|
39
|
+
function generatePreconditionChecks(preconditions, context, declared) {
|
|
36
40
|
if (preconditions.length === 0) return "";
|
|
37
|
-
|
|
41
|
+
if (!declared) declared = /* @__PURE__ */ new Set();
|
|
42
|
+
const checks = preconditions.map((pc) => matchPreconditionPattern(pc, context, declared));
|
|
38
43
|
return ` // === PRECONDITIONS ===
|
|
39
44
|
${checks.join("\n")}`;
|
|
40
45
|
}
|
|
41
|
-
function
|
|
46
|
+
function findIdParam(modelName, paramNames) {
|
|
47
|
+
const modelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
48
|
+
const idParam = paramNames.find((p) => p === `${modelVar}Id`);
|
|
49
|
+
if (idParam) return idParam;
|
|
50
|
+
if (paramNames.includes("id")) return "id";
|
|
51
|
+
return `${modelVar}Id`;
|
|
52
|
+
}
|
|
53
|
+
function matchPreconditionPattern(precondition, context, declared) {
|
|
42
54
|
const pc = precondition.toLowerCase();
|
|
43
|
-
const
|
|
55
|
+
const params = context.parameterNames || [];
|
|
44
56
|
const existsMatch = precondition.match(/^(\w+)\s+exists/i);
|
|
45
57
|
if (existsMatch) {
|
|
46
58
|
const entity = existsMatch[1];
|
|
47
59
|
const entityVar = entity.charAt(0).toLowerCase() + entity.slice(1);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
const idParam = findIdParam(entity, params);
|
|
61
|
+
if (declared.has(entityVar)) {
|
|
62
|
+
return ` // Guard: ${precondition} (already fetched)`;
|
|
63
|
+
}
|
|
64
|
+
declared.add(entityVar);
|
|
65
|
+
return ` const ${entityVar} = await prisma.${entityVar}.findUniqueOrThrow({ where: { id: ${idParam} } });`;
|
|
53
66
|
}
|
|
54
67
|
if (pc.includes("is not empty") || pc.includes("is required")) {
|
|
55
68
|
const fieldMatch = precondition.match(/^(\w+)\s+is/i);
|
|
56
69
|
if (fieldMatch) {
|
|
57
70
|
const field = fieldMatch[1];
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
throw new Error('Precondition failed: ${precondition}');
|
|
61
|
-
}`;
|
|
71
|
+
const paramRef = params.includes(field) ? field : `params.${field}`;
|
|
72
|
+
return ` if (!${paramRef}) throw new Error('${field} is required');`;
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
if (pc.includes("is valid")) {
|
|
65
|
-
return ` //
|
|
66
|
-
const validation = this.validate(params, { operation: '${context.operationName}' });
|
|
67
|
-
if (!validation.valid) {
|
|
68
|
-
throw new Error('Precondition failed: ${precondition} \u2014 ' + validation.errors.join(', '));
|
|
69
|
-
}`;
|
|
76
|
+
return ` // TODO: Implement validation: ${precondition}`;
|
|
70
77
|
}
|
|
71
78
|
const matchesMatch = precondition.match(/^(\w+)\s+(?:matches|equals)\s+(.+)/i);
|
|
72
79
|
if (matchesMatch) {
|
|
73
|
-
const left = matchesMatch[1];
|
|
74
|
-
const right = matchesMatch[2];
|
|
75
|
-
return `
|
|
76
|
-
if (params.${left.charAt(0).toLowerCase() + left.slice(1)} !== params.${right.charAt(0).toLowerCase() + right.slice(1)}) {
|
|
77
|
-
throw new Error('Precondition failed: ${precondition}');
|
|
78
|
-
}`;
|
|
80
|
+
const left = matchesMatch[1].charAt(0).toLowerCase() + matchesMatch[1].slice(1);
|
|
81
|
+
const right = matchesMatch[2].charAt(0).toLowerCase() + matchesMatch[2].slice(1);
|
|
82
|
+
return ` if (${left} !== ${right}) throw new Error('${matchesMatch[1]} must match ${matchesMatch[2]}');`;
|
|
79
83
|
}
|
|
80
84
|
const stateMatch = precondition.match(/^(\w+)\s+is\s+(\w+)$/i);
|
|
81
85
|
if (stateMatch) {
|
|
82
86
|
const model = stateMatch[1];
|
|
83
87
|
const state = stateMatch[2];
|
|
84
88
|
const modelVar = model.charAt(0).toLowerCase() + model.slice(1);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
89
|
+
const idParam = findIdParam(model, params);
|
|
90
|
+
if (!declared.has(modelVar)) {
|
|
91
|
+
declared.add(modelVar);
|
|
92
|
+
return ` const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { id: ${idParam} } });
|
|
93
|
+
if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
|
|
94
|
+
}
|
|
95
|
+
return ` if (${modelVar}.status !== '${state}') throw new Error('${model} must be ${state}, got ' + ${modelVar}.status);`;
|
|
90
96
|
}
|
|
91
|
-
return ` //
|
|
92
|
-
// TODO: Implement precondition check`;
|
|
97
|
+
return ` // TODO: Implement precondition: ${precondition}`;
|
|
93
98
|
}
|
|
94
|
-
function generateStepLogic(steps, context) {
|
|
99
|
+
function generateStepLogic(steps, context, preconditionDeclared) {
|
|
95
100
|
const helpers = [];
|
|
101
|
+
const unmatchedSteps = [];
|
|
102
|
+
const declaredVars = preconditionDeclared || /* @__PURE__ */ new Set();
|
|
96
103
|
if (steps && steps.length > 0) {
|
|
97
|
-
const stepCode = steps.map((
|
|
98
|
-
|
|
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") {
|
|
99
109
|
return ` // Step ${i + 1}: Complex operation \u2014 see expanded definition`;
|
|
100
110
|
}
|
|
101
111
|
const ctx = {
|
|
@@ -103,24 +113,40 @@ function generateStepLogic(steps, context) {
|
|
|
103
113
|
prismaModel: context.prismaModel || context.modelName,
|
|
104
114
|
serviceName: context.serviceName,
|
|
105
115
|
operationName: context.operationName,
|
|
106
|
-
stepNum: i + 1
|
|
116
|
+
stepNum: i + 1,
|
|
117
|
+
parameterNames: context.parameterNames,
|
|
118
|
+
declaredVars,
|
|
119
|
+
resultName
|
|
120
|
+
// Pass through the named result
|
|
107
121
|
};
|
|
108
|
-
const result = matchStep(
|
|
122
|
+
const result = matchStep(stepText, ctx);
|
|
109
123
|
if (result.helperMethod) {
|
|
110
124
|
helpers.push(result.helperMethod);
|
|
111
125
|
}
|
|
126
|
+
if (!result.matched && result.functionName) {
|
|
127
|
+
unmatchedSteps.push({
|
|
128
|
+
step: stepText,
|
|
129
|
+
functionName: result.functionName,
|
|
130
|
+
operationName: context.operationName,
|
|
131
|
+
inputs: result.inputs || [],
|
|
132
|
+
returns,
|
|
133
|
+
resultName: resultName || `step${i + 1}Result`
|
|
134
|
+
});
|
|
135
|
+
}
|
|
112
136
|
return result.call;
|
|
113
137
|
});
|
|
114
138
|
return {
|
|
115
139
|
code: ` // === EXECUTE ===
|
|
116
140
|
${stepCode.join("\n\n")}`,
|
|
117
|
-
helpers
|
|
141
|
+
helpers,
|
|
142
|
+
unmatchedSteps
|
|
118
143
|
};
|
|
119
144
|
}
|
|
120
145
|
return {
|
|
121
146
|
code: ` // === EXECUTE ===
|
|
122
147
|
${inferLogicFromOperationName(context)}`,
|
|
123
|
-
helpers
|
|
148
|
+
helpers,
|
|
149
|
+
unmatchedSteps
|
|
124
150
|
};
|
|
125
151
|
}
|
|
126
152
|
function inferLogicFromOperationName(context) {
|
|
@@ -154,10 +180,11 @@ function generatePostconditionVerification(postconditions) {
|
|
|
154
180
|
${checks.join("\n")}
|
|
155
181
|
}`;
|
|
156
182
|
}
|
|
157
|
-
function generateEventPublishing(sideEffects, operationName) {
|
|
183
|
+
function generateEventPublishing(sideEffects, operationName, parameterNames) {
|
|
158
184
|
if (!sideEffects || sideEffects.length === 0) return "";
|
|
185
|
+
const paramFields = parameterNames?.length ? parameterNames.join(", ") + ", " : "";
|
|
159
186
|
const publishes = sideEffects.map(
|
|
160
|
-
(event) => `
|
|
187
|
+
(event) => ` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() } as any);`
|
|
161
188
|
);
|
|
162
189
|
return ` // === EVENTS ===
|
|
163
190
|
${publishes.join("\n")}`;
|