@supacloud/compiler 0.53.0 → 0.54.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
@@ -3,22 +3,25 @@
3
3
  ## Enforced Database Governance
4
4
 
5
5
  Existing compiler callers retain `off` as a compatibility migration exception.
6
- Newly generated `command` starters set
7
- `databaseGovernance: { mode: "enforce", moduleFile: "db/modules.ts" }`:
8
- compile/check/build run the existing `@supacloud/db` source-boundary and
9
- contract-freshness gate, SQL lint and live Catalog reconciliation, including
6
+ Newly generated `command` starters keep `databaseGovernance: { mode: "check",
7
+ moduleFile: "db/modules.ts" }` for ordinary compile/check/build. `check` runs
8
+ the existing `@supacloud/db` source-boundary and contract-freshness gate plus SQL
9
+ lint, without importing project modules or opening a database connection. The
10
+ generated integration dev profile switches the same configuration to `enforce`
11
+ and supplies a loopback database URL explicitly.
12
+
13
+ `enforce` adds live Catalog reconciliation on top of `check`, including on
10
14
  incremental cache hits. Missing adoption files, stale contracts, SQL findings or
11
15
  Catalog drift block generation even with `writeOnError`; checks never generate
12
- contracts or migrations.
13
- The minimal/http/edge starters remain database-free.
16
+ contracts or migrations. The minimal/http/edge starters remain database-free.
14
17
 
15
18
  For an existing database project, adopt the governed path by declaring the real
16
- database modules and explicitly setting the following in `supacloud.config.ts`:
19
+ database modules and setting the following in `supacloud.config.ts`:
17
20
 
18
21
  ```ts
19
22
  export default {
20
23
  databaseGovernance: {
21
- mode: "enforce",
24
+ mode: "check",
22
25
  projectRoot: ".",
23
26
  moduleFile: "db/modules.ts",
24
27
  databaseUrlEnv: "SUPACLOUD_CHECK_DATABASE_URL",
@@ -27,11 +30,17 @@ export default {
27
30
  };
28
31
  ```
29
32
 
33
+ Keep `check` in committed project configuration so local builds and CI remain
34
+ hermetic. Run `enforce` only in an explicit release/deployment gate where the
35
+ platform injects a read-only PostgreSQL connection, or run the separate
36
+ `supacloud db module_check` command through the same controlled environment.
37
+ Application code and committed configuration must not contain a production
38
+ connection URL.
39
+
30
40
  `projectRoot` is relative to the configuration directory (or `rootDir` when
31
- calling `compileProject` / `checkProject` directly). Use a read-only PostgreSQL
32
- credential in the named environment variable, targeting an explicitly selected
33
- local or test database. The compiler does not load `.env` files for this worker,
34
- generate migrations, repair contracts, or apply SQL. Setting `mode: "off"` is
41
+ calling `compileProject` / `checkProject` directly). The compiler does not load
42
+ `.env` files for this worker, generate migrations, repair contracts, or apply
43
+ SQL. Never use a migration/owner role for this check. Setting `mode: "off"` is
35
44
  reserved for an explicit migration exception and should not be used in a new
36
45
  database starter.
37
46
 
package/dist/cli.js CHANGED
@@ -1475,6 +1475,9 @@ var init_validate = __esm(() => {
1475
1475
  "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1476
1476
  "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1477
1477
  "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1478
+ "effect-retry-target-required": { code: "SC3048", docsUrl: "https://supacloud.dev/errors/SC3048" },
1479
+ "effect-retryable-invalid": { code: "SC3049", docsUrl: "https://supacloud.dev/errors/SC3049" },
1480
+ "effect-retry-required": { code: "SC3050", docsUrl: "https://supacloud.dev/errors/SC3050" },
1478
1481
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1479
1482
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1480
1483
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -19105,7 +19108,8 @@ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
19105
19108
  requireExactDependencyTypes: true,
19106
19109
  requireTimeoutForDependencies: true,
19107
19110
  forbidDirectRuntimeExecution: true,
19108
- forbidDirectThrows: true
19111
+ forbidDirectThrows: true,
19112
+ requireRetryPolicy: true
19109
19113
  };
19110
19114
  function nodeText(node) {
19111
19115
  return node.getText(node.getSourceFile());
@@ -19178,7 +19182,8 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19178
19182
  requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
19179
19183
  requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
19180
19184
  forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
19181
- forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
19185
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows,
19186
+ requireRetryPolicy: effectOptions.requireRetryPolicy ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireRetryPolicy
19182
19187
  }
19183
19188
  };
