@supacloud/compiler 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -382,6 +382,7 @@ var init_di_resolution = __esm(() => {
382
382
  "conflicting-injection-flags",
383
383
  "cross-module-scoped-dependency",
384
384
  "mixed-multi-provider-scopes",
385
+ "effect-contract-required",
385
386
  "dynamic-aspect-reference",
386
387
  "invalid-aspect-reference",
387
388
  "aspect-operation-mismatch"
@@ -2453,6 +2454,7 @@ function renderApplication(graph, options) {
2453
2454
  executor: route.command === undefined ? "route" : "command",
2454
2455
  ...command === undefined ? {} : { commandBoundary: route.command === undefined ? "handler" : "route" },
2455
2456
  ...route.effect === undefined ? {} : { effect: route.effect },
2457
+ ...route.effectMode === undefined ? {} : { effectMode: route.effectMode },
2456
2458
  invoker: isPipeline ? "pipeline" : "direct",
2457
2459
  stages: operationPlans.find((plan) => plan.module === module.name && plan.kind === "route" && plan.name === `${route.method} ${joinRoutePaths(controller.path, route.path)}`).stages,
2458
2460
  ...commandRef === undefined ? {} : { command: command?.name ?? commandRef },
@@ -2832,6 +2834,8 @@ class ModuleGenerator {
2832
2834
  if (route.effect !== undefined) {
2833
2835
  fields.push(`effect: ${JSON.stringify(route.effect)}`);
2834
2836
  }
2837
+ if (route.effectMode !== undefined)
2838
+ fields.push(`effectMode: ${JSON.stringify(route.effectMode)}`);
2835
2839
  if (idempotency !== undefined) {
2836
2840
  fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
2837
2841
  }
@@ -4681,6 +4685,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4681
4685
  maxAttempts?: number;
4682
4686
  timeoutMs?: number;
4683
4687
  };
4688
+ /** Simple route profile; no Effect interpreter or error mapping is required. */
4689
+ effectMode?: "simple";
4684
4690
  /** Compile-time ownership and transport classification for the route boundary. */
4685
4691
  contract?: {
4686
4692
  body?: "framework" | "domain";
@@ -19186,6 +19192,8 @@ async function scanCommandBoundaries(source, file, checker, commands, boundRoute
19186
19192
  return false;
19187
19193
  if (!ts4.isCallExpression(node.parent) || node.parent.expression !== node)
19188
19194
  return false;
19195
+ if (command.rpc !== undefined && propertyName(method.name) === "execute")
19196
+ return true;
19189
19197
  for (let owner = node.parent.parent;owner; owner = owner.parent) {
19190
19198
  if (ts4.isMethodDeclaration(owner)) {
19191
19199
  if (owner.parent === method.parent && (ts4.isPropertyAccessExpression(node) || ts4.isElementAccessExpression(node)) && node.expression.kind === ts4.SyntaxKind.ThisKeyword)
@@ -19951,7 +19959,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19951
19959
  }
19952
19960
  const governedClasses = modules.flatMap((module) => module.commands.flatMap((command) => {
19953
19961
  const declaration = ctx.classesByName.get(command.className)?.decl;
19954
- return declaration ? [{ declaration, name: command.name }] : [];
19962
+ return declaration ? [{ declaration, name: command.name, ...command.rpc === undefined ? {} : { rpc: command.rpc } }] : [];
19955
19963
  }));
19956
19964
  const boundRoutes = new Map;
19957
19965
  for (const module of modules)
@@ -21486,111 +21494,102 @@ async function parseController(input, ctx) {
21486
21494
  }
21487
21495
  const effectExpr = getProp(optionsObject, "effect");
21488
21496
  if (effectExpr) {
21489
- const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21490
- if (!effectObject) {
21491
- ctx.diagnostics.push({
21492
- severity: "error",
21493
- code: "invalid-effect-contract",
21494
- errorCode: "SC3030",
21495
- docsUrl: "https://supacloud.dev/errors/SC3030",
21496
- file,
21497
- line: lineOf(effectExpr),
21498
- message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21499
- suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21500
- });
21497
+ if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
21498
+ route.effectMode = "simple";
21501
21499
  } else {
21502
- const effect = { required: true };
21503
- const requiredExpr = getProp(effectObject, "required");
21504
- if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21500
+ const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21501
+ if (!effectObject) {
21505
21502
  ctx.diagnostics.push({
21506
21503
  severity: "error",
21507
21504
  code: "invalid-effect-contract",
21508
21505
  errorCode: "SC3030",
21509
21506
  docsUrl: "https://supacloud.dev/errors/SC3030",
21510
21507
  file,
21511
- line: lineOf(requiredExpr),
21512
- message: `Route ${route.handler} effect.required must be the literal true.`
21508
+ line: lineOf(effectExpr),
21509
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21510
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21513
21511
  });
21514
- }
21515
- const retry = stringLiteralProp(effectObject, "retry");
21516
- if (retry !== undefined) {
21517
- if (retry !== "none" && retry !== "explicit") {
21512
+ } else {
21513
+ const effect = { required: true };
21514
+ const requiredExpr = getProp(effectObject, "required");
21515
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21518
21516
  ctx.diagnostics.push({
21519
21517
  severity: "error",
21520
- code: "invalid-effect-retry",
21521
- errorCode: "SC3031",
21522
- docsUrl: "https://supacloud.dev/errors/SC3031",
21518
+ code: "invalid-effect-contract",
21519
+ errorCode: "SC3030",
21520
+ docsUrl: "https://supacloud.dev/errors/SC3030",
21523
21521
  file,
21524
- line: lineOf(getProp(effectObject, "retry")),
21525
- message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
21522
+ line: lineOf(requiredExpr),
21523
+ message: `Route ${route.handler} effect.required must be the literal true.`
21526
21524
  });
21527
- } else {
21528
- effect.retry = retry;
21529
21525
  }
21530
- } else if (ctx.effectOptions.requireRetryPolicy === false) {
21531
- effect.retry = "none";
21532
- }
21533
- const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21534
- if (maxAttemptsExpr !== undefined) {
21535
- const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21536
- if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
21526
+ const retry = stringLiteralProp(effectObject, "retry");
21527
+ if (retry !== undefined) {
21528
+ if (retry !== "none" && retry !== "explicit") {
21529
+ ctx.diagnostics.push({
21530
+ severity: "error",
21531
+ code: "invalid-effect-retry",
21532
+ errorCode: "SC3031",
21533
+ docsUrl: "https://supacloud.dev/errors/SC3031",
21534
+ file,
21535
+ line: lineOf(getProp(effectObject, "retry")),
21536
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
21537
+ });
21538
+ } else {
21539
+ effect.retry = retry;
21540
+ }
21541
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
21542
+ effect.retry = "none";
21543
+ }
21544
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21545
+ if (maxAttemptsExpr !== undefined) {
21546
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21547
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
21548
+ ctx.diagnostics.push({
21549
+ severity: "error",
21550
+ code: "invalid-effect-retry",
21551
+ errorCode: "SC3031",
21552
+ docsUrl: "https://supacloud.dev/errors/SC3031",
21553
+ file,
21554
+ line: lineOf(maxAttemptsExpr),
21555
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
21556
+ });
21557
+ } else {
21558
+ effect.maxAttempts = maxAttempts;
21559
+ }
21560
+ }
21561
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21537
21562
  ctx.diagnostics.push({
21538
21563
  severity: "error",
21539
21564
  code: "invalid-effect-retry",
21540
21565
  errorCode: "SC3031",
21541
21566
  docsUrl: "https://supacloud.dev/errors/SC3031",
21542
21567
  file,
21543
- line: lineOf(maxAttemptsExpr),
21544
- message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
21568
+ line: lineOf(effectExpr),
21569
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21545
21570
  });
21546
- } else {
21547
- effect.maxAttempts = maxAttempts;
21548
21571
  }
21549
- }
21550
- if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21551
- ctx.diagnostics.push({
21552
- severity: "error",
21553
- code: "invalid-effect-retry",
21554
- errorCode: "SC3031",
21555
- docsUrl: "https://supacloud.dev/errors/SC3031",
21556
- file,
21557
- line: lineOf(effectExpr),
21558
- message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21559
- });
21560
- }
21561
- const timeoutExpr = getProp(effectObject, "timeoutMs");
21562
- if (timeoutExpr !== undefined) {
21563
- const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21564
- if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21565
- ctx.diagnostics.push({
21566
- severity: "error",
21567
- code: "invalid-effect-timeout",
21568
- errorCode: "SC3032",
21569
- docsUrl: "https://supacloud.dev/errors/SC3032",
21570
- file,
21571
- line: lineOf(timeoutExpr),
21572
- message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21573
- });
21574
- } else {
21575
- effect.timeoutMs = timeoutMs;
21572
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
21573
+ if (timeoutExpr !== undefined) {
21574
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21575
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21576
+ ctx.diagnostics.push({
21577
+ severity: "error",
21578
+ code: "invalid-effect-timeout",
21579
+ errorCode: "SC3032",
21580
+ docsUrl: "https://supacloud.dev/errors/SC3032",
21581
+ file,
21582
+ line: lineOf(timeoutExpr),
21583
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21584
+ });
21585
+ } else {
21586
+ effect.timeoutMs = timeoutMs;
21587
+ }
21576
21588
  }
21577
- }
21578
- const dependenciesExpr = getProp(effectObject, "dependencies");
21579
- if (dependenciesExpr !== undefined) {
21580
- const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21581
- if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21582
- ctx.diagnostics.push({
21583
- severity: "error",
21584
- code: "invalid-effect-dependencies",
21585
- errorCode: "SC3033",
21586
- docsUrl: "https://supacloud.dev/errors/SC3033",
21587
- file,
21588
- line: lineOf(dependenciesExpr),
21589
- message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21590
- });
21591
- } else {
21592
- effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21593
- if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21589
+ const dependenciesExpr = getProp(effectObject, "dependencies");
21590
+ if (dependenciesExpr !== undefined) {
21591
+ const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21592
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21594
21593
  ctx.diagnostics.push({
21595
21594
  severity: "error",
21596
21595
  code: "invalid-effect-dependencies",
@@ -21598,148 +21597,161 @@ async function parseController(input, ctx) {
21598
21597
  docsUrl: "https://supacloud.dev/errors/SC3033",
21599
21598
  file,
21600
21599
  line: lineOf(dependenciesExpr),
21601
- message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21602
- suggestion: "List each logical Effect dependency exactly once."
21600
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21603
21601
  });
21604
- }
21605
- }
21606
- }
21607
- const errorsExpr = getProp(effectObject, "errors");
21608
- if (errorsExpr !== undefined) {
21609
- const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21610
- if (!errorsArray) {
21611
- ctx.diagnostics.push({
21612
- severity: "error",
21613
- code: "invalid-effect-errors",
21614
- errorCode: "SC3034",
21615
- docsUrl: "https://supacloud.dev/errors/SC3034",
21616
- file,
21617
- line: lineOf(errorsExpr),
21618
- message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21619
- });
21620
- } else {
21621
- const errors = [];
21622
- for (const errorExpr of errorsArray.elements) {
21623
- if (!ts8.isObjectLiteralExpression(errorExpr)) {
21602
+ } else {
21603
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21604
+ if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21624
21605
  ctx.diagnostics.push({
21625
21606
  severity: "error",
21626
- code: "invalid-effect-errors",
21627
- errorCode: "SC3034",
21628
- docsUrl: "https://supacloud.dev/errors/SC3034",
21607
+ code: "invalid-effect-dependencies",
21608
+ errorCode: "SC3033",
21609
+ docsUrl: "https://supacloud.dev/errors/SC3033",
21629
21610
  file,
21630
- line: lineOf(errorExpr),
21631
- message: `Route ${route.handler} effect.errors entries must be object literals.`
21611
+ line: lineOf(dependenciesExpr),
21612
+ message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21613
+ suggestion: "List each logical Effect dependency exactly once."
21632
21614
  });
21633
- continue;
21634
21615
  }
21635
- const tag = stringLiteralProp(errorExpr, "tag");
21636
- const code = stringLiteralProp(errorExpr, "code");
21637
- const status = numberLiteralProp(errorExpr, "status");
21638
- const message = stringLiteralProp(errorExpr, "message");
21639
- const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21640
- const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21641
- const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21642
- const retryable = booleanLiteralProp(errorExpr, "retryable");
21643
- const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21644
- if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21645
- ctx.diagnostics.push({
21646
- severity: "error",
21647
- code: "invalid-effect-errors",
21648
- errorCode: "SC3034",
21649
- docsUrl: "https://supacloud.dev/errors/SC3034",
21650
- file,
21651
- line: lineOf(errorExpr),
21652
- message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21616
+ }
21617
+ }
21618
+ const errorsExpr = getProp(effectObject, "errors");
21619
+ if (errorsExpr !== undefined) {
21620
+ const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21621
+ if (!errorsArray) {
21622
+ ctx.diagnostics.push({
21623
+ severity: "error",
21624
+ code: "invalid-effect-errors",
21625
+ errorCode: "SC3034",
21626
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21627
+ file,
21628
+ line: lineOf(errorsExpr),
21629
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21630
+ });
21631
+ } else {
21632
+ const errors = [];
21633
+ for (const errorExpr of errorsArray.elements) {
21634
+ if (!ts8.isObjectLiteralExpression(errorExpr)) {
21635
+ ctx.diagnostics.push({
21636
+ severity: "error",
21637
+ code: "invalid-effect-errors",
21638
+ errorCode: "SC3034",
21639
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21640
+ file,
21641
+ line: lineOf(errorExpr),
21642
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
21643
+ });
21644
+ continue;
21645
+ }
21646
+ const tag = stringLiteralProp(errorExpr, "tag");
21647
+ const code = stringLiteralProp(errorExpr, "code");
21648
+ const status = numberLiteralProp(errorExpr, "status");
21649
+ const message = stringLiteralProp(errorExpr, "message");
21650
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21651
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21652
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21653
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21654
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21655
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21656
+ ctx.diagnostics.push({
21657
+ severity: "error",
21658
+ code: "invalid-effect-errors",
21659
+ errorCode: "SC3034",
21660
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21661
+ file,
21662
+ line: lineOf(errorExpr),
21663
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21664
+ });
21665
+ continue;
21666
+ }
21667
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21668
+ ctx.diagnostics.push({
21669
+ severity: "error",
21670
+ code: "invalid-effect-errors",
21671
+ errorCode: "SC3034",
21672
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21673
+ file,
21674
+ line: lineOf(errorExpr),
21675
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
21676
+ });
21677
+ continue;
21678
+ }
21679
+ errors.push({
21680
+ tag,
21681
+ code,
21682
+ status,
21683
+ ...message === undefined ? {} : { message },
21684
+ ...codeFromError === undefined ? {} : { codeFromError },
21685
+ ...messageFromError === undefined ? {} : { messageFromError },
21686
+ ...statusFromError === undefined ? {} : { statusFromError },
21687
+ ...retryable === undefined ? {} : { retryable },
21688
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21653
21689
  });
21654
- continue;
21655
21690
  }
21656
- if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21691
+ effect.errors = errors;
21692
+ if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21657
21693
  ctx.diagnostics.push({
21658
21694
  severity: "error",
21659
21695
  code: "invalid-effect-errors",
21660
21696
  errorCode: "SC3034",
21661
21697
  docsUrl: "https://supacloud.dev/errors/SC3034",
21662
21698
  file,
21663
- line: lineOf(errorExpr),
21664
- message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
21699
+ line: lineOf(errorsExpr),
21700
+ message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21701
+ suggestion: "Map each tagged failure exactly once."
21665
21702
  });
21666
- continue;
21667
21703
  }
21668
- errors.push({
21669
- tag,
21670
- code,
21671
- status,
21672
- ...message === undefined ? {} : { message },
21673
- ...codeFromError === undefined ? {} : { codeFromError },
21674
- ...messageFromError === undefined ? {} : { messageFromError },
21675
- ...statusFromError === undefined ? {} : { statusFromError },
21676
- ...retryable === undefined ? {} : { retryable },
21677
- ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21678
- });
21679
- }
21680
- effect.errors = errors;
21681
- if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21682
- ctx.diagnostics.push({
21683
- severity: "error",
21684
- code: "invalid-effect-errors",
21685
- errorCode: "SC3034",
21686
- docsUrl: "https://supacloud.dev/errors/SC3034",
21687
- file,
21688
- line: lineOf(errorsExpr),
21689
- message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21690
- suggestion: "Map each tagged failure exactly once."
21691
- });
21692
21704
  }
21693
21705
  }
21694
- }
21695
- route.effect = effect;
21696
- const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21697
- const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21698
- if (promiseReturn) {
21699
- pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
21700
- } else if (!effectArguments) {
21701
- ctx.diagnostics.push({
21702
- severity: "error",
21703
- code: "effect-return-type",
21704
- errorCode: "SC3035",
21705
- docsUrl: "https://supacloud.dev/errors/SC3035",
21706
- file,
21707
- line: lineOf(method),
21708
- message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21709
- suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21710
- });
21711
- }
21712
- if (effectArguments) {
21713
- const [, failureType, environmentType] = effectArguments;
21714
- const failure = await effectFailureTags(failureType, method, ctx);
21715
- if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21716
- pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
21717
- } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21718
- const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21719
- const typedTags = sortedUnique(failure.tags);
21720
- if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21721
- pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
21722
- }
21706
+ route.effect = effect;
21707
+ const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21708
+ const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21709
+ if (promiseReturn) {
21710
+ pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
21711
+ } else if (!effectArguments) {
21712
+ ctx.diagnostics.push({
21713
+ severity: "error",
21714
+ code: "effect-return-type",
21715
+ errorCode: "SC3035",
21716
+ docsUrl: "https://supacloud.dev/errors/SC3035",
21717
+ file,
21718
+ line: lineOf(method),
21719
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21720
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21721
+ });
21723
21722
  }
21724
- const environment = await effectEnvironmentNames(environmentType, method, ctx);
21725
- if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21726
- if (environment.invalid) {
21727
- pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
21728
- } else {
21729
- const declaredDependencies = sortedUnique(effect.dependencies);
21730
- const typedDependencies = sortedUnique(environment.names);
21731
- if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21732
- pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
21723
+ if (effectArguments) {
21724
+ const [, failureType, environmentType] = effectArguments;
21725
+ const failure = await effectFailureTags(failureType, method, ctx);
21726
+ if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21727
+ pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
21728
+ } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21729
+ const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21730
+ const typedTags = sortedUnique(failure.tags);
21731
+ if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21732
+ pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
21733
21733
  }
21734
21734
  }
