@supacloud/compiler 0.40.1 → 0.41.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
@@ -8,6 +8,13 @@ response unions, caller decoders, binary/stream routes and GraphQL stay intact.
8
8
  These errors do not prove write rollback and do not add automatic retries.
9
9
  See [typed clients and recovery](../../docs/framework-composition.md).
10
10
 
11
+ For Elysia routes, the compiler governs Effect contracts by default:
12
+ `effect.dependencies` documents the runtime environment, `effect.errors`
13
+ documents safe HTTP mappings, and `effect.retry` requires explicit bounded
14
+ attempts. The default policy requires `requireRouteEffects`,
15
+ `requireErrorMappings` and `requireDependencies`. See
16
+ [Effect governance](../../docs/effect-governance.md).
17
+
11
18
  ## Unified Database Contracts
12
19
 
13
20
  `supacloud-compiler database-contracts database-contracts.json` generates a shared
@@ -320,6 +327,7 @@ bunx supacloud-compiler dev
320
327
  | OpenAPI 3.1 module | 开启 |
321
328
  | permissions manifest | 开启 |
322
329
  | module boundary preset | `modular-monolith` |
330
+ | Effect route governance | 三项严格检查均开启 |
323
331
  | provider tree-shaking | 开启 |
324
332
 
325
333
  需要覆盖默认值时,在项目根目录添加 `supacloud.config.ts`:
package/dist/cli.js CHANGED
@@ -1237,6 +1237,17 @@ var init_validate = __esm(() => {
1237
1237
  "route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
1238
1238
  "route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
1239
1239
  "route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
1240
+ "invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
1241
+ "invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
1242
+ "invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
1243
+ "invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
1244
+ "invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
1245
+ "effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
1246
+ "effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
1247
+ "effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
1248
+ "effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
1249
+ "effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
1250
+ "effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
1240
1251
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1241
1252
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1242
1253
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -1775,6 +1786,9 @@ class ModuleGenerator {
1775
1786
  });
1776
1787
  fields.push(`responses: { ${responseFields.join(", ")} }`);
1777
1788
  }
1789
+ if (route.effect !== undefined) {
1790
+ fields.push(`effect: ${JSON.stringify(route.effect)}`);
1791
+ }
1778
1792
  if (route.command)
1779
1793
  fields.push(`command: ${JSON.stringify(route.command)}`);