19184
19189
  if (ctx.effectOptions.forbidDirectRuntimeExecution) {
@@ -20001,6 +20006,21 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
20001
20006
  }
20002
20007
  return ts7.isObjectLiteralExpression(input) ? input : undefined;
20003
20008
  }
20009
+ function resolveStaticArrayLiteral(input, ctx, seen = new Set) {
20010
+ if (!input || seen.has(input))
20011
+ return;
20012
+ seen.add(input);
20013
+ if (ts7.isAsExpression(input) || ts7.isSatisfiesExpression(input) || ts7.isParenthesizedExpression(input)) {
20014
+ return resolveStaticArrayLiteral(input.expression, ctx, seen);
20015
+ }
20016
+ if (ts7.isIdentifier(input)) {
20017
+ const declaration = resolveDeclaration(input, ctx).find(ts7.isVariableDeclaration);
20018
+ const list = declaration?.parent;
20019
+ const isConst = Boolean(list && ts7.isVariableDeclarationList(list) && (list.flags & ts7.NodeFlags.Const) !== 0);
20020
+ return isConst && declaration?.initializer ? resolveStaticArrayLiteral(declaration.initializer, ctx, seen) : undefined;
20021
+ }
20022
+ return ts7.isArrayLiteralExpression(input) ? input : undefined;
20023
+ }
20004
20024
  function routePolicyProperty(object, name, ctx, seen = new Set) {
20005
20025
  if (seen.has(object))
20006
20026
  return { unresolved: true };
@@ -20961,7 +20981,7 @@ function parseController(input, ctx) {
20961
20981
  file,
20962
20982
  line: lineOf(effectExpr),
20963
20983
  message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
20964
- suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
20984
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
20965
20985
  });
20966
20986
  } else {
20967
20987
  const effect = { required: true };
@@ -20992,6 +21012,8 @@ function parseController(input, ctx) {
20992
21012
  } else {
20993
21013
  effect.retry = retry;
20994
21014
  }
21015
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
21016
+ effect.retry = "none";
20995
21017
  }
20996
21018
  const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
