@supacloud/compiler 0.24.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/README.md CHANGED
@@ -96,6 +96,24 @@ protocol migration are documented in `docs/fa-consumer-governance.md` in the
96
96
  repository. `context <module> --json` reports `routeContracts` and standalone
97
97
  command execution plans; these are declarations and obligations, not runtime proof.
98
98
 
99
+ ## Migration Assessment
100
+
101
+ `migration-assess` creates a local, read-only compatibility report from the
102
+ installed tested tuple, generated contract manifest, compiler drift checks and
103
+ an optional OpenAPI baseline/current pair:
104
+
105
+ ```bash
106
+ bunx supacloud-compiler migration-assess --json \
107
+ --baseline-openapi artifacts/openapi-baseline.json \
108
+ --current-openapi artifacts/openapi-current.json
109
+ ```
110
+
111
+ The report distinguishes compatible changes, review items, breaking changes and
112
+ evidence that is not yet proven. It records artifact hashes and never writes
113
+ files, databases or remote environments. Rendering is explicitly independent:
114
+ CSR/SPA, SvelteKit, Nuxt, SSR, edge and trusted-server arrangements are valid;
115
+ SSR is never a migration prerequisite.
116
+
99
117
  SupaCloud 应用静态编译器:读取 `@supacloud/app` 装饰器元数据的原生 TypeScript AST,构建 ApplicationGraph,做静态校验,并生成**无反射、无容器**的工厂代码与 manifest。
100
118
 
101
119
  本包不依赖 `@supacloud/app`:AST 只按装饰器名匹配(`Module`/`Injectable`/`Inject`/`Command`/`Query`/`Controller`/`Get`/`Post`/`Put`/`Patch`/`Delete`/`defineModule`/`InjectionToken`),不校验 import 来源。
package/dist/cli.js CHANGED
@@ -126,6 +126,9 @@ function buildContractManifest(graph, artifacts) {
126
126
  ...command?.permission === undefined ? {} : { permission: command.permission },
127
127
  requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
128
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
+ },
129
132
  ...route.contract?.evidence === undefined ? {} : { evidence: route.contract.evidence }
130
133
  });
131
134
  }
@@ -2697,12 +2700,12 @@ var init_graphql = __esm(() => {
2697
2700
  });
2698
2701
 
2699
2702
  // src/database-contracts.ts
2700
- import { createHash as createHash9 } from "node:crypto";
2701
- import { mkdir as mkdir5, readFile as readFile11 } from "node:fs/promises";
2702
- 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";
2703
2706
  import * as ts13 from "@typescript/typescript6";
2704
2707
  function hash2(value) {
2705
- return createHash9("sha256").update(value).digest("hex");
2708
+ return createHash10("sha256").update(value).digest("hex");
2706
2709
  }
2707
2710
  function importPath(out, path) {
2708
2711
  const value = relative12(out, path).replaceAll("\\", "/").replace(/\.(?:d\.)?[cm]?ts$/, "");
@@ -2727,19 +2730,19 @@ function parseDatabaseContractsOptions(value, directory) {
2727
2730
  throw new TypeError("migrations must be an ordered list of SQL files");
2728
2731
  }
2729
2732
  return {
2730
- rootDir: resolve15(directory, field("rootDir")),
2731
- outDir: resolve15(directory, field("outDir")),
2732
- postgrestTypes: resolve15(directory, field("postgrestTypes")),
2733
- 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")),
2734
2737
  role: field("role"),
2735
- graphql: { ...graphql, schema: resolve15(directory, graphql.schema) },
2736
- migrations: migrations.map((file) => resolve15(directory, file))
2738
+ graphql: { ...graphql, schema: resolve16(directory, graphql.schema) },
2739
+ migrations: migrations.map((file) => resolve16(directory, file))
2737
2740
  };
2738
2741
  }
