@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.
Files changed (68) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +126 -0
  2. package/dist/ai/behavior-ai-service.d.ts +65 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -0
  4. package/dist/ai/behavior-ai-service.js +205 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -0
  6. package/dist/ai/index.d.ts +27 -0
  7. package/dist/ai/index.d.ts.map +1 -1
  8. package/dist/ai/index.js +30 -0
  9. package/dist/ai/index.js.map +1 -1
  10. package/dist/ai/prompt-loader.js +2 -2
  11. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  12. package/dist/inference/quint-transpiler.js +204 -4
  13. package/dist/inference/quint-transpiler.js.map +1 -1
  14. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  15. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  16. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  17. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +97 -22
  18. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
  19. package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
  20. package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
  21. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
  22. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
  23. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  24. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +249 -0
  25. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
  26. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -54
  27. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  28. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
  29. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  30. package/dist/realize/index.d.ts.map +1 -1
  31. package/dist/realize/index.js +192 -23
  32. package/dist/realize/index.js.map +1 -1
  33. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  34. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  35. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  36. package/libs/instance-factories/cli/templates/commander/command-generator.ts +115 -22
  37. package/libs/instance-factories/communication/event-emitter.yaml +16 -12
  38. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
  39. package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
  40. package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
  41. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
  42. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
  43. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  44. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
  45. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
  46. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
  47. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  48. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
  49. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  50. package/package.json +1 -1
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  57. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  58. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  59. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  60. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  61. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  62. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  63. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  64. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  65. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  66. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  67. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  68. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -22,23 +22,29 @@ export default function generatePrismaService(context: TemplateContext): string
22
22
  const hasEvents = (service.publishes && service.publishes.length > 0) ||
23
23
  (service.subscribes && service.subscribes.length > 0);
24
24
 
25
+ // Generate operations first so we can detect what symbols they use in the output
26
+ const operationsCode = generateOperationsWithHelpers(service);
27
+ // If the generated code contains `aiBehaviors.`, we need to import the AI file
28
+ const hasAiBehaviors = /\baiBehaviors\./.test(operationsCode);
29
+ // Only emit the prisma client binding + import if the operations actually use it
30
+ const usesPrisma = /\bprisma\b/.test(operationsCode);
31
+
25
32
  return `/**
26
33
  * ${serviceName}
27
34
  * Abstract business logic service
28
35
  * ${service.description || ''}
29
36
  */
30
-
31
- import { PrismaClient } from '@prisma/client';
32
- ${hasEvents ? `import { eventBus, EventName } from '../events/eventBus.js';` : ''}
33
-
34
- const prisma = new PrismaClient();
37
+ ${usesPrisma ? `\nimport { PrismaClient } from '@prisma/client';` : ''}
38
+ ${hasEvents ? `import { eventBus } from '../events/eventBus.js';` : ''}
39
+ ${hasAiBehaviors ? `import * as aiBehaviors from '../behaviors/${serviceName}.ai.js';` : ''}
40
+ ${usesPrisma ? `\nconst prisma = new PrismaClient();` : ''}
35
41
 
36
42
  /**
37
43
  * ${serviceName} class
38
44
  */
39
45
  export class ${serviceName} {
40
46
  ${generateConstructor(service)}
41
- ${generateOperationsWithHelpers(service)}
47
+ ${operationsCode}
42
48
  ${generateEventSubscriptions(service)}
43
49
  }
44
50
 
@@ -143,8 +149,12 @@ function generateOperations(service: any): string {
143
149
  * Generate individual operation
144
150
  */
145
151
  function generateOperation(operationName: string, operation: any, service: any): string {
146
- const params = generateOperationParams(operation);
152
+ const rawParams = generateOperationParams(operation);
147
153
  const hasPublish = service.publishes && service.publishes.length > 0;
154
+ const body = generateOperationLogic(operation, service);
155
+ // Rename any operation parameter that the body doesn't reference so tsc's
156
+ // noUnusedParameters doesn't trip on placeholder service stubs.
157
+ const params = renameUnusedParams(rawParams, body);
148
158
 
149
159
  return `
150
160
  /**
@@ -153,7 +163,7 @@ function generateOperation(operationName: string, operation: any, service: any):
153
163
  */
