@specverse/engines 4.1.18 → 4.1.20

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.
@@ -34,7 +34,7 @@ function generatePackageJson(context) {
34
34
  "postcss": "^8.4.47",
35
35
  "tailwindcss": "^3.4.13",
36
36
  "typescript": "^5.2.0",
37
- "vite": "^5.0.0",
37
+ "vite": "^6.0.0",
38
38
  "eslint": "^8.53.0",
39
39
  "@typescript-eslint/eslint-plugin": "^6.10.0",
40
40
  "@typescript-eslint/parser": "^6.10.0",
@@ -36,7 +36,7 @@ function generateRuntimePackageJson(context) {
36
36
  "postcss": "^8.4.47",
37
37
  "tailwindcss": "^3.4.13",
38
38
  "typescript": "^5.2.0",
39
- "vite": "^5.0.0",
39
+ "vite": "^6.0.0",
40
40
  "eslint": "^8.53.0",
41
41
  "@typescript-eslint/eslint-plugin": "^6.10.0",
42
42
  "@typescript-eslint/parser": "^6.10.0",
@@ -13,9 +13,17 @@ function generateFastifyServer(context) {
13
13
  const specEvents = spec.events ? Object.keys(spec.events) : [];
14
14
  const hasEvents = specEvents.length > 0 || modelNames.length > 0;
15
15
  const servicesList = spec.services ? Object.keys(spec.services) : [];
16
- const serviceImports = servicesList.map(
17
- (name) => `import './${name.charAt(0).toLowerCase() + name.slice(1)}.js'; // Initialize ${name} event subscriptions`
18
- );
16
+ const serviceSingletonImports = servicesList.map((name) => {
17
+ const lower = name.charAt(0).toLowerCase() + name.slice(1);
18
+ return `import ${lower} from './services/${name}.js';`;
19
+ }).join("\n");
20
+ const serviceRouteImports = servicesList.map(
21
+ (name) => `import ${name}Routes from './routes/${name}Routes.js';`
22
+ ).join("\n");
23
+ const serviceRouteRegistrations = servicesList.map((name) => {
24
+ const lower = name.charAt(0).toLowerCase() + name.slice(1);
25
+ return ` await fastify.register(${name}Routes, { prefix: '/api/services/${name}', services: { ${name}: ${lower} } });`;
26
+ }).join("\n");
19
27
  return `/**
20
28
  * Fastify Server
21
29
  * Generated from SpecVerse specification
@@ -60,11 +68,13 @@ fastify.get('/api/events', async () => eventBus.getHistory());` : ""}
60
68
  // Register routes
61
69
  ${routeImports}
62
70
 
63
- ${serviceImports.length > 0 ? `// Initialize services (registers event subscriptions)
64
- ${serviceImports.join("\n")}` : ""}
71
+ ${serviceSingletonImports ? `// Service singletons + their Fastify route files
72
+ ${serviceSingletonImports}
73
+ ${serviceRouteImports}` : ""}
65
74
 
66
75
  async function registerRoutes() {
67
76
  ${routeRegistrations}
77
+ ${serviceRouteRegistrations ? "\n // Service operation routes (RPC-style under /api/services/{ServiceName})\n" + serviceRouteRegistrations : ""}
68
78
  }
69
79
 
70
80
  // Start server
@@ -0,0 +1,87 @@
1
+ function generateFastifyServiceRoutes(context) {
2
+ const { service } = context;
3
+ if (!service) {
4
+ throw new Error("Service is required in template context");
5
+ }
6
+ const serviceName = service.name;
7
+ if (!serviceName) {
8
+ throw new Error("Service must have a name");
9
+ }
10
+ const operationEntries = Array.isArray(service.operations) ? service.operations.map((op) => [op.name, op]) : Object.entries(service.operations || {});
11
+ if (operationEntries.length === 0) {
12
+ return `/**
13
+ * ${serviceName} \u2014 no operations declared, no routes generated.
14
+ */
15
+ import type { FastifyInstance } from 'fastify';
16
+
17
+ export default async function ${serviceName}Routes(_fastify: FastifyInstance, _options: any) {
18
+ // No operations declared on ${serviceName}.
19
+ }
20
+ `;
21
+ }
22
+ const routeHandlers = operationEntries.map(([opName, op]) => generateOperationRoute(opName, op, serviceName)).join("\n\n");
23
+ return `import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
24
+
25
+ /**
26
+ * ${serviceName} Routes
27
+ *
28
+ * Generated from SpecVerse specification \u2014 exposes service operations
29
+ * as HTTP endpoints under /api/services/${serviceName}/.
30
+ *
31
+ * Operations: ${operationEntries.map(([n]) => n).join(", ")}
32
+ */
33
+ export default async function ${serviceName}Routes(
34
+ fastify: FastifyInstance,
35
+ options: any
36
+ ) {
37
+ const service = options.services.${serviceName};
38
+ if (!service) {
39
+ throw new Error('Service ${serviceName} not registered in options.services');
40
+ }
41
+
42
+ ${routeHandlers.split("\n").map((line) => " " + line).join("\n")}
43
+ }
44
+ `;
45
+ }
46
+ function generateOperationRoute(opName, op, serviceName) {
47
+ const params = op?.parameters || {};
48
+ const paramNames = Object.keys(params);
49
+ const validations = [];
50
+ for (const [pName, pDef] of Object.entries(params)) {
51
+ const required = typeof pDef === "string" ? pDef.includes("required") : pDef?.required;
52
+ const typeStr = typeof pDef === "string" ? pDef.split(" ")[0] : pDef?.type || "String";
53
+ if (required) {
54
+ validations.push(` if (body.${pName} === undefined || body.${pName} === null) errors.push({ field: '${pName}', message: '${pName} is required' });`);
55
+ }
56
+ if (typeStr === "UUID" || typeStr === "String" || typeStr === "Email") {
57
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'string') errors.push({ field: '${pName}', message: '${pName} must be a string' });`);
58
+ } else if (typeStr === "Integer" || typeStr === "Number" || typeStr === "Float") {
59
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'number') errors.push({ field: '${pName}', message: '${pName} must be a number' });`);
60
+ } else if (typeStr === "Boolean") {
61
+ validations.push(` if (body.${pName} !== undefined && typeof body.${pName} !== 'boolean') errors.push({ field: '${pName}', message: '${pName} must be a boolean' });`);
62
+ }
63
+ }
64
+ const validationBlock = validations.length > 0 ? ` const errors: Array<{ field: string; message: string }> = [];
65
+ ${validations.join("\n")}
66
+ if (errors.length > 0) return reply.status(400).send({ error: 'Validation failed', details: errors });` : "";
67
+ const callExpression = paramNames.length > 0 ? `service.${opName}(${paramNames.map((p) => `body.${p}`).join(", ")})` : `service.${opName}(body)`;
68
+ return `// ${opName}${op?.description ? ` \u2014 ${op.description}` : ""}
69
+ fastify.post('/${opName}', {
70
+ handler: async (request: FastifyRequest, reply: FastifyReply) => {
71
+ try {
72
+ const body = (request.body || {}) as Record<string, any>;
73
+ ${validationBlock}
74
+ const result = await ${callExpression};
75
+ return reply.send(result ?? { success: true });
76
+ } catch (error) {
77
+ return reply.status(400).send({
78
+ error: 'Failed to execute ${serviceName}.${opName}',
79
+ message: error instanceof Error ? error.message : String(error)
80
+ });
81
+ }
82
+ }
83
+ });`;
84
+ }
85
+ export {
86
+ generateFastifyServiceRoutes as default
87
+ };
@@ -195,7 +195,13 @@ function generatePackageJson(commands, _entityTypes, specVersion = "1.0.0") {
195
195
  devDependencies: {
196
196
  "@types/vscode": "^1.80.0",
197
197
  "@vscode/vsce": "^3.0.0",
198
- esbuild: "^0.19.0"
198
+ esbuild: "^0.25.0"
199
+ },
200
+ overrides: {
201
+ // Force the patched version of lodash through @vscode/vsce →
202
+ // @secretlint → @textlint transitive chain (4.17.23 has a
203
+ // code-injection advisory in `_.template`).
204
+ lodash: "^4.18.1"
199
205
  }
200
206
  };