1780
1794
  if (route.guards && route.guards.length > 0) {
@@ -3586,6 +3600,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3586
3600
  cookie?: unknown;
3587
3601
  response?: unknown;
3588
3602
  responses?: Record<string | number, unknown>;
3603
+ effect?: {
3604
+ required: true;
3605
+ errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
3606
+ dependencies?: string[];
3607
+ retry?: "none" | "explicit";
3608
+ maxAttempts?: number;
3609
+ timeoutMs?: number;
3610
+ };
3589
3611
  /** Compile-time ownership and transport classification for the route boundary. */
3590
3612
  contract?: {
3591
3613
  body?: "framework" | "domain";
@@ -29330,6 +29352,188 @@ function parseController(input, ctx) {
29330
29352
  Object.assign(route.contract, { [field]: value });
29331
29353
  }
29332
29354
  }
29355
+ const effectExpr = getProp(optionsObject, "effect");
29356
+ if (effectExpr) {
29357
+ const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
29358
+ if (!effectObject) {
29359
+ ctx.diagnostics.push({
29360
+ severity: "error",
29361
+ code: "invalid-effect-contract",
29362
+ errorCode: "SC3030",
29363
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29364
+ file,
29365
+ line: lineOf(effectExpr),
29366
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
29367
+ suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
29368
+ });
29369
+ } else {
29370
+ const effect = { required: true };
29371
+ const requiredExpr = getProp(effectObject, "required");
29372
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
29373
+ ctx.diagnostics.push({
29374
+ severity: "error",
29375
+ code: "invalid-effect-contract",
29376
+ errorCode: "SC3030",
29377
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29378
+ file,
29379
+ line: lineOf(requiredExpr),
29380
+ message: `Route ${route.handler} effect.required must be the literal true.`
29381
+ });
29382
+ }
29383
+ const retry = stringLiteralProp(effectObject, "retry");
29384
+ if (retry !== undefined) {
29385
+ if (retry !== "none" && retry !== "explicit") {
29386
+ ctx.diagnostics.push({
29387
+ severity: "error",
29388
+ code: "invalid-effect-retry",
29389
+ errorCode: "SC3031",
29390
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29391
+ file,
29392
+ line: lineOf(getProp(effectObject, "retry")),
29393
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
29394
+ });
29395
+ } else {
29396
+ effect.retry = retry;
29397
+ }
29398
+ }
29399
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
29400
+ if (maxAttemptsExpr !== undefined) {
29401
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
29402
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
29403
+ ctx.diagnostics.push({
29404
+ severity: "error",
29405
+ code: "invalid-effect-retry",
29406
+ errorCode: "SC3031",
29407
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29408
+ file,
29409
+ line: lineOf(maxAttemptsExpr),
29410
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
29411
+ });
29412
+ } else {
29413
+ effect.maxAttempts = maxAttempts;
29414
+ }
29415
+ }
29416
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
29417
+ ctx.diagnostics.push({
29418
+ severity: "error",
29419
+ code: "invalid-effect-retry",
29420
+ errorCode: "SC3031",
29421
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29422
+ file,
29423
+ line: lineOf(effectExpr),
29424
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
29425
+ });
29426
+ }
29427
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
29428
+ if (timeoutExpr !== undefined) {
29429
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
29430
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
29431
+ ctx.diagnostics.push({
29432
+ severity: "error",
29433
+ code: "invalid-effect-timeout",
29434
+ errorCode: "SC3032",
29435
+ docsUrl: "https://supacloud.dev/errors/SC3032",
29436
+ file,
29437
+ line: lineOf(timeoutExpr),
29438
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
29439
+ });
29440
+ } else {
29441
+ effect.timeoutMs = timeoutMs;
29442
+ }
29443
+ }
29444
+ const dependenciesExpr = getProp(effectObject, "dependencies");
29445
+ if (dependenciesExpr !== undefined) {
29446
+ if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
29447
+ ctx.diagnostics.push({
29448
+ severity: "error",
29449
+ code: "invalid-effect-dependencies",
29450
+ errorCode: "SC3033",
29451
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29452
+ file,
29453
+ line: lineOf(dependenciesExpr),
29454
+ message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
29455
+ });
29456
+ } else {
29457
+ effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
29458
+ }
29459
+ }
29460
+ const errorsExpr = getProp(effectObject, "errors");
29461
+ if (errorsExpr !== undefined) {
29462
+ if (!ts7.isArrayLiteralExpression(errorsExpr)) {
29463
+ ctx.diagnostics.push({
29464
+ severity: "error",
29465
+ code: "invalid-effect-errors",
29466
+ errorCode: "SC3034",
29467
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29468
+ file,
29469
+ line: lineOf(errorsExpr),
29470
+ message: `Route ${route.handler} effect.errors must be an explicit object array.`
29471
+ });
29472
+ } else {
29473
+ const errors = [];
29474
+ for (const errorExpr of errorsExpr.elements) {
29475
+ if (!ts7.isObjectLiteralExpression(errorExpr)) {
29476
+ ctx.diagnostics.push({
29477
+ severity: "error",
29478
+ code: "invalid-effect-errors",
29479
+ errorCode: "SC3034",
29480
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29481
+ file,
29482
+ line: lineOf(errorExpr),
29483
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
29484
+ });
29485
+ continue;
29486
+ }
29487
+ const tag = stringLiteralProp(errorExpr, "tag");
29488
+ const code = stringLiteralProp(errorExpr, "code");
29489
+ const status = numberLiteralProp(errorExpr, "status");
29490
+ const message = stringLiteralProp(errorExpr, "message");
29491
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
29492
+ ctx.diagnostics.push({
29493
+ severity: "error",
29494
+ code: "invalid-effect-errors",
29495
+ errorCode: "SC3034",
29496
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29497
+ file,
29498
+ line: lineOf(errorExpr),
29499
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
29500
+ });
29501
+ continue;
29502
+ }
29503
+ errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29504
+ }
29505
+ effect.errors = errors;
29506
+ }
29507
+ }
29508
+ route.effect = effect;
29509
+ if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
29510
+ ctx.diagnostics.push({
29511
+ severity: "error",
29512
+ code: "effect-return-type",
29513
+ errorCode: "SC3035",
29514
+ docsUrl: "https://supacloud.dev/errors/SC3035",
29515
+ file,
29516
+ line: lineOf(method),
29517
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
29518
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29519
+ });
29520
+ }
29521
+ for (const dependency of effect.dependencies ?? []) {
29522
+ if (!resultType?.includes(dependency)) {
29523
+ ctx.diagnostics.push({
29524
+ severity: "error",
29525
+ code: "effect-dependency-type",
29526
+ errorCode: "SC3040",
29527
+ docsUrl: "https://supacloud.dev/errors/SC3040",
29528
+ file,
29529
+ line: lineOf(method),
29530
+ message: `Route ${route.handler} declares Effect dependency "${dependency}", but it is absent from the return type environment ${resultType ?? "unknown"}.`,
29531
+ suggestion: "Keep effect.dependencies aligned with the third Effect type parameter."
29532
+ });
29533
+ }
29534
+ }
29535
+ }
29536
+ }
29333
29537
  for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
29334
29538
  const schemaExpr = getProp(optionsObject, field);
29335
29539
  if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
@@ -29913,6 +30117,13 @@ function stringLiteralProp(obj, name) {
29913
30117
  const expr = getProp(obj, name);
29914
30118
  return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
29915
30119
  }