154
164
  public async ${operationName}(${params}): Promise<any> {
155
165
  try {
156
- ${generateOperationLogic(operation, service)}
166
+ ${body}
157
167
 
158
168
  ${hasPublish ? `
159
169
  // Publish event (example)
@@ -173,6 +183,24 @@ function generateOperation(operationName: string, operation: any, service: any):
173
183
  `;
174
184
  }
175
185
 
186
+ /**
187
+ * Rename each parameter with a leading underscore if its name isn't referenced
188
+ * in the operation body (suppresses TS6133 on placeholder stubs).
189
+ */
190
+ function renameUnusedParams(paramsString: string, body: string): string {
191
+ if (!paramsString.trim()) return paramsString;
192
+ return paramsString.split(',').map(segment => {
193
+ const trimmed = segment.trim();
194
+ const nameMatch = trimmed.match(/^(\w+)/);
195
+ if (!nameMatch) return segment;
196
+ const name = nameMatch[1];
197
+ if (name.startsWith('_')) return segment;
198
+ const re = new RegExp(`\\b${name}\\b`);
199
+ if (re.test(body)) return segment;
200
+ return segment.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
201
+ }).join(', ');
202
+ }
203
+
176
204
  /**
177
205
  * Generate operation parameters
178
206
  */
@@ -214,18 +242,20 @@ function generateOperationLogic(operation: any, service: any): string {
214
242
  if (impl.preconditions?.length || impl.postconditions?.length || impl.steps?.length || impl.transactional) {
215
243
  // L3: Generate from behavioral specification
216
244
  const modelName = inferModelFromServiceName(service.name);
245
+ const parameterNames = Object.keys(operation.parameters || {});
217
246
  const context: BehaviorContext = {
218
247
  modelName,
219
248
  serviceName: service.name,
220
249
  operationName: operation.name || 'execute',
221
- prismaModel: modelName
250
+ prismaModel: modelName,
251
+ parameterNames,
222
252
  };
223
253
 
224
254
  const behavior: BehaviorMetadata = {
225
255
  preconditions: impl.preconditions || [],
226
256
  postconditions: impl.postconditions || [],
227
257
  sideEffects: impl.sideEffects || [],
228
- steps: impl.steps || [],
258
+ steps: impl.steps || operation.steps || [],
229
259
  transactional: impl.transactional || false
230
260
  };
231
261
 
@@ -18,6 +18,12 @@ export interface StepContext {
18
18
  serviceName: string;
19
19
  operationName: string;
20
20
  stepNum: number;
21
+ /** Parameter names from the action (e.g., ['pollId', 'optionId']) */
22
+ parameterNames?: string[];
23
+ /** Variables already declared (to avoid redeclaration) */
24
+ declaredVars?: Set<string>;
25
+ /** Named result variable (from spec's `as:` clause) */
26
+ resultName?: string;
21
27
  }
22
28
 
23
29
  function toVar(name: string): string {
@@ -28,15 +34,81 @@ function toMethod(words: string): string {
28
34
  return words.trim().replace(/\s+(.)/g, (_, c) => c.toUpperCase()).replace(/^\w/, c => c.toLowerCase());
29
35
  }
30
36
 
37
+ /**
38
+ * Resolve a value expression in a step to a TypeScript expression.
39
+ *
40
+ * Handles:
41
+ * - "current time", "now", "timestamp" → new Date().toISOString()
42
+ * - References to declared variables (e.g., "discount.amount") → discount.amount
43
+ * - References to operation parameters → parameter name
44
+ * - References to stepNResult → stepNResult
45
+ * - Numbers → numeric literal
46
+ * - Boolean literals → true/false
47
+ * - Everything else → quoted string literal
48
+ */
49
+ function resolveValue(rawValue: string, ctx: StepContext): string {
50
+ const value = rawValue.trim().replace(/^['"]|['"]$/g, '');
51
+
52
+ // Time expressions
53
+ if (/^(current\s*time|now|timestamp)$/i.test(value)) {
54
+ return 'new Date().toISOString()';
55
+ }
56
+
57
+ // Boolean literals
58
+ if (value === 'true' || value === 'false') return value;
59
+
60
+ // Numeric literal
61
+ if (/^-?\d+(\.\d+)?$/.test(value)) return value;
62
+
63
+ const declared = ctx.declaredVars || new Set();
64
+ const params = ctx.parameterNames || [];
65
+
66
+ // Reference: "varName" or "varName.field" — check if root matches a declared var or parameter
67
+ const rootMatch = value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z0-9_.]+)?$/);
68
+ if (rootMatch) {
69
+ const root = rootMatch[1];
70
+ if (declared.has(root) || params.includes(root)) {
71
+ return value; // use as-is (e.g., "discount.amount")
72
+ }
73
+ }
74
+
75
+ // Natural language reference: "calculated amount", "the discount" etc.
76
+ // Try to match to a declared var by tokenising
77
+ const tokens = value.toLowerCase().split(/\s+/);
78
+ for (const declaredName of [...declared, ...params]) {
79
+ const normalized = declaredName.toLowerCase();
80
+ if (tokens.includes(normalized)) return declaredName;
81
+ }
82
+
83
+ // "calculated X" / "computed X" / "the X" — look for a stepNResult with
84
+ // matching field. Since we don't know the AI function's return shape at
85
+ // compile time, emit a typed access with a TODO comment the developer can fix.
86
+ const calcMatch = value.match(/^(?:the\s+|calculated\s+|computed\s+|resulting\s+)(\w+)/i);
87
+ if (calcMatch) {
88
+ const field = calcMatch[1];
89
+ // Find most recent stepNResult
90
+ const stepResults = [...declared].filter(v => /^step\d+Result$/.test(v)).sort().reverse();
91
+ if (stepResults.length > 0) {
92
+ return `(${stepResults[0]} as any).${field} /* TODO: verify field name */`;
93
+ }
94
+ }
95
+
96
+ // Looks like natural language (multiple words with spaces) — emit TODO
97
+ if (/\s/.test(value)) {
98
+ return `/* TODO: resolve "${value}" — no matching declared variable */ null`;
99
+ }
100
+
101
+ // Fallback: quoted string literal
102
+ return `'${value.replace(/'/g, "\\'")}'`;
103
+ }
104
+
31
105
  export const STEP_CONVENTIONS: StepConvention[] = [
32
106
  // --- Validation ---
33
107
  {
34
108
  name: 'validate',
35
109
  pattern: /^validate\s+(.+)/i,
36
110
  generateCall: (m, ctx) => ` // Step ${ctx.stepNum}: Validate ${m[1]}
37
- const validationResult = this.validate(params, { operation: '${ctx.operationName}' });
38
- if (!validationResult.valid) {
39
- throw new Error(\`Validation failed: \${validationResult.errors.join(', ')}\`);
111
+ // TODO: Add validation logic for ${m[1]}
40
112
  }`,
41
113
  },
42
114
 
@@ -48,21 +120,9 @@ export const STEP_CONVENTIONS: StepConvention[] = [
48
120
  const condition = m[1];
49
121
  const methodName = toMethod('check ' + condition);
50
122
  return ` // Step ${ctx.stepNum}: Check ${condition}
51
- const checkResult = await this.${methodName}(params);
52
- if (!checkResult) {
53
- throw new Error('Check failed: ${condition}');
54
- }`;
55
- },
56
- generateMethod: (m, ctx) => {
57
- const condition = m[1];
58
- const methodName = toMethod('check ' + condition);
59
- return `
60
- private async ${methodName}(params: any): Promise<boolean> {
61
123
  // TODO: Implement check — ${condition}
62
- const ${toVar(ctx.prismaModel)} = await this.prisma.${toVar(ctx.prismaModel)}.findUnique({ where: { id: params.id } });
63
- if (!${toVar(ctx.prismaModel)}) return false;
64
- return true;
65
- }`;
124
+ // const checkResult = ...
125
+ // if (!checkResult) throw new Error('Check failed: ${condition}');`;
66
126
  },
67
127
  },
68
128
 
@@ -73,22 +133,40 @@ export const STEP_CONVENTIONS: StepConvention[] = [
73
133
  generateCall: (m, ctx) => {
74
134
  const model = m[1];
75
135
  const field = m[2];
136
+ const modelVar = toVar(model);
137
+ const params = ctx.parameterNames || [];
138
+ const declared = ctx.declaredVars || new Set();
139
+
140
+ // Find the ID parameter: modelId (e.g., pollId) or field directly
141
+ const idParam = field === 'id'
142
+ ? (params.find(p => p === `${modelVar}Id`) || params.find(p => p === 'id') || `${modelVar}Id`)
143
+ : (params.find(p => p === field) || `params.${field}`);
144
+
145
+ // Skip if already declared by precondition
146
+ if (declared.has(modelVar)) {
147
+ return ` // Step ${ctx.stepNum}: Find ${model} by ${field} (already loaded)`;
148
+ }
149
+ declared.add(modelVar);
150
+
76
151
  return ` // Step ${ctx.stepNum}: Find ${model} by ${field}
77
- const ${toVar(model)} = await this.prisma.${toVar(model)}.findUnique({ where: { ${field}: params.${field} } });
78
- if (!${toVar(model)}) {
79
- throw new Error('${model} not found by ${field}');
80
- }`;
152
+ const ${modelVar} = await prisma.${modelVar}.findUniqueOrThrow({ where: { ${field}: ${idParam} } });`;
81
153
  },
82
154
  },
83
155
 
84
156
  // --- Create ---
85
157
  {
86
158
  name: 'create',
87
- pattern: /^create\s+(\w+)(?:\s+record)?/i,
159
+ pattern: /^create\s+(\w+)(?:\s+(?:with\s+)?(.+))?/i,
88
160
  generateCall: (m, ctx) => {
89
161
  const model = m[1];
162
+ const modelVar = toVar(model);
163
+ // Build data from parameter names if available
164
+ const paramNames = ctx.parameterNames || [];
165
+ const dataFields = paramNames.length > 0
166
+ ? `{ ${paramNames.join(', ')} }`
167
+ : 'data';
90
168
  return ` // Step ${ctx.stepNum}: Create ${model}
91
- const ${toVar(model)} = await this.prisma.${toVar(model)}.create({ data: params });`;
169
+ const ${modelVar} = await prisma.${modelVar}.create({ data: ${dataFields} });`;
92
170
  },
93
171
  },
94
172
 
@@ -99,12 +177,12 @@ export const STEP_CONVENTIONS: StepConvention[] = [
99
177
  generateCall: (m, ctx) => {
100
178
  const model = m[1];
101
179
  const field = m[2];
102
- const value = m[3].trim().replace(/"/g, "'");
103
- const isString = /^[a-z]/.test(value) && !value.includes('.');
104
- const val = isString ? `'${value}'` : value;
105
- return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${value}
106
- await this.prisma.${toVar(model)}.update({
107
- where: { id: params.id },
180
+ const rawValue = m[3];
181
+ const modelVar = toVar(model);
182
+ const val = resolveValue(rawValue, ctx);
183
+ return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${rawValue.trim()}
184
+ await prisma.${modelVar}.update({
185
+ where: { id: ${modelVar}.id },
108
186
  data: { ${field}: ${val} },
109
187
  });`;
110
188
  },
@@ -116,9 +194,10 @@ export const STEP_CONVENTIONS: StepConvention[] = [
116
194
  pattern: /^update\s+(\w+)(?:\s+(.+))?/i,
117
195
  generateCall: (m, ctx) => {
118
196
  const model = m[1];
197
+ const modelVar = toVar(model);
119
198
  return ` // Step ${ctx.stepNum}: Update ${model}
120
- await this.prisma.${toVar(model)}.update({
121
- where: { id: params.id },
199
+ await prisma.${modelVar}.update({
200
+ where: { id: ${modelVar}.id },
122
201
  data: params,
123
202
  });`;
124
203
  },
@@ -130,8 +209,9 @@ export const STEP_CONVENTIONS: StepConvention[] = [
130
209
  pattern: /^delete\s+(\w+)/i,
131
210
  generateCall: (m, ctx) => {
132
211
  const model = m[1];
212
+ const modelVar = toVar(model);
133
213
  return ` // Step ${ctx.stepNum}: Delete ${model}
134
- await this.prisma.${toVar(model)}.delete({ where: { id: params.id } });`;
214
+ await prisma.${modelVar}.delete({ where: { id: ${modelVar}.id } });`;
135
215
  },
136
216
  },
137
217
 
@@ -142,11 +222,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
142
222
  generateCall: (m, ctx) => {
143
223
  const model = m[1];
144
224
  const state = m[2];
225
+ const modelVar = toVar(model);
145
226
  return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
146
- const current = await this.prisma.${toVar(model)}.findUniqueOrThrow({ where: { id: params.id } });
147
- // Lifecycle validation would check valid transition here
148
- await this.prisma.${toVar(model)}.update({
149
- where: { id: params.id },
227
+ if (${modelVar}.status === '${state}') throw new Error('${model} is already ${state}');
228
+ await prisma.${modelVar}.update({
229
+ where: { id: ${modelVar}.id },
150
230
  data: { status: '${state}' },
151
231
  });`;
152
232
  },
@@ -158,12 +238,12 @@ export const STEP_CONVENTIONS: StepConvention[] = [
158
238
  pattern: /^set\s+(\w+)\s+to\s+(.+)/i,
159
239
  generateCall: (m, ctx) => {
160
240
  const field = m[1];
161
- const value = m[2].trim();
162
- const isCurrentTime = /current time|now|timestamp/i.test(value);
163
- const val = isCurrentTime ? 'new Date()' : `'${value}'`;
164
- return ` // Step ${ctx.stepNum}: Set ${field} to ${value}
165
- await this.prisma.${toVar(ctx.prismaModel)}.update({
166
- where: { id: params.id },
241
+ const rawValue = m[2];
242
+ const modelVar = toVar(ctx.prismaModel);
243
+ const val = resolveValue(rawValue, ctx);
244
+ return ` // Step ${ctx.stepNum}: Set ${field} to ${rawValue.trim()}
245
+ await prisma.${modelVar}.update({
246
+ where: { id: ${modelVar}.id },
167
247
  data: { ${field}: ${val} },
168
248
  });`;
169
249
  },
@@ -176,10 +256,11 @@ export const STEP_CONVENTIONS: StepConvention[] = [
176
256
  generateCall: (m, ctx) => {
177
257
  const field = m[1];
178
258
  const amount = m[2];
179
- const amountVal = /^\d+$/.test(amount) ? amount : `params.${amount}`;
259
+ const modelVar = toVar(ctx.prismaModel);
260
+ const amountVal = /^\d+$/.test(amount) ? amount : amount;
180
261
  return ` // Step ${ctx.stepNum}: Increment ${field} by ${amount}
181
- await this.prisma.${toVar(ctx.prismaModel)}.update({
182
- where: { id: params.id },
262
+ await prisma.${modelVar}.update({
263
+ where: { id: ${modelVar}.id },
183
264
  data: { ${field}: { increment: ${amountVal} } },
184
265
  });`;
185
266
  },
@@ -192,39 +273,19 @@ export const STEP_CONVENTIONS: StepConvention[] = [
192
273
  generateCall: (m, ctx) => {
193
274
  const field = m[1];
194
275
  const amount = m[2];
195
- const amountVal = /^\d+$/.test(amount) ? amount : `params.${amount}`;
276
+ const modelVar = toVar(ctx.prismaModel);
277
+ const amountVal = /^\d+$/.test(amount) ? amount : amount;
196
278
  return ` // Step ${ctx.stepNum}: Decrement ${field} by ${amount}
197
- const current${field} = await this.prisma.${toVar(ctx.prismaModel)}.findUniqueOrThrow({ where: { id: params.id } });
198
- if ((current${field}.${field} || 0) < ${amountVal}) {
199
- throw new Error('Cannot decrement ${field} below zero');
200
- }
201
- await this.prisma.${toVar(ctx.prismaModel)}.update({
202
- where: { id: params.id },
279
+ await prisma.${modelVar}.update({
280
+ where: { id: ${modelVar}.id },
203
281
  data: { ${field}: { decrement: ${amountVal} } },
204
282
  });`;
205
283
  },
206
284
  },
207
285
 
208
- // --- Calculate ---
209
- {
210
- name: 'calculate',
211
- pattern: /^calculate\s+(.+)/i,
212
- generateCall: (m, ctx) => {
213
- const metric = m[1];
214
- const methodName = toMethod('calculate ' + metric);
215
- return ` // Step ${ctx.stepNum}: Calculate ${metric}
216
- const ${toVar(metric.replace(/\s+/g, ''))} = await this.${methodName}(params);`;
217
- },
218
- generateMethod: (m, ctx) => {
219
- const metric = m[1];
220
- const methodName = toMethod('calculate ' + metric);
221
- return `
222
- private async ${methodName}(params: any): Promise<number> {
223
- // TODO: Implement calculation — ${metric}
224
- return 0;
225
- }`;
226
- },
227
- },
286
+ // NOTE: "calculate X" is intentionally NOT a convention — it falls through to AI
287
+ // behaviors because calculations are domain-specific pure functions that benefit
288
+ // from AI generation with the full input context.
228
289
 
229
290
  // --- Send event ---
230
291
  {
@@ -233,7 +294,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
233
294
  generateCall: (m, ctx) => {
234
295
  const event = m[1];
235
296
  return ` // Step ${ctx.stepNum}: Emit ${event} event
236
- this.emit('${event}', { ${toVar(ctx.prismaModel)}Id: params.id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() });`;
297
+ await eventBus.publish('${event}', { ${toVar(ctx.prismaModel)}Id: ${toVar(ctx.prismaModel)}.id, operation: '${ctx.operationName}', timestamp: new Date().toISOString() });`;
237
298
  },
238
299
  },
239
300
 
@@ -244,7 +305,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
244
305
  generateCall: (m, ctx) => {
245
306
  const type = m[1];
246
307
  return ` // Step ${ctx.stepNum}: Send ${type} notification
247
- this.emit('${type}Notification', { ${toVar(ctx.prismaModel)}Id: params.id, operation: '${ctx.operationName}' });`;
308
+ await eventBus.publish('${type}Notification', { ${toVar(ctx.prismaModel)}Id: ${toVar(ctx.prismaModel)}.id, operation: '${ctx.operationName}' });`;
248
309
  },
249
310
  },
250
311
 
@@ -256,7 +317,7 @@ export const STEP_CONVENTIONS: StepConvention[] = [
256
317
  const service = m[1];
257
318
  const method = m[2];
258
319
  return ` // Step ${ctx.stepNum}: Call ${service}.${method}
259
- await this.${toVar(service)}.${method}(params);`;
320
+ await ${toVar(service)}.${method}({ ${(ctx.parameterNames || []).join(', ')} });`;
260
321
  },
261
322
  },
262
323
 
@@ -280,30 +341,53 @@ export const STEP_CONVENTIONS: StepConvention[] = [
280
341
  /**
281
342
  * Match a step string against all conventions. Returns the generated call
282
343
  * code and optionally a helper method to add to the class.
344
+ *
345
+ * For unmatched steps, the call is wired to a pure AI function that takes
346
+ * all currently-declared variables as a typed input object and returns
347
+ * a result that subsequent steps can use.
283
348
  */
284
349
  export function matchStep(
285
350
  step: string,
286
351
  ctx: StepContext
287
- ): { call: string; helperMethod?: string } {
352
+ ): {
353
+ call: string;
354
+ helperMethod?: string;
355
+ matched: boolean;
356
+ functionName?: string;
357
+ inputs?: string[];
358
+ resultVar?: string;
359
+ } {
288
360
  for (const convention of STEP_CONVENTIONS) {
289
361
  const match = step.match(convention.pattern);
290
362
  if (match) {
291
363
  return {
292
364
  call: convention.generateCall(match, ctx),
293
365
  helperMethod: convention.generateMethod?.(match, ctx),
366
+ matched: true,
294
367
  };
295
368
  }
296
369
  }
297
370
 
298
- // No match — generate stub method
299
- const methodName = toMethod(step);
371
+ // No match — pure AI function call
372
+ // Inputs = all variables declared by previous steps + operation parameters
373
+ const functionName = toMethod(step);
374
+ const declared = Array.from(ctx.declaredVars || []);
375
+ const paramNames = ctx.parameterNames || [];
376
+ const inputs = [...paramNames, ...declared];
377
+
378
+ // Use named result from spec (`as:`) or default to stepNResult
379
+ const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
380
+ const inputObj = inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}';
381
+
382
+ // Register the result variable so subsequent steps can reference it
383
+ if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
384
+
300
385
  return {
301
- call: ` // Step ${ctx.stepNum}: ${step}
302
- await this.${methodName}(params);`,
303
- helperMethod: `
304
- private async ${methodName}(params: any): Promise<void> {
305
- // TODO: Implement — ${step}
306
- throw new Error('Not implemented: ${methodName}');
307
- }`,
386
+ call: ` // Step ${ctx.stepNum}: ${step} [AI-generated — pure function]
387
+ const ${resultVar} = await aiBehaviors.${functionName}(${inputObj});`,
388
+ matched: false,
389
+ functionName,
390
+ inputs,
391
+ resultVar,
308
392
  };
309
393
  }
@@ -37,27 +37,67 @@ export default function generateReactComponent(context: TemplateContext): string
37
37
  const lifecycle = getLifecycle(model);
38
38
  const classified = classifyAttrs(attrs, lifecycle);
39
39
 
40
+ let body: string;
40
41
  switch (viewType) {
41
42
  case 'list':
42
- return generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
43
+ body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
44
+ break;
43
45
  case 'detail':
44
- return generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
46
+ body = generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
47
+ break;
45
48
  case 'form':
46
- return generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
49
+ body = generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
50
+ break;
47
51
  case 'dashboard':
48
- return generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
52
+ body = generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
53
+ break;
49
54
  case 'board':
50
55
  case 'workflow':
51
- return generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
56
+ body = generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
57
+ break;
52
58
  case 'timeline':
53
- return generateTimelineView(componentName, modelName, lower, plural, api, view);
59
+ body = generateTimelineView(componentName, modelName, lower, plural, api, view);
60
+ break;
54
61
  case 'calendar':
55
- return generateCalendarView(componentName, modelName, lower, plural, api, view, model);
62
+ body = generateCalendarView(componentName, modelName, lower, plural, api, view, model);
63
+ break;
56
64
  case 'analytics':
57
- return generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
65
+ body = generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
66
+ break;
58
67
  default:
59
- return generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
68
+ body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
60
69
  }
70
+ return stripUnusedImports(body);
71
+ }
72
+
73
+ /**
74
+ * Rewrite the top-of-file named imports to drop symbols the body doesn't
75
+ * reference. The view generators import every helper unconditionally; this
76
+ * pass keeps them clean under tsc's `noUnusedLocals`.
77
+ */
78
+ function stripUnusedImports(source: string): string {
79
+ // Process only the leading contiguous run of named imports.
80
+ const lines = source.split('\n');
81
+ const out: string[] = [];
82
+ let i = 0;
83
+ // Find the end of the import block.
84
+ while (i < lines.length) {
85
+ const line = lines[i];
86
+ const match = line.match(/^import\s+\{\s*([^}]+?)\s*\}\s+from\s+(['"][^'"]+['"]);?\s*$/);
87
+ if (!match) break;
88
+ const names = match[1].split(',').map(s => s.trim()).filter(Boolean);
89
+ const from = match[2];
90
+ // Join all following lines as the usage body so we can scan for references.
91
+ const rest = lines.slice(i + 1).join('\n');
92
+ const used = names.filter(n => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(rest));
93
+ if (used.length === 0) {
94
+ // drop the import entirely
95
+ } else {
96
+ out.push(`import { ${used.join(', ')} } from ${from};`);
97
+ }
98
+ i++;
99
+ }
100
+ return [...out, ...lines.slice(i)].join('\n');
61
101
  }
62
102
 
63
103
  // ============================================================================
@@ -427,7 +467,7 @@ ${lifecycle.states.map(s => ` <option value="${s}">${s.replace(/[_-]/g,
427
467
  }
428
468
 
429
469
  const t = type.toLowerCase();
430
- if (t === 'boolean') return ` <input type="checkbox" name="${n}" checked={!!form.${n}} onChange={e => setForm(f => ({...f, ${n}: e.target.checked}))} className="h-4 w-4 text-blue-600 rounded" />`;
470
+ if (t === 'boolean') return ` <input type="checkbox" name="${n}" checked={!!form.${n}} onChange={e => setForm((f: any) => ({...f, ${n}: e.target.checked}))} className="h-4 w-4 text-blue-600 rounded" />`;
431
471
  if (t.includes('date') || t.includes('timestamp')) return ` <input type="datetime-local" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
432
472
  if (t === 'integer' || t === 'number' || t === 'money' || t === 'decimal' || t === 'float') return ` <input type="number" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
433
473
  if (n.toLowerCase().includes('description') || n.toLowerCase().includes('content') || n.toLowerCase().includes('body')) return ` <textarea name="${n}" value={form.${n} || ''} onChange={handleChange} rows={4} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "4.1.13",
3
+ "version": "4.1.15",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",