@supacloud/compiler 0.23.0 → 0.25.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.
package/dist/index.js CHANGED
@@ -77,6 +77,86 @@ function findClosestMatch(target, candidates) {
77
77
  }
78
78
  var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
79
79
 
80
+ // src/contract-manifest.ts
81
+ function buildContractManifest(graph, artifacts) {
82
+ const commands = [];
83
+ const queries = [];
84
+ const routes = [];
85
+ const permissions = new Set;
86
+ const rpc = [];
87
+ const events = [];
88
+ const fixtures = new Set;
89
+ for (const module of graph.modules) {
90
+ for (const command of module.commands) {
91
+ if (command.permission)
92
+ permissions.add(command.permission);
93
+ if (command.rpc)
94
+ rpc.push({ command: command.name, adapter: command.rpc });
95
+ if (command.audit)
96
+ events.push({ name: command.audit, source: "command.audit", command: command.name });
97
+ commands.push({
98
+ name: command.name,
99
+ className: command.className,
100
+ module: module.name,
101
+ ...command.permission === undefined ? {} : { permission: command.permission },
102
+ ...command.rpc === undefined ? {} : { rpc: command.rpc },
103
+ transaction: command.transaction,
104
+ idempotency: command.idempotency,
105
+ ...command.audit === undefined ? {} : { auditEvent: command.audit }
106
+ });
107
+ }
108
+ for (const query of module.queries) {
109
+ queries.push({ name: query.name, className: query.className, module: module.name });
110
+ }
111
+ for (const controller of module.controllers) {
112
+ for (const route of controller.routes) {
113
+ const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
114
+ if (command?.permission)
115
+ permissions.add(command.permission);
116
+ if (route.contract?.evidence)
117
+ fixtures.add(route.contract.evidence);
118
+ routes.push({
119
+ method: route.method,
120
+ path: joinRoutePaths(controller.path, route.path),
121
+ module: module.name,
122
+ controller: controller.className,
123
+ handler: route.handler,
124
+ ...route.command === undefined ? {} : { command: route.command },
125
+ ...command?.permission === undefined ? {} : { permission: command.permission },
126
+ requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
127
+ ...route.response === undefined ? {} : { responseSchema: route.response },
128
+ ...route.responses === undefined ? {} : {
129
+ responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
130
+ },
131
+ ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
132
+ });
133
+ }
134
+ }
135
+ }
136
+ commands.sort((left, right) => left.name.localeCompare(right.name));
137
+ queries.sort((left, right) => left.name.localeCompare(right.name));
138
+ routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
139
+ rpc.sort((left, right) => left.command.localeCompare(right.command));
140
+ events.sort((left, right) => left.name.localeCompare(right.name));
141
+ return {
142
+ version: 1,
143
+ commands,
144
+ queries,
145
+ routes,
146
+ permissions: [...permissions].sort(),
147
+ rpc,
148
+ events,
149
+ fixtures: [...fixtures].sort(),
150
+ artifacts: {
151
+ ...artifacts,
152
+ sdk: "generated-client",
153
+ openapiDocument: "generated",
154
+ permissionManifest: "generated"
155
+ }
156
+ };
157
+ }
158
+ var init_contract_manifest = () => {};
159
+
80
160
  // src/generate.ts
81
161
  import { createHash as createHash4 } from "node:crypto";
82
162
  import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