201
207
  }
@@ -0,0 +1,244 @@
1
+ import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, copyFileSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __generatorDir = dirname(fileURLToPath(import.meta.url));
5
+ function generateVSCodeExtension(context) {
6
+ const { spec, outputDir } = context;
7
+ const extensionDir = join(outputDir || ".", "tools", "vscode-extension");
8
+ if (!existsSync(extensionDir)) mkdirSync(extensionDir, { recursive: true });
9
+ const distribution = extractDistribution(spec);
10
+ let cliCommands = [];
11
+ if (distribution?.commands) {
12
+ cliCommands = distribution.commands.map((cmd) => {
13
+ const parts = (cmd.from || cmd.command || "").split(".");
14
+ const name = parts[parts.length - 1] || "unknown";
15
+ return {
16
+ command: `specverse.${name}`,
17
+ title: cmd.title || `${capitalize(name)}: ${cmd.description || ""}`,
18
+ category: "SpecVerse"
19
+ };
20
+ });
21
+ }
22
+ if (cliCommands.length === 0) {
23
+ cliCommands = extractCLICommands(spec);
24
+ }
25
+ if (cliCommands.length === 0) {
26
+ cliCommands = getStandardCommands();
27
+ }
28
+ const entityTypes = extractEntityTypes(spec);
29
+ const specVersion = distribution?.version || spec?.metadata?.version || spec?.version || "4.0.0";
30
+ const packageJson = generatePackageJson(cliCommands, entityTypes, specVersion);
31
+ if (distribution) {
32
+ if (distribution.displayName) packageJson.displayName = distribution.displayName;
33
+ if (distribution.publisher) packageJson.publisher = distribution.publisher;
34
+ if (distribution.description) packageJson.description = distribution.description;
35
+ if (distribution.languages && distribution.languages.length > 0) {
36
+ packageJson.contributes.languages = distribution.languages.map((lang) => ({
37
+ id: lang.id,
38
+ aliases: lang.aliases || [lang.id],
39
+ extensions: lang.extensions || [],
40
+ configuration: `./language-configuration.json`
41
+ }));
42
+ packageJson.contributes.grammars = distribution.languages.filter((lang) => lang.grammar).map((lang) => ({
43
+ language: lang.id,
44
+ scopeName: lang.grammar,
45
+ path: `./syntaxes/${lang.id}.tmLanguage.json`
46
+ }));
47
+ }
48
+ if (distribution.themes && distribution.themes.length > 0) {
49
+ packageJson.contributes.themes = distribution.themes.map((theme) => ({
50
+ label: theme.name,
51
+ uiTheme: theme.type === "dark" ? "vs-dark" : theme.type === "light" ? "vs" : "hc-black",
52
+ path: `./themes/${theme.name.toLowerCase().replace(/\s+/g, "-")}-theme.json`
53
+ }));
54
+ }
55
+ }
56
+ writeFileSync(join(extensionDir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n");
57
+ let staticDir = join(__generatorDir, "static");
58
+ if (!existsSync(staticDir)) {
59
+ staticDir = join(__generatorDir.replace("/dist/libs/", "/libs/"), "static");
60
+ }
61
+ if (existsSync(staticDir)) {
62
+ copyRecursive(staticDir, extensionDir);
63
+ }
64
+ const srcDir = join(extensionDir, "src");
65
+ if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
66
+ const extTs = join(staticDir, "extension.ts");
67
+ if (existsSync(extTs)) {
68
+ copyFileSync(extTs, join(srcDir, "extension.ts"));
69
+ }
70
+ const buildScript = `const esbuild = require('esbuild');
71
+ esbuild.build({
72
+ entryPoints: ['src/extension.ts'],
73
+ bundle: true,
74
+ outfile: 'dist/extension.js',
75
+ external: ['vscode'],
76
+ format: 'cjs',
77
+ platform: 'node',
78
+ }).catch(() => process.exit(1));
79
+ `;
80
+ const scriptsDir = join(extensionDir, "scripts");
81
+ if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
82
+ writeFileSync(join(scriptsDir, "build.js"), buildScript);
83
+ return `VSCode extension generated in: ${extensionDir}
84
+ ${cliCommands.length} commands, ${entityTypes.length} entity keywords`;
85
+ }
86
+ function extractDistribution(spec) {
87
+ const allDistributions = [];
88
+ if (spec?.distributions) {
89
+ const entries = Array.isArray(spec.distributions) ? spec.distributions : Object.entries(spec.distributions).map(([name, data]) => ({ name, ...data }));
90
+ allDistributions.push(...entries);
91
+ }
92
+ const components = spec?.components || {};
93
+ const componentList = Array.isArray(components) ? components : Object.values(components);
94
+ for (const comp of componentList) {
95
+ const distributions = comp?.distributions;
96
+ if (!distributions) continue;
97
+ const distEntries = Array.isArray(distributions) ? distributions : Object.entries(distributions).map(([name, data]) => ({ name, ...data }));
98
+ allDistributions.push(...distEntries);
99
+ }
100
+ for (const dist of allDistributions) {
101
+ if (dist.type === "ide") return dist;
102
+ }
103
+ return allDistributions.length > 0 ? allDistributions[0] : null;
104
+ }
105
+ function extractCLICommands(spec) {
106
+ const commands = [];
107
+ const components = spec?.components || {};
108
+ const componentList = Array.isArray(components) ? components : Object.entries(components).map(([name, data]) => ({ name, ...data }));
109
+ for (const comp of componentList) {
110
+ const cliCommands = comp?.commands;
111
+ if (!cliCommands) continue;
112
+ for (const [rootName, rootDef] of Object.entries(cliCommands)) {
113
+ const subcommands = rootDef?.subcommands || {};
114
+ for (const [subName, subDef] of Object.entries(subcommands)) {
115
+ const sub = subDef;
116
+ const nestedSubs = sub?.subcommands;
117
+ if (nestedSubs) {
118
+ for (const [nestedName, nestedDef] of Object.entries(nestedSubs)) {
119
+ commands.push({
120
+ command: `specverse.${subName}.${nestedName}`,
121
+ title: `${capitalize(subName)} ${capitalize(nestedName)}: ${nestedDef.description || ""}`,
122
+ category: "SpecVerse"
123
+ });
124
+ }
125
+ } else {
126
+ commands.push({
127
+ command: `specverse.${subName}`,
128
+ title: `${capitalize(subName)}: ${sub.description || ""}`,
129
+ category: "SpecVerse"
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+ return commands;
136
+ }
137
+ function extractEntityTypes(spec) {
138
+ const types = /* @__PURE__ */ new Set();
139
+ const components = spec?.components || [];
140
+ for (const component of Array.isArray(components) ? components : Object.values(components)) {
141
+ const comp = component;
142
+ const models = comp?.models;
143
+ if (models) {
144
+ if (Array.isArray(models)) {
145
+ models.forEach((m) => types.add(m.name));
146
+ } else {
147
+ Object.keys(models).forEach((name) => types.add(name));
148
+ }
149
+ }
150
+ }
151
+ return [...types];
152
+ }
153
+ function generatePackageJson(commands, _entityTypes, specVersion = "1.0.0") {
154
+ return {
155
+ name: "specverse",
156
+ displayName: "SpecVerse",
157
+ description: "SpecVerse specification language support \u2014 syntax highlighting, validation, IntelliSense",
158
+ version: specVersion,
159
+ publisher: "specverse",
160
+ engines: { vscode: "^1.80.0" },
161
+ categories: ["Programming Languages", "Linters", "Snippets"],
162
+ activationEvents: ["onLanguage:specverse"],
163
+ main: "./dist/extension.js",
164
+ contributes: {
165
+ languages: [{
166
+ id: "specverse",
167
+ aliases: ["SpecVerse", "specly"],
168
+ extensions: [".specly", ".specverse"],
169
+ configuration: "./language-configuration.json"
170
+ }],
171
+ grammars: [{
172
+ language: "specverse",
173
+ scopeName: "source.specverse",
174
+ path: "./syntaxes/specverse.tmLanguage.json"
175
+ }],
176
+ jsonValidation: [{
177
+ fileMatch: ["*.specly", "*.specverse"],
178
+ url: "./schemas/specverse-v3-schema.json"
179
+ }],
180
+ themes: [
181
+ { label: "SpecVerse Dark", uiTheme: "vs-dark", path: "./themes/specverse-complete-theme.json" },
182
+ { label: "SpecVerse Basic", uiTheme: "vs-dark", path: "./themes/specverse-basic-theme.json" }
183
+ ],
184
+ commands: commands.map((c) => ({
185
+ command: c.command,
186
+ title: c.title,
187
+ category: c.category
188
+ }))
189
+ },
190
+ scripts: {
191
+ "vscode:prepublish": "npm run build",
192
+ build: "node scripts/build.js",
193
+ package: "npx @vscode/vsce package"
194
+ },
195
+ devDependencies: {
196
+ "@types/vscode": "^1.80.0",
197
+ "@vscode/vsce": "^3.0.0",
198
+ esbuild: "^0.25.0"
199
+ },
200
+ overrides: {
201
+ // Force the patched version of lodash through @vscode/vsce →
202
+ // @secretlint → @textlint transitive chain (4.17.23 has a
203
+ // code-injection advisory in `_.template`).
204
+ lodash: "^4.18.1"
205
+ }
206
+ };
207
+ }
208
+ function copyRecursive(src, dest) {
209
+ for (const entry of readdirSync(src)) {
210
+ if (entry === "extension.ts") continue;
211
+ const srcPath = join(src, entry);
212
+ const destPath = join(dest, entry);
213
+ if (statSync(srcPath).isDirectory()) {
214
+ if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
215
+ copyRecursive(srcPath, destPath);
216
+ } else {
217
+ copyFileSync(srcPath, destPath);
218
+ }
219
+ }
220
+ }
221
+ function getStandardCommands() {
222
+ return [
223
+ { command: "specverse.validate", title: "Validate specification", category: "SpecVerse" },
224
+ { command: "specverse.infer", title: "Infer full architecture", category: "SpecVerse" },
225
+ { command: "specverse.realize", title: "Generate code from specification", category: "SpecVerse" },
226
+ { command: "specverse.init", title: "Initialize new project", category: "SpecVerse" },
227
+ { command: "specverse.gen.diagrams", title: "Generate Mermaid diagrams", category: "SpecVerse" },
228
+ { command: "specverse.gen.docs", title: "Generate documentation", category: "SpecVerse" },
229
+ { command: "specverse.gen.uml", title: "Generate UML diagrams", category: "SpecVerse" },
230
+ { command: "specverse.dev.format", title: "Format .specly file", category: "SpecVerse" },
231
+ { command: "specverse.dev.watch", title: "Watch and validate on change", category: "SpecVerse" },
232
+ { command: "specverse.dev.quick", title: "Quick validation", category: "SpecVerse" },
233
+ { command: "specverse.cache", title: "Manage import cache", category: "SpecVerse" },
234
+ { command: "specverse.ai.docs", title: "Generate AI implementation prompt", category: "SpecVerse" },
235
+ { command: "specverse.ai.suggest", title: "Get spec improvement suggestions", category: "SpecVerse" },
236
+ { command: "specverse.ai.template", title: "Load AI prompt template", category: "SpecVerse" }
237
+ ];
238
+ }
239
+ function capitalize(str) {
240
+ return str.charAt(0).toUpperCase() + str.slice(1);
241
+ }
242
+ export {
243
+ generateVSCodeExtension as default
244
+ };
@@ -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;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"}
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;IA4lB9F;;;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"}
@@ -349,6 +349,27 @@ class SpecVerseRealizeEngine {
349
349
  }
350
350
  console.log(` ✅ Routes: ${allModels.length} route handler(s)`);
351
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
+ }
352
373
  // 5. Views, Forms, Hooks
353
374
  const viewsResolved = tryResolve('ui.components');
354
375
  if (viewsResolved && spec.views) {