@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/cli.js CHANGED
@@ -78,6 +78,86 @@ function findClosestMatch(target, candidates) {
78
78
  }
79
79
  var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
80
80
 
81
+ // src/contract-manifest.ts
82
+ function buildContractManifest(graph, artifacts) {
83
+ const commands = [];
84
+ const queries = [];
85
+ const routes = [];
86
+ const permissions = new Set;
87
+ const rpc = [];
88
+ const events = [];
89
+ const fixtures = new Set;
90
+ for (const module of graph.modules) {
91
+ for (const command of module.commands) {
92
+ if (command.permission)
93
+ permissions.add(command.permission);
94
+ if (command.rpc)
95
+ rpc.push({ command: command.name, adapter: command.rpc });
96
+ if (command.audit)
97
+ events.push({ name: command.audit, source: "command.audit", command: command.name });
98
+ commands.push({
99
+ name: command.name,
100
+ className: command.className,
101
+ module: module.name,
102
+ ...command.permission === undefined ? {} : { permission: command.permission },
103
+ ...command.rpc === undefined ? {} : { rpc: command.rpc },
104
+ transaction: command.transaction,
105
+ idempotency: command.idempotency,
106
+ ...command.audit === undefined ? {} : { auditEvent: command.audit }
107
+ });
108
+ }
109
+ for (const query of module.queries) {
110
+ queries.push({ name: query.name, className: query.className, module: module.name });
111
+ }
112
+ for (const controller of module.controllers) {
113
+ for (const route of controller.routes) {
114
+ const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
115
+ if (command?.permission)
116
+ permissions.add(command.permission);
117
+ if (route.contract?.evidence)
118
+ fixtures.add(route.contract.evidence);
119
+ routes.push({
120
+ method: route.method,
121
+ path: joinRoutePaths(controller.path, route.path),
122
+ module: module.name,
123
+ controller: controller.className,
124
+ handler: route.handler,
125
+ ...route.command === undefined ? {} : { command: route.command },
126
+ ...command?.permission === undefined ? {} : { permission: command.permission },
127
+ requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
128
+ ...route.response === undefined ? {} : { responseSchema: route.response },
129
+ ...route.responses === undefined ? {} : {
130
+ responseSchemas: Object.fromEntries(Object.entries(route.responses).sort(([left], [right]) => left.localeCompare(right)))
131
+ },
132
+ ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
133
+ });
134
+ }
135
+ }
136
+ }
137
+ commands.sort((left, right) => left.name.localeCompare(right.name));
138
+ queries.sort((left, right) => left.name.localeCompare(right.name));
139
+ routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
140
+ rpc.sort((left, right) => left.command.localeCompare(right.command));
141
+ events.sort((left, right) => left.name.localeCompare(right.name));
142
+ return {
143
+ version: 1,
144
+ commands,
145
+ queries,
146
+ routes,
147
+ permissions: [...permissions].sort(),
148
+ rpc,
149
+ events,
150
+ fixtures: [...fixtures].sort(),
151
+ artifacts: {
152
+ ...artifacts,
153
+ sdk: "generated-client",
154
+ openapiDocument: "generated",
155
+ permissionManifest: "generated"
156
+ }
157
+ };
158
+ }
159
+ var init_contract_manifest = () => {};
160
+
81
161
  // src/generate.ts
82
162
  import { createHash as createHash4 } from "node:crypto";
83
163
  import { access, mkdir, rename, unlink, writeFile } from "node:fs/promises";
