@specverse/engines 4.1.19 → 4.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/ai/behavior-regenerate.d.ts +24 -0
  2. package/dist/ai/behavior-regenerate.d.ts.map +1 -0
  3. package/dist/ai/behavior-regenerate.js +41 -0
  4. package/dist/ai/behavior-regenerate.js.map +1 -0
  5. package/dist/ai/index.d.ts +2 -0
  6. package/dist/ai/index.d.ts.map +1 -1
  7. package/dist/ai/index.js +3 -0
  8. package/dist/ai/index.js.map +1 -1
  9. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +1 -1
  10. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -1
  11. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +81 -0
  12. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +153 -3
  13. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +7 -1
  14. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +244 -0
  15. package/dist/realize/index.js.bak +758 -0
  16. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +1 -1
  17. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +1 -1
  18. package/libs/instance-factories/cli/templates/commander/command-generator.ts +81 -0
  19. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +213 -2
  20. package/libs/instance-factories/tools/templates/mcp/static/package.json +2 -2
  21. package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +7 -1
  22. package/package.json +1 -1
@@ -0,0 +1,758 @@
1
+ /**
2
+ * SpecVerse Code Realization
3
+ *
4
+ * Main entry point for code generation (realization) from SpecVerse specifications.
5
+ * Transforms minimal specs into production-ready implementations.
6
+ */
7
+ // Types (TemplateContext, etc.)
8
+ export * from './types/index.js';
9
+ // Utilities
10
+ export * from './utils/index.js';
11
+ // Generators
12
+ export * from './generators/index.js';
13
+ // Library
14
+ export { InstanceFactoryLibrary, createDefaultLibrary } from './library/library.js';
15
+ export { createResolver } from './library/resolver.js';
16
+ // Code generator
17
+ export { createCodeGenerator } from './engines/code-generator.js';
18
+ import { writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs';
19
+ import { dirname, join, basename } from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ import { createDefaultLibrary } from './library/library.js';
22
+ import { createResolver } from './library/resolver.js';
23
+ import { createCodeGenerator } from './engines/code-generator.js';
24
+ import { loadManifest } from './utils/manifest-loader.js';
25
+ class SpecVerseRealizeEngine {
26
+ name = 'realize';
27
+ version = '3.5.2';
28
+ capabilities = ['realize', 'code-generation', 'manifest-resolution', 'instance-factories'];
29
+ library = null;
30
+ resolver = null;
31
+ codeGenerator = null;
32
+ manifest = null;
33
+ initialized = false;
34
+ async initialize(config) {
35
+ const workingDir = config?.workingDir || process.cwd();
36
+ // Load instance factory library
37
+ this.library = await createDefaultLibrary(workingDir);
38
+ // Load and resolve manifest if provided
39
+ if (config?.manifestPath) {
40
+ this.manifest = loadManifest(config.manifestPath);
41
+ this.resolver = createResolver(this.library, this.manifest);
42
+ }
43
+ // Create code generator
44
+ this.codeGenerator = createCodeGenerator();
45
+ this.initialized = true;
46
+ }
47
+ getInfo() {
48
+ return { name: this.name, version: this.version, capabilities: this.capabilities };
49
+ }
50
+ resolve(capability) {
51
+ if (!this.resolver)
52
+ throw new Error('Realize engine not initialized with manifest.');
53
+ return this.resolver.resolveCapability(capability);
54
+ }
55
+ async generate(resolved, template, context) {
56
+ if (!this.codeGenerator)
57
+ throw new Error('Realize engine not initialized.');
58
+ return this.codeGenerator.generateFromTemplate(resolved, template, context);
59
+ }
60
+ /**
61
+ * Realize all code from an AI-optimized spec.
62
+ * This is the full pipeline equivalent of the hand-written CLI's `case 'all':`.
63
+ */
64
+ async realizeAll(spec, outputDir) {
65
+ if (!this.initialized || !this.resolver || !this.codeGenerator) {
66
+ throw new Error('Realize engine not initialized. Call initialize() with manifestPath.');
67
+ }
68
+ const files = [];
69
+ const errors = [];
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.
75
+ const writeOutput = (output) => {
76
+ // A generator can signal "skip this file" by returning an empty string.
77
+ if (output.code === '')
78
+ return;
79
+ const dir = dirname(output.filePath);
80
+ if (!existsSync(dir))
81
+ mkdirSync(dir, { recursive: true });
82
+ writeFileSync(output.filePath, output.code, 'utf-8');
83
+ files.push(basename(output.filePath));
84
+ };
85
+ // Prefer compiled .js in dist/libs/ over .ts in libs/ (Node v24+ can't strip types from node_modules)
86
+ const resolveGenPath = (tsPath) => {
87
+ // Try dist/libs/ compiled JS first
88
+ if (tsPath.includes('/libs/')) {
89
+ const distJsPath = tsPath.replace('/libs/', '/dist/libs/').replace(/\.tsx?$/, '.js');
90
+ if (existsSync(distJsPath))
91
+ return distJsPath;
92
+ }
93
+ // Fall back to .js alongside .ts
94
+ const jsPath = tsPath.replace(/\.tsx?$/, '.js');
95
+ if (existsSync(jsPath))
96
+ return jsPath;
97
+ // Fall back to .ts (works in dev, fails in node_modules on Node v24+)
98
+ if (existsSync(tsPath))
99
+ return tsPath;
100
+ return null;
101
+ };
102
+ const tryResolve = (capability) => {
103
+ try {
104
+ return this.resolver.resolveCapability(capability);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ };
110
+ // 1. ORM Schema
111
+ const ormResolved = tryResolve('orm.schema');
112
+ if (ormResolved?.instanceFactory?.codeTemplates?.schema) {
113
+ try {
114
+ const output = await this.codeGenerator.generateFromTemplate(ormResolved, 'schema', { spec, models: allModels }, { outputDir });
115
+ writeOutput(output);
116
+ console.log(` ✅ ORM schema: ${output.filePath}`);
117
+ }
118
+ catch (e) {
119
+ errors.push(`ORM: ${e.message}`);
120
+ }
121
+ }
122
+ // 1.5 Event infrastructure (types + bus + websocket bridge)
123
+ const eventResolved = tryResolve('messaging.events');
124
+ if (eventResolved?.instanceFactory?.codeTemplates) {
125
+ const eventFiles = [];
126
+ // Generate in dependency order: types first, then bus, then websocket
127
+ const orderedTemplates = ['types', 'bus', 'websocket'].filter(t => eventResolved.instanceFactory.codeTemplates[t]);
128
+ // Also generate any other templates (publisher, subscriber)
129
+ for (const t of Object.keys(eventResolved.instanceFactory.codeTemplates)) {
130
+ if (!orderedTemplates.includes(t))
131
+ orderedTemplates.push(t);
132
+ }
133
+ for (const templateName of orderedTemplates) {
134
+ try {
135
+ const output = await this.codeGenerator.generateFromTemplate(eventResolved, templateName, { spec, models: allModels, manifest: this.manifest }, { outputDir });
136
+ writeOutput(output);
137
+ eventFiles.push(basename(output.filePath));
138
+ }
139
+ catch (e) {
140
+ errors.push(`Event ${templateName}: ${e.message}`);
141
+ }
142
+ }
143
+ if (eventFiles.length)
144
+ console.log(` ✅ Events: ${eventFiles.join(', ')}`);
145
+ }
146
+ // 2. Controllers (per model) — use spec controllers if available, else generate from model
147
+ const ctrlResolved = tryResolve('service.controller');
148
+ if (ctrlResolved?.instanceFactory?.codeTemplates?.controllers) {
149
+ // Build controller lookup from spec
150
+ const specControllers = Array.isArray(spec.controllers)
151
+ ? spec.controllers
152
+ : Object.values(spec.controllers || {});
153
+ const controllerLookup = {};
154
+ for (const c of specControllers)
155
+ controllerLookup[c.name] = c;
156
+ for (const model of allModels) {
157
+ try {
158
+ const ctrlName = `${model.name}Controller`;
159
+ const specCtrl = controllerLookup[ctrlName];
160
+ const controller = specCtrl ? {
161
+ ...specCtrl,
162
+ model: specCtrl.modelReference || specCtrl.model || model.name,
163
+ modelReference: specCtrl.modelReference || specCtrl.model || model.name,
164
+ cured: specCtrl.cured || { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
165
+ } : {
166
+ name: ctrlName,
167
+ model: model.name,
168
+ modelReference: model.name,
169
+ cured: { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
170
+ };
171
+ const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'controllers', { spec, model, controller, models: allModels }, { outputDir });
172
+ writeOutput(output);
173
+ }
174
+ catch (e) {
175
+ errors.push(`Controller ${model.name}: ${e.message}`);
176
+ }
177
+ }
178
+ console.log(` ✅ Controllers: ${allModels.length} controller(s)`);
179
+ // 2.5 AI Behaviors — generate .ai.ts files for unmatched steps
180
+ try {
181
+ const aiBehaviorsGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'ai-behaviors-generator.ts');
182
+ const aiBehaviorsGen = resolveGenPath(aiBehaviorsGenPath);
183
+ if (aiBehaviorsGen) {
184
+ const { default: generateAiBehaviors } = await import(aiBehaviorsGen);
185
+ let aiBehaviorCount = 0;
186
+ for (const model of allModels) {
187
+ const ctrlName = `${model.name}Controller`;
188
+ const specCtrl = controllerLookup[ctrlName];
189
+ if (specCtrl?.actions && Object.keys(specCtrl.actions).length > 0) {
190
+ const code = await generateAiBehaviors({ spec, model, controller: specCtrl, models: allModels });
191
+ if (code) {
192
+ const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${ctrlName}.ai.ts`);
193
+ const dir = dirname(filePath);
194
+ if (!existsSync(dir))
195
+ mkdirSync(dir, { recursive: true });
196
+ writeFileSync(filePath, code);
197
+ aiBehaviorCount++;
198
+ }
199
+ }
200
+ }
201
+ if (aiBehaviorCount > 0) {
202
+ console.log(` ✅ AI Behaviors: ${aiBehaviorCount} file(s) — review in behaviors/*.ai.ts`);
203
+ }
204
+ }
205
+ }
206
+ catch (e) {
207
+ errors.push(`AI Behaviors: ${e.message}`);
208
+ }
209
+ }
210
+ // 3. Services
211
+ const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
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;
292
+ for (const service of servicesList) {
293
+ try {
294
+ const svcName = service.name || 'Service';
295
+ const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'services', { spec, service: { name: svcName, ...service } }, { outputDir });
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
+ }
317
+ }
318
+ catch (e) {
319
+ errors.push(`Service: ${e.message}`);
320
+ }
321
+ }
322
+ console.log(` ✅ Services: ${servicesList.length} service(s)${svcAiCount > 0 ? ` (${svcAiCount} AI behaviors file${svcAiCount > 1 ? 's' : ''})` : ''}`);
323
+ }
324
+ // 4. Routes (per model) — use spec controllers for endpoint data
325
+ const routeResolved = tryResolve('api.rest');
326
+ if (routeResolved?.instanceFactory?.codeTemplates?.routes) {
327
+ const specControllers2 = Array.isArray(spec.controllers)
328
+ ? spec.controllers
329
+ : Object.values(spec.controllers || {});
330
+ const ctrlLookup2 = {};
331
+ for (const c of specControllers2)
332
+ ctrlLookup2[c.name] = c;
333
+ for (const model of allModels) {
334
+ try {
335
+ const ctrlName = `${model.name}Controller`;
336
+ const specCtrl = ctrlLookup2[ctrlName];
337
+ const controller = specCtrl || {
338
+ name: ctrlName,
339
+ model: model.name,
340
+ modelReference: model.name,
341
+ cured: { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
342
+ };
343
+ const output = await this.codeGenerator.generateFromTemplate(routeResolved, 'routes', { spec, model, controller, models: allModels }, { outputDir });
344
+ writeOutput(output);
345
+ }
346
+ catch (e) {
347
+ errors.push(`Route ${model.name}: ${e.message}`);
348
+ }
349
+ }
350
+ console.log(` ✅ Routes: ${allModels.length} route handler(s)`);
351
+ }
352
+ // 4.5 Service routes — HTTP endpoints for service operations
353
+ // POST /api/services/{ServiceName}/{operationName}
354
+ // Mirrors app-demo's dynamic runtime URL shape (see
355
+ // specverse-app-demo/src/api/http-server.ts:611) so smoke-parity
356
+ // tests can hit both backends identically.
357
+ if (routeResolved?.instanceFactory?.codeTemplates?.serviceRoutes && servicesList.length > 0) {
358
+ let svcRouteCount = 0;
359
+ for (const service of servicesList) {
360
+ try {
361
+ const svcName = service.name || 'Service';
362
+ const output = await this.codeGenerator.generateFromTemplate(routeResolved, 'serviceRoutes', { spec, service: { name: svcName, ...service }, models: allModels }, { outputDir });
363
+ writeOutput(output);
364
+ svcRouteCount++;
365
+ }
366
+ catch (e) {
367
+ errors.push(`Service route ${service?.name}: ${e.message}`);
368
+ }
369
+ }
370
+ if (svcRouteCount > 0)
371
+ console.log(` ✅ Service routes: ${svcRouteCount} file(s)`);
372
+ }
373
+ // 5. Views, Forms, Hooks
374
+ const viewsResolved = tryResolve('ui.components');
375
+ if (viewsResolved && spec.views) {
376
+ const views = Array.isArray(spec.views) ? spec.views : Object.values(spec.views);
377
+ let viewCount = 0, formCount = 0, hookCount = 0;
378
+ for (const viewData of views) {
379
+ const modelRef = viewData.primaryModel || viewData.model;
380
+ const model = modelRef ? allModels.find((m) => m.name === modelRef) : undefined;
381
+ // View component
382
+ if (viewsResolved.instanceFactory.codeTemplates?.components) {
383
+ try {
384
+ const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'components', { spec, view: viewData, model, models: allModels }, { outputDir });
385
+ writeOutput(output);
386
+ viewCount++;
387
+ }
388
+ catch (e) {
389
+ errors.push(`View ${viewData?.name || 'unknown'}: ${e.message}`);
390
+ }
391
+ }
392
+ // Form
393
+ if (model && viewsResolved.instanceFactory.codeTemplates?.forms) {
394
+ try {
395
+ const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'forms', { spec, model, view: viewData, models: allModels }, { outputDir });
396
+ writeOutput(output);
397
+ formCount++;
398
+ }
399
+ catch (e) {
400
+ errors.push(`Form ${model?.name || 'unknown'}: ${e.message}`);
401
+ }
402
+ }
403
+ // Hook
404
+ if (model && viewsResolved.instanceFactory.codeTemplates?.hooks) {
405
+ try {
406
+ const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'hooks', { spec, model, view: viewData, models: allModels }, { outputDir });
407
+ writeOutput(output);
408
+ hookCount++;
409
+ }
410
+ catch (e) {
411
+ errors.push(`Hook ${model?.name || 'unknown'}: ${e.message}`);
412
+ }
413
+ }
414
+ }
415
+ if (viewCount)
416
+ console.log(` ✅ Views: ${viewCount} component(s)`);
417
+ if (formCount)
418
+ console.log(` ✅ Forms: ${formCount} form(s)`);
419
+ if (hookCount)
420
+ console.log(` ✅ Hooks: ${hookCount} hook(s)`);
421
+ }
422
+ // 6. Types (per model)
423
+ if (viewsResolved?.instanceFactory?.codeTemplates?.types) {
424
+ for (const model of allModels) {
425
+ try {
426
+ const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'types', { spec, model, models: allModels }, { outputDir });
427
+ writeOutput(output);
428
+ }
429
+ catch (e) {
430
+ errors.push(`Type ${model?.name || 'unknown'}: ${e.message}`);
431
+ }
432
+ }
433
+ console.log(` ✅ Types: ${allModels.length} model type(s)`);
434
+ }
435
+ // 7. Scaffolding (package.json, tsconfig, etc.)
436
+ const scaffoldResolved = tryResolve('project.scaffold');
437
+ if (scaffoldResolved?.instanceFactory?.codeTemplates) {
438
+ const scaffoldFiles = [];
439
+ for (const [templateName] of Object.entries(scaffoldResolved.instanceFactory.codeTemplates)) {
440
+ try {
441
+ const output = await this.codeGenerator.generateFromTemplate(scaffoldResolved, templateName, { spec, models: allModels, manifest: this.manifest, instanceFactories: [] }, { outputDir });
442
+ writeOutput(output);
443
+ scaffoldFiles.push(basename(output.filePath));
444
+ }
445
+ catch (e) {
446
+ errors.push(`Scaffold: ${e.message}`);
447
+ }
448
+ }
449
+ if (scaffoldFiles.length)
450
+ console.log(` ✅ Project scaffolding: ${scaffoldFiles.join(', ')}`);
451
+ }
452
+ // 8. Backend app entry point + Fastify server with route wiring
453
+ const appResolved = tryResolve('app.entrypoint');
454
+ if (appResolved?.instanceFactory?.codeTemplates) {
455
+ const appFiles = [];
456
+ for (const [templateName] of Object.entries(appResolved.instanceFactory.codeTemplates)) {
457
+ try {
458
+ const output = await this.codeGenerator.generateFromTemplate(appResolved, templateName, { spec, models: allModels, manifest: this.manifest }, { outputDir });
459
+ writeOutput(output);
460
+ appFiles.push(basename(output.filePath));
461
+ }
462
+ catch (e) {
463
+ errors.push(`App ${templateName}: ${e.message}`);
464
+ }
465
+ }
466
+ // Generate Fastify server with auto-wired routes
467
+ try {
468
+ const serverGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'controllers', 'templates', 'fastify', 'server-generator.ts');
469
+ const serverGen = resolveGenPath(serverGenPath);
470
+ if (serverGen) {
471
+ const genPath = serverGen;
472
+ const { default: generateServer } = await import(genPath);
473
+ const serverCode = generateServer({ spec, models: allModels });
474
+ const serverPath = join(outputDir, 'backend', 'src', 'main.ts');
475
+ const serverDir = dirname(serverPath);
476
+ if (!existsSync(serverDir))
477
+ mkdirSync(serverDir, { recursive: true });
478
+ writeFileSync(serverPath, serverCode);
479
+ appFiles.push('main.ts');
480
+ }
481
+ }
482
+ catch (e) {
483
+ errors.push(`Server: ${e.message}`);
484
+ }
485
+ if (appFiles.length)
486
+ console.log(` ✅ Backend application: ${appFiles.join(', ')}`);
487
+ }
488
+ // 9. Frontend app
489
+ const frontendResolved = tryResolve('app.frontend');
490
+ if (frontendResolved?.instanceFactory?.codeTemplates) {
491
+ const frontendFiles = [];
492
+ for (const [templateName] of Object.entries(frontendResolved.instanceFactory.codeTemplates)) {
493
+ try {
494
+ const output = await this.codeGenerator.generateFromTemplate(frontendResolved, templateName, { spec, models: allModels, views: spec.views, manifest: this.manifest }, { outputDir });
495
+ writeOutput(output);
496
+ frontendFiles.push(basename(output.filePath));
497
+ }
498
+ catch (e) {
499
+ errors.push(`Frontend ${templateName}: ${e.message}`);
500
+ }
501
+ }
502
+ if (frontendFiles.length)
503
+ console.log(` ✅ Frontend application: ${frontendFiles.join(', ')}`);
504
+ // 9a. Generate shared view utilities and Tailwind config.
505
+ // The frontend factory may declare outputStructure=standalone with
506
+ // frontendDir="." — in that case views/lib live at the output root
507
+ // instead of under `frontend/`.
508
+ const frontendCfg = frontendResolved?.configuration || frontendResolved?.instanceFactory?.configuration || {};
509
+ const frontendOutputStructure = frontendCfg.outputStructure || 'monorepo';
510
+ const frontendRelDir = frontendOutputStructure === 'standalone'
511
+ ? '.'
512
+ : (frontendCfg.frontendDir || 'frontend');
513
+ const frontendDir = frontendRelDir === '.' ? outputDir : join(outputDir, frontendRelDir);
514
+ try {
515
+ const sharedUtilsGen = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'views', 'templates', 'react', 'shared-utils-generator.ts');
516
+ const sharedUtilsResolved = resolveGenPath(sharedUtilsGen);
517
+ if (sharedUtilsResolved) {
518
+ const genPath = sharedUtilsResolved;
519
+ const { default: generateSharedUtils } = await import(genPath);
520
+ const result = generateSharedUtils({ spec });
521
+ const libDir = join(frontendDir, 'src', 'lib');
522
+ if (!existsSync(libDir))
523
+ mkdirSync(libDir, { recursive: true });
524
+ for (const file of result.files) {
525
+ writeFileSync(join(libDir, file.path), file.content);
526
+ }
527
+ }
528
+ // Tailwind config
529
+ writeFileSync(join(frontendDir, 'tailwind.config.js'), `import path from 'path';
530
+ import { createRequire } from 'module';
531
+ const require = createRequire(import.meta.url);
532
+
533
+ /** @type {import('tailwindcss').Config} */
534
+ export default {
535
+ darkMode: 'class',
536
+ content: [
537
+ './index.html',
538
+ './src/**/*.{js,ts,jsx,tsx}',
539
+ path.join(path.dirname(require.resolve('@specverse/runtime/package.json')), 'dist/**/*.js'),
540
+ ],
541
+ theme: { extend: {} },
542
+ plugins: [],
543
+ };
544
+ `);
545
+ writeFileSync(join(frontendDir, 'postcss.config.js'), `export default {
546
+ plugins: {
547
+ tailwindcss: {},
548
+ autoprefixer: {},
549
+ },
550
+ };
551
+ `);
552
+ }
553
+ catch (e) {
554
+ errors.push(`SharedUtils: ${e.message}`);
555
+ }
556
+ }
557
+ // 9b. Quint guards are no longer emitted to the generated backend.
558
+ // They are L3 verification assertions about the SPEC, not runtime
559
+ // checks on the user's data — so they run at realize time (see the
560
+ // L3 verification gate at the start of realizeAll) and don't ship
561
+ // to the user's project.
562
+ // 10. CLI commands (Commander.js)
563
+ try {
564
+ const cliDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'cli', 'templates', 'commander');
565
+ const entryGen = join(cliDir, 'cli-entry-generator.ts');
566
+ const cmdGen = join(cliDir, 'command-generator.ts');
567
+ const entryGenResolved = resolveGenPath(entryGen);
568
+ const cmdGenResolved = resolveGenPath(cmdGen);
569
+ if (entryGenResolved && cmdGenResolved) {
570
+ // Extract commands from spec — check each component for a commands section
571
+ const components = spec.components || [];
572
+ const componentList = Array.isArray(components)
573
+ ? components
574
+ : Object.entries(components).map(([name, c]) => ({ name, ...c }));
575
+ let commands = [];
576
+ let cliComponentVersion;
577
+ for (const comp of componentList) {
578
+ if (comp.commands) {
579
+ commands = Array.isArray(comp.commands)
580
+ ? comp.commands
581
+ : Object.entries(comp.commands).map(([name, def]) => ({ name, ...def }));
582
+ cliComponentVersion = comp.version;
583
+ break;
584
+ }
585
+ }
586
+ if (commands.length > 0) {
587
+ const cliOutputDir = join(outputDir, 'backend', 'src', 'cli');
588
+ if (!existsSync(cliOutputDir))
589
+ mkdirSync(cliOutputDir, { recursive: true });
590
+ // Generate entry point
591
+ const { default: generateCLIEntry } = await import(entryGenResolved);
592
+ const entryCode = generateCLIEntry({ spec, commands, componentVersion: cliComponentVersion });
593
+ const entryPath = join(cliOutputDir, 'index.ts');
594
+ writeFileSync(entryPath, entryCode);
595
+ // Generate individual command files
596
+ const { default: generateCommand } = await import(cmdGenResolved);
597
+ const cmdOutputDir = join(cliOutputDir, 'commands');
598
+ if (!existsSync(cmdOutputDir))
599
+ mkdirSync(cmdOutputDir, { recursive: true });
600
+ // The root command's subcommands are the actual CLI commands
601
+ const rootCommand = commands[0];
602
+ const subcommands = rootCommand.subcommands || {};
603
+ let cmdCount = 0;
604
+ for (const [cmdName, cmdDef] of Object.entries(subcommands)) {
605
+ try {
606
+ const code = generateCommand({ spec, command: { name: cmdName, ...cmdDef } });
607
+ writeFileSync(join(cmdOutputDir, `${cmdName}.ts`), code);
608
+ cmdCount++;
609
+ }
610
+ catch (e) {
611
+ errors.push(`CLI ${cmdName}: ${e.message}`);
612
+ }
613
+ }
614
+ console.log(` ✅ CLI: ${cmdCount} command(s) generated`);
615
+ files.push('index.ts', ...Object.keys(subcommands).map(n => `${n}.ts`));
616
+ }
617
+ }
618
+ }
619
+ catch (e) {
620
+ errors.push(`CLI generation: ${e.message}`);
621
+ }
622
+ // 11. Developer tools (VSCode extension, MCP server)
623
+ // Only generate when the manifest maps tools.vscode or tools.mcp capabilities
624
+ try {
625
+ const toolsDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'tools', 'templates');
626
+ // VSCode extension — only if manifest maps tools.vscode
627
+ if (tryResolve('tools.vscode')) {
628
+ const vscodeGen = resolveGenPath(join(toolsDir, 'vscode', 'vscode-extension-generator.ts'));
629
+ if (vscodeGen) {
630
+ try {
631
+ const { default: generateVSCodeExtension } = await import(vscodeGen);
632
+ const result = generateVSCodeExtension({ spec, outputDir, models: allModels });
633
+ console.log(` ✅ VSCode extension: ${result}`);
634
+ }
635
+ catch (e) {
636
+ errors.push(`VSCode: ${e.message}`);
637
+ }
638
+ }
639
+ }
640
+ // MCP server — only if manifest maps tools.mcp
641
+ if (tryResolve('tools.mcp')) {
642
+ const mcpGen = resolveGenPath(join(toolsDir, 'mcp', 'mcp-server-generator.ts'));
643
+ if (mcpGen) {
644
+ try {
645
+ const { default: generateMCPServer } = await import(mcpGen);
646
+ const result = generateMCPServer({ spec, outputDir, models: allModels });
647
+ console.log(` ✅ MCP server: ${result}`);
648
+ }
649
+ catch (e) {
650
+ errors.push(`MCP: ${e.message}`);
651
+ }
652
+ }
653
+ }
654
+ }
655
+ catch (e) {
656
+ errors.push(`Tools: ${e.message}`);
657
+ }
658
+ // 12. Ship assets (templates, examples, schema) from engine packages
659
+ try {
660
+ const assetsCopied = await this.copyAssets(outputDir);
661
+ if (assetsCopied.length > 0) {
662
+ console.log(` ✅ Assets: ${assetsCopied.join(', ')}`);
663
+ }
664
+ }
665
+ catch (e) {
666
+ errors.push(`Assets: ${e.message}`);
667
+ }
668
+ console.log(`\n✅ All code generated in: ${outputDir}`);
669
+ if (errors.length) {
670
+ console.warn(`⚠️ ${errors.length} warning(s) during generation`);
671
+ for (const err of errors)
672
+ console.warn(` ${err}`);
673
+ }
674
+ return { files, errors };
675
+ }
676
+ /**
677
+ * Copy static assets (templates, examples, schema) from engine packages
678
+ * into the output directory so the generated project is self-contained.
679
+ */
680
+ async copyAssets(outputDir) {
681
+ const copied = [];
682
+ const copyDir = (src, dest, label) => {
683
+ if (!existsSync(src))
684
+ return;
685
+ if (!existsSync(dest))
686
+ mkdirSync(dest, { recursive: true });
687
+ const copyRecursive = (s, d) => {
688
+ for (const entry of readdirSync(s)) {
689
+ const srcPath = join(s, entry);
690
+ const destPath = join(d, entry);
691
+ if (statSync(srcPath).isDirectory()) {
692
+ if (!existsSync(destPath))
693
+ mkdirSync(destPath, { recursive: true });
694
+ copyRecursive(srcPath, destPath);
695
+ }
696
+ else {
697
+ copyFileSync(srcPath, destPath);
698
+ }
699
+ }
700
+ };
701
+ copyRecursive(src, dest);
702
+ copied.push(label);
703
+ };
704
+ // Assets live in engines/assets/ (co-located with this package)
705
+ const enginesAssets = this.resolvePackageAssets('@specverse/engines', 'assets');
706
+ if (enginesAssets) {
707
+ copyDir(join(enginesAssets, 'examples'), join(outputDir, 'examples'), 'examples');
708
+ copyDir(join(enginesAssets, 'prompts'), join(outputDir, 'prompts'), 'prompts');
709
+ }
710
+ // Copy composed schema from engine-entities (single source of truth)
711
+ const thisDir = dirname(fileURLToPath(import.meta.url));
712
+ const schemaCandidates = [];
713
+ try {
714
+ const { createRequire } = await import('module');
715
+ const require = createRequire(import.meta.url);
716
+ const entitiesPkg = dirname(require.resolve('@specverse/entities/package.json'));
717
+ schemaCandidates.push(join(entitiesPkg, 'schema', 'SPECVERSE-SCHEMA.json'));
718
+ }
719
+ catch { /* engine-entities not available */ }
720
+ schemaCandidates.push(join(thisDir, '..', 'schema', 'SPECVERSE-SCHEMA.json'), join(thisDir, '../..', 'schema', 'SPECVERSE-SCHEMA.json'));
721
+ for (const schemaFile of schemaCandidates) {
722
+ if (existsSync(schemaFile)) {
723
+ const destSchema = join(outputDir, 'backend', 'schema');
724
+ if (!existsSync(destSchema))
725
+ mkdirSync(destSchema, { recursive: true });
726
+ copyFileSync(schemaFile, join(destSchema, 'SPECVERSE-SCHEMA.json'));
727
+ copied.push('schema');
728
+ break;
729
+ }
730
+ }
731
+ return copied;
732
+ }
733
+ resolvePackageAssets(packageName, subdir) {
734
+ const thisDir = dirname(fileURLToPath(import.meta.url));
735
+ const isSelf = packageName === '@specverse/engines' || packageName === '@specverse/engine-realize';
736
+ const candidates = [];
737
+ if (isSelf) {
738
+ // For our own package, look relative to this file
739
+ candidates.push(join(thisDir, '..', subdir));
740
+ candidates.push(join(thisDir, '../..', subdir));
741
+ }
742
+ // Try via node_modules (workspace layout and npm install)
743
+ for (let i = 2; i <= 5; i++) {
744
+ const up = Array(i).fill('..').join('/');
745
+ candidates.push(join(thisDir, up, 'node_modules', ...packageName.split('/'), subdir));
746
+ }
747
+ candidates.push(join(process.cwd(), 'node_modules', ...packageName.split('/'), subdir));
748
+ for (const candidate of candidates) {
749
+ if (existsSync(candidate))
750
+ return candidate;
751
+ }
752
+ return null;
753
+ }
754
+ }
755
+ export const engine = new SpecVerseRealizeEngine();
756
+ export default engine;
757
+ export { SpecVerseRealizeEngine };
758
+ //# sourceMappingURL=index.js.map