21735
+ const environment = await effectEnvironmentNames(environmentType, method, ctx);
21736
+ if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21737
+ if (environment.invalid) {
21738
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
21739
+ } else {
21740
+ const declaredDependencies = sortedUnique(effect.dependencies);
21741
+ const typedDependencies = sortedUnique(environment.names);
21742
+ if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21743
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
21744
+ }
21745
+ }
21746
+ }
21747
+ if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21748
+ pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
21749
+ }
21735
21750
  }
21736
- if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21737
- pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
21738
- }
21739
- }
21740
- if (ctx.effectOptions.forbidDirectThrows && method.body) {
21741
- for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21742
- pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
21751
+ if (ctx.effectOptions.forbidDirectThrows && method.body) {
21752
+ for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21753
+ pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
21754
+ }
21743
21755
  }
21744
21756
  }
21745
21757
  }
@@ -23373,6 +23385,14 @@ function validateEffectPolicies(graph, options = {}) {
23373
23385
  for (const controller of module.controllers) {
23374
23386
  for (const route of controller.routes) {
23375
23387
  const label = routeLabel(route, controller.path);
23388
+ if (route.effectMode === "simple") {
23389
+ const featureTransition = featureTransitionForRoute(module, controller, route);
23390
+ const commandRef = route.command ?? featureTransition?.command;
23391
+ if (commandRef !== undefined || !["GET", "HEAD", "OPTIONS"].includes(route.method)) {
23392
+ diagnostics.push(diagnostic("effect-contract-required", commandRef === undefined ? `Route ${label} cannot use effect: false for a write operation.` : `Route ${label} cannot use effect: false when it enters command ${commandRef}.`, controller.file, "Keep Effect enabled for writes and durable commands; effect: false is only for simple read routes."));
23393
+ }
23394
+ continue;
23395
+ }
23376
23396
  if (options.requireRouteEffects && route.effect === undefined) {
23377
23397
  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.'));
23378
23398
  continue;
@@ -26430,10 +26450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
26430
26450
  import { resolve as resolve19 } from "node:path";
26431
26451
 
26432
26452
  // src/migration-app-version.ts
26433
- var MIGRATION_APP_VERSION = "0.38.0";
26453
+ var MIGRATION_APP_VERSION = "0.39.0";
26434
26454
 
26435
26455
  // src/migration-elysia-version.ts
26436
- var MIGRATION_ELYSIA_VERSION = "0.40.0";
26456
+ var MIGRATION_ELYSIA_VERSION = "0.41.0";
26437
26457
 
26438
26458
  // src/migration-policy.ts
26439
26459
  var cachedCompilerVersion;
@@ -3,6 +3,8 @@ import type { Diagnostic } from "./types";
3
3
  interface GovernedClass {
4
4
  declaration: ts.ClassDeclaration;
5
5
  name: string;
6
+ /** Declared durable persistence boundary (the database RPC that owns receipt, audit and transaction). */
7
+ rpc?: string;
6
8
  }
7
9
  /** A conservative source check for typed command references, not a JavaScript sandbox. */
8
10
  export declare function scanCommandBoundaries(source: ts.SourceFile, file: string, checker: ts.TypeChecker, commands: readonly GovernedClass[], boundRoutes: ReadonlyMap<ts.MethodDeclaration, string>): Promise<Diagnostic[]>;
package/dist/index.js CHANGED
@@ -381,6 +381,7 @@ var init_di_resolution = __esm(() => {
381
381
  "conflicting-injection-flags",
382
382
  "cross-module-scoped-dependency",
383
383
  "mixed-multi-provider-scopes",
384
+ "effect-contract-required",
384
385
  "dynamic-aspect-reference",
385
386
  "invalid-aspect-reference",
386
387
  "aspect-operation-mismatch"
@@ -2452,6 +2453,7 @@ function renderApplication(graph, options) {
2452
2453
  executor: route.command === undefined ? "route" : "command",
2453
2454
  ...command === undefined ? {} : { commandBoundary: route.command === undefined ? "handler" : "route" },
2454
2455
  ...route.effect === undefined ? {} : { effect: route.effect },
2456
+ ...route.effectMode === undefined ? {} : { effectMode: route.effectMode },
2455
2457
  invoker: isPipeline ? "pipeline" : "direct",
2456
2458
  stages: operationPlans.find((plan) => plan.module === module.name && plan.kind === "route" && plan.name === `${route.method} ${joinRoutePaths(controller.path, route.path)}`).stages,
2457
2459
  ...commandRef === undefined ? {} : { command: command?.name ?? commandRef },
@@ -2831,6 +2833,8 @@ class ModuleGenerator {
2831
2833
  if (route.effect !== undefined) {
2832
2834
  fields.push(`effect: ${JSON.stringify(route.effect)}`);
2833
2835
  }
2836
+ if (route.effectMode !== undefined)
2837
+ fields.push(`effectMode: ${JSON.stringify(route.effectMode)}`);
2834
2838
  if (idempotency !== undefined) {
2835
2839
  fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
2836
2840
  }
@@ -4680,6 +4684,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4680
4684
  maxAttempts?: number;
4681
4685
  timeoutMs?: number;
4682
4686
  };
4687
+ /** Simple route profile; no Effect interpreter or error mapping is required. */
4688
+ effectMode?: "simple";
4683
4689
  /** Compile-time ownership and transport classification for the route boundary. */
4684
4690
  contract?: {
4685
4691
  body?: "framework" | "domain";
@@ -19145,6 +19151,8 @@ async function scanCommandBoundaries(source, file, checker, commands, boundRoute
19145
19151
  return false;
19146
19152
  if (!ts4.isCallExpression(node.parent) || node.parent.expression !== node)
19147
19153
  return false;
19154
+ if (command.rpc !== undefined && propertyName(method.name) === "execute")
19155
+ return true;
19148
19156
  for (let owner = node.parent.parent;owner; owner = owner.parent) {
19149
19157
  if (ts4.isMethodDeclaration(owner)) {
19150
19158
  if (owner.parent === method.parent && (ts4.isPropertyAccessExpression(node) || ts4.isElementAccessExpression(node)) && node.expression.kind === ts4.SyntaxKind.ThisKeyword)
@@ -19910,7 +19918,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19910
19918
  }
19911
19919
  const governedClasses = modules.flatMap((module) => module.commands.flatMap((command) => {
19912
19920
  const declaration = ctx.classesByName.get(command.className)?.decl;
19913
- return declaration ? [{ declaration, name: command.name }] : [];
19921
+ return declaration ? [{ declaration, name: command.name, ...command.rpc === undefined ? {} : { rpc: command.rpc } }] : [];
19914
19922
  }));
19915
19923
  const boundRoutes = new Map;
19916
19924
  for (const module of modules)
@@ -21445,111 +21453,102 @@ async function parseController(input, ctx) {
21445
21453
  }
21446
21454
  const effectExpr = getProp(optionsObject, "effect");
21447
21455
  if (effectExpr) {
21448
- const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21449
- if (!effectObject) {
21450
- ctx.diagnostics.push({
21451
- severity: "error",
21452
- code: "invalid-effect-contract",
21453
- errorCode: "SC3030",
21454
- docsUrl: "https://supacloud.dev/errors/SC3030",
21455
- file,
21456
- line: lineOf(effectExpr),
21457
- message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21458
- suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21459
- });
21456
+ if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
21457
+ route.effectMode = "simple";
21460
21458
  } else {
21461
- const effect = { required: true };
21462
- const requiredExpr = getProp(effectObject, "required");
21463
- if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21459
+ const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21460
+ if (!effectObject) {
21464
21461
  ctx.diagnostics.push({
21465
21462
  severity: "error",
21466
21463
  code: "invalid-effect-contract",
21467
21464
  errorCode: "SC3030",
21468
21465
  docsUrl: "https://supacloud.dev/errors/SC3030",
21469
21466
  file,
21470
- line: lineOf(requiredExpr),
21471
- message: `Route ${route.handler} effect.required must be the literal true.`
21467
+ line: lineOf(effectExpr),
21468
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21469
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21472
21470
  });
21473
- }
21474
- const retry = stringLiteralProp(effectObject, "retry");
21475
- if (retry !== undefined) {
21476
- if (retry !== "none" && retry !== "explicit") {
21471
+ } else {
21472
+ const effect = { required: true };
21473
+ const requiredExpr = getProp(effectObject, "required");
21474
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21477
21475
  ctx.diagnostics.push({
21478
21476
  severity: "error",
21479
- code: "invalid-effect-retry",
21480
- errorCode: "SC3031",
21481
- docsUrl: "https://supacloud.dev/errors/SC3031",
21477
+ code: "invalid-effect-contract",
21478
+ errorCode: "SC3030",
21479
+ docsUrl: "https://supacloud.dev/errors/SC3030",
21482
21480
  file,
21483
- line: lineOf(getProp(effectObject, "retry")),
21484
- message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
21481
+ line: lineOf(requiredExpr),
21482
+ message: `Route ${route.handler} effect.required must be the literal true.`
21485
21483
  });
21486
- } else {
21487
- effect.retry = retry;
21488
21484
  }
21489
- } else if (ctx.effectOptions.requireRetryPolicy === false) {
21490
- effect.retry = "none";
21491
- }
21492
- const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21493
- if (maxAttemptsExpr !== undefined) {
21494
- const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21495
- if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
21485
+ const retry = stringLiteralProp(effectObject, "retry");
21486
+ if (retry !== undefined) {
21487
+ if (retry !== "none" && retry !== "explicit") {
21488
+ ctx.diagnostics.push({
21489
+ severity: "error",
21490
+ code: "invalid-effect-retry",
21491
+ errorCode: "SC3031",
21492
+ docsUrl: "https://supacloud.dev/errors/SC3031",
21493
+ file,
21494
+ line: lineOf(getProp(effectObject, "retry")),
21495
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
21496
+ });
21497
+ } else {
21498
+ effect.retry = retry;
21499
+ }
21500
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
21501
+ effect.retry = "none";
21502
+ }
21503
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21504
+ if (maxAttemptsExpr !== undefined) {
21505
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21506
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
21507
+ ctx.diagnostics.push({
21508
+ severity: "error",
21509
+ code: "invalid-effect-retry",
21510
+ errorCode: "SC3031",
21511
+ docsUrl: "https://supacloud.dev/errors/SC3031",
21512
+ file,
21513
+ line: lineOf(maxAttemptsExpr),
21514
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
21515
+ });
21516
+ } else {
21517
+ effect.maxAttempts = maxAttempts;
21518
+ }
21519
+ }
21520
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21496
21521
  ctx.diagnostics.push({
21497
21522
  severity: "error",
21498
21523
  code: "invalid-effect-retry",
21499
21524
  errorCode: "SC3031",
21500
21525
  docsUrl: "https://supacloud.dev/errors/SC3031",
21501
21526
  file,
21502
- line: lineOf(maxAttemptsExpr),
21503
- message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
21527
+ line: lineOf(effectExpr),
21528
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21504
21529
  });
21505
- } else {
21506
- effect.maxAttempts = maxAttempts;
21507
21530
  }
21508
- }
21509
- if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21510
- ctx.diagnostics.push({
21511
- severity: "error",
21512
- code: "invalid-effect-retry",
21513
- errorCode: "SC3031",
21514
- docsUrl: "https://supacloud.dev/errors/SC3031",
21515
- file,
21516
- line: lineOf(effectExpr),
21517
- message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21518
- });
21519
- }
21520
- const timeoutExpr = getProp(effectObject, "timeoutMs");
21521
- if (timeoutExpr !== undefined) {
21522
- const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21523
- if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21524
- ctx.diagnostics.push({
21525
- severity: "error",
21526
- code: "invalid-effect-timeout",
21527
- errorCode: "SC3032",
21528
- docsUrl: "https://supacloud.dev/errors/SC3032",
21529
- file,
21530
- line: lineOf(timeoutExpr),
21531
- message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21532
- });
21533
- } else {
21534
- effect.timeoutMs = timeoutMs;
21531
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
21532
+ if (timeoutExpr !== undefined) {
21533
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21534
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21535
+ ctx.diagnostics.push({
21536
+ severity: "error",
21537
+ code: "invalid-effect-timeout",
21538
+ errorCode: "SC3032",
21539
+ docsUrl: "https://supacloud.dev/errors/SC3032",
21540
+ file,
21541
+ line: lineOf(timeoutExpr),
21542
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21543
+ });
21544
+ } else {
21545
+ effect.timeoutMs = timeoutMs;
21546
+ }
21535
21547
  }
21536
- }
21537
- const dependenciesExpr = getProp(effectObject, "dependencies");
21538
- if (dependenciesExpr !== undefined) {
21539
- const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21540
- if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21541
- ctx.diagnostics.push({
21542
- severity: "error",
21543
- code: "invalid-effect-dependencies",
21544
- errorCode: "SC3033",
21545
- docsUrl: "https://supacloud.dev/errors/SC3033",
21546
- file,
21547
- line: lineOf(dependenciesExpr),
21548
- message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21549
- });
21550
- } else {
21551
- effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21552
- if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21548
+ const dependenciesExpr = getProp(effectObject, "dependencies");
21549
+ if (dependenciesExpr !== undefined) {
21550
+ const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21551
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21553
21552
  ctx.diagnostics.push({
21554
21553
  severity: "error",
21555
21554
  code: "invalid-effect-dependencies",
@@ -21557,148 +21556,161 @@ async function parseController(input, ctx) {
21557
21556
  docsUrl: "https://supacloud.dev/errors/SC3033",
21558
21557
  file,
21559
21558
  line: lineOf(dependenciesExpr),
21560
- message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21561
- suggestion: "List each logical Effect dependency exactly once."
21559
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21562
21560
  });
21563
- }
21564
- }
21565
- }
21566
- const errorsExpr = getProp(effectObject, "errors");
21567
- if (errorsExpr !== undefined) {
21568
- const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21569
- if (!errorsArray) {
21570
- ctx.diagnostics.push({
21571
- severity: "error",
21572
- code: "invalid-effect-errors",
21573
- errorCode: "SC3034",
21574
- docsUrl: "https://supacloud.dev/errors/SC3034",
21575
- file,
21576
- line: lineOf(errorsExpr),
21577
- message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21578
- });
21579
- } else {
21580
- const errors = [];
21581
- for (const errorExpr of errorsArray.elements) {
21582
- if (!ts8.isObjectLiteralExpression(errorExpr)) {
21561
+ } else {
21562
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21563
+ if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21583
21564
  ctx.diagnostics.push({
21584
21565
  severity: "error",
21585
- code: "invalid-effect-errors",
21586
- errorCode: "SC3034",
21587
- docsUrl: "https://supacloud.dev/errors/SC3034",
21566
+ code: "invalid-effect-dependencies",
21567
+ errorCode: "SC3033",
21568
+ docsUrl: "https://supacloud.dev/errors/SC3033",
21588
21569
  file,
21589
- line: lineOf(errorExpr),
21590
- message: `Route ${route.handler} effect.errors entries must be object literals.`
21570
+ line: lineOf(dependenciesExpr),
21571
+ message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21572
+ suggestion: "List each logical Effect dependency exactly once."
21591
21573
  });
21592
- continue;
21593
21574
  }
21594
- const tag = stringLiteralProp(errorExpr, "tag");
21595
- const code = stringLiteralProp(errorExpr, "code");
21596
- const status = numberLiteralProp(errorExpr, "status");
21597
- const message = stringLiteralProp(errorExpr, "message");
21598
- const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21599
- const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21600
- const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21601
- const retryable = booleanLiteralProp(errorExpr, "retryable");
21602
- const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21603
- if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21604
- ctx.diagnostics.push({
21605
- severity: "error",
21606
- code: "invalid-effect-errors",
21607
- errorCode: "SC3034",
21608
- docsUrl: "https://supacloud.dev/errors/SC3034",
21609
- file,
21610
- line: lineOf(errorExpr),
21611
- message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21575
+ }
21576
+ }
21577
+ const errorsExpr = getProp(effectObject, "errors");
21578
+ if (errorsExpr !== undefined) {
21579
+ const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21580
+ if (!errorsArray) {
21581
+ ctx.diagnostics.push({
21582
+ severity: "error",
21583
+ code: "invalid-effect-errors",
21584
+ errorCode: "SC3034",
21585
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21586
+ file,
21587
+ line: lineOf(errorsExpr),
21588
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21589
+ });
21590
+ } else {
21591
+ const errors = [];
21592
+ for (const errorExpr of errorsArray.elements) {
21593
+ if (!ts8.isObjectLiteralExpression(errorExpr)) {
21594
+ ctx.diagnostics.push({
21595
+ severity: "error",
21596
+ code: "invalid-effect-errors",
21597
+ errorCode: "SC3034",
21598
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21599
+ file,
21600
+ line: lineOf(errorExpr),
21601
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
21602
+ });
21603
+ continue;
21604
+ }
21605
+ const tag = stringLiteralProp(errorExpr, "tag");
21606
+ const code = stringLiteralProp(errorExpr, "code");
21607
+ const status = numberLiteralProp(errorExpr, "status");
21608
+ const message = stringLiteralProp(errorExpr, "message");
21609
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21610
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21611
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21612
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21613
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21614
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21615
+ ctx.diagnostics.push({
21616
+ severity: "error",
21617
+ code: "invalid-effect-errors",
21618
+ errorCode: "SC3034",
21619
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21620
+ file,
21621
+ line: lineOf(errorExpr),
21622
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21623
+ });
21624
+ continue;
21625
+ }
21626
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21627
+ ctx.diagnostics.push({
21628
+ severity: "error",
21629
+ code: "invalid-effect-errors",
21630
+ errorCode: "SC3034",
21631
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21632
+ file,
21633
+ line: lineOf(errorExpr),
21634
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
21635
+ });
21636
+ continue;
21637
+ }
21638
+ errors.push({
21639
+ tag,
21640
+ code,
21641
+ status,
21642
+ ...message === undefined ? {} : { message },
21643
+ ...codeFromError === undefined ? {} : { codeFromError },
21644
+ ...messageFromError === undefined ? {} : { messageFromError },
21645
+ ...statusFromError === undefined ? {} : { statusFromError },
21646
+ ...retryable === undefined ? {} : { retryable },
21647
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21612
21648
  });
21613
- continue;
21614
21649
  }
21615
- if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21650
+ effect.errors = errors;
21651
+ if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21616
21652
  ctx.diagnostics.push({
21617
21653
  severity: "error",
21618
21654
  code: "invalid-effect-errors",
21619
21655
  errorCode: "SC3034",
21620
21656
  docsUrl: "https://supacloud.dev/errors/SC3034",
21621
21657
  file,
21622
- line: lineOf(errorExpr),
21623
- message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
21658
+ line: lineOf(errorsExpr),
21659
+ message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21660
+ suggestion: "Map each tagged failure exactly once."
21624
21661
  });
21625
- continue;
21626
21662
  }
21627
- errors.push({
21628
- tag,
21629
- code,
21630
- status,
21631
- ...message === undefined ? {} : { message },
21632
- ...codeFromError === undefined ? {} : { codeFromError },
21633
- ...messageFromError === undefined ? {} : { messageFromError },
21634
- ...statusFromError === undefined ? {} : { statusFromError },
21635
- ...retryable === undefined ? {} : { retryable },
21636
- ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21637
- });
21638
- }
21639
- effect.errors = errors;
21640
- if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21641
- ctx.diagnostics.push({
21642
- severity: "error",
21643
- code: "invalid-effect-errors",
21644
- errorCode: "SC3034",
21645
- docsUrl: "https://supacloud.dev/errors/SC3034",
21646
- file,
21647
- line: lineOf(errorsExpr),
21648
- message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21649
- suggestion: "Map each tagged failure exactly once."
21650
- });
21651
21663
  }
21652
21664
  }
21653
- }
21654
- route.effect = effect;
21655
- const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21656
- const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21657
- if (promiseReturn) {
21658
- pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
21659
- } else if (!effectArguments) {
21660
- ctx.diagnostics.push({
21661
- severity: "error",
21662
- code: "effect-return-type",
21663
- errorCode: "SC3035",
21664
- docsUrl: "https://supacloud.dev/errors/SC3035",
21665
- file,
21666
- line: lineOf(method),
21667
- message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21668
- suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21669
- });
21670
- }
21671
- if (effectArguments) {
21672
- const [, failureType, environmentType] = effectArguments;
21673
- const failure = await effectFailureTags(failureType, method, ctx);
21674
- if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21675
- pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
21676
- } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21677
- const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21678
- const typedTags = sortedUnique(failure.tags);
21679
- if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21680
- pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
21681
- }
21665
+ route.effect = effect;
21666
+ const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21667
+ const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21668
+ if (promiseReturn) {
21669
+ pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
21670
+ } else if (!effectArguments) {
21671
+ ctx.diagnostics.push({
21672
+ severity: "error",
21673
+ code: "effect-return-type",
21674
+ errorCode: "SC3035",
21675
+ docsUrl: "https://supacloud.dev/errors/SC3035",
21676
+ file,
21677
+ line: lineOf(method),
21678
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21679
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21680
+ });
21682
21681
  }
21683
- const environment = await effectEnvironmentNames(environmentType, method, ctx);
21684
- if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21685
- if (environment.invalid) {
21686
- pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
21687
- } else {
21688
- const declaredDependencies = sortedUnique(effect.dependencies);
21689
- const typedDependencies = sortedUnique(environment.names);
21690
- if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21691
- pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
21682
+ if (effectArguments) {
21683
+ const [, failureType, environmentType] = effectArguments;
21684
+ const failure = await effectFailureTags(failureType, method, ctx);
21685
+ if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21686
+ pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
21687
+ } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21688
+ const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21689
+ const typedTags = sortedUnique(failure.tags);
21690
+ if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21691
+ pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
21692
21692
  }
21693
21693
  }
21694
+ const environment = await effectEnvironmentNames(environmentType, method, ctx);
21695
+ if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21696
+ if (environment.invalid) {
21697
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
21698
+ } else {
21699
+ const declaredDependencies = sortedUnique(effect.dependencies);
21700
+ const typedDependencies = sortedUnique(environment.names);
21701
+ if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21702
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
21703
+ }
21704
+ }
21705
+ }
21706
+ if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21707
+ pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
21708
+ }
21694
21709
  }