2739
2742
  async function generateDatabaseContracts(options, check = false) {
2740
- const rootDir = resolve15(options.rootDir), outDir = resolve15(options.outDir);
2741
- const postgrestTypes = resolve15(rootDir, options.postgrestTypes), drizzleSchema = resolve15(rootDir, options.drizzleSchema);
2742
- 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");
2743
2746
  const syntax = ts13.createSourceFile(postgrestTypes, snapshot, ts13.ScriptTarget.Latest, true);
2744
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));
2745
2748
  if (!database)
@@ -2766,8 +2769,8 @@ async function generateDatabaseContracts(options, check = false) {
2766
2769
  }
2767
2770
  const inputs = {};
2768
2771
  const addInput = async (path) => {
2769
- const absolute = resolve15(rootDir, path);
2770
- 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"));
2771
2774
  };
2772
2775
  await addInput(postgrestTypes);
2773
2776
  await addInput(drizzleSchema);
@@ -2783,8 +2786,8 @@ async function generateDatabaseContracts(options, check = false) {
2783
2786
  if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
2784
2787
  await addInput(source.fileName);
2785
2788
  }
2786
- await addInput(resolve15(rootDir, options.graphql.schema));
2787
- 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) {
2788
2791
  throw new Error("Duplicate migration in database contracts configuration");
2789
2792
  }
2790
2793
  for (const path of options.migrations)
@@ -2805,17 +2808,17 @@ async function generateDatabaseContracts(options, check = false) {
2805
2808
  version: 1,
2806
2809
  role: options.role,
2807
2810
  inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => a.localeCompare(b))),
2808
- migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve15(rootDir, path)).replaceAll("\\", "/")),
2811
+ migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve16(rootDir, path)).replaceAll("\\", "/")),
2809
2812
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash2(text)]))
2810
2813
  };
2811
2814
  files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
2812
2815
  `;
2813
2816
  const mismatches = [];
2814
2817
  for (const [file, content] of Object.entries(files)) {
2815
- const path = resolve15(outDir, file);
2818
+ const path = resolve16(outDir, file);
2816
2819
  let current;
2817
2820
  try {
2818
- current = await readFile11(path, "utf8");
2821
+ current = await readFile12(path, "utf8");
2819
2822
  } catch (error) {
2820
2823
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2821
2824
  throw error;
@@ -2826,13 +2829,13 @@ async function generateDatabaseContracts(options, check = false) {
2826
2829
  if (!check) {
2827
2830
  await mkdir5(outDir, { recursive: true });
2828
2831
  for (const [file, content] of Object.entries(files))
2829
- await writeFileIfChanged(resolve15(outDir, file), content);
2832
+ await writeFileIfChanged(resolve16(outDir, file), content);
2830
2833
  }
2831
2834
  return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
2832
2835
  }
2833
2836
  async function runDatabaseContractsFile(path, check = false) {
2834
- const absolute = resolve15(path);
2835
- const value = JSON.parse(await readFile11(absolute, "utf8"));
2837
+ const absolute = resolve16(path);
2838
+ const value = JSON.parse(await readFile12(absolute, "utf8"));
2836
2839
  return generateDatabaseContracts(parseDatabaseContractsOptions(value, dirname10(absolute)), check);
2837
2840
  }
2838
2841
  var init_database_contracts = __esm(() => {
@@ -2842,9 +2845,9 @@ var init_database_contracts = __esm(() => {
2842
2845
  });
2843
2846
 
2844
2847
  // src/graphql-schema.ts
2845
- import { mkdir as mkdir6, readFile as readFile12 } from "node:fs/promises";
2846
- import { createHash as createHash10 } from "node:crypto";
2847
- 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";
2848
2851
  async function pullGraphqlSchema(options) {
2849
2852
  assertGraphqlOptions({ schema: options.output });
2850
2853
  const endpoint = new URL(options.url);
@@ -2884,7 +2887,7 @@ async function pullGraphqlSchema(options) {
2884
2887
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
2885
2888
  }
2886
2889
  const schema = lexicographicSortSchema(buildClientSchema(data));
2887
- const path = resolve16(options.output);
2890
+ const path = resolve17(options.output);
2888
2891
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
2889
2892
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
2890
2893
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -2892,13 +2895,13 @@ async function pullGraphqlSchema(options) {
2892
2895
  `;
