@specverse/engines 4.1.14 → 4.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/prompts/core/standard/v9/behavior.prompt.yaml +7 -1
- package/dist/ai/behavior-ai-service.d.ts +2 -0
- package/dist/ai/behavior-ai-service.d.ts.map +1 -1
- package/dist/ai/behavior-ai-service.js +2 -0
- package/dist/ai/behavior-ai-service.js.map +1 -1
- package/dist/ai/prompt-loader.js +2 -2
- package/dist/inference/index.d.ts +2 -1
- package/dist/inference/index.d.ts.map +1 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/index.js.map +1 -1
- package/dist/inference/quint-transpiler.d.ts +18 -1
- package/dist/inference/quint-transpiler.d.ts.map +1 -1
- package/dist/inference/quint-transpiler.js +501 -21
- package/dist/inference/quint-transpiler.js.map +1 -1
- package/dist/inference/verification.d.ts +78 -0
- package/dist/inference/verification.d.ts.map +1 -0
- package/dist/inference/verification.js +263 -0
- package/dist/inference/verification.js.map +1 -0
- package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
- package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
- package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +111 -27
- package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
- package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
- package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +123 -25
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
- package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
- package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +134 -27
- package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
- package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
- package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
- package/package.json +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
- package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
- package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
|
@@ -13,6 +13,7 @@ function generatePrismaController(context) {
|
|
|
13
13
|
const rawModelVar = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
14
14
|
const RESERVED_WORDS = /* @__PURE__ */ 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"]);
|
|
15
15
|
const modelVar = RESERVED_WORDS.has(rawModelVar) ? `${rawModelVar}Item` : rawModelVar;
|
|
16
|
+
const prismaDelegate = RESERVED_WORDS.has(rawModelVar) ? `prisma['${rawModelVar}']` : `prisma.${rawModelVar}`;
|
|
16
17
|
const curedOps = controller.cured || {};
|
|
17
18
|
const idAttr = (Array.isArray(model.attributes) ? model.attributes : Object.values(model.attributes || {})).find((a) => a.name === "id");
|
|
18
19
|
const idType = idAttr?.type || "UUID";
|
|
@@ -40,11 +41,11 @@ function parseId(id: string): ${needsIntParse ? "number" : "string"} {
|
|
|
40
41
|
*/
|
|
41
42
|
export class ${controllerName} {
|
|
42
43
|
${generateValidateMethod(model, modelName)}
|
|
43
|
-
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, controller, allModels) : ""}
|
|
44
|
-
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar) : ""}
|
|
45
|
-
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, controller, allModels) : ""}
|
|
46
|
-
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, controller) : ""}
|
|
47
|
-
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, controller) : ""}
|
|
44
|
+
${curedOps.create ? generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ""}
|
|
45
|
+
${curedOps.retrieve ? generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) : ""}
|
|
46
|
+
${curedOps.update ? generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) : ""}
|
|
47
|
+
${curedOps.evolve ? generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) : ""}
|
|
48
|
+
${curedOps.delete ? generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) : ""}
|
|
48
49
|
${customActions.code}
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -116,9 +117,7 @@ function generateValidationLogic(model, dataParam = "_data", contextParam = "_co
|
|
|
116
117
|
});
|
|
117
118
|
return validations.join("\n") || "// No validation rules defined";
|
|
118
119
|
}
|
|
119
|
-
function generateCreateMethod(model, modelName, modelVar, controller, allModels) {
|
|
120
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
121
|
-
const createEvent = hasEvents ? controller.publishes.find((e) => e.includes("Created")) : null;
|
|
120
|
+
function generateCreateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
|
|
122
121
|
return `
|
|
123
122
|
/**
|
|
124
123
|
* Create a new ${modelName}
|
|
@@ -135,29 +134,24 @@ function generateCreateMethod(model, modelName, modelVar, controller, allModels)
|
|
|
135
134
|
${generateFKTransform(model, "prismaData", allModels)}
|
|
136
135
|
|
|
137
136
|
// Create record
|
|
138
|
-
const ${modelVar} = await
|
|
137
|
+
const ${modelVar} = await ${prismaDelegate}.create({
|
|
139
138
|
data: prismaData${generateIncludeRelationships(model)}
|
|
140
139
|
});
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
eventBus.publish(EventName.${createEvent}, {
|
|
145
|
-
${modelVar},
|
|
146
|
-
timestamp: new Date().toISOString()
|
|
147
|
-
});
|
|
148
|
-
` : ""}
|
|
141
|
+
// Publish CURED event
|
|
142
|
+
await eventBus.publish('${modelName}Created', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
149
143
|
|
|
150
144
|
return ${modelVar};
|
|
151
145
|
}
|
|
152
146
|
`;
|
|
153
147
|
}
|
|
154
|
-
function generateRetrieveMethod(model, modelName, modelVar) {
|
|
148
|
+
function generateRetrieveMethod(model, modelName, modelVar, prismaDelegate) {
|
|
155
149
|
return `
|
|
156
150
|
/**
|
|
157
151
|
* Retrieve ${modelName} by ID
|
|
158
152
|
*/
|
|
159
153
|
public async retrieve(id: string): Promise<any> {
|
|
160
|
-
const ${modelVar} = await
|
|
154
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({
|
|
161
155
|
where: { id: parseId(id) }${generateIncludeRelationships(model)}
|
|
162
156
|
});
|
|
163
157
|
|
|
@@ -172,16 +166,14 @@ function generateRetrieveMethod(model, modelName, modelVar) {
|
|
|
172
166
|
* Retrieve all ${modelName}s
|
|
173
167
|
*/
|
|
174
168
|
public async retrieveAll(options: { skip?: number; take?: number } = {}): Promise<any[]> {
|
|
175
|
-
return await
|
|
169
|
+
return await ${prismaDelegate}.findMany({
|
|
176
170
|
skip: options.skip,
|
|
177
171
|
take: options.take${generateIncludeRelationships(model)}
|
|
178
172
|
});
|
|
179
173
|
}
|
|
180
174
|
`;
|
|
181
175
|
}
|
|
182
|
-
function generateUpdateMethod(model, modelName, modelVar, controller, allModels) {
|
|
183
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
184
|
-
const updateEvent = hasEvents ? controller.publishes.find((e) => e.includes("Updated")) : null;
|
|
176
|
+
function generateUpdateMethod(model, modelName, modelVar, prismaDelegate, controller, allModels) {
|
|
185
177
|
return `
|
|
186
178
|
/**
|
|
187
179
|
* Update ${modelName}
|
|
@@ -206,24 +198,19 @@ function generateUpdateMethod(model, modelName, modelVar, controller, allModels)
|
|
|
206
198
|
${generateFKTransform(model, "updateData", allModels)}
|
|
207
199
|
|
|
208
200
|
// Update record
|
|
209
|
-
const ${modelVar} = await
|
|
201
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
210
202
|
where: { id: parseId(id) },
|
|
211
203
|
data: updateData${generateIncludeRelationships(model)}
|
|
212
204
|
});
|
|
213
205
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
eventBus.publish(EventName.${updateEvent}, {
|
|
217
|
-
${modelVar},
|
|
218
|
-
timestamp: new Date().toISOString()
|
|
219
|
-
});
|
|
220
|
-
` : ""}
|
|
206
|
+
// Publish CURED event
|
|
207
|
+
await eventBus.publish('${modelName}Updated', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
221
208
|
|
|
222
209
|
return ${modelVar};
|
|
223
210
|
}
|
|
224
211
|
`;
|
|
225
212
|
}
|
|
226
|
-
function generateEvolveMethod(model, modelName, modelVar, controller) {
|
|
213
|
+
function generateEvolveMethod(model, modelName, modelVar, prismaDelegate, controller) {
|
|
227
214
|
const lifecycles = Array.isArray(model.lifecycles) ? model.lifecycles : model.lifecycles ? Object.entries(model.lifecycles).map(([name, lc]) => ({ name, ...lc })) : [];
|
|
228
215
|
const lifecycle = lifecycles[0];
|
|
229
216
|
const lifecycleName = lifecycle?.name || "status";
|
|
@@ -242,7 +229,7 @@ function generateEvolveMethod(model, modelName, modelVar, controller) {
|
|
|
242
229
|
}
|
|
243
230
|
|
|
244
231
|
// Get current record to check lifecycle state
|
|
245
|
-
const current = await
|
|
232
|
+
const current = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
246
233
|
if (!current) {
|
|
247
234
|
throw new Error('${modelName} not found');
|
|
248
235
|
}
|
|
@@ -261,41 +248,35 @@ function generateEvolveMethod(model, modelName, modelVar, controller) {
|
|
|
261
248
|
` : ""}
|
|
262
249
|
|
|
263
250
|
// Update record
|
|
264
|
-
const ${modelVar} = await
|
|
251
|
+
const ${modelVar} = await ${prismaDelegate}.update({
|
|
265
252
|
where: { id: parseId(id) },
|
|
266
253
|
data${generateIncludeRelationships(model)}
|
|
267
254
|
});
|
|
268
255
|
|
|
256
|
+
// Publish CURED event
|
|
257
|
+
await eventBus.publish('${modelName}Evolved', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
258
|
+
|
|
269
259
|
return ${modelVar};
|
|
270
260
|
}
|
|
271
261
|
`;
|
|
272
262
|
}
|
|
273
|
-
function generateDeleteMethod(model, modelName, modelVar, controller) {
|
|
274
|
-
const hasEvents = controller.publishes && Array.isArray(controller.publishes);
|
|
275
|
-
const deleteEvent = hasEvents ? controller.publishes.find((e) => e.includes("Deleted")) : null;
|
|
263
|
+
function generateDeleteMethod(model, modelName, modelVar, prismaDelegate, controller) {
|
|
276
264
|
return `
|
|
277
265
|
/**
|
|
278
266
|
* Delete ${modelName}
|
|
279
267
|
*/
|
|
280
268
|
public async delete(id: string): Promise<void> {
|
|
281
|
-
${deleteEvent ? `
|
|
282
269
|
// Get record before deletion for event
|
|
283
|
-
const ${modelVar} = await
|
|
284
|
-
` : ""}
|
|
270
|
+
const ${modelVar} = await ${prismaDelegate}.findUnique({ where: { id: parseId(id) } });
|
|
285
271
|
|
|
286
|
-
await
|
|
272
|
+
await ${prismaDelegate}.delete({
|
|
287
273
|
where: { id: parseId(id) }
|
|
288
274
|
});
|
|
289
275
|
|
|
290
|
-
|
|
291
|
-
// Publish event
|
|
276
|
+
// Publish CURED event
|
|
292
277
|
if (${modelVar}) {
|
|
293
|
-
eventBus.publish(
|
|
294
|
-
${modelVar},
|
|
295
|
-
timestamp: new Date().toISOString()
|
|
296
|
-
});
|
|
278
|
+
await eventBus.publish('${modelName}Deleted', { ...${modelVar}, timestamp: new Date().toISOString() } as any);
|
|
297
279
|
}
|
|
298
|
-
` : ""}
|
|
299
280
|
}
|
|
300
281
|
`;
|
|
301
282
|
}
|
|
@@ -417,13 +398,7 @@ ${includes}
|
|
|
417
398
|
}`;
|
|
418
399
|
}
|
|
419
400
|
function hasEventPublishing(curedOps, controller) {
|
|
420
|
-
|
|
421
|
-
if (controller.actions) {
|
|
422
|
-
for (const action of Object.values(controller.actions)) {
|
|
423
|
-
if (action.publishes?.length > 0 || action.events?.length > 0 || action.sideEffects?.length > 0) return true;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return false;
|
|
401
|
+
return true;
|
|
427
402
|
}
|
|
428
403
|
export {
|
|
429
404
|
generatePrismaController as default
|
|
@@ -6,23 +6,27 @@ function generatePrismaService(context) {
|
|
|
6
6
|
}
|
|
7
7
|
const serviceName = service.name;
|
|
8
8
|
const hasEvents = service.publishes && service.publishes.length > 0 || service.subscribes && service.subscribes.length > 0;
|
|
9
|
+
const operationsCode = generateOperationsWithHelpers(service);
|
|
10
|
+
const hasAiBehaviors = /\baiBehaviors\./.test(operationsCode);
|
|
11
|
+
const usesPrisma = /\bprisma\b/.test(operationsCode);
|
|
9
12
|
return `/**
|
|
10
13
|
* ${serviceName}
|
|
11
14
|
* Abstract business logic service
|
|
12
15
|
* ${service.description || ""}
|
|
13
16
|
*/
|
|
14
|
-
|
|
15
|
-
import { PrismaClient } from '@prisma/client'
|
|
16
|
-
${hasEvents ? `import { eventBus
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
${usesPrisma ? `
|
|
18
|
+
import { PrismaClient } from '@prisma/client';` : ""}
|
|
19
|
+
${hasEvents ? `import { eventBus } from '../events/eventBus.js';` : ""}
|
|
20
|
+
${hasAiBehaviors ? `import * as aiBehaviors from '../behaviors/${serviceName}.ai.js';` : ""}
|
|
21
|
+
${usesPrisma ? `
|
|
22
|
+
const prisma = new PrismaClient();` : ""}
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* ${serviceName} class
|
|
22
26
|
*/
|
|
23
27
|
export class ${serviceName} {
|
|
24
28
|
${generateConstructor(service)}
|
|
25
|
-
${
|
|
29
|
+
${operationsCode}
|
|
26
30
|
${generateEventSubscriptions(service)}
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -97,8 +101,10 @@ function generateOperations(service) {
|
|
|
97
101
|
return operations.join("\n");
|
|
98
102
|
}
|
|
99
103
|
function generateOperation(operationName, operation, service) {
|
|
100
|
-
const
|
|
104
|
+
const rawParams = generateOperationParams(operation);
|
|
101
105
|
const hasPublish = service.publishes && service.publishes.length > 0;
|
|
106
|
+
const body = generateOperationLogic(operation, service);
|
|
107
|
+
const params = renameUnusedParams(rawParams, body);
|
|
102
108
|
return `
|
|
103
109
|
/**
|
|
104
110
|
* ${operationName}
|
|
@@ -106,7 +112,7 @@ function generateOperation(operationName, operation, service) {
|
|
|
106
112
|
*/
|
|
107
113
|
public async ${operationName}(${params}): Promise<any> {
|
|
108
114
|
try {
|
|
109
|
-
${
|
|
115
|
+
${body}
|
|
110
116
|
|
|
111
117
|
${hasPublish ? `
|
|
112
118
|
// Publish event (example)
|
|
@@ -125,6 +131,19 @@ function generateOperation(operationName, operation, service) {
|
|
|
125
131
|
}
|
|
126
132
|
`;
|
|
127
133
|
}
|
|
134
|
+
function renameUnusedParams(paramsString, body) {
|
|
135
|
+
if (!paramsString.trim()) return paramsString;
|
|
136
|
+
return paramsString.split(",").map((segment) => {
|
|
137
|
+
const trimmed = segment.trim();
|
|
138
|
+
const nameMatch = trimmed.match(/^(\w+)/);
|
|
139
|
+
if (!nameMatch) return segment;
|
|
140
|
+
const name = nameMatch[1];
|
|
141
|
+
if (name.startsWith("_")) return segment;
|
|
142
|
+
const re = new RegExp(`\\b${name}\\b`);
|
|
143
|
+
if (re.test(body)) return segment;
|
|
144
|
+
return segment.replace(new RegExp(`\\b${name}\\b`), `_${name}`);
|
|
145
|
+
}).join(", ");
|
|
146
|
+
}
|
|
128
147
|
function generateOperationParams(operation) {
|
|
129
148
|
if (!operation.parameters || Object.keys(operation.parameters).length === 0) {
|
|
130
149
|
return "params: any = {}";
|
|
@@ -149,17 +168,19 @@ function generateOperationLogic(operation, service) {
|
|
|
149
168
|
}
|
|
150
169
|
if (impl.preconditions?.length || impl.postconditions?.length || impl.steps?.length || impl.transactional) {
|
|
151
170
|
const modelName = inferModelFromServiceName(service.name);
|
|
171
|
+
const parameterNames = Object.keys(operation.parameters || {});
|
|
152
172
|
const context = {
|
|
153
173
|
modelName,
|
|
154
174
|
serviceName: service.name,
|
|
155
175
|
operationName: operation.name || "execute",
|
|
156
|
-
prismaModel: modelName
|
|
176
|
+
prismaModel: modelName,
|
|
177
|
+
parameterNames
|
|
157
178
|
};
|
|
158
179
|
const behavior = {
|
|
159
180
|
preconditions: impl.preconditions || [],
|
|
160
181
|
postconditions: impl.postconditions || [],
|
|
161
182
|
sideEffects: impl.sideEffects || [],
|
|
162
|
-
steps: impl.steps || [],
|
|
183
|
+
steps: impl.steps || operation.steps || [],
|
|
163
184
|
transactional: impl.transactional || false
|
|
164
185
|
};
|
|
165
186
|
const opMeta = {
|
|
@@ -263,7 +263,7 @@ function matchStep(step, ctx) {
|
|
|
263
263
|
const declared = Array.from(ctx.declaredVars || []);
|
|
264
264
|
const paramNames = ctx.parameterNames || [];
|
|
265
265
|
const inputs = [...paramNames, ...declared];
|
|
266
|
-
const resultVar = `step${ctx.stepNum}Result`;
|
|
266
|
+
const resultVar = ctx.resultName || `step${ctx.stepNum}Result`;
|
|
267
267
|
const inputObj = inputs.length > 0 ? `{ ${inputs.join(", ")} }` : "{}";
|
|
268
268
|
if (ctx.declaredVars) ctx.declaredVars.add(resultVar);
|
|
269
269
|
return {
|
|
@@ -16,27 +16,57 @@ function generateReactComponent(context) {
|
|
|
16
16
|
const hasMany = getHasManyRelationships(model);
|
|
17
17
|
const lifecycle = getLifecycle(model);
|
|
18
18
|
const classified = classifyAttrs(attrs, lifecycle);
|
|
19
|
+
let body;
|
|
19
20
|
switch (viewType) {
|
|
20
21
|
case "list":
|
|
21
|
-
|
|
22
|
+
body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
|
|
23
|
+
break;
|
|
22
24
|
case "detail":
|
|
23
|
-
|
|
25
|
+
body = generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
|
|
26
|
+
break;
|
|
24
27
|
case "form":
|
|
25
|
-
|
|
28
|
+
body = generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
|
|
29
|
+
break;
|
|
26
30
|
case "dashboard":
|
|
27
|
-
|
|
31
|
+
body = generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
|
|
32
|
+
break;
|
|
28
33
|
case "board":
|
|
29
34
|
case "workflow":
|
|
30
|
-
|
|
35
|
+
body = generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
|
|
36
|
+
break;
|
|
31
37
|
case "timeline":
|
|
32
|
-
|
|
38
|
+
body = generateTimelineView(componentName, modelName, lower, plural, api, view);
|
|
39
|
+
break;
|
|
33
40
|
case "calendar":
|
|
34
|
-
|
|
41
|
+
body = generateCalendarView(componentName, modelName, lower, plural, api, view, model);
|
|
42
|
+
break;
|
|
35
43
|
case "analytics":
|
|
36
|
-
|
|
44
|
+
body = generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
|
|
45
|
+
break;
|
|
37
46
|
default:
|
|
38
|
-
|
|
47
|
+
body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
|
|
39
48
|
}
|
|
49
|
+
return stripUnusedImports(body);
|
|
50
|
+
}
|
|
51
|
+
function stripUnusedImports(source) {
|
|
52
|
+
const lines = source.split("\n");
|
|
53
|
+
const out = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < lines.length) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
const match = line.match(/^import\s+\{\s*([^}]+?)\s*\}\s+from\s+(['"][^'"]+['"]);?\s*$/);
|
|
58
|
+
if (!match) break;
|
|
59
|
+
const names = match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
60
|
+
const from = match[2];
|
|
61
|
+
const rest = lines.slice(i + 1).join("\n");
|
|
62
|
+
const used = names.filter((n) => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(rest));
|
|
63
|
+
if (used.length === 0) {
|
|
64
|
+
} else {
|
|
65
|
+
out.push(`import { ${used.join(", ")} } from ${from};`);
|
|
66
|
+
}
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
return [...out, ...lines.slice(i)].join("\n");
|
|
40
70
|
}
|
|
41
71
|
function getModelAttributes(model) {
|
|
42
72
|
if (!model?.attributes) return [];
|
|
@@ -351,7 +381,7 @@ ${lifecycle.states.map((s) => ` <option value="${s}">${s.replace(/[_-]/
|
|
|
351
381
|
</select>`;
|
|
352
382
|
}
|
|
353
383
|
const t = type.toLowerCase();
|
|
354
|
-
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" />`;
|
|
384
|
+
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" />`;
|
|
355
385
|
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} />`;
|
|
356
386
|
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} />`;
|
|
357
387
|
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} />`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AASnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realize/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,uBAAuB,CAAC;AAGtC,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAMlE,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AASnF,cAAM,sBAAuB,YAAW,aAAa;IACnD,IAAI,SAAa;IACjB,OAAO,SAAW;IAClB,YAAY,WAA+E;IAE3F,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,WAAW,CAAS;IAEtB,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjB,OAAO,IAAI,UAAU;IAIrB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG;IAK1B,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC;IAKvF;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAskB9F;;;OAGG;YACW,UAAU;IAwDxB,OAAO,CAAC,oBAAoB;CAuB7B;AAED,eAAO,MAAM,MAAM,wBAA+B,CAAC;AACnD,eAAe,MAAM,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
|
package/dist/realize/index.js
CHANGED
|
@@ -68,7 +68,14 @@ class SpecVerseRealizeEngine {
|
|
|
68
68
|
const files = [];
|
|
69
69
|
const errors = [];
|
|
70
70
|
const allModels = Object.values(spec.models || {});
|
|
71
|
+
// L3 verification (Quint guards) is NOT run here. It lives in
|
|
72
|
+
// `validate --verify` so users can opt into it explicitly and run
|
|
73
|
+
// it on any spec — raw or inferred — without also generating code.
|
|
74
|
+
// See engines/src/inference/verification.ts.
|
|
71
75
|
const writeOutput = (output) => {
|
|
76
|
+
// A generator can signal "skip this file" by returning an empty string.
|
|
77
|
+
if (output.code === '')
|
|
78
|
+
return;
|
|
72
79
|
const dir = dirname(output.filePath);
|
|
73
80
|
if (!existsSync(dir))
|
|
74
81
|
mkdirSync(dir, { recursive: true });
|
|
@@ -203,17 +210,116 @@ class SpecVerseRealizeEngine {
|
|
|
203
210
|
// 3. Services
|
|
204
211
|
const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
|
|
205
212
|
if (ctrlResolved?.instanceFactory?.codeTemplates?.services) {
|
|
213
|
+
// Load helpers for pre-scanning unmatched steps + generating AI behavior files
|
|
214
|
+
let scanUnmatched = null;
|
|
215
|
+
let generateAiFile = null;
|
|
216
|
+
try {
|
|
217
|
+
const scanPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'step-conventions.ts');
|
|
218
|
+
const scanResolved = resolveGenPath(scanPath);
|
|
219
|
+
if (scanResolved) {
|
|
220
|
+
const mod = await import(scanResolved);
|
|
221
|
+
scanUnmatched = mod.matchStep;
|
|
222
|
+
}
|
|
223
|
+
const aiGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'ai-behaviors-generator.ts');
|
|
224
|
+
const aiResolved = resolveGenPath(aiGenPath);
|
|
225
|
+
if (aiResolved) {
|
|
226
|
+
const mod = await import(aiResolved);
|
|
227
|
+
generateAiFile = mod.generateAiBehaviorsFile;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { /* non-fatal */ }
|
|
231
|
+
/**
|
|
232
|
+
* Pre-scan a service's operations to determine which steps don't match
|
|
233
|
+
* conventions and will need AI-generated behavior functions. Runs the
|
|
234
|
+
* same matchStep logic the service-generator uses, but in this pipeline's
|
|
235
|
+
* module context so we don't need cross-module state.
|
|
236
|
+
*/
|
|
237
|
+
const scanServiceUnmatched = (service) => {
|
|
238
|
+
if (!scanUnmatched)
|
|
239
|
+
return [];
|
|
240
|
+
const operations = service.operations || {};
|
|
241
|
+
const entries = Array.isArray(operations)
|
|
242
|
+
? operations.map((op) => [op.name, op])
|
|
243
|
+
: Object.entries(operations);
|
|
244
|
+
const unmatched = [];
|
|
245
|
+
const seenFunctions = new Set();
|
|
246
|
+
for (const [opName, operation] of entries) {
|
|
247
|
+
const steps = operation.steps || operation.implementation?.steps || [];
|
|
248
|
+
const parameterNames = Object.keys(operation.parameters || {});
|
|
249
|
+
const declaredVars = new Set();
|
|
250
|
+
// Simulate precondition variable declarations
|
|
251
|
+
const preconditions = operation.requires || operation.preconditions || [];
|
|
252
|
+
for (const pc of preconditions) {
|
|
253
|
+
const m = typeof pc === 'string' ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
|
|
254
|
+
if (m)
|
|
255
|
+
declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
|
|
256
|
+
}
|
|
257
|
+
for (let i = 0; i < steps.length; i++) {
|
|
258
|
+
const stepInput = steps[i];
|
|
259
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
|
|
260
|
+
const stepAs = typeof stepInput === 'object' ? stepInput?.as : undefined;
|
|
261
|
+
const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
|
|
262
|
+
if (typeof stepText !== 'string')
|
|
263
|
+
continue;
|
|
264
|
+
const ctx = {
|
|
265
|
+
modelName: service.name.replace(/Service$/, ''),
|
|
266
|
+
prismaModel: service.name.replace(/Service$/, '').charAt(0).toLowerCase() + service.name.replace(/Service$/, '').slice(1),
|
|
267
|
+
serviceName: service.name,
|
|
268
|
+
operationName: opName,
|
|
269
|
+
stepNum: i + 1,
|
|
270
|
+
parameterNames,
|
|
271
|
+
declaredVars,
|
|
272
|
+
resultName: stepAs,
|
|
273
|
+
};
|
|
274
|
+
const result = scanUnmatched(stepText, ctx);
|
|
275
|
+
if (!result.matched && result.functionName && !seenFunctions.has(result.functionName)) {
|
|
276
|
+
seenFunctions.add(result.functionName);
|
|
277
|
+
unmatched.push({
|
|
278
|
+
step: stepText,
|
|
279
|
+
functionName: result.functionName,
|
|
280
|
+
operationName: opName,
|
|
281
|
+
parameterNames,
|
|
282
|
+
inputs: result.inputs || [],
|
|
283
|
+
returns: stepReturns,
|
|
284
|
+
modelName: service.name, // AI file uses service name as owner
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return unmatched;
|
|
290
|
+
};
|
|
291
|
+
let svcAiCount = 0;
|
|
206
292
|
for (const service of servicesList) {
|
|
207
293
|
try {
|
|
208
294
|
const svcName = service.name || 'Service';
|
|
209
295
|
const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'services', { spec, service: { name: svcName, ...service } }, { outputDir });
|
|
210
296
|
writeOutput(output);
|
|
297
|
+
// Pre-scan this service for unmatched steps and generate AI behaviors file if any
|
|
298
|
+
if (generateAiFile) {
|
|
299
|
+
const unmatched = scanServiceUnmatched(service);
|
|
300
|
+
if (unmatched.length > 0) {
|
|
301
|
+
const aiFile = await generateAiFile({
|
|
302
|
+
ownerName: svcName,
|
|
303
|
+
unmatchedFunctions: unmatched,
|
|
304
|
+
availableModels: allModels.map((m) => m.name),
|
|
305
|
+
spec,
|
|
306
|
+
});
|
|
307
|
+
if (aiFile) {
|
|
308
|
+
const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${svcName}.ai.ts`);
|
|
309
|
+
const dir = dirname(filePath);
|
|
310
|
+
if (!existsSync(dir))
|
|
311
|
+
mkdirSync(dir, { recursive: true });
|
|
312
|
+
writeFileSync(filePath, aiFile);
|
|
313
|
+
svcAiCount++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
211
317
|
}
|
|
212
318
|
catch (e) {
|
|
213
319
|
errors.push(`Service: ${e.message}`);
|
|
214
320
|
}
|
|
215
321
|
}
|
|
216
|
-
console.log(` ✅ Services: ${servicesList.length} service(s)`);
|
|
322
|
+
console.log(` ✅ Services: ${servicesList.length} service(s)${svcAiCount > 0 ? ` (${svcAiCount} AI behaviors file${svcAiCount > 1 ? 's' : ''})` : ''}`);
|
|
217
323
|
}
|
|
218
324
|
// 4. Routes (per model) — use spec controllers for endpoint data
|
|
219
325
|
const routeResolved = tryResolve('api.rest');
|
|
@@ -374,7 +480,16 @@ class SpecVerseRealizeEngine {
|
|
|
374
480
|
}
|
|
375
481
|
if (frontendFiles.length)
|
|
376
482
|
console.log(` ✅ Frontend application: ${frontendFiles.join(', ')}`);
|
|
377
|
-
// 9a. Generate shared view utilities and Tailwind config
|
|
483
|
+
// 9a. Generate shared view utilities and Tailwind config.
|
|
484
|
+
// The frontend factory may declare outputStructure=standalone with
|
|
485
|
+
// frontendDir="." — in that case views/lib live at the output root
|
|
486
|
+
// instead of under `frontend/`.
|
|
487
|
+
const frontendCfg = frontendResolved?.configuration || frontendResolved?.instanceFactory?.configuration || {};
|
|
488
|
+
const frontendOutputStructure = frontendCfg.outputStructure || 'monorepo';
|
|
489
|
+
const frontendRelDir = frontendOutputStructure === 'standalone'
|
|
490
|
+
? '.'
|
|
491
|
+
: (frontendCfg.frontendDir || 'frontend');
|
|
492
|
+
const frontendDir = frontendRelDir === '.' ? outputDir : join(outputDir, frontendRelDir);
|
|
378
493
|
try {
|
|
379
494
|
const sharedUtilsGen = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'views', 'templates', 'react', 'shared-utils-generator.ts');
|
|
380
495
|
const sharedUtilsResolved = resolveGenPath(sharedUtilsGen);
|
|
@@ -382,7 +497,7 @@ class SpecVerseRealizeEngine {
|
|
|
382
497
|
const genPath = sharedUtilsResolved;
|
|
383
498
|
const { default: generateSharedUtils } = await import(genPath);
|
|
384
499
|
const result = generateSharedUtils({ spec });
|
|
385
|
-
const libDir = join(
|
|
500
|
+
const libDir = join(frontendDir, 'src', 'lib');
|
|
386
501
|
if (!existsSync(libDir))
|
|
387
502
|
mkdirSync(libDir, { recursive: true });
|
|
388
503
|
for (const file of result.files) {
|
|
@@ -390,7 +505,6 @@ class SpecVerseRealizeEngine {
|
|
|
390
505
|
}
|
|
391
506
|
}
|
|
392
507
|
// Tailwind config
|
|
393
|
-
const frontendDir = join(outputDir, 'frontend');
|
|
394
508
|
writeFileSync(join(frontendDir, 'tailwind.config.js'), `import path from 'path';
|
|
395
509
|
import { createRequire } from 'module';
|
|
396
510
|
const require = createRequire(import.meta.url);
|
|
@@ -419,27 +533,11 @@ export default {
|
|
|
419
533
|
errors.push(`SharedUtils: ${e.message}`);
|
|
420
534
|
}
|
|
421
535
|
}
|
|
422
|
-
// 9b.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const entitiesPkg = req.resolve('@specverse/entities/package.json');
|
|
428
|
-
const entitiesSrc = join(dirname(entitiesPkg), 'src');
|
|
429
|
-
const guards = transpileEntityGuards(entitiesSrc);
|
|
430
|
-
if (guards.length > 0) {
|
|
431
|
-
const guardsCode = generateGuardsModule(guards);
|
|
432
|
-
const guardsPath = join(outputDir, 'backend', 'src', 'guards.ts');
|
|
433
|
-
const guardsDir = dirname(guardsPath);
|
|
434
|
-
if (!existsSync(guardsDir))
|
|
435
|
-
mkdirSync(guardsDir, { recursive: true });
|
|
436
|
-
writeFileSync(guardsPath, guardsCode);
|
|
437
|
-
console.log(` ✅ Runtime guards: ${guards.length} guards (${guards.filter((g) => g.kind === 'function').length} functions, ${guards.filter((g) => g.kind === 'invariant').length} invariants) from Quint specs`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
catch (e) {
|
|
441
|
-
errors.push(`Guards: ${e.message}`);
|
|
442
|
-
}
|
|
536
|
+
// 9b. Quint guards are no longer emitted to the generated backend.
|
|
537
|
+
// They are L3 verification assertions about the SPEC, not runtime
|
|
538
|
+
// checks on the user's data — so they run at realize time (see the
|
|
539
|
+
// L3 verification gate at the start of realizeAll) and don't ship
|
|
540
|
+
// to the user's project.
|
|
443
541
|
// 10. CLI commands (Commander.js)
|
|
444
542
|
try {
|
|
445
543
|
const cliDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'cli', 'templates', 'commander');
|