20997
21019
  if (maxAttemptsExpr !== undefined) {
@@ -21040,7 +21062,8 @@ function parseController(input, ctx) {
21040
21062
  }
21041
21063
  const dependenciesExpr = getProp(effectObject, "dependencies");
21042
21064
  if (dependenciesExpr !== undefined) {
21043
- if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21065
+ const dependenciesArray = resolveStaticArrayLiteral(dependenciesExpr, ctx);
21066
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21044
21067
  ctx.diagnostics.push({
21045
21068
  severity: "error",
21046
21069
  code: "invalid-effect-dependencies",
@@ -21048,10 +21071,10 @@ function parseController(input, ctx) {
21048
21071
  docsUrl: "https://supacloud.dev/errors/SC3033",
21049
21072
  file,
21050
21073
  line: lineOf(dependenciesExpr),
21051
- message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
21074
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21052
21075
  });
21053
21076
  } else {
21054
- effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
21077
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21055
21078
  if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21056
21079
  ctx.diagnostics.push({
21057
21080
  severity: "error",
@@ -21068,7 +21091,8 @@ function parseController(input, ctx) {
21068
21091
  }
21069
21092
  const errorsExpr = getProp(effectObject, "errors");
21070
21093
  if (errorsExpr !== undefined) {
21071
- if (!ts7.isArrayLiteralExpression(errorsExpr)) {
21094
+ const errorsArray = resolveStaticArrayLiteral(errorsExpr, ctx);
21095
+ if (!errorsArray) {
21072
21096
  ctx.diagnostics.push({
21073
21097
  severity: "error",
21074
21098
  code: "invalid-effect-errors",
@@ -21076,11 +21100,11 @@ function parseController(input, ctx) {
21076
21100
  docsUrl: "https://supacloud.dev/errors/SC3034",
21077
21101
  file,
21078
21102
  line: lineOf(errorsExpr),
21079
- message: `Route ${route.handler} effect.errors must be an explicit object array.`
21103
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21080
21104
  });
21081
21105
  } else {
21082
21106
  const errors = [];
21083
- for (const errorExpr of errorsExpr.elements) {
21107
+ for (const errorExpr of errorsArray.elements) {
21084
21108
  if (!ts7.isObjectLiteralExpression(errorExpr)) {
21085
21109
  ctx.diagnostics.push({
21086
21110
  severity: "error",
@@ -21097,6 +21121,23 @@ function parseController(input, ctx) {
21097
21121
  const code = stringLiteralProp(errorExpr, "code");
21098
21122
  const status = numberLiteralProp(errorExpr, "status");
21099
21123
  const message = stringLiteralProp(errorExpr, "message");
21124
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21125
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21126
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21127
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21128
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21129
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21130
+ ctx.diagnostics.push({
21131
+ severity: "error",
21132
+ code: "invalid-effect-errors",
21133
+ errorCode: "SC3034",
21134
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21135
+ file,
21136
+ line: lineOf(errorExpr),
21137
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21138
+ });
21139
+ continue;
21140
+ }
21100
21141
  if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21101
21142
  ctx.diagnostics.push({
21102
21143
  severity: "error",
@@ -21109,7 +21150,17 @@ function parseController(input, ctx) {
21109
21150
  });
21110
21151
  continue;
21111
21152
  }
21112
- errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
21153
+ errors.push({
21154
+ tag,
21155
+ code,
21156
+ status,
21157
+ ...message === undefined ? {} : { message },
21158
+ ...codeFromError === undefined ? {} : { codeFromError },
21159
+ ...messageFromError === undefined ? {} : { messageFromError },
21160
+ ...statusFromError === undefined ? {} : { statusFromError },
21161
+ ...retryable === undefined ? {} : { retryable },
21162
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21163
+ });
21113
21164
  }
21114
21165
  effect.errors = errors;
21115
21166
  if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
@@ -21837,6 +21888,19 @@ function numberLiteralProp(obj, name) {
21837
21888
  const value = Number(expr.text);
21838
21889
  return Number.isFinite(value) ? value : undefined;
21839
21890
  }
21891
+ function booleanLiteralProp(obj, name) {
21892
+ const expr = getProp(obj, name);
21893
+ if (!expr || !isBooleanLiteral(expr))
21894
+ return;
21895
+ return expr.kind === ts7.SyntaxKind.TrueKeyword;
21896
+ }
21897
+ function isBooleanLiteral(expr) {
21898
+ return expr.kind === ts7.SyntaxKind.TrueKeyword || expr.kind === ts7.SyntaxKind.FalseKeyword;
21899
+ }
21900
+ function invalidBooleanProp(obj, name) {
21901
+ const expr = getProp(obj, name);
21902
+ return expr !== undefined && !isBooleanLiteral(expr);
21903
+ }
21840
21904
  function arrayProp(obj, name) {
21841
21905
  const expr = getProp(obj, name);
21842
21906
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -22777,6 +22841,9 @@ function validateEffectPolicies(graph, options = {}) {
22777
22841
  if (options.requireDependencies && effect.dependencies === undefined) {
22778
22842
  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."));
22779
22843
  }
22844
+ if (effect.retry === undefined && options.requireRetryPolicy !== false) {
22845
+ diagnostics.push(diagnostic("effect-retry-required", `Route ${label} declares an Effect contract but does not declare retry.`, controller.file, 'Set effect.retry to "none". That is the required default; use "explicit" only with maxAttempts and a retryable 5xx allow-list.'));
22846
+ }
22780
22847
  if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
22781
22848
  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."));
22782
22849
  }
@@ -22787,6 +22854,18 @@ function validateEffectPolicies(graph, options = {}) {
22787
22854
  if (command && command.idempotency !== "required") {
22788
22855
  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".'));
22789
22856
  }
22857
+ const retryable = (effect.errors ?? []).filter((entry) => entry.retryable === true);
22858
+ if (retryable.length === 0) {
22859
+ diagnostics.push(diagnostic("effect-retry-target-required", `Route ${label} enables explicit Effect retry without a transient failure allow-list.`, controller.file, 'Mark only 5xx failures that did not commit with retryable: true, or use effect.retry: "none".'));
22860
+ }
22861
+ }
22862
+ for (const entry of effect.errors ?? []) {
22863
+ if (entry.retryable !== true)
22864
+ continue;
22865
+ const transient = effect.retry === "explicit" && entry.outcomeUnknown !== true && entry.statusFromError !== true && entry.status >= 500 && entry.status <= 599;
22866
+ if (!transient) {
22867
+ diagnostics.push(diagnostic("effect-retryable-invalid", `Route ${label} marks "${entry.tag}" retryable, but in-process retry is only for declared 5xx failures that are not outcome-unknown.`, controller.file, 'Remove retryable, or keep it on a 5xx mapping with retry: "explicit" and without outcomeUnknown or statusFromError.'));
22868
+ }
22790
22869
  }
22791
22870
  }
22792
22871
  }
@@ -23787,7 +23866,9 @@ var DEFAULT_SUPACLOUD_CONFIG = {
23787
23866
  requireExactDependencyTypes: true,
23788
23867
  requireTimeoutForDependencies: true,
23789
23868
  forbidDirectRuntimeExecution: true,
23790
- forbidDirectThrows: true
23869
+ forbidDirectThrows: true,
23870
+ requireRetryPolicy: true,
23871
+ defaultRetry: "none"
23791
23872
  }
23792
23873
  };
23793
23874
  function defineSupacloudConfig(config = {}) {
@@ -23869,7 +23950,9 @@ function validateGovernanceConfig(config) {
23869
23950
  "requireExactDependencyTypes",
23870
23951
  "requireTimeoutForDependencies",
23871
23952
  "forbidDirectRuntimeExecution",
23872
- "forbidDirectThrows"
23953
+ "forbidDirectThrows",
23954
+ "requireRetryPolicy",
23955
+ "defaultRetry"
23873
23956
  ].includes(key))) {
23874
23957
  throw new Error("effect contains an unsupported governance option.");
23875
23958
  }
@@ -23881,12 +23964,16 @@ function validateGovernanceConfig(config) {
23881
23964
  "requireExactDependencyTypes",
23882
23965
  "requireTimeoutForDependencies",
23883
23966
  "forbidDirectRuntimeExecution",
23884
- "forbidDirectThrows"
23967
+ "forbidDirectThrows",
23968
+ "requireRetryPolicy"
23885
23969
  ]) {
23886
23970
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
23887
23971
  throw new Error(`effect.${key} must be boolean.`);
23888
23972
  }
23889
23973
  }
23974
+ if (config.effect["defaultRetry"] !== undefined && config.effect["defaultRetry"] !== "none") {
23975
+ throw new Error('effect.defaultRetry must be "none". explicit can only be written on a route, with maxAttempts and a retryable 5xx allow-list.');
23976
+ }
23890
23977
  }
23891
23978
  }
23892
23979
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
@@ -23985,10 +24072,7 @@ async function realDirectory(path) {
23985
24072
  }
23986
24073
  if (!stat.isDirectory() || stat.isSymbolicLink())
23987
24074
  return fail("PREVIEW_FIX_ROOT_UNSUPPORTED");
23988
- const real = await realpath4(resolved);
23989
- if (real !== resolved)
23990
- return fail("PREVIEW_FIX_ROOT_UNSUPPORTED");
23991
- return real;
24075
+ return realpath4(resolved);
23992
24076
  }
23993
24077
  async function treeDigest(root) {
23994
24078
  const files = [];
@@ -25734,10 +25818,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25734
25818
  import { resolve as resolve19 } from "node:path";
25735
25819
 
25736
25820
  // src/migration-app-version.ts
25737
- var MIGRATION_APP_VERSION = "0.35.0";
25821
+ var MIGRATION_APP_VERSION = "0.36.0";
25738
25822
 
25739
25823
  // src/migration-elysia-version.ts
25740
- var MIGRATION_ELYSIA_VERSION = "0.37.0";
25824
+ var MIGRATION_ELYSIA_VERSION = "0.38.0";
25741
25825
 
25742
25826
  // src/migration-policy.ts
25743
25827
  var cachedCompilerVersion;
@@ -25756,7 +25840,7 @@ function migrationDependencies() {
25756
25840
  "@supacloud/app": MIGRATION_APP_VERSION,
25757
25841
  "@supacloud/compiler": compilerVersion(),
25758
25842
  "@supacloud/elysia": MIGRATION_ELYSIA_VERSION,
25759
- elysia: "2.0.0-rc.0",
25843
+ elysia: "2.0.0-rc.1",
25760
25844
  typescript: "7.0.2"
25761
25845
  };
25762
25846
  }
package/dist/index.js CHANGED
@@ -1474,6 +1474,9 @@ var init_validate = __esm(() => {
1474
1474
  "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1475
1475
  "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1476
1476
  "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1477
+ "effect-retry-target-required": { code: "SC3048", docsUrl: "https://supacloud.dev/errors/SC3048" },
1478
+ "effect-retryable-invalid": { code: "SC3049", docsUrl: "https://supacloud.dev/errors/SC3049" },
1479
+ "effect-retry-required": { code: "SC3050", docsUrl: "https://supacloud.dev/errors/SC3050" },
1477
1480
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1478
1481
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1479
1482
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -19064,7 +19067,8 @@ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
19064
19067
  requireExactDependencyTypes: true,
19065
19068
  requireTimeoutForDependencies: true,
19066
19069
  forbidDirectRuntimeExecution: true,
19067
- forbidDirectThrows: true
19070
+ forbidDirectThrows: true,
19071
+ requireRetryPolicy: true
19068
19072
  };
19069
19073
  function nodeText(node) {
19070
19074
  return node.getText(node.getSourceFile());
@@ -19137,7 +19141,8 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19137
19141
  requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
19138
19142
  requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
19139
19143
  forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
19140
- forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
19144
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows,
19145
+ requireRetryPolicy: effectOptions.requireRetryPolicy ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireRetryPolicy
19141
19146
  }
19142
19147
  };
19143
19148
  if (ctx.effectOptions.forbidDirectRuntimeExecution) {
@@ -19960,6 +19965,21 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19960
19965
  }
19961
19966
  return ts7.isObjectLiteralExpression(input) ? input : undefined;
19962
19967
  }
19968
+ function resolveStaticArrayLiteral(input, ctx, seen = new Set) {
19969
+ if (!input || seen.has(input))
19970
+ return;
19971
+ seen.add(input);
19972
+ if (ts7.isAsExpression(input) || ts7.isSatisfiesExpression(input) || ts7.isParenthesizedExpression(input)) {
19973
+ return resolveStaticArrayLiteral(input.expression, ctx, seen);
19974
+ }
19975
+ if (ts7.isIdentifier(input)) {
19976
+ const declaration = resolveDeclaration(input, ctx).find(ts7.isVariableDeclaration);
19977
+ const list = declaration?.parent;
19978
+ const isConst = Boolean(list && ts7.isVariableDeclarationList(list) && (list.flags & ts7.NodeFlags.Const) !== 0);
19979
+ return isConst && declaration?.initializer ? resolveStaticArrayLiteral(declaration.initializer, ctx, seen) : undefined;
19980
+ }
19981
+ return ts7.isArrayLiteralExpression(input) ? input : undefined;
19982
+ }
19963
19983
  function routePolicyProperty(object, name, ctx, seen = new Set) {
19964
19984
  if (seen.has(object))
19965
19985
  return { unresolved: true };
@@ -20920,7 +20940,7 @@ function parseController(input, ctx) {
20920
20940
  file,
20921
20941
  line: lineOf(effectExpr),
20922
20942
  message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
20923
- suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
20943
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
20924
20944
  });
20925
20945
  } else {
20926
20946
  const effect = { required: true };
@@ -20951,6 +20971,8 @@ function parseController(input, ctx) {
20951
20971
  } else {
20952
20972
  effect.retry = retry;
20953
20973
  }
20974
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
20975
+ effect.retry = "none";
20954
20976
  }
20955
20977
  const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
20956
20978
  if (maxAttemptsExpr !== undefined) {
@@ -20999,7 +21021,8 @@ function parseController(input, ctx) {
20999
21021
  }
21000
21022
  const dependenciesExpr = getProp(effectObject, "dependencies");
21001
21023
  if (dependenciesExpr !== undefined) {
21002
- if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21024
+ const dependenciesArray = resolveStaticArrayLiteral(dependenciesExpr, ctx);
21025
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21003
21026
  ctx.diagnostics.push({
21004
21027
  severity: "error",
21005
21028
  code: "invalid-effect-dependencies",
@@ -21007,10 +21030,10 @@ function parseController(input, ctx) {
21007
21030
  docsUrl: "https://supacloud.dev/errors/SC3033",
21008
21031
  file,
21009
21032
  line: lineOf(dependenciesExpr),
21010
- message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
21033
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21011
21034
  });
21012
21035
  } else {
21013
- effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
21036
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21014
21037
  if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21015
21038
  ctx.diagnostics.push({
21016
21039
  severity: "error",
@@ -21027,7 +21050,8 @@ function parseController(input, ctx) {
21027
21050
  }
21028
21051
  const errorsExpr = getProp(effectObject, "errors");
21029
21052
  if (errorsExpr !== undefined) {
21030
- if (!ts7.isArrayLiteralExpression(errorsExpr)) {
21053
+ const errorsArray = resolveStaticArrayLiteral(errorsExpr, ctx);
21054
+ if (!errorsArray) {
21031
21055
  ctx.diagnostics.push({
21032
21056
  severity: "error",
21033
21057
  code: "invalid-effect-errors",
@@ -21035,11 +21059,11 @@ function parseController(input, ctx) {
21035
21059
  docsUrl: "https://supacloud.dev/errors/SC3034",
21036
21060
  file,
21037
21061
  line: lineOf(errorsExpr),
21038
- message: `Route ${route.handler} effect.errors must be an explicit object array.`
21062
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21039
21063
  });
21040
21064
  } else {
21041
21065
  const errors = [];
21042
- for (const errorExpr of errorsExpr.elements) {
21066
+ for (const errorExpr of errorsArray.elements) {
21043
21067
  if (!ts7.isObjectLiteralExpression(errorExpr)) {
21044
21068
  ctx.diagnostics.push({
21045
21069
  severity: "error",
@@ -21056,6 +21080,23 @@ function parseController(input, ctx) {
21056
21080
  const code = stringLiteralProp(errorExpr, "code");
21057
21081
  const status = numberLiteralProp(errorExpr, "status");
21058
21082
  const message = stringLiteralProp(errorExpr, "message");
21083
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21084
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21085
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21086
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21087
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21088
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21089
+ ctx.diagnostics.push({
21090
+ severity: "error",
21091
+ code: "invalid-effect-errors",
21092
+ errorCode: "SC3034",
21093
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21094
+ file,
21095
+ line: lineOf(errorExpr),
21096
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21097
+ });
21098
+ continue;
21099
+ }
21059
21100
  if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21060
21101
  ctx.diagnostics.push({
21061
21102
  severity: "error",
@@ -21068,7 +21109,17 @@ function parseController(input, ctx) {
21068
21109
  });
21069
21110
  continue;
21070
21111
  }
21071
- errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
21112
+ errors.push({
21113
+ tag,
21114
+ code,
21115
+ status,
21116
+ ...message === undefined ? {} : { message },
21117
+ ...codeFromError === undefined ? {} : { codeFromError },
21118
+ ...messageFromError === undefined ? {} : { messageFromError },
21119
+ ...statusFromError === undefined ? {} : { statusFromError },
21120
+ ...retryable === undefined ? {} : { retryable },
21121
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21122
+ });
21072
21123
  }
21073
21124
  effect.errors = errors;
21074
21125
  if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
@@ -21796,6 +21847,19 @@ function numberLiteralProp(obj, name) {
21796
21847
  const value = Number(expr.text);
21797
21848
  return Number.isFinite(value) ? value : undefined;
21798
21849
  }
21850
+ function booleanLiteralProp(obj, name) {
21851
+ const expr = getProp(obj, name);
21852
+ if (!expr || !isBooleanLiteral(expr))
21853
+ return;
21854
+ return expr.kind === ts7.SyntaxKind.TrueKeyword;
21855
+ }
21856
+ function isBooleanLiteral(expr) {
21857
+ return expr.kind === ts7.SyntaxKind.TrueKeyword || expr.kind === ts7.SyntaxKind.FalseKeyword;
21858
+ }
21859
+ function invalidBooleanProp(obj, name) {
21860
+ const expr = getProp(obj, name);
21861
+ return expr !== undefined && !isBooleanLiteral(expr);
21862
+ }
21799
21863
  function arrayProp(obj, name) {
21800
21864
  const expr = getProp(obj, name);
21801
21865
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -22738,6 +22802,9 @@ function validateEffectPolicies(graph, options = {}) {
22738
22802
  if (options.requireDependencies && effect.dependencies === undefined) {
22739
22803
  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."));
22740
22804
  }
22805
+ if (effect.retry === undefined && options.requireRetryPolicy !== false) {
22806
+ diagnostics.push(diagnostic("effect-retry-required", `Route ${label} declares an Effect contract but does not declare retry.`, controller.file, 'Set effect.retry to "none". That is the required default; use "explicit" only with maxAttempts and a retryable 5xx allow-list.'));
22807
+ }
22741
22808
  if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
22742
22809
  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."));
22743
22810
  }
@@ -22748,6 +22815,18 @@ function validateEffectPolicies(graph, options = {}) {
22748
22815
  if (command && command.idempotency !== "required") {
22749
22816
  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".'));
22750
22817
  }
22818
+ const retryable = (effect.errors ?? []).filter((entry) => entry.retryable === true);
22819
+ if (retryable.length === 0) {
22820
+ diagnostics.push(diagnostic("effect-retry-target-required", `Route ${label} enables explicit Effect retry without a transient failure allow-list.`, controller.file, 'Mark only 5xx failures that did not commit with retryable: true, or use effect.retry: "none".'));
22821
+ }
22822
+ }
22823
+ for (const entry of effect.errors ?? []) {
22824
+ if (entry.retryable !== true)
22825
+ continue;
22826
+ const transient = effect.retry === "explicit" && entry.outcomeUnknown !== true && entry.statusFromError !== true && entry.status >= 500 && entry.status <= 599;
22827
+ if (!transient) {
22828
+ diagnostics.push(diagnostic("effect-retryable-invalid", `Route ${label} marks "${entry.tag}" retryable, but in-process retry is only for declared 5xx failures that are not outcome-unknown.`, controller.file, 'Remove retryable, or keep it on a 5xx mapping with retry: "explicit" and without outcomeUnknown or statusFromError.'));
22829
+ }
22751
22830
  }
22752
22831
  }
22753
22832
  }
@@ -24991,10 +25070,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24991
25070
  import { resolve as resolve17 } from "node:path";
24992
25071
 
24993
25072
  // src/migration-app-version.ts
24994
- var MIGRATION_APP_VERSION = "0.35.0";
25073
+ var MIGRATION_APP_VERSION = "0.36.0";
24995
25074
 
24996
25075
  // src/migration-elysia-version.ts
24997
- var MIGRATION_ELYSIA_VERSION = "0.37.0";
25076
+ var MIGRATION_ELYSIA_VERSION = "0.38.0";
24998
25077
 
24999
25078
  // src/migration-policy.ts
25000
25079
  var cachedCompilerVersion;
@@ -25013,7 +25092,7 @@ function migrationDependencies() {
25013
25092
  "@supacloud/app": MIGRATION_APP_VERSION,
25014
25093
  "@supacloud/compiler": compilerVersion(),
25015
25094
  "@supacloud/elysia": MIGRATION_ELYSIA_VERSION,
25016
- elysia: "2.0.0-rc.0",
25095
+ elysia: "2.0.0-rc.1",
25017
25096
  typescript: "7.0.2"
25018
25097
  };
25019
25098
  }
@@ -26357,7 +26436,9 @@ var DEFAULT_SUPACLOUD_CONFIG = {
26357
26436
  requireExactDependencyTypes: true,
26358
26437
  requireTimeoutForDependencies: true,
26359
26438
  forbidDirectRuntimeExecution: true,
26360
- forbidDirectThrows: true
26439
+ forbidDirectThrows: true,
26440
+ requireRetryPolicy: true,
26441
+ defaultRetry: "none"
26361
26442
  }
26362
26443
  };
26363
26444
  function defineSupacloudConfig(config = {}) {
@@ -26439,7 +26520,9 @@ function validateGovernanceConfig(config) {
26439
26520
  "requireExactDependencyTypes",
26440
26521
  "requireTimeoutForDependencies",
26441
26522
  "forbidDirectRuntimeExecution",
26442
- "forbidDirectThrows"
26523
+ "forbidDirectThrows",
26524
+ "requireRetryPolicy",
26525
+ "defaultRetry"
26443
26526
  ].includes(key))) {
26444
26527
  throw new Error("effect contains an unsupported governance option.");
26445
26528
  }
@@ -26451,12 +26534,16 @@ function validateGovernanceConfig(config) {
26451
26534
  "requireExactDependencyTypes",
26452
26535
  "requireTimeoutForDependencies",
26453
26536
  "forbidDirectRuntimeExecution",
26454
- "forbidDirectThrows"
26537
+ "forbidDirectThrows",
26538
+ "requireRetryPolicy"
26455
26539
  ]) {
26456
26540
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
26457
26541
  throw new Error(`effect.${key} must be boolean.`);
26458
26542
  }
26459
26543
  }
26544
+ if (config.effect["defaultRetry"] !== undefined && config.effect["defaultRetry"] !== "none") {
26545
+ throw new Error('effect.defaultRetry must be "none". explicit can only be written on a route, with maxAttempts and a retryable 5xx allow-list.');
26546
+ }
26460
26547
  }
26461
26548
  }
26462
26549
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.35.0";
1
+ export declare const MIGRATION_APP_VERSION = "0.36.0";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.37.0";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.38.0";
package/dist/types.d.ts CHANGED
@@ -191,6 +191,19 @@ export interface EffectErrorMappingNode {
191
191
  status: number;
192
192
  code: string;
193
193
  message?: string;
194
+ /** Use the failure value's own `code` string when it is non-empty and bounded. */
195
+ codeFromError?: boolean;
196
+ /** Use the failure value's own `message` string when it is non-empty and bounded. */
197
+ messageFromError?: boolean;
198
+ /** Use the failure value's own HTTP status when it is an integer from 400 to 599. */
199
+ statusFromError?: boolean;
200
+ /**
201
+ * In-process retry allow-list. Only a declared 5xx failure that did not
202
+ * commit may be retried by the framework adapter.
203
+ */
204
+ retryable?: boolean;
205
+ /** The write may already have happened. The framework adapter must not retry it. */
206
+ outcomeUnknown?: boolean;
194
207
  }
195
208
  export interface EffectRouteNode {
196
209
  required: true;
@@ -511,6 +524,13 @@ export interface EffectCompilerOptions {
511
524
  forbidDirectRuntimeExecution?: boolean;
512
525
  /** Reject direct throw statements inside governed route handlers. */
513
526
  forbidDirectThrows?: boolean;
527
+ /** Require every route that declares an Effect contract to write an explicit retry policy. */
528
+ requireRetryPolicy?: boolean;
529
+ /**
530
+ * Materialize an omitted retry policy when requireRetryPolicy is disabled.
531
+ * The only project-level default is "none"; "explicit" stays route-local.
532
+ */
533
+ defaultRetry?: "none";
514
534
  }
515
535
  export interface GraphqlOptions {
516
536
  /** Additionally emit standard TypedDocumentNode artifacts for GraphQL ecosystem clients. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.53.0",
3
+ "version": "0.54.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",
@@ -57,7 +57,7 @@
57
57
  "directory": "packages/compiler"
58
58
  },
59
59
  "dependencies": {
60
- "@supacloud/db": "0.20.3",
60
+ "@supacloud/db": "0.20.4",
61
61
  "@supacloud/delivery": "0.24.0",
62
62
  "typebox": "1.4.3",
63
63
  "@graphql-codegen/core": "^6.2.2",