2893
2896
  let previous;
2894
2897
  try {
2895
- previous = await readFile12(path, "utf8");
2898
+ previous = await readFile13(path, "utf8");
2896
2899
  } catch (error) {
2897
2900
  if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
2898
2901
  throw error;
2899
2902
  }
2900
2903
  const upToDate = previous === content;
2901
- const schemaHash = createHash10("sha256").update(content).digest("hex");
2904
+ const schemaHash = createHash11("sha256").update(content).digest("hex");
2902
2905
  if (options.check)
2903
2906
  return { path, schemaHash, upToDate, written: false };
2904
2907
  await mkdir6(dirname11(path), { recursive: true });
@@ -2911,8 +2914,8 @@ var init_graphql_schema = __esm(() => {
2911
2914
  });
2912
2915
 
2913
2916
  // src/cli.ts
2914
- import { resolve as resolve17 } from "node:path";
2915
- 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";
2916
2919
 
2917
2920
  // src/analyze.ts
2918
2921
  import { createHash as createHash3 } from "node:crypto";
@@ -7611,6 +7614,8 @@ function optionsKeyOf(options) {
7611
7614
  requireRouteContracts: options.requireRouteContracts,
7612
7615
  detectOrphanModules: options.detectOrphanModules,
7613
7616
  generateClient: options.generateClient,
7617
+ generateOpenApi: options.generateOpenApi,
7618
+ openApi: options.openApi,
7614
7619
  generatePermissions: options.generatePermissions,
7615
7620
  typeSafety: options.typeSafety,
7616
7621
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
@@ -7745,6 +7750,16 @@ function findAffectedModules(previous, current, changedFiles) {
7745
7750
 
7746
7751
  // src/watch.ts
7747
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
+ }
7748
7763
  function watchProject(options) {
7749
7764
  const rootDir = resolve6(options.rootDir);
7750
7765
  const outDir = resolve6(options.outDir);
@@ -7837,7 +7852,7 @@ function watchProject(options) {
7837
7852
  const relativePath = relative7(outDir, changedPath);
7838
7853
  if (!relativePath.startsWith("..") && relativePath !== "")
7839
7854
  return;
7840
- 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)) {
7841
7856
  schedule(relative7(rootDir, changedPath));
7842
7857
  }
7843
7858
  });
@@ -12716,6 +12731,11 @@ async function migrateProject(options) {
12716
12731
  };
12717
12732
  }
12718
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
+
12719
12739
  // src/openapi-tools.ts
