@archora/forge-cli 1.3.0 → 2.0.0

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.
@@ -0,0 +1,4701 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../config/dist/index.js
4
+ import { readFile } from "fs/promises";
5
+ import { pathToFileURL } from "url";
6
+ import { createJiti } from "jiti";
7
+ function defineForgeConfig(config) {
8
+ return config;
9
+ }
10
+ function createForgeConfigPreset(name, config) {
11
+ if (name === "simple") {
12
+ return {
13
+ ...config,
14
+ output: {
15
+ root: "./src",
16
+ generatedDir: "./src/api/generated",
17
+ featuresDir: "./src/api/features",
18
+ pagesDir: "./src/pages",
19
+ mocksDir: "./src/api/mocks",
20
+ ...config.output
21
+ },
22
+ target: {
23
+ framework: "neutral",
24
+ language: "typescript",
25
+ query: "promise",
26
+ ui: "metadata",
27
+ architecture: "simple",
28
+ ...config.target
29
+ }
30
+ };
31
+ }
32
+ if (name === "monorepo" && config.inputs) {
33
+ return {
34
+ ...config,
35
+ inputs: config.inputs.map((input) => ({
36
+ ...input,
37
+ output: {
38
+ generatedDir: `./src/generated/${input.name}`,
39
+ ...input.output
40
+ }
41
+ }))
42
+ };
43
+ }
44
+ return {
45
+ ...config,
46
+ output: {
47
+ root: "./src",
48
+ generatedDir: "./src/shared/api/generated",
49
+ featuresDir: "./src/features",
50
+ pagesDir: "./src/pages",
51
+ mocksDir: "./src/shared/mocks",
52
+ ...config.output
53
+ },
54
+ target: {
55
+ framework: "neutral",
56
+ language: "typescript",
57
+ query: "promise",
58
+ ui: "metadata",
59
+ architecture: "feature-sliced",
60
+ ...config.target
61
+ }
62
+ };
63
+ }
64
+ function resolveForgeConfig(config) {
65
+ return {
66
+ input: config.input ?? config.inputs?.[0]?.path ?? "",
67
+ inputs: config.inputs ?? [],
68
+ output: {
69
+ root: config.output?.root ?? "./src",
70
+ generatedDir: config.output?.generatedDir ?? "./src/shared/api/generated",
71
+ featuresDir: config.output?.featuresDir ?? "./src/features",
72
+ pagesDir: config.output?.pagesDir ?? "./src/pages",
73
+ mocksDir: config.output?.mocksDir ?? "./src/shared/mocks"
74
+ },
75
+ target: {
76
+ framework: config.target?.framework ?? "neutral",
77
+ language: config.target?.language ?? "typescript",
78
+ query: config.target?.query ?? "promise",
79
+ ui: config.target?.ui ?? "metadata",
80
+ architecture: config.target?.architecture ?? "feature-sliced"
81
+ },
82
+ validation: config.validation ?? "none",
83
+ mocks: {
84
+ adapter: config.mocks?.adapter ?? "simple"
85
+ },
86
+ schemaRequest: {
87
+ headers: interpolateHeaders(config.schemaRequest?.headers ?? {}),
88
+ timeoutMs: config.schemaRequest?.timeoutMs ?? 3e4
89
+ },
90
+ plugins: config.plugins ?? [],
91
+ templates: config.templates ?? {},
92
+ uiAdapter: config.uiAdapter ?? {},
93
+ naming: {
94
+ resourceCase: config.naming?.resourceCase ?? "kebab",
95
+ fileCase: config.naming?.fileCase ?? "pascal",
96
+ componentPrefix: config.naming?.componentPrefix ?? "",
97
+ generatedSuffix: config.naming?.generatedSuffix ?? ".generated"
98
+ },
99
+ ci: {
100
+ failOnWarnings: config.ci?.failOnWarnings ?? false,
101
+ failOnUnsupportedFeatures: config.ci?.failOnUnsupportedFeatures ?? true,
102
+ failOnMissingSchemas: config.ci?.failOnMissingSchemas ?? false,
103
+ failOnDrift: config.ci?.failOnDrift ?? true,
104
+ minHealthScore: config.ci?.minHealthScore
105
+ },
106
+ resources: config.resources ?? {},
107
+ overwrite: {
108
+ generated: config.overwrite?.generated ?? true,
109
+ custom: config.overwrite?.custom ?? false
110
+ }
111
+ };
112
+ }
113
+ function interpolateHeaders(headers) {
114
+ return Object.fromEntries(
115
+ Object.entries(headers).map(([key, value]) => [
116
+ key,
117
+ value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => process.env[name] ?? "")
118
+ ])
119
+ );
120
+ }
121
+ async function loadForgeConfig(filePath) {
122
+ if (filePath.endsWith(".json")) {
123
+ return parseConfig(JSON.parse(await readFile(filePath, "utf8")));
124
+ }
125
+ const jiti = createJiti(import.meta.url, {
126
+ interopDefault: true,
127
+ moduleCache: false
128
+ });
129
+ try {
130
+ const loaded = await jiti.import(pathToFileURL(filePath).href, { default: true });
131
+ return parseConfig(loaded);
132
+ } catch (error) {
133
+ const source = await readFile(filePath, "utf8");
134
+ const fallback = loadConfigFromSource(source);
135
+ if (fallback) {
136
+ return fallback;
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+ function parseConfig(value) {
142
+ if (!isForgeConfig(value)) {
143
+ throw new Error("Invalid Archora Forge config: `input` must be a string or `inputs` must contain at least one schema entry.");
144
+ }
145
+ return value;
146
+ }
147
+ function isForgeConfig(value) {
148
+ if (typeof value !== "object" || value === null) return false;
149
+ if ("input" in value && value.input !== void 0 && typeof value.input !== "string") return false;
150
+ if ("input" in value && typeof value.input === "string") return true;
151
+ return "inputs" in value && Array.isArray(value.inputs) && value.inputs.length > 0 && value.inputs.every((input) => isInputEntry(input));
152
+ }
153
+ function isInputEntry(value) {
154
+ return typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" && "path" in value && typeof value.path === "string";
155
+ }
156
+ function loadConfigFromSource(source) {
157
+ const match = source.match(/export\s+default\s+defineForgeConfig\(([\s\S]*)\)\s*;?\s*$/);
158
+ if (!match?.[1]) {
159
+ return null;
160
+ }
161
+ const createConfig = new Function("defineForgeConfig", `return defineForgeConfig(${match[1]})`);
162
+ return parseConfig(createConfig(defineForgeConfig));
163
+ }
164
+
165
+ // src/schema-request.ts
166
+ function parseSchemaRequestHeaders(value) {
167
+ const entries = Array.isArray(value) ? value : value ? [value] : [];
168
+ return Object.fromEntries(entries.map(parseSchemaRequestHeader));
169
+ }
170
+ function parseSchemaRequestHeader(value) {
171
+ const separator = findHeaderSeparator(value);
172
+ if (separator === -1) {
173
+ throw new Error(`Invalid schema header "${value}". Expected "name:value" or "name=value".`);
174
+ }
175
+ const name = value.slice(0, separator).trim();
176
+ const headerValue = value.slice(separator + 1).trim();
177
+ if (!name || !headerValue) {
178
+ throw new Error(`Invalid schema header "${value}". Header name and value are required.`);
179
+ }
180
+ return [name, headerValue];
181
+ }
182
+ function findHeaderSeparator(value) {
183
+ const colon = value.indexOf(":");
184
+ const equals = value.indexOf("=");
185
+ if (colon === -1) return equals;
186
+ if (equals === -1) return colon;
187
+ return Math.min(colon, equals);
188
+ }
189
+
190
+ // src/config.ts
191
+ import { access } from "fs/promises";
192
+ import { dirname, isAbsolute, join, resolve } from "path";
193
+ var configNames = ["archora-forge.config.ts", "archora-forge.config.js", "archora-forge.config.mjs", "archora-forge.config.json"];
194
+ async function loadCliConfig(schema, options = {}) {
195
+ const configPath = options.config ? resolve(options.config) : await discoverConfig(process.cwd());
196
+ const loaded = configPath ? await loadForgeConfig(configPath) : { input: schema ?? "" };
197
+ const input = schema ?? loaded.input;
198
+ if (!input) {
199
+ throw new Error("OpenAPI schema path is required when no Archora Forge config is found.");
200
+ }
201
+ const inputBaseDir = configPath ? dirname(configPath) : process.cwd();
202
+ const absoluteInput = isRemoteInput(input) ? input : isAbsolute(input) ? input : resolve(inputBaseDir, input);
203
+ const baseDir = configPath ? dirname(configPath) : isRemoteInput(absoluteInput) ? process.cwd() : dirname(absoluteInput);
204
+ const config = resolveForgeConfig({
205
+ ...loaded,
206
+ input,
207
+ schemaRequest: mergeSchemaRequestHeaders(loaded.schemaRequest, options.schemaHeader),
208
+ output: {
209
+ ...loaded.output,
210
+ generatedDir: resolveOutput(baseDir, loaded.output?.generatedDir),
211
+ featuresDir: resolveOutput(baseDir, loaded.output?.featuresDir),
212
+ pagesDir: resolveOutput(baseDir, loaded.output?.pagesDir),
213
+ mocksDir: resolveOutput(baseDir, loaded.output?.mocksDir)
214
+ },
215
+ overwrite: {
216
+ ...loaded.overwrite,
217
+ custom: options.force ?? loaded.overwrite?.custom
218
+ }
219
+ });
220
+ return {
221
+ config,
222
+ schema: absoluteInput,
223
+ configPath,
224
+ cwd: baseDir
225
+ };
226
+ }
227
+ async function loadCliConfigSet(schema, options = {}) {
228
+ if (schema) return [await loadCliConfig(schema, options)];
229
+ const configPath = options.config ? resolve(options.config) : await discoverConfig(process.cwd());
230
+ if (!configPath) return [await loadCliConfig(schema, options)];
231
+ const loaded = await loadForgeConfig(configPath);
232
+ if (!loaded.inputs || loaded.inputs.length === 0) return [await loadCliConfig(schema, options)];
233
+ const baseDir = dirname(configPath);
234
+ return loaded.inputs.map((input) => {
235
+ const absoluteInput = isRemoteInput(input.path) ? input.path : isAbsolute(input.path) ? input.path : resolve(baseDir, input.path);
236
+ const output = {
237
+ ...loaded.output,
238
+ ...input.output
239
+ };
240
+ const config = resolveForgeConfig({
241
+ ...loaded,
242
+ input: input.path,
243
+ schemaRequest: mergeSchemaRequestHeaders(loaded.schemaRequest, options.schemaHeader),
244
+ output: {
245
+ ...output,
246
+ generatedDir: resolveOutput(baseDir, output.generatedDir),
247
+ featuresDir: resolveOutput(baseDir, output.featuresDir),
248
+ pagesDir: resolveOutput(baseDir, output.pagesDir),
249
+ mocksDir: resolveOutput(baseDir, output.mocksDir)
250
+ },
251
+ overwrite: {
252
+ ...loaded.overwrite,
253
+ custom: options.force ?? loaded.overwrite?.custom
254
+ }
255
+ });
256
+ return {
257
+ name: input.name,
258
+ config,
259
+ schema: absoluteInput,
260
+ configPath,
261
+ cwd: baseDir
262
+ };
263
+ });
264
+ }
265
+ async function discoverConfig(cwd) {
266
+ for (const name of configNames) {
267
+ const path = join(cwd, name);
268
+ try {
269
+ await access(path);
270
+ return path;
271
+ } catch {
272
+ continue;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ function resolveOutput(baseDir, value) {
278
+ if (!value) return void 0;
279
+ return isAbsolute(value) ? value : value.startsWith("./") ? value : join(baseDir, value);
280
+ }
281
+ function mergeSchemaRequestHeaders(schemaRequest, schemaHeader) {
282
+ return {
283
+ ...schemaRequest,
284
+ headers: {
285
+ ...schemaRequest?.headers,
286
+ ...parseSchemaRequestHeaders(schemaHeader)
287
+ }
288
+ };
289
+ }
290
+ function isRemoteInput(value) {
291
+ return value.startsWith("http://") || value.startsWith("https://");
292
+ }
293
+
294
+ // src/report-file.ts
295
+ import { mkdir, writeFile } from "fs/promises";
296
+ import { dirname as dirname2, resolve as resolve2 } from "path";
297
+ async function writeReportFile(path, content) {
298
+ const resolvedPath = resolve2(path);
299
+ await mkdir(dirname2(resolvedPath), { recursive: true });
300
+ await writeFile(resolvedPath, `${content.trimEnd()}
301
+ `, "utf8");
302
+ return resolvedPath;
303
+ }
304
+
305
+ // src/ui/logger.ts
306
+ import pc from "picocolors";
307
+ function printPendingPhaseMessage(command, detail) {
308
+ console.log(pc.bold("Archora Forge"));
309
+ console.log(`${pc.cyan(command)} is wired, but ${detail}.`);
310
+ }
311
+ var logger = {
312
+ title() {
313
+ console.log(pc.bold("Archora Forge"));
314
+ },
315
+ line(message = "") {
316
+ console.log(message);
317
+ },
318
+ success(message) {
319
+ console.log(`${pc.green("\u2713")} ${message}`);
320
+ },
321
+ warn(message) {
322
+ console.log(`${pc.yellow("-")} ${message}`);
323
+ },
324
+ error(message) {
325
+ console.error(`${pc.red("x")} ${message}`);
326
+ }
327
+ };
328
+
329
+ // src/html-report.ts
330
+ function createHtmlReport(title, payload) {
331
+ const diagnostics = payload.diagnostics ?? [];
332
+ const drift = payload.drift ?? [];
333
+ const failedChecks = payload.failedChecks ?? [];
334
+ const healthScore = payload.healthScore ?? payload.health?.score ?? minDefined((payload.schemas ?? []).map((schema) => schema.healthScore ?? schema.health?.score));
335
+ const resources = typeof payload.resources === "number" ? payload.resources : payload.resourceCount ?? payload.resources?.length;
336
+ const status = payload.ok === false ? "failed" : "passed";
337
+ const generatedFiles = payload.generatedFiles ?? sumDefined((payload.schemas ?? []).map((schema) => schema.generatedFiles));
338
+ const topAffectedResources = collectAffectedResources(payload);
339
+ const diagnosticsByCode = groupBy(diagnostics, (diagnostic) => diagnostic.code ?? "diagnostic");
340
+ const diagnosticsBySeverity = groupBy(diagnostics, (diagnostic) => diagnostic.severity ?? "warning");
341
+ const driftByCategory = groupBy(drift, (entry) => fileCategory(entry.path ?? ""));
342
+ const unsupportedDiagnostics = diagnostics.filter((diagnostic) => (diagnostic.code ?? "").startsWith("unsupported-"));
343
+ const ciSummary = [
344
+ `Status: ${status}`,
345
+ `Health: ${healthScore ?? "n/a"}`,
346
+ `Resources: ${resources ?? "n/a"}`,
347
+ `Generated files: ${generatedFiles ?? "n/a"}`,
348
+ `Diagnostics: ${diagnostics.length}`,
349
+ `Drift: ${drift.length}`,
350
+ `Failed checks: ${failedChecks.length > 0 ? failedChecks.join(", ") : "none"}`
351
+ ].join("\n");
352
+ return `<!doctype html>
353
+ <html lang="en">
354
+ <head>
355
+ <meta charset="utf-8">
356
+ <meta name="viewport" content="width=device-width, initial-scale=1">
357
+ <title>${escapeHtml(title)}</title>
358
+ <style>
359
+ :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #17202a; background: #f6f7f9; }
360
+ body { margin: 0; }
361
+ main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 48px; }
362
+ h1, h2, h3 { margin: 0; line-height: 1.2; }
363
+ h1 { font-size: 30px; }
364
+ h2 { margin-top: 28px; font-size: 18px; }
365
+ p { margin: 8px 0 0; color: #526071; }
366
+ .top { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; }
367
+ .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 6px 10px; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .04em; }
368
+ .passed { color: #0b6b3a; background: #dff7ea; }
369
+ .failed { color: #9d2a1f; background: #ffe2dd; }
370
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-top: 18px; }
371
+ .metric, .card { background: #fff; border: 1px solid #dfe4ea; border-radius: 8px; padding: 14px; box-shadow: 0 1px 2px rgba(23, 32, 42, .04); }
372
+ .metric strong { display: block; font-size: 24px; color: #17202a; }
373
+ .metric span, .muted { color: #667386; font-size: 13px; }
374
+ table { width: 100%; border-collapse: collapse; margin-top: 12px; background: #fff; border: 1px solid #dfe4ea; border-radius: 8px; overflow: hidden; }
375
+ th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #edf0f3; vertical-align: top; font-size: 14px; }
376
+ th { background: #f0f3f6; color: #3f4b59; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
377
+ tr:last-child td { border-bottom: 0; }
378
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; background: #eef2f6; padding: 2px 4px; border-radius: 4px; }
379
+ .warning { color: #8a5a00; }
380
+ .error { color: #a32116; }
381
+ .summary { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(280px, .7fr); gap: 12px; margin-top: 18px; }
382
+ .summary ul { margin: 10px 0 0; padding-left: 18px; }
383
+ .summary li { margin: 6px 0; }
384
+ .pill-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
385
+ .pill { display: inline-flex; gap: 6px; align-items: center; border: 1px solid #dfe4ea; border-radius: 999px; padding: 5px 9px; background: #f8fafc; font-size: 12px; }
386
+ details { background: #fff; border: 1px solid #dfe4ea; border-radius: 8px; margin-top: 12px; overflow: hidden; }
387
+ summary { cursor: pointer; padding: 12px 14px; font-weight: 700; background: #f8fafc; }
388
+ pre { white-space: pre-wrap; word-break: break-word; background: #17202a; color: #f6f7f9; border-radius: 8px; padding: 12px; font-size: 12px; margin: 12px 0 0; }
389
+ .empty { background: #fff; border: 1px dashed #cad2dc; border-radius: 8px; padding: 16px; color: #667386; margin-top: 12px; }
390
+ .footer { margin-top: 32px; font-size: 12px; color: #7a8797; }
391
+ </style>
392
+ </head>
393
+ <body>
394
+ <main>
395
+ <section class="top">
396
+ <div>
397
+ <h1>${escapeHtml(title)}</h1>
398
+ <p>${escapeHtml(payload.schema ?? "Archora Forge CLI report")}</p>
399
+ </div>
400
+ <span class="badge ${status}">${status}</span>
401
+ </section>
402
+
403
+ <section class="grid">
404
+ ${metric("Health", healthScore ?? "n/a")}
405
+ ${metric("Diagnostics", diagnostics.length)}
406
+ ${metric("Failed checks", failedChecks.length)}
407
+ ${metric("Drift", drift.length)}
408
+ ${metric("Resources", resources ?? "n/a")}
409
+ ${metric("Generated files", generatedFiles ?? "n/a")}
410
+ </section>
411
+
412
+ ${renderExecutiveSummary({ status, healthScore, resources, generatedFiles, diagnostics, drift, failedChecks, topAffectedResources })}
413
+ ${renderReadiness(payload.readiness)}
414
+ ${renderCiSummary(ciSummary)}
415
+ ${renderGeneratedFileCategories(driftByCategory, payload.files, generatedFiles)}
416
+ ${renderDiagnosticGroups(diagnosticsBySeverity, diagnosticsByCode)}
417
+ ${renderUnsupportedOperations(unsupportedDiagnostics)}
418
+ ${renderAffectedResources(topAffectedResources)}
419
+ ${renderResourceExplorer(payload.resourceExplorer)}
420
+ ${renderCoverageMatrix(payload.coverage)}
421
+ ${renderImpactCenter(payload)}
422
+ ${renderContractDiffSummary(payload)}
423
+ ${renderSchemas(payload.schemas)}
424
+ ${renderFailedChecks(failedChecks)}
425
+ ${renderDiagnostics(diagnostics)}
426
+ ${renderDrift(drift)}
427
+
428
+ <p class="footer">Generated by Archora Forge CLI. Commit this report as a CI artifact or attach it to release validation notes.</p>
429
+ </main>
430
+ </body>
431
+ </html>
432
+ `;
433
+ }
434
+ function renderReadiness(readiness) {
435
+ if (!readiness) return "";
436
+ return `<section class="summary">
437
+ <div class="card">
438
+ <h2>Pilot Readiness</h2>
439
+ <p>Status: <strong>${escapeHtml(readiness.status ?? "n/a")}</strong></p>
440
+ ${readiness.gate ? `<p>Gate: <strong>${escapeHtml(readiness.gate.result ?? "n/a")}</strong></p>
441
+ <p>Recommended CI mode: <strong>${escapeHtml(readiness.gate.recommendedCiMode ?? "n/a")}</strong></p>
442
+ <p>${escapeHtml(readiness.gate.reason ?? "")}</p>` : ""}
443
+ <p>${escapeHtml(readiness.decision ?? "")}</p>
444
+ </div>
445
+ <div class="card">
446
+ <h2>Next Actions</h2>
447
+ ${renderSimpleList(readiness.nextActions ?? [], "No action required.")}
448
+ </div>
449
+ <div class="card">
450
+ <h2>Blockers</h2>
451
+ ${renderSimpleList(readiness.blockers ?? [], "No blockers.")}
452
+ </div>
453
+ <div class="card">
454
+ <h2>Warnings</h2>
455
+ ${renderSimpleList(readiness.warnings ?? [], "No warnings.")}
456
+ </div>
457
+ ${readiness.reviewerChecklist ? `<div class="card">
458
+ <h2>Reviewer Checklist</h2>
459
+ ${renderSimpleList(readiness.reviewerChecklist, "No reviewer checklist.")}
460
+ </div>` : ""}
461
+ </section>`;
462
+ }
463
+ function renderSimpleList(items, empty) {
464
+ const values = items.length > 0 ? items : [empty];
465
+ return `<ul>${values.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
466
+ }
467
+ function renderExecutiveSummary(input) {
468
+ const health = input.healthScore ?? "n/a";
469
+ const mainFinding = input.status === "passed" ? "Generated output is up to date for the configured checks." : "Generated output needs attention before this schema should pass CI.";
470
+ const affected = input.topAffectedResources.length > 0 ? input.topAffectedResources.slice(0, 5).map((item) => `${item.name} (${item.count})`).join(", ") : "none";
471
+ return `<section class="summary">
472
+ <div class="card">
473
+ <h2>Executive Summary</h2>
474
+ <ul>
475
+ <li>${escapeHtml(mainFinding)}</li>
476
+ <li>Health score: <strong>${escapeHtml(String(health))}</strong>.</li>
477
+ <li>Resources: <strong>${escapeHtml(String(input.resources ?? "n/a"))}</strong>; generated files: <strong>${escapeHtml(String(input.generatedFiles ?? "n/a"))}</strong>.</li>
478
+ <li>Diagnostics: <strong>${input.diagnostics.length}</strong>; drift entries: <strong>${input.drift.length}</strong>.</li>
479
+ <li>Top affected resources: ${escapeHtml(affected)}.</li>
480
+ </ul>
481
+ </div>
482
+ <div class="card">
483
+ <h2>Decision Signal</h2>
484
+ <p>${input.failedChecks.length === 0 ? "Ready for CI under the current policy." : `Blocked by: ${escapeHtml(input.failedChecks.join(", "))}.`}</p>
485
+ </div>
486
+ </section>`;
487
+ }
488
+ function renderCiSummary(summary) {
489
+ return `<section><h2>Copyable CI Summary</h2><pre>${escapeHtml(summary)}</pre></section>`;
490
+ }
491
+ function renderGeneratedFileCategories(groupedDrift, files, generatedFiles) {
492
+ const pills = [
493
+ generatedFiles !== void 0 ? ["total generated", generatedFiles] : null,
494
+ files?.create !== void 0 ? ["create", files.create] : null,
495
+ files?.update !== void 0 ? ["update", files.update] : null,
496
+ files?.protected !== void 0 ? ["protected", files.protected] : null,
497
+ ...[...groupedDrift.entries()].map(([name, entries]) => [name, entries.length])
498
+ ].filter((item) => Boolean(item));
499
+ if (pills.length === 0) return '<section><h2>Generated Files by Category</h2><div class="empty">No generated file summary available.</div></section>';
500
+ return `<section><h2>Generated Files by Category</h2><div class="pill-list">${pills.map(([name, count]) => `<span class="pill"><strong>${escapeHtml(String(count))}</strong>${escapeHtml(name)}</span>`).join("")}</div></section>`;
501
+ }
502
+ function renderDiagnosticGroups(bySeverity, byCode) {
503
+ if (bySeverity.size === 0 && byCode.size === 0) return '<section><h2>Diagnostics Grouped</h2><div class="empty">No diagnostics to group.</div></section>';
504
+ return `<section><h2>Diagnostics Grouped</h2>
505
+ <details open><summary>By severity</summary><div class="pill-list">${renderGroupPills(bySeverity)}</div></details>
506
+ <details><summary>By code</summary><div class="pill-list">${renderGroupPills(byCode)}</div></details>
507
+ </section>`;
508
+ }
509
+ function renderUnsupportedOperations(diagnostics) {
510
+ if (diagnostics.length === 0) return '<section><h2>Unsupported Operations</h2><div class="empty">No unsupported operations detected.</div></section>';
511
+ const grouped = groupBy(diagnostics, (diagnostic) => diagnostic.code ?? "unsupported");
512
+ return `<section><h2>Unsupported Operations</h2><div class="pill-list">${renderGroupPills(grouped)}</div></section>`;
513
+ }
514
+ function renderAffectedResources(resources) {
515
+ if (resources.length === 0) return '<section><h2>Top Affected Resources</h2><div class="empty">No affected resources detected.</div></section>';
516
+ return `<section><h2>Top Affected Resources</h2><div class="pill-list">${resources.slice(0, 20).map((item) => `<span class="pill"><strong>${item.count}</strong>${escapeHtml(item.name)}</span>`).join("")}</div></section>`;
517
+ }
518
+ function renderResourceExplorer(resources) {
519
+ if (!resources) return "";
520
+ if (resources.length === 0) return '<section><h2>Resource Explorer</h2><div class="empty">No resources detected.</div></section>';
521
+ const cards = resources.slice(0, 50).map((resource) => {
522
+ const operations = (resource.operations ?? []).slice(0, 20).map((operation) => `<li><code>${escapeHtml(`${operation.method?.toUpperCase() ?? ""} ${operation.path ?? ""}`.trim())}</code> ${escapeHtml(operation.id ?? "")} <span class="muted">${escapeHtml(operation.kind ?? "")}</span></li>`).join("");
523
+ const files = (resource.generatedFiles ?? []).slice(0, 12).map((file) => `<li><code>${escapeHtml(file)}</code></li>`).join("");
524
+ return `<div class="card">
525
+ <h3>${escapeHtml(resource.name ?? "resource")}</h3>
526
+ <p>${escapeHtml(String(resource.operations?.length ?? 0))} operations; ${escapeHtml(String(resource.generatedFiles?.length ?? 0))} generated files.</p>
527
+ ${operations ? `<details><summary>Operations</summary><ul>${operations}</ul></details>` : ""}
528
+ ${files ? `<details><summary>Generated files</summary><ul>${files}</ul></details>` : ""}
529
+ </div>`;
530
+ }).join("");
531
+ return `<section><h2>Resource Explorer</h2><div class="summary">${cards}</div></section>`;
532
+ }
533
+ function renderCoverageMatrix(coverage) {
534
+ if (!coverage) return "";
535
+ return `<section>
536
+ <h2>Schema Coverage Matrix</h2>
537
+ <div class="grid">
538
+ ${metric("Operations", coverage.operations?.total ?? "n/a")}
539
+ ${metric("Generated operations", coverage.operations?.generated ?? "n/a")}
540
+ ${metric("Fallback cases", coverage.cases?.fallback ?? "n/a")}
541
+ ${metric("Diagnostic-only cases", coverage.cases?.diagnosticOnly ?? "n/a")}
542
+ ${metric("Schemas", coverage.schemas?.total ?? "n/a")}
543
+ ${metric("Skipped operations", coverage.cases?.skipped ?? "n/a")}
544
+ </div>
545
+ <details open><summary>Operation types</summary><div class="pill-list">${renderRecordPills(coverage.operations?.byKind)}</div></details>
546
+ <details><summary>Request shapes</summary><div class="pill-list">${renderRecordPills(coverage.operations?.byRequestShape)}</div></details>
547
+ <details><summary>Response shapes</summary><div class="pill-list">${renderRecordPills(coverage.operations?.byResponseShape)}</div></details>
548
+ <details><summary>Unsupported schema constructs</summary><div class="pill-list">${renderRecordPills(coverage.schemas?.unsupportedConstructs)}</div></details>
549
+ </section>`;
550
+ }
551
+ function renderContractDiffSummary(payload) {
552
+ if (!payload.changes?.length && !payload.affectedResources?.length) {
553
+ return '<section><h2>Contract Diff Summary</h2><div class="empty">No contract diff changes in this report.</div></section>';
554
+ }
555
+ const breaking = payload.changes?.filter((change) => change.severity === "breaking").length ?? 0;
556
+ const nonBreaking = (payload.changes?.length ?? 0) - breaking;
557
+ return `<section><h2>Contract Diff Summary</h2><div class="card"><p>Breaking changes: <strong>${breaking}</strong>; non-breaking changes: <strong>${nonBreaking}</strong>; affected resources: <strong>${payload.affectedResources?.length ?? 0}</strong>.</p></div></section>`;
558
+ }
559
+ function renderImpactCenter(payload) {
560
+ if (!payload.decision && !payload.summary && !payload.migrationHints?.length && !payload.prSummary) return "";
561
+ return `<section>
562
+ <h2>Frontend Impact Center</h2>
563
+ <div class="summary">
564
+ <div class="card">
565
+ <h2>Decision</h2>
566
+ <p>Status: <strong>${escapeHtml(payload.decision?.status ?? "n/a")}</strong></p>
567
+ <p>Merge risk: <strong>${escapeHtml(payload.decision?.mergeRisk ?? "n/a")}</strong></p>
568
+ <p>${escapeHtml(payload.decision?.reason ?? "")}</p>
569
+ </div>
570
+ <div class="card">
571
+ <h2>Impact Counts</h2>
572
+ <ul>
573
+ <li>Breaking: <strong>${escapeHtml(String(payload.summary?.breaking ?? 0))}</strong></li>
574
+ <li>Warnings: <strong>${escapeHtml(String(payload.summary?.warnings ?? 0))}</strong></li>
575
+ <li>Non-breaking: <strong>${escapeHtml(String(payload.summary?.nonBreaking ?? 0))}</strong></li>
576
+ <li>Affected files: <strong>${escapeHtml(String(payload.affectedFiles?.length ?? 0))}</strong></li>
577
+ </ul>
578
+ </div>
579
+ </div>
580
+ <details open><summary>PR Summary</summary><pre>${escapeHtml(payload.prSummary ?? "")}</pre></details>
581
+ <details open><summary>Migration Hints</summary>${renderSimpleList(payload.migrationHints ?? [], "No migration hints.")}</details>
582
+ <details><summary>Impacted Surface</summary>
583
+ <div class="card">
584
+ <p>Operation IDs: ${escapeHtml(payload.impactedSurface?.operationIds?.join(", ") || "none")}</p>
585
+ <p>Client methods: ${escapeHtml(payload.impactedSurface?.clientMethods?.join(", ") || "none")}</p>
586
+ <p>Query hooks: ${escapeHtml(payload.impactedSurface?.queryHooks?.join(", ") || "none")}</p>
587
+ </div>
588
+ </details>
589
+ ${renderSourceUsages(payload.sourceUsages)}
590
+ </section>`;
591
+ }
592
+ function renderSourceUsages(usages) {
593
+ if (!usages) return "";
594
+ if (usages.length === 0) return '<details open><summary>Source Usage</summary><div class="empty">No impacted source usages found.</div></details>';
595
+ const rows = usages.slice(0, 100).map((usage) => `<tr><td><code>${escapeHtml(`${usage.path ?? ""}${usage.lines?.length ? `:${usage.lines.join(",")}` : ""}`)}</code></td><td>${escapeHtml((usage.matches ?? []).join(", "))}</td></tr>`).join("");
596
+ return `<details open><summary>Source Usage</summary><table><thead><tr><th>File</th><th>Matches</th></tr></thead><tbody>${rows}</tbody></table></details>`;
597
+ }
598
+ function renderSchemas(schemas) {
599
+ if (!schemas?.length) return "";
600
+ const rows = schemas.map((schema) => {
601
+ const healthScore = schema.healthScore ?? schema.health?.score ?? "n/a";
602
+ const resources = typeof schema.resources === "number" ? schema.resources : schema.resourceCount ?? schema.resources?.length ?? "n/a";
603
+ return `<tr><td>${escapeHtml(schema.name ?? "default")}</td><td><code>${escapeHtml(schema.schema ?? "")}</code></td><td>${escapeHtml(String(healthScore))}</td><td>${escapeHtml(String(resources))}</td><td>${escapeHtml(String(schema.diagnosticsCount ?? "n/a"))}</td></tr>`;
604
+ }).join("");
605
+ return `<section><h2>Schemas</h2><table><thead><tr><th>Name</th><th>Schema</th><th>Health</th><th>Resources</th><th>Diagnostics</th></tr></thead><tbody>${rows}</tbody></table></section>`;
606
+ }
607
+ function renderFailedChecks(failedChecks) {
608
+ if (failedChecks.length === 0) return '<section><h2>Failed Checks</h2><div class="empty">No failed checks.</div></section>';
609
+ return `<section><h2>Failed Checks</h2><div class="card">${failedChecks.map((check) => `<code>${escapeHtml(check)}</code>`).join(" ")}</div></section>`;
610
+ }
611
+ function renderDiagnostics(diagnostics) {
612
+ if (diagnostics.length === 0) return '<section><h2>Diagnostics</h2><div class="empty">No diagnostics.</div></section>';
613
+ const rows = diagnostics.slice(0, 200).map(
614
+ (diagnostic) => `<tr><td class="${escapeHtml(diagnostic.severity ?? "warning")}">${escapeHtml(diagnostic.severity ?? "warning")}</td><td><code>${escapeHtml(diagnostic.code ?? "diagnostic")}</code></td><td>${escapeHtml(diagnostic.location ?? "")}</td><td>${escapeHtml(diagnostic.message ?? "")}${diagnostic.suggestion ? `<p>${escapeHtml(diagnostic.suggestion)}</p>` : ""}</td></tr>`
615
+ ).join("");
616
+ return `<section><h2>Diagnostics</h2><table><thead><tr><th>Severity</th><th>Code</th><th>Location</th><th>Message</th></tr></thead><tbody>${rows}</tbody></table></section>`;
617
+ }
618
+ function renderDrift(drift) {
619
+ if (drift.length === 0) return '<section><h2>Drift</h2><div class="empty">No drift detected.</div></section>';
620
+ const rows = drift.map((entry) => `<tr><td><code>${escapeHtml(entry.path ?? "")}</code></td><td>${escapeHtml(entry.kind ?? "")}</td></tr>`).join("");
621
+ return `<section><h2>Drift</h2><table><thead><tr><th>Path</th><th>Kind</th></tr></thead><tbody>${rows}</tbody></table></section>`;
622
+ }
623
+ function metric(label, value) {
624
+ return `<div class="metric"><strong>${escapeHtml(String(value))}</strong><span>${escapeHtml(label)}</span></div>`;
625
+ }
626
+ function escapeHtml(value) {
627
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
628
+ }
629
+ function minDefined(values) {
630
+ const defined = values.filter((value) => value !== void 0);
631
+ return defined.length > 0 ? Math.min(...defined) : void 0;
632
+ }
633
+ function sumDefined(values) {
634
+ const defined = values.filter((value) => value !== void 0);
635
+ return defined.length > 0 ? defined.reduce((total, value) => total + value, 0) : void 0;
636
+ }
637
+ function groupBy(items, key) {
638
+ const groups = /* @__PURE__ */ new Map();
639
+ for (const item of items) {
640
+ const group = key(item);
641
+ groups.set(group, [...groups.get(group) ?? [], item]);
642
+ }
643
+ return groups;
644
+ }
645
+ function renderGroupPills(groups) {
646
+ return [...groups.entries()].sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0])).map(([name, items]) => `<span class="pill"><strong>${items.length}</strong>${escapeHtml(name)}</span>`).join("");
647
+ }
648
+ function renderRecordPills(record) {
649
+ if (!record || Object.keys(record).length === 0) return '<span class="muted">none</span>';
650
+ return Object.entries(record).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([name, count]) => `<span class="pill"><strong>${count}</strong>${escapeHtml(name)}</span>`).join("");
651
+ }
652
+ function collectAffectedResources(payload) {
653
+ const counts = /* @__PURE__ */ new Map();
654
+ for (const resource of payload.affectedResources ?? []) {
655
+ counts.set(resource, (counts.get(resource) ?? 0) + 1);
656
+ }
657
+ for (const diagnostic of payload.diagnostics ?? []) {
658
+ const resource = resourceFromLocation(diagnostic.location);
659
+ if (resource) counts.set(resource, (counts.get(resource) ?? 0) + 1);
660
+ }
661
+ for (const entry of payload.drift ?? []) {
662
+ const resource = resourceFromPath(entry.path);
663
+ if (resource) counts.set(resource, (counts.get(resource) ?? 0) + 1);
664
+ }
665
+ return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
666
+ }
667
+ function resourceFromLocation(location) {
668
+ const path = location?.match(/[A-Z]+\s+([^\s]+)/)?.[1];
669
+ if (!path) return null;
670
+ return path.split("/").filter(Boolean).filter((segment) => !segment.startsWith("{") && !/^v\d+$/i.test(segment)).at(-1) ?? null;
671
+ }
672
+ function resourceFromPath(path) {
673
+ if (!path) return null;
674
+ const feature = path.match(/src\/features\/([^/]+)/)?.[1];
675
+ if (feature) return feature;
676
+ return path.match(/src\/shared\/api\/generated\/([^/]+)/)?.[1] ?? null;
677
+ }
678
+ function fileCategory(path) {
679
+ if (path.includes("/features/") && path.includes("/api/")) return "feature api";
680
+ if (path.includes("/features/") && path.includes("/model/")) return "feature model";
681
+ if (path.includes("/shared/api/generated/")) return "api generated";
682
+ if (path.includes("/shared/mocks/")) return "mocks";
683
+ return "other";
684
+ }
685
+
686
+ // src/impact-report.ts
687
+ import { readdir, readFile as readFile2 } from "fs/promises";
688
+ import { join as join2, relative } from "path";
689
+ function formatImpactReport(format2, payload) {
690
+ if (format2 === "json") return JSON.stringify(payload, null, 2);
691
+ if (format2 === "html") return createHtmlReport("Frontend Impact Center", payload);
692
+ return formatImpactMarkdown(payload);
693
+ }
694
+ function formatImpactMarkdown(payload) {
695
+ const lines = [
696
+ "# Frontend API Impact",
697
+ "",
698
+ `Decision: ${payload.decision.status}`,
699
+ `Merge risk: ${payload.decision.mergeRisk}`,
700
+ `Reason: ${payload.decision.reason}`,
701
+ "",
702
+ "## Summary",
703
+ "",
704
+ `- Breaking: ${payload.summary.breaking}`,
705
+ `- Warnings: ${payload.summary.warnings}`,
706
+ `- Non-breaking: ${payload.summary.nonBreaking}`,
707
+ `- Affected resources: ${payload.affectedResources.length > 0 ? payload.affectedResources.join(", ") : "none"}`,
708
+ `- Affected generated files: ${payload.affectedFiles.length}`,
709
+ "",
710
+ "## PR Summary",
711
+ "",
712
+ payload.prSummary,
713
+ "",
714
+ "## Migration Hints",
715
+ "",
716
+ ...payload.migrationHints.length > 0 ? payload.migrationHints.map((hint) => `- ${hint}`) : ["No migration hints."],
717
+ "",
718
+ "## Changes",
719
+ "",
720
+ ...payload.changes.length > 0 ? payload.changes.map((change) => `- ${change.severity} ${change.code}: ${change.message} (${change.location})`) : ["No contract changes."],
721
+ "",
722
+ "## Impacted Surface",
723
+ "",
724
+ `- Operation IDs: ${payload.impactedSurface.operationIds.length > 0 ? payload.impactedSurface.operationIds.join(", ") : "none"}`,
725
+ `- Client methods: ${payload.impactedSurface.clientMethods.length > 0 ? payload.impactedSurface.clientMethods.join(", ") : "none"}`,
726
+ `- Query hooks: ${payload.impactedSurface.queryHooks.length > 0 ? payload.impactedSurface.queryHooks.join(", ") : "none"}`,
727
+ "",
728
+ "## Source Usage",
729
+ "",
730
+ ...formatSourceUsageLines(payload.sourceUsages ?? []),
731
+ ""
732
+ ];
733
+ return lines.join("\n");
734
+ }
735
+ function formatPullRequestComment(payload) {
736
+ return [
737
+ "## Frontend API Impact",
738
+ "",
739
+ `Merge decision: ${formatMergeDecision(payload.decision.status)}`,
740
+ `Merge risk: ${payload.decision.mergeRisk}`,
741
+ `Reason: ${payload.decision.reason}`,
742
+ "",
743
+ payload.prSummary,
744
+ "",
745
+ "## Next Actions",
746
+ "",
747
+ ...formatNextActionLines(payload),
748
+ "",
749
+ "## Source Usage",
750
+ "",
751
+ ...formatSourceUsageLines(payload.sourceUsages ?? []),
752
+ ""
753
+ ].join("\n");
754
+ }
755
+ function formatMergeDecision(status) {
756
+ if (status === "blocked") return "block";
757
+ if (status === "review") return "review";
758
+ return "pass";
759
+ }
760
+ function formatNextActionLines(payload) {
761
+ if (payload.decision.status === "blocked") {
762
+ return [
763
+ "- Do not merge until the breaking frontend contract changes are handled.",
764
+ "- Update impacted source usages before regenerating committed Forge output.",
765
+ "- Re-run `archora-forge impact` after the OpenAPI or frontend changes are updated."
766
+ ];
767
+ }
768
+ if (payload.decision.status === "review") {
769
+ return [
770
+ "- Review warnings with the frontend owner before merge.",
771
+ "- Regenerate Forge output in the branch if the contract change is accepted.",
772
+ "- Keep the PR comment attached to the API change for reviewer context."
773
+ ];
774
+ }
775
+ return [
776
+ "- Merge can continue from the API impact perspective.",
777
+ "- Regenerate Forge output when the schema change is accepted.",
778
+ "- Keep `archora-forge check` in CI to catch drift after regeneration."
779
+ ];
780
+ }
781
+ function formatSourceUsageLines(usages) {
782
+ if (usages.length === 0) return ["No impacted source usages found."];
783
+ return usages.slice(0, 50).map((usage) => `- \`${usage.path}:${usage.lines.join(",")}\`: ${usage.matches.join(", ")}`);
784
+ }
785
+ async function scanSourceUsages(repo, report) {
786
+ const tokens = createUsageTokens(report);
787
+ if (tokens.length === 0) return [];
788
+ const patterns = tokens.map((token) => ({ token, pattern: tokenPattern(token) }));
789
+ const files = await collectSourceFiles(repo);
790
+ const usages = [];
791
+ for (const file of files) {
792
+ const content = await readFile2(file, "utf8");
793
+ const matches = patterns.filter(({ pattern }) => pattern.test(content)).map(({ token }) => token);
794
+ if (matches.length > 0) {
795
+ usages.push({
796
+ path: normalizePath(relative(repo, file)),
797
+ matches: [...new Set(matches)].sort(),
798
+ lines: collectMatchedLines(content, matches)
799
+ });
800
+ }
801
+ }
802
+ return usages.sort((left, right) => left.path.localeCompare(right.path)).slice(0, 200);
803
+ }
804
+ function collectMatchedLines(content, matches) {
805
+ const patterns = matches.map((match) => tokenPattern(match));
806
+ const lines = content.split(/\r?\n/);
807
+ const matchedLines = /* @__PURE__ */ new Set();
808
+ lines.forEach((line, index) => {
809
+ if (patterns.some((pattern) => pattern.test(line))) matchedLines.add(index + 1);
810
+ });
811
+ return [...matchedLines].sort((left, right) => left - right).slice(0, 20);
812
+ }
813
+ function tokenPattern(token) {
814
+ return new RegExp(`(?<![\\w$])${escapeRegExp(token)}(?![\\w$])`);
815
+ }
816
+ function escapeRegExp(value) {
817
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
818
+ }
819
+ function createUsageTokens(report) {
820
+ const clientMethods = report.impactedSurface.clientMethods.map((method) => method.replace(/\(\)$/, ""));
821
+ const resourceTokens = report.affectedResources.flatMap((resource) => [`${resource}Client`, `${resource}QueryKeys`, `${resource}Config`, `${resource}Permissions`]);
822
+ return [...new Set([...report.impactedSurface.operationIds, ...clientMethods, ...report.impactedSurface.queryHooks, ...resourceTokens].filter(Boolean))].sort();
823
+ }
824
+ async function collectSourceFiles(root) {
825
+ const entries = await readdir(root, { withFileTypes: true });
826
+ const files = [];
827
+ for (const entry of entries) {
828
+ if (shouldSkipEntry(entry.name)) continue;
829
+ const path = join2(root, entry.name);
830
+ if (entry.isDirectory()) {
831
+ files.push(...await collectSourceFiles(path));
832
+ } else if (entry.isFile() && isSourceFile(entry.name)) {
833
+ files.push(path);
834
+ }
835
+ }
836
+ return files;
837
+ }
838
+ function shouldSkipEntry(name) {
839
+ return name === "node_modules" || name === ".git" || name === "dist" || name === "build" || name === "coverage" || name === ".vitepress" || name === ".turbo";
840
+ }
841
+ function isSourceFile(name) {
842
+ return /\.(cjs|cts|js|jsx|mjs|mts|svelte|ts|tsx|vue)$/.test(name);
843
+ }
844
+ function normalizePath(path) {
845
+ return path.replaceAll("\\", "/");
846
+ }
847
+
848
+ // src/audit-package.ts
849
+ import { execFile } from "child_process";
850
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
851
+ import { dirname as dirname4, join as join4, relative as relative3, resolve as resolve4 } from "path";
852
+ import { promisify } from "util";
853
+
854
+ // ../core/dist/index.js
855
+ import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
856
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3, sep } from "path";
857
+ import { access as access2 } from "fs/promises";
858
+ import { createHash } from "crypto";
859
+ import { join as join22 } from "path";
860
+ import { format } from "prettier";
861
+ import { pathToFileURL as pathToFileURL2 } from "url";
862
+ import { resolve as resolve22 } from "path";
863
+ import { readFile as readFile22 } from "fs/promises";
864
+ import { join as join32 } from "path";
865
+ import { readFile as readFile32 } from "fs/promises";
866
+ import { parse as parseYaml } from "yaml";
867
+ var ForgeError = class extends Error {
868
+ details;
869
+ constructor(message, details = {}) {
870
+ super(message);
871
+ this.name = "ForgeError";
872
+ this.details = details;
873
+ }
874
+ };
875
+ var forgeCoreVersion = "2.0.0";
876
+ var forgeGeneratedMarker = "// @archora-forge-generated";
877
+ var forgeGeneratedMetadataMarker = "// @archora-forge-meta";
878
+ async function writeGeneratedFiles(files, options) {
879
+ const result2 = {
880
+ created: 0,
881
+ updated: 0,
882
+ unchanged: 0,
883
+ protected: 0
884
+ };
885
+ for (const file of files) {
886
+ const content = toWritableGeneratedContent(file);
887
+ if (file.exists && !file.overwrite) {
888
+ result2.protected += 1;
889
+ continue;
890
+ }
891
+ const absolutePath = join3(options.cwd, file.path);
892
+ if (file.exists && await readExistingFile(absolutePath) === content) {
893
+ result2.unchanged += 1;
894
+ continue;
895
+ }
896
+ if (file.exists) {
897
+ result2.updated += 1;
898
+ } else {
899
+ result2.created += 1;
900
+ }
901
+ if (options.dryRun) {
902
+ continue;
903
+ }
904
+ await mkdir2(dirname3(absolutePath), { recursive: true });
905
+ await writeFile2(absolutePath, content, "utf8");
906
+ }
907
+ return result2;
908
+ }
909
+ function toWritableGeneratedContent(file) {
910
+ if (file.kind !== "generated" || !file.path.endsWith(".ts")) {
911
+ return file.content;
912
+ }
913
+ const contentWithoutHeaders = stripGeneratedHeaders(file.content);
914
+ return `${forgeGeneratedMarker}
915
+ ${formatGeneratedMetadata(file)}${contentWithoutHeaders}`;
916
+ }
917
+ function formatGeneratedMetadata(file) {
918
+ if (!file.metadata) return "";
919
+ return `${forgeGeneratedMetadataMarker} ${JSON.stringify(file.metadata)}
920
+ `;
921
+ }
922
+ function stripGeneratedHeaders(content) {
923
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
924
+ while (lines[0] === forgeGeneratedMarker || lines[0]?.startsWith(`${forgeGeneratedMetadataMarker} `)) {
925
+ lines.shift();
926
+ }
927
+ return lines.join("\n");
928
+ }
929
+ async function findPrunableGeneratedFiles(files, options) {
930
+ const planned = new Set(files.map((file) => normalizePath2(file.path)));
931
+ const candidates = [];
932
+ const seen = /* @__PURE__ */ new Set();
933
+ for (const root of options.roots) {
934
+ const absoluteRoot = resolveRoot(options.cwd, root);
935
+ for (const absolutePath of await listFiles(absoluteRoot)) {
936
+ const path = normalizePath2(relative2(options.cwd, absolutePath));
937
+ if (seen.has(path) || planned.has(path)) continue;
938
+ seen.add(path);
939
+ const content = await readExistingFile(absolutePath);
940
+ if (!content?.startsWith(forgeGeneratedMarker)) continue;
941
+ candidates.push({ path });
942
+ }
943
+ }
944
+ return candidates.sort((left, right) => left.path.localeCompare(right.path));
945
+ }
946
+ async function pruneGeneratedFiles(candidates, options) {
947
+ const result2 = { deleted: [], skipped: [] };
948
+ for (const candidate of candidates) {
949
+ if (options.dryRun) continue;
950
+ try {
951
+ await rm(resolve3(options.cwd, candidate.path), { force: false });
952
+ result2.deleted.push(candidate);
953
+ } catch (error) {
954
+ result2.skipped.push({ ...candidate, reason: error instanceof Error ? error.message : String(error) });
955
+ }
956
+ }
957
+ return result2;
958
+ }
959
+ async function readExistingFile(path) {
960
+ try {
961
+ return await readFile3(path, "utf8");
962
+ } catch {
963
+ return null;
964
+ }
965
+ }
966
+ async function listFiles(root) {
967
+ try {
968
+ const entries = await readdir2(root, { withFileTypes: true });
969
+ const nested = await Promise.all(
970
+ entries.map(async (entry) => {
971
+ const path = join3(root, entry.name);
972
+ if (entry.isDirectory()) return listFiles(path);
973
+ if (entry.isFile()) return [path];
974
+ return [];
975
+ })
976
+ );
977
+ return nested.flat();
978
+ } catch {
979
+ return [];
980
+ }
981
+ }
982
+ function resolveRoot(cwd, root) {
983
+ return isAbsolute2(root) ? root : resolve3(cwd, root);
984
+ }
985
+ function normalizePath2(path) {
986
+ return path.split(sep).join("/");
987
+ }
988
+ function analyzeAllOfSchema(document, schema) {
989
+ if (!schema.allOf) return { kind: "unsupported", reason: "Schema does not use allOf." };
990
+ const analysis = mergeAllOfBranches(document, schema.allOf, /* @__PURE__ */ new Set());
991
+ if (analysis.kind === "mergeable") return { kind: "mergeable" };
992
+ return analysis;
993
+ }
994
+ function mergeSimpleAllOfSchema(document, schema) {
995
+ if (!schema.allOf) return schema;
996
+ const unwrapped = unwrapAnnotationOnlyAllOfSchema(schema);
997
+ if (unwrapped) return unwrapped;
998
+ const analysis = mergeAllOfBranches(document, schema.allOf, /* @__PURE__ */ new Set());
999
+ if (analysis.kind !== "mergeable") return null;
1000
+ return {
1001
+ ...schema,
1002
+ type: "object",
1003
+ properties: analysis.properties,
1004
+ required: analysis.required.length > 0 ? analysis.required : void 0
1005
+ };
1006
+ }
1007
+ function unwrapAnnotationOnlyAllOfSchema(schema) {
1008
+ if (!schema.allOf) return null;
1009
+ const structuralBranches = schema.allOf.filter((branch) => !isAnnotationOnlySchema(branch));
1010
+ if (structuralBranches.length !== 1) return null;
1011
+ return {
1012
+ ...structuralBranches[0],
1013
+ nullable: schema.nullable ?? structuralBranches[0]?.nullable,
1014
+ description: schema.description ?? structuralBranches[0]?.description
1015
+ };
1016
+ }
1017
+ function mergeAllOfBranches(document, branches, seenRefs) {
1018
+ const properties = {};
1019
+ const required = /* @__PURE__ */ new Set();
1020
+ for (const branch of branches) {
1021
+ const resolved = resolveBranch(document, branch, seenRefs);
1022
+ if (!resolved) {
1023
+ return { kind: "unsupported", reason: "A branch references an unknown or circular schema." };
1024
+ }
1025
+ if (!isObjectLikeSchema(resolved)) {
1026
+ return { kind: "unsupported", reason: "Only object allOf branches can be merged safely." };
1027
+ }
1028
+ for (const [name, property] of Object.entries(resolved.properties ?? {})) {
1029
+ const existing = properties[name];
1030
+ if (existing && !areCompatibleProperties(existing, property)) {
1031
+ return {
1032
+ kind: "conflicting",
1033
+ property: name,
1034
+ reason: `Property "${name}" is defined with incompatible schema constraints across allOf branches.`
1035
+ };
1036
+ }
1037
+ properties[name] = property;
1038
+ }
1039
+ for (const name of resolved.required ?? []) {
1040
+ required.add(name);
1041
+ }
1042
+ }
1043
+ return {
1044
+ kind: "mergeable",
1045
+ properties,
1046
+ required: [...required]
1047
+ };
1048
+ }
1049
+ function resolveBranch(document, branch, seenRefs) {
1050
+ const resolved = branch.$ref ? resolveRef(document, branch.$ref, seenRefs) : branch;
1051
+ if (!resolved) return null;
1052
+ if (!resolved.allOf) return resolved;
1053
+ const merged = mergeSimpleAllOfSchema(document, resolved);
1054
+ return merged ?? resolved;
1055
+ }
1056
+ function resolveRef(document, ref, seenRefs) {
1057
+ if (seenRefs.has(ref)) return null;
1058
+ seenRefs.add(ref);
1059
+ const name = ref.startsWith("#/components/schemas/") ? ref.split("/").at(-1) : null;
1060
+ const schema = name ? document.components?.schemas?.[name] : null;
1061
+ if (!schema) return null;
1062
+ return schema.$ref || schema.allOf ? resolveBranch(document, schema, seenRefs) : schema;
1063
+ }
1064
+ function isObjectLikeSchema(schema) {
1065
+ return schema.type === "object" || Boolean(schema.properties);
1066
+ }
1067
+ function isAnnotationOnlySchema(schema) {
1068
+ const annotationKeys = /* @__PURE__ */ new Set(["default", "description", "deprecated", "example", "examples", "nullable", "title"]);
1069
+ return Object.keys(schema).length > 0 && Object.keys(schema).every((key) => annotationKeys.has(key));
1070
+ }
1071
+ function areCompatibleProperties(left, right) {
1072
+ return left.type === right.type && left.format === right.format && Boolean(left.nullable) === Boolean(right.nullable) && JSON.stringify(left.enum ?? null) === JSON.stringify(right.enum ?? null);
1073
+ }
1074
+ var identifierPattern = /^[A-Za-z_$][\w$]*$/;
1075
+ var reservedWords = /* @__PURE__ */ new Set([
1076
+ "abstract",
1077
+ "any",
1078
+ "as",
1079
+ "async",
1080
+ "await",
1081
+ "boolean",
1082
+ "break",
1083
+ "case",
1084
+ "catch",
1085
+ "class",
1086
+ "const",
1087
+ "constructor",
1088
+ "continue",
1089
+ "debugger",
1090
+ "declare",
1091
+ "default",
1092
+ "delete",
1093
+ "do",
1094
+ "else",
1095
+ "enum",
1096
+ "export",
1097
+ "extends",
1098
+ "false",
1099
+ "finally",
1100
+ "for",
1101
+ "from",
1102
+ "function",
1103
+ "get",
1104
+ "if",
1105
+ "implements",
1106
+ "import",
1107
+ "in",
1108
+ "infer",
1109
+ "instanceof",
1110
+ "interface",
1111
+ "is",
1112
+ "keyof",
1113
+ "let",
1114
+ "module",
1115
+ "namespace",
1116
+ "never",
1117
+ "new",
1118
+ "null",
1119
+ "number",
1120
+ "object",
1121
+ "of",
1122
+ "package",
1123
+ "private",
1124
+ "protected",
1125
+ "public",
1126
+ "readonly",
1127
+ "require",
1128
+ "return",
1129
+ "set",
1130
+ "static",
1131
+ "string",
1132
+ "super",
1133
+ "switch",
1134
+ "symbol",
1135
+ "this",
1136
+ "throw",
1137
+ "true",
1138
+ "try",
1139
+ "type",
1140
+ "typeof",
1141
+ "undefined",
1142
+ "unique",
1143
+ "unknown",
1144
+ "var",
1145
+ "void",
1146
+ "while",
1147
+ "with",
1148
+ "yield"
1149
+ ]);
1150
+ var cyrillicMap = {
1151
+ \u0430: "a",
1152
+ \u0431: "b",
1153
+ \u0432: "v",
1154
+ \u0433: "g",
1155
+ \u0434: "d",
1156
+ \u0435: "e",
1157
+ \u0451: "e",
1158
+ \u0436: "zh",
1159
+ \u0437: "z",
1160
+ \u0438: "i",
1161
+ \u0439: "y",
1162
+ \u043A: "k",
1163
+ \u043B: "l",
1164
+ \u043C: "m",
1165
+ \u043D: "n",
1166
+ \u043E: "o",
1167
+ \u043F: "p",
1168
+ \u0440: "r",
1169
+ \u0441: "s",
1170
+ \u0442: "t",
1171
+ \u0443: "u",
1172
+ \u0444: "f",
1173
+ \u0445: "h",
1174
+ \u0446: "c",
1175
+ \u0447: "ch",
1176
+ \u0448: "sh",
1177
+ \u0449: "sch",
1178
+ \u044A: "",
1179
+ \u044B: "y",
1180
+ \u044C: "",
1181
+ \u044D: "e",
1182
+ \u044E: "yu",
1183
+ \u044F: "ya"
1184
+ };
1185
+ function toSafeIdentifier(value, fallback = "value") {
1186
+ const parts = transliterate(value).trim().split(/[^A-Za-z0-9_$]+/).filter(Boolean);
1187
+ const identifier = parts.length === 0 ? fallback : parts.map((part, index) => {
1188
+ const cleaned = part.replace(/[^\w$]/g, "");
1189
+ if (!cleaned) return "";
1190
+ if (index === 0) return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
1191
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
1192
+ }).join("");
1193
+ const safe = ensureIdentifierStart(identifier || fallback);
1194
+ return avoidReservedWord(identifierPattern.test(safe) ? safe : ensureIdentifierStart(fallback));
1195
+ }
1196
+ function toSafeTypeName(value, fallback = "GeneratedType") {
1197
+ const identifier = toSafeIdentifier(value, fallback);
1198
+ const pascal = identifier.charAt(0).toUpperCase() + identifier.slice(1);
1199
+ return avoidReservedWord(identifierPattern.test(pascal) ? pascal : toSafeIdentifier(fallback, "GeneratedType"));
1200
+ }
1201
+ function toSafeFileName(value, fallback = "file") {
1202
+ return toSafeIdentifier(value, fallback);
1203
+ }
1204
+ function pluralizeTypeName(value) {
1205
+ if (/(s|x|z|ch|sh)$/i.test(value)) return `${value}es`;
1206
+ if (/[^aeiou]y$/i.test(value)) return `${value.slice(0, -1)}ies`;
1207
+ return `${value}s`;
1208
+ }
1209
+ function createIdentifierRegistry() {
1210
+ const used = /* @__PURE__ */ new Set();
1211
+ const unique = (name) => {
1212
+ let candidate = name;
1213
+ let index = 2;
1214
+ while (used.has(candidate)) {
1215
+ candidate = `${name}${index}`;
1216
+ index += 1;
1217
+ }
1218
+ used.add(candidate);
1219
+ return candidate;
1220
+ };
1221
+ return {
1222
+ identifier: (value, fallback) => unique(toSafeIdentifier(value, fallback)),
1223
+ typeName: (value, fallback) => unique(toSafeTypeName(value, fallback)),
1224
+ fileName: (value, fallback) => unique(toSafeFileName(value, fallback))
1225
+ };
1226
+ }
1227
+ function quoteObjectKeyIfNeeded(value) {
1228
+ return identifierPattern.test(value) ? value : `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1229
+ }
1230
+ function ensureIdentifierStart(value) {
1231
+ if (!value) return "_";
1232
+ return /^[A-Za-z_$]/.test(value) ? value : `_${value}`;
1233
+ }
1234
+ function avoidReservedWord(value) {
1235
+ return reservedWords.has(value) ? `${value}Value` : value;
1236
+ }
1237
+ function transliterate(value) {
1238
+ return [...value].map((char) => {
1239
+ const lower = char.toLowerCase();
1240
+ const transliterated = cyrillicMap[lower];
1241
+ if (transliterated == null) return char;
1242
+ return char === lower ? transliterated : capitalize(transliterated);
1243
+ }).join("");
1244
+ }
1245
+ function capitalize(value) {
1246
+ return value.charAt(0).toUpperCase() + value.slice(1);
1247
+ }
1248
+ function createResourceTypeNames(resource) {
1249
+ const entity = toSafeTypeName(resource.entity);
1250
+ const collection = pluralizeTypeName(entity);
1251
+ return {
1252
+ idType: `${entity}Id`,
1253
+ detailResponseType: `${entity}DetailResponse`,
1254
+ listParamsType: `${collection}ListParams`,
1255
+ listResponseType: `${collection}ListResponse`,
1256
+ createRequestType: `Create${entity}Request`,
1257
+ createResponseType: `Create${entity}Response`,
1258
+ updateRequestType: `Update${entity}Request`,
1259
+ updateResponseType: `Update${entity}Response`,
1260
+ entityType: entity
1261
+ };
1262
+ }
1263
+ function createOperationTypeNames(operation) {
1264
+ const operationName = toSafeTypeName(operation.id ?? `${operation.method}Operation`);
1265
+ return {
1266
+ paramsType: `${operationName}OperationParams`,
1267
+ requestType: `${operationName}OperationRequest`,
1268
+ responseType: `${operationName}OperationResponse`
1269
+ };
1270
+ }
1271
+ function createTypeScriptTypes(normalized, resource) {
1272
+ const declarations = [];
1273
+ if (!normalized.schemas.some((schema) => toSafeTypeName(schema.name) === toSafeTypeName(resource.entity))) {
1274
+ declarations.push(`export interface ${toSafeTypeName(resource.entity)} {
1275
+ [key: string]: unknown
1276
+ }`);
1277
+ }
1278
+ declarations.push(createOperationAliases(normalized, resource));
1279
+ const body = declarations.filter(Boolean).join("\n\n");
1280
+ const imports = collectSharedTypeImports(normalized, body);
1281
+ const importBlock = imports.length > 0 ? `import type { ${imports.join(", ")} } from '../components.types'
1282
+
1283
+ export type { ${imports.join(", ")} } from '../components.types'
1284
+
1285
+ ` : "";
1286
+ return `${importBlock}${body}
1287
+ `;
1288
+ }
1289
+ function createSharedSchemaTypes(normalized) {
1290
+ const declarations = [];
1291
+ const enumAliases = /* @__PURE__ */ new Map();
1292
+ for (const schema of normalized.schemas) {
1293
+ declarations.push(createSchemaDeclaration(normalized, toSafeTypeName(schema.name), schema.schema, enumAliases));
1294
+ }
1295
+ declarations.unshift(...[...enumAliases.entries()].map(([name, value]) => `export type ${name} = ${value}`));
1296
+ return declarations.length > 0 ? `${declarations.filter(Boolean).join("\n\n")}
1297
+ ` : "export {}\n";
1298
+ }
1299
+ function schemaToTypeScript(normalized, schema, options = { mode: "response" }) {
1300
+ if (!schema) return "unknown";
1301
+ if (schema.$ref) return toSafeTypeName(schema.$ref.split("/").at(-1) ?? "unknown");
1302
+ const unwrappedAllOf = unwrapAnnotationOnlyAllOfSchema(schema);
1303
+ if (unwrappedAllOf) return schemaToTypeScript(normalized, unwrappedAllOf, options);
1304
+ if (Array.isArray(schema.type)) {
1305
+ const nonNullTypes = schema.type.filter((type) => type !== "null");
1306
+ if (nonNullTypes.length !== 1) return "unknown";
1307
+ const base = schemaToTypeScript(normalized, { ...schema, type: nonNullTypes[0] }, options);
1308
+ return schema.type.includes("null") ? `${base} | null` : base;
1309
+ }
1310
+ if (schema.oneOf?.length || schema.anyOf?.length) {
1311
+ const branches = schema.oneOf ?? schema.anyOf ?? [];
1312
+ const discriminator = getDiscriminatorInfo(schema);
1313
+ return branches.map((branch) => renderUnionBranch(normalized, branch, options, discriminator)).join(" | ");
1314
+ }
1315
+ if (hasConst(schema)) return enumValueToTypeScript(schema.const);
1316
+ if (schema.enum) return schema.enum.map(enumValueToTypeScript).join(" | ");
1317
+ if (schema.type === "string" && schema.format === "binary") return options.mode === "request" || options.indent !== void 0 ? "Blob | File" : "Blob";
1318
+ if (schema.type === "string") return "string";
1319
+ if (schema.type === "number" || schema.type === "integer") return "number";
1320
+ if (schema.type === "boolean") return "boolean";
1321
+ if (schema.type === "array") return `${toArrayElementType(schemaToTypeScript(normalized, schema.items, options))}[]`;
1322
+ if (isPureDictionarySchema(schema)) return createDictionaryType(normalized, schema.additionalProperties, options);
1323
+ if (schema.type === "object" && !schema.properties) return "Record<string, unknown>";
1324
+ if (schema.type === "object" || schema.properties) {
1325
+ const indent = options.indent ?? 0;
1326
+ const childIndent = " ".repeat(indent + 2);
1327
+ const closeIndent = " ".repeat(indent);
1328
+ const required = new Set(schema.required ?? []);
1329
+ const lines = Object.entries(schema.properties ?? {}).filter(([, property]) => options.mode === "request" ? !property.readOnly : !property.writeOnly).map(([name, property]) => {
1330
+ const optional = required.has(name) ? "" : "?";
1331
+ const nullable = property.nullable ? " | null" : "";
1332
+ return `${childIndent}${quoteObjectKeyIfNeeded(name)}${optional}: ${schemaToTypeScript(normalized, property, {
1333
+ ...options,
1334
+ indent: indent + 2
1335
+ })}${nullable}`;
1336
+ });
1337
+ const indexSignature = createAdditionalPropertiesIndex(normalized, schema, options, childIndent, required);
1338
+ if (indexSignature) lines.push(indexSignature);
1339
+ return `{
1340
+ ${lines.join("\n")}
1341
+ ${closeIndent}}`;
1342
+ }
1343
+ return "unknown";
1344
+ }
1345
+ function getDiscriminatorInfo(schema) {
1346
+ const raw = schema.discriminator;
1347
+ if (!raw || typeof raw !== "object") return null;
1348
+ const propertyName = raw.propertyName;
1349
+ if (typeof propertyName !== "string" || propertyName.length === 0) return null;
1350
+ const mapping = {};
1351
+ const rawMapping = raw.mapping;
1352
+ if (rawMapping && typeof rawMapping === "object") {
1353
+ for (const [key, value] of Object.entries(rawMapping)) {
1354
+ if (typeof value === "string") mapping[key] = value;
1355
+ }
1356
+ }
1357
+ return { propertyName, mapping };
1358
+ }
1359
+ function renderUnionBranch(normalized, branch, options, discriminator) {
1360
+ const type = schemaToTypeScript(normalized, branch, options);
1361
+ if (!discriminator || type === "unknown" || !isObjectSchemaBranch(normalized, branch)) return type;
1362
+ const literal = discriminatorLiteral(discriminator, branch);
1363
+ if (literal === null) return type;
1364
+ return `(${type} & { ${quoteObjectKeyIfNeeded(discriminator.propertyName)}: ${JSON.stringify(literal)} })`;
1365
+ }
1366
+ function discriminatorLiteral(discriminator, branch) {
1367
+ if (!branch.$ref) return null;
1368
+ const refName = branch.$ref.split("/").at(-1) ?? "";
1369
+ for (const [key, target] of Object.entries(discriminator.mapping)) {
1370
+ if (target === branch.$ref || target.split("/").at(-1) === refName) return key;
1371
+ }
1372
+ return refName.length > 0 ? refName : null;
1373
+ }
1374
+ function isObjectSchemaBranch(normalized, branch) {
1375
+ const resolved = branch.$ref ? resolveSchema(normalized, branch) : branch;
1376
+ if (!resolved) return false;
1377
+ return resolved.type === "object" || Boolean(resolved.properties) || Boolean(resolved.allOf);
1378
+ }
1379
+ function isSupportedUnion(normalized, schema, branches) {
1380
+ if (branches.length === 0) return false;
1381
+ const renderable = branches.every(
1382
+ (branch) => schemaToTypeScript(normalized, branch, { mode: "response" }) !== "unknown"
1383
+ );
1384
+ if (!renderable) return false;
1385
+ if (getDiscriminatorInfo(schema))
1386
+ return branches.every((branch) => isObjectSchemaBranch(normalized, branch));
1387
+ return true;
1388
+ }
1389
+ function getPathParams(operation) {
1390
+ return (operation?.parameters ?? []).filter((parameter) => parameter.in === "path");
1391
+ }
1392
+ function getQueryParams(operation) {
1393
+ return (operation?.parameters ?? []).filter((parameter) => parameter.in === "query");
1394
+ }
1395
+ function getHeaderParams(operation) {
1396
+ return (operation?.parameters ?? []).filter((parameter) => parameter.in === "header");
1397
+ }
1398
+ function getOperationParams(operation) {
1399
+ return (operation?.parameters ?? []).filter((parameter) => parameter.in === "path" || parameter.in === "query" || parameter.in === "header");
1400
+ }
1401
+ function getCollectionParams(resource) {
1402
+ const params = [
1403
+ ...getPathParams(resource.operations.list),
1404
+ ...getQueryParams(resource.operations.list),
1405
+ ...getPathParams(resource.operations.create)
1406
+ ];
1407
+ const seen = /* @__PURE__ */ new Set();
1408
+ return params.filter((param) => {
1409
+ const key = `${param.in}:${param.name}`;
1410
+ if (seen.has(key)) return false;
1411
+ seen.add(key);
1412
+ return true;
1413
+ });
1414
+ }
1415
+ function getIdentityParams(resource) {
1416
+ return [resource.operations.detail, resource.operations.update, resource.operations.delete].map((operation) => getPathParams(operation)).find((params) => params.length > 0) ?? [];
1417
+ }
1418
+ function resolveSchema(normalized, schema) {
1419
+ if (!schema) return null;
1420
+ if (!schema.$ref) return schema;
1421
+ const name = schema.$ref.split("/").at(-1);
1422
+ return normalized.schemas.find((candidate) => candidate.name === name || toSafeTypeName(candidate.name) === toSafeTypeName(name ?? ""))?.schema ?? schema;
1423
+ }
1424
+ function resolveSchemaName(schema) {
1425
+ if (!schema?.$ref) return null;
1426
+ return toSafeTypeName(schema.$ref.split("/").at(-1) ?? "");
1427
+ }
1428
+ function createSchemaDeclaration(normalized, name, schema, enumAliases) {
1429
+ if (schema.oneOf?.length || schema.anyOf?.length) {
1430
+ return `export type ${name} = ${schemaToTypeScript(normalized, schema, { mode: "response" })}`;
1431
+ }
1432
+ if (isPureDictionarySchema(schema)) {
1433
+ return `export type ${name} = ${createDictionaryType(normalized, schema.additionalProperties, { mode: "response" })}`;
1434
+ }
1435
+ const mode = name.endsWith("Dto") ? "request" : "response";
1436
+ const required = new Set(schema.required ?? []);
1437
+ const lines = Object.entries(schema.properties ?? {}).filter(([, property]) => mode === "request" ? !property.readOnly : !property.writeOnly).map(([propertyName, property]) => {
1438
+ const enumName = property.enum ? `${name}${toSafeTypeName(propertyName)}` : void 0;
1439
+ if (enumName && property.enum) {
1440
+ enumAliases.set(enumName, property.enum.map(enumValueToTypeScript).join(" | "));
1441
+ }
1442
+ const optional = required.has(propertyName) ? "" : "?";
1443
+ const baseType = enumName ?? schemaToTypeScript(normalized, property, { mode, indent: 2 });
1444
+ const nullable = property.nullable ? " | null" : "";
1445
+ return ` ${quoteObjectKeyIfNeeded(propertyName)}${optional}: ${baseType}${nullable}`;
1446
+ });
1447
+ const indexSignature = createAdditionalPropertiesIndex(normalized, schema, { mode, indent: 0 }, " ", required);
1448
+ if (indexSignature) lines.push(indexSignature);
1449
+ return `export interface ${name} {
1450
+ ${lines.join("\n")}
1451
+ }`;
1452
+ }
1453
+ function isPureDictionarySchema(schema) {
1454
+ return Boolean(
1455
+ (schema.type === "object" || schema.additionalProperties !== void 0) && schema.additionalProperties !== void 0 && Object.keys(schema.properties ?? {}).length === 0
1456
+ );
1457
+ }
1458
+ function createDictionaryType(normalized, additionalProperties, options) {
1459
+ if (additionalProperties === false) return "Record<string, never>";
1460
+ if (additionalProperties === true || additionalProperties === void 0) return "Record<string, unknown>";
1461
+ return `Record<string, ${schemaToTypeScript(normalized, additionalProperties, options)}>`;
1462
+ }
1463
+ function createAdditionalPropertiesIndex(normalized, schema, options, childIndent, required) {
1464
+ if (schema.additionalProperties === void 0 || schema.additionalProperties === false || isPureDictionarySchema(schema)) return null;
1465
+ const valueTypes = /* @__PURE__ */ new Set();
1466
+ if (schema.additionalProperties === true) {
1467
+ valueTypes.add("unknown");
1468
+ } else {
1469
+ valueTypes.add(schemaToTypeScript(normalized, schema.additionalProperties, options));
1470
+ }
1471
+ for (const [propertyName, property] of Object.entries(schema.properties ?? {})) {
1472
+ if (options.mode === "request" && property.readOnly) continue;
1473
+ if (options.mode === "response" && property.writeOnly) continue;
1474
+ valueTypes.add(schemaToTypeScript(normalized, property, options));
1475
+ if (property.nullable) valueTypes.add("null");
1476
+ if (!required.has(propertyName)) valueTypes.add("undefined");
1477
+ }
1478
+ return `${childIndent}[key: string]: ${[...valueTypes].join(" | ")}`;
1479
+ }
1480
+ function toArrayElementType(type) {
1481
+ return type.includes(" | ") ? `(${type})` : type;
1482
+ }
1483
+ function enumValueToTypeScript(value) {
1484
+ if (typeof value === "string") return JSON.stringify(value);
1485
+ if (value === null) return "null";
1486
+ return String(value);
1487
+ }
1488
+ function hasConst(schema) {
1489
+ return Object.hasOwn(schema, "const");
1490
+ }
1491
+ function createOperationAliases(normalized, resource) {
1492
+ const names = createResourceTypeNames(resource);
1493
+ const identityParams = getIdentityParams(resource);
1494
+ const idDeclaration = identityParams.length > 1 ? createParamsInterface(normalized, names.idType, identityParams) : `export type ${names.idType} = ${identityParams[0]?.schema ? schemaToTypeScript(normalized, identityParams[0].schema, { mode: "request" }) : "string"}`;
1495
+ const listParams = createParamsInterface(normalized, names.listParamsType, getCollectionParams(resource));
1496
+ const listResponse = createResponseDeclaration(normalized, names.listResponseType, resource.operations.list);
1497
+ const detailResponse = createResponseType(normalized, resource.operations.detail, names.entityType);
1498
+ const createRequest = createRequestBodyType(normalized, resource.operations.create) ?? `Partial<${names.entityType}>`;
1499
+ const updateRequest = createRequestBodyType(normalized, resource.operations.update) ?? `Partial<${names.entityType}>`;
1500
+ const createResponse = createResponseType(normalized, resource.operations.create, names.entityType);
1501
+ const updateResponse = createResponseType(normalized, resource.operations.update, names.entityType);
1502
+ const operationAliases = resource.operationsList.filter((operation) => operation.operationKind !== "unsupported-operation" && !Object.values(resource.operations).includes(operation)).map((operation) => createOperationAlias(normalized, operation));
1503
+ return [
1504
+ idDeclaration,
1505
+ `export type ${names.detailResponseType} = ${detailResponse}`,
1506
+ listParams,
1507
+ listResponse,
1508
+ `export type ${names.createRequestType} = ${createRequest}`,
1509
+ `export type ${names.createResponseType} = ${createResponse}`,
1510
+ `export type ${names.updateRequestType} = ${updateRequest}`,
1511
+ `export type ${names.updateResponseType} = ${updateResponse}`,
1512
+ ...operationAliases
1513
+ ].filter(Boolean).join("\n\n");
1514
+ }
1515
+ function createOperationAlias(normalized, operation) {
1516
+ const names = createOperationTypeNames(operation);
1517
+ const params = createParamsInterface(normalized, names.paramsType, getOperationParams(operation));
1518
+ const request = createRequestBodyType(normalized, operation) ?? "void";
1519
+ const response = createResponseType(normalized, operation, "unknown");
1520
+ return [params, `export type ${names.requestType} = ${request}`, `export type ${names.responseType} = ${response}`].join("\n\n");
1521
+ }
1522
+ function createRequestBodyType(normalized, operation) {
1523
+ if (!operation?.requestBodySchema) return null;
1524
+ if (operation.requestContentTypes.some(isMultipartContentType)) return "FormData";
1525
+ if (operation.requestContentTypes.some(isFormUrlEncodedContentType)) return "URLSearchParams";
1526
+ if (operation.requestContentTypes.some(isBinaryContentType)) return "Blob | ArrayBuffer | ReadableStream";
1527
+ return resolveSchemaName(operation.requestBodySchema) ?? schemaToTypeScript(normalized, operation.requestBodySchema, { mode: "request" });
1528
+ }
1529
+ function isMultipartContentType(contentType) {
1530
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1531
+ return normalized === "multipart/form-data";
1532
+ }
1533
+ function isFormUrlEncodedContentType(contentType) {
1534
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1535
+ return normalized === "application/x-www-form-urlencoded";
1536
+ }
1537
+ function isBinaryContentType(contentType) {
1538
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1539
+ return normalized === "application/octet-stream" || normalized === "application/pdf" || normalized.startsWith("image/");
1540
+ }
1541
+ function createParamsInterface(normalized, name, params) {
1542
+ if (params.length === 0) {
1543
+ return `export type ${name} = Record<string, never>`;
1544
+ }
1545
+ const lines = params.map((param) => {
1546
+ const optional = param.required ? "" : "?";
1547
+ return ` ${quoteObjectKeyIfNeeded(param.name)}${optional}: ${schemaToTypeScript(normalized, param.schema, { mode: "request" })}`;
1548
+ });
1549
+ return `export interface ${name} {
1550
+ ${lines.join("\n")}
1551
+ }`;
1552
+ }
1553
+ function createResponseDeclaration(normalized, name, operation) {
1554
+ if (operation?.responseBodyEmpty) return `export type ${name} = void`;
1555
+ const schema = operation?.responseSchema;
1556
+ const schemaName = resolveSchemaName(schema);
1557
+ if (schemaName) return `export type ${name} = ${schemaName}`;
1558
+ if (!schema) return `export type ${name} = unknown`;
1559
+ const type = schemaToTypeScript(normalized, schema, { mode: "response" });
1560
+ return type.startsWith("{") ? `export interface ${name} ${type}` : `export type ${name} = ${type}`;
1561
+ }
1562
+ function createResponseType(normalized, operation, fallback) {
1563
+ if (operation?.responseBodyEmpty) return "void";
1564
+ const schema = operation?.responseSchema;
1565
+ return resolveSchemaName(schema) ?? (schema ? schemaToTypeScript(normalized, schema, { mode: "response" }) : fallback);
1566
+ }
1567
+ function collectSharedTypeImports(normalized, content) {
1568
+ return normalized.schemas.map((schema) => toSafeTypeName(schema.name)).filter((name, index, names) => names.indexOf(name) === index).filter((name) => new RegExp(`\\b${escapeRegExp2(name)}\\b`).test(content)).sort((left, right) => left.localeCompare(right));
1569
+ }
1570
+ function escapeRegExp2(value) {
1571
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1572
+ }
1573
+ function normalizePlugins(plugins) {
1574
+ return (plugins ?? []).filter(isForgePlugin);
1575
+ }
1576
+ async function runPluginArtifactHooks(input) {
1577
+ const artifacts = [];
1578
+ for (const plugin of normalizePlugins(input.plugins)) {
1579
+ if (!plugin.generateArtifacts) continue;
1580
+ try {
1581
+ artifacts.push(...await plugin.generateArtifacts({ normalized: input.normalized, cwd: input.cwd }));
1582
+ } catch (error) {
1583
+ throw new Error(
1584
+ `Plugin "${plugin.name}" failed during generateArtifacts: ${error instanceof Error ? error.message : String(error)}`
1585
+ );
1586
+ }
1587
+ }
1588
+ return artifacts;
1589
+ }
1590
+ async function runPluginAfterGenerateHooks(input) {
1591
+ for (const plugin of normalizePlugins(input.plugins)) {
1592
+ if (!plugin.afterGenerate) continue;
1593
+ await plugin.afterGenerate(input.files);
1594
+ }
1595
+ }
1596
+ function runPluginDiagnostics(input) {
1597
+ const diagnostics = [];
1598
+ for (const plugin of normalizePlugins(input.plugins)) {
1599
+ if (!plugin.diagnostics) continue;
1600
+ try {
1601
+ const result2 = plugin.diagnostics(input.normalized);
1602
+ if (Array.isArray(result2)) diagnostics.push(...result2);
1603
+ } catch (error) {
1604
+ diagnostics.push({
1605
+ severity: "error",
1606
+ code: "plugin-diagnostic-error",
1607
+ message: `Plugin "${plugin.name}" failed while collecting diagnostics.`,
1608
+ location: `plugins.${plugin.name}`,
1609
+ suggestion: error instanceof Error ? error.message : "Inspect the plugin diagnostics hook."
1610
+ });
1611
+ }
1612
+ }
1613
+ return diagnostics;
1614
+ }
1615
+ function isForgePlugin(value) {
1616
+ return typeof value === "object" && value !== null && "name" in value && typeof value.name === "string";
1617
+ }
1618
+ function collectDiagnostics(normalized, options = {}) {
1619
+ const diagnostics = [];
1620
+ const groupedParameters = /* @__PURE__ */ new Map();
1621
+ const unsupportedSecuritySchemes = Object.entries(normalized.document.components?.securitySchemes ?? {}).filter(
1622
+ ([, scheme]) => !isRuntimeSupportedSecurityScheme(scheme)
1623
+ );
1624
+ if (unsupportedSecuritySchemes.length > 0) {
1625
+ diagnostics.push({
1626
+ severity: "warning",
1627
+ code: "unsupported-security-schemes",
1628
+ message: "Unsupported security schemes are detected.",
1629
+ location: unsupportedSecuritySchemes.map(([name]) => `components.securitySchemes.${name}`).join(", "),
1630
+ suggestion: "Configure auth headers through the runtime client until security scheme generation is supported."
1631
+ });
1632
+ }
1633
+ for (const operation of normalized.operations) {
1634
+ const location = `${operation.method.toUpperCase()} ${operation.path}`;
1635
+ const requestContentTypes = getContentTypes(operation.operation.requestBody);
1636
+ const responseContentTypes = Object.values(operation.operation.responses ?? {}).flatMap((response) => Object.keys(response.content ?? {}));
1637
+ if (operation.operationKind === "unsupported-operation") {
1638
+ diagnostics.push({
1639
+ severity: "warning",
1640
+ code: "unsupported-operation",
1641
+ message: `${location} is preserved but is not generated as an interactive resource yet.`,
1642
+ location,
1643
+ impact: "Generated output uses an explicit unsupported operation panel instead of silently dropping the operation.",
1644
+ suggestion: "Use a supported HTTP method and JSON-compatible request/response schemas, or implement this endpoint with custom code."
1645
+ });
1646
+ }
1647
+ if (operation.operation.requestBody && !operation.requestBodySchema) {
1648
+ diagnostics.push({
1649
+ severity: "warning",
1650
+ code: requestContentTypes.some((type) => !isJsonCompatibleContentType(type)) ? "unsupported-content-type" : "missing-request-schema",
1651
+ message: `${location} has no supported JSON request schema. Generated request type will use an explicit fallback.`,
1652
+ location,
1653
+ impact: "Request payload typing may fall back to an explicit generic type.",
1654
+ suggestion: "Add an application/json request body schema to improve generated request types."
1655
+ });
1656
+ }
1657
+ if (!operation.responseSchema && !operation.responseBodyEmpty && operation.method !== "delete") {
1658
+ diagnostics.push({
1659
+ severity: "warning",
1660
+ code: responseContentTypes.some((type) => !isJsonCompatibleContentType(type)) ? "unsupported-content-type" : "missing-response-schema",
1661
+ message: `${location} has no 2xx JSON response schema. Generated response type will be unknown.`,
1662
+ location,
1663
+ impact: "Response typing and generated UI metadata may be incomplete.",
1664
+ suggestion: "Add an application/json 2xx response schema to improve generated response types."
1665
+ });
1666
+ }
1667
+ for (const parameter of operation.parameters) {
1668
+ if (parameter.in === "header" || parameter.in === "cookie") {
1669
+ if (parameter.in === "header" && parameter.name.toLowerCase() === "authorization") {
1670
+ continue;
1671
+ }
1672
+ if (parameter.in === "header" && operation.operationKind !== "crud-resource" && parameter.name.toLowerCase() !== "authorization") {
1673
+ continue;
1674
+ }
1675
+ if (parameter.in === "cookie" && parameter.name.toLowerCase() === "authorization") {
1676
+ const key = `${parameter.in}:${parameter.name.toLowerCase()}`;
1677
+ const grouped = groupedParameters.get(key) ?? { in: parameter.in, name: parameter.name, locations: [] };
1678
+ grouped.locations.push(location);
1679
+ groupedParameters.set(key, grouped);
1680
+ continue;
1681
+ }
1682
+ diagnostics.push({
1683
+ severity: "warning",
1684
+ code: `unsupported-${parameter.in}-parameter`,
1685
+ message: `${location} uses an unsupported ${parameter.in} parameter "${parameter.name}".`,
1686
+ location,
1687
+ impact: "Generated client signatures do not include this parameter yet.",
1688
+ suggestion: "Model this value through runtime headers until header/cookie parameter generation is supported."
1689
+ });
1690
+ }
1691
+ }
1692
+ if (operation.operation.security) {
1693
+ diagnostics.push({
1694
+ severity: "warning",
1695
+ code: "unsupported-operation-security",
1696
+ message: `${location} defines operation-level security that is not generated yet.`,
1697
+ location,
1698
+ impact: "Generated client calls require runtime auth configuration.",
1699
+ suggestion: "Configure auth through createApiClient headers/getHeaders for now."
1700
+ });
1701
+ }
1702
+ }
1703
+ for (const grouped of groupedParameters.values()) {
1704
+ diagnostics.push({
1705
+ severity: "warning",
1706
+ code: `unsupported-${grouped.in}-parameter`,
1707
+ message: `${grouped.locations.length} operations use unsupported ${grouped.in} parameter "${grouped.name}".`,
1708
+ location: grouped.locations.slice(0, 3).join(", ") + (grouped.locations.length > 3 ? ", ..." : ""),
1709
+ impact: "Repeated auth/header parameters are grouped to reduce diagnostic noise.",
1710
+ suggestion: "Model this value through runtime headers until header/cookie parameter generation is supported."
1711
+ });
1712
+ }
1713
+ const schemaDiagnostics = [];
1714
+ for (const schema of normalized.schemas) {
1715
+ const originalSchema = normalized.document.components?.schemas?.[schema.name] ?? schema.schema;
1716
+ collectSchemaDiagnostics(normalized, originalSchema, `#/components/schemas/${schema.name}`, schemaDiagnostics);
1717
+ }
1718
+ diagnostics.push(...schemaDiagnostics);
1719
+ diagnostics.push(...runPluginDiagnostics({ plugins: options.plugins, normalized }));
1720
+ return diagnostics;
1721
+ }
1722
+ function collectSchemaDiagnostics(normalized, schema, location, diagnostics) {
1723
+ if (schema.allOf) {
1724
+ const unwrapped = unwrapAnnotationOnlyAllOfSchema(schema);
1725
+ const analysis = unwrapped ? null : analyzeAllOfSchema(normalized.document, schema);
1726
+ if (!analysis) {
1727
+ } else if (analysis.kind === "mergeable") {
1728
+ } else if (analysis.kind === "conflicting") {
1729
+ diagnostics.push({
1730
+ severity: "error",
1731
+ code: "conflicting-allof",
1732
+ message: `${location} has conflicting allOf branches for property "${analysis.property}".`,
1733
+ location,
1734
+ suggestion: analysis.reason
1735
+ });
1736
+ } else {
1737
+ diagnostics.push({
1738
+ severity: "warning",
1739
+ code: "unsupported-allof",
1740
+ message: `${location} uses allOf, which is reported but not deeply modeled yet.`,
1741
+ location,
1742
+ suggestion: analysis.reason
1743
+ });
1744
+ }
1745
+ }
1746
+ for (const key of ["oneOf", "anyOf"]) {
1747
+ if (!schema[key]) continue;
1748
+ if (isSupportedUnion(normalized, schema, schema[key])) continue;
1749
+ const branches = schema[key].map((_, index) => `${location}/${key}/${index}`).join(", ");
1750
+ diagnostics.push({
1751
+ severity: "warning",
1752
+ code: `unsupported-${key.toLowerCase()}`,
1753
+ message: `${location} uses ${key}, which is reported but not deeply modeled yet.`,
1754
+ location,
1755
+ impact: schema.discriminator ? `Generated types may fall back to a broader shape for discriminator-based branches: ${branches}.` : `Generated types may fall back to a broader shape for branches: ${branches}.`,
1756
+ suggestion: schema.discriminator ? "Discriminator is present; validate generated fallback types carefully or prefer an explicit non-polymorphic response shape for v1 generation." : "Prefer a concrete object schema for v1 generation, or validate generated fallback types carefully."
1757
+ });
1758
+ }
1759
+ if (schema.discriminator && !isSupportedDiscriminatedUnion(normalized, schema)) {
1760
+ diagnostics.push({
1761
+ severity: "warning",
1762
+ code: "unsupported-discriminator",
1763
+ message: `${location} uses discriminator, which is not modeled yet.`,
1764
+ location,
1765
+ impact: "Generated union narrowing will not reflect discriminator semantics yet.",
1766
+ suggestion: "Avoid discriminator-dependent generation until composition support is expanded."
1767
+ });
1768
+ }
1769
+ for (const [name, property] of Object.entries(schema.properties ?? {})) {
1770
+ collectSchemaDiagnostics(normalized, property, `${location}/properties/${name}`, diagnostics);
1771
+ }
1772
+ if (schema.items) {
1773
+ collectSchemaDiagnostics(normalized, schema.items, `${location}/items`, diagnostics);
1774
+ }
1775
+ }
1776
+ function isSupportedDiscriminatedUnion(normalized, schema) {
1777
+ const branches = schema.oneOf ?? schema.anyOf;
1778
+ return Boolean(branches?.length) && branches.every((branch) => isObjectSchemaBranch(normalized, branch));
1779
+ }
1780
+ function isRuntimeSupportedSecurityScheme(value) {
1781
+ if (!value || typeof value !== "object") return false;
1782
+ const scheme = value;
1783
+ if (scheme.type === "http" && typeof scheme.scheme === "string" && scheme.scheme.toLowerCase() === "bearer") return true;
1784
+ if (scheme.type === "oauth2") return true;
1785
+ return scheme.type === "apiKey" && scheme.in === "header";
1786
+ }
1787
+ function getContentTypes(value) {
1788
+ if (!value || typeof value !== "object" || !("content" in value)) return [];
1789
+ const content = value.content;
1790
+ return content && typeof content === "object" ? Object.keys(content) : [];
1791
+ }
1792
+ function isJsonCompatibleContentType(contentType) {
1793
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1794
+ return normalized === "application/json" || normalized.endsWith("+json");
1795
+ }
1796
+ function createSchemaCoverageMatrix(normalized, diagnostics) {
1797
+ const operations = normalized.operations;
1798
+ const unsupportedConstructs = countUnsupportedSchemaConstructs(normalized);
1799
+ const operationDiagnosticOnly = operations.filter((operation) => operation.operationKind === "unsupported-operation").length;
1800
+ const schemaDiagnosticOnly = Object.values(unsupportedConstructs).reduce((total, count) => total + count, 0);
1801
+ const generated = operations.filter((operation) => operation.operationKind !== "unsupported-operation").length;
1802
+ const fallback = operations.filter(hasFallbackShape).length + diagnostics.filter((diagnostic) => diagnostic.code === "missing-request-schema" || diagnostic.code === "missing-response-schema").length;
1803
+ return {
1804
+ operations: {
1805
+ total: operations.length,
1806
+ generated,
1807
+ diagnosticOnly: operationDiagnosticOnly,
1808
+ byKind: countBy(operations, (operation) => operation.operationKind),
1809
+ byRequestShape: countBy(operations, requestShape),
1810
+ byResponseShape: countBy(operations, responseShape)
1811
+ },
1812
+ schemas: {
1813
+ total: normalized.schemas.length,
1814
+ unsupportedConstructs
1815
+ },
1816
+ cases: {
1817
+ generated,
1818
+ skipped: operationDiagnosticOnly,
1819
+ fallback,
1820
+ diagnosticOnly: operationDiagnosticOnly + schemaDiagnosticOnly
1821
+ }
1822
+ };
1823
+ }
1824
+ function mergeSchemaCoverageMatrices(matrices) {
1825
+ return {
1826
+ operations: {
1827
+ total: sum(matrices, (matrix) => matrix.operations.total),
1828
+ generated: sum(matrices, (matrix) => matrix.operations.generated),
1829
+ diagnosticOnly: sum(matrices, (matrix) => matrix.operations.diagnosticOnly),
1830
+ byKind: mergeRecords(matrices.map((matrix) => matrix.operations.byKind)),
1831
+ byRequestShape: mergeRecords(matrices.map((matrix) => matrix.operations.byRequestShape)),
1832
+ byResponseShape: mergeRecords(matrices.map((matrix) => matrix.operations.byResponseShape))
1833
+ },
1834
+ schemas: {
1835
+ total: sum(matrices, (matrix) => matrix.schemas.total),
1836
+ unsupportedConstructs: mergeRecords(matrices.map((matrix) => matrix.schemas.unsupportedConstructs))
1837
+ },
1838
+ cases: {
1839
+ generated: sum(matrices, (matrix) => matrix.cases.generated),
1840
+ skipped: sum(matrices, (matrix) => matrix.cases.skipped),
1841
+ fallback: sum(matrices, (matrix) => matrix.cases.fallback),
1842
+ diagnosticOnly: sum(matrices, (matrix) => matrix.cases.diagnosticOnly)
1843
+ }
1844
+ };
1845
+ }
1846
+ function requestShape(operation) {
1847
+ if (!operation.operation.requestBody) return "none";
1848
+ if (operation.hasFilePayload) return "binary";
1849
+ if (operation.isJsonRequest) return "json";
1850
+ if (operation.requestContentTypes.some((type) => type.startsWith("text/"))) return "text";
1851
+ if (operation.requestContentTypes.includes("multipart/form-data")) return "multipart";
1852
+ if (operation.requestContentTypes.includes("application/x-www-form-urlencoded")) return "form";
1853
+ return operation.requestBodySchema ? "non-json" : "missing";
1854
+ }
1855
+ function responseShape(operation) {
1856
+ if (operation.responseBodyEmpty) return "empty";
1857
+ if (operation.hasFilePayload) return "binary";
1858
+ if (operation.isJsonResponse) return "json";
1859
+ if (operation.responseContentTypes.some((type) => type.startsWith("text/"))) return "text";
1860
+ return operation.responseSchema ? "non-json" : "missing";
1861
+ }
1862
+ function hasFallbackShape(operation) {
1863
+ if (operation.operationKind === "unsupported-operation") return false;
1864
+ const requestFallback = Boolean(operation.operation.requestBody) && !operation.isJsonRequest && !operation.hasFilePayload;
1865
+ const responseFallback = !operation.responseBodyEmpty && !operation.isJsonResponse && !operation.hasFilePayload && operation.method !== "delete";
1866
+ return requestFallback || responseFallback;
1867
+ }
1868
+ function countUnsupportedSchemaConstructs(normalized) {
1869
+ const counts = {};
1870
+ for (const { schema } of normalized.schemas) {
1871
+ visitSchema(normalized, schema, counts);
1872
+ }
1873
+ return counts;
1874
+ }
1875
+ function visitSchema(normalized, schema, counts) {
1876
+ if (schema.allOf) counts.allOf = (counts.allOf ?? 0) + 1;
1877
+ if (schema.oneOf && !isSupportedUnion(normalized, schema, schema.oneOf)) counts.oneOf = (counts.oneOf ?? 0) + 1;
1878
+ if (schema.anyOf && !isSupportedUnion(normalized, schema, schema.anyOf)) counts.anyOf = (counts.anyOf ?? 0) + 1;
1879
+ const union = schema.oneOf ?? schema.anyOf;
1880
+ if (schema.discriminator && !(union && isSupportedUnion(normalized, schema, union))) counts.discriminator = (counts.discriminator ?? 0) + 1;
1881
+ for (const property of Object.values(schema.properties ?? {})) {
1882
+ visitSchema(normalized, property, counts);
1883
+ }
1884
+ if (schema.items) {
1885
+ visitSchema(normalized, schema.items, counts);
1886
+ }
1887
+ for (const branch of [...schema.allOf ?? [], ...schema.oneOf ?? [], ...schema.anyOf ?? []]) {
1888
+ visitSchema(normalized, branch, counts);
1889
+ }
1890
+ }
1891
+ function countBy(items, key) {
1892
+ const counts = {};
1893
+ for (const item of items) {
1894
+ const value = key(item);
1895
+ counts[value] = (counts[value] ?? 0) + 1;
1896
+ }
1897
+ return counts;
1898
+ }
1899
+ function mergeRecords(records) {
1900
+ const merged = {};
1901
+ for (const record of records) {
1902
+ for (const [key, value] of Object.entries(record)) {
1903
+ merged[key] = (merged[key] ?? 0) + value;
1904
+ }
1905
+ }
1906
+ return merged;
1907
+ }
1908
+ function sum(items, value) {
1909
+ return items.reduce((total, item) => total + value(item), 0);
1910
+ }
1911
+ function createListComposable(resourceName2, resource) {
1912
+ const names = createResourceTypeNames(resource);
1913
+ const collection = pluralizeTypeName(resource.entity);
1914
+ if (!resource.operations.list?.id) return createMissingComposable(`use${collection}Query`);
1915
+ const requiresParams = getPathParams(resource.operations.list).length > 0;
1916
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1917
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1918
+ import type { ${names.listParamsType}, ${names.listResponseType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
1919
+
1920
+ export function use${collection}Query(params${requiresParams ? "" : "?"}: ${names.listParamsType}): Promise<${names.listResponseType}> {
1921
+ ${resourceName2}QueryKeys.list(params)
1922
+ return ${resourceName2}Client.${resource.operations.list.id}(params)
1923
+ }
1924
+ `;
1925
+ }
1926
+ function createDetailComposable(resourceName2, resource) {
1927
+ const names = createResourceTypeNames(resource);
1928
+ if (!resource.operations.detail?.id) return createMissingComposable(`use${resource.entity}Query`);
1929
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1930
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1931
+ import type { ${names.detailResponseType}, ${names.idType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
1932
+
1933
+ export function use${resource.entity}Query(id: ${names.idType}): Promise<${names.detailResponseType}> {
1934
+ ${resourceName2}QueryKeys.detail(id)
1935
+ return ${resourceName2}Client.${resource.operations.detail.id}(id)
1936
+ }
1937
+ `;
1938
+ }
1939
+ function createCreateMutation(resourceName2, resource) {
1940
+ const names = createResourceTypeNames(resource);
1941
+ if (!resource.operations.create?.id) return createMissingComposable(`useCreate${resource.entity}Mutation`);
1942
+ const pathParams = getPathParams(resource.operations.create);
1943
+ if (pathParams.length > 0) {
1944
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1945
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1946
+ import type { ${names.listParamsType}, ${names.createRequestType}, ${names.createResponseType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
1947
+
1948
+ export function useCreate${resource.entity}Mutation(): {
1949
+ mutate: (input: { params: ${names.listParamsType}; payload: ${names.createRequestType} }) => Promise<${names.createResponseType}>
1950
+ invalidate: (params: ${names.listParamsType}) => ReturnType<typeof ${resourceName2}QueryKeys.list>
1951
+ } {
1952
+ return {
1953
+ mutate: (input) => ${resourceName2}Client.${resource.operations.create.id}(input.params, input.payload),
1954
+ invalidate: (params) => ${resourceName2}QueryKeys.list(params),
1955
+ }
1956
+ }
1957
+ `;
1958
+ }
1959
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1960
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1961
+ import type { ${names.createRequestType}, ${names.createResponseType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
1962
+
1963
+ export function useCreate${resource.entity}Mutation(): {
1964
+ mutate: (payload: ${names.createRequestType}) => Promise<${names.createResponseType}>
1965
+ invalidate: () => ReturnType<typeof ${resourceName2}QueryKeys.list>
1966
+ } {
1967
+ return {
1968
+ mutate: (payload: ${names.createRequestType}) => ${resourceName2}Client.${resource.operations.create.id}(payload),
1969
+ invalidate: () => ${resourceName2}QueryKeys.list(),
1970
+ }
1971
+ }
1972
+ `;
1973
+ }
1974
+ function createUpdateMutation(resourceName2, resource) {
1975
+ const names = createResourceTypeNames(resource);
1976
+ if (!resource.operations.update?.id) return createMissingComposable(`useUpdate${resource.entity}Mutation`);
1977
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1978
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1979
+ import type { ${names.idType}, ${names.updateRequestType}, ${names.updateResponseType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
1980
+
1981
+ export function useUpdate${resource.entity}Mutation(): {
1982
+ mutate: (input: { id: ${names.idType}; payload: ${names.updateRequestType} }) => Promise<${names.updateResponseType}>
1983
+ invalidate: (id: ${names.idType}) => ReturnType<typeof ${resourceName2}QueryKeys.detail>
1984
+ } {
1985
+ return {
1986
+ mutate: ({ id, payload }) => ${resourceName2}Client.${resource.operations.update.id}(id, payload),
1987
+ invalidate: (id) => ${resourceName2}QueryKeys.detail(id),
1988
+ }
1989
+ }
1990
+ `;
1991
+ }
1992
+ function createDeleteMutation(resourceName2, resource) {
1993
+ const names = createResourceTypeNames(resource);
1994
+ if (!resource.operations.delete?.id) return createMissingComposable(`useDelete${resource.entity}Mutation`);
1995
+ const requiresListParams = getPathParams(resource.operations.delete).length > 1;
1996
+ if (requiresListParams) {
1997
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
1998
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
1999
+ import type { ${names.idType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
2000
+
2001
+ export function useDelete${resource.entity}Mutation(): {
2002
+ mutate: (id: ${names.idType}) => Promise<void>
2003
+ invalidate: (id: ${names.idType}) => ReturnType<typeof ${resourceName2}QueryKeys.detail>
2004
+ } {
2005
+ return {
2006
+ mutate: (id) => ${resourceName2}Client.${resource.operations.delete.id}(id),
2007
+ invalidate: (id) => ${resourceName2}QueryKeys.detail(id),
2008
+ }
2009
+ }
2010
+ `;
2011
+ }
2012
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
2013
+ import { ${resourceName2}QueryKeys } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.query-keys'
2014
+ import type { ${names.idType} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
2015
+
2016
+ export function useDelete${resource.entity}Mutation(): {
2017
+ mutate: (id: ${names.idType}) => Promise<void>
2018
+ invalidate: () => ReturnType<typeof ${resourceName2}QueryKeys.list>
2019
+ } {
2020
+ return {
2021
+ mutate: (id) => ${resourceName2}Client.${resource.operations.delete.id}(id),
2022
+ invalidate: () => ${resourceName2}QueryKeys.list(),
2023
+ }
2024
+ }
2025
+ `;
2026
+ }
2027
+ function createOperationComposable(resourceName2, operation) {
2028
+ const names = createOperationTypeNames(operation);
2029
+ const composableName = operationComposableName(operation);
2030
+ const hasPayload = Boolean(operation.requestBodySchema);
2031
+ const hasParams = getOperationParams(operation).length > 0;
2032
+ const imports = [
2033
+ ...hasPayload ? [names.requestType] : [],
2034
+ ...hasParams ? [names.paramsType] : [],
2035
+ names.responseType
2036
+ ];
2037
+ return `import { ${resourceName2}Client } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.client'
2038
+ import type { ${imports.join(", ")} } from '../../../shared/api/generated/${resourceName2}/${resourceName2}.types'
2039
+
2040
+ type ${toInputTypeName(operation)} = ${operationInputType(names, hasPayload, hasParams)}
2041
+
2042
+ export function ${composableName}(): {
2043
+ mutate: (input: ${toInputTypeName(operation)}) => Promise<${names.responseType}>
2044
+ } {
2045
+ return {
2046
+ mutate: (input) => ${operationClientCall(resourceName2, operation, hasPayload, hasParams, true)},
2047
+ }
2048
+ }
2049
+ `;
2050
+ }
2051
+ function operationComposableName(operation) {
2052
+ const operationName = createOperationTypeNames(operation).requestType.replace(/OperationRequest$/, "");
2053
+ return `use${operationName}${operation.method === "get" ? "Query" : "Mutation"}`;
2054
+ }
2055
+ function createMissingComposable(name) {
2056
+ return `export function ${name}(): never {
2057
+ throw new Error('${name} is not available: missing OpenAPI operation for this resource.')
2058
+ }
2059
+ `;
2060
+ }
2061
+ function toInputTypeName(operation) {
2062
+ return `${createOperationTypeNames(operation).requestType.replace(/Request$/, "")}Input`;
2063
+ }
2064
+ function operationInputType(names, hasPayload, hasParams) {
2065
+ if (hasPayload && hasParams) return `{ payload: ${names.requestType}; params: ${names.paramsType} }`;
2066
+ if (hasPayload) return names.requestType;
2067
+ if (hasParams) return names.paramsType;
2068
+ return "void";
2069
+ }
2070
+ function operationClientCall(resourceName2, operation, hasPayload, hasParams, fromInput) {
2071
+ const target = `${resourceName2}Client.${operation.id}`;
2072
+ if (hasPayload && hasParams) return `${target}(${fromInput ? "input.payload, input.params" : "payload, params"})`;
2073
+ if (hasPayload) return `${target}(${fromInput ? "input" : "payload"})`;
2074
+ if (hasParams) return `${target}(${fromInput ? "input" : "params"})`;
2075
+ return `${target}()`;
2076
+ }
2077
+ var neutralComposables = {
2078
+ list: createListComposable,
2079
+ detail: createDetailComposable,
2080
+ createMutation: createCreateMutation,
2081
+ updateMutation: createUpdateMutation,
2082
+ deleteMutation: createDeleteMutation,
2083
+ operation: createOperationComposable
2084
+ };
2085
+ function extractPathParameters(path) {
2086
+ const params = [];
2087
+ forEachPathParameter(path, (name) => {
2088
+ params.push(name);
2089
+ });
2090
+ return params;
2091
+ }
2092
+ function hasPathParameters(path) {
2093
+ let found = false;
2094
+ forEachPathParameter(path, () => {
2095
+ found = true;
2096
+ });
2097
+ return found;
2098
+ }
2099
+ function renderPathTemplate(path, renderParameter) {
2100
+ let output = "";
2101
+ let cursor = 0;
2102
+ forEachPathParameter(path, (name, start, end) => {
2103
+ output += path.slice(cursor, start);
2104
+ output += renderParameter(name);
2105
+ cursor = end + 1;
2106
+ });
2107
+ return output + path.slice(cursor);
2108
+ }
2109
+ function forEachPathParameter(path, visit) {
2110
+ let index = 0;
2111
+ while (index < path.length) {
2112
+ const start = path.indexOf("{", index);
2113
+ if (start === -1) return;
2114
+ const end = path.indexOf("}", start + 1);
2115
+ if (end === -1) return;
2116
+ const name = path.slice(start + 1, end);
2117
+ if (name.length > 0) {
2118
+ visit(name, start, end);
2119
+ }
2120
+ index = end + 1;
2121
+ }
2122
+ }
2123
+ function createClientArtifact(normalized, resourceName2, resource) {
2124
+ const names = createResourceTypeNames(resource);
2125
+ const requestOptionsType = `${toSafeTypeName(resource.name)}RequestOptions`;
2126
+ const signatures = [];
2127
+ const implementations = [];
2128
+ if (resource.operations.list?.id) {
2129
+ const response = resource.operations.list.responseSchema || resource.operations.list.responseBodyEmpty ? names.listResponseType : "unknown";
2130
+ const pathParams = getPathParams(resource.operations.list);
2131
+ const queryParams = getQueryParams(resource.operations.list);
2132
+ const requiresParams = pathParams.length > 0;
2133
+ signatures.push(` ${resource.operations.list.id}: (params${requiresParams ? "" : "?"}: ${names.listParamsType}, options?: ${requestOptionsType}) => Promise<${response}>`);
2134
+ implementations.push(
2135
+ ` ${resource.operations.list.id}: (params, options) => apiClient.request<${response}>('GET', ${pathWithParamsObject(resource.operations.list.path)}${createListRequestOptions(queryParams, requiresParams)}),`
2136
+ );
2137
+ }
2138
+ if (resource.operations.detail?.id) {
2139
+ const response = names.detailResponseType;
2140
+ const pathParams = getPathParams(resource.operations.detail);
2141
+ const paramSignature = createPathParamSignature(pathParams, names.idType);
2142
+ const paramName = pathParams.length > 1 ? "params" : toSafeIdentifier(pathParams[0]?.name ?? "id");
2143
+ signatures.push(` ${resource.operations.detail.id}: (${paramName}: ${paramSignature}, options?: ${requestOptionsType}) => Promise<${response}>`);
2144
+ implementations.push(
2145
+ ` ${resource.operations.detail.id}: (${paramName}, options) => apiClient.request<${response}>('GET', ${pathWithParams(resource.operations.detail.path, pathParams)}, options),`
2146
+ );
2147
+ }
2148
+ if (resource.operations.create?.id) {
2149
+ const response = names.createResponseType;
2150
+ const pathParams = getPathParams(resource.operations.create);
2151
+ if (pathParams.length > 0) {
2152
+ signatures.push(` ${resource.operations.create.id}: (params: ${names.listParamsType}, payload: ${names.createRequestType}, options?: ${requestOptionsType}) => Promise<${response}>`);
2153
+ implementations.push(
2154
+ ` ${resource.operations.create.id}: (params, payload, options) => apiClient.request<${response}>('POST', ${pathWithParamsObject(resource.operations.create.path)}, { ...options, body: payload }),`
2155
+ );
2156
+ } else {
2157
+ signatures.push(` ${resource.operations.create.id}: (payload: ${names.createRequestType}, options?: ${requestOptionsType}) => Promise<${response}>`);
2158
+ implementations.push(
2159
+ ` ${resource.operations.create.id}: (payload, options) => apiClient.request<${response}>('POST', '${resource.operations.create.path}', { ...options, body: payload }),`
2160
+ );
2161
+ }
2162
+ }
2163
+ if (resource.operations.update?.id) {
2164
+ const response = names.updateResponseType;
2165
+ const pathParams = getPathParams(resource.operations.update);
2166
+ const paramSignature = createPathParamSignature(pathParams, names.idType);
2167
+ const paramName = pathParams.length > 1 ? "params" : toSafeIdentifier(pathParams[0]?.name ?? "id");
2168
+ signatures.push(` ${resource.operations.update.id}: (${paramName}: ${paramSignature}, payload: ${names.updateRequestType}, options?: ${requestOptionsType}) => Promise<${response}>`);
2169
+ implementations.push(
2170
+ ` ${resource.operations.update.id}: (${paramName}, payload, options) => apiClient.request<${response}>('PATCH', ${pathWithParams(resource.operations.update.path, pathParams)}, { ...options, body: payload }),`
2171
+ );
2172
+ }
2173
+ if (resource.operations.delete?.id) {
2174
+ const pathParams = getPathParams(resource.operations.delete);
2175
+ const paramSignature = createPathParamSignature(pathParams, names.idType);
2176
+ const paramName = pathParams.length > 1 ? "params" : toSafeIdentifier(pathParams[0]?.name ?? "id");
2177
+ signatures.push(` ${resource.operations.delete.id}: (${paramName}: ${paramSignature}, options?: ${requestOptionsType}) => Promise<void>`);
2178
+ implementations.push(
2179
+ ` ${resource.operations.delete.id}: (${paramName}, options) => apiClient.request<void>('DELETE', ${pathWithParams(resource.operations.delete.path, pathParams)}, options),`
2180
+ );
2181
+ }
2182
+ for (const operation of resource.operationsList.filter((item) => isGeneratedOperation(resource, item))) {
2183
+ const names2 = createOperationTypeNames(operation);
2184
+ const response = names2.responseType;
2185
+ const params = getOperationParams(operation);
2186
+ const queryParams = getQueryParams(operation);
2187
+ const headerParams = getHeaderParams(operation);
2188
+ const method = operation.method.toUpperCase();
2189
+ const pathExpression = pathWithOperationParams(operation.path);
2190
+ const options = createOperationRequestOptions(operation.requestBodySchema ? "payload" : null, queryParams, headerParams);
2191
+ signatures.push(` ${operation.id}: ${createOperationSignature(names2, Boolean(operation.requestBodySchema), params.length > 0, response, requestOptionsType)}`);
2192
+ implementations.push(` ${operation.id}: ${createOperationImplementationArgs(Boolean(operation.requestBodySchema), params.length > 0)} => apiClient.request<${response}>('${method}', ${pathExpression}${options}),`);
2193
+ }
2194
+ const imports = collectClientTypes(resource);
2195
+ const typeImport = imports.length > 0 ? `import type { ${imports.join(", ")} } from './${resourceName2}.types'
2196
+
2197
+ ` : "";
2198
+ const runtimeImports = ["createApiClient", ...usesQueryParamWrapper(resource) ? ["queryParam"] : []];
2199
+ const entityCollectionName = pluralizeTypeName(resource.entity);
2200
+ const configureName = `configure${entityCollectionName}Client`;
2201
+ const setName = `set${entityCollectionName}Client`;
2202
+ const operationHeaderHelper = resource.operationsList.filter((operation) => isGeneratedOperation(resource, operation)).some((operation) => getHeaderParams(operation).length > 0) ? `
2203
+ function createOperationHeaders(base: Record<string, string> | undefined, params: Record<string, unknown>): Record<string, string> {
2204
+ const headers = { ...(base ?? {}) }
2205
+ for (const [key, value] of Object.entries(params)) {
2206
+ if (value !== undefined && value !== null) headers[key] = String(value)
2207
+ }
2208
+ return headers
2209
+ }
2210
+ ` : "";
2211
+ return `import { ${runtimeImports.join(", ")}, type ApiClient, type ApiClientOptions, type ApiRequestOptions } from '@archora/forge-runtime'
2212
+ ${typeImport}export type ${requestOptionsType} = Omit<ApiRequestOptions, 'body' | 'params'>
2213
+
2214
+ let apiClient = createApiClient({ baseUrl: '' })
2215
+
2216
+ export function ${configureName}(options: ApiClientOptions): void {
2217
+ apiClient = createApiClient(options)
2218
+ }
2219
+
2220
+ export function ${setName}(client: ApiClient): void {
2221
+ apiClient = client
2222
+ }
2223
+
2224
+ export const ${resourceName2}Client: {
2225
+ ${signatures.join("\n")}
2226
+ } = {
2227
+ ${implementations.join("\n")}
2228
+ }
2229
+ ${operationHeaderHelper}`;
2230
+ }
2231
+ function collectClientTypes(resource) {
2232
+ const names = createResourceTypeNames(resource);
2233
+ const types = /* @__PURE__ */ new Set();
2234
+ if (resource.operations.list?.id) {
2235
+ types.add(names.listParamsType);
2236
+ if (resource.operations.list.responseSchema || resource.operations.list.responseBodyEmpty) types.add(names.listResponseType);
2237
+ }
2238
+ if (resource.operations.detail?.id) {
2239
+ types.add(names.idType);
2240
+ types.add(names.detailResponseType);
2241
+ }
2242
+ if (resource.operations.create?.id) {
2243
+ if (!resource.operations.list?.id && getPathParams(resource.operations.create).length > 0) types.add(names.listParamsType);
2244
+ types.add(names.createRequestType);
2245
+ types.add(names.createResponseType);
2246
+ }
2247
+ if (resource.operations.update?.id) {
2248
+ types.add(names.idType);
2249
+ types.add(names.updateRequestType);
2250
+ types.add(names.updateResponseType);
2251
+ }
2252
+ if (resource.operations.delete?.id) {
2253
+ types.add(names.idType);
2254
+ }
2255
+ for (const operation of resource.operationsList.filter((item) => isGeneratedOperation(resource, item))) {
2256
+ const operationNames = createOperationTypeNames(operation);
2257
+ if (getOperationParams(operation).length > 0) types.add(operationNames.paramsType);
2258
+ if (operation.requestBodySchema) types.add(operationNames.requestType);
2259
+ types.add(operationNames.responseType);
2260
+ }
2261
+ return [...types];
2262
+ }
2263
+ function createPathParamSignature(params, singleType) {
2264
+ return params.length <= 1 ? singleType : singleType;
2265
+ }
2266
+ function pathWithParams(path, params) {
2267
+ if (params.length <= 1) {
2268
+ const paramName = toSafeIdentifier(params[0]?.name ?? "id");
2269
+ return `\`${renderPathTemplate(path, () => `\${${encodePathParam(paramName)}}`)}\``;
2270
+ }
2271
+ return `\`${renderPathTemplate(path, (name) => `\${${encodePathParam(paramValueAccess("params", name))}}`)}\``;
2272
+ }
2273
+ function pathWithParamsObject(path) {
2274
+ return `\`${renderPathTemplate(path, (name) => `\${${encodePathParam(paramValueAccess("params", name))}}`)}\``;
2275
+ }
2276
+ function isGeneratedOperation(resource, operation) {
2277
+ return operation.operationKind !== "unsupported-operation" && Boolean(operation.id) && !Object.values(resource.operations).includes(operation);
2278
+ }
2279
+ function createOperationSignature(names, hasPayload, hasParams, response, requestOptionsType) {
2280
+ if (hasPayload && hasParams) return `(payload: ${names.requestType}, params: ${names.paramsType}, options?: ${requestOptionsType}) => Promise<${response}>`;
2281
+ if (hasPayload) return `(payload: ${names.requestType}, options?: ${requestOptionsType}) => Promise<${response}>`;
2282
+ if (hasParams) return `(params: ${names.paramsType}, options?: ${requestOptionsType}) => Promise<${response}>`;
2283
+ return `(options?: ${requestOptionsType}) => Promise<${response}>`;
2284
+ }
2285
+ function createOperationImplementationArgs(hasPayload, hasParams) {
2286
+ if (hasPayload && hasParams) return "(payload, params, options)";
2287
+ if (hasPayload) return "(payload, options)";
2288
+ if (hasParams) return "(params, options)";
2289
+ return "(options)";
2290
+ }
2291
+ function createOperationRequestOptions(payloadName, queryParams, headerParams) {
2292
+ const options = [];
2293
+ options.push("...options");
2294
+ if (payloadName) options.push(`body: ${payloadName}`);
2295
+ if (queryParams.length > 0) {
2296
+ options.push(`params: ${createQueryParamsObject(queryParams, false)}`);
2297
+ }
2298
+ if (headerParams.length > 0) {
2299
+ options.push(`headers: createOperationHeaders(options?.headers, { ${headerParams.map((param) => `${quoteObjectKeyIfNeeded(param.name)}: ${paramValueAccess("params", param.name)}`).join(", ")} })`);
2300
+ }
2301
+ return options.length > 1 ? `, { ${options.join(", ")} }` : ", options";
2302
+ }
2303
+ function createListRequestOptions(queryParams, requiresParams) {
2304
+ if (queryParams.length === 0) return ", options";
2305
+ if (!requiresParams && !queryParams.some(requiresQueryParamWrapper)) return ", { ...options, params: params as Record<string, unknown> | undefined }";
2306
+ return `, { ...options, params: ${createQueryParamsObject(queryParams, !requiresParams)} }`;
2307
+ }
2308
+ function pathWithOperationParams(path) {
2309
+ if (!hasPathParameters(path)) return `'${path}'`;
2310
+ return `\`${renderPathTemplate(path, (name) => `\${${encodePathParam(paramValueAccess("params", name))}}`)}\``;
2311
+ }
2312
+ function encodePathParam(expression) {
2313
+ return `encodeURIComponent(String(${expression}))`;
2314
+ }
2315
+ function paramValueAccess(objectName, paramName) {
2316
+ return /^[A-Za-z_$][\w$]*$/.test(paramName) ? `${objectName}.${paramName}` : `${objectName}[${JSON.stringify(paramName)}]`;
2317
+ }
2318
+ function createQueryParamsObject(queryParams, optionalParams) {
2319
+ return `{ ${queryParams.map((param) => `${quoteObjectKeyIfNeeded(param.name)}: ${queryParamValue(param, optionalParams)}`).join(", ")} }`;
2320
+ }
2321
+ function queryParamValue(param, optionalParams) {
2322
+ const access22 = optionalParams ? optionalParamValueAccess("params", param.name) : paramValueAccess("params", param.name);
2323
+ if (!requiresQueryParamWrapper(param)) return access22;
2324
+ return `queryParam(${access22}, { style: 'form', explode: false })`;
2325
+ }
2326
+ function optionalParamValueAccess(objectName, paramName) {
2327
+ return /^[A-Za-z_$][\w$]*$/.test(paramName) ? `${objectName}?.${paramName}` : `${objectName}?.[${JSON.stringify(paramName)}]`;
2328
+ }
2329
+ function requiresQueryParamWrapper(param) {
2330
+ return param.in === "query" && param.schema?.type === "array" && (param.style ?? "form") === "form" && param.explode === false;
2331
+ }
2332
+ function usesQueryParamWrapper(resource) {
2333
+ return resource.operationsList.some((operation) => getQueryParams(operation).some(requiresQueryParamWrapper));
2334
+ }
2335
+ function createFixturesArtifact(resourceName2, entity) {
2336
+ return `export const ${resourceName2}Fixtures: ${entity}Fixture[] = []
2337
+
2338
+ type ${entity}Fixture = Record<string, unknown>
2339
+ `;
2340
+ }
2341
+ function createHandlersArtifact(resourceName2) {
2342
+ return `export const ${resourceName2}Handlers = {
2343
+ list: () => ({ status: 200, body: [] }),
2344
+ detail: () => ({ status: 200, body: {} }),
2345
+ create: () => ({ status: 201, body: {} }),
2346
+ validationError: () => ({ status: 422, body: { message: 'Validation error' } }),
2347
+ forbidden: () => ({ status: 403, body: { message: 'Forbidden' } }),
2348
+ serverError: () => ({ status: 500, body: { message: 'Server error' } }),
2349
+ } as const
2350
+ `;
2351
+ }
2352
+ function createScenariosArtifact(resourceName2) {
2353
+ return `export const ${resourceName2}Scenarios = ['success-list', 'empty-list', 'detail-success', 'create-success', 'validation-error', 'forbidden', 'server-error'] as const
2354
+ `;
2355
+ }
2356
+ function toCode(value) {
2357
+ return JSON.stringify(value, null, 2).replace(/"([^"]+)":/g, "$1:");
2358
+ }
2359
+ function createPermissionsArtifact(resourceName2, config) {
2360
+ return `export const ${resourceName2}Permissions = {
2361
+ view: '${resourceName2}.read',
2362
+ create: '${resourceName2}.create',
2363
+ update: '${resourceName2}.update',
2364
+ delete: '${resourceName2}.delete',
2365
+ } as const
2366
+ `.replace(`'${resourceName2}.read'`, `'${config?.permissions?.view ?? `${resourceName2}.read`}'`).replace(`'${resourceName2}.create'`, `'${config?.permissions?.create ?? `${resourceName2}.create`}'`).replace(`'${resourceName2}.update'`, `'${config?.permissions?.update ?? `${resourceName2}.update`}'`).replace(`'${resourceName2}.delete'`, `'${config?.permissions?.delete ?? `${resourceName2}.delete`}'`);
2367
+ }
2368
+ function createI18nArtifact(resourceName2, entity, model) {
2369
+ const collection = pluralizeTypeName(entity);
2370
+ const fields = [
2371
+ ...model.formFields.map((field) => field.name),
2372
+ ...model.filterFields.map((field) => field.name),
2373
+ ...model.tableColumns.map((column) => column.name)
2374
+ ];
2375
+ const uniqueFields = [...new Set(fields)].map((field) => ` ${quoteObjectKeyIfNeeded(field)}: '${field.replace(/([A-Z])/g, " $1").replace(/^./, (char) => char.toUpperCase())}',`).join("\n");
2376
+ return `export const ${resourceName2}I18n = {
2377
+ title: '${collection}',
2378
+ create: 'Create ${entity.toLowerCase()}',
2379
+ edit: 'Edit ${entity.toLowerCase()}',
2380
+ delete: 'Delete ${entity.toLowerCase()}',
2381
+ fields: {
2382
+ ${uniqueFields}
2383
+ },
2384
+ } as const
2385
+ `;
2386
+ }
2387
+ function createResourceConfigArtifact(resourceName2, model) {
2388
+ return `export const ${resourceName2}Config = {
2389
+ resource: '${resourceName2}',
2390
+ pagination: ${toCode(model.pagination)},
2391
+ fields: ${toCode(model.formFields)},
2392
+ filters: ${toCode(model.filterFields)},
2393
+ columns: ${toCode(model.tableColumns)},
2394
+ } as const
2395
+ `;
2396
+ }
2397
+ function createIndex(exports) {
2398
+ return `${exports.map((item) => `export * from './${item}'`).join("\n")}
2399
+ `;
2400
+ }
2401
+ function createQueryKeysArtifact(resourceName2, resource) {
2402
+ const names = createResourceTypeNames(resource);
2403
+ const requiresListParams = getPathParams(resource.operations.list).length > 0;
2404
+ return `import type { ${names.idType}, ${names.listParamsType} } from './${resourceName2}.types'
2405
+
2406
+ export const ${resourceName2}QueryKeys = {
2407
+ all: ['${resourceName2}'] as const,
2408
+ list: (params${requiresListParams ? "" : "?"}: ${names.listParamsType}) => [...${resourceName2}QueryKeys.all, 'list', params] as const,
2409
+ detail: (id: ${names.idType}) => [...${resourceName2}QueryKeys.all, 'detail', id] as const,
2410
+ } as const
2411
+ `;
2412
+ }
2413
+ function createValidationSchemas(normalized, resourceName2, resource, validation) {
2414
+ if (validation === "valibot") {
2415
+ const createSchema2 = resource.operations.create?.requestBodySchema;
2416
+ const updateSchema2 = resource.operations.update?.requestBodySchema;
2417
+ const entity2 = resource.entity.charAt(0).toLowerCase() + resource.entity.slice(1);
2418
+ return `import * as v from 'valibot'
2419
+
2420
+ export const create${resource.entity}Schema = ${schemaToValibot(normalized, createSchema2)}
2421
+
2422
+ export const update${resource.entity}Schema = ${schemaToValibot(normalized, updateSchema2)}
2423
+
2424
+ export const ${entity2}ValidationSchemas = {
2425
+ create: create${resource.entity}Schema,
2426
+ update: update${resource.entity}Schema,
2427
+ } as const
2428
+ `;
2429
+ }
2430
+ const createSchema = resource.operations.create?.requestBodySchema;
2431
+ const updateSchema = resource.operations.update?.requestBodySchema;
2432
+ const entity = resource.entity.charAt(0).toLowerCase() + resource.entity.slice(1);
2433
+ return `import { z } from 'zod'
2434
+
2435
+ export const create${resource.entity}Schema = ${schemaToZod(normalized, createSchema)}
2436
+
2437
+ export const update${resource.entity}Schema = ${schemaToZod(normalized, updateSchema)}
2438
+
2439
+ export const ${entity}ValidationSchemas = {
2440
+ create: create${resource.entity}Schema,
2441
+ update: update${resource.entity}Schema,
2442
+ } as const
2443
+ `;
2444
+ }
2445
+ function schemaToZod(normalized, schema, resolvingRefs = []) {
2446
+ if (schema?.$ref) {
2447
+ const refName = resolveSchemaName(schema);
2448
+ if (refName && resolvingRefs.includes(refName)) {
2449
+ return appendNullable("z.lazy(() => z.unknown())", schema);
2450
+ }
2451
+ const resolved2 = resolveSchema(normalized, schema);
2452
+ if (!resolved2 || resolved2 === schema) return appendNullable("z.unknown()", schema);
2453
+ return schemaToZod(normalized, resolved2, refName ? [...resolvingRefs, refName] : resolvingRefs);
2454
+ }
2455
+ const resolved = resolveSchema(normalized, schema);
2456
+ if (!resolved) return "z.object({}).passthrough()";
2457
+ if (Array.isArray(resolved.type)) {
2458
+ const normalizedSchema = normalizeNullableTypeArray(resolved);
2459
+ if (normalizedSchema) return schemaToZod(normalized, normalizedSchema, resolvingRefs);
2460
+ return appendNullable("z.unknown()", resolved);
2461
+ }
2462
+ if (resolved.oneOf?.length || resolved.anyOf?.length) {
2463
+ return appendNullable(createZodUnion(normalized, resolved.oneOf ?? resolved.anyOf ?? [], resolvingRefs), resolved);
2464
+ }
2465
+ if (hasConst2(resolved)) return appendNullable(`z.literal(${JSON.stringify(resolved.const)})`, resolved);
2466
+ if (resolved.enum?.length) return appendNullable(createZodEnum(resolved.enum), resolved);
2467
+ if (resolved.type === "string") return zodString(resolved);
2468
+ if (resolved.type === "integer") return appendNullable("z.number().int()", resolved);
2469
+ if (resolved.type === "number") return appendNullable(zodNumber(resolved), resolved);
2470
+ if (resolved.type === "boolean") return appendNullable("z.boolean()", resolved);
2471
+ if (resolved.type === "array") return appendNullable(`z.array(${schemaToZod(normalized, resolved.items, resolvingRefs)})`, resolved);
2472
+ if (isPureDictionarySchema2(resolved)) {
2473
+ return appendNullable(`z.record(z.string(), ${schemaToZodRecordValue(normalized, resolved.additionalProperties, resolvingRefs)})`, resolved);
2474
+ }
2475
+ if (resolved.type === "object" || resolved.properties) {
2476
+ const required = new Set(resolved.required ?? []);
2477
+ const fields = Object.entries(resolved.properties ?? {}).map(([name, property]) => {
2478
+ const base = schemaToZod(normalized, property, resolvingRefs);
2479
+ const optional = required.has(name) ? base : `${base}.optional()`;
2480
+ return ` ${toCodeKey(name)}: ${optional},`;
2481
+ }).join("\n");
2482
+ return appendNullable(`z.object({
2483
+ ${fields}
2484
+ })`, resolved);
2485
+ }
2486
+ return appendNullable("z.unknown()", resolved);
2487
+ }
2488
+ function schemaToValibot(normalized, schema, resolvingRefs = []) {
2489
+ if (schema?.$ref) {
2490
+ const refName = resolveSchemaName(schema);
2491
+ if (refName && resolvingRefs.includes(refName)) {
2492
+ return wrapValibotNullable("v.lazy(() => v.unknown())", schema);
2493
+ }
2494
+ const resolved2 = resolveSchema(normalized, schema);
2495
+ if (!resolved2 || resolved2 === schema) return wrapValibotNullable("v.unknown()", schema);
2496
+ return schemaToValibot(normalized, resolved2, refName ? [...resolvingRefs, refName] : resolvingRefs);
2497
+ }
2498
+ const resolved = resolveSchema(normalized, schema);
2499
+ if (!resolved) return "v.looseObject({})";
2500
+ if (Array.isArray(resolved.type)) {
2501
+ const normalizedSchema = normalizeNullableTypeArray(resolved);
2502
+ if (normalizedSchema) return schemaToValibot(normalized, normalizedSchema, resolvingRefs);
2503
+ return wrapValibotNullable("v.unknown()", resolved);
2504
+ }
2505
+ if (resolved.oneOf?.length || resolved.anyOf?.length) {
2506
+ return wrapValibotNullable(createValibotUnion(normalized, resolved.oneOf ?? resolved.anyOf ?? [], resolvingRefs), resolved);
2507
+ }
2508
+ if (hasConst2(resolved)) return wrapValibotNullable(`v.literal(${JSON.stringify(resolved.const)})`, resolved);
2509
+ if (resolved.enum?.length) {
2510
+ return wrapValibotNullable(`v.picklist([${resolved.enum.map((value) => JSON.stringify(value)).join(", ")}])`, resolved);
2511
+ }
2512
+ if (resolved.type === "string") return valibotString(resolved);
2513
+ if (resolved.type === "integer") return wrapValibotNullable("v.pipe(v.number(), v.integer())", resolved);
2514
+ if (resolved.type === "number") return wrapValibotNullable(valibotNumber(resolved), resolved);
2515
+ if (resolved.type === "boolean") return wrapValibotNullable("v.boolean()", resolved);
2516
+ if (resolved.type === "array") return wrapValibotNullable(`v.array(${schemaToValibot(normalized, resolved.items, resolvingRefs)})`, resolved);
2517
+ if (isPureDictionarySchema2(resolved)) {
2518
+ return wrapValibotNullable(`v.record(v.string(), ${schemaToValibotRecordValue(normalized, resolved.additionalProperties, resolvingRefs)})`, resolved);
2519
+ }
2520
+ if (resolved.type === "object" || resolved.properties) {
2521
+ const required = new Set(resolved.required ?? []);
2522
+ const fields = Object.entries(resolved.properties ?? {}).map(([name, property]) => {
2523
+ const base = schemaToValibot(normalized, property, resolvingRefs);
2524
+ const optional = required.has(name) ? base : `v.optional(${base})`;
2525
+ return ` ${toCodeKey(name)}: ${optional},`;
2526
+ }).join("\n");
2527
+ return wrapValibotNullable(`v.object({
2528
+ ${fields}
2529
+ })`, resolved);
2530
+ }
2531
+ return wrapValibotNullable("v.unknown()", resolved);
2532
+ }
2533
+ function zodString(schema) {
2534
+ let expression = "z.string()";
2535
+ if (schema.format === "email") expression += ".email()";
2536
+ if (schema.format === "uuid") expression += ".uuid()";
2537
+ if (schema.format === "uri") expression += ".url()";
2538
+ if (schema.format === "date") expression += ".date()";
2539
+ if (schema.format === "date-time") expression += ".datetime()";
2540
+ if (schema.minLength !== void 0) expression += `.min(${schema.minLength})`;
2541
+ if (schema.maxLength !== void 0) expression += `.max(${schema.maxLength})`;
2542
+ return appendNullable(expression, schema);
2543
+ }
2544
+ function zodNumber(schema) {
2545
+ let expression = "z.number()";
2546
+ if (schema.minimum !== void 0) expression += `.min(${schema.minimum})`;
2547
+ if (schema.maximum !== void 0) expression += `.max(${schema.maximum})`;
2548
+ return expression;
2549
+ }
2550
+ function appendNullable(expression, schema) {
2551
+ return schema.nullable ? `${expression}.nullable()` : expression;
2552
+ }
2553
+ function createZodEnum(values) {
2554
+ if (values.every((value) => typeof value === "string")) {
2555
+ return `z.enum([${values.map((value) => JSON.stringify(value)).join(", ")}])`;
2556
+ }
2557
+ const literals = values.map((value) => `z.literal(${JSON.stringify(value)})`);
2558
+ return literals.length === 1 ? literals[0] ?? "z.unknown()" : `z.union([${literals.join(", ")}])`;
2559
+ }
2560
+ function createZodUnion(normalized, branches, resolvingRefs) {
2561
+ const schemas = branches.map((branch) => schemaToZod(normalized, branch, resolvingRefs));
2562
+ if (schemas.length === 0) return "z.unknown()";
2563
+ if (schemas.length === 1) return schemas[0] ?? "z.unknown()";
2564
+ return `z.union([${schemas.join(", ")}])`;
2565
+ }
2566
+ function valibotString(schema) {
2567
+ const actions = [];
2568
+ if (schema.format === "email") actions.push("v.email()");
2569
+ if (schema.format === "uuid") actions.push("v.uuid()");
2570
+ if (schema.format === "uri") actions.push("v.url()");
2571
+ if (schema.format === "date") actions.push("v.isoDate()");
2572
+ if (schema.format === "date-time") actions.push("v.isoDateTime()");
2573
+ if (schema.minLength !== void 0) actions.push(`v.minLength(${schema.minLength})`);
2574
+ if (schema.maxLength !== void 0) actions.push(`v.maxLength(${schema.maxLength})`);
2575
+ const expression = actions.length === 0 ? "v.string()" : `v.pipe(v.string(), ${actions.join(", ")})`;
2576
+ return wrapValibotNullable(expression, schema);
2577
+ }
2578
+ function valibotNumber(schema) {
2579
+ const actions = [];
2580
+ if (schema.minimum !== void 0) actions.push(`v.minValue(${schema.minimum})`);
2581
+ if (schema.maximum !== void 0) actions.push(`v.maxValue(${schema.maximum})`);
2582
+ return actions.length === 0 ? "v.number()" : `v.pipe(v.number(), ${actions.join(", ")})`;
2583
+ }
2584
+ function wrapValibotNullable(expression, schema) {
2585
+ return schema.nullable ? `v.nullable(${expression})` : expression;
2586
+ }
2587
+ function createValibotUnion(normalized, branches, resolvingRefs) {
2588
+ const schemas = branches.map((branch) => schemaToValibot(normalized, branch, resolvingRefs));
2589
+ if (schemas.length === 0) return "v.unknown()";
2590
+ if (schemas.length === 1) return schemas[0] ?? "v.unknown()";
2591
+ return `v.union([${schemas.join(", ")}])`;
2592
+ }
2593
+ function isPureDictionarySchema2(schema) {
2594
+ return Boolean(
2595
+ (schema.type === "object" || schema.additionalProperties !== void 0) && schema.additionalProperties !== void 0 && Object.keys(schema.properties ?? {}).length === 0
2596
+ );
2597
+ }
2598
+ function normalizeNullableTypeArray(schema) {
2599
+ if (!Array.isArray(schema.type)) return schema;
2600
+ const nonNullTypes = schema.type.filter((type) => type !== "null");
2601
+ if (nonNullTypes.length !== 1) return null;
2602
+ return {
2603
+ ...schema,
2604
+ type: nonNullTypes[0],
2605
+ nullable: schema.nullable || schema.type.includes("null")
2606
+ };
2607
+ }
2608
+ function schemaToZodRecordValue(normalized, additionalProperties, resolvingRefs) {
2609
+ if (additionalProperties === false) return "z.never()";
2610
+ if (additionalProperties === true || additionalProperties === void 0) return "z.unknown()";
2611
+ return schemaToZod(normalized, additionalProperties, resolvingRefs);
2612
+ }
2613
+ function schemaToValibotRecordValue(normalized, additionalProperties, resolvingRefs) {
2614
+ if (additionalProperties === false) return "v.never()";
2615
+ if (additionalProperties === true || additionalProperties === void 0) return "v.unknown()";
2616
+ return schemaToValibot(normalized, additionalProperties, resolvingRefs);
2617
+ }
2618
+ function toCodeKey(value) {
2619
+ return /^[A-Za-z_$][\w$]*$/.test(value) ? value : JSON.stringify(value);
2620
+ }
2621
+ function hasConst2(schema) {
2622
+ return Object.hasOwn(schema, "const");
2623
+ }
2624
+ async function formatGeneratedContent(path, content) {
2625
+ const parser = getParser(path);
2626
+ if (!parser) {
2627
+ return content;
2628
+ }
2629
+ try {
2630
+ return await format(content, {
2631
+ parser,
2632
+ singleQuote: true,
2633
+ semi: false,
2634
+ trailingComma: "all",
2635
+ printWidth: 100
2636
+ });
2637
+ } catch {
2638
+ return content;
2639
+ }
2640
+ }
2641
+ function getParser(path) {
2642
+ if (path.endsWith(".ts")) return "typescript";
2643
+ return null;
2644
+ }
2645
+ function createResourceUiModel(input) {
2646
+ const entitySchema = resolveEntitySchema(input.normalized, input.resource);
2647
+ const formSchema = resolveOperationBodySchema(input.normalized, input.resource.operations.create) ?? entitySchema;
2648
+ const tableSchema = resolveListItemSchema(input.normalized, input.resource.operations.list) ?? entitySchema;
2649
+ const formRequired = new Set(formSchema?.required ?? entitySchema?.required ?? []);
2650
+ const tableProperties = tableSchema?.properties ?? {};
2651
+ const formProperties = formSchema?.properties ?? tableProperties;
2652
+ return {
2653
+ resourceName: input.resource.name,
2654
+ entityName: input.resource.entity,
2655
+ formFields: selectProperties(formProperties, input.config?.form?.fields).filter(([, schema]) => !schema.readOnly).map(([name, schema]) => createFormField(name, schema, formRequired.has(name))),
2656
+ filterFields: selectProperties(tableProperties, input.config?.table?.filters).filter(([, schema]) => !schema.writeOnly).map(([name, schema]) => createFormField(name, schema, false)),
2657
+ tableColumns: selectProperties(tableProperties, input.config?.table?.columns).filter(([, schema]) => !schema.writeOnly).map(([name, schema]) => createTableColumn(name, schema)),
2658
+ pagination: detectPagination(input.normalized, input.resource.operations.list)
2659
+ };
2660
+ }
2661
+ function selectProperties(properties, explicitOrder) {
2662
+ if (!explicitOrder || explicitOrder.length === 0) {
2663
+ return Object.entries(properties);
2664
+ }
2665
+ return explicitOrder.flatMap((name) => {
2666
+ const schema = properties[name];
2667
+ return schema ? [[name, schema]] : [];
2668
+ });
2669
+ }
2670
+ function createFormField(name, schema, required) {
2671
+ return {
2672
+ name,
2673
+ label: toLabel(name),
2674
+ input: mapFormInput(name, schema),
2675
+ component: mapFieldComponent(name, schema),
2676
+ required,
2677
+ nullable: schema.nullable ?? isNullableTypeArray(schema),
2678
+ enumValues: schema.enum,
2679
+ ...Object.hasOwn(schema, "default") ? { defaultValue: schema.default } : {},
2680
+ ...schema.deprecated ? { deprecated: true } : {},
2681
+ hint: schema.description,
2682
+ validation: {
2683
+ minLength: schema.minLength,
2684
+ maxLength: schema.maxLength,
2685
+ minimum: schema.minimum,
2686
+ maximum: schema.maximum
2687
+ }
2688
+ };
2689
+ }
2690
+ function createTableColumn(name, schema) {
2691
+ const type = primarySchemaType(schema);
2692
+ return {
2693
+ name,
2694
+ label: toLabel(name),
2695
+ cell: mapTableCell(schema),
2696
+ sortable: ["string", "number", "integer"].includes(type ?? "") || schema.format === "date" || schema.format === "date-time",
2697
+ nullable: schema.nullable ?? isNullableTypeArray(schema),
2698
+ ...schema.deprecated ? { deprecated: true } : {},
2699
+ hint: schema.description
2700
+ };
2701
+ }
2702
+ function mapFormInput(name, schema) {
2703
+ if (schema.enum) return "select";
2704
+ const type = primarySchemaType(schema);
2705
+ if (name.toLowerCase().includes("password")) return "password";
2706
+ if (schema.format === "email") return "email";
2707
+ if (schema.format === "uri") return "url";
2708
+ if (schema.format === "date") return "date";
2709
+ if (schema.format === "date-time") return "dateTime";
2710
+ if (type === "number" || type === "integer") return "number";
2711
+ if (type === "boolean") return "switch";
2712
+ if ((schema.maxLength ?? 0) > 160) return "textarea";
2713
+ return "text";
2714
+ }
2715
+ function mapFieldComponent(name, schema) {
2716
+ const input = mapFormInput(name, schema);
2717
+ if (input === "select") return "ArchSelect";
2718
+ if (input === "switch") return "ArchSwitch";
2719
+ if (input === "date" || input === "dateTime") return "ArchDatePicker";
2720
+ if (input === "textarea") return "ArchTextarea";
2721
+ return "ArchInput";
2722
+ }
2723
+ function mapTableCell(schema) {
2724
+ if (schema.enum) return "badge";
2725
+ const type = primarySchemaType(schema);
2726
+ if (schema.format === "date") return "date";
2727
+ if (schema.format === "date-time") return "dateTime";
2728
+ if (type === "boolean") return "boolean";
2729
+ if (type === "number" || type === "integer") return "number";
2730
+ if (type === "object" || type === "array") return "json";
2731
+ return "text";
2732
+ }
2733
+ function primarySchemaType(schema) {
2734
+ return Array.isArray(schema.type) ? schema.type.find((type) => type !== "null") : schema.type;
2735
+ }
2736
+ function isNullableTypeArray(schema) {
2737
+ return Array.isArray(schema.type) && schema.type.includes("null");
2738
+ }
2739
+ function detectPagination(normalized, operation) {
2740
+ const responseSchema = operation?.responseSchema ? resolveSchema2(normalized, operation.responseSchema) : null;
2741
+ const properties = responseSchema?.properties ?? {};
2742
+ const itemsPath = properties.items?.type === "array" ? "items" : void 0;
2743
+ const totalPath = properties.total ? "total" : properties.totalCount ? "totalCount" : void 0;
2744
+ const pagePath = properties.page ? "page" : properties.pageIndex ? "pageIndex" : void 0;
2745
+ return {
2746
+ enabled: Boolean(itemsPath && totalPath),
2747
+ itemsPath,
2748
+ totalPath,
2749
+ pagePath
2750
+ };
2751
+ }
2752
+ function resolveEntitySchema(normalized, resource) {
2753
+ return normalized.schemas.find((schema) => schema.name === resource.entity)?.schema ?? null;
2754
+ }
2755
+ function resolveOperationBodySchema(normalized, operation) {
2756
+ if (!operation?.requestBodySchema) {
2757
+ return null;
2758
+ }
2759
+ return resolveSchema2(normalized, operation.requestBodySchema);
2760
+ }
2761
+ function resolveListItemSchema(normalized, operation) {
2762
+ if (!operation?.responseSchema) {
2763
+ return null;
2764
+ }
2765
+ const responseSchema = resolveSchema2(normalized, operation.responseSchema);
2766
+ if (responseSchema?.type === "array" && responseSchema.items) {
2767
+ return resolveSchema2(normalized, responseSchema.items);
2768
+ }
2769
+ const items = responseSchema?.properties?.items;
2770
+ if (items?.type === "array" && items.items) {
2771
+ return resolveSchema2(normalized, items.items);
2772
+ }
2773
+ return responseSchema;
2774
+ }
2775
+ function resolveSchema2(normalized, schema) {
2776
+ if (schema.$ref) {
2777
+ const name = schema.$ref.split("/").at(-1);
2778
+ return normalized.schemas.find((candidate) => candidate.name === name)?.schema ?? schema;
2779
+ }
2780
+ return schema;
2781
+ }
2782
+ function toLabel(value) {
2783
+ return value.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/^./, (char) => char.toUpperCase()).trim();
2784
+ }
2785
+ async function loadTemplateOverride(path, cwd) {
2786
+ const absolutePath = resolve22(cwd, path);
2787
+ let loaded;
2788
+ try {
2789
+ loaded = await import(pathToFileURL2(absolutePath).href);
2790
+ } catch (error) {
2791
+ throw new Error(`Failed to load template override "${path}": ${error instanceof Error ? error.message : String(error)}`);
2792
+ }
2793
+ const candidate = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : loaded;
2794
+ if (typeof candidate !== "function") {
2795
+ throw new Error(`Template override "${path}" must export a default render function.`);
2796
+ }
2797
+ return candidate;
2798
+ }
2799
+ async function createGenerationPlan(input) {
2800
+ const metadata = createGeneratedFileMetadata(input);
2801
+ const files = [
2802
+ await createFile(input.cwd, join22(input.config.output.generatedDir, "components.types.ts"), "generated", createSharedSchemaTypes(input.normalized), true)
2803
+ ];
2804
+ const templateOverrides = await loadTemplateOverrides(input.config.templates?.override, input.cwd);
2805
+ const resolvedResources = [];
2806
+ for (const resource of input.resources) {
2807
+ if (input.config.resources?.[resource.name]?.enabled === false) continue;
2808
+ const resolvedResource = applyResourceConfig(resource, input.config.resources?.[resource.name]);
2809
+ resolvedResources.push(resolvedResource);
2810
+ files.push(...await createResourceFiles(input, resolvedResource, templateOverrides));
2811
+ }
2812
+ for (const artifact of await runPluginArtifactHooks({ plugins: input.config.plugins, normalized: input.normalized, cwd: input.cwd })) {
2813
+ files.push(await createFile(input.cwd, artifact.path, artifact.kind, artifact.content, artifact.overwrite));
2814
+ }
2815
+ await runPluginAfterGenerateHooks({ plugins: input.config.plugins, files });
2816
+ for (const file of files) {
2817
+ if (file.kind === "generated") file.metadata = metadata;
2818
+ }
2819
+ return {
2820
+ resources: resolvedResources.map((resource) => ({
2821
+ ...resource,
2822
+ outputName: resource.name,
2823
+ permissions: {
2824
+ view: `${resource.name}.read`,
2825
+ create: `${resource.name}.create`,
2826
+ update: `${resource.name}.update`,
2827
+ delete: `${resource.name}.delete`
2828
+ }
2829
+ })),
2830
+ files
2831
+ };
2832
+ }
2833
+ function createGeneratedFileMetadata(input) {
2834
+ return {
2835
+ version: forgeCoreVersion,
2836
+ schemaHash: stableHash(input.normalized),
2837
+ configHash: stableHash({
2838
+ output: input.config.output,
2839
+ target: input.config.target,
2840
+ validation: input.config.validation,
2841
+ resources: input.config.resources,
2842
+ templates: input.config.templates
2843
+ })
2844
+ };
2845
+ }
2846
+ function stableHash(value) {
2847
+ return createHash("sha256").update(stableStringify(value)).digest("hex").slice(0, 12);
2848
+ }
2849
+ function stableStringify(value) {
2850
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
2851
+ if (value && typeof value === "object") {
2852
+ return `{${Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
2853
+ }
2854
+ return JSON.stringify(value);
2855
+ }
2856
+ function applyResourceConfig(resource, config) {
2857
+ return {
2858
+ ...resource,
2859
+ entity: config?.entity ?? resource.entity
2860
+ };
2861
+ }
2862
+ async function createResourceFiles(input, resource, templateOverrides) {
2863
+ const uiModel = createResourceUiModel({
2864
+ normalized: input.normalized,
2865
+ resource,
2866
+ config: input.config.resources?.[resource.name]
2867
+ });
2868
+ const paths = resourcePaths(input.config, resource);
2869
+ const validation = input.config.validation ?? "none";
2870
+ const apiExports = [`${resource.name}.client`, `${resource.name}.types`, `${resource.name}.query-keys`];
2871
+ const validationArtifact = validation === "none" ? null : createValidationSchemas(input.normalized, resource.name, resource, validation);
2872
+ if (validationArtifact) apiExports.push(`${resource.name}.validation`);
2873
+ const generatedFiles = [
2874
+ [join22(paths.apiDir, `${resource.name}.client.ts`), createClientArtifact(input.normalized, resource.name, resource)],
2875
+ [join22(paths.apiDir, `${resource.name}.types.ts`), createTypeScriptTypes(input.normalized, resource)],
2876
+ [join22(paths.apiDir, `${resource.name}.query-keys.ts`), createQueryKeysArtifact(resource.name, resource)],
2877
+ ...validationArtifact ? [[join22(paths.apiDir, `${resource.name}.validation.ts`), validationArtifact]] : [],
2878
+ [join22(paths.apiDir, "index.ts"), createIndex(apiExports)],
2879
+ ...createFeatureApiFiles(paths.featureApiDir, resource.name, resource, input.composables ?? neutralComposables),
2880
+ ...await createModelFiles(input, resource, paths.featureModelDir, uiModel, templateOverrides),
2881
+ ...createMockFiles(paths.mocksDir, resource)
2882
+ ];
2883
+ return Promise.all(generatedFiles.map(([path, content]) => createFile(input.cwd, path, "generated", content, true)));
2884
+ }
2885
+ function createFeatureApiFiles(featureApiDir, resourceName2, resource, composables) {
2886
+ const entity = resource.entity;
2887
+ const collection = pluralizeTypeName(entity);
2888
+ const crudComposables = [];
2889
+ const crudExports = [];
2890
+ if (resource.operations.list?.id) {
2891
+ crudComposables.push([join22(featureApiDir, `use${collection}Query.ts`), composables.list(resourceName2, resource)]);
2892
+ crudExports.push(`use${collection}Query`);
2893
+ }
2894
+ if (resource.operations.detail?.id) {
2895
+ crudComposables.push([join22(featureApiDir, `use${entity}Query.ts`), composables.detail(resourceName2, resource)]);
2896
+ crudExports.push(`use${entity}Query`);
2897
+ }
2898
+ if (resource.operations.create?.id) {
2899
+ crudComposables.push([join22(featureApiDir, `useCreate${entity}Mutation.ts`), composables.createMutation(resourceName2, resource)]);
2900
+ crudExports.push(`useCreate${entity}Mutation`);
2901
+ }
2902
+ if (resource.operations.update?.id) {
2903
+ crudComposables.push([join22(featureApiDir, `useUpdate${entity}Mutation.ts`), composables.updateMutation(resourceName2, resource)]);
2904
+ crudExports.push(`useUpdate${entity}Mutation`);
2905
+ }
2906
+ if (resource.operations.delete?.id) {
2907
+ crudComposables.push([join22(featureApiDir, `useDelete${entity}Mutation.ts`), composables.deleteMutation(resourceName2, resource)]);
2908
+ crudExports.push(`useDelete${entity}Mutation`);
2909
+ }
2910
+ const generatedOperations = resource.operationsList.filter((operation) => isGeneratedOperation2(resource, operation));
2911
+ const operationComposables = generatedOperations.map((operation) => {
2912
+ const composableName = operationComposableName(operation);
2913
+ return [join22(featureApiDir, `${composableName}.ts`), composables.operation(resourceName2, operation)];
2914
+ });
2915
+ const operationExports = generatedOperations.map(operationComposableName);
2916
+ const exports = [...crudExports, ...operationExports];
2917
+ return [...crudComposables, ...operationComposables, ...exports.length > 0 ? [[join22(featureApiDir, "index.ts"), createIndex(exports)]] : []];
2918
+ }
2919
+ function isGeneratedOperation2(resource, operation) {
2920
+ return operation.operationKind !== "unsupported-operation" && Boolean(operation.id) && !Object.values(resource.operations).includes(operation);
2921
+ }
2922
+ async function createModelFiles(input, resource, featureModelDir, uiModel, templateOverrides) {
2923
+ const config = input.config.resources?.[resource.name];
2924
+ return [
2925
+ [join22(featureModelDir, `${resource.name}.permissions.ts`), createPermissionsArtifact(resource.name, config)],
2926
+ [join22(featureModelDir, `${resource.name}.i18n.ts`), await renderTemplate(templateOverrides.i18n, input.cwd, resource, uiModel, () => createI18nArtifact(resource.name, resource.entity, uiModel))],
2927
+ [join22(featureModelDir, `${resource.name}.config.ts`), createResourceConfigArtifact(resource.name, uiModel)],
2928
+ [join22(featureModelDir, "index.ts"), createIndex([`${resource.name}.permissions`, `${resource.name}.i18n`, `${resource.name}.config`])]
2929
+ ];
2930
+ }
2931
+ function createMockFiles(mocksDir, resource) {
2932
+ return [
2933
+ [join22(mocksDir, `${resource.name}.fixtures.ts`), createFixturesArtifact(resource.name, resource.entity)],
2934
+ [join22(mocksDir, `${resource.name}.handlers.ts`), createHandlersArtifact(resource.name)],
2935
+ [join22(mocksDir, `${resource.name}.scenarios.ts`), createScenariosArtifact(resource.name)],
2936
+ [join22(mocksDir, "index.ts"), createIndex([`${resource.name}.fixtures`, `${resource.name}.handlers`, `${resource.name}.scenarios`])]
2937
+ ];
2938
+ }
2939
+ function resourcePaths(config, resource) {
2940
+ return {
2941
+ apiDir: join22(config.output.generatedDir, resource.name),
2942
+ featureApiDir: join22(config.output.featuresDir, resource.name, "api"),
2943
+ featureModelDir: join22(config.output.featuresDir, resource.name, "model"),
2944
+ mocksDir: join22(config.output.mocksDir ?? "./src/shared/mocks", resource.name)
2945
+ };
2946
+ }
2947
+ async function loadTemplateOverrides(overrides, cwd) {
2948
+ const registry = {};
2949
+ for (const [key, path] of Object.entries(overrides ?? {})) {
2950
+ registry[key] = await loadTemplateOverride(path, cwd);
2951
+ }
2952
+ return registry;
2953
+ }
2954
+ async function renderTemplate(template, cwd, resource, uiModel, fallback) {
2955
+ return template ? template({ cwd, resource, uiModel }) : fallback();
2956
+ }
2957
+ function summarizeFilePlan(files) {
2958
+ return files.reduce(
2959
+ (summary, file) => {
2960
+ if (file.exists && !file.overwrite) summary.protected += 1;
2961
+ else if (file.exists) summary.update += 1;
2962
+ else summary.create += 1;
2963
+ return summary;
2964
+ },
2965
+ { create: 0, update: 0, protected: 0 }
2966
+ );
2967
+ }
2968
+ async function createFile(cwd, path, kind, content, overwrite) {
2969
+ const exists = await fileExists(join22(cwd, path));
2970
+ return {
2971
+ path,
2972
+ content: await formatGeneratedContent(path, content),
2973
+ kind,
2974
+ overwrite,
2975
+ exists
2976
+ };
2977
+ }
2978
+ async function fileExists(path) {
2979
+ return access2(path).then(() => true).catch(() => false);
2980
+ }
2981
+ async function calculateDrift(files, options) {
2982
+ const drift = [];
2983
+ for (const file of files) {
2984
+ if (file.kind !== "generated") continue;
2985
+ const absolutePath = join32(options.cwd, file.path);
2986
+ try {
2987
+ const current = await readFile22(absolutePath, "utf8");
2988
+ if (normalize(current) !== normalize(toWritableGeneratedContent(file))) {
2989
+ drift.push({ path: file.path, kind: "outdated" });
2990
+ }
2991
+ } catch {
2992
+ drift.push({ path: file.path, kind: "missing" });
2993
+ }
2994
+ }
2995
+ return drift;
2996
+ }
2997
+ function normalize(value) {
2998
+ return value.replace(/\r\n/g, "\n").trimEnd();
2999
+ }
3000
+ var crudOperations = ["list", "detail", "create", "update", "delete"];
3001
+ function detectResources(operations) {
3002
+ const grouped = /* @__PURE__ */ new Map();
3003
+ for (const operation of operations) {
3004
+ const resourceName2 = createResourceGroupName(operation);
3005
+ grouped.set(resourceName2, [...grouped.get(resourceName2) ?? [], operation]);
3006
+ }
3007
+ const resourceNames = createIdentifierRegistry();
3008
+ return [...grouped.entries()].map(([rawName, operationsList]) => {
3009
+ const name = resourceNames.identifier(rawName, "resource");
3010
+ const resourceOperations = createCrudOperations(operationsList);
3011
+ const operationsByKind = groupByKind(operationsList);
3012
+ const kind = classifyResourceKind(operationsList, resourceOperations);
3013
+ const missing = crudOperations.filter((operation) => !resourceOperations[operation]);
3014
+ return {
3015
+ name,
3016
+ entity: singularize(toSafeTypeName(name, "Resource")),
3017
+ kind,
3018
+ operationsList,
3019
+ operationsByKind,
3020
+ operations: resourceOperations,
3021
+ missing,
3022
+ isCrudCandidate: resourceOperations.list !== void 0 && resourceOperations.detail !== void 0
3023
+ };
3024
+ });
3025
+ }
3026
+ function createCrudOperations(operations) {
3027
+ const resourceOperations = {};
3028
+ for (const operation of operations) {
3029
+ const crudOperation = classifyCrudOperation(operation);
3030
+ if (!crudOperation || resourceOperations[crudOperation]) continue;
3031
+ resourceOperations[crudOperation] = operation;
3032
+ }
3033
+ return resourceOperations;
3034
+ }
3035
+ function classifyCrudOperation(operation) {
3036
+ if (operation.operationKind !== "crud-resource") return null;
3037
+ const hasResourceIdentityParam = isResourceIdentityPath(operation.path);
3038
+ if (operation.method === "get" && !hasResourceIdentityParam) return "list";
3039
+ if (operation.method === "get" && hasResourceIdentityParam) return "detail";
3040
+ if (operation.method === "post" && !hasResourceIdentityParam) return "create";
3041
+ if ((operation.method === "patch" || operation.method === "put") && hasResourceIdentityParam) return "update";
3042
+ if (operation.method === "delete" && hasResourceIdentityParam) return "delete";
3043
+ return null;
3044
+ }
3045
+ function isResourceIdentityPath(path) {
3046
+ return significantSegments(path).at(-1)?.startsWith("{") ?? false;
3047
+ }
3048
+ function createResourceGroupName(operation) {
3049
+ const segments = meaningfulSegments(operation.path);
3050
+ if (operation.operationKind === "crud-resource") {
3051
+ return createCrudResourceGroupName(operation.path) ?? operation.id ?? "resource";
3052
+ }
3053
+ return segments.length > 0 ? segments.join("-") : operation.id ?? "operation";
3054
+ }
3055
+ function createCrudResourceGroupName(path) {
3056
+ const segments = significantSegments(path);
3057
+ const lastIndex = segments.length - 1;
3058
+ const last = segments.at(lastIndex);
3059
+ if (!last) return null;
3060
+ if (last.startsWith("{")) {
3061
+ return segments.slice(0, lastIndex).filter((segment) => !segment.startsWith("{")).at(-1) ?? null;
3062
+ }
3063
+ return last;
3064
+ }
3065
+ function meaningfulSegments(path) {
3066
+ return significantSegments(path).filter((segment) => !segment.startsWith("{"));
3067
+ }
3068
+ function significantSegments(path) {
3069
+ return path.split("/").filter(Boolean).filter((segment) => !["api", "v1", "v2", "v3"].includes(segment.toLowerCase()));
3070
+ }
3071
+ function groupByKind(operations) {
3072
+ const grouped = {};
3073
+ for (const operation of operations) {
3074
+ grouped[operation.operationKind] = [...grouped[operation.operationKind] ?? [], operation];
3075
+ }
3076
+ return grouped;
3077
+ }
3078
+ function classifyResourceKind(operations, resourceOperations) {
3079
+ const hasCrudSurface = Boolean(resourceOperations.list) || Boolean(resourceOperations.create) || Boolean(resourceOperations.update) || Boolean(resourceOperations.delete) || Boolean(resourceOperations.detail) && operations.every((operation) => operation.operationKind === "crud-resource");
3080
+ if (hasCrudSurface) return "crud-resource";
3081
+ const order = [
3082
+ "file-operation",
3083
+ "search-resource",
3084
+ "action-operation",
3085
+ "dashboard-resource",
3086
+ "read-only-resource",
3087
+ "unsupported-operation"
3088
+ ];
3089
+ return order.find((kind) => operations.some((operation) => operation.operationKind === kind)) ?? "unsupported-operation";
3090
+ }
3091
+ function singularize(value) {
3092
+ if (value.endsWith("ies")) {
3093
+ return `${value.slice(0, -3)}y`;
3094
+ }
3095
+ if (value.endsWith("s")) {
3096
+ return value.slice(0, -1);
3097
+ }
3098
+ return value;
3099
+ }
3100
+ function calculateSchemaHealth(normalized) {
3101
+ const warnings = [];
3102
+ for (const operation of normalized.operations) {
3103
+ const target = `${operation.method.toUpperCase()} ${operation.path}`;
3104
+ if (!operation.id) warnings.push({ code: "operation-id-missing", message: "Operation has no operationId.", target });
3105
+ if (operation.tags.length === 0) warnings.push({ code: "tags-missing", message: "Operation has no tags.", target });
3106
+ if (!operation.responseSchema && !operation.responseBodyEmpty) warnings.push({ code: "response-schema-missing", message: "Operation has no explicit 2xx response schema.", target });
3107
+ if (!operation.hasErrorResponse) warnings.push({ code: "error-response-missing", message: "Operation has no 4xx/5xx response.", target });
3108
+ }
3109
+ const resourceCount = detectResources(normalized.operations).filter((resource) => resource.isCrudCandidate).length;
3110
+ const score = calculateScore(normalized, warnings);
3111
+ return {
3112
+ score,
3113
+ endpointCount: normalized.operations.length,
3114
+ schemaCount: normalized.schemas.length,
3115
+ tagCount: normalized.tags.length,
3116
+ crudCandidateCount: resourceCount,
3117
+ enumCount: normalized.schemas.reduce((count, schema) => count + countEnums(schema.schema), 0),
3118
+ warnings
3119
+ };
3120
+ }
3121
+ function calculateScore(normalized, warnings) {
3122
+ const maxPenalty = normalized.operations.length * 8 + normalized.schemas.length * 2;
3123
+ const warningPenalty = warnings.reduce((sum2, warning) => {
3124
+ if (warning.code === "operation-id-missing") return sum2 + 8;
3125
+ if (warning.code === "tags-missing") return sum2 + 6;
3126
+ if (warning.code === "response-schema-missing") return sum2 + 8;
3127
+ if (warning.code === "error-response-missing") return sum2 + 3;
3128
+ return sum2 + 2;
3129
+ }, 0);
3130
+ const schemaBonus = normalized.schemas.length > 0 ? 5 : 0;
3131
+ const resourceBonus = detectResources(normalized.operations).some((resource) => resource.isCrudCandidate) ? 5 : 0;
3132
+ return Math.max(0, Math.min(100, Math.round(100 - warningPenalty / Math.max(maxPenalty, 1) * 50 + schemaBonus + resourceBonus)));
3133
+ }
3134
+ function countEnums(schema) {
3135
+ const own = schema.enum ? 1 : 0;
3136
+ const propertyEnums = Object.values(schema.properties ?? {}).reduce((count, property) => count + countEnums(property), 0);
3137
+ const itemEnums = schema.items ? countEnums(schema.items) : 0;
3138
+ return own + propertyEnums + itemEnums;
3139
+ }
3140
+ function diffOpenApiContracts(oldSchema, newSchema) {
3141
+ const changes = [];
3142
+ const oldOperations = operationMap(oldSchema.operations);
3143
+ const newOperations = operationMap(newSchema.operations);
3144
+ for (const [key, operation] of oldOperations) {
3145
+ if (!newOperations.has(key)) {
3146
+ const resource = resourceName(operation);
3147
+ changes.push({
3148
+ code: "removed-endpoint",
3149
+ severity: "breaking",
3150
+ resource,
3151
+ location: key,
3152
+ message: `${operation.method.toUpperCase()} ${operation.path} was removed.`
3153
+ });
3154
+ }
3155
+ }
3156
+ for (const [key, operation] of newOperations) {
3157
+ const previous = oldOperations.get(key);
3158
+ const resource = resourceName(operation);
3159
+ if (!previous) {
3160
+ changes.push({
3161
+ code: "added-endpoint",
3162
+ severity: "non-breaking",
3163
+ resource,
3164
+ location: key,
3165
+ message: `${operation.method.toUpperCase()} ${operation.path} was added.`
3166
+ });
3167
+ continue;
3168
+ }
3169
+ changes.push(...diffObjectSchemas(oldSchema, newSchema, previous.requestBodySchema, operation.requestBodySchema, resource, `${key} request`));
3170
+ changes.push(...diffObjectSchemas(oldSchema, newSchema, previous.responseSchema, operation.responseSchema, resource, `${key} response`));
3171
+ }
3172
+ const affectedResources = [...new Set(changes.map((change) => change.resource))].sort();
3173
+ const affectedFiles = affectedResources.flatMap((resource) => [
3174
+ `src/shared/api/generated/${resource}/${resource}.types.ts`,
3175
+ `src/shared/api/generated/${resource}/${resource}.client.ts`,
3176
+ `src/features/${resource}/api/index.ts`
3177
+ ]);
3178
+ const summary = summarizeChanges(changes);
3179
+ const decision = createImpactDecision(summary);
3180
+ const impactedSurface = collectImpactedSurface(changes, oldOperations, newOperations);
3181
+ const migrationHints = createMigrationHints(changes);
3182
+ const changelog = formatContractDiffChangelog({ changes, affectedResources, affectedFiles });
3183
+ return {
3184
+ changes,
3185
+ affectedResources,
3186
+ affectedFiles,
3187
+ changelog,
3188
+ summary,
3189
+ decision,
3190
+ impactedSurface,
3191
+ migrationHints,
3192
+ prSummary: formatPullRequestSummary({ summary, decision, affectedResources, affectedFiles, migrationHints })
3193
+ };
3194
+ }
3195
+ function formatContractDiffChangelog(report) {
3196
+ const lines = [];
3197
+ const byResource = /* @__PURE__ */ new Map();
3198
+ for (const change of report.changes) {
3199
+ byResource.set(change.resource, [...byResource.get(change.resource) ?? [], change]);
3200
+ }
3201
+ for (const [resource, changes] of [...byResource.entries()].sort(([left], [right]) => left.localeCompare(right))) {
3202
+ const breaking = changes.filter((change) => change.severity === "breaking").length;
3203
+ const nonBreaking = changes.filter((change) => change.severity === "non-breaking").length;
3204
+ const warnings = changes.filter((change) => change.severity === "warning").length;
3205
+ const prefix = breaking > 0 ? "BREAKING" : warnings > 0 ? "WARNING" : "CHANGED";
3206
+ lines.push(`${prefix} ${resource}: ${breaking} breaking, ${nonBreaking} non-breaking, ${warnings} warning changes.`);
3207
+ }
3208
+ if (report.affectedFiles.length > 0) {
3209
+ lines.push(`${report.affectedFiles.length} resource contract files affected.`);
3210
+ }
3211
+ return lines;
3212
+ }
3213
+ function operationMap(operations) {
3214
+ return new Map(operations.map((operation) => [`${operation.method.toUpperCase()} ${operation.path}`, operation]));
3215
+ }
3216
+ function summarizeChanges(changes) {
3217
+ const breaking = changes.filter((change) => change.severity === "breaking").length;
3218
+ const warnings = changes.filter((change) => change.severity === "warning").length;
3219
+ const nonBreaking = changes.filter((change) => change.severity === "non-breaking").length;
3220
+ return { breaking, warnings, nonBreaking, total: changes.length };
3221
+ }
3222
+ function createImpactDecision(summary) {
3223
+ if (summary.breaking > 0) {
3224
+ return {
3225
+ status: "blocked",
3226
+ mergeRisk: "high",
3227
+ reason: `${summary.breaking} breaking frontend contract change${summary.breaking === 1 ? "" : "s"} detected.`
3228
+ };
3229
+ }
3230
+ if (summary.warnings > 0) {
3231
+ return {
3232
+ status: "review",
3233
+ mergeRisk: "medium",
3234
+ reason: `${summary.warnings} schema change${summary.warnings === 1 ? "" : "s"} need frontend review.`
3235
+ };
3236
+ }
3237
+ return {
3238
+ status: "approved",
3239
+ mergeRisk: "low",
3240
+ reason: summary.nonBreaking > 0 ? "Only non-breaking frontend contract changes detected." : "No frontend contract changes detected."
3241
+ };
3242
+ }
3243
+ function collectImpactedSurface(changes, oldOperations, newOperations) {
3244
+ const operationIds = /* @__PURE__ */ new Set();
3245
+ for (const change of changes) {
3246
+ const operation = oldOperations.get(operationKeyFromLocation(change.location)) ?? newOperations.get(operationKeyFromLocation(change.location));
3247
+ const operationId = operation?.sourceOperationId ?? operation?.id;
3248
+ if (operationId) operationIds.add(operationId);
3249
+ }
3250
+ const sortedOperationIds = [...operationIds].sort();
3251
+ const resources = detectResources([...new Map([...oldOperations, ...newOperations]).values()]);
3252
+ const queryHooks = /* @__PURE__ */ new Set();
3253
+ for (const id of sortedOperationIds) {
3254
+ queryHooks.add(
3255
+ `use${pascalCase(id)}${id.toLowerCase().startsWith("get") || id.toLowerCase().startsWith("list") || id.toLowerCase().startsWith("search") ? "Query" : "Mutation"}`
3256
+ );
3257
+ const generatedHook = generatedHookName(resources, id);
3258
+ if (generatedHook) queryHooks.add(generatedHook);
3259
+ }
3260
+ return {
3261
+ operationIds: sortedOperationIds,
3262
+ clientMethods: sortedOperationIds.map((id) => `${id}()`),
3263
+ queryHooks: [...queryHooks].sort()
3264
+ };
3265
+ }
3266
+ function generatedHookName(resources, operationId) {
3267
+ const isOp = (operation) => Boolean(operation) && (operation.id === operationId || operation.sourceOperationId === operationId);
3268
+ for (const resource of resources) {
3269
+ const { entity, operations } = resource;
3270
+ if (isOp(operations.list)) return `use${pluralizeTypeName(entity)}Query`;
3271
+ if (isOp(operations.detail)) return `use${entity}Query`;
3272
+ if (isOp(operations.create)) return `useCreate${entity}Mutation`;
3273
+ if (isOp(operations.update)) return `useUpdate${entity}Mutation`;
3274
+ if (isOp(operations.delete)) return `useDelete${entity}Mutation`;
3275
+ const generated = resource.operationsList.find(
3276
+ (operation) => isOp(operation) && operation.operationKind !== "unsupported-operation" && !Object.values(operations).includes(operation)
3277
+ );
3278
+ if (generated) return operationComposableName(generated);
3279
+ }
3280
+ return null;
3281
+ }
3282
+ function operationKeyFromLocation(location) {
3283
+ const match = location.match(/^([A-Z]+)\s+([^\s]+)/);
3284
+ return match ? `${match[1]} ${match[2]}` : location;
3285
+ }
3286
+ function createMigrationHints(changes) {
3287
+ const hints = changes.map((change) => {
3288
+ switch (change.code) {
3289
+ case "removed-endpoint":
3290
+ return `${change.resource}: replace usages before regenerating. ${change.message}`;
3291
+ case "required-field-added":
3292
+ return `${change.resource}: update create/update payload builders and forms for the new required field. ${change.message}`;
3293
+ case "field-removed":
3294
+ return `${change.resource}: remove reads, table columns and form bindings for the deleted field. ${change.message}`;
3295
+ case "type-changed":
3296
+ return `${change.resource}: update TypeScript consumers and runtime formatting for the changed field type. ${change.message}`;
3297
+ case "enum-value-removed":
3298
+ return `${change.resource}: remove UI options and branch handling for the removed enum value. ${change.message}`;
3299
+ case "enum-value-added":
3300
+ return `${change.resource}: add UI labels and handling for the new enum value if this field is user-visible. ${change.message}`;
3301
+ case "request-schema-changed":
3302
+ return `${change.resource}: review request mappers, forms and mutation payloads. ${change.message}`;
3303
+ case "response-schema-changed":
3304
+ return `${change.resource}: review response readers, tables and detail views. ${change.message}`;
3305
+ case "added-endpoint":
3306
+ return `${change.resource}: new endpoint is available after regeneration. ${change.message}`;
3307
+ }
3308
+ });
3309
+ return [...new Set(hints)].slice(0, 30);
3310
+ }
3311
+ function formatPullRequestSummary(input) {
3312
+ const lines = [
3313
+ `Frontend API impact: ${input.decision.status} (${input.decision.mergeRisk} risk).`,
3314
+ input.decision.reason,
3315
+ `Changes: ${input.summary.breaking} breaking, ${input.summary.warnings} warnings, ${input.summary.nonBreaking} non-breaking.`,
3316
+ `Affected resources: ${input.affectedResources.length > 0 ? input.affectedResources.join(", ") : "none"}.`,
3317
+ `Affected generated files: ${input.affectedFiles.length}.`
3318
+ ];
3319
+ if (input.migrationHints.length > 0) {
3320
+ lines.push("Migration hints:");
3321
+ lines.push(...input.migrationHints.slice(0, 5).map((hint) => `- ${hint}`));
3322
+ }
3323
+ return lines.join("\n");
3324
+ }
3325
+ function pascalCase(value) {
3326
+ return value.replace(/[^A-Za-z0-9]+(.)/g, (_match, char) => char.toUpperCase()).replace(/^./, (char) => char.toUpperCase());
3327
+ }
3328
+ function diffObjectSchemas(oldNormalized, newNormalized, oldSchema, newSchema, resource, location) {
3329
+ const oldResolved = resolveSchema(oldNormalized, oldSchema);
3330
+ const newResolved = resolveSchema(newNormalized, newSchema);
3331
+ if (!oldResolved || !newResolved) {
3332
+ if (oldResolved === newResolved) return [];
3333
+ return [
3334
+ {
3335
+ code: oldResolved ? "response-schema-changed" : "request-schema-changed",
3336
+ severity: "warning",
3337
+ resource,
3338
+ location,
3339
+ message: `${location} schema presence changed.`
3340
+ }
3341
+ ];
3342
+ }
3343
+ const changes = [];
3344
+ const oldRequired = new Set(oldResolved.required ?? []);
3345
+ const newRequired = new Set(newResolved.required ?? []);
3346
+ const oldProperties = oldResolved.properties ?? {};
3347
+ const newProperties = newResolved.properties ?? {};
3348
+ for (const name of Object.keys(oldProperties)) {
3349
+ if (!newProperties[name]) {
3350
+ changes.push({
3351
+ code: "field-removed",
3352
+ severity: "breaking",
3353
+ resource,
3354
+ location: `${location}.${name}`,
3355
+ message: `Field "${name}" was removed.`
3356
+ });
3357
+ }
3358
+ }
3359
+ for (const [name, property] of Object.entries(newProperties)) {
3360
+ const previous = oldProperties[name];
3361
+ if (newRequired.has(name) && !oldRequired.has(name)) {
3362
+ changes.push({
3363
+ code: "required-field-added",
3364
+ severity: "breaking",
3365
+ resource,
3366
+ location: `${location}.${name}`,
3367
+ message: `Field "${name}" became required.`
3368
+ });
3369
+ }
3370
+ if (!previous) continue;
3371
+ if (previous.type !== property.type) {
3372
+ changes.push({
3373
+ code: "type-changed",
3374
+ severity: "breaking",
3375
+ resource,
3376
+ location: `${location}.${name}`,
3377
+ message: `Field "${name}" changed type from ${previous.type ?? "unknown"} to ${property.type ?? "unknown"}.`
3378
+ });
3379
+ }
3380
+ changes.push(...diffEnumValues(previous, property, resource, `${location}.${name}`));
3381
+ }
3382
+ return changes;
3383
+ }
3384
+ function diffEnumValues(previous, next, resource, location) {
3385
+ const oldValues = new Set(previous.enum ?? []);
3386
+ const newValues = new Set(next.enum ?? []);
3387
+ const changes = [];
3388
+ for (const value of oldValues) {
3389
+ if (!newValues.has(value)) {
3390
+ changes.push({
3391
+ code: "enum-value-removed",
3392
+ severity: "breaking",
3393
+ resource,
3394
+ location,
3395
+ message: `Enum value "${value}" was removed.`
3396
+ });
3397
+ }
3398
+ }
3399
+ for (const value of newValues) {
3400
+ if (!oldValues.has(value)) {
3401
+ changes.push({
3402
+ code: "enum-value-added",
3403
+ severity: "non-breaking",
3404
+ resource,
3405
+ location,
3406
+ message: `Enum value "${value}" was added.`
3407
+ });
3408
+ }
3409
+ }
3410
+ return changes;
3411
+ }
3412
+ function resourceName(operation) {
3413
+ const tag = operation.tags[0];
3414
+ if (tag) return tag.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/\s+/g, "-").toLowerCase();
3415
+ const firstPathSegment = operation.path.split("/").filter(Boolean).find((segment) => !segment.startsWith("{")) ?? "api";
3416
+ return firstPathSegment.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase();
3417
+ }
3418
+ var httpMethods = ["get", "post", "put", "patch", "delete", "options", "head"];
3419
+ function normalizeOpenApi(document) {
3420
+ const operations = [];
3421
+ const operationIds = createIdentifierRegistry();
3422
+ for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
3423
+ for (const method of httpMethods) {
3424
+ const operation = pathItem[method];
3425
+ if (!operation) {
3426
+ continue;
3427
+ }
3428
+ operations.push(normalizeOperation(document, path, method, operation, pathItem.parameters ?? [], operationIds));
3429
+ }
3430
+ }
3431
+ return {
3432
+ document,
3433
+ operations,
3434
+ schemas: normalizeSchemas(document),
3435
+ tags: normalizeTags(document, operations)
3436
+ };
3437
+ }
3438
+ function normalizeOperation(document, path, method, operation, pathParameters, operationIds) {
3439
+ const requestContentTypes = getRequestContentTypes(operation.requestBody);
3440
+ const responseContentTypes = getResponseContentTypes(operation.responses);
3441
+ const requestBodySchema = extractRequestBodySchema(operation.requestBody);
3442
+ const responseSchema = extractResponseSchema(operation.responses);
3443
+ const responseBodyEmpty = hasEmptySuccessResponse(operation.responses);
3444
+ const hasFilePayload = hasFileContentType(requestContentTypes) || hasFileContentType(responseContentTypes) || hasBinarySchema(requestBodySchema);
3445
+ return {
3446
+ id: operation.operationId ? operationIds.identifier(operation.operationId, `${method}Operation`) : null,
3447
+ sourceOperationId: operation.operationId ?? null,
3448
+ method,
3449
+ path,
3450
+ tags: operation.tags ?? [],
3451
+ summary: operation.summary ?? null,
3452
+ parameters: mergeParameters(document, pathParameters ?? [], operation.parameters ?? []),
3453
+ requestContentTypes,
3454
+ responseContentTypes,
3455
+ isJsonRequest: requestContentTypes.some(isJsonCompatibleContentType2),
3456
+ isJsonResponse: responseContentTypes.some(isJsonCompatibleContentType2),
3457
+ hasFilePayload,
3458
+ operationKind: classifyOperationKind(path, method, operation, requestContentTypes, responseContentTypes, hasFilePayload),
3459
+ requestBodySchema,
3460
+ responseSchema,
3461
+ responseBodyEmpty,
3462
+ hasErrorResponse: Object.keys(operation.responses ?? {}).some((status) => status.startsWith("4") || status.startsWith("5")),
3463
+ operation
3464
+ };
3465
+ }
3466
+ function mergeParameters(document, pathParameters, operationParameters) {
3467
+ const merged = /* @__PURE__ */ new Map();
3468
+ for (const candidate of [...pathParameters, ...operationParameters]) {
3469
+ const parameter = resolveParameterRef(document, candidate);
3470
+ if (!parameter) continue;
3471
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
3472
+ }
3473
+ return [...merged.values()];
3474
+ }
3475
+ function resolveParameterRef(document, parameter) {
3476
+ if (!("$ref" in parameter)) return parameter;
3477
+ const name = parameter.$ref.match(/^#\/components\/parameters\/(.+)$/)?.[1];
3478
+ return name ? document.components?.parameters?.[name] ?? null : null;
3479
+ }
3480
+ function normalizeSchemas(document) {
3481
+ return Object.entries(document.components?.schemas ?? {}).map(([name, schema]) => ({
3482
+ name,
3483
+ schema: mergeSimpleAllOfSchema(document, schema) ?? schema
3484
+ }));
3485
+ }
3486
+ function normalizeTags(document, operations) {
3487
+ const tags = /* @__PURE__ */ new Set();
3488
+ for (const tag of document.tags ?? []) {
3489
+ tags.add(tag.name);
3490
+ }
3491
+ for (const operation of operations) {
3492
+ for (const tag of operation.tags) {
3493
+ tags.add(tag);
3494
+ }
3495
+ }
3496
+ return [...tags];
3497
+ }
3498
+ function extractRequestBodySchema(requestBody) {
3499
+ if (!isObject(requestBody) || !isObject(requestBody.content)) {
3500
+ return null;
3501
+ }
3502
+ return extractSchema(requestBody.content, isJsonCompatibleContentType2) ?? extractSchema(requestBody.content, isMultipartContentType2) ?? extractSchema(requestBody.content, isBinaryContentType2) ?? extractSchema(requestBody.content, isFormUrlEncodedContentType2) ?? extractSchema(requestBody.content, isTextContentType);
3503
+ }
3504
+ function extractResponseSchema(responses) {
3505
+ if (!responses) {
3506
+ return null;
3507
+ }
3508
+ const successStatus = Object.keys(responses).find((status) => status.startsWith("2")) ?? "default";
3509
+ const response = responses[successStatus];
3510
+ if (!response?.content) {
3511
+ return null;
3512
+ }
3513
+ return extractSchema(response.content, isJsonCompatibleContentType2) ?? extractSchema(response.content, isBinaryContentType2) ?? extractSchema(response.content, isWildcardContentType, hasBinarySchema) ?? extractSchema(response.content, isTextContentType) ?? extractSchema(response.content, isWildcardContentType);
3514
+ }
3515
+ function hasEmptySuccessResponse(responses) {
3516
+ if (!responses) return false;
3517
+ const successStatus = Object.keys(responses).find((status) => status.startsWith("2")) ?? "default";
3518
+ const response = responses[successStatus];
3519
+ return Boolean(response && (!response.content || Object.keys(response.content).length === 0));
3520
+ }
3521
+ function extractSchema(content, predicate, schemaPredicate = () => true) {
3522
+ if (!isObject(content)) {
3523
+ return null;
3524
+ }
3525
+ const entry = Object.entries(content).find(([contentType, mediaType2]) => {
3526
+ if (!predicate(contentType) || !isObject(mediaType2) || !isObject(mediaType2.schema)) return false;
3527
+ return schemaPredicate(mediaType2.schema);
3528
+ });
3529
+ const mediaType = entry?.[1];
3530
+ if (!isObject(mediaType) || !isObject(mediaType.schema)) {
3531
+ return null;
3532
+ }
3533
+ return mediaType.schema;
3534
+ }
3535
+ function getRequestContentTypes(requestBody) {
3536
+ if (!isObject(requestBody) || !isObject(requestBody.content)) return [];
3537
+ return Object.keys(requestBody.content).map(normalizeContentType);
3538
+ }
3539
+ function getResponseContentTypes(responses) {
3540
+ return Object.values(responses ?? {}).flatMap((response) => Object.keys(response.content ?? {}).map(normalizeContentType));
3541
+ }
3542
+ function normalizeContentType(contentType) {
3543
+ return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3544
+ }
3545
+ function isJsonCompatibleContentType2(contentType) {
3546
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3547
+ return normalized === "application/json" || normalized.endsWith("+json");
3548
+ }
3549
+ function isMultipartContentType2(contentType) {
3550
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3551
+ return normalized === "multipart/form-data";
3552
+ }
3553
+ function isFormUrlEncodedContentType2(contentType) {
3554
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3555
+ return normalized === "application/x-www-form-urlencoded";
3556
+ }
3557
+ function isBinaryContentType2(contentType) {
3558
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3559
+ return normalized === "application/octet-stream" || normalized === "application/pdf" || normalized.startsWith("image/");
3560
+ }
3561
+ function isTextContentType(contentType) {
3562
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3563
+ return normalized.startsWith("text/") || normalized === "application/text";
3564
+ }
3565
+ function isWildcardContentType(contentType) {
3566
+ return contentType.split(";")[0]?.trim().toLowerCase() === "*/*";
3567
+ }
3568
+ function hasFileContentType(contentTypes) {
3569
+ return contentTypes.some((contentType) => {
3570
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3571
+ return normalized === "multipart/form-data" || normalized === "application/octet-stream";
3572
+ });
3573
+ }
3574
+ function hasBinarySchema(schema) {
3575
+ if (!schema) return false;
3576
+ if (schema.format === "binary") return true;
3577
+ if (schema.items && hasBinarySchema(schema.items)) return true;
3578
+ return Object.values(schema.properties ?? {}).some(hasBinarySchema);
3579
+ }
3580
+ function classifyOperationKind(path, method, operation, requestContentTypes, responseContentTypes, hasFilePayload) {
3581
+ if (method === "options" || method === "head") return "unsupported-operation";
3582
+ if (hasFilePayload || hasFileContentType(requestContentTypes) || hasFileContentType(responseContentTypes)) return "file-operation";
3583
+ const haystack = `${path} ${operation.operationId ?? ""} ${operation.summary ?? ""} ${(operation.tags ?? []).join(" ")}`.toLowerCase();
3584
+ if ((method === "post" || method === "get" && !hasTrailingIdentitySegment(path)) && hasSearchIntent(haystack)) return "search-resource";
3585
+ if ((method === "post" || method === "put" || method === "patch" || method === "delete") && (haystack.includes("/action/") || haystack.includes("/actions/") || !isCanonicalCrudPath(path) || isSubresourceActionPath(path) || hasActionIntent(haystack)) && /(\/actions?\/|approve|archive|confirm|reject|start|submit|generate|import|export|manual|forward|change|block|unblock|details|personal|status|validate|cancel)/.test(haystack)) {
3586
+ return "action-operation";
3587
+ }
3588
+ if (method === "get" && /(dashboard|summary|metrics|stats|statistics|main|categories)/.test(haystack)) return "dashboard-resource";
3589
+ if (method === "get" && (isSubresourceActionPath(path) || hasReadOnlyIntent(haystack))) return "read-only-resource";
3590
+ if (method === "get" && !isCanonicalCrudPath(path)) return "read-only-resource";
3591
+ return "crud-resource";
3592
+ }
3593
+ function isCanonicalCrudPath(path) {
3594
+ const segments = path.split("/").filter(Boolean).filter((segment) => !["api", "v1", "v2", "v3"].includes(segment.toLowerCase()));
3595
+ const last = segments.at(-1);
3596
+ if (!last) return false;
3597
+ if (last.startsWith("{")) return true;
3598
+ return true;
3599
+ }
3600
+ function hasTrailingIdentitySegment(path) {
3601
+ return significantSegments2(path).at(-1)?.startsWith("{") ?? false;
3602
+ }
3603
+ function isSubresourceActionPath(path) {
3604
+ const segments = significantSegments2(path);
3605
+ const last = segments.at(-1)?.toLowerCase();
3606
+ if (!last || last.startsWith("{")) return false;
3607
+ const hasParentIdentity = segments.slice(0, -1).some((segment) => segment.startsWith("{"));
3608
+ return hasParentIdentity && /^(details?|personal|status|state|validate|validation|cancel|confirm|approve|archive|reject)$/.test(last);
3609
+ }
3610
+ function significantSegments2(path) {
3611
+ return path.split("/").filter(Boolean).filter((segment) => !["api", "v1", "v2", "v3"].includes(segment.toLowerCase()));
3612
+ }
3613
+ function hasActionIntent(value) {
3614
+ return /(approve|archive|confirm|reject|start|submit|generate|import|export|manual|forward|change|block|unblock|validate|cancel)/.test(value);
3615
+ }
3616
+ function hasSearchIntent(value) {
3617
+ return /(^|[^a-z0-9])(search|find|query|filter)([^a-z0-9]|$)/.test(value);
3618
+ }
3619
+ function hasReadOnlyIntent(value) {
3620
+ return /(lookup|options|reference|preview|history|status|state|availability)/.test(value);
3621
+ }
3622
+ function isObject(value) {
3623
+ return typeof value === "object" && value !== null;
3624
+ }
3625
+ async function parseOpenApi(filePath, options = {}) {
3626
+ const loaded = isRemoteInput2(filePath) ? await fetchOpenApiSource(filePath, options) : await readOpenApiSource(filePath);
3627
+ const value = parseOpenApiSource(loaded.source, filePath, loaded.kind);
3628
+ if (!isOpenApiDocument(value)) {
3629
+ throw new ForgeError(`Invalid OpenAPI document: ${filePath}`, {
3630
+ reason: "The document does not contain a valid OpenAPI 3.x version.",
3631
+ suggestion: "Add an `openapi: 3.x.x` field at the document root."
3632
+ });
3633
+ }
3634
+ return value;
3635
+ }
3636
+ async function readOpenApiSource(filePath) {
3637
+ const source = await readFile32(filePath, "utf8").catch((error) => {
3638
+ throw new ForgeError(`Failed to read OpenAPI schema: ${filePath}`, {
3639
+ reason: error instanceof Error ? error.message : "Unknown file system error",
3640
+ suggestion: "Check that the schema path exists and is readable."
3641
+ });
3642
+ });
3643
+ return { source, kind: filePath.endsWith(".json") ? "json" : "yaml" };
3644
+ }
3645
+ async function fetchOpenApiSource(filePath, options) {
3646
+ const fetchImpl = options.fetchImpl ?? fetch;
3647
+ const controller = new AbortController();
3648
+ const timeoutReason = options.timeoutMs ? `Request timed out after ${options.timeoutMs}ms` : void 0;
3649
+ const timeout = timeoutReason ? setTimeout(() => controller.abort(new DOMException(timeoutReason, "TimeoutError")), options.timeoutMs) : void 0;
3650
+ try {
3651
+ const response = await fetchImpl(filePath, {
3652
+ headers: options.headers,
3653
+ signal: controller.signal
3654
+ });
3655
+ if (!response.ok) {
3656
+ throw new ForgeError(`Failed to fetch OpenAPI schema: ${filePath}`, {
3657
+ reason: `${response.status} ${response.statusText}`.trim(),
3658
+ suggestion: "Check the remote schema URL, credentials and server availability."
3659
+ });
3660
+ }
3661
+ const contentType = response.headers.get("content-type") ?? "";
3662
+ return {
3663
+ source: await response.text(),
3664
+ kind: contentType.includes("json") || filePath.endsWith(".json") ? "json" : "yaml"
3665
+ };
3666
+ } catch (error) {
3667
+ if (error instanceof ForgeError) throw error;
3668
+ const reason = error instanceof Error ? error.message : "Unknown network error";
3669
+ throw new ForgeError(`Failed to fetch OpenAPI schema: ${filePath}${reason ? ` (${reason})` : ""}`, {
3670
+ reason,
3671
+ suggestion: "Check the remote schema URL, network access and configured request headers."
3672
+ });
3673
+ } finally {
3674
+ if (timeout) clearTimeout(timeout);
3675
+ }
3676
+ }
3677
+ function parseOpenApiSource(source, filePath, kind) {
3678
+ try {
3679
+ if (kind === "json") {
3680
+ return JSON.parse(source);
3681
+ }
3682
+ return parseYaml(source);
3683
+ } catch (error) {
3684
+ throw new ForgeError(`Failed to parse OpenAPI schema: ${filePath}`, {
3685
+ reason: error instanceof Error ? error.message : "Unknown parse error",
3686
+ suggestion: "Use valid JSON or YAML syntax."
3687
+ });
3688
+ }
3689
+ }
3690
+ function isRemoteInput2(value) {
3691
+ return value.startsWith("http://") || value.startsWith("https://");
3692
+ }
3693
+ function isOpenApiDocument(value) {
3694
+ return typeof value === "object" && value !== null && "openapi" in value && typeof value.openapi === "string" && value.openapi.startsWith("3.");
3695
+ }
3696
+ function lintOpenApi(normalized, options = {}) {
3697
+ const disabled = new Set(options.disabledRules ?? []);
3698
+ const diagnostics = [];
3699
+ const operationIds = /* @__PURE__ */ new Map();
3700
+ for (const operation of normalized.operations) {
3701
+ const location = `${operation.method.toUpperCase()} ${operation.path}`;
3702
+ if (operation.operation.operationId) {
3703
+ operationIds.set(operation.operation.operationId, [...operationIds.get(operation.operation.operationId) ?? [], location]);
3704
+ }
3705
+ if (!operation.operation.operationId) {
3706
+ diagnostics.push({
3707
+ severity: "warning",
3708
+ code: "missing-operation-id",
3709
+ message: `${location} does not define operationId.`,
3710
+ location,
3711
+ suggestion: "Add a stable operationId so generated method names do not depend on fallback naming."
3712
+ });
3713
+ }
3714
+ if (operation.tags.length === 0) {
3715
+ diagnostics.push({
3716
+ severity: "warning",
3717
+ code: "missing-tags",
3718
+ message: `${location} has no tags, so resource detection is less precise.`,
3719
+ location,
3720
+ suggestion: "Add a resource tag such as Users, Orders or Reports."
3721
+ });
3722
+ }
3723
+ if (operation.tags.length > 1) {
3724
+ diagnostics.push({
3725
+ severity: "warning",
3726
+ code: "multiple-resource-tags",
3727
+ message: `${location} has multiple tags, so resource ownership is ambiguous.`,
3728
+ location,
3729
+ suggestion: "Use one primary resource tag and move secondary grouping into summary or operationId naming."
3730
+ });
3731
+ }
3732
+ for (const name of extractPathParameters(operation.path)) {
3733
+ if (!operation.parameters.some((parameter) => parameter.in === "path" && parameter.name === name)) {
3734
+ diagnostics.push({
3735
+ severity: "warning",
3736
+ code: "path-template-parameter-missing",
3737
+ message: `${location} uses path parameter "${name}" without a matching OpenAPI path parameter.`,
3738
+ location,
3739
+ suggestion: "Define every path template parameter with in: path and required: true."
3740
+ });
3741
+ }
3742
+ }
3743
+ if ((operation.method === "post" || operation.method === "put" || operation.method === "patch") && !operation.requestBodySchema) {
3744
+ diagnostics.push({
3745
+ severity: "warning",
3746
+ code: "missing-request-schema",
3747
+ message: `${location} has no JSON request schema.`,
3748
+ location,
3749
+ suggestion: "Add an application/json request schema for typed generated payloads."
3750
+ });
3751
+ }
3752
+ if (operation.method !== "delete" && !operation.responseSchema && !operation.responseBodyEmpty) {
3753
+ diagnostics.push({
3754
+ severity: "warning",
3755
+ code: "missing-response-schema",
3756
+ message: `${location} has no 2xx JSON response schema.`,
3757
+ location,
3758
+ suggestion: "Add an application/json 2xx response schema for typed generated responses."
3759
+ });
3760
+ }
3761
+ if (!operation.hasErrorResponse) {
3762
+ diagnostics.push({
3763
+ severity: "warning",
3764
+ code: "missing-error-response",
3765
+ message: `${location} does not document a 4xx or 5xx response.`,
3766
+ location,
3767
+ suggestion: "Document common error responses so frontend clients can model failure states."
3768
+ });
3769
+ }
3770
+ if (operation.operation.operationId && toSafeIdentifier(operation.operation.operationId, "operation") !== operation.operation.operationId) {
3771
+ diagnostics.push({
3772
+ severity: "warning",
3773
+ code: "unsafe-identifiers",
3774
+ message: `${location} has an operationId that must be sanitized for TypeScript output.`,
3775
+ location,
3776
+ suggestion: "Use alphanumeric camelCase operationId values when possible."
3777
+ });
3778
+ }
3779
+ }
3780
+ for (const [operationId, locations] of operationIds) {
3781
+ if (locations.length < 2) continue;
3782
+ diagnostics.push({
3783
+ severity: "warning",
3784
+ code: "duplicate-operation-id",
3785
+ message: `operationId "${operationId}" is used by ${locations.length} operations.`,
3786
+ location: locations.slice(0, 3).join(", ") + (locations.length > 3 ? ", ..." : ""),
3787
+ suggestion: "Use stable unique operationId values so generated method names do not require collision suffixes."
3788
+ });
3789
+ }
3790
+ diagnostics.push(...collectDiagnostics(normalized));
3791
+ const filtered = dedupeDiagnostics(diagnostics.filter((diagnostic) => !disabled.has(diagnostic.code)));
3792
+ const grouped = filtered.reduce((groups, diagnostic) => {
3793
+ groups[diagnostic.code] = [...groups[diagnostic.code] ?? [], diagnostic];
3794
+ return groups;
3795
+ }, {});
3796
+ const errors = filtered.filter((diagnostic) => diagnostic.severity === "error").length;
3797
+ const warnings = filtered.length - errors;
3798
+ const ruleCount = Object.keys(grouped).length;
3799
+ const score = Math.max(0, 100 - errors * 20 - ruleCount * 6 - warnings);
3800
+ const ok = options.strict ? filtered.length === 0 : errors === 0;
3801
+ return { ok, score, diagnostics: filtered, grouped };
3802
+ }
3803
+ function dedupeDiagnostics(diagnostics) {
3804
+ const seen = /* @__PURE__ */ new Set();
3805
+ const deduped = [];
3806
+ for (const diagnostic of diagnostics) {
3807
+ const key = `${diagnostic.code}:${diagnostic.location ?? diagnostic.message}`;
3808
+ if (seen.has(key)) continue;
3809
+ seen.add(key);
3810
+ deduped.push(diagnostic);
3811
+ }
3812
+ return deduped;
3813
+ }
3814
+
3815
+ // ../adapters/dist/index.js
3816
+ var tanstackQueryComposables = createQueryComposables("@tanstack/react-query");
3817
+ var vueQueryComposables = createQueryComposables("@tanstack/vue-query");
3818
+ function resolveQueryComposables(target) {
3819
+ if (target === "tanstack-query") return tanstackQueryComposables;
3820
+ if (target === "vue-query") return vueQueryComposables;
3821
+ return void 0;
3822
+ }
3823
+ function createQueryComposables(queryModule) {
3824
+ return {
3825
+ list: (resourceName2, resource) => listHook(queryModule, resourceName2, resource),
3826
+ detail: (resourceName2, resource) => detailHook(queryModule, resourceName2, resource),
3827
+ createMutation: (resourceName2, resource) => createMutationHook(queryModule, resourceName2, resource),
3828
+ updateMutation: (resourceName2, resource) => updateMutationHook(queryModule, resourceName2, resource),
3829
+ deleteMutation: (resourceName2, resource) => deleteMutationHook(queryModule, resourceName2, resource),
3830
+ operation: (resourceName2, operation) => operationHook(queryModule, resourceName2, operation)
3831
+ };
3832
+ }
3833
+ function importPaths(resourceName2) {
3834
+ const base = `../../../shared/api/generated/${resourceName2}/${resourceName2}`;
3835
+ return { client: `${base}.client`, keys: `${base}.query-keys`, types: `${base}.types` };
3836
+ }
3837
+ function clientName(resourceName2) {
3838
+ return `${resourceName2}Client`;
3839
+ }
3840
+ function keysName(resourceName2) {
3841
+ return `${resourceName2}QueryKeys`;
3842
+ }
3843
+ function missingHook(name) {
3844
+ return `export function ${name}(): never {
3845
+ throw new Error('${name} is not available: missing OpenAPI operation for this resource.')
3846
+ }
3847
+ `;
3848
+ }
3849
+ function invalidateOnSuccess(invalidation, usesVariables) {
3850
+ const signature = usesVariables ? "(_data, variables)" : "()";
3851
+ return `onSuccess: ${signature} => {
3852
+ queryClient.invalidateQueries({ queryKey: ${invalidation} })
3853
+ },`;
3854
+ }
3855
+ function listHook(queryModule, resourceName2, resource) {
3856
+ const collection = pluralizeTypeName(resource.entity);
3857
+ if (!resource.operations.list?.id) return missingHook(`use${collection}Query`);
3858
+ const names = createResourceTypeNames(resource);
3859
+ const paths = importPaths(resourceName2);
3860
+ const requiresParams = getPathParams(resource.operations.list).length > 0;
3861
+ return `import { useQuery, type UseQueryOptions } from '${queryModule}'
3862
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3863
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3864
+ import type { ${names.listParamsType}, ${names.listResponseType} } from '${paths.types}'
3865
+
3866
+ export function use${collection}Query(
3867
+ params${requiresParams ? "" : "?"}: ${names.listParamsType},
3868
+ options?: Omit<UseQueryOptions<${names.listResponseType}>, 'queryKey' | 'queryFn'>,
3869
+ ) {
3870
+ return useQuery({
3871
+ queryKey: ${keysName(resourceName2)}.list(params),
3872
+ queryFn: () => ${clientName(resourceName2)}.${resource.operations.list.id}(params),
3873
+ ...options,
3874
+ })
3875
+ }
3876
+ `;
3877
+ }
3878
+ function detailHook(queryModule, resourceName2, resource) {
3879
+ if (!resource.operations.detail?.id) return missingHook(`use${resource.entity}Query`);
3880
+ const names = createResourceTypeNames(resource);
3881
+ const paths = importPaths(resourceName2);
3882
+ return `import { useQuery, type UseQueryOptions } from '${queryModule}'
3883
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3884
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3885
+ import type { ${names.detailResponseType}, ${names.idType} } from '${paths.types}'
3886
+
3887
+ export function use${resource.entity}Query(
3888
+ id: ${names.idType},
3889
+ options?: Omit<UseQueryOptions<${names.detailResponseType}>, 'queryKey' | 'queryFn'>,
3890
+ ) {
3891
+ return useQuery({
3892
+ queryKey: ${keysName(resourceName2)}.detail(id),
3893
+ queryFn: () => ${clientName(resourceName2)}.${resource.operations.detail.id}(id),
3894
+ ...options,
3895
+ })
3896
+ }
3897
+ `;
3898
+ }
3899
+ function createMutationHook(queryModule, resourceName2, resource) {
3900
+ if (!resource.operations.create?.id) return missingHook(`useCreate${resource.entity}Mutation`);
3901
+ const names = createResourceTypeNames(resource);
3902
+ const paths = importPaths(resourceName2);
3903
+ const hasPathParams = getPathParams(resource.operations.create).length > 0;
3904
+ const inputType = hasPathParams ? `{ params: ${names.listParamsType}; payload: ${names.createRequestType} }` : names.createRequestType;
3905
+ const callArgs = hasPathParams ? "input.params, input.payload" : "input";
3906
+ const invalidation = hasPathParams ? `${keysName(resourceName2)}.list(variables.params)` : `${keysName(resourceName2)}.list()`;
3907
+ const typeImports = hasPathParams ? `${names.createRequestType}, ${names.createResponseType}, ${names.listParamsType}` : `${names.createRequestType}, ${names.createResponseType}`;
3908
+ return `import { useMutation, useQueryClient, type UseMutationOptions } from '${queryModule}'
3909
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3910
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3911
+ import type { ${typeImports} } from '${paths.types}'
3912
+
3913
+ export function useCreate${resource.entity}Mutation(
3914
+ options?: Omit<UseMutationOptions<${names.createResponseType}, Error, ${inputType}>, 'mutationFn'>,
3915
+ ) {
3916
+ const queryClient = useQueryClient()
3917
+ return useMutation({
3918
+ mutationFn: (input: ${inputType}) => ${clientName(resourceName2)}.${resource.operations.create.id}(${callArgs}),
3919
+ ${invalidateOnSuccess(invalidation, hasPathParams)}
3920
+ ...options,
3921
+ })
3922
+ }
3923
+ `;
3924
+ }
3925
+ function updateMutationHook(queryModule, resourceName2, resource) {
3926
+ if (!resource.operations.update?.id) return missingHook(`useUpdate${resource.entity}Mutation`);
3927
+ const names = createResourceTypeNames(resource);
3928
+ const paths = importPaths(resourceName2);
3929
+ const inputType = `{ id: ${names.idType}; payload: ${names.updateRequestType} }`;
3930
+ return `import { useMutation, useQueryClient, type UseMutationOptions } from '${queryModule}'
3931
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3932
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3933
+ import type { ${names.idType}, ${names.updateRequestType}, ${names.updateResponseType} } from '${paths.types}'
3934
+
3935
+ export function useUpdate${resource.entity}Mutation(
3936
+ options?: Omit<UseMutationOptions<${names.updateResponseType}, Error, ${inputType}>, 'mutationFn'>,
3937
+ ) {
3938
+ const queryClient = useQueryClient()
3939
+ return useMutation({
3940
+ mutationFn: (input: ${inputType}) => ${clientName(resourceName2)}.${resource.operations.update.id}(input.id, input.payload),
3941
+ ${invalidateOnSuccess(`${keysName(resourceName2)}.detail(variables.id)`, true)}
3942
+ ...options,
3943
+ })
3944
+ }
3945
+ `;
3946
+ }
3947
+ function deleteMutationHook(queryModule, resourceName2, resource) {
3948
+ if (!resource.operations.delete?.id) return missingHook(`useDelete${resource.entity}Mutation`);
3949
+ const names = createResourceTypeNames(resource);
3950
+ const paths = importPaths(resourceName2);
3951
+ const invalidatesDetail = getPathParams(resource.operations.delete).length > 1;
3952
+ const invalidation = invalidatesDetail ? `${keysName(resourceName2)}.detail(variables)` : `${keysName(resourceName2)}.list()`;
3953
+ const onSuccess = invalidateOnSuccess(invalidation, invalidatesDetail);
3954
+ return `import { useMutation, useQueryClient, type UseMutationOptions } from '${queryModule}'
3955
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3956
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3957
+ import type { ${names.idType} } from '${paths.types}'
3958
+
3959
+ export function useDelete${resource.entity}Mutation(
3960
+ options?: Omit<UseMutationOptions<void, Error, ${names.idType}>, 'mutationFn'>,
3961
+ ) {
3962
+ const queryClient = useQueryClient()
3963
+ return useMutation({
3964
+ mutationFn: (id: ${names.idType}) => ${clientName(resourceName2)}.${resource.operations.delete.id}(id),
3965
+ ${onSuccess}
3966
+ ...options,
3967
+ })
3968
+ }
3969
+ `;
3970
+ }
3971
+ function operationHook(queryModule, resourceName2, operation) {
3972
+ const names = createOperationTypeNames(operation);
3973
+ const hookName = operationComposableName(operation);
3974
+ const paths = importPaths(resourceName2);
3975
+ const hasPayload = Boolean(operation.requestBodySchema);
3976
+ const hasParams = getOperationParams(operation).length > 0;
3977
+ const isQuery = operation.method === "get";
3978
+ const inputType = operationInputType2(names, hasPayload, hasParams);
3979
+ const hasInput = inputType !== "void";
3980
+ const callArgs = operationCallArgs(hasPayload, hasParams);
3981
+ const typeImports = [
3982
+ ...hasPayload ? [names.requestType] : [],
3983
+ ...hasParams ? [names.paramsType] : [],
3984
+ names.responseType
3985
+ ].join(", ");
3986
+ if (isQuery) {
3987
+ const keyEntries = hasInput ? `[...${keysName(resourceName2)}.all, '${operation.id}', input]` : `[...${keysName(resourceName2)}.all, '${operation.id}']`;
3988
+ return `import { useQuery, type UseQueryOptions } from '${queryModule}'
3989
+ import { ${clientName(resourceName2)} } from '${paths.client}'
3990
+ import { ${keysName(resourceName2)} } from '${paths.keys}'
3991
+ import type { ${typeImports} } from '${paths.types}'
3992
+
3993
+ export function ${hookName}(
3994
+ ${hasInput ? ` input: ${inputType},
3995
+ ` : ""} options?: Omit<UseQueryOptions<${names.responseType}>, 'queryKey' | 'queryFn'>,
3996
+ ) {
3997
+ return useQuery({
3998
+ queryKey: ${keyEntries},
3999
+ queryFn: () => ${clientName(resourceName2)}.${operation.id}(${callArgs}),
4000
+ ...options,
4001
+ })
4002
+ }
4003
+ `;
4004
+ }
4005
+ return `import { useMutation, type UseMutationOptions } from '${queryModule}'
4006
+ import { ${clientName(resourceName2)} } from '${paths.client}'
4007
+ import type { ${typeImports} } from '${paths.types}'
4008
+
4009
+ export function ${hookName}(
4010
+ options?: Omit<UseMutationOptions<${names.responseType}, Error, ${inputType}>, 'mutationFn'>,
4011
+ ) {
4012
+ return useMutation({
4013
+ mutationFn: (${hasInput ? `input: ${inputType}` : ""}) => ${clientName(resourceName2)}.${operation.id}(${callArgs}),
4014
+ ...options,
4015
+ })
4016
+ }
4017
+ `;
4018
+ }
4019
+ function operationInputType2(names, hasPayload, hasParams) {
4020
+ if (hasPayload && hasParams)
4021
+ return `{ payload: ${names.requestType}; params: ${names.paramsType} }`;
4022
+ if (hasPayload) return names.requestType;
4023
+ if (hasParams) return names.paramsType;
4024
+ return "void";
4025
+ }
4026
+ function operationCallArgs(hasPayload, hasParams) {
4027
+ if (hasPayload && hasParams) return "input.payload, input.params";
4028
+ if (hasPayload || hasParams) return "input";
4029
+ return "";
4030
+ }
4031
+
4032
+ // src/audit-package.ts
4033
+ var execFileAsync = promisify(execFile);
4034
+ async function runAuditPackage(schema, options) {
4035
+ const outDir = resolve4(options.out ?? "forge-audit");
4036
+ const minHealthScore = parseMinHealthScore(options.minHealthScore) ?? 90;
4037
+ const loaded = await loadCliConfigSet(schema, options);
4038
+ const entries = await Promise.all(loaded.map(runAuditEntry));
4039
+ const primary = entries[0];
4040
+ if (!primary) throw new Error("No OpenAPI schema inputs were resolved.");
4041
+ await mkdir3(outDir, { recursive: true });
4042
+ await writeGeneratedPreview(outDir, entries);
4043
+ const typecheck = options.skipTypecheck ? createSkippedTypecheck(outDir) : await runGeneratedOutputTypecheck(outDir, entries);
4044
+ const payload = createAuditPayload(entries, { outDir, minHealthScore, typecheck });
4045
+ const html = createHtmlReport("Archora Forge Audit", payload);
4046
+ const markdown = createAuditMarkdown(payload);
4047
+ await Promise.all([
4048
+ writeReportFile(join4(outDir, "index.html"), html),
4049
+ writeReportFile(join4(outDir, "report.json"), JSON.stringify(payload, null, 2)),
4050
+ writeReportFile(join4(outDir, "report.md"), markdown),
4051
+ writeReportFile(join4(outDir, "adoption-plan.md"), createAdoptionPlan(payload)),
4052
+ writeReportFile(join4(outDir, "ci.yml"), createCiWorkflow(payload)),
4053
+ writeReportFile(join4(outDir, "typecheck.md"), createTypecheckMarkdown(typecheck))
4054
+ ]);
4055
+ return { outDir, entries, payload };
4056
+ }
4057
+ async function runAuditEntry(loaded) {
4058
+ const document = await parseOpenApi(loaded.schema, loaded.config.schemaRequest);
4059
+ const normalized = normalizeOpenApi(document);
4060
+ const resources = detectResources(normalized.operations).filter(
4061
+ (resource) => loaded.config.resources[resource.name]?.enabled !== false
4062
+ );
4063
+ const plan = await createGenerationPlan({
4064
+ config: loaded.config,
4065
+ normalized,
4066
+ resources,
4067
+ cwd: loaded.cwd,
4068
+ composables: resolveQueryComposables(loaded.config.target.query)
4069
+ });
4070
+ const fileSummary = summarizeFilePlan(plan.files);
4071
+ const diagnostics = collectDiagnostics(normalized);
4072
+ const coverage = createSchemaCoverageMatrix(normalized, diagnostics);
4073
+ const drift = await calculateDrift(plan.files, { cwd: loaded.cwd });
4074
+ return {
4075
+ name: loaded.name ?? "default",
4076
+ schema: loaded.schema,
4077
+ configPath: loaded.configPath,
4078
+ healthScore: calculateSchemaHealth(normalized).score,
4079
+ resources: resources.length,
4080
+ generatedFiles: plan.files.filter((file) => file.kind === "generated").length,
4081
+ protectedFiles: fileSummary.protected,
4082
+ drift,
4083
+ diagnostics,
4084
+ coverage,
4085
+ plan,
4086
+ resourceExplorer: resources.map((resource) => createResourceExplorerEntry(resource, plan))
4087
+ };
4088
+ }
4089
+ function createAuditPayload(entries, options) {
4090
+ const diagnostics = entries.flatMap((entry) => entry.diagnostics);
4091
+ const drift = entries.flatMap((entry) => entry.drift);
4092
+ const healthScore = Math.min(...entries.map((entry) => entry.healthScore));
4093
+ const generatedFiles = entries.reduce((total, entry) => total + entry.generatedFiles, 0);
4094
+ const protectedFiles = entries.reduce((total, entry) => total + entry.protectedFiles, 0);
4095
+ const resources = entries.reduce((total, entry) => total + entry.resources, 0);
4096
+ const coverage = mergeSchemaCoverageMatrices(entries.map((entry) => entry.coverage));
4097
+ const generator = summarizeAuditGeneratorMetadata(entries);
4098
+ const scorecard = createScorecard({
4099
+ healthScore,
4100
+ diagnostics,
4101
+ drift,
4102
+ coverage,
4103
+ typecheck: options.typecheck
4104
+ });
4105
+ const failedChecks = [
4106
+ ...drift.length > 0 ? ["drift"] : [],
4107
+ ...healthScore < options.minHealthScore ? ["health-score"] : [],
4108
+ ...diagnostics.some((diagnostic) => diagnostic.severity === "error") ? ["errors"] : [],
4109
+ ...options.typecheck.status === "failed" ? ["generated-typecheck"] : []
4110
+ ];
4111
+ const ok = failedChecks.length === 0;
4112
+ return {
4113
+ ok,
4114
+ schema: displayPath(entries[0]?.schema ?? ""),
4115
+ schemas: entries.map((entry) => ({
4116
+ name: entry.name,
4117
+ schema: displayPath(entry.schema),
4118
+ configPath: entry.configPath ? displayPath(entry.configPath) : null,
4119
+ healthScore: entry.healthScore,
4120
+ resources: entry.resources,
4121
+ generatedFiles: entry.generatedFiles,
4122
+ protectedFiles: entry.protectedFiles,
4123
+ driftCount: entry.drift.length,
4124
+ diagnosticsCount: entry.diagnostics.length,
4125
+ coverage: entry.coverage
4126
+ })),
4127
+ audit: {
4128
+ outDir: displayPath(options.outDir),
4129
+ generatedPreview: displayPath(join4(options.outDir, "generated-preview")),
4130
+ artifacts: [
4131
+ "index.html",
4132
+ "report.md",
4133
+ "report.json",
4134
+ "typecheck.md",
4135
+ "ci.yml",
4136
+ "adoption-plan.md",
4137
+ "generated-preview/"
4138
+ ]
4139
+ },
4140
+ healthScore,
4141
+ resources,
4142
+ generatedFiles,
4143
+ protectedFiles,
4144
+ failedChecks,
4145
+ scorecard,
4146
+ generator,
4147
+ coverage,
4148
+ typecheck: options.typecheck,
4149
+ resourceExplorer: entries.flatMap((entry) => entry.resourceExplorer),
4150
+ fixSuggestions: createFixSuggestions(diagnostics),
4151
+ readiness: createAuditReadiness({
4152
+ ok,
4153
+ healthScore,
4154
+ minHealthScore: options.minHealthScore,
4155
+ diagnostics,
4156
+ drift,
4157
+ typecheck: options.typecheck,
4158
+ generatedFiles,
4159
+ resources
4160
+ }),
4161
+ drift,
4162
+ diagnostics
4163
+ };
4164
+ }
4165
+ function createResourceExplorerEntry(resource, plan) {
4166
+ return {
4167
+ name: resource.name,
4168
+ entity: resource.entity,
4169
+ kind: resource.kind,
4170
+ operations: resource.operationsList.map((operation) => ({
4171
+ method: operation.method.toUpperCase(),
4172
+ path: operation.path,
4173
+ operationId: operation.id,
4174
+ kind: operation.operationKind
4175
+ })),
4176
+ generatedFiles: plan.files.filter(
4177
+ (file) => file.path.includes(`/features/${resource.name}/`) || file.path.includes(`/shared/api/generated/${resource.name}/`)
4178
+ ).map((file) => file.path).sort()
4179
+ };
4180
+ }
4181
+ async function writeGeneratedPreview(outDir, entries) {
4182
+ await Promise.all(
4183
+ entries.flatMap(
4184
+ (entry) => entry.plan.files.filter((file) => file.kind === "generated").map(async (file) => {
4185
+ const path = join4(outDir, "generated-preview", entry.name, file.path);
4186
+ await mkdir3(dirname4(path), { recursive: true });
4187
+ await writeFile3(path, file.content, "utf8");
4188
+ })
4189
+ )
4190
+ );
4191
+ }
4192
+ async function runGeneratedOutputTypecheck(outDir, entries) {
4193
+ const workspace = join4(outDir, "generated-output-typecheck");
4194
+ const tsconfigPath = join4(workspace, "tsconfig.json");
4195
+ const srcDir = join4(workspace, "src");
4196
+ await Promise.all(
4197
+ entries.flatMap(
4198
+ (entry) => entry.plan.files.filter((file) => file.kind === "generated" && file.path.endsWith(".ts")).map(async (file) => {
4199
+ const path = join4(srcDir, entry.name, file.path);
4200
+ await mkdir3(dirname4(path), { recursive: true });
4201
+ await writeFile3(path, file.content, "utf8");
4202
+ })
4203
+ )
4204
+ );
4205
+ await writeFile3(tsconfigPath, JSON.stringify(createTypecheckTsconfig(workspace), null, 2), "utf8");
4206
+ const command = `pnpm exec tsc -p ${displayPath(tsconfigPath)}`;
4207
+ try {
4208
+ await execFileAsync("pnpm", ["exec", "tsc", "-p", tsconfigPath], {
4209
+ cwd: process.cwd(),
4210
+ timeout: 12e4
4211
+ });
4212
+ return { status: "passed", command, workspace: displayPath(workspace), errors: [] };
4213
+ } catch (error) {
4214
+ const details = error;
4215
+ const output = [details.stdout, details.stderr, details.message].filter(Boolean).join("\n").trim();
4216
+ return {
4217
+ status: "failed",
4218
+ command,
4219
+ workspace: displayPath(workspace),
4220
+ errors: output ? output.split("\n").slice(0, 80) : [`tsc failed with code ${String(details.code ?? "unknown")}`]
4221
+ };
4222
+ }
4223
+ }
4224
+ function createTypecheckTsconfig(workspace) {
4225
+ const runtimeSource = relative3(
4226
+ workspace,
4227
+ resolve4(process.cwd(), "packages/runtime/src/index.ts")
4228
+ ).replace(/\\/g, "/");
4229
+ return {
4230
+ compilerOptions: {
4231
+ baseUrl: ".",
4232
+ module: "ESNext",
4233
+ moduleResolution: "Bundler",
4234
+ noEmit: true,
4235
+ paths: {
4236
+ "@archora/forge-runtime": [
4237
+ runtimeSource,
4238
+ "./node_modules/@archora/forge-runtime/dist/index.d.ts"
4239
+ ]
4240
+ },
4241
+ skipLibCheck: true,
4242
+ strict: true,
4243
+ target: "ES2022"
4244
+ },
4245
+ include: ["src/**/*.ts"]
4246
+ };
4247
+ }
4248
+ function createSkippedTypecheck(outDir) {
4249
+ return {
4250
+ status: "skipped",
4251
+ command: "skipped by --skip-typecheck",
4252
+ workspace: displayPath(join4(outDir, "generated-output-typecheck")),
4253
+ errors: []
4254
+ };
4255
+ }
4256
+ function createScorecard(input) {
4257
+ const errorCount = input.diagnostics.filter(
4258
+ (diagnostic) => diagnostic.severity === "error"
4259
+ ).length;
4260
+ const warningCount = input.diagnostics.filter(
4261
+ (diagnostic) => diagnostic.severity === "warning"
4262
+ ).length;
4263
+ return {
4264
+ frontendReadiness: clamp(
4265
+ input.healthScore - errorCount * 15 - warningCount * 2 - input.drift.length * 2
4266
+ ),
4267
+ typeSafety: input.typecheck.status === "passed" ? 100 : input.typecheck.status === "skipped" ? 70 : 30,
4268
+ resourceCoverage: input.coverage.operations.total === 0 ? 100 : Math.round(input.coverage.operations.generated / input.coverage.operations.total * 100),
4269
+ driftSafety: input.drift.length === 0 ? 100 : clamp(100 - input.drift.length * 10),
4270
+ ciAdoption: input.typecheck.status === "failed" ? 60 : 100
4271
+ };
4272
+ }
4273
+ function createFixSuggestions(diagnostics) {
4274
+ const groups = /* @__PURE__ */ new Map();
4275
+ for (const diagnostic of diagnostics) {
4276
+ groups.set(diagnostic.code, [...groups.get(diagnostic.code) ?? [], diagnostic]);
4277
+ }
4278
+ return [...groups.entries()].map(([code, items]) => ({
4279
+ code,
4280
+ count: items.length,
4281
+ suggestion: items.find((item) => item.suggestion)?.suggestion ?? defaultSuggestion(code)
4282
+ })).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
4283
+ }
4284
+ function defaultSuggestion(code) {
4285
+ if (code.includes("response-schema"))
4286
+ return "Add explicit 2xx JSON response schemas for generated response types.";
4287
+ if (code.includes("request-schema"))
4288
+ return "Add explicit application/json request body schemas for generated request types.";
4289
+ if (code.startsWith("unsupported-"))
4290
+ return "Keep the endpoint in custom code or simplify the schema shape for generated v1 adoption.";
4291
+ return "Review the diagnostic and decide whether the schema or the adoption scope should change.";
4292
+ }
4293
+ function createAuditReadiness(input) {
4294
+ const blockers = [
4295
+ ...input.healthScore < input.minHealthScore ? [`Health score ${input.healthScore} is below the audit threshold ${input.minHealthScore}.`] : [],
4296
+ ...input.drift.length > 0 ? ["Generated output drift is present."] : [],
4297
+ ...input.diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `Error diagnostic: ${diagnostic.code}.`),
4298
+ ...input.typecheck.status === "failed" ? ["Generated TypeScript typecheck failed."] : []
4299
+ ];
4300
+ const warnings = [
4301
+ ...input.diagnostics.some((diagnostic) => diagnostic.severity === "warning") ? [
4302
+ `${input.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length} warning diagnostics need review.`
4303
+ ] : [],
4304
+ ...input.typecheck.status === "skipped" ? ["Generated TypeScript typecheck was skipped."] : []
4305
+ ];
4306
+ const nextActions = [
4307
+ ...input.drift.length > 0 ? ["Run `archora-forge generate` and review generated drift."] : [],
4308
+ ...input.typecheck.status === "failed" ? ["Open `typecheck.md`, fix generator/schema blockers, then rerun `archora-forge audit`."] : [],
4309
+ ...input.diagnostics.length > 0 ? ["Use fix suggestions to decide which OpenAPI changes are required before purchase."] : [],
4310
+ ...input.ok ? ["Use this audit package as the paid pilot evidence bundle."] : []
4311
+ ];
4312
+ const status = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "needs-attention" : "ready";
4313
+ const gate = status === "ready" ? {
4314
+ result: "pass",
4315
+ recommendedCiMode: "block",
4316
+ reason: "Generated resource layer is ready for a blocking CI gate under the current policy."
4317
+ } : status === "needs-attention" ? {
4318
+ result: "warn",
4319
+ recommendedCiMode: "comment",
4320
+ reason: "Continue evaluation, but require explicit owner review before blocking merges."
4321
+ } : {
4322
+ result: "fail",
4323
+ recommendedCiMode: "block",
4324
+ reason: "Do not widen rollout until blockers are fixed or explicitly accepted."
4325
+ };
4326
+ return {
4327
+ status,
4328
+ gate,
4329
+ decision: input.ok ? "Generated resource layer is ready for local adoption review." : "Generated resource layer needs fixes or explicit risk acceptance before purchase.",
4330
+ blockers,
4331
+ warnings,
4332
+ nextActions: nextActions.length > 0 ? nextActions : ["No action required."],
4333
+ reviewerChecklist: [
4334
+ "Open `index.html` and confirm detected resources match the frontend mental model.",
4335
+ "Open `report.md` and review blockers, warnings, scorecard and fix suggestions.",
4336
+ "Open `typecheck.md` and confirm generated TypeScript passed or every failure is triaged.",
4337
+ "Compare generated files with the existing API layer before committing rollout scope."
4338
+ ],
4339
+ summary: {
4340
+ healthScore: input.healthScore,
4341
+ resources: input.resources,
4342
+ generatedFiles: input.generatedFiles,
4343
+ protectedFiles: 0,
4344
+ diagnostics: input.diagnostics.length,
4345
+ drift: input.drift.length,
4346
+ failedChecks: blockers.length
4347
+ }
4348
+ };
4349
+ }
4350
+ function summarizeAuditGeneratorMetadata(entries) {
4351
+ const files = entries.flatMap((entry) => entry.plan.files);
4352
+ const total = files.filter((file) => file.kind === "generated").length;
4353
+ return {
4354
+ status: "previewed",
4355
+ version: files.find((file) => file.metadata)?.metadata?.version ?? "unknown",
4356
+ files: {
4357
+ total,
4358
+ missingMetadata: files.filter((file) => file.kind === "generated" && !file.metadata).map((file) => ({ path: file.path })),
4359
+ versionMismatches: [],
4360
+ schemaHashMismatches: [],
4361
+ configHashMismatches: []
4362
+ }
4363
+ };
4364
+ }
4365
+ function createAuditMarkdown(payload) {
4366
+ return `# Archora Forge Audit
4367
+
4368
+ Status: ${payload.ok ? "passed" : "failed"}
4369
+
4370
+ ## Executive Review
4371
+
4372
+ Readiness: ${payload.readiness.status}
4373
+
4374
+ Gate: ${payload.readiness.gate.result}
4375
+
4376
+ Recommended CI mode: ${payload.readiness.gate.recommendedCiMode}
4377
+
4378
+ Gate reason: ${payload.readiness.gate.reason}
4379
+
4380
+ Decision: ${payload.readiness.decision}
4381
+
4382
+ Health score: ${payload.healthScore}
4383
+
4384
+ Resources: ${payload.resources}
4385
+
4386
+ Generated files: ${payload.generatedFiles}
4387
+
4388
+ Typecheck: ${payload.typecheck.status}
4389
+
4390
+ ## Scorecard
4391
+
4392
+ ${Object.entries(payload.scorecard).map(([key, value]) => `- ${key}: ${value}`).join("\n")}
4393
+
4394
+ ## Fix Suggestions
4395
+
4396
+ ${payload.fixSuggestions.length > 0 ? payload.fixSuggestions.map((item) => `- ${item.code} (${item.count}): ${item.suggestion}`).join("\n") : "- No fix suggestions."}
4397
+
4398
+ ## Reviewer Checklist
4399
+
4400
+ ${payload.readiness.reviewerChecklist.map((item) => `- ${item}`).join("\n")}
4401
+
4402
+ ## Artifacts
4403
+
4404
+ ${payload.audit.artifacts.map((artifact) => `- ${artifact}`).join("\n")}
4405
+ `;
4406
+ }
4407
+ function createAdoptionPlan(payload) {
4408
+ return `# Adoption Plan
4409
+
4410
+ Decision: ${payload.readiness.decision}
4411
+
4412
+ ## Next Actions
4413
+
4414
+ ${payload.readiness.nextActions.map((action) => `- ${action}`).join("\n")}
4415
+
4416
+ ## Acceptance Gates
4417
+
4418
+ - Check report status: ${payload.ok ? "passed" : "failed"}
4419
+ - Generated TypeScript typecheck: ${payload.typecheck.status}
4420
+ - Drift entries: ${payload.drift.length}
4421
+ - Diagnostics: ${payload.diagnostics.length}
4422
+
4423
+ ## First Resource Candidates
4424
+
4425
+ ${payload.resourceExplorer.slice(0, 10).map(
4426
+ (resource) => `- ${resource.name}: ${resource.operations.length} operations, ${resource.generatedFiles.length} generated files`
4427
+ ).join("\n")}
4428
+ `;
4429
+ }
4430
+ function createCiWorkflow(payload) {
4431
+ return `name: archora-forge-audit
4432
+
4433
+ on:
4434
+ pull_request:
4435
+ push:
4436
+ branches: [main]
4437
+
4438
+ jobs:
4439
+ forge-audit:
4440
+ runs-on: ubuntu-latest
4441
+ steps:
4442
+ - uses: actions/checkout@v4
4443
+ - uses: pnpm/action-setup@v4
4444
+ - uses: actions/setup-node@v4
4445
+ with:
4446
+ node-version: 22
4447
+ cache: pnpm
4448
+ - run: pnpm install --frozen-lockfile
4449
+ - run: pnpm exec archora-forge audit ${payload.schema} --out forge-audit
4450
+ - uses: actions/upload-artifact@v4
4451
+ if: always()
4452
+ with:
4453
+ name: archora-forge-audit
4454
+ path: forge-audit
4455
+ `;
4456
+ }
4457
+ function createTypecheckMarkdown(typecheck) {
4458
+ return `# Generated Output Typecheck
4459
+
4460
+ - Status: ${typecheck.status}
4461
+ - Command: \`${typecheck.command}\`
4462
+ - Workspace: \`${typecheck.workspace}\`
4463
+
4464
+ ## Errors
4465
+
4466
+ ${typecheck.errors.length > 0 ? typecheck.errors.map((line) => `- ${line}`).join("\n") : "- None."}
4467
+ `;
4468
+ }
4469
+ function parseMinHealthScore(value) {
4470
+ if (value === void 0) return void 0;
4471
+ const parsed = Number(value);
4472
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100) {
4473
+ throw new Error(`Invalid minimum health score "${value}". Expected a number between 0 and 100.`);
4474
+ }
4475
+ return parsed;
4476
+ }
4477
+ function clamp(value) {
4478
+ return Math.max(0, Math.min(100, Math.round(value)));
4479
+ }
4480
+ function displayPath(path) {
4481
+ if (!path) return path;
4482
+ const relativePath = relative3(process.cwd(), path).replace(/\\/g, "/");
4483
+ return relativePath && !relativePath.startsWith("..") ? relativePath : path.replace(/\\/g, "/");
4484
+ }
4485
+
4486
+ // src/license.ts
4487
+ import { mkdir as mkdir4, readFile as readFile4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
4488
+ import { Buffer } from "buffer";
4489
+ import { webcrypto } from "crypto";
4490
+ import { homedir } from "os";
4491
+ import { dirname as dirname5, join as join5 } from "path";
4492
+ import { TextEncoder } from "util";
4493
+ var LICENSE_PREFIX = "ARCHORA-FORGE-";
4494
+ var CLOCK_SKEW_MS = 5 * 60 * 1e3;
4495
+ function resolvePublicKeyJwk() {
4496
+ const embedded = true ? '{"key_ops":["verify"],"ext":true,"kty":"EC","x":"ywdUlfoB7-K8wgdlEZtA7HdxmFk2pdxfhgNS_BKp3e8","y":"HTlnIHOKdtJXA5bueRZKLxkFTCtICneFMvaNqDC1Fx4","crv":"P-256"}' : "";
4497
+ return embedded || process.env.ARCHORA_FORGE_LICENSE_PUBLIC_KEY_JWK || "";
4498
+ }
4499
+ function isLicenseEnforcementConfigured() {
4500
+ return resolvePublicKeyJwk().length > 0;
4501
+ }
4502
+ async function activateLicenseKey(licenseKey, options = {}) {
4503
+ const validation = await validateLicenseKey(licenseKey, options);
4504
+ if (validation.status !== "active") return validation;
4505
+ const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
4506
+ await writeStoredLicense({
4507
+ key: licenseKey.trim(),
4508
+ activatedAt: now,
4509
+ lastSeenAt: now
4510
+ });
4511
+ return validation;
4512
+ }
4513
+ async function getStoredLicenseStatus(options = {}) {
4514
+ const stored = await readStoredLicense();
4515
+ if (!stored) return result("missing", null, "License key is not activated");
4516
+ const validation = await validateLicenseKey(stored.key, { ...options, lastSeenAt: stored.lastSeenAt });
4517
+ if (validation.status === "active") {
4518
+ await writeStoredLicense({
4519
+ ...stored,
4520
+ lastSeenAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString()
4521
+ });
4522
+ }
4523
+ return validation;
4524
+ }
4525
+ async function removeStoredLicense() {
4526
+ await rm2(licenseFilePath(), { force: true });
4527
+ }
4528
+ async function requireCommercialLicense(command) {
4529
+ if (!isLicenseEnforcementConfigured()) return;
4530
+ const status = await getStoredLicenseStatus();
4531
+ if (status.status === "active") return;
4532
+ throw new Error(
4533
+ [
4534
+ `License key required for "${command}".`,
4535
+ status.message,
4536
+ "Run: archora-forge license activate <license-key>",
4537
+ "For evaluation or purchase, contact akotov@archora.dev or Telegram @akotofff."
4538
+ ].join("\n")
4539
+ );
4540
+ }
4541
+ async function validateLicenseKey(licenseKey, options = {}) {
4542
+ const now = options.now ?? /* @__PURE__ */ new Date();
4543
+ const publicKey = options.publicKey ?? publicLicenseKey();
4544
+ if (!publicKey) return result("missingPublicKey", null, "License public key is not configured");
4545
+ const parsed = parseLicenseKey(licenseKey);
4546
+ if (!parsed) return result("invalid", null, "Invalid license format");
4547
+ const payload = parsePayload(parsed.payloadSegment);
4548
+ if (!payload) return result("invalid", null, "Invalid license payload");
4549
+ const verified = await verifySignature(publicKey, parsed.payloadSegment, parsed.signature);
4550
+ if (!verified) return result("invalid", payload, "Invalid license signature");
4551
+ if (isClockRollback(now, options.lastSeenAt)) {
4552
+ return result("clockRollback", payload, "System clock is earlier than the last CLI run");
4553
+ }
4554
+ const expiresAt = Date.parse(payload.expiresAt);
4555
+ if (!Number.isFinite(expiresAt) || now.getTime() > expiresAt) {
4556
+ return result("expired", payload, "License has expired");
4557
+ }
4558
+ return result("active", payload, "License is active");
4559
+ }
4560
+ function publicLicenseKey() {
4561
+ const raw = resolvePublicKeyJwk();
4562
+ if (!raw) return null;
4563
+ try {
4564
+ return JSON.parse(raw);
4565
+ } catch {
4566
+ return null;
4567
+ }
4568
+ }
4569
+ function licenseFilePath() {
4570
+ if (process.env.ARCHORA_FORGE_LICENSE_FILE) return process.env.ARCHORA_FORGE_LICENSE_FILE;
4571
+ const configHome = process.env.XDG_CONFIG_HOME ?? join5(homedir(), ".config");
4572
+ return join5(configHome, "archora-forge", "license.json");
4573
+ }
4574
+ async function readStoredLicense() {
4575
+ try {
4576
+ const parsed = JSON.parse(await readFile4(licenseFilePath(), "utf8"));
4577
+ if (typeof parsed.key !== "string" || typeof parsed.activatedAt !== "string" || typeof parsed.lastSeenAt !== "string") {
4578
+ return null;
4579
+ }
4580
+ return {
4581
+ key: parsed.key,
4582
+ activatedAt: parsed.activatedAt,
4583
+ lastSeenAt: parsed.lastSeenAt
4584
+ };
4585
+ } catch {
4586
+ return null;
4587
+ }
4588
+ }
4589
+ async function writeStoredLicense(value) {
4590
+ const path = licenseFilePath();
4591
+ await mkdir4(dirname5(path), { recursive: true });
4592
+ await writeFile4(path, `${JSON.stringify(value, null, 2)}
4593
+ `, "utf8");
4594
+ }
4595
+ function parseLicenseKey(input) {
4596
+ const trimmed = input.trim();
4597
+ if (!trimmed.startsWith(LICENSE_PREFIX)) return null;
4598
+ const body = trimmed.slice(LICENSE_PREFIX.length);
4599
+ const [payloadSegment, signatureSegment, extra] = body.split(".");
4600
+ if (!payloadSegment || !signatureSegment || extra) return null;
4601
+ try {
4602
+ return {
4603
+ payloadSegment,
4604
+ signature: base64UrlToBytes(signatureSegment)
4605
+ };
4606
+ } catch {
4607
+ return null;
4608
+ }
4609
+ }
4610
+ function parsePayload(segment) {
4611
+ try {
4612
+ const raw = Buffer.from(base64UrlToBase64(segment), "base64").toString("utf8");
4613
+ const payload = JSON.parse(raw);
4614
+ if (typeof payload.licenseId !== "string" || typeof payload.customer !== "string" || typeof payload.issuedAt !== "string" || typeof payload.expiresAt !== "string" || !isLicensePlan(payload.plan)) {
4615
+ return null;
4616
+ }
4617
+ return {
4618
+ licenseId: payload.licenseId,
4619
+ customer: payload.customer,
4620
+ issuedAt: payload.issuedAt,
4621
+ expiresAt: payload.expiresAt,
4622
+ plan: payload.plan
4623
+ };
4624
+ } catch {
4625
+ return null;
4626
+ }
4627
+ }
4628
+ async function verifySignature(publicKey, payloadSegment, signature) {
4629
+ try {
4630
+ const key = await webcrypto.subtle.importKey("jwk", publicKey, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
4631
+ return webcrypto.subtle.verify(
4632
+ { name: "ECDSA", hash: "SHA-256" },
4633
+ key,
4634
+ bytesToArrayBuffer(signature),
4635
+ new TextEncoder().encode(payloadSegment)
4636
+ );
4637
+ } catch {
4638
+ return false;
4639
+ }
4640
+ }
4641
+ function isLicensePlan(value) {
4642
+ return value === "trial" || value === "pilot" || value === "team" || value === "organization";
4643
+ }
4644
+ function isClockRollback(now, lastSeenAt) {
4645
+ if (!lastSeenAt) return false;
4646
+ const lastSeen = Date.parse(lastSeenAt);
4647
+ if (!Number.isFinite(lastSeen)) return false;
4648
+ return now.getTime() + CLOCK_SKEW_MS < lastSeen;
4649
+ }
4650
+ function base64UrlToBytes(value) {
4651
+ return new Uint8Array(Buffer.from(base64UrlToBase64(value), "base64"));
4652
+ }
4653
+ function base64UrlToBase64(value) {
4654
+ return value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
4655
+ }
4656
+ function bytesToArrayBuffer(bytes) {
4657
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
4658
+ }
4659
+ function result(status, payload, message) {
4660
+ return { status, payload, message };
4661
+ }
4662
+
4663
+ export {
4664
+ writeGeneratedFiles,
4665
+ findPrunableGeneratedFiles,
4666
+ pruneGeneratedFiles,
4667
+ collectDiagnostics,
4668
+ createSchemaCoverageMatrix,
4669
+ mergeSchemaCoverageMatrices,
4670
+ createGenerationPlan,
4671
+ summarizeFilePlan,
4672
+ calculateDrift,
4673
+ detectResources,
4674
+ calculateSchemaHealth,
4675
+ diffOpenApiContracts,
4676
+ normalizeOpenApi,
4677
+ parseOpenApi,
4678
+ lintOpenApi,
4679
+ resolveQueryComposables,
4680
+ defineForgeConfig,
4681
+ createForgeConfigPreset,
4682
+ parseSchemaRequestHeaders,
4683
+ loadCliConfig,
4684
+ loadCliConfigSet,
4685
+ writeReportFile,
4686
+ printPendingPhaseMessage,
4687
+ logger,
4688
+ createHtmlReport,
4689
+ formatImpactReport,
4690
+ formatImpactMarkdown,
4691
+ formatPullRequestComment,
4692
+ formatSourceUsageLines,
4693
+ scanSourceUsages,
4694
+ runAuditPackage,
4695
+ isLicenseEnforcementConfigured,
4696
+ activateLicenseKey,
4697
+ getStoredLicenseStatus,
4698
+ removeStoredLicense,
4699
+ requireCommercialLicense,
4700
+ validateLicenseKey
4701
+ };