@supacloud/compiler 0.57.1 → 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";
@@ -21488,111 +21494,102 @@ async function parseController(input, ctx) {
21488
21494
  }
21489
21495
  const effectExpr = getProp(optionsObject, "effect");
21490
21496
  if (effectExpr) {
21491
- const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21492
- if (!effectObject) {
21493
- ctx.diagnostics.push({
21494
- severity: "error",
21495
- code: "invalid-effect-contract",
21496
- errorCode: "SC3030",
21497
- docsUrl: "https://supacloud.dev/errors/SC3030",
21498
- file,
21499
- line: lineOf(effectExpr),
21500
- message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21501
- suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21502
- });
21497
+ if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
21498
+ route.effectMode = "simple";
21503
21499
  } else {
21504
- const effect = { required: true };
21505
- const requiredExpr = getProp(effectObject, "required");
21506
- if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21500
+ const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21501
+ if (!effectObject) {
21507
21502
  ctx.diagnostics.push({
21508
21503
  severity: "error",
21509
21504
  code: "invalid-effect-contract",
21510
21505
  errorCode: "SC3030",
21511
21506
  docsUrl: "https://supacloud.dev/errors/SC3030",
21512
21507
  file,
21513
- line: lineOf(requiredExpr),
21514
- 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" }.'
21515
21511
  });
21516
- }
21517
- const retry = stringLiteralProp(effectObject, "retry");
21518
- if (retry !== undefined) {
21519
- 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) {
21520
21516
  ctx.diagnostics.push({
21521
21517
  severity: "error",
21522
- code: "invalid-effect-retry",
21523
- errorCode: "SC3031",
21524
- docsUrl: "https://supacloud.dev/errors/SC3031",
21518
+ code: "invalid-effect-contract",
21519
+ errorCode: "SC3030",
21520
+ docsUrl: "https://supacloud.dev/errors/SC3030",
21525
21521
  file,
21526
- line: lineOf(getProp(effectObject, "retry")),
21527
- 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.`
21528
21524
  });
21529
- } else {
21530
- effect.retry = retry;
21531
21525
  }
21532
- } else if (ctx.effectOptions.requireRetryPolicy === false) {
21533
- effect.retry = "none";
21534
- }
21535
- const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21536
- if (maxAttemptsExpr !== undefined) {
21537
- const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21538
- 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) {
21539
21562
  ctx.diagnostics.push({
21540
21563
  severity: "error",
21541
21564
  code: "invalid-effect-retry",
21542
21565
  errorCode: "SC3031",
21543
21566
  docsUrl: "https://supacloud.dev/errors/SC3031",
21544
21567
  file,
21545
- line: lineOf(maxAttemptsExpr),
21546
- 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".`
21547
21570
  });
21548
- } else {
21549
- effect.maxAttempts = maxAttempts;
21550
21571
  }
21551
- }
21552
- if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21553
- ctx.diagnostics.push({
21554
- severity: "error",
21555
- code: "invalid-effect-retry",
21556
- errorCode: "SC3031",
21557
- docsUrl: "https://supacloud.dev/errors/SC3031",
21558
- file,
21559
- line: lineOf(effectExpr),
21560
- message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21561
- });
21562
- }
21563
- const timeoutExpr = getProp(effectObject, "timeoutMs");
21564
- if (timeoutExpr !== undefined) {
21565
- const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21566
- if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21567
- ctx.diagnostics.push({
21568
- severity: "error",
21569
- code: "invalid-effect-timeout",
21570
- errorCode: "SC3032",
21571
- docsUrl: "https://supacloud.dev/errors/SC3032",
21572
- file,
21573
- line: lineOf(timeoutExpr),
21574
- message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21575
- });
21576
- } else {
21577
- 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
+ }
21578
21588
  }