12720
12740
  import { mkdir as mkdir4, readFile as readFile10, rename as rename6, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
12721
12741
  import { dirname as dirname9, resolve as resolve14 } from "node:path";
@@ -13221,10 +13241,199 @@ function formatOpenApiDiff(result) {
13221
13241
  `);
13222
13242
  }
13223
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
+
13224
13430
  // src/cli.ts
13225
13431
  function isModuleBoundaryPresetName(value) {
13226
13432
  return value === "modular-monolith" || value === "feature-slices" || value === "vertical-slices" || value === "angular-enterprise" || value === "angular" || value === "clean-architecture" || value === "domain-driven";
13227
13433
  }
13434
+ function isMigrationRenderMode(value) {
13435
+ return value === "unspecified" || value === "browser" || value === "ssr" || value === "edge" || value === "trusted-server";
13436
+ }
13228
13437
  function printUsage() {
13229
13438
  console.log(`
13230
13439
  @supacloud/compiler CLI
@@ -13238,6 +13447,7 @@ Usage:
13238
13447
  supacloud-compiler context <module> [rootDir] [options]
13239
13448
  supacloud-compiler doctor [rootDir] [options]
13240
13449
  supacloud-compiler migrate [rootDir] [options]
13450
+ supacloud-compiler migration-assess [rootDir] [options]
13241
13451
  supacloud-compiler plan [rootDir] [options]
13242
13452
  supacloud-compiler build-delivery [rootDir] [options]
13243
13453
  supacloud-compiler openapi-export <openapi-module> <output.json> [options]
@@ -13255,6 +13465,7 @@ Commands:
13255
13465
  context Extract an AI-sized module context pack
13256
13466
  doctor Run project and generated-artifact health checks
13257
13467
  migrate Preview or apply versioned source migrations
13468
+ migration-assess Produce a read-only migration compatibility report
13258
13469
  plan Preview deterministic workload targets without writing or deploying
13259
13470
  build-delivery Build independent local factories and an atomic delivery manifest (Bun)
13260
13471
  openapi-export Export a generated OpenAPI module to a standalone JSON document
@@ -13278,13 +13489,16 @@ Options:
13278
13489
  --token-env <name> Environment variable holding the intended user's access token
13279
13490
  --check graphql-schema: compare the remote schema without changing the snapshot
13280
13491
  --debounce <ms> Debounce source changes in dev mode (default: 100)
13281
- --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
13282
13493
  --space <n> openapi-export: JSON indentation (0-10, default: 2)
13283
13494
  --delivery <file> plan/build-delivery: validated JSON configuration (overrides config.delivery)
13284
13495
  --dry-run Preview a fix without writing the target file
13285
13496
  --write Apply a fix or migration to disk (preview-only by default)
13286
13497
  --from-version Migration source-format checkpoint (requires --to-version)
13287
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
13288
13502
  --preset, -p <name> Architecture preset ('modular-monolith' | 'angular-enterprise' | 'clean-architecture')
13289
13503
  --help, -h Show this help
13290
13504
  `);
@@ -13308,7 +13522,7 @@ async function run() {
13308
13522
  process.exitCode = 1;
13309
13523
  return;
13310
13524
  }
13311
- 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)) {
13312
13526
  console.error(`Error: unknown command "${command}"`);
13313
13527
  printUsage();
13314
13528
  process.exit(1);
@@ -13332,6 +13546,9 @@ async function run() {
13332
13546
  let tokenEnv;
13333
13547
  let checkSchema = false;
13334
13548
  let deliveryPath;
13549
+ let baselineOpenApi;
13550
+ let currentOpenApi;
13551
+ let renderMode = "unspecified";
13335
13552
  const openApiDiffPaths = [];
13336
13553
  const openApiExportPaths = [];
13337
13554
  let openApiExportSpace;
@@ -13436,9 +13653,27 @@ async function run() {
13436
13653
  } else if (arg === "--dry-run") {
13437
13654
  dryRun = true;
13438
13655
  } else if (arg === "--write") {
13439
- if (command === "plan")
13440
- 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`);
13441
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;
13442
13677
  } else if (arg === "--from-version" || arg === "--to-version") {
13443
13678
  if (command !== "migrate")
13444
13679
  throw new Error(`${arg} is only supported by migrate`);
@@ -13479,7 +13714,7 @@ async function run() {
13479
13714
  const currentPath = openApiDiffPaths[1];
13480
13715
  if (!basePath || !currentPath)
13481
13716
  throw new Error("openapi-diff requires two JSON file paths");
13482
- 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)));
13483
13718
  console.log(json ? JSON.stringify(result, null, 2) : formatOpenApiDiff(result));
13484
13719
  if (!result.ok)
13485
13720
  process.exitCode = 1;