21695
- if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21696
- pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
21697
- }
21698
- }
21699
- if (ctx.effectOptions.forbidDirectThrows && method.body) {
21700
- for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21701
- pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
21710
+ if (ctx.effectOptions.forbidDirectThrows && method.body) {
21711
+ for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21712
+ pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
21713
+ }
21702
21714
  }
21703
21715
  }
21704
21716
  }
@@ -23334,6 +23346,14 @@ function validateEffectPolicies(graph, options = {}) {
23334
23346
  for (const controller of module.controllers) {
23335
23347
  for (const route of controller.routes) {
23336
23348
  const label = routeLabel(route, controller.path);
23349
+ if (route.effectMode === "simple") {
23350
+ const featureTransition = featureTransitionForRoute(module, controller, route);
23351
+ const commandRef = route.command ?? featureTransition?.command;
23352
+ if (commandRef !== undefined || !["GET", "HEAD", "OPTIONS"].includes(route.method)) {
23353
+ diagnostics.push(diagnostic("effect-contract-required", commandRef === undefined ? `Route ${label} cannot use effect: false for a write operation.` : `Route ${label} cannot use effect: false when it enters command ${commandRef}.`, controller.file, "Keep Effect enabled for writes and durable commands; effect: false is only for simple read routes."));
23354
+ }
23355
+ continue;
23356
+ }
23337
23357
  if (options.requireRouteEffects && route.effect === undefined) {
23338
23358
  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.'));
23339
23359
  continue;
@@ -25682,10 +25702,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25682
25702
  import { resolve as resolve17 } from "node:path";
25683
25703
 
25684
25704
  // src/migration-app-version.ts
25685
- var MIGRATION_APP_VERSION = "0.38.0";
25705
+ var MIGRATION_APP_VERSION = "0.39.0";
25686
25706
 
25687
25707
  // src/migration-elysia-version.ts
25688
- var MIGRATION_ELYSIA_VERSION = "0.40.0";
25708
+ var MIGRATION_ELYSIA_VERSION = "0.41.0";
25689
25709
 
25690
25710
  // src/migration-policy.ts
25691
25711
  var cachedCompilerVersion;
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.38.0";
1
+ export declare const MIGRATION_APP_VERSION = "0.39.0";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.40.0";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.41.0";
package/dist/types.d.ts CHANGED
@@ -253,6 +253,8 @@ export interface RouteNode {
253
253
  responses?: Record<string, string>;
254
254
  /** Static Effect execution contract for this route. */
255
255
  effect?: EffectRouteNode;
256
+ /** Simple route profile: handler may return a Promise/value without Effect governance. */
257
+ effectMode?: "simple";
256
258
  /** @Command-decorated class explicitly bound by the route. */
257
259
  command?: string;
258
260
  /** Route guards executed before handler (Angular CanActivateFn style). */
@@ -404,6 +406,7 @@ export interface ExecutionPlanRoute {
404
406
  invoker: "direct" | "pipeline";
405
407
  commandBoundary?: "route" | "handler";
406
408
  effect?: EffectRouteNode;
409
+ effectMode?: "simple";
407
410
  stages: string[];
408
411
  command?: string;
409
412
  guards: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.57.0",
3
+ "version": "0.58.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.21.0",
60
+ "@supacloud/db": "0.22.0",
61
61
  "@supacloud/delivery": "0.25.0",
62
62
  "typebox": "1.4.3",
63
63
  "@graphql-codegen/core": "^6.2.2",