@@ -218,9 +298,16 @@ function renderApplication(graph, options) {
218
298
  const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
219
299
  const openApiCode = options.generateOpenApi ? renderOpenApi(graph, options) : undefined;
220
300
  const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
301
+ const contractsManifest = buildContractManifest(graph, {
302
+ client: clientCode !== undefined,
303
+ openapi: openApiCode !== undefined,
304
+ permissions: permissionsCode !== undefined
305
+ });
221
306
  return {
222
307
  applicationCode: code,
223
308
  manifestJson: JSON.stringify(manifest, null, 2) + `
309
+ `,
310
+ contractsManifestJson: JSON.stringify(contractsManifest, null, 2) + `
224
311
  `,
225
312
  ...clientCode === undefined ? {} : { clientCode },
226
313
  ...openApiCode === undefined ? {} : { openApiCode },
@@ -232,9 +319,11 @@ async function generateApplication(graph, options) {
232
319
  await mkdir(options.outDir, { recursive: true });
233
320
  const applicationPath = join2(options.outDir, "application.ts");
234
321
  const manifestPath = join2(options.outDir, "app.manifest.json");
322
+ const contractsManifestPath = join2(options.outDir, "contracts.manifest.json");
235
323
  const writeCandidates = [
236
324
  { path: applicationPath, content: rendered.applicationCode },
237
- { path: manifestPath, content: rendered.manifestJson }
325
+ { path: manifestPath, content: rendered.manifestJson },
326
+ { path: contractsManifestPath, content: rendered.contractsManifestJson }
238
327
  ];
239
328
  if (rendered.clientCode) {
240
329
  writeCandidates.push({ path: join2(options.outDir, "client.ts"), content: rendered.clientCode });
@@ -2095,7 +2184,9 @@ function destroyScopeInstances(
2095
2184
  scopeDestructions.set(scope, destruction);
2096
2185
  return destruction;
2097
2186
  }`;
2098
- var init_generate = () => {};
2187
+ var init_generate = __esm(() => {
2188
+ init_contract_manifest();
2189
+ });
2099
2190
 
2100
2191
  // src/graphql-client.ts
2101
2192
  var GRAPHQL_CLIENT_SOURCE = `
@@ -2608,9 +2699,9 @@ var init_graphql = __esm(() => {
2608
2699
  });
2609
2700
 
2610
2701
  // src/graphql-schema.ts
2611
- import { mkdir as mkdir5, readFile as readFile11 } from "node:fs/promises";
2612
- import { createHash as createHash9 } from "node:crypto";
2613
- import { dirname as dirname10, resolve as resolve15 } from "node:path";
2702
+ import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
2703
+ import { createHash as createHash10 } from "node:crypto";
2704
+ import { dirname as dirname10, resolve as resolve16 } from "node:path";
2614
2705
  async function pullGraphqlSchema(options) {
2615
2706
  assertGraphqlOptions({ schema: options.output });
2616
2707
  const endpoint = new URL(options.url);
@@ -2650,7 +2741,7 @@ async function pullGraphqlSchema(options) {
2650
2741
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
2651
2742
  }
2652
2743
  const schema = lexicographicSortSchema(buildClientSchema(data));
2653
- const path = resolve15(options.output);
2744
+ const path = resolve16(options.output);
2654
2745
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
2655
2746
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
2656
2747
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -2658,13 +2749,13 @@ async function pullGraphqlSchema(options) {
2658
2749
  `;
2659
2750
  let previous;
2660
2751
  try {
2661
- previous = await readFile11(path, "utf8");
2752
+ previous = await readFile12(path, "utf8");
2662
2753
  } catch (error) {
2663
2754
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2664
2755
  throw error;
2665
2756
  }
2666
2757
  const upToDate = previous === content;
2667
- const schemaHash = createHash9("sha256").update(content).digest("hex");
2758
+ const schemaHash = createHash10("sha256").update(content).digest("hex");
2668
2759
  if (options.check)
2669
2760
  return { path, schemaHash, upToDate, written: false };
2670
2761
  await mkdir5(dirname10(path), { recursive: true });
@@ -2677,12 +2768,12 @@ var init_graphql_schema = __esm(() => {
2677
2768
  });
2678
2769
 
2679
2770
  // src/database-contracts.ts
2680
- import { createHash as createHash10 } from "node:crypto";
2681
- import { mkdir as mkdir6, readFile as readFile12 } from "node:fs/promises";
2682
- import { dirname as dirname11, relative as relative12, resolve as resolve16 } from "node:path";
2771
+ import { createHash as createHash11 } from "node:crypto";
2772
+ import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
2773
+ import { dirname as dirname11, relative as relative12, resolve as resolve17 } from "node:path";
2683
2774
  import * as ts13 from "@typescript/typescript6";
2684
2775
  function hash2(value) {
2685
- return createHash10("sha256").update(value).digest("hex");
2776
+ return createHash11("sha256").update(value).digest("hex");
2686
2777
  }
2687
2778
  function importPath(out, path) {
2688
2779
  const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
@@ -2707,19 +2798,19 @@ function parseDatabaseContractsOptions(value, directory) {
2707
2798
  throw new TypeError("migrations must be an ordered list of SQL files");
2708
2799
  }
2709
2800
  return {
2710
- rootDir: resolve16(directory, field("rootDir")),
2711
- outDir: resolve16(directory, field("outDir")),
2712
- postgrestTypes: resolve16(directory, field("postgrestTypes")),
2713
- drizzleSchema: resolve16(directory, field("drizzleSchema")),
2801
+ rootDir: resolve17(directory, field("rootDir")),
2802
+ outDir: resolve17(directory, field("outDir")),
2803
+ postgrestTypes: resolve17(directory, field("postgrestTypes")),
2804
+ drizzleSchema: resolve17(directory, field("drizzleSchema")),
2714
2805
  role: field("role"),
2715
- graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
2716
- migrations: migrations.map((file) => resolve16(directory, file))
2806
+ graphql: { ...graphql, schema: resolve17(directory, graphql.schema) },
2807
+ migrations: migrations.map((file) => resolve17(directory, file))
2717
2808
  };
2718
2809
  }
2719
2810
  async function generateDatabaseContracts(options, check = false) {
2720
- const rootDir = resolve16(options.rootDir), outDir = resolve16(options.outDir);
2721
- const postgrestTypes = resolve16(rootDir, options.postgrestTypes), drizzleSchema = resolve16(rootDir, options.drizzleSchema);
2722
- const snapshot = await readFile12(postgrestTypes, "utf8");
2811
+ const rootDir = resolve17(options.rootDir), outDir = resolve17(options.outDir);
2812
+ const postgrestTypes = resolve17(rootDir, options.postgrestTypes), drizzleSchema = resolve17(rootDir, options.drizzleSchema);
2813
+ const snapshot = await readFile13(postgrestTypes, "utf8");
2723
2814
  const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
2724
2815
  const database = syntax.statements.find((node) => (ts13.isTypeAliasDeclaration(node) || ts13.isInterfaceDeclaration(node)) && node.name.text === "Database" && node.modifiers?.some((modifier) => modifier.kind === ts13.SyntaxKind.ExportKeyword));
2725
2816
  if (!database)
@@ -2746,8 +2837,8 @@ async function generateDatabaseContracts(options, check = false) {
2746
2837
  }
2747
2838
  const inputs = {};
2748
2839
  const addInput = async (path) => {
2749
- const absolute = resolve16(rootDir, path);
2750
- inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile12(absolute, "utf8"));
2840
+ const absolute = resolve17(rootDir, path);
2841
+ inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile13(absolute, "utf8"));
2751
2842
  };
2752
2843
  await addInput(postgrestTypes);
2753
2844
  await addInput(drizzleSchema);
@@ -2763,8 +2854,8 @@ async function generateDatabaseContracts(options, check = false) {
2763
2854
  if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
2764
2855
  await addInput(source.fileName);
2765
2856
  }
2766
- await addInput(resolve16(rootDir, options.graphql.schema));
2767
- if (new Set(options.migrations.map((path) => resolve16(rootDir, path))).size !== options.migrations.length) {
2857
+ await addInput(resolve17(rootDir, options.graphql.schema));
2858
+ if (new Set(options.migrations.map((path) => resolve17(rootDir, path))).size !== options.migrations.length) {
2768
2859
  throw new Error("Duplicate migration in database contracts configuration");
2769
2860
  }
2770
2861
  for (const path of options.migrations)
@@ -2785,17 +2876,17 @@ async function generateDatabaseContracts(options, check = false) {
2785
2876
  version: 1,
2786
2877
  role: options.role,
2787
2878
  inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
2788
- migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
2879
+ migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve17(rootDir, path)).replaceAll("\\", "/")),
2789
2880
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
2790
2881
  };
2791
2882
  files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
2792
2883
  `;
2793
2884
  const mismatches = [];
2794
2885
  for (const [file, content] of Object.entries(files)) {
2795
- const path = resolve16(outDir, file);
2886
+ const path = resolve17(outDir, file);
2796
2887
  let current;
2797
2888
  try {
2798
- current = await readFile12(path, "utf8");
2889
+ current = await readFile13(path, "utf8");
2799
2890
  } catch (error) {
2800
2891
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2801
2892
  throw error;
@@ -2806,13 +2897,13 @@ async function generateDatabaseContracts(options, check = false) {
2806
2897
  if (!check) {
2807
2898
  await mkdir6(outDir, { recursive: true });
2808
2899
  for (const [file, content] of Object.entries(files))
2809
- await writeFileIfChanged(resolve16(outDir, file), content);
2900
+ await writeFileIfChanged(resolve17(outDir, file), content);
2810
2901
  }
2811
2902
  return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
2812
2903
  }
2813
2904
  async function runDatabaseContractsFile(path, check = false) {
2814
- const absolute = resolve16(path);
2815
- const value = JSON.parse(await readFile12(absolute, "utf8"));
2905
+ const absolute = resolve17(path);
2906
+ const value = JSON.parse(await readFile13(absolute, "utf8"));
2816
2907
  return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname11(absolute)), check);
2817
2908
  }
2818
2909
  var init_database_contracts = __esm(() => {
@@ -6982,6 +7073,7 @@ async function compileProject(options) {
6982
7073
  "client.ts": rendered.clientCode,
6983
7074
  "openapi.ts": rendered.openApiCode,
6984
7075
  "permissions.ts": rendered.permissionsCode,
7076
+ "contracts.manifest.json": rendered.contractsManifestJson,
6985
7077
  "graphql.ts": graphql.files["graphql.ts"],
6986
7078
  "graphql.documents.ts": graphql.files["graphql.documents.ts"]
6987
7079
  }, options.strict ?? false));
@@ -7051,7 +7143,8 @@ async function checkProject(options) {
7051
7143
  const expectedFiles = {
7052
7144
  ...graphql.files,
7053
7145
  "application.ts": rendered.applicationCode,
7054
- "app.manifest.json": rendered.manifestJson
7146
+ "app.manifest.json": rendered.manifestJson,
7147
+ "contracts.manifest.json": rendered.contractsManifestJson
7055
7148
  };
7056
7149
  if (rendered.clientCode) {
7057
7150
  expectedFiles["client.ts"] = rendered.clientCode;
@@ -11495,6 +11588,8 @@ function optionsKeyOf(options) {
11495
11588
  requireRouteContracts: options.requireRouteContracts,
11496
11589
  detectOrphanModules: options.detectOrphanModules,
11497
11590
  generateClient: options.generateClient,
11591
+ generateOpenApi: options.generateOpenApi,
11592
+ openApi: options.openApi,
11498
11593
  generatePermissions: options.generatePermissions,
11499
11594
  typeSafety: options.typeSafety,
11500
11595
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
@@ -11629,6 +11724,16 @@ function findAffectedModules(previous, current, changedFiles) {
11629
11724
 
11630
11725
  // src/watch.ts
11631
11726
  var DEFAULT_DEBOUNCE_MS = 100;
11727
+ function isCompilerConfigurationPath(rootDir, changedPath) {
11728
+ const relativePath = relative9(rootDir, changedPath).split(sep9).join("/");
11729
+ return [
11730
+ "supacloud.config.ts",
11731
+ "supacloud.config.mts",
11732
+ "supacloud.config.js",
11733
+ "supacloud.config.mjs",
11734
+ "tsconfig.json"
11735
+ ].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
11736
+ }
11632
11737
  function watchProject(options) {
11633
11738
  const rootDir = resolve10(options.rootDir);
11634
11739
  const outDir = resolve10(options.outDir);
@@ -11721,7 +11826,7 @@ function watchProject(options) {
11721
11826
  const relativePath = relative9(outDir, changedPath);
11722
11827
  if (!relativePath.startsWith("..") && relativePath !== "")
11723
11828
  return;
11724
- if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
11829
+ if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
11725
11830
  schedule(relative9(rootDir, changedPath));
11726
11831
  }
11727
11832
  });
@@ -11775,7 +11880,7 @@ function compilerVersion() {
11775
11880
  }
11776
11881
  function migrationDependencies() {
11777
11882
  return {
11778
- "@supacloud/app": "0.15.0",
11883
+ "@supacloud/app": "0.16.0",
11779
11884
  "@supacloud/compiler": compilerVersion(),
11780
11885
  "@supacloud/elysia": "0.18.0",
11781
11886
  elysia: "1.4.30",
@@ -12195,797 +12300,989 @@ async function migrateProject(options) {
12195
12300
  issues
12196
12301
  };
12197
12302
  }
12198
- // src/inspect.ts
12199
- import { existsSync as existsSync4 } from "node:fs";
12200
- import { dirname as dirname8, join as join7, relative as relative11, sep as sep10 } from "node:path";
12201
- function formatGraph(graph) {
12202
- const lines = [];
12203
- for (const module of graph.modules) {
12204
- lines.push(`MODULE ${module.name}`);
12205
- lines.push(` file: ${module.file}:${module.line}`);
12206
- lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
12207
- lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
12208
- lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
12209
- lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
12303
+ // src/migration-assess.ts
12304
+ import { createHash as createHash9 } from "node:crypto";
12305
+ import { readFile as readFile11 } from "node:fs/promises";
12306
+ import { join as join7, resolve as resolve14 } from "node:path";
12307
+
12308
+ // src/openapi-tools.ts
12309
+ import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
12310
+ import { dirname as dirname8, resolve as resolve13 } from "node:path";
12311
+ import { pathToFileURL } from "node:url";
12312
+
12313
+ class OpenApiDocumentError extends Error {
12314
+ code = "OPENAPI_DOCUMENT_INVALID";
12315
+ constructor() {
12316
+ super("OpenAPI document is invalid or could not be loaded.");
12317
+ this.name = "OpenApiDocumentError";
12210
12318
  }
12211
- lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
12212
- return lines.join(`
12213
- `);
12214
12319
  }
12215
- function explainGraph(graph, subject) {
12216
- const module = graph.modules.find((candidate) => candidate.name === subject);
12217
- if (module)
12218
- return explainModule(graph, module);
12219
- const provider = findProvider(graph, subject);
12220
- if (provider)
12221
- return explainProvider(graph, provider.module, provider.provider);
12222
- if (graph.externalTokens.includes(subject)) {
12223
- const references = graph.modules.flatMap((candidate) => [
12224
- ...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
12225
- ...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
12226
- ]);
12227
- return [
12228
- `EXTERNAL TOKEN ${subject}`,
12229
- " provided by: platform runtime",
12230
- ` references: ${references.length > 0 ? references.join(", ") : "-"}`
12231
- ].join(`
12232
- `);
12233
- }
12234
- const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
12235
- throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
12320
+ var HTTP_METHODS = [
12321
+ "get",
12322
+ "put",
12323
+ "post",
12324
+ "delete",
12325
+ "options",
12326
+ "head",
12327
+ "patch",
12328
+ "trace"
12329
+ ];
12330
+ var COMPONENT_GROUPS = [
12331
+ "schemas",
12332
+ "responses",
12333
+ "parameters",
12334
+ "requestBodies",
12335
+ "headers",
12336
+ "securitySchemes"
12337
+ ];
12338
+ function isRecord(value) {
12339
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12236
12340
  }
12237
- function createContextPack(graph, subject) {
12238
- const subjectModule = graph.modules.find((module) => module.name === subject);
12239
- if (!subjectModule) {
12240
- throw new Error(`No module named "${subject}". Context packs require a module name.`);
12341
+ function stringValue(value) {
12342
+ return typeof value === "string" ? value : undefined;
12343
+ }
12344
+ function recordValue(value) {
12345
+ return isRecord(value) ? value : undefined;
12346
+ }
12347
+ function arrayValue(value) {
12348
+ return Array.isArray(value) ? value : [];
12349
+ }
12350
+ function sortedKeys(value) {
12351
+ return value ? Object.keys(value).sort() : [];
12352
+ }
12353
+ function sameJson(left, right) {
12354
+ return JSON.stringify(left) === JSON.stringify(right);
12355
+ }
12356
+ function decodeJsonPointerPart(value) {
12357
+ return value.replace(/~1/g, "/").replace(/~0/g, "~");
12358
+ }
12359
+ function resolveSchema(value, document, seen = new Set) {
12360
+ const schema = recordValue(value);
12361
+ if (!schema)
12362
+ return;
12363
+ const ref = stringValue(schema.$ref);
12364
+ if (!ref || !ref.startsWith("#/components/schemas/"))
12365
+ return schema;
12366
+ if (seen.has(ref))
12367
+ return schema;
12368
+ const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
12369
+ const components = recordValue(document.components);
12370
+ const schemas = recordValue(components?.schemas);
12371
+ const target = schemas?.[name];
12372
+ if (target === undefined)
12373
+ return schema;
12374
+ return resolveSchema(target, document, new Set([...seen, ref]));
12375
+ }
12376
+ function schemaEnum(value) {
12377
+ return Array.isArray(value?.enum) ? value.enum : undefined;
12378
+ }
12379
+ function schemaProperties(value) {
12380
+ return recordValue(value?.properties);
12381
+ }
12382
+ function schemaRequired(value) {
12383
+ return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
12384
+ }
12385
+ function schemaType(value) {
12386
+ return value?.type;
12387
+ }
12388
+ function addChange(changes, change) {
12389
+ if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
12390
+ return;
12391
+ changes.push(change);
12392
+ }
12393
+ function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
12394
+ const baseSchema = resolveSchema(baseValue, baseDocument);
12395
+ const currentSchema = resolveSchema(currentValue, currentDocument);
12396
+ if (!baseSchema || !currentSchema)
12397
+ return;
12398
+ const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
12399
+ if (seen.has(pair))
12400
+ return;
12401
+ seen.add(pair);
12402
+ if (schemaType(baseSchema) !== undefined && schemaType(currentSchema) !== undefined && !sameJson(schemaType(baseSchema), schemaType(currentSchema))) {
12403
+ addChange(changes, {
12404
+ kind: "breaking",
12405
+ code: "schema-type-changed",
12406
+ path,
12407
+ message: "The schema type changed."
12408
+ });
12241
12409
  }
12242
- const byName = new Map(graph.modules.map((module) => [module.name, module]));
12243
- const selected = new Set([subjectModule.name]);
12244
- const reverseDependents = new Map;
12245
- for (const mod of graph.modules) {
12246
- for (const imp of mod.imports) {
12247
- const list = reverseDependents.get(imp);
12248
- if (list)
12249
- list.push(mod.name);
12250
- else
12251
- reverseDependents.set(imp, [mod.name]);
12410
+ const baseEnum = schemaEnum(baseSchema);
12411
+ const currentEnum = schemaEnum(currentSchema);
12412
+ if (baseEnum && currentEnum) {
12413
+ for (const value of baseEnum) {
12414
+ if (!currentEnum.some((candidate) => sameJson(candidate, value))) {
12415
+ addChange(changes, {
12416
+ kind: "breaking",
12417
+ code: "schema-enum-value-removed",
12418
+ path,
12419
+ message: "An enum value accepted by the previous contract was removed."
12420
+ });
12421
+ break;
12422
+ }
12252
12423
  }
12253
12424
  }
12254
- for (const direction of ["imports", "dependents"]) {
12255
- const visited = new Set([subjectModule.name]);
12256
- const queue = [subjectModule.name];
12257
- let head = 0;
12258
- while (head < queue.length) {
12259
- const current = queue[head++];
12260
- if (!current)
12261
- continue;
12262
- const module = byName.get(current);
12263
- if (!module)
12264
- continue;
12265
- const neighbors = direction === "imports" ? module.imports : reverseDependents.get(module.name) ?? [];
12266
- for (const neighbor of neighbors) {
12267
- if (!visited.has(neighbor) && byName.has(neighbor)) {
12268
- visited.add(neighbor);
12269
- selected.add(neighbor);
12270
- queue.push(neighbor);
12425
+ const baseProperties = schemaProperties(baseSchema);
12426
+ const currentProperties = schemaProperties(currentSchema);
12427
+ if (baseProperties && currentProperties) {
12428
+ for (const name of sortedKeys(baseProperties)) {
12429
+ if (currentProperties[name] === undefined) {
12430
+ const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
12431
+ if (direction === "output" || inputRemovalIsBreaking) {
12432
+ addChange(changes, {
12433
+ kind: "breaking",
12434
+ code: direction === "output" ? "response-property-removed" : "request-property-removed",
12435
+ path: `${path}.properties.${name}`,
12436
+ message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
12437
+ });
12271
12438
  }
12439
+ continue;
12272
12440
  }
12441
+ compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
12273
12442
  }
12274
12443
  }
12275
- const modules = graph.modules.filter((module) => selected.has(module.name));
12276
- const queryDocuments = graph.graphql?.documents.filter((file) => modules.some((module) => {
12277
- const path = relative11(dirname8(module.file), file);
12278
- return path !== ".." && !path.startsWith(`..${sep10}`);
12279
- })) ?? [];
12280
- const graphql = graph.graphql ? {
12281
- ...graph.graphql,
12282
- documents: queryDocuments,
12283
- operations: graph.graphql.operations.filter((operation) => queryDocuments.includes(operation.file))
12284
- } : undefined;
12285
- const files = [...new Set(modules.flatMap((module) => [
12286
- module.file,
12287
- ...module.providers.map((provider) => provider.file),
12288
- ...module.controllers.map((controller) => controller.file),
12289
- ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
12290
- ]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
12291
- const referencedTokens = new Set;
12292
- for (const module of modules) {
12293
- for (const provider of module.providers) {
12294
- for (const token of provider.deps)
12295
- referencedTokens.add(token);
12296
- }
12297
- for (const controller of module.controllers) {
12298
- for (const token of controller.deps)
12299
- referencedTokens.add(token);
12444
+ const baseRequired = schemaRequired(baseSchema);
12445
+ const currentRequired = schemaRequired(currentSchema);
12446
+ if (direction === "input") {
12447
+ for (const name of currentRequired) {
12448
+ if (!baseRequired.has(name)) {
12449
+ addChange(changes, {
12450
+ kind: "breaking",
12451
+ code: "request-property-required",
12452
+ path: `${path}.required`,
12453
+ message: `Request property '${name}' became required.`
12454
+ });
12455
+ }
12300
12456
  }
12301
- }
12302
- return {
12303
- version: 1,
12304
- subject: subjectModule.name,
12305
- modules,
12306
- files,
12307
- externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
12308
- executionPlans: createExecutionPlans({ ...graph, modules }),
12309
- routeContracts: inspectRouteContracts({ ...graph, modules }),
12310
- ...graphql ? { graphql } : {},
12311
- diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
12312
- relatedModules: {
12313
- imports: subjectModule.imports.filter((name) => selected.has(name)),
12314
- importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
12457
+ if (baseSchema.additionalProperties !== false && currentSchema.additionalProperties === false) {
12458
+ addChange(changes, {
12459
+ kind: "breaking",
12460
+ code: "request-additional-properties-rejected",
12461
+ path,
12462
+ message: "The request schema now rejects additional properties."
12463
+ });
12315
12464
  }
12316
- };
12317
- }
12318
- function allAspects(module) {
12319
- return [
12320
- ...module.aspects ?? [],
12321
- ...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
12322
- ...module.commands.flatMap((command) => command.aspects ?? []),
12323
- ...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
12324
- ];
12325
- }
12326
- function createExecutionPlans(graph) {
12327
- const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
12328
- return graph.modules.flatMap((module) => [
12329
- ...module.controllers.flatMap((controller) => controller.routes.map((route) => {
12330
- const command = module.commands.find((item) => item.className === route.command);
12331
- const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
12332
- return {
12333
- module: module.name,
12334
- kind: "route",
12335
- name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
12336
- ...command ? { command: command.name } : {},
12337
- stages: [
12338
- ...aspects(`module:${module.name}`, module.aspects),
12339
- ...aspects("route", route.aspects),
12340
- ...aspects("command", command?.aspects),
12341
- ...command ? [
12342
- "commandExecutor",
12343
- "authorize",
12344
- ...command.rpc ? [`rpc:${command.rpc}`] : [
12345
- ...command.idempotency === "required" ? ["idempotency"] : [],
12346
- ...command.transaction === "required" ? ["transaction"] : []
12347
- ]
12348
- ] : [],
12349
- "handler",
12350
- ...command?.audit && !command.rpc ? ["audit"] : []
12351
- ]
12352
- };
12353
- })),
12354
- ...module.commands.map((command) => ({
12355
- module: module.name,
12356
- kind: "command",
12357
- name: command.name,
12358
- command: command.name,
12359
- stages: [
12360
- "authorize",
12361
- ...command.rpc ? [`rpc:${command.rpc}`] : [
12362
- ...command.idempotency === "required" ? ["idempotency"] : [],
12363
- ...command.transaction === "required" ? ["transaction"] : []
12364
- ],
12365
- ...aspects(`module:${module.name}`, module.aspects),
12366
- ...aspects("command", command.aspects),
12367
- "handler",
12368
- ...command.audit && !command.rpc ? ["audit"] : []
12369
- ]
12370
- })),
12371
- ...(module.jobs ?? []).map((job) => ({
12372
- module: module.name,
12373
- kind: "job",
12374
- name: job.name,
12375
- stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
12376
- }))
12377
- ]);
12378
- }
12379
- function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
12380
- const checks = [
12381
- {
12382
- name: "project-root",
12383
- ok: existsSync4(rootDir),
12384
- detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
12385
- },
12386
- {
12387
- name: "tsconfig",
12388
- ok: existsSync4(join7(rootDir, "tsconfig.json")),
12389
- detail: existsSync4(join7(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
12390
- },
12391
- {
12392
- name: "modules",
12393
- ok: graph.modules.length > 0,
12394
- detail: `${graph.modules.length} module(s) discovered`
12395
- },
12396
- {
12397
- name: "generated-artifacts",
12398
- ok: upToDate,
12399
- detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
12465
+ } else {
12466
+ for (const name of baseRequired) {
12467
+ if (!currentRequired.has(name)) {
12468
+ addChange(changes, {
12469
+ kind: "breaking",
12470
+ code: "response-property-optional",
12471
+ path: `${path}.required`,
12472
+ message: `Response property '${name}' is no longer guaranteed.`
12473
+ });
12474
+ }
12400
12475
  }
12401
- ];
12402
- const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
12403
- return {
12404
- checks,
12405
- diagnostics: allDiagnostics,
12406
- errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
12407
- };
12408
- }
12409
- function explainModule(graph, module) {
12410
- const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
12411
- return [
12412
- `MODULE ${module.name}`,
12413
- ` file: ${module.file}:${module.line}`,
12414
- ` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
12415
- ` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
12416
- ` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
12417
- ` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
12418
- ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
12419
- ...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
12420
- ].join(`
12421
- `);
12422
- }
12423
- function explainProvider(graph, module, provider) {
12424
- const dependents = graph.modules.flatMap((candidate) => [
12425
- ...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
12426
- ...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
12427
- ]);
12428
- return [
12429
- `PROVIDER ${provider.token}`,
12430
- ` module: ${module.name}`,
12431
- ` file: ${provider.file}:${provider.line}`,
12432
- ` kind: ${provider.kind}`,
12433
- ` scope: ${provider.scope}`,
12434
- ` exported: ${provider.exported ? "yes" : "no"}`,
12435
- ` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
12436
- ` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
12437
- ].join(`
12438
- `);
12439
- }
12440
- function findProvider(graph, subject) {
12441
- for (const module of graph.modules) {
12442
- const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
12443
- if (provider)
12444
- return { module, provider };
12445
12476
  }
12446
- return;
12447
12477
  }
12448
- function exportGraphMermaid(graph) {
12449
- const lines = ["graph TD"];
12450
- for (const mod of graph.modules) {
12451
- const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
12452
- lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
12453
- for (const imp of mod.imports) {
12454
- const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
12455
- lines.push(` ${safeId} --> ${safeImp}`);
12478
+ function parameterKey(value) {
12479
+ const parameter = recordValue(value);
12480
+ const name = stringValue(parameter?.name);
12481
+ const location = stringValue(parameter?.in);
12482
+ return name && location ? `${location}:${name}` : undefined;
12483
+ }
12484
+ function operationParameters(pathItem, operation) {
12485
+ const result = new Map;
12486
+ for (const source of [pathItem.parameters, operation.parameters]) {
12487
+ for (const item of arrayValue(source)) {
12488
+ const parameter = recordValue(item);
12489
+ const key = parameterKey(parameter);
12490
+ if (parameter && key)
12491
+ result.set(key, parameter);
12456
12492
  }
12457
12493
  }
12458
- return lines.join(`
12459
- `);
12494
+ return result;
12460
12495
  }
12461
- function exportGraphDot(graph) {
12462
- const lines = [
12463
- "digraph ApplicationGraph {",
12464
- " rankdir=LR;",
12465
- ' node [shape=box, fontname="Helvetica"];'
12466
- ];
12467
- for (const mod of graph.modules) {
12468
- lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
12469
- for (const imp of mod.imports) {
12470
- lines.push(` "${mod.name}" -> "${imp}";`);
12496
+ function parameterSchema(value) {
12497
+ return value.schema;
12498
+ }
12499
+ function compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12500
+ const baseParameters = operationParameters(basePathItem, baseOperation);
12501
+ const currentParameters = operationParameters(currentPathItem, currentOperation);
12502
+ for (const key of [...baseParameters.keys()].sort()) {
12503
+ const baseParameter = baseParameters.get(key);
12504
+ const currentParameter = currentParameters.get(key);
12505
+ if (!baseParameter || !currentParameter) {
12506
+ addChange(changes, {
12507
+ kind: "breaking",
12508
+ code: "parameter-removed",
12509
+ path: `${path}.parameters.${key}`,
12510
+ message: "A parameter from the previous contract was removed."
12511
+ });
12512
+ continue;
12513
+ }
12514
+ if (currentParameter.required === true && baseParameter.required !== true) {
12515
+ addChange(changes, {
12516
+ kind: "breaking",
12517
+ code: "parameter-required",
12518
+ path: `${path}.parameters.${key}`,
12519
+ message: "An optional parameter became required."
12520
+ });
12521
+ }
12522
+ if (parameterSchema(baseParameter) !== undefined && parameterSchema(currentParameter) !== undefined) {
12523
+ compareSchema(parameterSchema(baseParameter), parameterSchema(currentParameter), baseDocument, currentDocument, `${path}.parameters.${key}.schema`, "input", changes);
12471
12524
  }
12472
12525
  }
12473
- lines.push("}");
12474
- return lines.join(`
12475
- `);
12476
- }
12477
-
12478
- // src/index.ts
12479
- init_generate();
12480
-
12481
- // src/openapi-tools.ts
12482
- import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
12483
- import { dirname as dirname9, resolve as resolve13 } from "node:path";
12484
- import { pathToFileURL } from "node:url";
12485
-
12486
- class OpenApiDocumentError extends Error {
12487
- code = "OPENAPI_DOCUMENT_INVALID";
12488
- constructor() {
12489
- super("OpenAPI document is invalid or could not be loaded.");
12490
- this.name = "OpenApiDocumentError";
12526
+ for (const key of [...currentParameters.keys()].sort()) {
12527
+ if (baseParameters.has(key))
12528
+ continue;
12529
+ const parameter = currentParameters.get(key);
12530
+ if (!parameter)
12531
+ continue;
12532
+ addChange(changes, {
12533
+ kind: parameter.required === true ? "breaking" : "non-breaking",
12534
+ code: parameter.required === true ? "parameter-required" : "parameter-added",
12535
+ path: `${path}.parameters.${key}`,
12536
+ message: parameter.required === true ? "A new required parameter was added." : "An optional parameter was added."
12537
+ });
12491
12538
  }
12492
12539
  }
12493
- var HTTP_METHODS = [
12494
- "get",
12495
- "put",
12496
- "post",
12497
- "delete",
12498
- "options",
12499
- "head",
12500
- "patch",
12501
- "trace"
12502
- ];
12503
- var COMPONENT_GROUPS = [
12504
- "schemas",
12505
- "responses",
12506
- "parameters",
12507
- "requestBodies",
12508
- "headers",
12509
- "securitySchemes"
12510
- ];
12511
- function isRecord(value) {
12512
- return value !== null && typeof value === "object" && !Array.isArray(value);
12513
- }
12514
- function stringValue(value) {
12515
- return typeof value === "string" ? value : undefined;
12516
- }
12517
- function recordValue(value) {
12518
- return isRecord(value) ? value : undefined;
12519
- }
12520
- function arrayValue(value) {
12521
- return Array.isArray(value) ? value : [];
12522
- }
12523
- function sortedKeys(value) {
12524
- return value ? Object.keys(value).sort() : [];
12525
- }
12526
- function sameJson(left, right) {
12527
- return JSON.stringify(left) === JSON.stringify(right);
12528
- }
12529
- function decodeJsonPointerPart(value) {
12530
- return value.replace(/~1/g, "/").replace(/~0/g, "~");
12531
- }
12532
- function resolveSchema(value, document, seen = new Set) {
12533
- const schema = recordValue(value);
12534
- if (!schema)
12535
- return;
12536
- const ref = stringValue(schema.$ref);
12537
- if (!ref || !ref.startsWith("#/components/schemas/"))
12538
- return schema;
12539
- if (seen.has(ref))
12540
- return schema;
12541
- const name = decodeJsonPointerPart(ref.slice("#/components/schemas/".length));
12542
- const components = recordValue(document.components);
12543
- const schemas = recordValue(components?.schemas);
12544
- const target = schemas?.[name];
12545
- if (target === undefined)
12546
- return schema;
12547
- return resolveSchema(target, document, new Set([...seen, ref]));
12548
- }
12549
- function schemaEnum(value) {
12550
- return Array.isArray(value?.enum) ? value.enum : undefined;
12551
- }
12552
- function schemaProperties(value) {
12553
- return recordValue(value?.properties);
12554
- }
12555
- function schemaRequired(value) {
12556
- return new Set(arrayValue(value?.required).filter((item) => typeof item === "string"));
12557
- }
12558
- function schemaType(value) {
12559
- return value?.type;
12560
- }
12561
- function addChange(changes, change) {
12562
- if (changes.some((item) => item.kind === change.kind && item.code === change.code && item.path === change.path && item.message === change.message))
12563
- return;
12564
- changes.push(change);
12565
- }
12566
- function compareSchema(baseValue, currentValue, baseDocument, currentDocument, path, direction, changes, seen = new Set) {
12567
- const baseSchema = resolveSchema(baseValue, baseDocument);
12568
- const currentSchema = resolveSchema(currentValue, currentDocument);
12569
- if (!baseSchema || !currentSchema)
12570
- return;
12571
- const pair = `${path}|${JSON.stringify(baseSchema)}|${JSON.stringify(currentSchema)}`;
12572
- if (seen.has(pair))
12540
+ function compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12541
+ const baseBody = recordValue(baseOperation.requestBody);
12542
+ const currentBody = recordValue(currentOperation.requestBody);
12543
+ if (!baseBody || !currentBody) {
12544
+ if (baseBody && !currentBody) {
12545
+ addChange(changes, {
12546
+ kind: "breaking",
12547
+ code: "request-body-removed",
12548
+ path: `${path}.requestBody`,
12549
+ message: "A request body from the previous contract was removed."
12550
+ });
12551
+ } else if (!baseBody && currentBody?.required === true) {
12552
+ addChange(changes, {
12553
+ kind: "breaking",
12554
+ code: "request-body-required",
12555
+ path: `${path}.requestBody`,
12556
+ message: "A request body became required."
12557
+ });
12558
+ } else if (!baseBody && currentBody) {
12559
+ addChange(changes, {
12560
+ kind: "non-breaking",
12561
+ code: "request-body-added",
12562
+ path: `${path}.requestBody`,
12563
+ message: "An optional request body was added."
12564
+ });
12565
+ }
12573
12566
  return;
12574
- seen.add(pair);
12575
- if (schemaType(baseSchema) !== undefined && schemaType(currentSchema) !== undefined && !sameJson(schemaType(baseSchema), schemaType(currentSchema))) {
12567
+ }
12568
+ if (currentBody.required === true && baseBody.required !== true) {
12576
12569
  addChange(changes, {
12577
12570
  kind: "breaking",
12578
- code: "schema-type-changed",
12579
- path,
12580
- message: "The schema type changed."
12571
+ code: "request-body-required",
12572
+ path: `${path}.requestBody`,
12573
+ message: "An optional request body became required."
12581
12574
  });
12582
12575
  }
12583
- const baseEnum = schemaEnum(baseSchema);
12584
- const currentEnum = schemaEnum(currentSchema);
12585
- if (baseEnum && currentEnum) {
12586
- for (const value of baseEnum) {
12587
- if (!currentEnum.some((candidate) => sameJson(candidate, value))) {
12588
- addChange(changes, {
12589
- kind: "breaking",
12590
- code: "schema-enum-value-removed",
12591
- path,
12592
- message: "An enum value accepted by the previous contract was removed."
12593
- });
12594
- break;
12595
- }
12576
+ const baseContent = recordValue(baseBody.content);
12577
+ const currentContent = recordValue(currentBody.content);
12578
+ for (const mediaType of sortedKeys(baseContent)) {
12579
+ const baseMedia = recordValue(baseContent?.[mediaType]);
12580
+ const currentMedia = recordValue(currentContent?.[mediaType]);
12581
+ if (!baseMedia || !currentMedia) {
12582
+ addChange(changes, {
12583
+ kind: "breaking",
12584
+ code: "request-media-type-removed",
12585
+ path: `${path}.requestBody.content.${mediaType}`,
12586
+ message: "A request media type from the previous contract was removed."
12587
+ });
12588
+ continue;
12596
12589
  }
12597
- }
12598
- const baseProperties = schemaProperties(baseSchema);
12599
- const currentProperties = schemaProperties(currentSchema);
12600
- if (baseProperties && currentProperties) {
12601
- for (const name of sortedKeys(baseProperties)) {
12602
- if (currentProperties[name] === undefined) {
12603
- const inputRemovalIsBreaking = baseSchema.additionalProperties === false;
12604
- if (direction === "output" || inputRemovalIsBreaking) {
12605
- addChange(changes, {
12606
- kind: "breaking",
12607
- code: direction === "output" ? "response-property-removed" : "request-property-removed",
12608
- path: `${path}.properties.${name}`,
12609
- message: direction === "output" ? "A property returned by the previous contract was removed." : "A request property was removed while additional properties are rejected."
12610
- });
12611
- }
12612
- continue;
12613
- }
12614
- compareSchema(baseProperties[name], currentProperties[name], baseDocument, currentDocument, `${path}.properties.${name}`, direction, changes, seen);
12590
+ if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
12591
+ compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
12615
12592
  }
12616
12593
  }
12617
- const baseRequired = schemaRequired(baseSchema);
12618
- const currentRequired = schemaRequired(currentSchema);
12619
- if (direction === "input") {
12620
- for (const name of currentRequired) {
12621
- if (!baseRequired.has(name)) {
12594
+ }
12595
+ function compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12596
+ const baseResponses = recordValue(baseOperation.responses);
12597
+ const currentResponses = recordValue(currentOperation.responses);
12598
+ for (const status of sortedKeys(baseResponses)) {
12599
+ const baseResponse = recordValue(baseResponses?.[status]);
12600
+ const currentResponse = recordValue(currentResponses?.[status]);
12601
+ if (!baseResponse || !currentResponse) {
12602
+ addChange(changes, {
12603
+ kind: "breaking",
12604
+ code: "response-removed",
12605
+ path: `${path}.responses.${status}`,
12606
+ message: "A response status from the previous contract was removed."
12607
+ });
12608
+ continue;
12609
+ }
12610
+ const baseContent = recordValue(baseResponse.content);
12611
+ const currentContent = recordValue(currentResponse.content);
12612
+ for (const mediaType of sortedKeys(baseContent)) {
12613
+ const baseMedia = recordValue(baseContent?.[mediaType]);
12614
+ const currentMedia = recordValue(currentContent?.[mediaType]);
12615
+ if (!baseMedia || !currentMedia) {
12622
12616
  addChange(changes, {
12623
- kind: "breaking",
12624
- code: "request-property-required",
12625
- path: `${path}.required`,
12626
- message: `Request property '${name}' became required.`
12617
+ kind: "breaking",
12618
+ code: "response-media-type-removed",
12619
+ path: `${path}.responses.${status}.content.${mediaType}`,
12620
+ message: "A response media type from the previous contract was removed."
12627
12621
  });
12622
+ continue;
12623
+ }
12624
+ if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
12625
+ compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.responses.${status}.content.${mediaType}.schema`, "output", changes);
12628
12626
  }
12629
12627
  }
12630
- if (baseSchema.additionalProperties !== false && currentSchema.additionalProperties === false) {
12628
+ }
12629
+ for (const status of sortedKeys(currentResponses)) {
12630
+ if (baseResponses?.[status] !== undefined)
12631
+ continue;
12632
+ addChange(changes, {
12633
+ kind: "non-breaking",
12634
+ code: "response-added",
12635
+ path: `${path}.responses.${status}`,
12636
+ message: "A response status was added."
12637
+ });
12638
+ }
12639
+ }
12640
+ function compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12641
+ compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12642
+ compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12643
+ compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12644
+ const baseSecurity = baseOperation.security;
12645
+ const currentSecurity = currentOperation.security;
12646
+ if (Array.isArray(currentSecurity) && currentSecurity.length > 0 && (!Array.isArray(baseSecurity) || baseSecurity.length === 0)) {
12647
+ addChange(changes, {
12648
+ kind: "breaking",
12649
+ code: "security-requirement-added",
12650
+ path: `${path}.security`,
12651
+ message: "The operation now requires authentication or an additional security scheme."
12652
+ });
12653
+ }
12654
+ }
12655
+ function compareComponents(baseDocument, currentDocument, changes) {
12656
+ const baseComponents = recordValue(baseDocument.components);
12657
+ const currentComponents = recordValue(currentDocument.components);
12658
+ for (const group of COMPONENT_GROUPS) {
12659
+ const baseGroup = recordValue(baseComponents?.[group]);
12660
+ const currentGroup = recordValue(currentComponents?.[group]);
12661
+ for (const name of sortedKeys(baseGroup)) {
12662
+ if (currentGroup?.[name] !== undefined)
12663
+ continue;
12631
12664
  addChange(changes, {
12632
12665
  kind: "breaking",
12633
- code: "request-additional-properties-rejected",
12634
- path,
12635
- message: "The request schema now rejects additional properties."
12666
+ code: "component-removed",
12667
+ path: `components.${group}.${name}`,
12668
+ message: "A reusable component from the previous contract was removed."
12636
12669
  });
12637
12670
  }
12638
- } else {
12639
- for (const name of baseRequired) {
12640
- if (!currentRequired.has(name)) {
12641
- addChange(changes, {
12642
- kind: "breaking",
12643
- code: "response-property-optional",
12644
- path: `${path}.required`,
12645
- message: `Response property '${name}' is no longer guaranteed.`
12646
- });
12647
- }
12671
+ for (const name of sortedKeys(currentGroup)) {
12672
+ if (baseGroup?.[name] !== undefined)
12673
+ continue;
12674
+ addChange(changes, {
12675
+ kind: "non-breaking",
12676
+ code: "component-added",
12677
+ path: `components.${group}.${name}`,
12678
+ message: "A reusable component was added."
12679
+ });
12648
12680
  }
12649
12681
  }
12650
12682
  }
12651
- function parameterKey(value) {
12652
- const parameter = recordValue(value);
12653
- const name = stringValue(parameter?.name);
12654
- const location = stringValue(parameter?.in);
12655
- return name && location ? `${location}:${name}` : undefined;
12683
+ function parseOpenApiDocument(value) {
12684
+ if (!isRecord(value) || typeof value.openapi !== "string" || !/^3\.[0-9]+(?:\.[0-9]+)?$/.test(value.openapi) || !isRecord(value.info) || !isRecord(value.paths)) {
12685
+ throw new OpenApiDocumentError;
12686
+ }
12687
+ return { ...value, openapi: value.openapi, info: value.info, paths: value.paths };
12656
12688
  }
12657
- function operationParameters(pathItem, operation) {
12658
- const result = new Map;
12659
- for (const source of [pathItem.parameters, operation.parameters]) {
12660
- for (const item of arrayValue(source)) {
12661
- const parameter = recordValue(item);
12662
- const key = parameterKey(parameter);
12663
- if (parameter && key)
12664
- result.set(key, parameter);
12665
- }
12689
+ function serializeOpenApiJson(document, space = 2) {
12690
+ const parsed = parseOpenApiDocument(document);
12691
+ if (!Number.isInteger(space) || space < 0 || space > 10)
12692
+ throw new OpenApiDocumentError;
12693
+ const serialized = JSON.stringify(parsed, null, space);
12694
+ if (serialized === undefined)
12695
+ throw new OpenApiDocumentError;
12696
+ return `${serialized}
12697
+ `;
12698
+ }
12699
+ async function readOpenApiJson(path) {
12700
+ try {
12701
+ const value = JSON.parse(await readFile10(resolve13(path), "utf8"));
12702
+ return parseOpenApiDocument(value);
12703
+ } catch (error) {
12704
+ if (error instanceof OpenApiDocumentError)
12705
+ throw error;
12706
+ throw new OpenApiDocumentError;
12666
12707
  }
12667
- return result;
12668
12708
  }
12669
- function parameterSchema(value) {
12670
- return value.schema;
12709
+ async function writeOpenApiJson(document, outputPath, space = 2) {
12710
+ const path = resolve13(outputPath);
12711
+ const content = serializeOpenApiJson(document, space);
12712
+ await mkdir4(dirname8(path), { recursive: true });
12713
+ try {
12714
+ if (await readFile10(path, "utf8") === content)
12715
+ return { path, written: false };
12716
+ } catch {}
12717
+ const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
12718
+ try {
12719
+ await writeFile5(temporaryPath, content, "utf8");
12720
+ await rename6(temporaryPath, path);
12721
+ } catch (error) {
12722
+ await unlink3(temporaryPath).catch(() => {
12723
+ return;
12724
+ });
12725
+ throw error;
12726
+ }
12727
+ return { path, written: true };
12671
12728
  }
12672
- function compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12673
- const baseParameters = operationParameters(basePathItem, baseOperation);
12674
- const currentParameters = operationParameters(currentPathItem, currentOperation);
12675
- for (const key of [...baseParameters.keys()].sort()) {
12676
- const baseParameter = baseParameters.get(key);
12677
- const currentParameter = currentParameters.get(key);
12678
- if (!baseParameter || !currentParameter) {
12729
+ async function loadGeneratedOpenApiDocument(modulePath) {
12730
+ try {
12731
+ const moduleUrl = pathToFileURL(resolve13(modulePath));
12732
+ moduleUrl.searchParams.set("supacloud-openapi-export", "1");
12733
+ const loaded = await import(moduleUrl.href);
12734
+ if (!isRecord(loaded))
12735
+ throw new OpenApiDocumentError;
12736
+ return parseOpenApiDocument(loaded.OPENAPI_DOCUMENT);
12737
+ } catch (error) {
12738
+ if (error instanceof OpenApiDocumentError)
12739
+ throw error;
12740
+ throw new OpenApiDocumentError;
12741
+ }
12742
+ }
12743
+ async function exportGeneratedOpenApiJson(options) {
12744
+ const document = await loadGeneratedOpenApiDocument(options.modulePath);
12745
+ return writeOpenApiJson(document, options.outputPath, options.space);
12746
+ }
12747
+ function diffOpenApiDocuments(baseValue, currentValue) {
12748
+ const baseDocument = parseOpenApiDocument(baseValue);
12749
+ const currentDocument = parseOpenApiDocument(currentValue);
12750
+ const changes = [];
12751
+ const basePaths = baseDocument.paths;
12752
+ const currentPaths = currentDocument.paths;
12753
+ for (const path of sortedKeys(basePaths)) {
12754
+ const basePathItem = recordValue(basePaths[path]);
12755
+ const currentPathItem = recordValue(currentPaths[path]);
12756
+ if (!basePathItem || !currentPathItem) {
12679
12757
  addChange(changes, {
12680
12758
  kind: "breaking",
12681
- code: "parameter-removed",
12682
- path: `${path}.parameters.${key}`,
12683
- message: "A parameter from the previous contract was removed."
12759
+ code: "path-removed",
12760
+ path: `paths.${path}`,
12761
+ message: "A path from the previous contract was removed."
12684
12762
  });
12685
12763
  continue;
12686
12764
  }
12687
- if (currentParameter.required === true && baseParameter.required !== true) {
12688
- addChange(changes, {
12689
- kind: "breaking",
12690
- code: "parameter-required",
12691
- path: `${path}.parameters.${key}`,
12692
- message: "An optional parameter became required."
12693
- });
12694
- }
12695
- if (parameterSchema(baseParameter) !== undefined && parameterSchema(currentParameter) !== undefined) {
12696
- compareSchema(parameterSchema(baseParameter), parameterSchema(currentParameter), baseDocument, currentDocument, `${path}.parameters.${key}.schema`, "input", changes);
12765
+ for (const method of HTTP_METHODS) {
12766
+ const baseOperation = recordValue(basePathItem[method]);
12767
+ const currentOperation = recordValue(currentPathItem[method]);
12768
+ if (!baseOperation || !currentOperation) {
12769
+ if (baseOperation) {
12770
+ addChange(changes, {
12771
+ kind: "breaking",
12772
+ code: "operation-removed",
12773
+ path: `paths.${path}.${method}`,
12774
+ message: "An operation from the previous contract was removed."
12775
+ });
12776
+ } else if (currentOperation) {
12777
+ addChange(changes, {
12778
+ kind: "non-breaking",
12779
+ code: "operation-added",
12780
+ path: `paths.${path}.${method}`,
12781
+ message: "An operation was added to an existing path."
12782
+ });
12783
+ }
12784
+ continue;
12785
+ }
12786
+ compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
12697
12787
  }
12698
12788
  }
12699
- for (const key of [...currentParameters.keys()].sort()) {
12700
- if (baseParameters.has(key))
12701
- continue;
12702
- const parameter = currentParameters.get(key);
12703
- if (!parameter)
12789
+ for (const path of sortedKeys(currentPaths)) {
12790
+ if (basePaths[path] !== undefined)
12704
12791
  continue;
12705
12792
  addChange(changes, {
12706
- kind: parameter.required === true ? "breaking" : "non-breaking",
12707
- code: parameter.required === true ? "parameter-required" : "parameter-added",
12708
- path: `${path}.parameters.${key}`,
12709
- message: parameter.required === true ? "A new required parameter was added." : "An optional parameter was added."
12793
+ kind: "non-breaking",
12794
+ code: "path-added",
12795
+ path: `paths.${path}`,
12796
+ message: "A path was added."
12710
12797
  });
12711
12798
  }
12799
+ compareComponents(baseDocument, currentDocument, changes);
12800
+ const breaking = changes.filter((change) => change.kind === "breaking");
12801
+ return { ok: breaking.length === 0, breaking, changes };
12712
12802
  }
12713
- function compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12714
- const baseBody = recordValue(baseOperation.requestBody);
12715
- const currentBody = recordValue(currentOperation.requestBody);
12716
- if (!baseBody || !currentBody) {
12717
- if (baseBody && !currentBody) {
12718
- addChange(changes, {
12719
- kind: "breaking",
12720
- code: "request-body-removed",
12721
- path: `${path}.requestBody`,
12722
- message: "A request body from the previous contract was removed."
12723
- });
12724
- } else if (!baseBody && currentBody?.required === true) {
12725
- addChange(changes, {
12726
- kind: "breaking",
12727
- code: "request-body-required",
12728
- path: `${path}.requestBody`,
12729
- message: "A request body became required."
12803
+ function formatOpenApiDiff(result) {
12804
+ if (result.changes.length === 0)
12805
+ return "OpenAPI diff passed: no contract changes.";
12806
+ return [
12807
+ `OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
12808
+ ...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
12809
+ ].join(`
12810
+ `);
12811
+ }
12812
+
12813
+ // src/migration-assess.ts
12814
+ var STATUS_PRIORITY = {
12815
+ compatible: 0,
12816
+ "needs-review": 1,
12817
+ "not-proven": 2,
12818
+ unsupported: 3,
12819
+ breaking: 4
12820
+ };
12821
+ function isRecord2(value) {
12822
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12823
+ }
12824
+ function arrayLength(value) {
12825
+ return Array.isArray(value) ? value.length : undefined;
12826
+ }
12827
+ async function sha256(path) {
12828
+ return createHash9("sha256").update(await readFile11(path)).digest("hex");
12829
+ }
12830
+ function overallStatus(findings) {
12831
+ return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
12832
+ }
12833
+ async function assessMigration(options) {
12834
+ const projectDir = resolve14(options.projectDir);
12835
+ const compile = {
12836
+ ...options.compile,
12837
+ rootDir: resolve14(options.compile.rootDir),
12838
+ outDir: resolve14(options.compile.outDir)
12839
+ };
12840
+ const findings = [];
12841
+ const dependencyProblems = await checkMigrationDependencies(projectDir);
12842
+ if (dependencyProblems.length > 0) {
12843
+ findings.push({
12844
+ code: "migration-dependency-tuple-not-proven",
12845
+ status: "not-proven",
12846
+ message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
12847
+ evidence: dependencyProblems.join("; "),
12848
+ remediation: "Install the exact tested package versions before applying a source migration."
12849
+ });
12850
+ }
12851
+ let compiler = {
12852
+ upToDate: false,
12853
+ diagnostics: [],
12854
+ mismatches: []
12855
+ };
12856
+ try {
12857
+ const checked = await checkProject(compile);
12858
+ compiler = {
12859
+ upToDate: checked.upToDate,
12860
+ diagnostics: checked.diagnostics,
12861
+ mismatches: checked.mismatches
12862
+ };
12863
+ const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
12864
+ if (errors.length > 0) {
12865
+ findings.push({
12866
+ code: "compiler-governance-not-proven",
12867
+ status: "not-proven",
12868
+ message: `Compiler governance has ${errors.length} error(s).`,
12869
+ remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
12730
12870
  });
12731
- } else if (!baseBody && currentBody) {
12732
- addChange(changes, {
12733
- kind: "non-breaking",
12734
- code: "request-body-added",
12735
- path: `${path}.requestBody`,
12736
- message: "An optional request body was added."
12871
+ }
12872
+ if (!checked.upToDate) {
12873
+ findings.push({
12874
+ code: "generated-artifact-drift",
12875
+ status: "needs-review",
12876
+ message: "Generated artifacts do not match the current source and compiler configuration.",
12877
+ evidence: checked.mismatches.join("; "),
12878
+ remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
12737
12879
  });
12738
12880
  }
12739
- return;
12881
+ } catch {
12882
+ findings.push({
12883
+ code: "compiler-check-unavailable",
12884
+ status: "not-proven",
12885
+ message: "The local compiler check could not complete.",
12886
+ remediation: "Verify the project source, configuration and installed dependencies, then reassess."
12887
+ });
12740
12888
  }
12741
- if (currentBody.required === true && baseBody.required !== true) {
12742
- addChange(changes, {
12743
- kind: "breaking",
12744
- code: "request-body-required",
12745
- path: `${path}.requestBody`,
12746
- message: "An optional request body became required."
12889
+ const contractsManifestPath = join7(compile.outDir, "contracts.manifest.json");
12890
+ let contractsManifest = {
12891
+ path: contractsManifestPath,
12892
+ present: false
12893
+ };
12894
+ try {
12895
+ const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
12896
+ if (!isRecord2(parsed) || parsed.version !== 1)
12897
+ throw new Error("invalid manifest");
12898
+ contractsManifest = {
12899
+ path: contractsManifestPath,
12900
+ present: true,
12901
+ sha256: await sha256(contractsManifestPath),
12902
+ version: 1,
12903
+ ...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
12904
+ ...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
12905
+ ...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
12906
+ };
12907
+ } catch {
12908
+ findings.push({
12909
+ code: "contract-manifest-not-proven",
12910
+ status: "not-proven",
12911
+ message: "contracts.manifest.json is missing or invalid.",
12912
+ evidence: contractsManifestPath,
12913
+ remediation: "Generate and review the contract manifest before migration."
12747
12914
  });
12748
12915
  }
12749
- const baseContent = recordValue(baseBody.content);
12750
- const currentContent = recordValue(currentBody.content);
12751
- for (const mediaType of sortedKeys(baseContent)) {
12752
- const baseMedia = recordValue(baseContent?.[mediaType]);
12753
- const currentMedia = recordValue(currentContent?.[mediaType]);
12754
- if (!baseMedia || !currentMedia) {
12755
- addChange(changes, {
12756
- kind: "breaking",
12757
- code: "request-media-type-removed",
12758
- path: `${path}.requestBody.content.${mediaType}`,
12759
- message: "A request media type from the previous contract was removed."
12916
+ let openApi;
12917
+ if (options.baselineOpenApiPath && options.currentOpenApiPath) {
12918
+ const baselinePath = resolve14(projectDir, options.baselineOpenApiPath);
12919
+ const currentPath = resolve14(projectDir, options.currentOpenApiPath);
12920
+ try {
12921
+ const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
12922
+ openApi = {
12923
+ baselinePath,
12924
+ baselineSha256: await sha256(baselinePath),
12925
+ currentPath,
12926
+ currentSha256: await sha256(currentPath),
12927
+ diff
12928
+ };
12929
+ if (!diff.ok) {
12930
+ findings.push({
12931
+ code: "openapi-breaking-change",
12932
+ status: "breaking",
12933
+ message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
12934
+ remediation: "Review and explicitly approve or redesign each breaking contract change."
12935
+ });
12936
+ } else if (diff.changes.length > 0) {
12937
+ findings.push({
12938
+ code: "openapi-compatible-change-review",
12939
+ status: "needs-review",
12940
+ message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
12941
+ });
12942
+ }
12943
+ } catch {
12944
+ findings.push({
12945
+ code: "openapi-diff-not-proven",
12946
+ status: "not-proven",
12947
+ message: "The OpenAPI baseline or current document could not be loaded.",
12948
+ remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
12760
12949
  });
12761
- continue;
12762
- }
12763
- if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
12764
- compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.requestBody.content.${mediaType}.schema`, "input", changes);
12765
12950
  }
12951
+ } else {
12952
+ findings.push({
12953
+ code: "openapi-baseline-not-proven",
12954
+ status: "not-proven",
12955
+ message: "No complete OpenAPI baseline/current pair was supplied.",
12956
+ remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
12957
+ });
12766
12958
  }
12959
+ findings.push({
12960
+ code: "rendering-mode-preserved",
12961
+ status: "compatible",
12962
+ message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
12963
+ evidence: `selected=${options.renderMode ?? "unspecified"}`
12964
+ });
12965
+ const status = overallStatus(findings);
12966
+ return {
12967
+ version: 1,
12968
+ ok: status === "compatible" || status === "needs-review",
12969
+ status,
12970
+ readOnly: true,
12971
+ writesPerformed: false,
12972
+ rendering: {
12973
+ selected: options.renderMode ?? "unspecified",
12974
+ ssrRequired: false,
12975
+ supportedModes: ["browser", "ssr", "edge", "trusted-server"],
12976
+ guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
12977
+ },
12978
+ dependencies: {
12979
+ expected: migrationDependencies(),
12980
+ problems: dependencyProblems
12981
+ },
12982
+ compiler,
12983
+ artifacts: {
12984
+ contractsManifest,
12985
+ ...openApi === undefined ? {} : { openApi }
12986
+ },
12987
+ findings
12988
+ };
12767
12989
  }
12768
- function compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12769
- const baseResponses = recordValue(baseOperation.responses);
12770
- const currentResponses = recordValue(currentOperation.responses);
12771
- for (const status of sortedKeys(baseResponses)) {
12772
- const baseResponse = recordValue(baseResponses?.[status]);
12773
- const currentResponse = recordValue(currentResponses?.[status]);
12774
- if (!baseResponse || !currentResponse) {
12775
- addChange(changes, {
12776
- kind: "breaking",
12777
- code: "response-removed",
12778
- path: `${path}.responses.${status}`,
12779
- message: "A response status from the previous contract was removed."
12780
- });
12781
- continue;
12990
+ function formatMigrationAssessment(result) {
12991
+ return [
12992
+ `Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
12993
+ `Rendering: ${result.rendering.selected}; SSR required: no.`,
12994
+ ...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
12995
+ ].join(`
12996
+ `);
12997
+ }
12998
+ // src/inspect.ts
12999
+ import { existsSync as existsSync4 } from "node:fs";
13000
+ import { dirname as dirname9, join as join8, relative as relative11, sep as sep10 } from "node:path";
13001
+ function formatGraph(graph) {
13002
+ const lines = [];
13003
+ for (const module of graph.modules) {
13004
+ lines.push(`MODULE ${module.name}`);
13005
+ lines.push(` file: ${module.file}:${module.line}`);
13006
+ lines.push(` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`);
13007
+ lines.push(` providers: ${module.providers.length > 0 ? module.providers.map((p) => p.token).join(", ") : "-"}`);
13008
+ lines.push(` controllers: ${module.controllers.length > 0 ? module.controllers.map((c) => c.className).join(", ") : "-"}`);
13009
+ lines.push(` commands: ${module.commands.length > 0 ? module.commands.map((c) => c.name).join(", ") : "-"}`);
13010
+ }
13011
+ lines.push(`EXTERNAL TOKENS ${graph.externalTokens.length > 0 ? graph.externalTokens.join(", ") : "-"}`);
13012
+ return lines.join(`
13013
+ `);
13014
+ }
13015
+ function explainGraph(graph, subject) {
13016
+ const module = graph.modules.find((candidate) => candidate.name === subject);
13017
+ if (module)
13018
+ return explainModule(graph, module);
13019
+ const provider = findProvider(graph, subject);
13020
+ if (provider)
13021
+ return explainProvider(graph, provider.module, provider.provider);
13022
+ if (graph.externalTokens.includes(subject)) {
13023
+ const references = graph.modules.flatMap((candidate) => [
13024
+ ...candidate.providers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.token}`),
13025
+ ...candidate.controllers.filter((item) => item.deps.includes(subject)).map((item) => `${candidate.name}.${item.className}`)
13026
+ ]);
13027
+ return [
13028
+ `EXTERNAL TOKEN ${subject}`,
13029
+ " provided by: platform runtime",
13030
+ ` references: ${references.length > 0 ? references.join(", ") : "-"}`
13031
+ ].join(`
13032
+ `);
13033
+ }
13034
+ const known = [...graph.modules.map((item) => item.name), ...graph.externalTokens].sort();
13035
+ throw new Error(`No module, provider, or external token named "${subject}". Known names: ${known.join(", ") || "(none)"}`);
13036
+ }
13037
+ function createContextPack(graph, subject) {
13038
+ const subjectModule = graph.modules.find((module) => module.name === subject);
13039
+ if (!subjectModule) {
13040
+ throw new Error(`No module named "${subject}". Context packs require a module name.`);
13041
+ }
13042
+ const byName = new Map(graph.modules.map((module) => [module.name, module]));
13043
+ const selected = new Set([subjectModule.name]);
13044
+ const reverseDependents = new Map;
13045
+ for (const mod of graph.modules) {
13046
+ for (const imp of mod.imports) {
13047
+ const list = reverseDependents.get(imp);
13048
+ if (list)
13049
+ list.push(mod.name);
13050
+ else
13051
+ reverseDependents.set(imp, [mod.name]);
12782
13052
  }
12783
- const baseContent = recordValue(baseResponse.content);
12784
- const currentContent = recordValue(currentResponse.content);
12785
- for (const mediaType of sortedKeys(baseContent)) {
12786
- const baseMedia = recordValue(baseContent?.[mediaType]);
12787
- const currentMedia = recordValue(currentContent?.[mediaType]);
12788
- if (!baseMedia || !currentMedia) {
12789
- addChange(changes, {
12790
- kind: "breaking",
12791
- code: "response-media-type-removed",
12792
- path: `${path}.responses.${status}.content.${mediaType}`,
12793
- message: "A response media type from the previous contract was removed."
12794
- });
13053
+ }
13054
+ for (const direction of ["imports", "dependents"]) {
13055
+ const visited = new Set([subjectModule.name]);
13056
+ const queue = [subjectModule.name];
13057
+ let head = 0;
13058
+ while (head < queue.length) {
13059
+ const current = queue[head++];
13060
+ if (!current)
12795
13061
  continue;
12796
- }
12797
- if (baseMedia.schema !== undefined && currentMedia.schema !== undefined) {
12798
- compareSchema(baseMedia.schema, currentMedia.schema, baseDocument, currentDocument, `${path}.responses.${status}.content.${mediaType}.schema`, "output", changes);
13062
+ const module = byName.get(current);
13063
+ if (!module)
13064
+ continue;
13065
+ const neighbors = direction === "imports" ? module.imports : reverseDependents.get(module.name) ?? [];
13066
+ for (const neighbor of neighbors) {
13067
+ if (!visited.has(neighbor) && byName.has(neighbor)) {
13068
+ visited.add(neighbor);
13069
+ selected.add(neighbor);
13070
+ queue.push(neighbor);
13071
+ }
12799
13072
  }
12800
13073
  }
12801
13074
  }
12802
- for (const status of sortedKeys(currentResponses)) {
12803
- if (baseResponses?.[status] !== undefined)
12804
- continue;
12805
- addChange(changes, {
12806
- kind: "non-breaking",
12807
- code: "response-added",
12808
- path: `${path}.responses.${status}`,
12809
- message: "A response status was added."
12810
- });
13075
+ const modules = graph.modules.filter((module) => selected.has(module.name));
13076
+ const queryDocuments = graph.graphql?.documents.filter((file) => modules.some((module) => {
13077
+ const path = relative11(dirname9(module.file), file);
13078
+ return path !== ".." && !path.startsWith(`..${sep10}`);
13079
+ })) ?? [];
13080
+ const graphql = graph.graphql ? {
13081
+ ...graph.graphql,
13082
+ documents: queryDocuments,
13083
+ operations: graph.graphql.operations.filter((operation) => queryDocuments.includes(operation.file))
13084
+ } : undefined;
13085
+ const files = [...new Set(modules.flatMap((module) => [
13086
+ module.file,
13087
+ ...module.providers.map((provider) => provider.file),
13088
+ ...module.controllers.map((controller) => controller.file),
13089
+ ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
13090
+ ]).concat(graphql ? [graphql.schema, ...queryDocuments] : []))].sort();
13091
+ const referencedTokens = new Set;
13092
+ for (const module of modules) {
13093
+ for (const provider of module.providers) {
13094
+ for (const token of provider.deps)
13095
+ referencedTokens.add(token);
13096
+ }
13097
+ for (const controller of module.controllers) {
13098
+ for (const token of controller.deps)
13099
+ referencedTokens.add(token);
13100
+ }
12811
13101
  }
13102
+ return {
13103
+ version: 1,
13104
+ subject: subjectModule.name,
13105
+ modules,
13106
+ files,
13107
+ externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
13108
+ executionPlans: createExecutionPlans({ ...graph, modules }),
13109
+ routeContracts: inspectRouteContracts({ ...graph, modules }),
13110
+ ...graphql ? { graphql } : {},
13111
+ diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
13112
+ relatedModules: {
13113
+ imports: subjectModule.imports.filter((name) => selected.has(name)),
13114
+ importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
13115
+ }
13116
+ };
12812
13117
  }
12813
- function compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes) {
12814
- compareParameters(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12815
- compareRequestBody(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12816
- compareResponses(baseOperation, currentOperation, baseDocument, currentDocument, path, changes);
12817
- const baseSecurity = baseOperation.security;
12818
- const currentSecurity = currentOperation.security;
12819
- if (Array.isArray(currentSecurity) && currentSecurity.length > 0 && (!Array.isArray(baseSecurity) || baseSecurity.length === 0)) {
12820
- addChange(changes, {
12821
- kind: "breaking",
12822
- code: "security-requirement-added",
12823
- path: `${path}.security`,
12824
- message: "The operation now requires authentication or an additional security scheme."
12825
- });
12826
- }
13118
+ function allAspects(module) {
13119
+ return [
13120
+ ...module.aspects ?? [],
13121
+ ...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
13122
+ ...module.commands.flatMap((command) => command.aspects ?? []),
13123
+ ...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
13124
+ ];
13125
+ }
13126
+ function createExecutionPlans(graph) {
13127
+ const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
13128
+ return graph.modules.flatMap((module) => [
13129
+ ...module.controllers.flatMap((controller) => controller.routes.map((route) => {
13130
+ const command = module.commands.find((item) => item.className === route.command);
13131
+ const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
13132
+ return {
13133
+ module: module.name,
13134
+ kind: "route",
13135
+ name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
13136
+ ...command ? { command: command.name } : {},
13137
+ stages: [
13138
+ ...aspects(`module:${module.name}`, module.aspects),
13139
+ ...aspects("route", route.aspects),
13140
+ ...aspects("command", command?.aspects),
13141
+ ...command ? [
13142
+ "commandExecutor",
13143
+ "authorize",
13144
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
13145
+ ...command.idempotency === "required" ? ["idempotency"] : [],
13146
+ ...command.transaction === "required" ? ["transaction"] : []
13147
+ ]
13148
+ ] : [],
13149
+ "handler",
13150
+ ...command?.audit && !command.rpc ? ["audit"] : []
13151
+ ]
13152
+ };
13153
+ })),
13154
+ ...module.commands.map((command) => ({
13155
+ module: module.name,
13156
+ kind: "command",
13157
+ name: command.name,
13158
+ command: command.name,
13159
+ stages: [
13160
+ "authorize",
13161
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
13162
+ ...command.idempotency === "required" ? ["idempotency"] : [],
13163
+ ...command.transaction === "required" ? ["transaction"] : []
13164
+ ],
13165
+ ...aspects(`module:${module.name}`, module.aspects),
13166
+ ...aspects("command", command.aspects),
13167
+ "handler",
13168
+ ...command.audit && !command.rpc ? ["audit"] : []
13169
+ ]
13170
+ })),
13171
+ ...(module.jobs ?? []).map((job) => ({
13172
+ module: module.name,
13173
+ kind: "job",
13174
+ name: job.name,
13175
+ stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
13176
+ }))
13177
+ ]);
12827
13178
  }
12828
- function compareComponents(baseDocument, currentDocument, changes) {
12829
- const baseComponents = recordValue(baseDocument.components);
12830
- const currentComponents = recordValue(currentDocument.components);
12831
- for (const group of COMPONENT_GROUPS) {
12832
- const baseGroup = recordValue(baseComponents?.[group]);
12833
- const currentGroup = recordValue(currentComponents?.[group]);
12834
- for (const name of sortedKeys(baseGroup)) {
12835
- if (currentGroup?.[name] !== undefined)
12836
- continue;
12837
- addChange(changes, {
12838
- kind: "breaking",
12839
- code: "component-removed",
12840
- path: `components.${group}.${name}`,
12841
- message: "A reusable component from the previous contract was removed."
12842
- });
12843
- }
12844
- for (const name of sortedKeys(currentGroup)) {
12845
- if (baseGroup?.[name] !== undefined)
12846
- continue;
12847
- addChange(changes, {
12848
- kind: "non-breaking",
12849
- code: "component-added",
12850
- path: `components.${group}.${name}`,
12851
- message: "A reusable component was added."
12852
- });
13179
+ function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
13180
+ const checks = [
13181
+ {
13182
+ name: "project-root",
13183
+ ok: existsSync4(rootDir),
13184
+ detail: existsSync4(rootDir) ? rootDir : `missing: ${rootDir}`
13185
+ },
13186
+ {
13187
+ name: "tsconfig",
13188
+ ok: existsSync4(join8(rootDir, "tsconfig.json")),
13189
+ detail: existsSync4(join8(rootDir, "tsconfig.json")) ? "tsconfig.json found" : "tsconfig.json missing"
13190
+ },
13191
+ {
13192
+ name: "modules",
13193
+ ok: graph.modules.length > 0,
13194
+ detail: `${graph.modules.length} module(s) discovered`
13195
+ },
13196
+ {
13197
+ name: "generated-artifacts",
13198
+ ok: upToDate,
13199
+ detail: upToDate ? "application.ts and app.manifest.json are up to date" : "generated artifacts are missing or stale"
12853
13200
  }
12854
- }
12855
- }
12856
- function parseOpenApiDocument(value) {
12857
- if (!isRecord(value) || typeof value.openapi !== "string" || !/^3\.[0-9]+(?:\.[0-9]+)?$/.test(value.openapi) || !isRecord(value.info) || !isRecord(value.paths)) {
12858
- throw new OpenApiDocumentError;
12859
- }
12860
- return { ...value, openapi: value.openapi, info: value.info, paths: value.paths };
13201
+ ];
13202
+ const allDiagnostics = [...graph.diagnostics ?? [], ...diagnostics];
13203
+ return {
13204
+ checks,
13205
+ diagnostics: allDiagnostics,
13206
+ errors: allDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length + checks.filter((check) => !check.ok).length
13207
+ };
12861
13208
  }
12862
- function serializeOpenApiJson(document, space = 2) {
12863
- const parsed = parseOpenApiDocument(document);
12864
- if (!Number.isInteger(space) || space < 0 || space > 10)
12865
- throw new OpenApiDocumentError;
12866
- const serialized = JSON.stringify(parsed, null, space);
12867
- if (serialized === undefined)
12868
- throw new OpenApiDocumentError;
12869
- return `${serialized}
12870
- `;
13209
+ function explainModule(graph, module) {
13210
+ const dependents = graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
13211
+ return [
13212
+ `MODULE ${module.name}`,
13213
+ ` file: ${module.file}:${module.line}`,
13214
+ ` imports: ${module.imports.length > 0 ? module.imports.join(", ") : "-"}`,
13215
+ ` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
13216
+ ` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
13217
+ ` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
13218
+ ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
13219
+ ...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
13220
+ ].join(`
13221
+ `);
12871
13222
  }
12872
- async function readOpenApiJson(path) {
12873
- try {
12874
- const value = JSON.parse(await readFile10(resolve13(path), "utf8"));
12875
- return parseOpenApiDocument(value);
12876
- } catch (error) {
12877
- if (error instanceof OpenApiDocumentError)
12878
- throw error;
12879
- throw new OpenApiDocumentError;
12880
- }
13223
+ function explainProvider(graph, module, provider) {
13224
+ const dependents = graph.modules.flatMap((candidate) => [
13225
+ ...candidate.providers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.token}`),
13226
+ ...candidate.controllers.filter((item) => item.deps.includes(provider.token)).map((item) => `${candidate.name}.${item.className}`)
13227
+ ]);
13228
+ return [
13229
+ `PROVIDER ${provider.token}`,
13230
+ ` module: ${module.name}`,
13231
+ ` file: ${provider.file}:${provider.line}`,
13232
+ ` kind: ${provider.kind}`,
13233
+ ` scope: ${provider.scope}`,
13234
+ ` exported: ${provider.exported ? "yes" : "no"}`,
13235
+ ` deps: ${provider.deps.length > 0 ? provider.deps.join(", ") : "-"}`,
13236
+ ` depended on by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`
13237
+ ].join(`
13238
+ `);
12881
13239
  }
12882
- async function writeOpenApiJson(document, outputPath, space = 2) {
12883
- const path = resolve13(outputPath);
12884
- const content = serializeOpenApiJson(document, space);
12885
- await mkdir4(dirname9(path), { recursive: true });
12886
- try {
12887
- if (await readFile10(path, "utf8") === content)
12888
- return { path, written: false };
12889
- } catch {}
12890
- const temporaryPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
12891
- try {
12892
- await writeFile5(temporaryPath, content, "utf8");
12893
- await rename6(temporaryPath, path);
12894
- } catch (error) {
12895
- await unlink3(temporaryPath).catch(() => {
12896
- return;
12897
- });
12898
- throw error;
13240
+ function findProvider(graph, subject) {
13241
+ for (const module of graph.modules) {
13242
+ const provider = module.providers.find((candidate) => candidate.token === subject || candidate.useClass === subject || candidate.useFactoryName === subject);
13243
+ if (provider)
13244
+ return { module, provider };
12899
13245
  }
12900
- return { path, written: true };
13246
+ return;
12901
13247
  }
12902
- async function loadGeneratedOpenApiDocument(modulePath) {
12903
- try {
12904
- const moduleUrl = pathToFileURL(resolve13(modulePath));
12905
- moduleUrl.searchParams.set("supacloud-openapi-export", "1");
12906
- const loaded = await import(moduleUrl.href);
12907
- if (!isRecord(loaded))
12908
- throw new OpenApiDocumentError;
12909
- return parseOpenApiDocument(loaded.OPENAPI_DOCUMENT);
12910
- } catch (error) {
12911
- if (error instanceof OpenApiDocumentError)
12912
- throw error;
12913
- throw new OpenApiDocumentError;
13248
+ function exportGraphMermaid(graph) {
13249
+ const lines = ["graph TD"];
13250
+ for (const mod of graph.modules) {
13251
+ const safeId = mod.name.replace(/[^a-zA-Z0-9_]/g, "_");
13252
+ lines.push(` ${safeId}["${mod.className ?? mod.name}"]`);
13253
+ for (const imp of mod.imports) {
13254
+ const safeImp = imp.replace(/[^a-zA-Z0-9_]/g, "_");
13255
+ lines.push(` ${safeId} --> ${safeImp}`);
13256
+ }
12914
13257
  }
13258
+ return lines.join(`
13259
+ `);
12915
13260
  }
12916
- async function exportGeneratedOpenApiJson(options) {
12917
- const document = await loadGeneratedOpenApiDocument(options.modulePath);
12918
- return writeOpenApiJson(document, options.outputPath, options.space);
12919
- }
12920
- function diffOpenApiDocuments(baseValue, currentValue) {
12921
- const baseDocument = parseOpenApiDocument(baseValue);
12922
- const currentDocument = parseOpenApiDocument(currentValue);
12923
- const changes = [];
12924
- const basePaths = baseDocument.paths;
12925
- const currentPaths = currentDocument.paths;
12926
- for (const path of sortedKeys(basePaths)) {
12927
- const basePathItem = recordValue(basePaths[path]);
12928
- const currentPathItem = recordValue(currentPaths[path]);
12929
- if (!basePathItem || !currentPathItem) {
12930
- addChange(changes, {
12931
- kind: "breaking",
12932
- code: "path-removed",
12933
- path: `paths.${path}`,
12934
- message: "A path from the previous contract was removed."
12935
- });
12936
- continue;
12937
- }
12938
- for (const method of HTTP_METHODS) {
12939
- const baseOperation = recordValue(basePathItem[method]);
12940
- const currentOperation = recordValue(currentPathItem[method]);
12941
- if (!baseOperation || !currentOperation) {
12942
- if (baseOperation) {
12943
- addChange(changes, {
12944
- kind: "breaking",
12945
- code: "operation-removed",
12946
- path: `paths.${path}.${method}`,
12947
- message: "An operation from the previous contract was removed."
12948
- });
12949
- } else if (currentOperation) {
12950
- addChange(changes, {
12951
- kind: "non-breaking",
12952
- code: "operation-added",
12953
- path: `paths.${path}.${method}`,
12954
- message: "An operation was added to an existing path."
12955
- });
12956
- }
12957
- continue;
12958
- }
12959
- compareOperation(basePathItem, currentPathItem, baseOperation, currentOperation, baseDocument, currentDocument, `paths.${path}.${method}`, changes);
13261
+ function exportGraphDot(graph) {
13262
+ const lines = [
13263
+ "digraph ApplicationGraph {",
13264
+ " rankdir=LR;",
13265
+ ' node [shape=box, fontname="Helvetica"];'
13266
+ ];
13267
+ for (const mod of graph.modules) {
13268
+ lines.push(` "${mod.name}" [label="${mod.className ?? mod.name}"];`);
13269
+ for (const imp of mod.imports) {
13270
+ lines.push(` "${mod.name}" -> "${imp}";`);
12960
13271
  }
12961
13272
  }
12962
- for (const path of sortedKeys(currentPaths)) {
12963
- if (basePaths[path] !== undefined)
12964
- continue;
12965
- addChange(changes, {
12966
- kind: "non-breaking",
12967
- code: "path-added",
12968
- path: `paths.${path}`,
12969
- message: "A path was added."
12970
- });
12971
- }
12972
- compareComponents(baseDocument, currentDocument, changes);
12973
- const breaking = changes.filter((change) => change.kind === "breaking");
12974
- return { ok: breaking.length === 0, breaking, changes };
12975
- }
12976
- function formatOpenApiDiff(result) {
12977
- if (result.changes.length === 0)
12978
- return "OpenAPI diff passed: no contract changes.";
12979
- return [
12980
- `OpenAPI diff ${result.ok ? "passed" : "failed"}: ${result.breaking.length} breaking change(s).`,
12981
- ...result.changes.map((change) => `${change.kind === "breaking" ? "BREAKING" : "NON-BREAKING"} ${change.code} at ${change.path}: ${change.message}`)
12982
- ].join(`
13273
+ lines.push("}");
13274
+ return lines.join(`
12983
13275
  `);
12984
13276
  }
13277
+
13278
+ // src/index.ts
13279
+ init_generate();
13280
+ init_contract_manifest();
13281
+
12985
13282
  // src/config.ts
12986
13283
  init_graphql_options();
12987
13284
  import { existsSync as existsSync5 } from "node:fs";
12988
- import { join as join8, resolve as resolve14 } from "node:path";
13285
+ import { join as join9, resolve as resolve15 } from "node:path";
12989
13286
  import { pathToFileURL as pathToFileURL2 } from "node:url";
12990
13287
  var DEFAULT_SUPACLOUD_CONFIG = {
12991
13288
  graphql: false,
@@ -13078,8 +13375,8 @@ function validateGovernanceConfig(config) {
13078
13375
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
13079
13376
  const resolved = defineSupacloudConfig(config);
13080
13377
  return {
13081
- rootDir: resolve14(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
13082
- outDir: resolve14(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
13378
+ rootDir: resolve15(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
13379
+ outDir: resolve15(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
13083
13380
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
13084
13381
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
13085
13382
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -13097,16 +13394,16 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
13097
13394
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
13098
13395
  graphql: resolved.graphql ? {
13099
13396
  ...resolved.graphql,
13100
- schema: resolve14(cwd, resolved.graphql.schema)
13397
+ schema: resolve15(cwd, resolved.graphql.schema)
13101
13398
  } : undefined
13102
13399
  };
13103
13400
  }
13104
13401
  async function loadSupacloudConfig(cwd = process.cwd()) {
13105
13402
  const candidates = [
13106
- join8(cwd, "supacloud.config.ts"),
13107
- join8(cwd, "supacloud.config.mts"),
13108
- join8(cwd, "supacloud.config.js"),
13109
- join8(cwd, "supacloud.config.mjs")
13403
+ join9(cwd, "supacloud.config.ts"),
13404
+ join9(cwd, "supacloud.config.mts"),
13405
+ join9(cwd, "supacloud.config.js"),
13406
+ join9(cwd, "supacloud.config.mjs")
13110
13407
  ];
13111
13408
  const configPath = candidates.find((candidate) => existsSync5(candidate));
13112
13409
  if (!configPath)
@@ -13143,6 +13440,8 @@ export {
13143
13440
  TraitCompiler,
13144
13441
  analyzeProject,
13145
13442
  applyDiagnosticFix,
13443
+ assessMigration,
13444
+ buildContractManifest,
13146
13445
  buildDeliveryProject,
13147
13446
  camelName,
13148
13447
  checkProject,
@@ -13164,6 +13463,7 @@ export {
13164
13463
  exportGraphMermaid,
13165
13464
  formatDeliveryPlan,
13166
13465
  formatGraph,
13466
+ formatMigrationAssessment,
13167
13467
  formatOpenApiDiff,
13168
13468
  generateApplication,
13169
13469
  generateDatabaseContracts,