@specverse/engines 4.1.14 → 4.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +7 -1
  2. package/dist/ai/behavior-ai-service.d.ts +2 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -1
  4. package/dist/ai/behavior-ai-service.js +2 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -1
  6. package/dist/ai/prompt-loader.js +2 -2
  7. package/dist/inference/index.d.ts +2 -1
  8. package/dist/inference/index.d.ts.map +1 -1
  9. package/dist/inference/index.js +2 -1
  10. package/dist/inference/index.js.map +1 -1
  11. package/dist/inference/quint-transpiler.d.ts +18 -1
  12. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  13. package/dist/inference/quint-transpiler.js +501 -21
  14. package/dist/inference/quint-transpiler.js.map +1 -1
  15. package/dist/inference/verification.d.ts +78 -0
  16. package/dist/inference/verification.d.ts.map +1 -0
  17. package/dist/inference/verification.js +263 -0
  18. package/dist/inference/verification.js.map +1 -0
  19. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  20. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  21. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  22. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +111 -27
  23. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
  24. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
  25. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  26. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
  27. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
  28. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
  29. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  30. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
  31. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  32. package/dist/realize/index.d.ts.map +1 -1
  33. package/dist/realize/index.js +123 -25
  34. package/dist/realize/index.js.map +1 -1
  35. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  36. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  37. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  38. package/libs/instance-factories/cli/templates/commander/command-generator.ts +134 -27
  39. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
  40. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
  41. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  42. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
  43. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
  44. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
  45. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  46. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
  47. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  48. package/package.json +1 -1
  49. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  50. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  57. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  58. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  59. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  60. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  61. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  62. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  63. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  64. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  65. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  66. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -19,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 step = steps[i];
65
- if (typeof step !== 'string') continue;
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(step, ctx);
152
+ const result = matchStep(stepText, ctx);
78
153
  if (!result.matched && result.functionName) {
79
- // Avoid duplicate function definitions, but capture the widest set of inputs
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
- // Start a Claude session for this controller — all unmatched functions share
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(`${modelName}Controller`);
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
- for (const { functionName, step, operationName, parameterNames, inputs } of unmatchedFunctions) {
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
- let body: string | null = null;
128
- let source = 'STUB';
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: context.spec,
277
+ spec,
278
+ returnType, // Pass declared return type to Claude
140
279
  });
141
- if (body) source = 'AI-GENERATED';
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: ${modelName}Controller.${operationName}()
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<any> {
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
- * ${modelName}Controller — AI-Generated Behaviors
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 ${modelName}Controller when executing custom actions.
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?: string[];
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: string[],
236
+ steps: Step[],
220
237
  context: BehaviorContext,
221
238
  preconditionDeclared?: Set<string>
222
- ): { code: string; helpers: string[]; unmatchedSteps: Array<{ step: string; functionName: string; operationName: string; inputs: string[] }> } {
239
+ ): { code: string; helpers: string[]; unmatchedSteps: BehaviorResult['unmatchedSteps'] } {
223
240
  const helpers: string[] = [];
224
- const unmatchedSteps: Array<{ step: string; functionName: string; operationName: string; inputs: string[] }> = [];
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((step, i) => {
231
- if (typeof step !== 'string') {
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(step, ctx);
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 prisma.${modelVar}.create({
189
+ const ${modelVar} = await ${prismaDelegate}.create({
187
190
  data: prismaData${generateIncludeRelationships(model)}
188
191
  });
189
192
 
190
- ${createEvent ? `
191
- // Publish event
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 prisma.${modelVar}.findUnique({
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 prisma.${modelVar}.findMany({
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 prisma.${modelVar}.update({
261
+ const ${modelVar} = await ${prismaDelegate}.update({
267
262
  where: { id: parseId(id) },
268
263
  data: updateData${generateIncludeRelationships(model)}
269
264
  });
270
265
 
271
- ${updateEvent ? `
272
- // Publish event
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 prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
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 prisma.${modelVar}.update({
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 prisma.${modelVar}.findUnique({ where: { id: parseId(id) } });
355
- ` : ''}
343
+ const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
356
344
 
357
- await prisma.${modelVar}.delete({
345
+ await ${prismaDelegate}.delete({
358
346
  where: { id: parseId(id) }
359
347
  });
360
348
 
361
- ${deleteEvent ? `
362
- // Publish event
349
+ // Publish CURED event
363
350
  if (${modelVar}) {
364
- eventBus.publish(EventName.${deleteEvent}, {
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
- if (controller.publishes && Array.isArray(controller.publishes) && controller.publishes.length > 0) return true;
552
- // Check if any custom action publishes events
553
- if (controller.actions) {
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
  }