30120
+ function numberLiteralProp(obj, name) {
30121
+ const expr = getProp(obj, name);
30122
+ if (!expr || !ts7.isNumericLiteral(expr))
30123
+ return;
30124
+ const value = Number(expr.text);
30125
+ return Number.isFinite(value) ? value : undefined;
30126
+ }
29916
30127
  function arrayProp(obj, name) {
29917
30128
  const expr = getProp(obj, name);
29918
30129
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -30574,6 +30785,57 @@ function validateClientSchemaBoundaries(graph, options) {
30574
30785
  return diagnostics;
30575
30786
  }
30576
30787
 
30788
+ // src/effect-governance.ts
30789
+ init_validate();
30790
+ function diagnostic(code, message, file, suggestion) {
30791
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
30792
+ return {
30793
+ severity: "error",
30794
+ code,
30795
+ message,
30796
+ ...file === undefined ? {} : { file },
30797
+ ...suggestion === undefined ? {} : { suggestion },
30798
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
30799
+ };
30800
+ }
30801
+ function routeLabel(route, controllerPath) {
30802
+ return `${route.method} ${controllerPath}${route.path}`;
30803
+ }
30804
+ function validateEffectPolicies(graph, options = {}) {
30805
+ const diagnostics = [];
30806
+ for (const module of graph.modules) {
30807
+ const commands = new Map(module.commands.map((command) => [command.className, command]));
30808
+ for (const controller of module.controllers) {
30809
+ for (const route of controller.routes) {
30810
+ const label = routeLabel(route, controller.path);
30811
+ if (options.requireRouteEffects && route.effect === undefined) {
30812
+ diagnostics.push(diagnostic("effect-contract-required", `Route ${label} must declare an Effect contract.`, controller.file, 'Add effect: { dependencies: [...], errors: [...], retry: "none" } to the route options.'));
30813
+ continue;
30814
+ }
30815
+ const effect = route.effect;
30816
+ if (effect === undefined)
30817
+ continue;
30818
+ if (options.requireErrorMappings && effect.errors === undefined) {
30819
+ diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
30820
+ }
30821
+ if (options.requireDependencies && effect.dependencies === undefined) {
30822
+ diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
30823
+ }
30824
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
30825
+ diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
30826
+ }
30827
+ if (route.command && effect.retry === "explicit") {
30828
+ const command = commands.get(route.command);
30829
+ if (command && command.idempotency !== "required") {
30830
+ diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
30831
+ }
30832
+ }
30833
+ }
30834
+ }
30835
+ }
30836
+ return diagnostics;
30837
+ }
30838
+
30577
30839
  // src/compile.ts
30578
30840
  function withDefaultGovernance(options) {
30579
30841
  return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
@@ -30593,7 +30855,8 @@ async function compileProject(options) {
30593
30855
  const diagnostics = [
30594
30856
  ...graph.diagnostics ?? [],
30595
30857
  ...validateGraph(graph, options),
30596
- ...validateClientSchemaBoundaries(graph, options)
30858
+ ...validateClientSchemaBoundaries(graph, options),
30859
+ ...validateEffectPolicies(graph, options.effect)
30597
30860
  ];
30598
30861
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30599
30862
  return { diagnostics, graph, written: [] };
@@ -30657,7 +30920,8 @@ async function checkProject(options) {
30657
30920
  const diagnostics = [
30658
30921
  ...graph.diagnostics ?? [],
30659
30922
  ...validateGraph(graph, options),
30660
- ...validateClientSchemaBoundaries(graph, options)
30923
+ ...validateClientSchemaBoundaries(graph, options),
30924
+ ...validateEffectPolicies(graph, options.effect)
30661
30925
  ];
30662
30926
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30663
30927
  return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
@@ -31467,7 +31731,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
31467
31731
  idempotency: true,
31468
31732
  transaction: true
31469
31733
  },
31470
- moduleBoundaryPreset: "modular-monolith"
31734
+ moduleBoundaryPreset: "modular-monolith",
31735
+ effect: {
31736
+ requireRouteEffects: true,
31737
+ requireErrorMappings: true,
31738
+ requireDependencies: true
31739
+ }
31471
31740
  };
31472
31741
  function defineSupacloudConfig(config = {}) {
31473
31742
  if (config.delivery !== undefined)
@@ -31479,7 +31748,8 @@ function defineSupacloudConfig(config = {}) {
31479
31748
  ...DEFAULT_SUPACLOUD_CONFIG,
31480
31749
  ...config,
31481
31750
  include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
31482
- graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
31751
+ graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
31752
+ effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
31483
31753
  };
31484
31754
  }
31485
31755
  function validateGovernanceConfig(config) {
@@ -31536,6 +31806,16 @@ function validateGovernanceConfig(config) {
31536
31806
  if (config[key] !== undefined && typeof config[key] !== "boolean")
31537
31807
  throw new Error(`${key} must be a boolean.`);
31538
31808
  }
31809
+ if (config.effect !== undefined) {
31810
+ if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => !["requireRouteEffects", "requireErrorMappings", "requireDependencies"].includes(key))) {
31811
+ throw new Error("effect accepts requireRouteEffects, requireErrorMappings and requireDependencies.");
31812
+ }
31813
+ for (const key of ["requireRouteEffects", "requireErrorMappings", "requireDependencies"]) {
31814
+ if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
31815
+ throw new Error(`effect.${key} must be boolean.`);
31816
+ }
31817
+ }
31818
+ }
31539
31819
  }