@@ -13494,8 +13729,8 @@ async function run() {
13494
13729
  if (!modulePath || !outputPath)
13495
13730
  throw new Error("openapi-export requires an OpenAPI module and output path");
13496
13731
  const result = await exportGeneratedOpenApiJson({
13497
- modulePath: resolve17(process.cwd(), modulePath),
13498
- outputPath: resolve17(process.cwd(), outputPath),
13732
+ modulePath: resolve18(process.cwd(), modulePath),
13733
+ outputPath: resolve18(process.cwd(), outputPath),
13499
13734
  ...openApiExportSpace === undefined ? {} : { space: openApiExportSpace }
13500
13735
  });
13501
13736
  console.log(json ? JSON.stringify({ ok: true, ...result }, null, 2) : result.written ? `OpenAPI JSON written: ${result.path}` : `OpenAPI JSON matches: ${result.path}`);
@@ -13503,7 +13738,7 @@ async function run() {
13503
13738
  }
13504
13739
  if (command === "migrate") {
13505
13740
  const result = await migrateProject({
13506
- rootDir: rootDir ? resolve17(process.cwd(), rootDir) : process.cwd(),
13741
+ rootDir: rootDir ? resolve18(process.cwd(), rootDir) : process.cwd(),
13507
13742
  write: !dryRun,
13508
13743
  ...fromVersion === undefined ? {} : { fromVersion },
13509
13744
  ...toVersion === undefined ? {} : { toVersion }
@@ -13531,8 +13766,8 @@ async function run() {
13531
13766
  if (checkSchema && command !== "graphql-schema")
13532
13767
  throw new Error("--check is only supported by graphql-schema");
13533
13768
  const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
13534
- const resolvedRoot = rootDir ? resolve17(process.cwd(), rootDir) : defaults.rootDir;
13535
- 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;
13536
13771
  const configured = compileOptionsFromConfig({
13537
13772
  ...loadedConfig,
13538
13773
  root: resolvedRoot,
@@ -13551,7 +13786,7 @@ async function run() {
13551
13786
  let delivery = loadedConfig.delivery;
13552
13787
  if (deliveryPath !== undefined) {
13553
13788
  try {
13554
- delivery = JSON.parse(await readFile13(resolve17(process.cwd(), deliveryPath), "utf8"));
13789
+ delivery = JSON.parse(await readFile14(resolve18(process.cwd(), deliveryPath), "utf8"));
13555
13790
  } catch {
13556
13791
  throw new DeliveryConfigurationError;
13557
13792
  }
@@ -13595,10 +13830,21 @@ ${item.suggestion ?? ""}`).join(`
13595
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.`);
13596
13831
  if (!result.upToDate)
13597
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;
13598
13844
  } else if (command === "fix") {
13599
13845
  if (!query)
13600
13846
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
13601
- const fix = JSON.parse(await readFile13(resolve17(process.cwd(), query), "utf8"));
13847
+ const fix = JSON.parse(await readFile14(resolve18(process.cwd(), query), "utf8"));
13602
13848
  const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
13603
13849
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
13604
13850
  } else if (command === "compile") {
@@ -26,6 +26,8 @@ export interface ContractManifest {
26
26
  permission?: string;
27
27
  requestSchemas: Partial<Record<"body" | "params" | "query" | "headers" | "cookie", string>>;
28
28
  responseSchema?: string;
29
+ /** Status-aware response schemas; legacy responseSchema remains for compatibility. */
30
+ responseSchemas?: Record<string, string>;
29
31
  evidence?: string;
30
32
  }>;
31
33
  permissions: string[];
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { AppliedDiagnosticFix, ApplyDiagnosticFixOptions } from "./fixes";
11
11
  export { checkProject, compileProject } from "./compile";
12
12
  export { watchProject } from "./watch";
13
13
  export { migrateProject, SUPACLOUD_MIGRATIONS, migrateRouteResponse } from "./migrations";
14
+ export { assessMigration, formatMigrationAssessment } from "./migration-assess";
15
+ export type { MigrationAssessmentFinding, MigrationAssessmentOptions, MigrationAssessmentResult, MigrationAssessmentStatus, MigrationRenderMode, } from "./migration-assess";
14
16
  export { createContextPack, createExecutionPlans, doctorProject, explainGraph, formatGraph, exportGraphDot, exportGraphMermaid, } from "./inspect";
15
17
  export { createIncrementalCompiler } from "./incremental";
16
18
  export { createDependencyGraphCache } from "./incremental";