@supacloud/cli 0.72.0 → 0.74.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/index.js CHANGED
@@ -207715,7 +207715,7 @@ var ACTION_POLICY = {
207715
207715
  read: ["status", "logs"],
207716
207716
  write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire"]
207717
207717
  },
207718
- db: { local: ["lint", "explain"], read: ["module_check"] },
207718
+ db: { local: ["context", "lint", "explain"], read: ["module_check"] },
207719
207719
  dev: { read: ["status"], write: ["sync", "watch", "migrate"] }
207720
207720
  };
207721
207721
  function declaredMode(moduleName, action) {
@@ -221498,6 +221498,24 @@ var init_validate = __esm2(() => {
221498
221498
  "route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
221499
221499
  "route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
221500
221500
  "route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
221501
+ "invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
221502
+ "invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
221503
+ "invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
221504
+ "invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
221505
+ "invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
221506
+ "effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
221507
+ "effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
221508
+ "effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
221509
+ "effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
221510
+ "effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
221511
+ "effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
221512
+ "effect-error-type": { code: "SC3041", docsUrl: "https://supacloud.dev/errors/SC3041" },
221513
+ "effect-error-mapping-type": { code: "SC3042", docsUrl: "https://supacloud.dev/errors/SC3042" },
221514
+ "effect-dependency-mismatch": { code: "SC3043", docsUrl: "https://supacloud.dev/errors/SC3043" },
221515
+ "effect-timeout-required": { code: "SC3044", docsUrl: "https://supacloud.dev/errors/SC3044" },
221516
+ "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
221517
+ "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
221518
+ "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
221501
221519
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
221502
221520
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
221503
221521
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -222026,6 +222044,9 @@ class ModuleGenerator {
222026
222044
  });
222027
222045
  fields.push(`responses: { ${responseFields.join(", ")} }`);
222028
222046
  }
222047
+ if (route.effect !== undefined) {
222048
+ fields.push(`effect: ${JSON.stringify(route.effect)}`);
222049
+ }
222029
222050
  if (route.command)
222030
222051
  fields.push(`command: ${JSON.stringify(route.command)}`);
222031
222052
  if (route.guards && route.guards.length > 0) {
@@ -223838,6 +223859,14 @@ var INTERFACES = `export interface CompiledRoute {
223838
223859
  cookie?: unknown;
223839
223860
  response?: unknown;
223840
223861
  responses?: Record<string | number, unknown>;
223862
+ effect?: {
223863
+ required: true;
223864
+ errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
223865
+ dependencies?: string[];
223866
+ retry?: "none" | "explicit";
223867
+ maxAttempts?: number;
223868
+ timeoutMs?: number;
223869
+ };
223841
223870
  /** Compile-time ownership and transport classification for the route boundary. */
223842
223871
  contract?: {
223843
223872
  body?: "framework" | "domain";
@@ -244728,6 +244757,13 @@ function withDiagnosticMetadata(diagnostic) {
244728
244757
  const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
244729
244758
  return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
244730
244759
  }
244760
+ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
244761
+ requireTaggedErrorTypes: true,
244762
+ requireExactDependencyTypes: true,
244763
+ requireTimeoutForDependencies: true,
244764
+ forbidDirectRuntimeExecution: true,
244765
+ forbidDirectThrows: true
244766
+ };
244731
244767
  function nodeText(node) {
244732
244768
  return node.getText(node.getSourceFile());
244733
244769
  }
@@ -244773,7 +244809,7 @@ function descendantsOfKind(root, predicate) {
244773
244809
  visit(root);
244774
244810
  return result;
244775
244811
  }
244776
- async function analyzeProject(rootDir, include, cache, changedPaths) {
244812
+ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
244777
244813
  const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
244778
244814
  if (cache)
244779
244815
  cache.programSession = session;
@@ -244792,8 +244828,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
244792
244828
  moduleHandlerFiles: new Map,
244793
244829
  infraResources: new Map,
244794
244830
  infraResourceClasses: new Map,
244795
- diagnostics: []
244831
+ diagnostics: [],
244832
+ effectOptions: {
244833
+ ...DEFAULT_EFFECT_ANALYSIS_OPTIONS,
244834
+ requireTaggedErrorTypes: effectOptions.requireTaggedErrorTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTaggedErrorTypes,
244835
+ requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
244836
+ requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
244837
+ forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
244838
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
244839
+ }
244796
244840
  };
244841
+ if (ctx.effectOptions.forbidDirectRuntimeExecution) {
244842
+ for (const sourceFile of sourceFiles) {
244843
+ scanEffectRuntimeEscapes(sourceFile, ctx);
244844
+ }
244845
+ }
244797
244846
  const nativeTraitFiles = new Map;
244798
244847
  for (const diagnostic of session.getDiagnostics()) {
244799
244848
  ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
@@ -245969,11 +246018,150 @@ function declaredClassType(declaration, ctx) {
245969
246018
  return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
245970
246019
  }
245971
246020
  function typeArgumentsOf(type, ctx) {
245972
- return isTypeReference(type) ? ctx.checker.getTypeArguments(type) : [];
246021
+ if (!isTypeReference(type))
246022
+ return [];
246023
+ const argumentsFromChecker = ctx.checker.getTypeArguments(type);
246024
+ if (argumentsFromChecker.length > 0)
246025
+ return argumentsFromChecker;
246026
+ const reference = type;
246027
+ if (reference.resolvedTypeArguments && reference.resolvedTypeArguments.length > 0) {
246028
+ return reference.resolvedTypeArguments;
246029
+ }
246030
+ return reference.aliasTypeArguments ?? [];
245973
246031
  }
245974
246032
  function isTypeReference(type) {
245975
246033
  return "target" in type;
245976
246034
  }
246035
+ function effectTypeArgumentsOf(type, ctx) {
246036
+ if (!type)
246037
+ return;
246038
+ const reference = isTypeReference(type) ? type : undefined;
246039
+ const target = reference?.target ?? type;
246040
+ const symbol = target.aliasSymbol ?? target.getSymbol();
246041
+ const text = ctx.checker.typeToString(type, undefined, ts7.TypeFormatFlags.NoTruncation);
246042
+ if (symbol?.getName() !== "Effect" && !/^(?:Effect\.)?Effect</.test(text))
246043
+ return;
246044
+ const args = typeArgumentsOf(type, ctx);
246045
+ return args.length >= 3 ? [args[0], args[1], args[2]] : undefined;
246046
+ }
246047
+ function effectTypeName(type, location, ctx) {
246048
+ return ctx.checker.typeToString(type, location, ts7.TypeFormatFlags.NoTruncation);
246049
+ }
246050
+ function stringLiteralTypeValue(type) {
246051
+ return (type.flags & ts7.TypeFlags.StringLiteral) !== 0 ? type.value : undefined;
246052
+ }
246053
+ function effectFailureTags(type, location, ctx) {
246054
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
246055
+ return { tags: [], invalid: false };
246056
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
246057
+ return { tags: [], invalid: true };
246058
+ }
246059
+ if (type.isUnion()) {
246060
+ return type.types.reduce((result, part) => {
246061
+ const next = effectFailureTags(part, location, ctx);
246062
+ result.tags.push(...next.tags);
246063
+ result.invalid ||= next.invalid;
246064
+ return result;
246065
+ }, { tags: [], invalid: false });
246066
+ }
246067
+ const apparent = ctx.checker.getApparentType(type);
246068
+ for (const propertyName of ["_tag", "tag"]) {
246069
+ const property = ctx.checker.getPropertyOfType(apparent, propertyName);
246070
+ if (!property)
246071
+ continue;
246072
+ const propertyType = ctx.checker.getTypeOfSymbolAtLocation(property, location);
246073
+ const tag = stringLiteralTypeValue(propertyType);
246074
+ if (tag)
246075
+ return { tags: [tag], invalid: false };
246076
+ }
246077
+ return { tags: [], invalid: true };
246078
+ }
246079
+ function effectEnvironmentNames(type, location, ctx) {
246080
+ if ((type.flags & ts7.TypeFlags.Never) !== 0)
246081
+ return { names: [], invalid: false };
246082
+ if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
246083
+ return { names: [], invalid: true };
246084
+ }
246085
+ if (type.isUnion() || type.isIntersection()) {
246086
+ return type.types.reduce((result, part) => {
246087
+ const next = effectEnvironmentNames(part, location, ctx);
246088
+ result.names.push(...next.names);
246089
+ result.invalid ||= next.invalid;
246090
+ return result;
246091
+ }, { names: [], invalid: false });
246092
+ }
246093
+ const literal = stringLiteralTypeValue(type);
246094
+ if (literal)
246095
+ return { names: [literal], invalid: false };
246096
+ const symbolName = type.getSymbol()?.getName();
246097
+ if (symbolName && symbolName !== "__type" && symbolName !== "__object") {
246098
+ return { names: [symbolName], invalid: false };
246099
+ }
246100
+ const text = effectTypeName(type, location, ctx).trim();
246101
+ if (text && text !== "object")
246102
+ return { names: [text], invalid: false };
246103
+ return { names: [], invalid: true };
246104
+ }
246105
+ function sortedUnique(values) {
246106
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
246107
+ }
246108
+ function pushEffectDiagnostic(ctx, code, file, line, message, suggestion) {
246109
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
246110
+ ctx.diagnostics.push({
246111
+ severity: "error",
246112
+ code,
246113
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {},
246114
+ file,
246115
+ line,
246116
+ message,
246117
+ suggestion
246118
+ });
246119
+ }
246120
+ function scanEffectRuntimeEscapes(sourceFile, ctx) {
246121
+ const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
246122
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativeFile))
246123
+ return;
246124
+ const namespaceBindings = new Set;
246125
+ const runtimeBindings = new Set;
246126
+ for (const statement of sourceFile.statements) {
246127
+ if (!ts7.isImportDeclaration(statement) || !ts7.isStringLiteral(statement.moduleSpecifier))
246128
+ continue;
246129
+ const moduleName = statement.moduleSpecifier.text;
246130
+ if (moduleName !== "effect" && moduleName !== "effect/Effect" && moduleName !== "effect/Runtime")
246131
+ continue;
246132
+ const clause = statement.importClause;
246133
+ if (!clause)
246134
+ continue;
246135
+ if (clause.name)
246136
+ namespaceBindings.add(clause.name.text);
246137
+ if (clause.namedBindings && ts7.isNamespaceImport(clause.namedBindings)) {
246138
+ namespaceBindings.add(clause.namedBindings.name.text);
246139
+ } else if (clause.namedBindings && ts7.isNamedImports(clause.namedBindings)) {
246140
+ for (const element of clause.namedBindings.elements) {
246141
+ const imported = element.propertyName?.text ?? element.name.text;
246142
+ if (moduleName === "effect" && (imported === "Effect" || imported === "Runtime")) {
246143
+ namespaceBindings.add(element.name.text);
246144
+ }
246145
+ if (["runPromise", "runPromiseExit", "runSync", "runFork"].includes(imported)) {
246146
+ runtimeBindings.add(element.name.text);
246147
+ }
246148
+ }
246149
+ }
246150
+ }
246151
+ if (namespaceBindings.size === 0 && runtimeBindings.size === 0)
246152
+ return;
246153
+ for (const call of descendantsOfKind(sourceFile, ts7.isCallExpression)) {
246154
+ let runtimeName;
246155
+ if (ts7.isPropertyAccessExpression(call.expression) && namespaceBindings.has(call.expression.expression.getText(sourceFile)) && ["runPromise", "runPromiseExit", "runSync", "runFork"].includes(call.expression.name.text)) {
246156
+ runtimeName = call.expression.name.text;
246157
+ } else if (ts7.isIdentifier(call.expression) && runtimeBindings.has(call.expression.text)) {
246158
+ runtimeName = call.expression.text;
246159
+ }
246160
+ if (!runtimeName)
246161
+ continue;
246162
+ pushEffectDiagnostic(ctx, "effect-direct-runtime-execution", sourcePath(ctx.rootDir, sourceFile.fileName), lineOf2(call), `Direct ${runtimeName} execution escapes the Effect boundary.`, "Return the Effect program to the Elysia boundary; only the framework adapter may interpret it.");
246163
+ }
246164
+ }
245977
246165
  function isUnknownOrAny(type) {
245978
246166
  return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
245979
246167
  }
@@ -246037,7 +246225,8 @@ function parseController(input, ctx) {
246037
246225
  handler: propertyName(method.name)
246038
246226
  };
246039
246227
  const signature = ctx.checker.getSignatureFromDeclaration(method);
246040
- const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
246228
+ const returnType = signature?.getReturnType();
246229
+ const resultType = returnType && ctx.checker.typeToString(returnType);
246041
246230
  if (resultType && /\bResponse\b/.test(resultType))
246042
246231
  route.nativeResponse = true;
246043
246232
  const pathParams = [];
@@ -246265,6 +246454,235 @@ function parseController(input, ctx) {
246265
246454
  Object.assign(route.contract, { [field]: value });
246266
246455
  }
246267
246456
  }
246457
+ const effectExpr = getProp(optionsObject, "effect");
246458
+ if (effectExpr) {
246459
+ const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
246460
+ if (!effectObject) {
246461
+ ctx.diagnostics.push({
246462
+ severity: "error",
246463
+ code: "invalid-effect-contract",
246464
+ errorCode: "SC3030",
246465
+ docsUrl: "https://supacloud.dev/errors/SC3030",
246466
+ file,
246467
+ line: lineOf2(effectExpr),
246468
+ message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
246469
+ suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
246470
+ });
246471
+ } else {
246472
+ const effect = { required: true };
246473
+ const requiredExpr = getProp(effectObject, "required");
246474
+ if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
246475
+ ctx.diagnostics.push({
246476
+ severity: "error",
246477
+ code: "invalid-effect-contract",
246478
+ errorCode: "SC3030",
246479
+ docsUrl: "https://supacloud.dev/errors/SC3030",
246480
+ file,
246481
+ line: lineOf2(requiredExpr),
246482
+ message: `Route ${route.handler} effect.required must be the literal true.`
246483
+ });
246484
+ }
246485
+ const retry = stringLiteralProp(effectObject, "retry");
246486
+ if (retry !== undefined) {
246487
+ if (retry !== "none" && retry !== "explicit") {
246488
+ ctx.diagnostics.push({
246489
+ severity: "error",
246490
+ code: "invalid-effect-retry",
246491
+ errorCode: "SC3031",
246492
+ docsUrl: "https://supacloud.dev/errors/SC3031",
246493
+ file,
246494
+ line: lineOf2(getProp(effectObject, "retry")),
246495
+ message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
246496
+ });
246497
+ } else {
246498
+ effect.retry = retry;
246499
+ }
246500
+ }
246501
+ const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
246502
+ if (maxAttemptsExpr !== undefined) {
246503
+ const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
246504
+ if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
246505
+ ctx.diagnostics.push({
246506
+ severity: "error",
246507
+ code: "invalid-effect-retry",
246508
+ errorCode: "SC3031",
246509
+ docsUrl: "https://supacloud.dev/errors/SC3031",
246510
+ file,
246511
+ line: lineOf2(maxAttemptsExpr),
246512
+ message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
246513
+ });
246514
+ } else {
246515
+ effect.maxAttempts = maxAttempts;
246516
+ }
246517
+ }
246518
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
246519
+ ctx.diagnostics.push({
246520
+ severity: "error",
246521
+ code: "invalid-effect-retry",
246522
+ errorCode: "SC3031",
246523
+ docsUrl: "https://supacloud.dev/errors/SC3031",
246524
+ file,
246525
+ line: lineOf2(effectExpr),
246526
+ message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
246527
+ });
246528
+ }
246529
+ const timeoutExpr = getProp(effectObject, "timeoutMs");
246530
+ if (timeoutExpr !== undefined) {
246531
+ const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
246532
+ if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
246533
+ ctx.diagnostics.push({
246534
+ severity: "error",
246535
+ code: "invalid-effect-timeout",
246536
+ errorCode: "SC3032",
246537
+ docsUrl: "https://supacloud.dev/errors/SC3032",
246538
+ file,
246539
+ line: lineOf2(timeoutExpr),
246540
+ message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
246541
+ });
246542
+ } else {
246543
+ effect.timeoutMs = timeoutMs;
246544
+ }
246545
+ }
246546
+ const dependenciesExpr = getProp(effectObject, "dependencies");
246547
+ if (dependenciesExpr !== undefined) {
246548
+ if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
246549
+ ctx.diagnostics.push({
246550
+ severity: "error",
246551
+ code: "invalid-effect-dependencies",
246552
+ errorCode: "SC3033",
246553
+ docsUrl: "https://supacloud.dev/errors/SC3033",
246554
+ file,
246555
+ line: lineOf2(dependenciesExpr),
246556
+ message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
246557
+ });
246558
+ } else {
246559
+ effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
246560
+ if (new Set(effect.dependencies).size !== effect.dependencies.length) {
246561
+ ctx.diagnostics.push({
246562
+ severity: "error",
246563
+ code: "invalid-effect-dependencies",
246564
+ errorCode: "SC3033",
246565
+ docsUrl: "https://supacloud.dev/errors/SC3033",
246566
+ file,
246567
+ line: lineOf2(dependenciesExpr),
246568
+ message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
246569
+ suggestion: "List each logical Effect dependency exactly once."
246570
+ });
246571
+ }
246572
+ }
246573
+ }
246574
+ const errorsExpr = getProp(effectObject, "errors");
246575
+ if (errorsExpr !== undefined) {
246576
+ if (!ts7.isArrayLiteralExpression(errorsExpr)) {
246577
+ ctx.diagnostics.push({
246578
+ severity: "error",
246579
+ code: "invalid-effect-errors",
246580
+ errorCode: "SC3034",
246581
+ docsUrl: "https://supacloud.dev/errors/SC3034",
246582
+ file,
246583
+ line: lineOf2(errorsExpr),
246584
+ message: `Route ${route.handler} effect.errors must be an explicit object array.`
246585
+ });
246586
+ } else {
246587
+ const errors = [];
246588
+ for (const errorExpr of errorsExpr.elements) {
246589
+ if (!ts7.isObjectLiteralExpression(errorExpr)) {
246590
+ ctx.diagnostics.push({
246591
+ severity: "error",
246592
+ code: "invalid-effect-errors",
246593
+ errorCode: "SC3034",
246594
+ docsUrl: "https://supacloud.dev/errors/SC3034",
246595
+ file,
246596
+ line: lineOf2(errorExpr),
246597
+ message: `Route ${route.handler} effect.errors entries must be object literals.`
246598
+ });
246599
+ continue;
246600
+ }
246601
+ const tag = stringLiteralProp(errorExpr, "tag");
246602
+ const code = stringLiteralProp(errorExpr, "code");
246603
+ const status = numberLiteralProp(errorExpr, "status");
246604
+ const message = stringLiteralProp(errorExpr, "message");
246605
+ if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
246606
+ ctx.diagnostics.push({
246607
+ severity: "error",
246608
+ code: "invalid-effect-errors",
246609
+ errorCode: "SC3034",
246610
+ docsUrl: "https://supacloud.dev/errors/SC3034",
246611
+ file,
246612
+ line: lineOf2(errorExpr),
246613
+ message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
246614
+ });
246615
+ continue;
246616
+ }
246617
+ errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
246618
+ }
246619
+ effect.errors = errors;
246620
+ if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
246621
+ ctx.diagnostics.push({
246622
+ severity: "error",
246623
+ code: "invalid-effect-errors",
246624
+ errorCode: "SC3034",
246625
+ docsUrl: "https://supacloud.dev/errors/SC3034",
246626
+ file,
246627
+ line: lineOf2(errorsExpr),
246628
+ message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
246629
+ suggestion: "Map each tagged failure exactly once."
246630
+ });
246631
+ }
246632
+ }
246633
+ }
246634
+ route.effect = effect;
246635
+ const effectArguments = effectTypeArgumentsOf(returnType, ctx);
246636
+ const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
246637
+ if (promiseReturn) {
246638
+ pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf2(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
246639
+ } else if (!effectArguments) {
246640
+ ctx.diagnostics.push({
246641
+ severity: "error",
246642
+ code: "effect-return-type",
246643
+ errorCode: "SC3035",
246644
+ docsUrl: "https://supacloud.dev/errors/SC3035",
246645
+ file,
246646
+ line: lineOf2(method),
246647
+ message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
246648
+ suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
246649
+ });
246650
+ }
246651
+ if (effectArguments) {
246652
+ const [, failureType, environmentType] = effectArguments;
246653
+ const failure = effectFailureTags(failureType, method, ctx);
246654
+ if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
246655
+ pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf2(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
246656
+ } else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
246657
+ const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
246658
+ const typedTags = sortedUnique(failure.tags);
246659
+ if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
246660
+ pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf2(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.");
246661
+ }
246662
+ }
246663
+ const environment = effectEnvironmentNames(environmentType, method, ctx);
246664
+ if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
246665
+ if (environment.invalid) {
246666
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf2(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.");
246667
+ } else {
246668
+ const declaredDependencies = sortedUnique(effect.dependencies);
246669
+ const typedDependencies = sortedUnique(environment.names);
246670
+ if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
246671
+ pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf2(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.");
246672
+ }
246673
+ }
246674
+ }
246675
+ if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
246676
+ pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf2(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.");
246677
+ }
246678
+ }
246679
+ if (ctx.effectOptions.forbidDirectThrows && method.body) {
246680
+ for (const throwStatement of descendantsOfKind(method.body, ts7.isThrowStatement)) {
246681
+ pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf2(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.");
246682
+ }
246683
+ }
246684
+ }
246685
+ }
246268
246686
  for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
246269
246687
  const schemaExpr = getProp(optionsObject, field);
246270
246688
  if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
@@ -246848,6 +247266,13 @@ function stringLiteralProp(obj, name) {
246848
247266
  const expr = getProp(obj, name);
246849
247267
  return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
246850
247268
  }
247269
+ function numberLiteralProp(obj, name) {
247270
+ const expr = getProp(obj, name);
247271
+ if (!expr || !ts7.isNumericLiteral(expr))
247272
+ return;
247273
+ const value = Number(expr.text);
247274
+ return Number.isFinite(value) ? value : undefined;
247275
+ }
246851
247276
  function arrayProp(obj, name) {
246852
247277
  const expr = getProp(obj, name);
246853
247278
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -247489,6 +247914,55 @@ function validateClientSchemaBoundaries(graph, options) {
247489
247914
  }
247490
247915
  return diagnostics;
247491
247916
  }
247917
+ init_validate();
247918
+ function diagnostic(code, message, file, suggestion) {
247919
+ const metadata = COMPILER_DIAGNOSTIC_CODES[code];
247920
+ return {
247921
+ severity: "error",
247922
+ code,
247923
+ message,
247924
+ ...file === undefined ? {} : { file },
247925
+ ...suggestion === undefined ? {} : { suggestion },
247926
+ ...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
247927
+ };
247928
+ }
247929
+ function routeLabel(route, controllerPath) {
247930
+ return `${route.method} ${controllerPath}${route.path}`;
247931
+ }
247932
+ function validateEffectPolicies(graph, options = {}) {
247933
+ const diagnostics = [];
247934
+ for (const module of graph.modules) {
247935
+ const commands = new Map(module.commands.map((command) => [command.className, command]));
247936
+ for (const controller of module.controllers) {
247937
+ for (const route of controller.routes) {
247938
+ const label = routeLabel(route, controller.path);
247939
+ if (options.requireRouteEffects && route.effect === undefined) {
247940
+ 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.'));
247941
+ continue;
247942
+ }
247943
+ const effect = route.effect;
247944
+ if (effect === undefined)
247945
+ continue;
247946
+ if (options.requireErrorMappings && effect.errors === undefined) {
247947
+ diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
247948
+ }
247949
+ if (options.requireDependencies && effect.dependencies === undefined) {
247950
+ diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
247951
+ }
247952
+ if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
247953
+ diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
247954
+ }
247955
+ if (route.command && effect.retry === "explicit") {
247956
+ const command = commands.get(route.command);
247957
+ if (command && command.idempotency !== "required") {
247958
+ diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
247959
+ }
247960
+ }
247961
+ }
247962
+ }
247963
+ }
247964
+ return diagnostics;
247965
+ }
247492
247966
  function withDefaultGovernance(options) {
247493
247967
  return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
247494
247968
  requirePersistentAdapters: true,
@@ -247503,11 +247977,12 @@ async function renderOptionalGraphql(options) {
247503
247977
  }
247504
247978
  async function compileProject(options) {
247505
247979
  options = withDefaultGovernance(options);
247506
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
247980
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
247507
247981
  const diagnostics = [
247508
247982
  ...graph.diagnostics ?? [],
247509
247983
  ...validateGraph(graph, options),
247510
- ...validateClientSchemaBoundaries(graph, options)
247984
+ ...validateClientSchemaBoundaries(graph, options),
247985
+ ...validateEffectPolicies(graph, options.effect)
247511
247986
  ];
247512
247987
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
247513
247988
  return { diagnostics, graph, written: [] };
@@ -247567,11 +248042,12 @@ async function compileProject(options) {
247567
248042
  }
247568
248043
  async function checkProject(options) {
247569
248044
  options = withDefaultGovernance(options);
247570
- const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
248045
+ const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
247571
248046
  const diagnostics = [
247572
248047
  ...graph.diagnostics ?? [],
247573
248048
  ...validateGraph(graph, options),
247574
- ...validateClientSchemaBoundaries(graph, options)
248049
+ ...validateClientSchemaBoundaries(graph, options),
248050
+ ...validateEffectPolicies(graph, options.effect)
247575
248051
  ];
247576
248052
  if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
247577
248053
  return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
@@ -247653,7 +248129,7 @@ init_delivery_schema();
247653
248129
  init_validate();
247654
248130
  var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
247655
248131
  var sorted = (values) => [...new Set(values)].sort(compare2);
247656
- function diagnostic(error) {
248132
+ function diagnostic2(error) {
247657
248133
  return {
247658
248134
  severity: error.severity,
247659
248135
  code: error.code,
@@ -247678,8 +248154,8 @@ function createDeliveryPlan(graph, input) {
247678
248154
  }] };
247679
248155
  }
247680
248156
  const diagnostics = [
247681
- ...(graph.diagnostics ?? []).map(diagnostic),
247682
- ...validateGraph(graph, { strict: true }).map(diagnostic)
248157
+ ...(graph.diagnostics ?? []).map(diagnostic2),
248158
+ ...validateGraph(graph, { strict: true }).map(diagnostic2)
247683
248159
  ];
247684
248160
  const fail = (code, message, suggestion) => {
247685
248161
  diagnostics.push({ severity: "error", code, message, suggestion });
@@ -248807,6 +249283,7 @@ function optionsKeyOf(options) {
248807
249283
  openApi: options.openApi,
248808
249284
  generatePermissions: options.generatePermissions,
248809
249285
  typeSafety: options.typeSafety,
249286
+ effect: options.effect,
248810
249287
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
248811
249288
  graphql: options.graphql
248812
249289
  });
@@ -249134,7 +249611,17 @@ var DEFAULT_SUPACLOUD_CONFIG = {
249134
249611
  idempotency: true,
249135
249612
  transaction: true
249136
249613
  },
249137
- moduleBoundaryPreset: "modular-monolith"
249614
+ moduleBoundaryPreset: "modular-monolith",
249615
+ effect: {
249616
+ requireRouteEffects: true,
249617
+ requireErrorMappings: true,
249618
+ requireDependencies: true,
249619
+ requireTaggedErrorTypes: true,
249620
+ requireExactDependencyTypes: true,
249621
+ requireTimeoutForDependencies: true,
249622
+ forbidDirectRuntimeExecution: true,
249623
+ forbidDirectThrows: true
249624
+ }
249138
249625
  };
249139
249626
  function defineSupacloudConfig(config = {}) {
249140
249627
  if (config.delivery !== undefined)
@@ -249146,7 +249633,8 @@ function defineSupacloudConfig(config = {}) {
249146
249633
  ...DEFAULT_SUPACLOUD_CONFIG,
249147
249634
  ...config,
249148
249635
  include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
249149
- graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
249636
+ graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
249637
+ effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
249150
249638
  };
249151
249639
  }
249152
249640
  function validateGovernanceConfig(config) {
@@ -249203,6 +249691,34 @@ function validateGovernanceConfig(config) {
249203
249691
  if (config[key] !== undefined && typeof config[key] !== "boolean")
249204
249692
  throw new Error(`${key} must be a boolean.`);
249205
249693
  }
249694
+ if (config.effect !== undefined) {
249695
+ if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => ![
249696
+ "requireRouteEffects",
249697
+ "requireErrorMappings",
249698
+ "requireDependencies",
249699
+ "requireTaggedErrorTypes",
249700
+ "requireExactDependencyTypes",
249701
+ "requireTimeoutForDependencies",
249702
+ "forbidDirectRuntimeExecution",
249703
+ "forbidDirectThrows"
249704
+ ].includes(key))) {
249705
+ throw new Error("effect contains an unsupported governance option.");
249706
+ }
249707
+ for (const key of [
249708
+ "requireRouteEffects",
249709
+ "requireErrorMappings",
249710
+ "requireDependencies",
249711
+ "requireTaggedErrorTypes",
249712
+ "requireExactDependencyTypes",
249713
+ "requireTimeoutForDependencies",
249714
+ "forbidDirectRuntimeExecution",
249715
+ "forbidDirectThrows"
249716
+ ]) {
249717
+ if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
249718
+ throw new Error(`effect.${key} must be boolean.`);
249719
+ }
249720
+ }
249721
+ }
249206
249722
  }
249207
249723
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
249208
249724
  const resolved = defineSupacloudConfig(config);
@@ -249223,6 +249739,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
249223
249739
  ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
249224
249740
  ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
249225
249741
  ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
249742
+ effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
249226
249743
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
249227
249744
  graphql: resolved.graphql ? {
249228
249745
  ...resolved.graphql,
@@ -249371,16 +249888,16 @@ var starter_metadata_default = {
249371
249888
  generatedBy: "supacloud/starter-metadata-v1",
249372
249889
  packages: {
249373
249890
  compiler: {
249374
- version: "0.40.1"
249891
+ version: "0.42.0"
249375
249892
  },
249376
249893
  app: {
249377
- version: "0.23.0",
249894
+ version: "0.24.0",
249378
249895
  dependencies: {
249379
249896
  rxjs: "7.8.2"
249380
249897
  }
249381
249898
  },
249382
249899
  elysia: {
249383
- version: "0.27.1"
249900
+ version: "0.29.0"
249384
249901
  },
249385
249902
  commands: {
249386
249903
  version: "0.9.1"
@@ -249734,6 +250251,7 @@ function baseFiles(name, sdkDependencies) {
249734
250251
  dependencies: {
249735
250252
  "@supacloud/app": `^${appMetadata.version}`,
249736
250253
  "@supacloud/elysia": `^${elysiaMetadata.version}`,
250254
+ effect: "4.0.2",
249737
250255
  elysia: "2.0.0-beta.21",
249738
250256
  ...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
249739
250257
  },
@@ -249778,6 +250296,16 @@ export default defineSupacloudConfig({
249778
250296
  outDir: "generated",
249779
250297
  strict: true,
249780
250298
  moduleBoundaryPreset: "modular-monolith",
250299
+ effect: {
250300
+ requireRouteEffects: true,
250301
+ requireErrorMappings: true,
250302
+ requireDependencies: true,
250303
+ requireTaggedErrorTypes: true,
250304
+ requireExactDependencyTypes: true,
250305
+ requireTimeoutForDependencies: true,
250306
+ forbidDirectRuntimeExecution: true,
250307
+ forbidDirectThrows: true,
250308
+ },
249781
250309
  typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
249782
250310
  disallowControllerDirectDb: true,
249783
250311
  detectOrphanModules: true,
@@ -249806,20 +250334,34 @@ export const CreateOrderBody = t.Object({ name: t.String({ minLength: 1 }) });
249806
250334
  export const OrderResult = t.Object({ id: t.String(), name: t.String() });
249807
250335
  export const HealthResult = t.Object({ ok: t.Boolean() });
249808
250336
  `,
249809
- "src/orders/orders.ts": `import { Body, Controller, Get, Param, Post, defineFeatureSlice } from "@supacloud/app";
250337
+ "src/orders/orders.ts": `import { Effect } from "effect";
250338
+ import { Body, Controller, Get, Param, Post, defineFeatureSlice } from "@supacloud/app";
249810
250339
  import { status } from "elysia";
249811
250340
  import { OrderParams, CreateOrderBody, OrderResult, HealthResult } from "./contracts";
249812
250341
  @Controller("/orders")
249813
250342
  export class OrdersController {
249814
- @Get("/health", { responses: { 200: HealthResult } })
249815
- health(): { ok: boolean } { return { ok: true }; }
249816
-
249817
- @Get("/:id", { params: OrderParams, responses: { 200: OrderResult } })
249818
- get(@Param("id") id: string): { id: string; name: string } { return { id, name: "demo" }; }
249819
-
249820
- @Post("/", { body: CreateOrderBody, responses: { 201: OrderResult } })
250343
+ @Get("/health", {
250344
+ responses: { 200: HealthResult },
250345
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
250346
+ })
250347
+ health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
250348
+
250349
+ @Get("/:id", {
250350
+ params: OrderParams,
250351
+ responses: { 200: OrderResult },
250352
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
250353
+ })
250354
+ get(@Param("id") id: string): Effect.Effect<{ id: string; name: string }, never, never> {
250355
+ return Effect.succeed({ id, name: "demo" });
250356
+ }
250357
+
250358
+ @Post("/", {
250359
+ body: CreateOrderBody,
250360
+ responses: { 201: OrderResult },
250361
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
250362
+ })
249821
250363
  create(@Body() body: { name: string }) {
249822
- return status(201, { id: "demo", name: body.name });
250364
+ return Effect.succeed(status(201, { id: "demo", name: body.name }));
249823
250365
  }
249824
250366
  }
249825
250367
 
@@ -249993,22 +250535,27 @@ function minimalTemplate(name, sdkDependencies) {
249993
250535
  "src/features/health/contracts.ts": `import { t } from "elysia";
249994
250536
  export const HealthResult = t.Object({ ok: t.Boolean() });
249995
250537
  `,
249996
- "src/features/health/health.ts": `import { Controller, Get, Module } from "@supacloud/app/core";
250538
+ "src/features/health/health.ts": `import { Effect } from "effect";
250539
+ import { Controller, Get, Module } from "@supacloud/app/core";
249997
250540
  import { HealthResult } from "./contracts";
249998
250541
  @Controller("/health")
249999
250542
  export class HealthController {
250000
- @Get("/", { responses: { 200: HealthResult } })
250001
- health(): { ok: boolean } { return { ok: true }; }
250543
+ @Get("/", {
250544
+ responses: { 200: HealthResult },
250545
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
250546
+ })
250547
+ health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
250002
250548
  }
250003
250549
 
250004
250550
  @Module({ name: "health", tags: ["type:feature"], controllers: [HealthController] })
250005
250551
  export class HealthModule {}
250006
250552
  `,
250007
- "src/features/health/health.test.ts": `import { expect, test } from "bun:test";
250553
+ "src/features/health/health.test.ts": `import { Effect } from "effect";
250554
+ import { expect, test } from "bun:test";
250008
250555
  import { HealthController } from "./health";
250009
250556
 
250010
- test("health is a public, side-effect-free operation", () => {
250011
- expect(new HealthController().health()).toEqual({ ok: true });
250557
+ test("health is a public, side-effect-free operation", async () => {
250558
+ expect(await Effect.runPromise(new HealthController().health())).toEqual({ ok: true });
250012
250559
  });
250013
250560
  `,
250014
250561
  "src/app.module.ts": `import { Module } from "@supacloud/app/core";
@@ -250533,13 +251080,25 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
250533
251080
  `;
250534
251081
 
250535
251082
  // src/shared/tools/app-starter-upload.ts
250536
- var STARTER_UPLOAD_FEATURE = `import { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from "@supacloud/app";
251083
+ var STARTER_UPLOAD_FEATURE = `import { Effect } from "effect";
251084
+ import { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from "@supacloud/app";
250537
251085
  import { ApplicationError } from "@supacloud/elysia";
250538
251086
  import { UploadBody, UploadLocation, UploadParams, UploadResult } from "./contracts";
250539
251087
 
250540
251088
  export interface ReviewUploadInput { reviewId: string; artifactId: string; expectedVersion: number }
250541
251089
  export interface ReviewUploadLocation { artifactId: string; bucketId: string; objectPath: string }
250542
251090
  export interface ReviewUploadResult { artifactId: string; runId: string; objectPath: string; sha256: string; bytes: number }
251091
+ type PreviewUploadError =
251092
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
251093
+ | { readonly _tag: "UPLOAD_INPUT_INVALID" }
251094
+ | { readonly _tag: "UPLOAD_DENIED" }
251095
+ | { readonly _tag: "UPLOADS_UNAVAILABLE" };
251096
+ type RegisterUploadError =
251097
+ | PreviewUploadError
251098
+ | { readonly _tag: "UPLOAD_CONTENT_INVALID" }
251099
+ | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
251100
+ | { readonly _tag: "COMMAND_REJECTED" }
251101
+ | { readonly _tag: "UPLOAD_OUTCOME_UNKNOWN" };
250543
251102
  export interface ReviewUploadPort {
250544
251103
  previewUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadLocation>;
250545
251104
  registerUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadResult>;
@@ -250563,13 +251122,47 @@ export class ReviewUploads {
250563
251122
  export class ReviewUploadsController {
250564
251123
  constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}
250565
251124
 
250566
- @Post("/:id/attachment-upload", { params: UploadParams, body: UploadBody, responses: { 200: UploadLocation } })
250567
- preview(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Promise<ReviewUploadLocation> {
250568
- return this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion });
250569
- }
250570
- @Post("/:id/attachment-registration", { params: UploadParams, body: UploadBody, responses: { 200: UploadResult } })
250571
- register(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Promise<ReviewUploadResult> {
250572
- return this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion });
251125
+ @Post("/:id/attachment-upload", {
251126
+ params: UploadParams,
251127
+ body: UploadBody,
251128
+ responses: { 200: UploadLocation },
251129
+ effect: {
251130
+ required: true,
251131
+ dependencies: [],
251132
+ errors: [
251133
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
251134
+ { tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
251135
+ { tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
251136
+ { tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
251137
+ ],
251138
+ retry: "none",
251139
+ },
251140
+ })
251141
+ preview(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {
251142
+ return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
251143
+ }
251144
+ @Post("/:id/attachment-registration", {
251145
+ params: UploadParams,
251146
+ body: UploadBody,
251147
+ responses: { 200: UploadResult },
251148
+ effect: {
251149
+ required: true,
251150
+ dependencies: [],
251151
+ errors: [
251152
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
251153
+ { tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
251154
+ { tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
251155
+ { tag: "UPLOAD_CONTENT_INVALID", status: 400, code: "UPLOAD_CONTENT_INVALID" },
251156
+ { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
251157
+ { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
251158
+ { tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
251159
+ { tag: "UPLOAD_OUTCOME_UNKNOWN", status: 503, code: "UPLOAD_OUTCOME_UNKNOWN" },
251160
+ ],
251161
+ retry: "none",
251162
+ },
251163
+ })
251164
+ register(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {
251165
+ return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
250573
251166
  }
250574
251167
  }
250575
251168
  `;
@@ -250853,6 +251446,10 @@ function rows(value: unknown): Record<string, unknown>[] {
250853
251446
  if (!Array.isArray(value)) throw new TypeError("Invalid database result");
250854
251447
  return value.map(record);
250855
251448
  }
251449
+ function isPublicConflict(value: unknown): boolean {
251450
+ return value instanceof ApplicationError && value.status === 409
251451
+ || isRecord(value) && value.expose === true && value.status === 409 && typeof value.code === "string";
251452
+ }
250856
251453
  function input(value: unknown): Approval {
250857
251454
  const item = record(value);
250858
251455
  if (typeof item.id !== "string" || !item.id.length || typeof item.expectedVersion !== "number"
@@ -250967,7 +251564,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
250967
251564
  execute: tx => scope.run(tx, async () => {
250968
251565
  try { return await next(); }
250969
251566
  catch (error) {
250970
- if (error instanceof ApplicationError && error.status === 409) {
251567
+ if (isPublicConflict(error)) {
250971
251568
  throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
250972
251569
  }
250973
251570
  throw error;
@@ -251276,6 +251873,7 @@ function appStarterFiles(name) {
251276
251873
  "drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
251277
251874
  "@supacloud/js": `^${sdkMetadata.version}`,
251278
251875
  "@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
251876
+ effect: "4.0.2",
251279
251877
  elysia: "2.0.0-beta.21",
251280
251878
  rxjs: appMetadata2.dependencies.rxjs
251281
251879
  },
@@ -251331,6 +251929,16 @@ export default defineSupacloudConfig({
251331
251929
  outDir: "generated",
251332
251930
  strict: true,
251333
251931
  moduleBoundaryPreset: "modular-monolith",
251932
+ effect: {
251933
+ requireRouteEffects: true,
251934
+ requireErrorMappings: true,
251935
+ requireDependencies: true,
251936
+ requireTaggedErrorTypes: true,
251937
+ requireExactDependencyTypes: true,
251938
+ requireTimeoutForDependencies: true,
251939
+ forbidDirectRuntimeExecution: true,
251940
+ forbidDirectThrows: true,
251941
+ },
251334
251942
  typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
251335
251943
  disallowControllerDirectDb: true,
251336
251944
  detectOrphanModules: true,
@@ -251531,7 +252139,8 @@ export function createCompiledModules(): never {
251531
252139
  throw new Error("Run bun run compile before starting the application");
251532
252140
  }
251533
252141
  `,
251534
- "src/review/review.ts": `import {
252142
+ "src/review/review.ts": `import { Effect } from "effect";
252143
+ import {
251535
252144
  Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
251536
252145
  defineFeatureSlice, defineFeatureSpec, type Aspect,
251537
252146
  } from "@supacloud/app";
@@ -251597,16 +252206,51 @@ export class ApproveReview {
251597
252206
  }
251598
252207
  }
251599
252208
 
252209
+ type ReviewError =
252210
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
252211
+ | { readonly _tag: "REVIEW_NOT_FOUND" }
252212
+ | { readonly _tag: "REVIEW_PERMISSION_DENIED" }
252213
+ | { readonly _tag: "REVIEW_CONFLICT" }
252214
+ | { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
252215
+ | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
252216
+ | { readonly _tag: "COMMAND_REJECTED" }
252217
+ | { readonly _tag: "REVIEW_COMMAND_UNSUPPORTED" }
252218
+ | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
252219
+
251600
252220
  @Controller("/reviews")
251601
252221
  export class ReviewController {
251602
252222
  constructor(@Inject(ApproveReview) private readonly approveReview: ApproveReview) {}
251603
252223
 
251604
- @Get("/health", { responses: { 200: HealthResult } })
251605
- health(): { ok: boolean } { return { ok: true }; }
251606
-
251607
- @Post("/:id/approve", { command: ApproveReview, params: Params, body: ApproveBody, responses: { 200: ReviewResult } })
251608
- approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Promise<Review> {
251609
- return this.approveReview.execute(id, body.expectedVersion);
252224
+ @Get("/health", {
252225
+ responses: { 200: HealthResult },
252226
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
252227
+ })
252228
+ health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
252229
+
252230
+ @Post("/:id/approve", {
252231
+ command: ApproveReview,
252232
+ params: Params,
252233
+ body: ApproveBody,
252234
+ responses: { 200: ReviewResult },
252235
+ effect: {
252236
+ required: true,
252237
+ dependencies: [],
252238
+ errors: [
252239
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
252240
+ { tag: "REVIEW_NOT_FOUND", status: 404, code: "REVIEW_NOT_FOUND" },
252241
+ { tag: "REVIEW_PERMISSION_DENIED", status: 403, code: "REVIEW_PERMISSION_DENIED" },
252242
+ { tag: "REVIEW_CONFLICT", status: 409, code: "REVIEW_CONFLICT" },
252243
+ { tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
252244
+ { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
252245
+ { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
252246
+ { tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
252247
+ { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
252248
+ ],
252249
+ retry: "none",
252250
+ },
252251
+ })
252252
+ approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, ReviewError, never> {
252253
+ return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
251610
252254
  }
251611
252255
  }
251612
252256
 
@@ -252145,7 +252789,8 @@ export class ${className}Service {
252145
252789
  }
252146
252790
  }
252147
252791
  `,
252148
- [`${name}.controller.ts`]: `import { Controller, Get, Inject, Param } from "@supacloud/app";
252792
+ [`${name}.controller.ts`]: `import { Effect } from "effect";
252793
+ import { Controller, Get, Inject, Param } from "@supacloud/app";
252149
252794
  import { ${className}Params, ${className}Response, type ${className}Result } from "./${name}.model";
252150
252795
  import { ${className}Service } from "./${name}.service";
252151
252796
 
@@ -252153,9 +252798,13 @@ import { ${className}Service } from "./${name}.service";
252153
252798
  export class ${className}Controller {
252154
252799
  constructor(@Inject(${className}Service) readonly service: Pick<${className}Service, "find">) {}
252155
252800
 
252156
- @Get("/:id", { params: ${className}Params, responses: { 200: ${className}Response } })
252157
- find(@Param("id") id: string): Promise<${className}Result> {
252158
- return this.service.find(id);
252801
+ @Get("/:id", {
252802
+ params: ${className}Params,
252803
+ responses: { 200: ${className}Response },
252804
+ effect: { required: true, dependencies: [], errors: [], retry: "none" },
252805
+ })
252806
+ find(@Param("id") id: string): Effect.Effect<${className}Result, never, never> {
252807
+ return Effect.promise(() => this.service.find(id));
252159
252808
  }
252160
252809
  }
252161
252810
  `,
@@ -252190,19 +252839,20 @@ test("${name} preserves permission denial without a privileged fallback", async
252190
252839
  await expect(service.find("example")).rejects.toBe(denied);
252191
252840
  });
252192
252841
  `,
252193
- [`${name}.controller.test.ts`]: `import { expect, test } from "bun:test";
252842
+ [`${name}.controller.test.ts`]: `import { Effect } from "effect";
252843
+ import { expect, test } from "bun:test";
252194
252844
  import { ${className}Controller } from "./${name}.controller";
252195
252845
 
252196
252846
  test("${name} controller delegates to the supplied asynchronous read port", async () => {
252197
252847
  // Unit-test double only; the generated runtime service remains fail-closed.
252198
252848
  const controller = new ${className}Controller({ find: async (id) => ({ id }) });
252199
- expect(await controller.find("example")).toEqual({ id: "example" });
252849
+ expect(await Effect.runPromise(controller.find("example"))).toEqual({ id: "example" });
252200
252850
  });
252201
252851
 
252202
252852
  test("${name} controller preserves an asynchronous read failure", async () => {
252203
252853
  const failure = new Error("read port unavailable");
252204
252854
  const controller = new ${className}Controller({ find: async () => { throw failure; } });
252205
- await expect(controller.find("example")).rejects.toBe(failure);
252855
+ await expect(Effect.runPromise(controller.find("example"))).rejects.toThrow("read port unavailable");
252206
252856
  });
252207
252857
  `
252208
252858
  };
@@ -253726,6 +254376,91 @@ function formatReconcileReport(report) {
253726
254376
  return lines.join(`
253727
254377
  `);
253728
254378
  }
254379
+ function moduleObjects2(module) {
254380
+ return [
254381
+ ...module.tables.map((table) => ({
254382
+ kind: "table",
254383
+ name: table,
254384
+ module: module.name,
254385
+ source: null,
254386
+ tests: []
254387
+ })),
254388
+ ...module.policies.map((policy) => ({
254389
+ kind: "policy",
254390
+ name: policy.name,
254391
+ module: module.name,
254392
+ table: policy.table,
254393
+ source: policy.source,
254394
+ tests: policy.tests ?? []
254395
+ })),
254396
+ ...module.functions.map((fn) => ({
254397
+ kind: "function",
254398
+ name: fn.name,
254399
+ module: module.name,
254400
+ source: fn.source,
254401
+ tests: fn.tests ?? [],
254402
+ security: fn.security,
254403
+ permission: fn.permission,
254404
+ transaction: fn.transaction,
254405
+ audit: fn.audit,
254406
+ idempotency: fn.idempotency
254407
+ })),
254408
+ ...module.triggers.map((trigger) => ({
254409
+ kind: "trigger",
254410
+ name: trigger.name,
254411
+ module: module.name,
254412
+ table: trigger.table,
254413
+ source: trigger.source,
254414
+ tests: []
254415
+ })),
254416
+ ...module.grants.map((grant) => ({
254417
+ kind: "grant",
254418
+ name: `${grant.object}:${grant.privilege}:${grant.role}`,
254419
+ module: module.name,
254420
+ source: grant.source,
254421
+ tests: []
254422
+ }))
254423
+ ];
254424
+ }
254425
+ function objectMatchesTarget(object, target) {
254426
+ return object.name === target || object.table === target || object.kind === "policy" && `${object.table}.${object.name}` === target || object.kind === "grant" && object.name.startsWith(`${target}:`);
254427
+ }
254428
+ function buildDatabaseAiContext(modules, root, moduleName, target) {
254429
+ const selected = moduleName ? modules.filter((module) => module.name === moduleName) : [...modules];
254430
+ if (moduleName && selected.length === 0) {
254431
+ throw new Error(`未找到数据库模块: ${moduleName}(可用: ${modules.map((module) => module.name).join(", ") || "none"})`);
254432
+ }
254433
+ const allObjects = selected.flatMap(moduleObjects2);
254434
+ const objects = target ? allObjects.filter((object) => objectMatchesTarget(object, target)) : allObjects;
254435
+ if (target && objects.length === 0)
254436
+ throw new Error(`未找到数据库对象: ${target}`);
254437
+ return {
254438
+ version: 1,
254439
+ scope: "database-ai-context",
254440
+ root,
254441
+ selectedModules: selected.map((module) => module.name),
254442
+ target: target ?? null,
254443
+ objects,
254444
+ maintenance: {
254445
+ structureSource: "drizzle",
254446
+ behaviorSource: "sql",
254447
+ generatedPaths: ["generated/", "bootstrap/schema.sql", "output/database-audit/"],
254448
+ protectedPaths: ["migrations/", "supabase/migrations/", "generated/"],
254449
+ forwardMigrationPath: "migrations/<timestamp>_<intent>.sql",
254450
+ rules: [
254451
+ "Edit the selected maintained source, not a generated snapshot.",
254452
+ "Keep RPC, RLS, trigger and grant behavior in explicit SQL sources.",
254453
+ "Create a new forward migration; never rewrite an applied migration.",
254454
+ "Run lint, focused behavior tests and catalog reconciliation after edits."
254455
+ ]
254456
+ },
254457
+ verification: [
254458
+ "supacloud-cli db lint --module_file db/modules.ts",
254459
+ "supacloud-cli db explain --target <object>",
254460
+ 'supacloud-cli db module_check --module_file db/modules.ts --database_url "$DATABASE_URL"'
254461
+ ]
254462
+ };
254463
+ }
253729
254464
  async function runModuleCheck(module, executor, schemas = ["public"]) {
253730
254465
  const catalog = await readCatalog(executor, schemas);
253731
254466
  return reconcileModule(module, catalog);
@@ -253769,6 +254504,10 @@ async function runExplain2(args, root) {
253769
254504
  const text = explainObject(manifest, target);
253770
254505
  return textResult2(text, text.startsWith("未找到对象"));
253771
254506
  }
254507
+ async function runContext2(args, root) {
254508
+ const modules = await loadDatabaseModules(root, args.module_file);
254509
+ return textResult2(JSON.stringify(buildDatabaseAiContext(modules, root, args.module, args.target), null, 2));
254510
+ }
253772
254511
  var defaultLiteSpawn = async (command, cwd) => {
253773
254512
  const proc = Bun.spawn({ cmd: command, cwd, stdout: "pipe", stderr: "pipe" });
253774
254513
  const [exitCode, stdout, stderr] = await Promise.all([
@@ -253826,12 +254565,12 @@ function registerDbGovernanceTools(server2, options = {}) {
253826
254565
  const environment = options.environment || process.env;
253827
254566
  const fallbackRoot = options.currentWorkingDirectory || process.cwd();
253828
254567
  const liteSpawn = options.liteSpawn || defaultLiteSpawn;
253829
- server2.tool("db", "Local database governance (@supacloud/db): lint declared modules, explain objects, reconcile against a live catalog or a local SupaCloud Lite project. Actions: lint, explain, module_check", {
253830
- action: withDescription(stringEnum(["lint", "explain", "module_check"]), "Database governance action"),
253831
- module: optional(String2(), "[lint] Only lint this manifest module (default: all)"),
254568
+ server2.tool("db", "Local database governance (@supacloud/db): provide bounded AI context, lint declared modules, explain objects, reconcile against a live catalog or a local SupaCloud Lite project. Actions: context, lint, explain, module_check", {
254569
+ action: withDescription(stringEnum(["context", "lint", "explain", "module_check"]), "Database governance action"),
254570
+ module: optional(String2(), "[context/lint] Only select this manifest module (default: all)"),
253832
254571
  root: optional(String2(), "[*] Project root (default: current directory)"),
253833
254572
  module_file: optional(String2(), "[*] File exporting defineDatabaseModule(...) (default: <root>/db/modules.ts)"),
253834
- target: optional(String2(), "[explain] Policy / function / table name to explain"),
254573
+ target: optional(String2(), "[context/explain] Policy / function / table name to explain"),
253835
254574
  database_url: optional(String2(), "[module_check] Postgres connection URL (default: DATABASE_URL)"),
253836
254575
  schema: optional(String2(), "[module_check] Comma-separated schemas to inspect (default: public)"),
253837
254576
  lite: optional(Boolean2(), "[module_check] Reconcile against a local SupaCloud Lite project (runs supacloud-lite db check)"),
@@ -253839,6 +254578,8 @@ function registerDbGovernanceTools(server2, options = {}) {
253839
254578
  }, async (request) => {
253840
254579
  const root = resolve25(request.root || fallbackRoot);
253841
254580
  switch (request.action) {
254581
+ case "context":
254582
+ return runContext2(request, root);
253842
254583
  case "lint":
253843
254584
  return runLint(request, root);
253844
254585
  case "explain":
@@ -256652,7 +257393,7 @@ function registerRemoteDevTools(server2, options = {}) {
256652
257393
  // package.json
256653
257394
  var package_default = {
256654
257395
  name: "@supacloud/cli",
256655
- version: "0.72.0",
257396
+ version: "0.74.0",
256656
257397
  description: "Project-scoped CLI for SupaCloud users",
256657
257398
  type: "module",
256658
257399
  main: "./dist/index.js",
@@ -256686,7 +257427,7 @@ var package_default = {
256686
257427
  directory: "packages/cli"
256687
257428
  },
256688
257429
  dependencies: {
256689
- "@supacloud/compiler": "^0.40.1",
257430
+ "@supacloud/compiler": "^0.42.0",
256690
257431
  "@supacloud/delivery": "0.8.0",
256691
257432
  "@supacloud/db": "^0.14.0",
256692
257433
  fflate: "^0.8.3",
@@ -256699,6 +257440,7 @@ var package_default = {
256699
257440
  devDependencies: {
256700
257441
  "@supacloud/elysia": "^0.24.1",
256701
257442
  "@types/bun": "^1.4.2",
257443
+ effect: "4.0.2",
256702
257444
  elysia: "2.0.0-beta.21",
256703
257445
  typescript: "^7.0.2"
256704
257446
  }
@@ -256981,6 +257723,8 @@ EXAMPLES
256981
257723
  ${preferredCommand} context --format json
256982
257724
  ${preferredCommand} context --target CaseModule --format json
256983
257725
  ${preferredCommand} doctor
257726
+ ${preferredCommand} db context --root . --module_file db/modules.ts --module cases
257727
+ ${preferredCommand} db context --root . --module_file db/modules.ts --target public.case_create
256984
257728
  ${preferredCommand} db lint --root . --module_file db/modules.ts
256985
257729
  ${preferredCommand} db explain --target public.cases --module_file db/modules.ts
256986
257730
  ${preferredCommand} db module_check --module_file db/modules.ts --database_url "postgresql://..."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.72.0",
3
+ "version": "0.74.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "directory": "packages/cli"
35
35
  },
36
36
  "dependencies": {
37
- "@supacloud/compiler": "^0.40.1",
37
+ "@supacloud/compiler": "^0.42.0",
38
38
  "@supacloud/delivery": "0.8.0",
39
39
  "@supacloud/db": "^0.14.0",
40
40
  "fflate": "^0.8.3",
@@ -47,6 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@supacloud/elysia": "^0.24.1",
49
49
  "@types/bun": "^1.4.2",
50
+ "effect": "4.0.2",
50
51
  "elysia": "2.0.0-beta.21",
51
52
  "typescript": "^7.0.2"
52
53
  }
@@ -31,12 +31,21 @@ Use `supacloud-cli` as the project-level control surface and keep durable change
31
31
 
32
32
  1. Classify the request using [references/command-map.md](references/command-map.md).
33
33
  2. For database work, load [references/database-workflow.md](references/database-workflow.md) and choose either new-change or historical-drift reconciliation.
34
- 3. Prefer the smallest read-only inspection that proves current state.
35
- 4. Create or update version-controlled artifacts before remote writes.
36
- 5. Verify locally, inspect the diff, and run the narrowest relevant tests.
37
- 6. Preview remote changes with `--dry_run`.
38
- 7. Apply only within the user-authorized environment and scope.
39
- 8. Read back migration history and affected resources; report exact evidence and any remaining drift.
34
+ 3. For AI-assisted database changes, first request bounded context:
35
+
36
+ ```bash
37
+ supacloud-cli db context --module_file db/modules.ts --module <module>
38
+ supacloud-cli db context --module_file db/modules.ts --target <object>
39
+ ```
40
+
41
+ Use the returned source/test paths and ownership boundary; do not scan or
42
+ edit the complete schema dump.
43
+ 4. Prefer the smallest read-only inspection that proves current state.
44
+ 5. Create or update version-controlled artifacts before remote writes.
45
+ 6. Verify locally, inspect the diff, and run the narrowest relevant tests.
46
+ 7. Preview remote changes with `--dry_run`.
47
+ 8. Apply only within the user-authorized environment and scope.
48
+ 9. Read back migration history and affected resources; report exact evidence and any remaining drift.
40
49
 
41
50
  ## Project endpoint inspection
42
51