31540
31820
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
31541
31821
  const resolved = defineSupacloudConfig(config);
@@ -31556,6 +31836,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
31556
31836
  ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
31557
31837
  ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
31558
31838
  ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
31839
+ effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
31559
31840
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
31560
31841
  graphql: resolved.graphql ? {
31561
31842
  ...resolved.graphql,
@@ -31644,7 +31925,7 @@ import { createHash as createHash10 } from "node:crypto";
31644
31925
  init_validate();
31645
31926
  var compare3 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31646
31927
  var sorted = (values) => [...new Set(values)].sort(compare3);
31647
- function diagnostic(error) {
31928
+ function diagnostic2(error) {
31648
31929
  return {
31649
31930
  severity: error.severity,
31650
31931
  code: error.code,
@@ -31669,8 +31950,8 @@ function createDeliveryPlan(graph, input) {
31669
31950
  }] };
31670
31951
  }
31671
31952
  const diagnostics = [
31672
- ...(graph.diagnostics ?? []).map(diagnostic),
31673
- ...validateGraph(graph, { strict: true }).map(diagnostic)
31953
+ ...(graph.diagnostics ?? []).map(diagnostic2),
31954
+ ...validateGraph(graph, { strict: true }).map(diagnostic2)
31674
31955
  ];
31675
31956
  const fail = (code, message, suggestion) => {
31676
31957
  diagnostics.push({ severity: "error", code, message, suggestion });
@@ -32840,10 +33121,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
32840
33121
  import { resolve as resolve18 } from "node:path";
32841
33122
 
32842
33123
  // src/migration-app-version.ts
32843
- var MIGRATION_APP_VERSION = "0.23.0";
33124
+ var MIGRATION_APP_VERSION = "0.24.0";
32844
33125
 
32845
33126
  // src/migration-elysia-version.ts
32846
- var MIGRATION_ELYSIA_VERSION = "0.27.1";
33127
+ var MIGRATION_ELYSIA_VERSION = "0.28.0";
32847
33128
 
32848
33129
  // src/migration-policy.ts
32849
33130
  var cachedCompilerVersion;
package/dist/config.d.ts CHANGED
@@ -20,12 +20,14 @@ export interface SupaCloudConfig {
20
20
  allowRouteCommandBindings?: boolean;
21
21
  disallowControllerDirectDb?: boolean;
22
22
  detectOrphanModules?: boolean;
23
+ effect?: NonNullable<CompileOptions["effect"]>;
23
24
  commandCapabilities?: CommandExecutionCapabilities;
24
25
  treeShakeUnusedProviders?: boolean;
25
26
  }
26
- export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "moduleBoundaries" | "typeSafety" | "delivery" | "allowRouteCommandBindings" | "disallowControllerDirectDb" | "detectOrphanModules" | "openApi">> & {
27
+ export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "moduleBoundaries" | "typeSafety" | "delivery" | "allowRouteCommandBindings" | "disallowControllerDirectDb" | "detectOrphanModules" | "openApi" | "effect">> & {
27
28
  include: string[];
28
29
  moduleBoundaryPreset: ModuleBoundaryPresetName;
30
+ effect: NonNullable<CompileOptions["effect"]>;
29
31
  };
30
32
  export declare function defineSupacloudConfig(config?: SupaCloudConfig): SupaCloudConfig;
31
33
  export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: string): {
@@ -45,6 +47,7 @@ export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: s
45
47
  allowRouteCommandBindings?: boolean;
46
48
  disallowControllerDirectDb?: boolean;
47
49
  detectOrphanModules?: boolean;
50
+ effect?: NonNullable<CompileOptions["effect"]>;
48
51
  treeShakeUnusedProviders: boolean;
49
52
  graphql?: GraphqlOptions;
50
53
  };
@@ -0,0 +1,3 @@
1
+ import type { ApplicationGraph, Diagnostic, EffectCompilerOptions } from "./types";
2
+ /** Validates the static Effect contract without inspecting Effect runtime internals. */
3
+ export declare function validateEffectPolicies(graph: ApplicationGraph, options?: EffectCompilerOptions): Diagnostic[];
package/dist/index.d.ts CHANGED
@@ -52,8 +52,9 @@ export { DEFAULT_SUPACLOUD_CONFIG, compileOptionsFromConfig, defineSupacloudConf
52
52
  export type { SupaCloudConfig } from "./config";
53
53
  export { camelName } from "./util";
54
54
  export { ANGULAR_ENTERPRISE_RULES, CLEAN_ARCHITECTURE_RULES, MODULAR_MONOLITH_RULES, MODULE_BOUNDARY_PROFILES, getModuleBoundaryPreset, getModuleBoundaryProfile, resolveModuleBoundaries, } from "./profiles";
55
- export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
55
+ export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, EffectCompilerOptions, EffectErrorMappingNode, EffectRetryPolicy, EffectRouteNode, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
56
56
  export { inspectRouteContracts, validateRouteContracts } from "./route-contracts";
57
+ export { validateEffectPolicies } from "./effect-governance";
57
58
  export { pullGraphqlSchema } from "./graphql-schema";
58
59
  export type { PullGraphqlSchemaOptions } from "./graphql-schema";
59
60
  export { checkGraphqlCompatibility, createRoleSnapshotReport, diffGraphqlSchemas, graphqlFeatureMatrix, introspectionQuery, readGraphqlSchema, schemaFromSource, } from "./graphql-governance";
package/dist/index.js CHANGED
@@ -1236,6 +1236,17 @@ var init_validate = __esm(() => {
1236
1236
  "route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
1237
1237
  "route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
1238
1238
  "route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
1239
+ "invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
1240
+ "invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
1241
+ "invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
1242
+ "invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
1243
+ "invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
1244
+ "effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
1245
+ "effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
1246
+ "effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
1247
+ "effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
1248
+ "effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
1249
+ "effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
1239
1250
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1240
1251
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1241
1252
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -1774,6 +1785,9 @@ class ModuleGenerator {
1774
1785
  });
1775
1786
  fields.push(`responses: { ${responseFields.join(", ")} }`);
1776
1787
  }
1788
+ if (route.effect !== undefined) {
1789
+ fields.push(`effect: ${JSON.stringify(route.effect)}`);
1790
+ }
1777
1791
  if (route.command)
1778
1792
  fields.push(`command: ${JSON.stringify(route.command)}`);
1779
1793
  if (route.guards && route.guards.length > 0) {
@@ -3585,6 +3599,14 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
3585
3599
  cookie?: unknown;
3586
3600
  response?: unknown;
3587
3601
  responses?: Record<string | number, unknown>;
3602
+ effect?: {
3603
+ required: true;
3604
+ errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
3605
+ dependencies?: string[];
3606
+ retry?: "none" | "explicit";
3607
+ maxAttempts?: number;
3608
+ timeoutMs?: number;
3609
+ };
3588
3610
  /** Compile-time ownership and transport classification for the route boundary. */
3589
3611
  contract?: {
3590
3612
  body?: "framework" | "domain";
@@ -29289,6 +29311,188 @@ function parseController(input, ctx) {
29289
29311
  Object.assign(route.contract, { [field]: value });
29290
29312
  }
29291
29313
  }
29314
+ const effectExpr = getProp(optionsObject, "effect");
29315
+ if (effectExpr) {
29316
+ const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
29317
+ if (!effectObject) {
29318
+ ctx.diagnostics.push({
29319
+ severity: "error",
29320
+ code: "invalid-effect-contract",
29321
+ errorCode: "SC3030",
29322
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29323
+ file,
29324
+ line: lineOf(effectExpr),
29325
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
29326
+ suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
29327
+ });
29328
+ } else {
29329
+ const effect = { required: true };
29330
+ const requiredExpr = getProp(effectObject, "required");
29331
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
29332
+ ctx.diagnostics.push({
29333
+ severity: "error",
29334
+ code: "invalid-effect-contract",
29335
+ errorCode: "SC3030",
29336
+ docsUrl: "https://supacloud.dev/errors/SC3030",
29337
+ file,
29338
+ line: lineOf(requiredExpr),
29339
+ message: `Route ${route.handler} effect.required must be the literal true.`
29340
+ });
29341
+ }
29342
+ const retry = stringLiteralProp(effectObject, "retry");
29343
+ if (retry !== undefined) {
29344
+ if (retry !== "none" && retry !== "explicit") {
29345
+ ctx.diagnostics.push({
29346
+ severity: "error",
29347
+ code: "invalid-effect-retry",
29348
+ errorCode: "SC3031",
29349
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29350
+ file,
29351
+ line: lineOf(getProp(effectObject, "retry")),
29352
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
29353
+ });
29354
+ } else {
29355
+ effect.retry = retry;
29356
+ }
29357
+ }
29358
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
29359
+ if (maxAttemptsExpr !== undefined) {
29360
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
29361
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
29362
+ ctx.diagnostics.push({
29363
+ severity: "error",
29364
+ code: "invalid-effect-retry",
29365
+ errorCode: "SC3031",
29366
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29367
+ file,
29368
+ line: lineOf(maxAttemptsExpr),
29369
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
29370
+ });
29371
+ } else {
29372
+ effect.maxAttempts = maxAttempts;
29373
+ }
29374
+ }
29375
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
29376
+ ctx.diagnostics.push({
29377
+ severity: "error",
29378
+ code: "invalid-effect-retry",
29379
+ errorCode: "SC3031",
29380
+ docsUrl: "https://supacloud.dev/errors/SC3031",
29381
+ file,
29382
+ line: lineOf(effectExpr),
29383
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
29384
+ });
29385
+ }
29386
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
29387
+ if (timeoutExpr !== undefined) {
29388
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
29389
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
29390
+ ctx.diagnostics.push({
29391
+ severity: "error",
29392
+ code: "invalid-effect-timeout",
29393
+ errorCode: "SC3032",
29394
+ docsUrl: "https://supacloud.dev/errors/SC3032",
29395
+ file,
29396
+ line: lineOf(timeoutExpr),
29397
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
29398
+ });
29399
+ } else {
29400
+ effect.timeoutMs = timeoutMs;
29401
+ }
29402
+ }
29403
+ const dependenciesExpr = getProp(effectObject, "dependencies");
29404
+ if (dependenciesExpr !== undefined) {
29405
+ if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
29406
+ ctx.diagnostics.push({
29407
+ severity: "error",
29408
+ code: "invalid-effect-dependencies",
29409
+ errorCode: "SC3033",
29410
+ docsUrl: "https://supacloud.dev/errors/SC3033",
29411
+ file,
29412
+ line: lineOf(dependenciesExpr),
29413
+ message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
29414
+ });
29415
+ } else {
29416
+ effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
29417
+ }
29418
+ }
29419
+ const errorsExpr = getProp(effectObject, "errors");
29420
+ if (errorsExpr !== undefined) {
29421
+ if (!ts7.isArrayLiteralExpression(errorsExpr)) {
29422
+ ctx.diagnostics.push({
29423
+ severity: "error",
29424
+ code: "invalid-effect-errors",
29425
+ errorCode: "SC3034",
29426
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29427
+ file,
29428
+ line: lineOf(errorsExpr),
29429
+ message: `Route ${route.handler} effect.errors must be an explicit object array.`
29430
+ });
29431
+ } else {
29432
+ const errors = [];
29433
+ for (const errorExpr of errorsExpr.elements) {
29434
+ if (!ts7.isObjectLiteralExpression(errorExpr)) {
29435
+ ctx.diagnostics.push({
29436
+ severity: "error",
29437
+ code: "invalid-effect-errors",
29438
+ errorCode: "SC3034",
29439
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29440
+ file,
29441
+ line: lineOf(errorExpr),
29442
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
29443
+ });
29444
+ continue;
29445
+ }
29446
+ const tag = stringLiteralProp(errorExpr, "tag");
29447
+ const code = stringLiteralProp(errorExpr, "code");
29448
+ const status = numberLiteralProp(errorExpr, "status");
29449
+ const message = stringLiteralProp(errorExpr, "message");
29450
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
29451
+ ctx.diagnostics.push({
29452
+ severity: "error",
29453
+ code: "invalid-effect-errors",
29454
+ errorCode: "SC3034",
29455
+ docsUrl: "https://supacloud.dev/errors/SC3034",
29456
+ file,
29457
+ line: lineOf(errorExpr),
29458
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
29459
+ });
29460
+ continue;
29461
+ }
29462
+ errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
29463
+ }
29464
+ effect.errors = errors;
29465
+ }
29466
+ }
29467
+ route.effect = effect;
29468
+ if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
29469
+ ctx.diagnostics.push({
29470
+ severity: "error",
29471
+ code: "effect-return-type",
29472
+ errorCode: "SC3035",
29473
+ docsUrl: "https://supacloud.dev/errors/SC3035",
29474
+ file,
29475
+ line: lineOf(method),
29476
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
29477
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
29478
+ });
29479
+ }
29480
+ for (const dependency of effect.dependencies ?? []) {
29481
+ if (!resultType?.includes(dependency)) {
29482
+ ctx.diagnostics.push({
29483
+ severity: "error",
29484
+ code: "effect-dependency-type",
29485
+ errorCode: "SC3040",
29486
+ docsUrl: "https://supacloud.dev/errors/SC3040",
29487
+ file,
29488
+ line: lineOf(method),
29489
+ message: `Route ${route.handler} declares Effect dependency "${dependency}", but it is absent from the return type environment ${resultType ?? "unknown"}.`,
29490
+ suggestion: "Keep effect.dependencies aligned with the third Effect type parameter."
29491
+ });
29492
+ }
29493
+ }
29494
+ }
29495
+ }
29292
29496
  for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