@@ -219,9 +299,16 @@ function renderApplication(graph, options) {
219
299
  const clientCode = options.generateClient ? renderClient(graph, options) : undefined;
220
300
  const openApiCode = options.generateOpenApi ? renderOpenApi(graph, options) : undefined;
221
301
  const permissionsCode = options.generatePermissions ? renderPermissions(graph) : undefined;
302
+ const contractsManifest = buildContractManifest(graph, {
303
+ client: clientCode !== undefined,
304
+ openapi: openApiCode !== undefined,
305
+ permissions: permissionsCode !== undefined
306
+ });
222
307
  return {
223
308
  applicationCode: code,
224
309
  manifestJson: JSON.stringify(manifest, null, 2) + `
310
+ `,
311
+ contractsManifestJson: JSON.stringify(contractsManifest, null, 2) + `
225
312
  `,
226
313
  ...clientCode === undefined ? {} : { clientCode },
227
314
  ...openApiCode === undefined ? {} : { openApiCode },
@@ -233,9 +320,11 @@ async function generateApplication(graph, options) {
233
320
  await mkdir(options.outDir, { recursive: true });
234
321
  const applicationPath = join2(options.outDir, "application.ts");
235
322
  const manifestPath = join2(options.outDir, "app.manifest.json");
323
+ const contractsManifestPath = join2(options.outDir, "contracts.manifest.json");
236
324
  const writeCandidates = [
237
325
  { path: applicationPath, content: rendered.applicationCode },
238
- { path: manifestPath, content: rendered.manifestJson }
326
+ { path: manifestPath, content: rendered.manifestJson },
327
+ { path: contractsManifestPath, content: rendered.contractsManifestJson }
239
328
  ];
240
329
  if (rendered.clientCode) {
241
330
  writeCandidates.push({ path: join2(options.outDir, "client.ts"), content: rendered.clientCode });
@@ -2096,7 +2185,9 @@ function destroyScopeInstances(
2096
2185
  scopeDestructions.set(scope, destruction);
2097
2186
  return destruction;
2098
2187
  }`;
2099
- var init_generate = () => {};
2188
+ var init_generate = __esm(() => {
2189
+ init_contract_manifest();
2190
+ });
2100
2191
 
2101
2192
  // src/graphql-client.ts
2102
2193
  var GRAPHQL_CLIENT_SOURCE = `
@@ -2609,12 +2700,12 @@ var init_graphql = __esm(() => {
2609
2700
  });
2610
2701
 
2611
2702
  // src/database-contracts.ts
2612
- import { createHash as createHash9 } from "node:crypto";
2613
- import { mkdir as mkdir5, readFile as readFile11 } from "node:fs/promises";
2614
- import { dirname as dirname10, relative as relative12, resolve as resolve15 } from "node:path";
2703
+ import { createHash as createHash10 } from "node:crypto";
2704
+ import { mkdir as mkdir5, readFile as readFile12 } from "node:fs/promises";
2705
+ import { dirname as dirname10, relative as relative12, resolve as resolve16 } from "node:path";
2615
2706
  import * as ts13 from "@typescript/typescript6";
2616
2707
  function hash2(value) {
2617
- return createHash9("sha256").update(value).digest("hex");
2708
+ return createHash10("sha256").update(value).digest("hex");
2618
2709
  }
2619
2710
  function importPath(out, path) {
2620
2711
  const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
@@ -2639,19 +2730,19 @@ function parseDatabaseContractsOptions(value, directory) {
2639
2730
  throw new TypeError("migrations must be an ordered list of SQL files");
2640
2731
  }
2641
2732
  return {
2642
- rootDir: resolve15(directory, field("rootDir")),
2643
- outDir: resolve15(directory, field("outDir")),
2644
- postgrestTypes: resolve15(directory, field("postgrestTypes")),
2645
- drizzleSchema: resolve15(directory, field("drizzleSchema")),
2733
+ rootDir: resolve16(directory, field("rootDir")),
2734
+ outDir: resolve16(directory, field("outDir")),
2735
+ postgrestTypes: resolve16(directory, field("postgrestTypes")),
2736
+ drizzleSchema: resolve16(directory, field("drizzleSchema")),
2646
2737
  role: field("role"),
2647
- graphql: { ...graphql, schema: resolve15(directory, graphql.schema) },
2648
- migrations: migrations.map((file) => resolve15(directory, file))
2738
+ graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
2739
+ migrations: migrations.map((file) => resolve16(directory, file))
2649
2740
  };
2650
2741
  }
2651
2742
  async function generateDatabaseContracts(options, check = false) {
2652
- const rootDir = resolve15(options.rootDir), outDir = resolve15(options.outDir);
2653
- const postgrestTypes = resolve15(rootDir, options.postgrestTypes), drizzleSchema = resolve15(rootDir, options.drizzleSchema);
2654
- const snapshot = await readFile11(postgrestTypes, "utf8");
2743
+ const rootDir = resolve16(options.rootDir), outDir = resolve16(options.outDir);
2744
+ const postgrestTypes = resolve16(rootDir, options.postgrestTypes), drizzleSchema = resolve16(rootDir, options.drizzleSchema);
2745
+ const snapshot = await readFile12(postgrestTypes, "utf8");
2655
2746
  const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
2656
2747
  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));
2657
2748
  if (!database)
@@ -2678,8 +2769,8 @@ async function generateDatabaseContracts(options, check = false) {
2678
2769
  }
2679
2770
  const inputs = {};
2680
2771
  const addInput = async (path) => {
2681
- const absolute = resolve15(rootDir, path);
2682
- inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile11(absolute, "utf8"));
2772
+ const absolute = resolve16(rootDir, path);
2773
+ inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash2(await readFile12(absolute, "utf8"));
2683
2774
  };
2684
2775
  await addInput(postgrestTypes);
2685
2776
  await addInput(drizzleSchema);
@@ -2695,8 +2786,8 @@ async function generateDatabaseContracts(options, check = false) {
2695
2786
  if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
2696
2787
  await addInput(source.fileName);
2697
2788
  }
2698
- await addInput(resolve15(rootDir, options.graphql.schema));
2699
- if (new Set(options.migrations.map((path) => resolve15(rootDir, path))).size !== options.migrations.length) {
2789
+ await addInput(resolve16(rootDir, options.graphql.schema));
2790
+ if (new Set(options.migrations.map((path) => resolve16(rootDir, path))).size !== options.migrations.length) {
2700
2791
  throw new Error("Duplicate migration in database contracts configuration");
2701
2792
  }
2702
2793
  for (const path of options.migrations)
@@ -2717,17 +2808,17 @@ async function generateDatabaseContracts(options, check = false) {
2717
2808
  version: 1,
2718
2809
  role: options.role,
2719
2810
  inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
2720
- migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve15(rootDir, path)).replaceAll("\\", "/")),
2811
+ migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
2721
2812
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
2722
2813
  };
2723
2814
  files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
2724
2815
  `;
2725
2816
  const mismatches = [];
2726
2817
  for (const [file, content] of Object.entries(files)) {
2727
- const path = resolve15(outDir, file);
2818
+ const path = resolve16(outDir, file);
2728
2819
  let current;
2729
2820
  try {
2730
- current = await readFile11(path, "utf8");
2821
+ current = await readFile12(path, "utf8");
2731
2822
  } catch (error) {
2732
2823
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2733
2824
  throw error;
@@ -2738,13 +2829,13 @@ async function generateDatabaseContracts(options, check = false) {
2738
2829
  if (!check) {
2739
2830
  await mkdir5(outDir, { recursive: true });
2740
2831
  for (const [file, content] of Object.entries(files))
2741
- await writeFileIfChanged(resolve15(outDir, file), content);
2832
+ await writeFileIfChanged(resolve16(outDir, file), content);
2742
2833
  }
2743
2834
  return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
2744
2835
  }
2745
2836
  async function runDatabaseContractsFile(path, check = false) {
2746
- const absolute = resolve15(path);
2747
- const value = JSON.parse(await readFile11(absolute, "utf8"));
2837
+ const absolute = resolve16(path);
2838
+ const value = JSON.parse(await readFile12(absolute, "utf8"));
2748
2839
  return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname10(absolute)), check);
2749
2840
  }
2750
2841
  var init_database_contracts = __esm(() => {
@@ -2754,9 +2845,9 @@ var init_database_contracts = __esm(() => {
2754
2845
  });
2755
2846
 
2756
2847
  // src/graphql-schema.ts
2757
- import { mkdir as mkdir6, readFile as readFile12 } from "node:fs/promises";
2758
- import { createHash as createHash10 } from "node:crypto";
2759
- import { dirname as dirname11, resolve as resolve16 } from "node:path";
2848
+ import { mkdir as mkdir6, readFile as readFile13 } from "node:fs/promises";
2849
+ import { createHash as createHash11 } from "node:crypto";
2850
+ import { dirname as dirname11, resolve as resolve17 } from "node:path";
2760
2851
  async function pullGraphqlSchema(options) {
2761
2852
  assertGraphqlOptions({ schema: options.output });
2762
2853
  const endpoint = new URL(options.url);
@@ -2796,7 +2887,7 @@ async function pullGraphqlSchema(options) {
2796
2887
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
2797
2888
  }
2798
2889
  const schema = lexicographicSortSchema(buildClientSchema(data));
2799
- const path = resolve16(options.output);
2890
+ const path = resolve17(options.output);
2800
2891
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
2801
2892
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
2802
2893
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -2804,13 +2895,13 @@ async function pullGraphqlSchema(options) {
2804
2895
  `;
2805
2896
  let previous;
2806
2897
  try {
2807
- previous = await readFile12(path, "utf8");
2898
+ previous = await readFile13(path, "utf8");
2808
2899
  } catch (error) {
2809
2900
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2810
2901
  throw error;
2811
2902
  }
2812
2903
  const upToDate = previous === content;
2813
- const schemaHash = createHash10("sha256").update(content).digest("hex");
2904
+ const schemaHash = createHash11("sha256").update(content).digest("hex");
2814
2905
  if (options.check)
2815
2906
  return { path, schemaHash, upToDate, written: false };
2816
2907
  await mkdir6(dirname11(path), { recursive: true });
@@ -2823,8 +2914,8 @@ var init_graphql_schema = __esm(() => {
2823
2914
  });
2824
2915
 
2825
2916
  // src/cli.ts
2826
- import { resolve as resolve17 } from "node:path";
2827
- import { readFile as readFile13 } from "node:fs/promises";
2917
+ import { resolve as resolve18 } from "node:path";
2918
+ import { readFile as readFile14 } from "node:fs/promises";
2828
2919
 
2829
2920
  // src/analyze.ts
2830
2921
  import { createHash as createHash3 } from "node:crypto";
@@ -6985,6 +7076,7 @@ async function compileProject(options) {
6985
7076
  "client.ts": rendered.clientCode,
6986
7077
  "openapi.ts": rendered.openApiCode,
6987
7078
  "permissions.ts": rendered.permissionsCode,
7079
+ "contracts.manifest.json": rendered.contractsManifestJson,
6988
7080
  "graphql.ts": graphql.files["graphql.ts"],
6989
7081
  "graphql.documents.ts": graphql.files["graphql.documents.ts"]
6990
7082
  }, options.strict ?? false));
