@specverse/engines 4.1.13 → 4.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/prompts/core/standard/v9/behavior.prompt.yaml +126 -0
- package/dist/ai/behavior-ai-service.d.ts +65 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -0
- package/dist/ai/behavior-ai-service.js +205 -0
- package/dist/ai/behavior-ai-service.js.map +1 -0
- package/dist/ai/index.d.ts +27 -0
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +30 -0
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/prompt-loader.js +2 -2
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +204 -4
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +97 -22
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +31 -31
- package/dist/libs/instance-factories/communication/templates/eventemitter/types-generator.js +79 -0
- package/dist/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.js +96 -0
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +45 -9
- package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +20 -2
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +249 -0
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +72 -45
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +61 -54
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +101 -84
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +192 -23
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +115 -22
- package/libs/instance-factories/communication/event-emitter.yaml +16 -12
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +33 -36
- package/libs/instance-factories/communication/templates/eventemitter/types-generator.ts +95 -0
- package/libs/instance-factories/communication/templates/eventemitter/websocket-bridge-generator.ts +105 -0
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +57 -11
- package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +23 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +376 -0
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +116 -45
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +83 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +169 -85
- package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
- package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
|
@@ -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 {
|
|
32
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
|
63
|
-
if (
|
|
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 ${
|
|
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+
|
|
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 ${
|
|
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
|
|
103
|
-
const
|
|
104
|
-
const val =
|
|
105
|
-
return ` // Step ${ctx.stepNum}: Update ${model} ${field} to ${
|
|
106
|
-
await
|
|
107
|
-
where: { 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
|
|
121
|
-
where: { 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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
162
|
-
const
|
|
163
|
-
const val =
|
|
164
|
-
return ` // Step ${ctx.stepNum}: Set ${field} to ${
|
|
165
|
-
await
|
|
166
|
-
where: { 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
|
|
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
|
|
182
|
-
where: { 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
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
): {
|
|
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 —
|
|
299
|
-
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
43
|
+
body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
|
|
44
|
+
break;
|
|
43
45
|
case 'detail':
|
|
44
|
-
|
|
46
|
+
body = generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
|
|
47
|
+
break;
|
|
45
48
|
case 'form':
|
|
46
|
-
|
|
49
|
+
body = generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
|
|
50
|
+
break;
|
|
47
51
|
case 'dashboard':
|
|
48
|
-
|
|
52
|
+
body = generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
|
|
53
|
+
break;
|
|
49
54
|
case 'board':
|
|
50
55
|
case 'workflow':
|
|
51
|
-
|
|
56
|
+
body = generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
|
|
57
|
+
break;
|
|
52
58
|
case 'timeline':
|
|
53
|
-
|
|
59
|
+
body = generateTimelineView(componentName, modelName, lower, plural, api, view);
|
|
60
|
+
break;
|
|
54
61
|
case 'calendar':
|
|
55
|
-
|
|
62
|
+
body = generateCalendarView(componentName, modelName, lower, plural, api, view, model);
|
|
63
|
+
break;
|
|
56
64
|
case 'analytics':
|
|
57
|
-
|
|
65
|
+
body = generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
|
|
66
|
+
break;
|
|
58
67
|
default:
|
|
59
|
-
|
|
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} />`;
|