29293
29497
  const schemaExpr = getProp(optionsObject, field);
29294
29498
  if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
@@ -29872,6 +30076,13 @@ function stringLiteralProp(obj, name) {
29872
30076
  const expr = getProp(obj, name);
29873
30077
  return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
29874
30078
  }
30079
+ function numberLiteralProp(obj, name) {
30080
+ const expr = getProp(obj, name);
30081
+ if (!expr || !ts7.isNumericLiteral(expr))
30082
+ return;
30083
+ const value = Number(expr.text);
30084
+ return Number.isFinite(value) ? value : undefined;
30085
+ }
29875
30086
  function arrayProp(obj, name) {
29876
30087
  const expr = getProp(obj, name);
29877
30088
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -30535,6 +30746,57 @@ function validateClientSchemaBoundaries(graph, options) {
30535
30746
  return diagnostics;
30536
30747
  }
30537
30748
 
30749
+ // src/effect-governance.ts
30750
+ init_validate();
30751
+ function diagnostic(code, message, file, suggestion) {
30752
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
30753
+ return {
30754
+ severity: "error",
30755
+ code,
30756
+ message,
30757
+ ...file === undefined ? {} : { file },
30758
+ ...suggestion === undefined ? {} : { suggestion },
30759
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
30760
+ };
30761
+ }
30762
+ function routeLabel(route, controllerPath) {
30763
+ return `${route.method} ${controllerPath}${route.path}`;
30764
+ }
30765
+ function validateEffectPolicies(graph, options = {}) {
30766
+ const diagnostics = [];
30767
+ for (const module of graph.modules) {
30768
+ const commands = new Map(module.commands.map((command) => [command.className, command]));
30769
+ for (const controller of module.controllers) {
30770
+ for (const route of controller.routes) {
30771
+ const label = routeLabel(route, controller.path);
30772
+ if (options.requireRouteEffects && route.effect === undefined) {
30773
+ diagnostics.push(diagnostic("effect-contract-required", `Route ${label} must declare an Effect contract.`, controller.file, 'Add effect: { dependencies: [...], errors: [...], retry: "none" } to the route options.'));
30774
+ continue;
30775
+ }
30776
+ const effect = route.effect;
30777
+ if (effect === undefined)
30778
+ continue;
30779
+ if (options.requireErrorMappings && effect.errors === undefined) {
30780
+ diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
30781
+ }
30782
+ if (options.requireDependencies && effect.dependencies === undefined) {
30783
+ diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
30784
+ }
30785
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
30786
+ diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
30787
+ }
30788
+ if (route.command && effect.retry === "explicit") {
30789
+ const command = commands.get(route.command);
30790
+ if (command && command.idempotency !== "required") {
30791
+ diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
30792
+ }
30793
+ }
30794
+ }
30795
+ }
30796
+ }
30797
+ return diagnostics;
30798
+ }
30799
+
30538
30800
  // src/compile.ts
