@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,6 +19,74 @@
|
|
|
19
19
|
|
|
20
20
|
import type { TemplateContext } from '@specverse/types';
|
|
21
21
|
import { matchStep, type StepContext } from './step-conventions.js';
|
|
22
|
+
import { createHash } from 'crypto';
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
24
|
+
import { join } from 'path';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate generated TypeScript using esbuild's transform.
|
|
28
|
+
* Returns null if valid, or an error message describing the problem.
|
|
29
|
+
*
|
|
30
|
+
* This catches syntax errors (unbalanced braces, unclosed strings, etc.)
|
|
31
|
+
* but not type errors — those need full tsc which requires the project
|
|
32
|
+
* context. That's a separate validation step run by the realize pipeline.
|
|
33
|
+
*/
|
|
34
|
+
async function validateTypeScript(code: string): Promise<string | null> {
|
|
35
|
+
try {
|
|
36
|
+
const esbuild = await import('esbuild');
|
|
37
|
+
await esbuild.transform(code, {
|
|
38
|
+
loader: 'ts',
|
|
39
|
+
format: 'esm',
|
|
40
|
+
target: 'es2022',
|
|
41
|
+
});
|
|
42
|
+
return null;
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
const msg = err?.errors?.[0]?.text || err?.message || 'unknown syntax error';
|
|
45
|
+
return msg;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* AI output cache — avoids re-calling Claude for unchanged steps.
|
|
51
|
+
*
|
|
52
|
+
* Cache key: sha256(step + modelName + operationName + inputs + promptVersion)
|
|
53
|
+
* Cache location: $SPECVERSE_USER_CWD/.specverse/ai-cache/<hash>.ts
|
|
54
|
+
*
|
|
55
|
+
* Invalidation: automatic — any change to the step text or inputs
|
|
56
|
+
* produces a new hash. The prompt version is part of the hash so
|
|
57
|
+
* prompt upgrades also invalidate.
|
|
58
|
+
*/
|
|
59
|
+
const PROMPT_VERSION = '9.1.0';
|
|
60
|
+
|
|
61
|
+
function cacheKey(step: string, modelName: string, operationName: string, functionName: string, inputs: string[]): string {
|
|
62
|
+
const payload = JSON.stringify({ step, modelName, operationName, functionName, inputs: [...inputs].sort(), v: PROMPT_VERSION });
|
|
63
|
+
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cacheDir(): string {
|
|
67
|
+
const cwd = process.env.SPECVERSE_USER_CWD || process.cwd();
|
|
68
|
+
return join(cwd, '.specverse', 'ai-cache');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cacheRead(key: string): string | null {
|
|
72
|
+
const path = join(cacheDir(), `${key}.ts`);
|
|
73
|
+
if (!existsSync(path)) return null;
|
|
74
|
+
try {
|
|
75
|
+
return readFileSync(path, 'utf8');
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cacheWrite(key: string, body: string): void {
|
|
82
|
+
const dir = cacheDir();
|
|
83
|
+
try {
|
|
84
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
85
|
+
writeFileSync(join(dir, `${key}.ts`), body, 'utf8');
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
console.warn(` [ai-cache] write failed: ${err?.message || err}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
22
90
|
|
|
23
91
|
/**
|
|
24
92
|
* Generate AI behaviors file for a controller's unmatched steps.
|
|
@@ -43,6 +111,7 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
43
111
|
operationName: string;
|
|
44
112
|
parameterNames: string[];
|
|
45
113
|
inputs: string[];
|
|
114
|
+
returns?: Record<string, string> | string;
|
|
46
115
|
}> = [];
|
|
47
116
|
|
|
48
117
|
for (const [actionName, action] of Object.entries(controller.actions) as [string, any][]) {
|
|
@@ -61,8 +130,13 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
61
130
|
}
|
|
62
131
|
|
|
63
132
|
for (let i = 0; i < steps.length; i++) {
|
|
64
|
-
const
|
|
65
|
-
|
|
133
|
+
const stepInput = steps[i];
|
|
134
|
+
// Support both string steps and object steps { step, as, returns }
|
|
135
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
|
|
136
|
+
const stepAs = typeof stepInput === 'object' ? stepInput?.as : undefined;
|
|
137
|
+
const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
|
|
138
|
+
|
|
139
|
+
if (typeof stepText !== 'string') continue;
|
|
66
140
|
|
|
67
141
|
const ctx: StepContext = {
|
|
68
142
|
modelName,
|
|
@@ -72,19 +146,21 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
72
146
|
stepNum: i + 1,
|
|
73
147
|
parameterNames,
|
|
74
148
|
declaredVars,
|
|
149
|
+
resultName: stepAs,
|
|
75
150
|
};
|
|
76
151
|
|
|
77
|
-
const result = matchStep(
|
|
152
|
+
const result = matchStep(stepText, ctx);
|
|
78
153
|
if (!result.matched && result.functionName) {
|
|
79
|
-
// Avoid duplicate function definitions
|
|
154
|
+
// Avoid duplicate function definitions
|
|
80
155
|
const existing = unmatchedFunctions.find(f => f.functionName === result.functionName);
|
|
81
156
|
if (!existing) {
|
|
82
157
|
unmatchedFunctions.push({
|
|
83
158
|
functionName: result.functionName,
|
|
84
|
-
step,
|
|
159
|
+
step: stepText,
|
|
85
160
|
operationName: actionName,
|
|
86
161
|
parameterNames,
|
|
87
162
|
inputs: result.inputs || [],
|
|
163
|
+
returns: stepReturns,
|
|
88
164
|
});
|
|
89
165
|
}
|
|
90
166
|
}
|
|
@@ -93,7 +169,40 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
93
169
|
|
|
94
170
|
if (unmatchedFunctions.length === 0) return '';
|
|
95
171
|
|
|
96
|
-
|
|
172
|
+
return generateAiBehaviorsFile({
|
|
173
|
+
ownerName: `${modelName}Controller`,
|
|
174
|
+
unmatchedFunctions: unmatchedFunctions.map(f => ({
|
|
175
|
+
...f,
|
|
176
|
+
modelName,
|
|
177
|
+
})),
|
|
178
|
+
availableModels,
|
|
179
|
+
spec: context.spec,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generate an AI behaviors file from a pre-collected list of unmatched
|
|
185
|
+
* functions. Used by both the controller generator (via generateAiBehaviors
|
|
186
|
+
* above) and the service generator (via collectUnmatched → this function).
|
|
187
|
+
*/
|
|
188
|
+
export async function generateAiBehaviorsFile(opts: {
|
|
189
|
+
ownerName: string;
|
|
190
|
+
unmatchedFunctions: Array<{
|
|
191
|
+
functionName: string;
|
|
192
|
+
step: string;
|
|
193
|
+
operationName: string;
|
|
194
|
+
parameterNames: string[];
|
|
195
|
+
inputs: string[];
|
|
196
|
+
returns?: Record<string, string> | string;
|
|
197
|
+
modelName: string;
|
|
198
|
+
}>;
|
|
199
|
+
availableModels: string[];
|
|
200
|
+
spec?: any;
|
|
201
|
+
}): Promise<string> {
|
|
202
|
+
const { ownerName, unmatchedFunctions, availableModels, spec } = opts;
|
|
203
|
+
if (unmatchedFunctions.length === 0) return '';
|
|
204
|
+
|
|
205
|
+
// Start a Claude session for this owner — all unmatched functions share
|
|
97
206
|
// the conversation context, so Claude builds understanding of the spec
|
|
98
207
|
// across calls (mirrors app-demo's session-based AI service)
|
|
99
208
|
let aiService: any = null;
|
|
@@ -103,17 +212,17 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
103
212
|
if (!aiService.isAvailable) {
|
|
104
213
|
aiService = null;
|
|
105
214
|
} else {
|
|
106
|
-
aiService.startSession(
|
|
215
|
+
aiService.startSession(ownerName);
|
|
107
216
|
}
|
|
108
217
|
} catch {
|
|
109
218
|
aiService = null;
|
|
110
219
|
}
|
|
111
220
|
|
|
112
|
-
const availableModels = (context.spec?.models ? Object.keys(context.spec.models) : []) as string[];
|
|
113
|
-
|
|
114
221
|
// Generate function bodies sequentially — same Claude session for all
|
|
115
222
|
const functions: string[] = [];
|
|
116
|
-
|
|
223
|
+
let cacheHits = 0;
|
|
224
|
+
let cacheMisses = 0;
|
|
225
|
+
for (const { functionName, step, operationName, parameterNames, inputs, returns, modelName } of unmatchedFunctions) {
|
|
117
226
|
// Pure function signature: all inputs as a typed destructured object
|
|
118
227
|
const signature = inputs.length > 0
|
|
119
228
|
? `input: { ${inputs.map(n => `${n}: any`).join('; ')} }`
|
|
@@ -124,10 +233,39 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
124
233
|
? ` const { ${inputs.join(', ')} } = input;`
|
|
125
234
|
: '';
|
|
126
235
|
|
|
127
|
-
|
|
128
|
-
|
|
236
|
+
// Build return type from spec declaration (if provided)
|
|
237
|
+
// returns can be:
|
|
238
|
+
// - a string: "number" → Promise<number>
|
|
239
|
+
// - an object: { amount: "number", rate: "number" } → Promise<{ amount: number; rate: number }>
|
|
240
|
+
// - undefined → Promise<any>
|
|
241
|
+
let returnType = 'any';
|
|
242
|
+
if (typeof returns === 'string') {
|
|
243
|
+
returnType = returns;
|
|
244
|
+
} else if (returns && typeof returns === 'object') {
|
|
245
|
+
const fields = Object.entries(returns).map(([k, v]) => `${k}: ${v}`).join('; ');
|
|
246
|
+
returnType = `{ ${fields} }`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check cache first — skip Claude if we've generated this exact step before
|
|
250
|
+
const key = cacheKey(step, modelName, operationName, functionName, inputs);
|
|
251
|
+
let body: string | null = cacheRead(key);
|
|
252
|
+
let source: 'AI-CACHED' | 'AI-GENERATED' | 'AI-INVALID' | 'STUB' = body ? 'AI-CACHED' : 'STUB';
|
|
253
|
+
if (body) {
|
|
254
|
+
// Validate cache entry — a previously valid entry may have been
|
|
255
|
+
// corrupted on disk or the validator rules may have changed
|
|
256
|
+
const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
|
|
257
|
+
const validationError = await validateTypeScript(testCode);
|
|
258
|
+
if (validationError) {
|
|
259
|
+
console.warn(` [ai-validate] cached ${functionName} failed validation: ${validationError}`);
|
|
260
|
+
body = null; // Force regeneration
|
|
261
|
+
source = 'STUB';
|
|
262
|
+
} else {
|
|
263
|
+
cacheHits++;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
129
266
|
|
|
130
|
-
if (aiService) {
|
|
267
|
+
if (!body && aiService) {
|
|
268
|
+
cacheMisses++;
|
|
131
269
|
try {
|
|
132
270
|
body = await aiService.generateBehavior({
|
|
133
271
|
step,
|
|
@@ -136,9 +274,24 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
136
274
|
functionName,
|
|
137
275
|
parameterNames: inputs, // the actual inputs to the pure function
|
|
138
276
|
availableModels,
|
|
139
|
-
spec
|
|
277
|
+
spec,
|
|
278
|
+
returnType, // Pass declared return type to Claude
|
|
140
279
|
});
|
|
141
|
-
|
|
280
|
+
|
|
281
|
+
if (body) {
|
|
282
|
+
// Validate the generated body as a standalone function
|
|
283
|
+
const testCode = `export async function ${functionName}(input: any): Promise<any> {\n${body}\n}`;
|
|
284
|
+
const validationError = await validateTypeScript(testCode);
|
|
285
|
+
if (validationError) {
|
|
286
|
+
console.warn(` [ai-validate] ${functionName} has syntax error: ${validationError}`);
|
|
287
|
+
// Don't cache invalid output; treat as failed generation
|
|
288
|
+
body = `// AI-generated code failed validation: ${validationError}\n // Step: ${step}\n throw new Error('AI behavior has invalid syntax — see comment above');`;
|
|
289
|
+
source = 'AI-INVALID';
|
|
290
|
+
} else {
|
|
291
|
+
source = 'AI-GENERATED';
|
|
292
|
+
cacheWrite(key, body);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
142
295
|
} catch {
|
|
143
296
|
// Fall through to stub
|
|
144
297
|
}
|
|
@@ -154,22 +307,29 @@ export default async function generateAiBehaviors(context: TemplateContext): Pro
|
|
|
154
307
|
const inputsDoc = inputs.length > 0
|
|
155
308
|
? ` * Inputs: ${inputs.join(', ')}\n`
|
|
156
309
|
: '';
|
|
310
|
+
const returnsDoc = returnType !== 'any'
|
|
311
|
+
? ` * Returns: ${returnType}\n`
|
|
312
|
+
: '';
|
|
157
313
|
|
|
158
314
|
functions.push(`/**
|
|
159
315
|
* ${functionName}
|
|
160
316
|
*
|
|
161
317
|
* Spec step: "${step}"
|
|
162
|
-
* Called by: ${
|
|
163
|
-
${inputsDoc} * Source: ${source}
|
|
318
|
+
* Called by: ${ownerName}.${operationName}()
|
|
319
|
+
${inputsDoc}${returnsDoc} * Source: ${source}
|
|
164
320
|
* Generated: ${new Date().toISOString().split('T')[0]}
|
|
165
321
|
*
|
|
166
322
|
* PURE FUNCTION — no database access, no event publishing, no external services.
|
|
167
323
|
* All data comes in via \`input\`; all effects happen in the calling controller.
|
|
168
324
|
* ${source === 'AI-GENERATED'
|
|
169
325
|
? 'AI-generated implementation. Review and test before deploying.'
|
|
326
|
+
: source === 'AI-CACHED'
|
|
327
|
+
? 'Restored from AI cache (.specverse/ai-cache/). Delete cache entry to regenerate.'
|
|
328
|
+
: source === 'AI-INVALID'
|
|
329
|
+
? 'AI returned code with syntax errors — function throws at runtime. Fix or regenerate.'
|
|
170
330
|
: 'STUB — Claude CLI unavailable. Install Claude Code or implement manually.'}
|
|
171
331
|
*/
|
|
172
|
-
export async function ${functionName}(${signature}): Promise
|
|
332
|
+
export async function ${functionName}(${signature}): Promise<${returnType}> {
|
|
173
333
|
${destructure ? destructure + '\n' : ''}${body}
|
|
174
334
|
}`);
|
|
175
335
|
}
|
|
@@ -177,13 +337,18 @@ ${destructure ? destructure + '\n' : ''}${body}
|
|
|
177
337
|
// End the session
|
|
178
338
|
if (aiService?.endSession) aiService.endSession();
|
|
179
339
|
|
|
340
|
+
// Report cache stats to stdout — picked up by realize pipeline
|
|
341
|
+
if (cacheHits > 0 || cacheMisses > 0) {
|
|
342
|
+
console.log(` AI cache: ${cacheHits} hit(s), ${cacheMisses} miss(es) for ${ownerName}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
180
345
|
return `/**
|
|
181
|
-
* ${
|
|
346
|
+
* ${ownerName} — AI-Generated Behaviors
|
|
182
347
|
*
|
|
183
348
|
* ⚠️ THIS FILE CONTAINS STUBS FOR STEPS THAT NEED IMPLEMENTATION
|
|
184
349
|
*
|
|
185
350
|
* These functions could not be generated from convention patterns.
|
|
186
|
-
* They are called by ${
|
|
351
|
+
* They are called by ${ownerName} when executing operations.
|
|
187
352
|
*
|
|
188
353
|
* Options for each function:
|
|
189
354
|
* - Implement manually (recommended for business-critical logic)
|
|
@@ -28,11 +28,24 @@ export interface BehaviorContext {
|
|
|
28
28
|
parameterNames?: string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A step can be a plain string or an object with extra metadata.
|
|
33
|
+
* String form: "Find Order by id"
|
|
34
|
+
* Object form: { step: "Apply discount", as: "discount", returns: { amount: "number" } }
|
|
35
|
+
*/
|
|
36
|
+
export type Step = string | {
|
|
37
|
+
step: string;
|
|
38
|
+
/** Name the result variable (default: stepNResult) */
|
|
39
|
+
as?: string;
|
|
40
|
+
/** Declare return type for AI functions so types flow through */
|
|
41
|
+
returns?: Record<string, string> | string;
|
|
42
|
+
};
|
|
43
|
+
|
|
31
44
|
export interface BehaviorMetadata {
|
|
32
45
|
preconditions?: string[];
|
|
33
46
|
postconditions?: string[];
|
|
34
47
|
sideEffects?: string[];
|
|
35
|
-
steps?:
|
|
48
|
+
steps?: Step[];
|
|
36
49
|
transactional?: boolean;
|
|
37
50
|
}
|
|
38
51
|
|
|
@@ -52,6 +65,10 @@ export interface BehaviorResult {
|
|
|
52
65
|
operationName: string;
|
|
53
66
|
/** Names of variables passed as the typed input object */
|
|
54
67
|
inputs: string[];
|
|
68
|
+
/** Declared return type from spec ({field: type} or a single type string) */
|
|
69
|
+
returns?: Record<string, string> | string;
|
|
70
|
+
/** Named result variable (default: stepNResult) */
|
|
71
|
+
resultName?: string;
|
|
55
72
|
}>;
|
|
56
73
|
}
|
|
57
74
|
|
|
@@ -216,19 +233,24 @@ function matchPreconditionPattern(
|
|
|
216
233
|
* Generate step logic using convention-based pattern matching.
|
|
217
234
|
*/
|
|
218
235
|
function generateStepLogic(
|
|
219
|
-
steps:
|
|
236
|
+
steps: Step[],
|
|
220
237
|
context: BehaviorContext,
|
|
221
238
|
preconditionDeclared?: Set<string>
|
|
222
|
-
): { code: string; helpers: string[]; unmatchedSteps:
|
|
239
|
+
): { code: string; helpers: string[]; unmatchedSteps: BehaviorResult['unmatchedSteps'] } {
|
|
223
240
|
const helpers: string[] = [];
|
|
224
|
-
const unmatchedSteps:
|
|
241
|
+
const unmatchedSteps: BehaviorResult['unmatchedSteps'] = [];
|
|
225
242
|
|
|
226
243
|
// Shared declared variables across steps — carried through from preconditions
|
|
227
244
|
const declaredVars = preconditionDeclared || new Set<string>();
|
|
228
245
|
|
|
229
246
|
if (steps && steps.length > 0) {
|
|
230
|
-
const stepCode = steps.map((
|
|
231
|
-
|
|
247
|
+
const stepCode = steps.map((stepInput, i) => {
|
|
248
|
+
// Normalize to { text, as, returns }
|
|
249
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput.step;
|
|
250
|
+
const resultName = typeof stepInput === 'object' ? stepInput.as : undefined;
|
|
251
|
+
const returns = typeof stepInput === 'object' ? stepInput.returns : undefined;
|
|
252
|
+
|
|
253
|
+
if (typeof stepText !== 'string') {
|
|
232
254
|
return ` // Step ${i + 1}: Complex operation — see expanded definition`;
|
|
233
255
|
}
|
|
234
256
|
|
|
@@ -240,18 +262,21 @@ function generateStepLogic(
|
|
|
240
262
|
stepNum: i + 1,
|
|
241
263
|
parameterNames: context.parameterNames,
|
|
242
264
|
declaredVars,
|
|
265
|
+
resultName, // Pass through the named result
|
|
243
266
|
};
|
|
244
267
|
|
|
245
|
-
const result = matchStep(
|
|
268
|
+
const result = matchStep(stepText, ctx);
|
|
246
269
|
if (result.helperMethod) {
|
|
247
270
|
helpers.push(result.helperMethod);
|
|
248
271
|
}
|
|
249
272
|
if (!result.matched && result.functionName) {
|
|
250
273
|
unmatchedSteps.push({
|
|
251
|
-
step,
|
|
274
|
+
step: stepText,
|
|
252
275
|
functionName: result.functionName,
|
|
253
276
|
operationName: context.operationName,
|
|
254
277
|
inputs: result.inputs || [],
|
|
278
|
+
returns,
|
|
279
|
+
resultName: resultName || `step${i + 1}Result`,
|
|
255
280
|
});
|
|
256
281
|
}
|
|
257
282
|
return result.call;
|
|
@@ -331,7 +356,7 @@ export function generateEventPublishing(
|
|
|
331
356
|
: '';
|
|
332
357
|
|
|
333
358
|
const publishes = sideEffects.map(event =>
|
|
334
|
-
` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() });`
|
|
359
|
+
` await eventBus.publish('${event}', { ${paramFields}timestamp: new Date().toISOString() } as any);`
|
|
335
360
|
);
|
|
336
361
|
|
|
337
362
|
return ` // === EVENTS ===\n${publishes.join('\n')}`;
|
|
@@ -26,9 +26,15 @@ export default function generatePrismaController(context: TemplateContext): stri
|
|
|
26
26
|
const controllerName = controller.name;
|
|
27
27
|
const modelName = model.name;
|
|
28
28
|
const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
29
|
-
// Avoid JavaScript reserved words as variable names
|
|
29
|
+
// Avoid JavaScript reserved words as local variable names
|
|
30
30
|
const RESERVED_WORDS = new Set(['import', 'export', 'default', 'class', 'function', 'return', 'delete', 'new', 'this', 'switch', 'case', 'break', 'continue', 'for', 'while', 'do', 'if', 'else', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'let', 'const', 'var', 'void', 'with', 'yield', 'async', 'await', 'enum', 'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'super', 'extends']);
|
|
31
31
|
const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
|
|
32
|
+
// Prisma delegate name always matches the Prisma model's camelCase form —
|
|
33
|
+
// even when the model name is a JS reserved word (prisma.import, prisma.export).
|
|
34
|
+
// Access these via bracket notation so tsc doesn't choke on the keyword.
|
|
35
|
+
const prismaDelegate = RESERVED_WORDS.has(rawModelVar)
|
|
36
|
+
? `prisma['${rawModelVar}']`
|
|
37
|
+
: `prisma.${rawModelVar}`;
|
|
32
38
|
const curedOps = controller.cured || {};
|
|
33
39
|
|
|
34
40
|
// Determine ID type for proper parsing
|
|
@@ -62,11 +68,11 @@ function parseId(id: string): ${needsIntParse ? 'number' : 'string'} {
|
|
|
62
68
|
*/
|
|
63
69
|
export class ${controllerName} {
|
|
64
70
|
${generateValidateMethod(model, modelName)}
|
|
65
|
-
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, controller, allModels) : ''}
|
|
66
|
-
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar) : ''}
|
|
67
|
-
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ''}
|
|
68
|
-
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ''}
|
|
69
|
-
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ''}
|
|
71
|
+
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ''}
|
|
72
|
+
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : ''}
|
|
73
|
+
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ''}
|
|
74
|
+
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) : ''}
|
|
75
|
+
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) : ''}
|
|
70
76
|
${customActions.code}
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -163,10 +169,7 @@ function generateValidationLogic(model: any, dataParam: string = '_data', contex
|
|
|
163
169
|
/**
|
|
164
170
|
* Generate create method
|
|
165
171
|
*/
|
|
166
|
-
function generateCreateMethod(model: any, modelName: string, modelVar: string, controller: any, allModels?: any[]): string {
|
|
167
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
168
|
-
const createEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Created')) : null;
|
|
169
|
-
|
|
172
|
+
function generateCreateMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any, allModels?: any[]): string {
|
|
170
173
|
return `
|
|
171
174
|
/**
|
|
172
175
|
* Create a new ${modelName}
|
|
@@ -183,17 +186,12 @@ function generateCreateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
183
186
|
${generateFKTransform(model, 'prismaData', allModels)}
|
|
184
187
|
|
|
185
188
|
// Create record
|
|
186
|
-
const ${modelVar} = await
|
|
189
|
+
const ${modelVar} = await ${prismaDelegate}.create({
|
|
187
190
|
data: prismaData${generateIncludeRelationships(model)}
|
|
188
191
|
});
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
eventBus.publish(EventName.${createEvent}, {
|
|
193
|
-
${modelVar},
|
|
194
|
-
timestamp: new Date().toISOString()
|
|
195
|
-
});
|
|
196
|
-
` : ''}
|
|
193
|
+
// Publish CURED event
|
|
194
|
+
await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
197
195
|
|
|
198
196
|
return ${modelVar};
|
|
199
197
|
}
|
|
@@ -203,13 +201,13 @@ function generateCreateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
203
201
|
/**
|
|
204
202
|
* Generate retrieve method
|
|
205
203
|
*/
|
|
206
|
-
function generateRetrieveMethod(model: any, modelName: string, modelVar: string): string {
|
|
204
|
+
function generateRetrieveMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string): string {
|
|
207
205
|
return `
|
|
208
206
|
/**
|
|
209
207
|
* Retrieve ${modelName} by ID
|
|
210
208
|
*/
|
|
211
209
|
public async retrieve(id: string): Promise<any> {
|
|
212
|
-
const ${modelVar} = await
|
|
210
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({
|
|
213
211
|
where: { id: parseId(id) }${generateIncludeRelationships(model)}
|
|
214
212
|
});
|
|
215
213
|
|
|
@@ -224,7 +222,7 @@ function generateRetrieveMethod(model: any, modelName: string, modelVar: string)
|
|
|
224
222
|
* Retrieve all ${modelName}s
|
|
225
223
|
*/
|
|
226
224
|
public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
|
|
227
|
-
return await
|
|
225
|
+
return await ${prismaDelegate}.findMany({
|
|
228
226
|
skip: options.skip,
|
|
229
227
|
take: options.take${generateIncludeRelationships(model)}
|
|
230
228
|
});
|
|
@@ -235,10 +233,7 @@ function generateRetrieveMethod(model: any, modelName: string, modelVar: string)
|
|
|
235
233
|
/**
|
|
236
234
|
* Generate update method
|
|
237
235
|
*/
|
|
238
|
-
function generateUpdateMethod(model: any, modelName: string, modelVar: string, controller: any, allModels?: any[]): string {
|
|
239
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
240
|
-
const updateEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Updated')) : null;
|
|
241
|
-
|
|
236
|
+
function generateUpdateMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any, allModels?: any[]): string {
|
|
242
237
|
return `
|
|
243
238
|
/**
|
|
244
239
|
* Update ${modelName}
|
|
@@ -263,18 +258,13 @@ function generateUpdateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
263
258
|
${generateFKTransform(model, 'updateData', allModels)}
|
|
264
259
|
|
|
265
260
|
// Update record
|
|
266
|
-
const ${modelVar} = await
|
|
261
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
267
262
|
where: { id: parseId(id) },
|
|
268
263
|
data: updateData${generateIncludeRelationships(model)}
|
|
269
264
|
});
|
|
270
265
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
eventBus.publish(EventName.${updateEvent}, {
|
|
274
|
-
${modelVar},
|
|
275
|
-
timestamp: new Date().toISOString()
|
|
276
|
-
});
|
|
277
|
-
` : ''}
|
|
266
|
+
// Publish CURED event
|
|
267
|
+
await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
278
268
|
|
|
279
269
|
return ${modelVar};
|
|
280
270
|
}
|
|
@@ -284,7 +274,7 @@ function generateUpdateMethod(model: any, modelName: string, modelVar: string, c
|
|
|
284
274
|
/**
|
|
285
275
|
* Generate evolve method (lifecycle-aware updates)
|
|
286
276
|
*/
|
|
287
|
-
function generateEvolveMethod(model: any, modelName: string, modelVar: string, controller: any): string {
|
|
277
|
+
function generateEvolveMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any): string {
|
|
288
278
|
// Extract lifecycle — handle both array and object format
|
|
289
279
|
const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles :
|
|
290
280
|
(model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]: [string, any]) => ({ name, ...lc })) : []);
|
|
@@ -308,7 +298,7 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
308
298
|
}
|
|
309
299
|
|
|
310
300
|
// Get current record to check lifecycle state
|
|
311
|
-
const current = await
|
|
301
|
+
const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
312
302
|
if (!current) {
|
|
313
303
|
throw new Error('${modelName} not found');
|
|
314
304
|
}
|
|
@@ -327,11 +317,14 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
327
317
|
` : ''}
|
|
328
318
|
|
|
329
319
|
// Update record
|
|
330
|
-
const ${modelVar} = await
|
|
320
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
331
321
|
where: { id: parseId(id) },
|
|
332
322
|
data${generateIncludeRelationships(model)}
|
|
333
323
|
});
|
|
334
324
|
|
|
325
|
+
// Publish CURED event
|
|
326
|
+
await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
327
|
+
|
|
335
328
|
return ${modelVar};
|
|
336
329
|
}
|
|
337
330
|
`;
|
|
@@ -340,33 +333,23 @@ function generateEvolveMethod(model: any, modelName: string, modelVar: string, c
|
|
|
340
333
|
/**
|
|
341
334
|
* Generate delete method
|
|
342
335
|
*/
|
|
343
|
-
function generateDeleteMethod(model: any, modelName: string, modelVar: string, controller: any): string {
|
|
344
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
345
|
-
const deleteEvent = hasEvents ? controller.publishes.find((e: string) => e.includes('Deleted')) : null;
|
|
346
|
-
|
|
336
|
+
function generateDeleteMethod(model: any, modelName: string, modelVar: string, prismaDelegate: string, controller: any): string {
|
|
347
337
|
return `
|
|
348
338
|
/**
|
|
349
339
|
* Delete ${modelName}
|
|
350
340
|
*/
|
|
351
341
|
public async delete(id: string): Promise<void> {
|
|
352
|
-
${deleteEvent ? `
|
|
353
342
|
// Get record before deletion for event
|
|
354
|
-
const ${modelVar} = await
|
|
355
|
-
` : ''}
|
|
343
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
356
344
|
|
|
357
|
-
await
|
|
345
|
+
await ${prismaDelegate}.delete({
|
|
358
346
|
where: { id: parseId(id) }
|
|
359
347
|
});
|
|
360
348
|
|
|
361
|
-
|
|
362
|
-
// Publish event
|
|
349
|
+
// Publish CURED event
|
|
363
350
|
if (${modelVar}) {
|
|
364
|
-
eventBus.publish(
|
|
365
|
-
${modelVar},
|
|
366
|
-
timestamp: new Date().toISOString()
|
|
367
|
-
});
|
|
351
|
+
await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
368
352
|
}
|
|
369
|
-
` : ''}
|
|
370
353
|
}
|
|
371
354
|
`;
|
|
372
355
|
}
|
|
@@ -548,12 +531,7 @@ ${includes}
|
|
|
548
531
|
* Check if controller publishes events
|
|
549
532
|
*/
|
|
550
533
|
function hasEventPublishing(curedOps: any, controller: any): boolean {
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
for (const action of Object.values(controller.actions) as any[]) {
|
|
555
|
-
if (action.publishes?.length > 0 || action.events?.length > 0 || action.sideEffects?.length > 0) return true;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
534
|
+
// Controllers always publish CURED events (Created, Updated, Deleted, Evolved)
|
|
535
|
+
// so the event bus is always needed
|
|
536
|
+
return true;
|
|
559
537
|
}
|