21579
- }
21580
- const dependenciesExpr = getProp(effectObject, "dependencies");
21581
- if (dependenciesExpr !== undefined) {
21582
- const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21583
- if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21584
- ctx.diagnostics.push({
21585
- severity: "error",
21586
- code: "invalid-effect-dependencies",
21587
- errorCode: "SC3033",
21588
- docsUrl: "https://supacloud.dev/errors/SC3033",
21589
- file,
21590
- line: lineOf(dependenciesExpr),
21591
- message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21592
- });
21593
- } else {
21594
- effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21595
- 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)) {
21596
21593
  ctx.diagnostics.push({
21597
21594
  severity: "error",
21598
21595
  code: "invalid-effect-dependencies",
@@ -21600,148 +21597,161 @@ async function parseController(input, ctx) {
21600
21597
  docsUrl: "https://supacloud.dev/errors/SC3033",
21601
21598
  file,
21602
21599
  line: lineOf(dependenciesExpr),
21603
- message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21604
- 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.`
21605
21601
  });
21606
- }
21607
- }
21608
- }
21609
- const errorsExpr = getProp(effectObject, "errors");
21610
- if (errorsExpr !== undefined) {
21611
- const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21612
- if (!errorsArray) {
21613
- ctx.diagnostics.push({
21614
- severity: "error",
21615
- code: "invalid-effect-errors",
21616
- errorCode: "SC3034",
21617
- docsUrl: "https://supacloud.dev/errors/SC3034",
21618
- file,
21619
- line: lineOf(errorsExpr),
21620
- message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21621
- });
21622
- } else {
21623
- const errors = [];
21624
- for (const errorExpr of errorsArray.elements) {
21625
- 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) {
21626
21605
  ctx.diagnostics.push({
21627
21606
  severity: "error",
21628
- code: "invalid-effect-errors",
21629
- errorCode: "SC3034",
21630
- docsUrl: "https://supacloud.dev/errors/SC3034",
21607
+ code: "invalid-effect-dependencies",
21608
+ errorCode: "SC3033",
21609
+ docsUrl: "https://supacloud.dev/errors/SC3033",
21631
21610
  file,
21632
- line: lineOf(errorExpr),
21633
- 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."
21634
21614
  });
21635
- continue;
21636
21615
  }
21637
- const tag = stringLiteralProp(errorExpr, "tag");
21638
- const code = stringLiteralProp(errorExpr, "code");
21639
- const status = numberLiteralProp(errorExpr, "status");
21640
- const message = stringLiteralProp(errorExpr, "message");
21641
- const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21642
- const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21643
- const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21644
- const retryable = booleanLiteralProp(errorExpr, "retryable");
21645
- const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21646
- if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21647
- ctx.diagnostics.push({
21648
- severity: "error",
21649
- code: "invalid-effect-errors",
21650
- errorCode: "SC3034",
21651
- docsUrl: "https://supacloud.dev/errors/SC3034",
21652
- file,
21653
- line: lineOf(errorExpr),
21654
- 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 }
21655
21689
  });
21656
- continue;
21657
21690
  }
21658
- 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) {
21659
21693
  ctx.diagnostics.push({
21660
21694
  severity: "error",
21661
21695
  code: "invalid-effect-errors",
21662
21696
  errorCode: "SC3034",
21663
21697
  docsUrl: "https://supacloud.dev/errors/SC3034",
21664
21698
  file,
21665
- line: lineOf(errorExpr),
21666
- 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."
21667
21702
  });
21668
- continue;
21669
21703
  }
21670
- errors.push({
21671
- tag,
21672
- code,
21673
- status,
21674
- ...message === undefined ? {} : { message },
21675
- ...codeFromError === undefined ? {} : { codeFromError },
21676
- ...messageFromError === undefined ? {} : { messageFromError },
21677
- ...statusFromError === undefined ? {} : { statusFromError },
21678
- ...retryable === undefined ? {} : { retryable },
21679
- ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21680
- });
21681
- }
21682
- effect.errors = errors;
21683
- if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21684
- ctx.diagnostics.push({
21685
- severity: "error",
21686
- code: "invalid-effect-errors",
21687
- errorCode: "SC3034",
21688
- docsUrl: "https://supacloud.dev/errors/SC3034",
21689
- file,
21690
- line: lineOf(errorsExpr),
21691
- message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21692
- suggestion: "Map each tagged failure exactly once."
21693
- });
21694
21704
  }
21695
21705
  }
21696
- }
21697
- route.effect = effect;
21698
- const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21699
- const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21700
- if (promiseReturn) {
21701
- 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.");
21702
- } else if (!effectArguments) {
21703
- ctx.diagnostics.push({
21704
- severity: "error",
21705
- code: "effect-return-type",
21706
- errorCode: "SC3035",
21707
- docsUrl: "https://supacloud.dev/errors/SC3035",
21708
- file,
21709
- line: lineOf(method),
21710
- message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21711
- suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21712
- });
21713
- }
21714
- if (effectArguments) {
21715
- const [, failureType, environmentType] = effectArguments;
21716
- const failure = await effectFailureTags(failureType, method, ctx);
21717
- if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21718
- 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.");
21719
- } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21720
- const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21721
- const typedTags = sortedUnique(failure.tags);
21722
- if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21723
- 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.");
21724
- }
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
+ });
21725
21722
  }
21726
- const environment = await effectEnvironmentNames(environmentType, method, ctx);
21727
- if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21728
- if (environment.invalid) {
21729
- 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.");
21730
- } else {
21731
- const declaredDependencies = sortedUnique(effect.dependencies);
21732
- const typedDependencies = sortedUnique(environment.names);
21733
- if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21734
- 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.");
21735
21733
  }
21736
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
+ }
21737
21750
  }
21738
- if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21739
- 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.");
21740
- }
21741
- }
21742
- if (ctx.effectOptions.forbidDirectThrows && method.body) {
21743
- for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21744
- 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
+ }
21745
21755
  }
21746
21756
  }
21747
21757
  }
@@ -23375,6 +23385,14 @@ function validateEffectPolicies(graph, options = {}) {
23375
23385
  for (const controller of module.controllers) {
23376
23386
  for (const route of controller.routes) {
23377
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
+ }
23378
23396
  if (options.requireRouteEffects && route.effect === undefined) {
23379
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.'));
23380
23398
  continue;
@@ -26432,10 +26450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
26432
26450
  import { resolve as resolve19 } from "node:path";
26433
26451
 
26434
26452
  // src/migration-app-version.ts
26435
- var MIGRATION_APP_VERSION = "0.38.0";
26453
+ var MIGRATION_APP_VERSION = "0.39.0";
26436
26454
 
26437
26455
  // src/migration-elysia-version.ts
26438
- var MIGRATION_ELYSIA_VERSION = "0.40.0";
26456
+ var MIGRATION_ELYSIA_VERSION = "0.41.0";
26439
26457
 
26440
26458
  // src/migration-policy.ts
26441
26459
  var cachedCompilerVersion;
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";
@@ -21447,111 +21453,102 @@ async function parseController(input, ctx) {
21447
21453
  }
21448
21454
  const effectExpr = getProp(optionsObject, "effect");
21449
21455
  if (effectExpr) {
21450
- const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21451
- if (!effectObject) {
21452
- ctx.diagnostics.push({
21453
- severity: "error",
21454
- code: "invalid-effect-contract",
21455
- errorCode: "SC3030",
21456
- docsUrl: "https://supacloud.dev/errors/SC3030",
21457
- file,
21458
- line: lineOf(effectExpr),
21459
- message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
21460
- suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
21461
- });
21456
+ if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
21457
+ route.effectMode = "simple";
21462
21458
  } else {
21463
- const effect = { required: true };
21464
- const requiredExpr = getProp(effectObject, "required");
21465
- if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
21459
+ const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
21460
+ if (!effectObject) {
21466
21461
  ctx.diagnostics.push({
21467
21462
  severity: "error",
21468
21463
  code: "invalid-effect-contract",
21469
21464
  errorCode: "SC3030",
21470
21465
  docsUrl: "https://supacloud.dev/errors/SC3030",
21471
21466
  file,
21472
- line: lineOf(requiredExpr),
21473
- 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" }.'
21474
21470
  });
21475
- }
21476
- const retry = stringLiteralProp(effectObject, "retry");
21477
- if (retry !== undefined) {
21478
- 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) {
21479
21475
  ctx.diagnostics.push({
21480
21476
  severity: "error",
21481
- code: "invalid-effect-retry",
21482
- errorCode: "SC3031",
21483
- docsUrl: "https://supacloud.dev/errors/SC3031",
21477
+ code: "invalid-effect-contract",
21478
+ errorCode: "SC3030",
21479
+ docsUrl: "https://supacloud.dev/errors/SC3030",
21484
21480
  file,
21485
- line: lineOf(getProp(effectObject, "retry")),
21486
- 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.`
21487
21483
  });
21488
- } else {
21489
- effect.retry = retry;
21490
21484
  }
21491
- } else if (ctx.effectOptions.requireRetryPolicy === false) {
21492
- effect.retry = "none";
21493
- }
21494
- const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
21495
- if (maxAttemptsExpr !== undefined) {
21496
- const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
21497
- 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) {
21498
21521
  ctx.diagnostics.push({
21499
21522
  severity: "error",
21500
21523
  code: "invalid-effect-retry",
21501
21524
  errorCode: "SC3031",
21502
21525
  docsUrl: "https://supacloud.dev/errors/SC3031",
21503
21526
  file,
21504
- line: lineOf(maxAttemptsExpr),
21505
- 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".`
21506
21529
  });
21507
- } else {
21508
- effect.maxAttempts = maxAttempts;
21509
21530
  }