30539
30801
  function withDefaultGovernance(options) {
30540
30802
  return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
@@ -30554,7 +30816,8 @@ async function compileProject(options) {
30554
30816
  const diagnostics = [
30555
30817
  ...graph.diagnostics ?? [],
30556
30818
  ...validateGraph(graph, options),
30557
- ...validateClientSchemaBoundaries(graph, options)
30819
+ ...validateClientSchemaBoundaries(graph, options),
30820
+ ...validateEffectPolicies(graph, options.effect)
30558
30821
  ];
30559
30822
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30560
30823
  return { diagnostics, graph, written: [] };
@@ -30618,7 +30881,8 @@ async function checkProject(options) {
30618
30881
  const diagnostics = [
30619
30882
  ...graph.diagnostics ?? [],
30620
30883
  ...validateGraph(graph, options),
30621
- ...validateClientSchemaBoundaries(graph, options)
30884
+ ...validateClientSchemaBoundaries(graph, options),
30885
+ ...validateEffectPolicies(graph, options.effect)
30622
30886
  ];
30623
30887
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
30624
30888
  return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
@@ -30704,7 +30968,7 @@ init_delivery_schema();
30704
30968
  init_validate();
30705
30969
  var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
30706
30970
  var sorted = (values) => [...new Set(values)].sort(compare2);
30707
- function diagnostic(error) {
30971
+ function diagnostic2(error) {
30708
30972
  return {
30709
30973
  severity: error.severity,
30710
30974
  code: error.code,
@@ -30729,8 +30993,8 @@ function createDeliveryPlan(graph, input) {
30729
30993
  }] };
30730
30994
  }
30731
30995
  const diagnostics = [
30732
- ...(graph.diagnostics ?? []).map(diagnostic),
30733
- ...validateGraph(graph, { strict: true }).map(diagnostic)
30996
+ ...(graph.diagnostics ?? []).map(diagnostic2),
30997
+ ...validateGraph(graph, { strict: true }).map(diagnostic2)
30734
30998
  ];
30735
30999
  const fail = (code, message, suggestion) => {
30736
31000
  diagnostics.push({ severity: "error", code, message, suggestion });
@@ -32573,10 +32837,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
32573
32837
  import { resolve as resolve17 } from "node:path";
32574
32838
 
32575
32839
  // src/migration-app-version.ts
32576
- var MIGRATION_APP_VERSION = "0.23.0";
32840
+ var MIGRATION_APP_VERSION = "0.24.0";
32577
32841
 
32578
32842
  // src/migration-elysia-version.ts
32579
- var MIGRATION_ELYSIA_VERSION = "0.27.1";
32843
+ var MIGRATION_ELYSIA_VERSION = "0.28.0";
32580
32844
 
32581
32845
  // src/migration-policy.ts
32582
32846
  var cachedCompilerVersion;
@@ -33746,7 +34010,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
33746
34010
  idempotency: true,
33747
34011
  transaction: true
33748
34012
  },
33749
- moduleBoundaryPreset: "modular-monolith"
34013
+ moduleBoundaryPreset: "modular-monolith",
34014
+ effect: {
34015
+ requireRouteEffects: true,
34016
+ requireErrorMappings: true,
34017
+ requireDependencies: true
34018
+ }
33750
34019
  };
33751
34020
  function defineSupacloudConfig(config = {}) {
33752
34021
  if (config.delivery !== undefined)
@@ -33758,7 +34027,8 @@ function defineSupacloudConfig(config = {}) {
33758
34027
  ...DEFAULT_SUPACLOUD_CONFIG,
33759
34028
  ...config,
33760
34029
  include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
33761
- graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
34030
+ graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
34031
+ effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
33762
34032
  };
33763
34033
  }
33764
34034
  function validateGovernanceConfig(config) {
@@ -33815,6 +34085,16 @@ function validateGovernanceConfig(config) {
33815
34085
  if (config[key] !== undefined && typeof config[key] !== "boolean")
33816
34086
  throw new Error(`${key} must be a boolean.`);
33817
34087
  }
34088
+ if (config.effect !== undefined) {
34089
+ if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => !["requireRouteEffects", "requireErrorMappings", "requireDependencies"].includes(key))) {
34090
+ throw new Error("effect accepts requireRouteEffects, requireErrorMappings and requireDependencies.");
34091
+ }
34092
+ for (const key of ["requireRouteEffects", "requireErrorMappings", "requireDependencies"]) {
34093
+ if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
34094
+ throw new Error(`effect.${key} must be boolean.`);
34095
+ }
34096
+ }
34097
+ }
33818
34098
  }