@@ -7054,7 +7146,8 @@ async function checkProject(options) {
7054
7146
  const expectedFiles = {
7055
7147
  ...graphql.files,
7056
7148
  "application.ts": rendered.applicationCode,
7057
- "app.manifest.json": rendered.manifestJson
7149
+ "app.manifest.json": rendered.manifestJson,
7150
+ "contracts.manifest.json": rendered.contractsManifestJson
7058
7151
  };
7059
7152
  if (rendered.clientCode) {
7060
7153
  expectedFiles["client.ts"] = rendered.clientCode;
@@ -7521,6 +7614,8 @@ function optionsKeyOf(options) {
7521
7614
  requireRouteContracts: options.requireRouteContracts,
7522
7615
  detectOrphanModules: options.detectOrphanModules,
7523
7616
  generateClient: options.generateClient,
7617
+ generateOpenApi: options.generateOpenApi,
7618
+ openApi: options.openApi,
7524
7619
  generatePermissions: options.generatePermissions,
7525
7620
  typeSafety: options.typeSafety,
7526
7621
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
@@ -7655,6 +7750,16 @@ function findAffectedModules(previous, current, changedFiles) {
7655
7750
 
7656
7751
  // src/watch.ts
7657
7752
  var DEFAULT_DEBOUNCE_MS = 100;
7753
+ function isCompilerConfigurationPath(rootDir, changedPath) {
7754
+ const relativePath = relative7(rootDir, changedPath).split(sep7).join("/");
7755
+ return [
7756
+ "supacloud.config.ts",
7757
+ "supacloud.config.mts",
7758
+ "supacloud.config.js",
7759
+ "supacloud.config.mjs",
7760
+ "tsconfig.json"
7761
+ ].includes(relativePath) || /^tsconfig\.[^/]+\.json$/.test(relativePath);
7762
+ }
7658
7763
  function watchProject(options) {
7659
7764
  const rootDir = resolve6(options.rootDir);
7660
7765
  const outDir = resolve6(options.outDir);
@@ -7747,7 +7852,7 @@ function watchProject(options) {
7747
7852
  const relativePath = relative7(outDir, changedPath);
7748
7853
  if (!relativePath.startsWith("..") && relativePath !== "")
7749
7854
  return;
7750
- if (/\.(tsx?|mts|cts)$/.test(changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
7855
+ if (/\.(tsx?|mts|cts)$/.test(changedPath) || isCompilerConfigurationPath(rootDir, changedPath) || options.graphql && (/\.(graphql|gql)$/.test(changedPath) || changedPath === schemaPath)) {
7751
7856
  schedule(relative7(rootDir, changedPath));
7752
7857
  }
7753
7858
  });
@@ -12205,7 +12310,7 @@ function compilerVersion() {
12205
12310
  }
12206
12311
  function migrationDependencies() {
12207
12312
  return {
12208
- "@supacloud/app": "0.15.0",
12313
+ "@supacloud/app": "0.16.0",
12209
12314
  "@supacloud/compiler": compilerVersion(),
12210
12315
  "@supacloud/elysia": "0.18.0",
12211
12316
  elysia: "1.4.30",
@@ -12626,6 +12731,11 @@ async function migrateProject(options) {
12626
12731
  };
12627
12732
  }
12628
12733
 
12734
+ // src/migration-assess.ts
12735
+ import { createHash as createHash9 } from "node:crypto";
12736
+ import { readFile as readFile11 } from "node:fs/promises";
12737
+ import { join as join9, resolve as resolve15 } from "node:path";
12738
+
12629
12739
  // src/openapi-tools.ts
12630
12740
  import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
12631
12741
  import { dirname as dirname9, resolve as resolve14 } from "node:path";
@@ -13131,10 +13241,199 @@ function formatOpenApiDiff(result) {
13131
13241
  `);
13132
13242
  }
13133
13243
 
13244
+ // src/migration-assess.ts
13245
+ var STATUS_PRIORITY = {
13246
+ compatible: 0,
13247
+ "needs-review": 1,
13248
+ "not-proven": 2,
13249
+ unsupported: 3,
13250
+ breaking: 4
13251
+ };
13252
+ function isRecord2(value) {
13253
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13254
+ }
13255
+ function arrayLength(value) {
13256
+ return Array.isArray(value) ? value.length : undefined;
13257
+ }
13258
+ async function sha256(path) {
13259
+ return createHash9("sha256").update(await readFile11(path)).digest("hex");
13260
+ }
13261
+ function overallStatus(findings) {
13262
+ return findings.reduce((current, finding) => STATUS_PRIORITY[finding.status] > STATUS_PRIORITY[current] ? finding.status : current, "compatible");
13263
+ }
13264
+ async function assessMigration(options) {
13265
+ const projectDir = resolve15(options.projectDir);
13266
+ const compile = {
13267
+ ...options.compile,
13268
+ rootDir: resolve15(options.compile.rootDir),
13269
+ outDir: resolve15(options.compile.outDir)
13270
+ };
13271
+ const findings = [];
13272
+ const dependencyProblems = await checkMigrationDependencies(projectDir);
13273
+ if (dependencyProblems.length > 0) {
13274
+ findings.push({
13275
+ code: "migration-dependency-tuple-not-proven",
13276
+ status: "not-proven",
13277
+ message: "The installed compiler migration dependency tuple is not the exact tested tuple.",
13278
+ evidence: dependencyProblems.join("; "),
13279
+ remediation: "Install the exact tested package versions before applying a source migration."
13280
+ });
13281
+ }
13282
+ let compiler = {
13283
+ upToDate: false,
13284
+ diagnostics: [],
13285
+ mismatches: []
13286
+ };
13287
+ try {
13288
+ const checked = await checkProject(compile);
13289
+ compiler = {
13290
+ upToDate: checked.upToDate,
13291
+ diagnostics: checked.diagnostics,
13292
+ mismatches: checked.mismatches
13293
+ };
13294
+ const errors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
13295
+ if (errors.length > 0) {
13296
+ findings.push({
13297
+ code: "compiler-governance-not-proven",
13298
+ status: "not-proven",
13299
+ message: `Compiler governance has ${errors.length} error(s).`,
13300
+ remediation: "Resolve compiler diagnostics without weakening governance, then reassess."
13301
+ });
13302
+ }
13303
+ if (!checked.upToDate) {
13304
+ findings.push({
13305
+ code: "generated-artifact-drift",
13306
+ status: "needs-review",
13307
+ message: "Generated artifacts do not match the current source and compiler configuration.",
13308
+ evidence: checked.mismatches.join("; "),
13309
+ remediation: "Regenerate candidate artifacts in isolation and review the diff before adoption."
13310
+ });
13311
+ }
13312
+ } catch {
13313
+ findings.push({
13314
+ code: "compiler-check-unavailable",
13315
+ status: "not-proven",
13316
+ message: "The local compiler check could not complete.",
13317
+ remediation: "Verify the project source, configuration and installed dependencies, then reassess."
13318
+ });
13319
+ }
13320
+ const contractsManifestPath = join9(compile.outDir, "contracts.manifest.json");
13321
+ let contractsManifest = {
13322
+ path: contractsManifestPath,
13323
+ present: false
13324
+ };
13325
+ try {
13326
+ const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
13327
+ if (!isRecord2(parsed) || parsed.version !== 1)
13328
+ throw new Error("invalid manifest");
13329
+ contractsManifest = {
13330
+ path: contractsManifestPath,
13331
+ present: true,
13332
+ sha256: await sha256(contractsManifestPath),
13333
+ version: 1,
13334
+ ...arrayLength(parsed.commands) === undefined ? {} : { commands: arrayLength(parsed.commands) },
13335
+ ...arrayLength(parsed.routes) === undefined ? {} : { routes: arrayLength(parsed.routes) },
13336
+ ...arrayLength(parsed.permissions) === undefined ? {} : { permissions: arrayLength(parsed.permissions) }
13337
+ };
13338
+ } catch {
13339
+ findings.push({
13340
+ code: "contract-manifest-not-proven",
13341
+ status: "not-proven",
13342
+ message: "contracts.manifest.json is missing or invalid.",
13343
+ evidence: contractsManifestPath,
13344
+ remediation: "Generate and review the contract manifest before migration."
13345
+ });
13346
+ }
13347
+ let openApi;
13348
+ if (options.baselineOpenApiPath && options.currentOpenApiPath) {
13349
+ const baselinePath = resolve15(projectDir, options.baselineOpenApiPath);
13350
+ const currentPath = resolve15(projectDir, options.currentOpenApiPath);
13351
+ try {
13352
+ const diff = diffOpenApiDocuments(await readOpenApiJson(baselinePath), await readOpenApiJson(currentPath));
13353
+ openApi = {
13354
+ baselinePath,
13355
+ baselineSha256: await sha256(baselinePath),
13356
+ currentPath,
13357
+ currentSha256: await sha256(currentPath),
13358
+ diff
13359
+ };
13360
+ if (!diff.ok) {
13361
+ findings.push({
13362
+ code: "openapi-breaking-change",
13363
+ status: "breaking",
13364
+ message: `OpenAPI contains ${diff.breaking.length} breaking change(s).`,
13365
+ remediation: "Review and explicitly approve or redesign each breaking contract change."
13366
+ });
13367
+ } else if (diff.changes.length > 0) {
13368
+ findings.push({
13369
+ code: "openapi-compatible-change-review",
13370
+ status: "needs-review",
13371
+ message: `OpenAPI contains ${diff.changes.length} non-breaking change(s) requiring release review.`
13372
+ });
13373
+ }
13374
+ } catch {
13375
+ findings.push({
13376
+ code: "openapi-diff-not-proven",
13377
+ status: "not-proven",
13378
+ message: "The OpenAPI baseline or current document could not be loaded.",
13379
+ remediation: "Export both OpenAPI documents as JSON and rerun the assessment."
13380
+ });
13381
+ }
13382
+ } else {
13383
+ findings.push({
13384
+ code: "openapi-baseline-not-proven",
13385
+ status: "not-proven",
13386
+ message: "No complete OpenAPI baseline/current pair was supplied.",
13387
+ remediation: "Provide both --baseline-openapi and --current-openapi for compatibility evidence."
13388
+ });
13389
+ }
13390
+ findings.push({
13391
+ code: "rendering-mode-preserved",
13392
+ status: "compatible",
13393
+ message: "SupaCloud migration does not require SSR and does not change the existing rendering topology.",
13394
+ evidence: `selected=${options.renderMode ?? "unspecified"}`
13395
+ });
13396
+ const status = overallStatus(findings);
13397
+ return {
13398
+ version: 1,
13399
+ ok: status === "compatible" || status === "needs-review",
13400
+ status,
13401
+ readOnly: true,
13402
+ writesPerformed: false,
13403
+ rendering: {
13404
+ selected: options.renderMode ?? "unspecified",
13405
+ ssrRequired: false,
13406
+ supportedModes: ["browser", "ssr", "edge", "trusted-server"],
13407
+ guidance: "Keep the application's existing CSR, SPA, SSR or edge topology; use trusted adapters only for governed server operations."
13408
+ },
13409
+ dependencies: {
13410
+ expected: migrationDependencies(),
13411
+ problems: dependencyProblems
13412
+ },
13413
+ compiler,
13414
+ artifacts: {
13415
+ contractsManifest,
13416
+ ...openApi === undefined ? {} : { openApi }
13417
+ },
13418
+ findings
13419
+ };
13420
+ }
13421
+ function formatMigrationAssessment(result) {
13422
+ return [
13423
+ `Migration assessment: ${result.status}. Read-only; no files, databases or remote environments were changed.`,
13424
+ `Rendering: ${result.rendering.selected}; SSR required: no.`,
13425
+ ...result.findings.map((finding) => `${finding.status.toUpperCase()} ${finding.code}: ${finding.message}`)
13426
+ ].join(`
13427
+ `);
13428
+ }
13429
+
13134
13430
  // src/cli.ts
13135
13431
  function isModuleBoundaryPresetName(value) {
13136
13432
  return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
13137
13433
  }
13434
+ function isMigrationRenderMode(value) {
13435
+ return value === "unspecified" || value === "browser" || value === "ssr" || value === "edge" || value === "trusted-server";
13436
+ }
13138
13437
  function printUsage() {
13139
13438
  console.log(`
13140
13439
  @supacloud/compiler CLI
@@ -13148,6 +13447,7 @@ Usage:
13148
13447
  supacloud-compiler context <module> [rootDir] [options]
13149
13448
  supacloud-compiler doctor [rootDir] [options]
13150
13449
  supacloud-compiler migrate [rootDir] [options]
13450
+ supacloud-compiler migration-assess [rootDir] [options]
13151
13451
  supacloud-compiler plan [rootDir] [options]
13152
13452
  supacloud-compiler build-delivery [rootDir] [options]
13153
13453
  supacloud-compiler openapi-export <openapi-module> <output.json> [options]
@@ -13165,6 +13465,7 @@ Commands:
13165
13465
  context Extract an AI-sized module context pack
13166
13466
  doctor Run project and generated-artifact health checks
13167
13467
  migrate Preview or apply versioned source migrations
13468
+ migration-assess Produce a read-only migration compatibility report
13168
13469
  plan Preview deterministic workload targets without writing or deploying
13169
13470
  build-delivery Build independent local factories and an atomic delivery manifest (Bun)
13170
13471
  openapi-export Export a generated OpenAPI module to a standalone JSON document
@@ -13188,13 +13489,16 @@ Options:
13188
13489
  --token-env <name> Environment variable holding the intended user's access token
13189
13490
  --check graphql-schema: compare the remote schema without changing the snapshot
13190
13491
  --debounce <ms> Debounce source changes in dev mode (default: 100)
13191
- --json Print machine-readable output for compile/check/graph/explain/context/doctor/plan/build-delivery/openapi-export/openapi-diff
13492
+ --json Print machine-readable output for compile/check/graph/explain/context/doctor/migration-assess/plan/build-delivery/openapi-export/openapi-diff
13192
13493
  --space <n> openapi-export: JSON indentation (0-10, default: 2)
13193
13494
  --delivery <file> plan/build-delivery: validated JSON configuration (overrides config.delivery)
13194
13495
  --dry-run Preview a fix without writing the target file
13195
13496
  --write Apply a fix or migration to disk (preview-only by default)
13196
13497
  --from-version Migration source-format checkpoint (requires --to-version)
13197
13498
  --to-version Migration target checkpoint; verifies installed dependencies
13499
+ --baseline-openapi OpenAPI baseline JSON for migration-assess
13500
+ --current-openapi Current OpenAPI JSON for migration-assess
13501
+ --render-mode Optional browser | ssr | edge | trusted-server label; SSR is never required
13198
13502
  --preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
13199
13503
  --help, -h Show this help
13200
13504
  `);
@@ -13218,7 +13522,7 @@ async function run() {
13218
13522
  process.exitCode = 1;
13219
13523
  return;
13220
13524
  }
13221
- if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
13525
+ if (!command || !["compile", "check", "dev", "graph", "explain", "context", "doctor", "migrate", "migration-assess", "fix", "graphql-schema", "plan", "build-delivery", "openapi-export", "openapi-diff"].includes(command)) {
13222
13526
  console.error(`Error: unknown command "${command}"`);
13223
13527
  printUsage();
13224
13528
  process.exit(1);
@@ -13242,6 +13546,9 @@ async function run() {
13242
13546
  let tokenEnv;
13243
13547
  let checkSchema = false;
13244
13548
  let deliveryPath;
13549
+ let baselineOpenApi;
13550
+ let currentOpenApi;
13551
+ let renderMode = "unspecified";
13245
13552
  const openApiDiffPaths = [];
13246
13553
  const openApiExportPaths = [];
13247
13554
  let openApiExportSpace;
@@ -13346,9 +13653,27 @@ async function run() {
13346
13653
  } else if (arg === "--dry-run") {
13347
13654
  dryRun = true;
13348
13655
  } else if (arg === "--write") {
13349
- if (command === "plan")
13350
- throw new Error("plan is read-only; --write is not supported");
13656
+ if (command === "plan" || command === "migration-assess")
13657
+ throw new Error(`${command} is read-only; --write is not supported`);
13351
13658
  dryRun = false;
13659
+ } else if (arg === "--baseline-openapi" || arg === "--current-openapi") {
13660
+ if (command !== "migration-assess")
13661
+ throw new Error(`${arg} is only supported by migration-assess`);
13662
+ const value = args[++i];
13663
+ if (!value || value.startsWith("-"))
13664
+ throw new Error(`${arg} requires a JSON file path`);
13665
+ if (arg === "--baseline-openapi")
13666
+ baselineOpenApi = value;
13667
+ else
13668
+ currentOpenApi = value;
13669
+ } else if (arg === "--render-mode") {
13670
+ if (command !== "migration-assess")
13671
+ throw new Error("--render-mode is only supported by migration-assess");
13672
+ const value = args[++i];
13673
+ if (!isMigrationRenderMode(value)) {
13674
+ throw new Error("--render-mode requires browser, ssr, edge or trusted-server");
13675
+ }
13676
+ renderMode = value;
13352
13677
  } else if (arg === "--from-version" || arg === "--to-version") {
13353
13678
  if (command !== "migrate")
13354
13679
  throw new Error(`${arg} is only supported by migrate`);
@@ -13389,7 +13714,7 @@ async function run() {
13389
13714
  const currentPath = openApiDiffPaths[1];
13390
13715
  if (!basePath || !currentPath)
13391
13716
  throw new Error("openapi-diff requires two JSON file paths");
13392
- const result = diffOpenApiDocuments(await readOpenApiJson(resolve17(process.cwd(), basePath)), await readOpenApiJson(resolve17(process.cwd(), currentPath)));
13717
+ const result = diffOpenApiDocuments(await readOpenApiJson(resolve18(process.cwd(), basePath)), await readOpenApiJson(resolve18(process.cwd(), currentPath)));
13393
13718
  console.log(json ? JSON.stringify(result, null, 2) : formatOpenApiDiff(result));
13394
13719
  if (!result.ok)
13395
13720
  process.exitCode = 1;
@@ -13404,8 +13729,8 @@ async function run() {
13404
13729
  if (!modulePath || !outputPath)
13405
13730
  throw new Error("openapi-export requires an OpenAPI module and output path");
13406
13731
  const result = await exportGeneratedOpenApiJson({
13407
- modulePath: resolve17(process.cwd(), modulePath),
13408
- outputPath: resolve17(process.cwd(), outputPath),
13732
+ modulePath: resolve18(process.cwd(), modulePath),
13733
+ outputPath: resolve18(process.cwd(), outputPath),
13409
13734
  ...openApiExportSpace === undefined ? {} : { space: openApiExportSpace }
13410
13735
  });
13411
13736
  console.log(json ? JSON.stringify({ ok: true, ...result }, null, 2) : result.written ? `OpenAPI JSON written: ${result.path}` : `OpenAPI JSON matches: ${result.path}`);
@@ -13413,7 +13738,7 @@ async function run() {
13413
13738
  }
13414
13739
  if (command === "migrate") {
13415
13740
  const result = await migrateProject({
13416
- rootDir: rootDir ? resolve17(process.cwd(), rootDir) : process.cwd(),
13741
+ rootDir: rootDir ? resolve18(process.cwd(), rootDir) : process.cwd(),
13417
13742
  write: !dryRun,
13418
13743
  ...fromVersion === undefined ? {} : { fromVersion },
13419
13744
  ...toVersion === undefined ? {} : { toVersion }
@@ -13441,8 +13766,8 @@ async function run() {
13441
13766
  if (checkSchema && command !== "graphql-schema")
13442
13767
  throw new Error("--check is only supported by graphql-schema");
13443
13768
  const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
13444
- const resolvedRoot = rootDir ? resolve17(process.cwd(), rootDir) : defaults.rootDir;
13445
- const resolvedOut = outDir ? resolve17(process.cwd(), outDir) : defaults.outDir;
13769
+ const resolvedRoot = rootDir ? resolve18(process.cwd(), rootDir) : defaults.rootDir;
13770
+ const resolvedOut = outDir ? resolve18(process.cwd(), outDir) : defaults.outDir;
13446
13771
  const configured = compileOptionsFromConfig({
13447
13772
  ...loadedConfig,
13448
13773
  root: resolvedRoot,
@@ -13461,7 +13786,7 @@ async function run() {
13461
13786
  let delivery = loadedConfig.delivery;
13462
13787
  if (deliveryPath !== undefined) {
13463
13788
  try {
13464
- delivery = JSON.parse(await readFile13(resolve17(process.cwd(), deliveryPath), "utf8"));
13789
+ delivery = JSON.parse(await readFile14(resolve18(process.cwd(), deliveryPath), "utf8"));
13465
13790
  } catch {
13466
13791
  throw new DeliveryConfigurationError;
13467
13792
  }
@@ -13505,10 +13830,21 @@ ${item.suggestion ?? ""}`).join(`
13505
13830
  console.log(json ? JSON.stringify({ ok: result.upToDate, ...result }, null, 2) : result.written ? `GraphQL schema written: ${result.path}` : result.upToDate ? `GraphQL schema matches: ${result.path}` : `GraphQL schema drift: ${result.path}. Export the role-scoped snapshot and compile before promotion.`);
13506
13831
  if (!result.upToDate)
13507
13832
  process.exit(1);
13833
+ } else if (command === "migration-assess") {
13834
+ const result = await assessMigration({
13835
+ projectDir: process.cwd(),
13836
+ compile: compileDefaults,
13837
+ ...baselineOpenApi === undefined ? {} : { baselineOpenApiPath: baselineOpenApi },
13838
+ ...currentOpenApi === undefined ? {} : { currentOpenApiPath: currentOpenApi },
13839
+ renderMode
13840
+ });
13841
+ console.log(json ? JSON.stringify(result, null, 2) : formatMigrationAssessment(result));
13842
+ if (result.status === "breaking" || result.status === "unsupported")
13843
+ process.exitCode = 1;
13508
13844
  } else if (command === "fix") {
13509
13845
  if (!query)
13510
13846
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
13511
- const fix = JSON.parse(await readFile13(resolve17(process.cwd(), query), "utf8"));
13847
+ const fix = JSON.parse(await readFile14(resolve18(process.cwd(), query), "utf8"));
13512
13848
  const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
13513
13849
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
13514
13850
  } else if (command === "compile") {