@specverse/engines 4.1.14 → 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 +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/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 +81 -22
- 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 +138 -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 +99 -22
- 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;IAklB9F;;;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
|
@@ -69,6 +69,9 @@ class SpecVerseRealizeEngine {
|
|
|
69
69
|
const errors = [];
|
|
70
70
|
const allModels = Object.values(spec.models || {});
|
|
71
71
|
const writeOutput = (output) => {
|
|
72
|
+
// A generator can signal "skip this file" by returning an empty string.
|
|
73
|
+
if (output.code === '')
|
|
74
|
+
return;
|
|
72
75
|
const dir = dirname(output.filePath);
|
|
73
76
|
if (!existsSync(dir))
|
|
74
77
|
mkdirSync(dir, { recursive: true });
|
|
@@ -203,17 +206,116 @@ class SpecVerseRealizeEngine {
|
|
|
203
206
|
// 3. Services
|
|
204
207
|
const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
|
|
205
208
|
if (ctrlResolved?.instanceFactory?.codeTemplates?.services) {
|
|
209
|
+
// Load helpers for pre-scanning unmatched steps + generating AI behavior files
|
|
210
|
+
let scanUnmatched = null;
|
|
211
|
+
let generateAiFile = null;
|
|
212
|
+
try {
|
|
213
|
+
const scanPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'step-conventions.ts');
|
|
214
|
+
const scanResolved = resolveGenPath(scanPath);
|
|
215
|
+
if (scanResolved) {
|
|
216
|
+
const mod = await import(scanResolved);
|
|
217
|
+
scanUnmatched = mod.matchStep;
|
|
218
|
+
}
|
|
219
|
+
const aiGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'ai-behaviors-generator.ts');
|
|
220
|
+
const aiResolved = resolveGenPath(aiGenPath);
|
|
221
|
+
if (aiResolved) {
|
|
222
|
+
const mod = await import(aiResolved);
|
|
223
|
+
generateAiFile = mod.generateAiBehaviorsFile;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { /* non-fatal */ }
|
|
227
|
+
/**
|
|
228
|
+
* Pre-scan a service's operations to determine which steps don't match
|
|
229
|
+
* conventions and will need AI-generated behavior functions. Runs the
|
|
230
|
+
* same matchStep logic the service-generator uses, but in this pipeline's
|
|
231
|
+
* module context so we don't need cross-module state.
|
|
232
|
+
*/
|
|
233
|
+
const scanServiceUnmatched = (service) => {
|
|
234
|
+
if (!scanUnmatched)
|
|
235
|
+
return [];
|
|
236
|
+
const operations = service.operations || {};
|
|
237
|
+
const entries = Array.isArray(operations)
|
|
238
|
+
? operations.map((op) => [op.name, op])
|
|
239
|
+
: Object.entries(operations);
|
|
240
|
+
const unmatched = [];
|
|
241
|
+
const seenFunctions = new Set();
|
|
242
|
+
for (const [opName, operation] of entries) {
|
|
243
|
+
const steps = operation.steps || operation.implementation?.steps || [];
|
|
244
|
+
const parameterNames = Object.keys(operation.parameters || {});
|
|
245
|
+
const declaredVars = new Set();
|
|
246
|
+
// Simulate precondition variable declarations
|
|
247
|
+
const preconditions = operation.requires || operation.preconditions || [];
|
|
248
|
+
for (const pc of preconditions) {
|
|
249
|
+
const m = typeof pc === 'string' ? pc.match(/^(\w+)\s+(?:exists|is\s+\w+)$/i) : null;
|
|
250
|
+
if (m)
|
|
251
|
+
declaredVars.add(m[1].charAt(0).toLowerCase() + m[1].slice(1));
|
|
252
|
+
}
|
|
253
|
+
for (let i = 0; i < steps.length; i++) {
|
|
254
|
+
const stepInput = steps[i];
|
|
255
|
+
const stepText = typeof stepInput === 'string' ? stepInput : stepInput?.step;
|
|
256
|
+
const stepAs = typeof stepInput === 'object' ? stepInput?.as : undefined;
|
|
257
|
+
const stepReturns = typeof stepInput === 'object' ? stepInput?.returns : undefined;
|
|
258
|
+
if (typeof stepText !== 'string')
|
|
259
|
+
continue;
|
|
260
|
+
const ctx = {
|
|
261
|
+
modelName: service.name.replace(/Service$/, ''),
|
|
262
|
+
prismaModel: service.name.replace(/Service$/, '').charAt(0).toLowerCase() + service.name.replace(/Service$/, '').slice(1),
|
|
263
|
+
serviceName: service.name,
|
|
264
|
+
operationName: opName,
|
|
265
|
+
stepNum: i + 1,
|
|
266
|
+
parameterNames,
|
|
267
|
+
declaredVars,
|
|
268
|
+
resultName: stepAs,
|
|
269
|
+
};
|
|
270
|
+
const result = scanUnmatched(stepText, ctx);
|
|
271
|
+
if (!result.matched && result.functionName && !seenFunctions.has(result.functionName)) {
|
|
272
|
+
seenFunctions.add(result.functionName);
|
|
273
|
+
unmatched.push({
|
|
274
|
+
step: stepText,
|
|
275
|
+
functionName: result.functionName,
|
|
276
|
+
operationName: opName,
|
|
277
|
+
parameterNames,
|
|
278
|
+
inputs: result.inputs || [],
|
|
279
|
+
returns: stepReturns,
|
|
280
|
+
modelName: service.name, // AI file uses service name as owner
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return unmatched;
|
|
286
|
+
};
|
|
287
|
+
let svcAiCount = 0;
|
|
206
288
|
for (const service of servicesList) {
|
|
207
289
|
try {
|
|
208
290
|
const svcName = service.name || 'Service';
|
|
209
291
|
const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'services', { spec, service: { name: svcName, ...service } }, { outputDir });
|
|
210
292
|
writeOutput(output);
|
|
293
|
+
// Pre-scan this service for unmatched steps and generate AI behaviors file if any
|
|
294
|
+
if (generateAiFile) {
|
|
295
|
+
const unmatched = scanServiceUnmatched(service);
|
|
296
|
+
if (unmatched.length > 0) {
|
|
297
|
+
const aiFile = await generateAiFile({
|
|
298
|
+
ownerName: svcName,
|
|
299
|
+
unmatchedFunctions: unmatched,
|
|
300
|
+
availableModels: allModels.map((m) => m.name),
|
|
301
|
+
spec,
|
|
302
|
+
});
|
|
303
|
+
if (aiFile) {
|
|
304
|
+
const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${svcName}.ai.ts`);
|
|
305
|
+
const dir = dirname(filePath);
|
|
306
|
+
if (!existsSync(dir))
|
|
307
|
+
mkdirSync(dir, { recursive: true });
|
|
308
|
+
writeFileSync(filePath, aiFile);
|
|
309
|
+
svcAiCount++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
211
313
|
}
|
|
212
314
|
catch (e) {
|
|
213
315
|
errors.push(`Service: ${e.message}`);
|
|
214
316
|
}
|
|
215
317
|
}
|
|
216
|
-
console.log(` ✅ Services: ${servicesList.length} service(s)`);
|
|
318
|
+
console.log(` ✅ Services: ${servicesList.length} service(s)${svcAiCount > 0 ? ` (${svcAiCount} AI behaviors file${svcAiCount > 1 ? 's' : ''})` : ''}`);
|
|
217
319
|
}
|
|
218
320
|
// 4. Routes (per model) — use spec controllers for endpoint data
|
|
219
321
|
const routeResolved = tryResolve('api.rest');
|
|
@@ -374,7 +476,16 @@ class SpecVerseRealizeEngine {
|
|
|
374
476
|
}
|
|
375
477
|
if (frontendFiles.length)
|
|
376
478
|
console.log(` ✅ Frontend application: ${frontendFiles.join(', ')}`);
|
|
377
|
-
// 9a. Generate shared view utilities and Tailwind config
|
|
479
|
+
// 9a. Generate shared view utilities and Tailwind config.
|
|
480
|
+
// The frontend factory may declare outputStructure=standalone with
|
|
481
|
+
// frontendDir="." — in that case views/lib live at the output root
|
|
482
|
+
// instead of under `frontend/`.
|
|
483
|
+
const frontendCfg = frontendResolved?.configuration || frontendResolved?.instanceFactory?.configuration || {};
|
|
484
|
+
const frontendOutputStructure = frontendCfg.outputStructure || 'monorepo';
|
|
485
|
+
const frontendRelDir = frontendOutputStructure === 'standalone'
|
|
486
|
+
? '.'
|
|
487
|
+
: (frontendCfg.frontendDir || 'frontend');
|
|
488
|
+
const frontendDir = frontendRelDir === '.' ? outputDir : join(outputDir, frontendRelDir);
|
|
378
489
|
try {
|
|
379
490
|
const sharedUtilsGen = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'views', 'templates', 'react', 'shared-utils-generator.ts');
|
|
380
491
|
const sharedUtilsResolved = resolveGenPath(sharedUtilsGen);
|
|
@@ -382,7 +493,7 @@ class SpecVerseRealizeEngine {
|
|
|
382
493
|
const genPath = sharedUtilsResolved;
|
|
383
494
|
const { default: generateSharedUtils } = await import(genPath);
|
|
384
495
|
const result = generateSharedUtils({ spec });
|
|
385
|
-
const libDir = join(
|
|
496
|
+
const libDir = join(frontendDir, 'src', 'lib');
|
|
386
497
|
if (!existsSync(libDir))
|
|
387
498
|
mkdirSync(libDir, { recursive: true });
|
|
388
499
|
for (const file of result.files) {
|
|
@@ -390,7 +501,6 @@ class SpecVerseRealizeEngine {
|
|
|
390
501
|
}
|
|
391
502
|
}
|
|
392
503
|
// Tailwind config
|
|
393
|
-
const frontendDir = join(outputDir, 'frontend');
|
|
394
504
|
writeFileSync(join(frontendDir, 'tailwind.config.js'), `import path from 'path';
|
|
395
505
|
import { createRequire } from 'module';
|
|
396
506
|
const require = createRequire(import.meta.url);
|
|
@@ -419,26 +529,31 @@ export default {
|
|
|
419
529
|
errors.push(`SharedUtils: ${e.message}`);
|
|
420
530
|
}
|
|
421
531
|
}
|
|
422
|
-
// 9b. Runtime guards — transpiled from Quint specifications
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
532
|
+
// 9b. Runtime guards — transpiled from Quint specifications.
|
|
533
|
+
// Only emit when the manifest declares a backend; a frontend-only
|
|
534
|
+
// layout has no `backend/src/` to write into.
|
|
535
|
+
const hasBackendForGuards = !!(tryResolve('service.controller') || tryResolve('api.rest') || tryResolve('orm.schema'));
|
|
536
|
+
if (hasBackendForGuards) {
|
|
537
|
+
try {
|
|
538
|
+
const { transpileEntityGuards, generateGuardsModule } = await import('../inference/index.js');
|
|
539
|
+
const { createRequire } = await import('module');
|
|
540
|
+
const req = createRequire(import.meta.url);
|
|
541
|
+
const entitiesPkg = req.resolve('@specverse/entities/package.json');
|
|
542
|
+
const entitiesSrc = join(dirname(entitiesPkg), 'src');
|
|
543
|
+
const guards = transpileEntityGuards(entitiesSrc);
|
|
544
|
+
if (guards.length > 0) {
|
|
545
|
+
const guardsCode = generateGuardsModule(guards);
|
|
546
|
+
const guardsPath = join(outputDir, 'backend', 'src', 'guards.ts');
|
|
547
|
+
const guardsDir = dirname(guardsPath);
|
|
548
|
+
if (!existsSync(guardsDir))
|
|
549
|
+
mkdirSync(guardsDir, { recursive: true });
|
|
550
|
+
writeFileSync(guardsPath, guardsCode);
|
|
551
|
+
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`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (e) {
|
|
555
|
+
errors.push(`Guards: ${e.message}`);
|
|
438
556
|
}
|
|
439
|
-
}
|
|
440
|
-
catch (e) {
|
|
441
|
-
errors.push(`Guards: ${e.message}`);
|
|
442
557
|
}
|
|
443
558
|
// 10. CLI commands (Commander.js)
|
|
444
559
|
try {
|