21510
- }
21511
- if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
21512
- ctx.diagnostics.push({
21513
- severity: "error",
21514
- code: "invalid-effect-retry",
21515
- errorCode: "SC3031",
21516
- docsUrl: "https://supacloud.dev/errors/SC3031",
21517
- file,
21518
- line: lineOf(effectExpr),
21519
- message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
21520
- });
21521
- }
21522
- const timeoutExpr = getProp(effectObject, "timeoutMs");
21523
- if (timeoutExpr !== undefined) {
21524
- const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
21525
- if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
21526
- ctx.diagnostics.push({
21527
- severity: "error",
21528
- code: "invalid-effect-timeout",
21529
- errorCode: "SC3032",
21530
- docsUrl: "https://supacloud.dev/errors/SC3032",
21531
- file,
21532
- line: lineOf(timeoutExpr),
21533
- message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
21534
- });
21535
- } else {
21536
- 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
+ }
21537
21547
  }
21538
- }
21539
- const dependenciesExpr = getProp(effectObject, "dependencies");
21540
- if (dependenciesExpr !== undefined) {
21541
- const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
21542
- if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
21543
- ctx.diagnostics.push({
21544
- severity: "error",
21545
- code: "invalid-effect-dependencies",
21546
- errorCode: "SC3033",
21547
- docsUrl: "https://supacloud.dev/errors/SC3033",
21548
- file,
21549
- line: lineOf(dependenciesExpr),
21550
- message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21551
- });
21552
- } else {
21553
- effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21554
- 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)) {
21555
21552
  ctx.diagnostics.push({
21556
21553
  severity: "error",
21557
21554
  code: "invalid-effect-dependencies",
@@ -21559,148 +21556,161 @@ async function parseController(input, ctx) {
21559
21556
  docsUrl: "https://supacloud.dev/errors/SC3033",
21560
21557
  file,
21561
21558
  line: lineOf(dependenciesExpr),
21562
- message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
21563
- 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.`
21564
21560
  });
21565
- }
21566
- }
21567
- }
21568
- const errorsExpr = getProp(effectObject, "errors");
21569
- if (errorsExpr !== undefined) {
21570
- const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
21571
- if (!errorsArray) {
21572
- ctx.diagnostics.push({
21573
- severity: "error",
21574
- code: "invalid-effect-errors",
21575
- errorCode: "SC3034",
21576
- docsUrl: "https://supacloud.dev/errors/SC3034",
21577
- file,
21578
- line: lineOf(errorsExpr),
21579
- message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21580
- });
21581
- } else {
21582
- const errors = [];
21583
- for (const errorExpr of errorsArray.elements) {
21584
- 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) {
21585
21564
  ctx.diagnostics.push({
21586
21565
  severity: "error",
21587
- code: "invalid-effect-errors",
21588
- errorCode: "SC3034",
21589
- docsUrl: "https://supacloud.dev/errors/SC3034",
21566
+ code: "invalid-effect-dependencies",
21567
+ errorCode: "SC3033",
21568
+ docsUrl: "https://supacloud.dev/errors/SC3033",
21590
21569
  file,
21591
- line: lineOf(errorExpr),
21592
- 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."
21593
21573
  });
21594
- continue;
21595
21574
  }
21596
- const tag = stringLiteralProp(errorExpr, "tag");
21597
- const code = stringLiteralProp(errorExpr, "code");
21598
- const status = numberLiteralProp(errorExpr, "status");
21599
- const message = stringLiteralProp(errorExpr, "message");
21600
- const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21601
- const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21602
- const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21603
- const retryable = booleanLiteralProp(errorExpr, "retryable");
21604
- const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21605
- if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21606
- ctx.diagnostics.push({
21607
- severity: "error",
21608
- code: "invalid-effect-errors",
21609
- errorCode: "SC3034",
21610
- docsUrl: "https://supacloud.dev/errors/SC3034",
21611
- file,
21612
- line: lineOf(errorExpr),
21613
- 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 }
21614
21648
  });
21615
- continue;
21616
21649
  }
21617
- 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) {
21618
21652
  ctx.diagnostics.push({
21619
21653
  severity: "error",
21620
21654
  code: "invalid-effect-errors",
21621
21655
  errorCode: "SC3034",
21622
21656
  docsUrl: "https://supacloud.dev/errors/SC3034",
21623
21657
  file,
21624
- line: lineOf(errorExpr),
21625
- 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."
21626
21661
  });
21627
- continue;
21628
21662
  }
21629
- errors.push({
21630
- tag,
21631
- code,
21632
- status,
21633
- ...message === undefined ? {} : { message },
21634
- ...codeFromError === undefined ? {} : { codeFromError },
21635
- ...messageFromError === undefined ? {} : { messageFromError },
21636
- ...statusFromError === undefined ? {} : { statusFromError },
21637
- ...retryable === undefined ? {} : { retryable },
21638
- ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21639
- });
21640
- }
21641
- effect.errors = errors;
21642
- if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
21643
- ctx.diagnostics.push({
21644
- severity: "error",
21645
- code: "invalid-effect-errors",
21646
- errorCode: "SC3034",
21647
- docsUrl: "https://supacloud.dev/errors/SC3034",
21648
- file,
21649
- line: lineOf(errorsExpr),
21650
- message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
21651
- suggestion: "Map each tagged failure exactly once."
21652
- });
21653
21663
  }
21654
21664
  }
21655
- }
21656
- route.effect = effect;
21657
- const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
21658
- const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
21659
- if (promiseReturn) {
21660
- 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.");
21661
- } else if (!effectArguments) {
21662
- ctx.diagnostics.push({
21663
- severity: "error",
21664
- code: "effect-return-type",
21665
- errorCode: "SC3035",
21666
- docsUrl: "https://supacloud.dev/errors/SC3035",
21667
- file,
21668
- line: lineOf(method),
21669
- message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
21670
- suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
21671
- });
21672
- }
21673
- if (effectArguments) {
21674
- const [, failureType, environmentType] = effectArguments;
21675
- const failure = await effectFailureTags(failureType, method, ctx);
21676
- if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
21677
- 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.");
21678
- } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
21679
- const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
21680
- const typedTags = sortedUnique(failure.tags);
21681
- if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
21682
- 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.");
21683
- }
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
+ });
21684
21681
  }
21685
- const environment = await effectEnvironmentNames(environmentType, method, ctx);
21686
- if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
21687
- if (environment.invalid) {
21688
- 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.");
21689
- } else {
21690
- const declaredDependencies = sortedUnique(effect.dependencies);
21691
- const typedDependencies = sortedUnique(environment.names);
21692
- if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
21693
- 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.");
21694
21692
  }
21695
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
+ }
21696
21709
  }
21697
- if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
21698
- 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.");
21699
- }
21700
- }
21701
- if (ctx.effectOptions.forbidDirectThrows && method.body) {
21702
- for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
21703
- 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
+ }
21704
21714
  }
21705
21715
  }
21706
21716
  }
@@ -23336,6 +23346,14 @@ function validateEffectPolicies(graph, options = {}) {
23336
23346
  for (const controller of module.controllers) {
23337
23347
  for (const route of controller.routes) {
23338
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
+ }
23339
23357
  if (options.requireRouteEffects && route.effect === undefined) {
23340
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.'));
23341
23359
  continue;
@@ -25684,10 +25702,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25684
25702
  import { resolve as resolve17 } from "node:path";
25685
25703
 
25686
25704
  // src/migration-app-version.ts
25687
- var MIGRATION_APP_VERSION = "0.38.0";
25705
+ var MIGRATION_APP_VERSION = "0.39.0";
25688
25706
 
25689
25707
  // src/migration-elysia-version.ts
25690
- var MIGRATION_ELYSIA_VERSION = "0.40.0";
25708
+ var MIGRATION_ELYSIA_VERSION = "0.41.0";
25691
25709
 
25692
25710
  // src/migration-policy.ts
25693
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.1",
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",