33819
34099
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
33820
34100
  const resolved = defineSupacloudConfig(config);
@@ -33835,6 +34115,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
33835
34115
  ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
33836
34116
  ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
33837
34117
  ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
34118
+ effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
33838
34119
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
33839
34120
  graphql: resolved.graphql ? {
33840
34121
  ...resolved.graphql,
@@ -34237,6 +34518,7 @@ export {
34237
34518
  scanProductionSource,
34238
34519
  schemaFromSource,
34239
34520
  serializeOpenApiJson,
34521
+ validateEffectPolicies,
34240
34522
  validateFeatureSpec,
34241
34523
  validateGraph,
34242
34524
  validateRouteContracts,
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.23.0";
1
+ export declare const MIGRATION_APP_VERSION = "0.24.0";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.27.1";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.28.0";
package/dist/types.d.ts CHANGED
@@ -185,6 +185,21 @@ export interface HandlerParamNode {
185
185
  transform?: "number" | "boolean" | "string";
186
186
  default?: unknown;
187
187
  }
188
+ export type EffectRetryPolicy = "none" | "explicit";
189
+ export interface EffectErrorMappingNode {
190
+ tag: string;
191
+ status: number;
192
+ code: string;
193
+ message?: string;
194
+ }
195
+ export interface EffectRouteNode {
196
+ required: true;
197
+ errors?: EffectErrorMappingNode[];
198
+ dependencies?: string[];
199
+ retry?: EffectRetryPolicy;
200
+ maxAttempts?: number;
201
+ timeoutMs?: number;
202
+ }
188
203
  export interface RouteNode {
189
204
  parse?: "none";
190
205
  allowDeleteBody?: true;
@@ -205,6 +220,8 @@ export interface RouteNode {
205
220
  cookie?: string;
206
221
  response?: string;
207
222
  responses?: Record<string, string>;
223
+ /** Static Effect execution contract for this route. */
224
+ effect?: EffectRouteNode;
208
225
  /** @Command-decorated class explicitly bound by the route. */
209
226
  command?: string;
210
227
  /** Route guards executed before handler (Angular CanActivateFn style). */
@@ -429,6 +446,16 @@ export interface CompileOptions {
429
446
  changedPaths?: string[];
430
447
  /** Type-safety gates for generated artifacts and production source. */
431
448
  typeSafety?: TypeSafetyOptions;
449
+ /** Compile-time Effect governance for routes and command handlers. */
450
+ effect?: EffectCompilerOptions;
451
+ }
452
+ export interface EffectCompilerOptions {
453
+ /** Require every discovered route to declare an Effect contract. */
454
+ requireRouteEffects?: boolean;
455
+ /** Require explicit public mappings for Effect failures. */
456
+ requireErrorMappings?: boolean;
457
+ /** Require an explicit dependency list for Effect environments. */
458
+ requireDependencies?: boolean;
432
459
  }
433
460
  export interface GraphqlOptions {
434
461
  /** Additionally emit standard TypedDocumentNode artifacts for GraphQL ecosystem clients. */
@@ -527,6 +554,7 @@ export interface ValidateOptions {
527
554
  disallowControllerDirectDb?: boolean;
528
555
  /** Detect modules declared in the project that are unreachable from any root module. */
529
556
  detectOrphanModules?: boolean;
557
+ effect?: EffectCompilerOptions;
530
558
  }
531
559
  /** Runtime capabilities declared by the Command executor. */
532
560
  export interface CommandExecutionCapabilities {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",