@supacloud/compiler 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @supacloud/compiler
2
2
 
3
+ FA-derived direct-command RPC ownership, contract inspection and POST command
4
+ protocol migration are documented in `docs/fa-consumer-governance.md` in the
5
+ repository. `context <module> --json` reports `routeContracts` and standalone
6
+ command execution plans; these are declarations and obligations, not runtime proof.
7
+
3
8
  SupaCloud 应用静态编译器:读取 `@supacloud/app` 装饰器元数据的原生 TypeScript AST,构建 ApplicationGraph,做静态校验,并生成**无反射、无容器**的工厂代码与 manifest。
4
9
 
5
10
  本包不依赖 `@supacloud/app`:AST 只按装饰器名匹配(`Module`/`Injectable`/`Inject`/`Command`/`Query`/`Controller`/`Get`/`Post`/`Put`/`Patch`/`Delete`/`defineModule`/`InjectionToken`),不校验 import 来源。
@@ -185,6 +190,7 @@ IDE 和 AI agent 做状态机漂移检查。
185
190
  | `unsupported-provider-helper` | warn(strict 时 error) | functional provider 的动态参数无法安全展开为静态 factory |
186
191
  | `dynamic-aspect-reference` | error | aspects 不是显式数组字面量,或包含 spread/表达式/字符串 pointcut |
187
192
  | `invalid-aspect-reference` | error | aspect 不是可静态解析的函数声明、箭头函数或函数表达式 |
193
+ | `invalid-command-mode` | error | transaction/idempotency 必须显式为 `"required"` 或 `"none"`,不允许拼写错误或动态值悄悄关闭治理(SC4012) |
188
194
  | `missing-deps` | warn(strict 时 error) | 构造/工厂依赖无法静态解析 |
189
195
  | `generated-any` | warn(strict 时 error) | 生成的 TypeScript 产物包含 `any` |
190
196
  | `source-any` | warn(strict 时 error) | 未被排除的生产源码包含显式 `any` |
@@ -238,6 +244,20 @@ supacloud-compiler context case --root ./app --json
238
244
  `supacloud-compiler fix ./fix.json --dry-run` 调用;CLI 默认预览,需显式
239
245
  使用 `--write` 才写盘。写盘前会重新解析 AST,
240
246
  前置条件不满足时拒绝修改,并通过临时文件原子替换。
247
+ CLI 修复的 `targetFile` 相对于配置的源码根目录解析,也可以用 `--root` 显式指定;
248
+ JSON 修复文件本身仍相对于当前工作目录读取。
249
+
250
+ 上下游分别沿单一方向遍历,不会经过共享基础模块再扩散到无关兄弟业务。
251
+ 上下文包还包含准确的切面源文件、校验诊断和 `executionPlans`;`explain <module>`
252
+ 也展示静态执行计划。计划描述标准命令治理;自定义 executor 的内部实现和短路行为
253
+ 仍需运行时追踪验证。成功审计在 handler 返回后执行,而非 handler 之前。
254
+
255
+ 例如 `@Command({ transaction: "requried" })` 会报告 `invalid-command-mode`,
256
+ 并输出 `set_command_mode` 修复建议。必须显式给 fix 的 `value` 选择 `"required"`
257
+ 或 `"none"` 才能预览或写入;不会推断较弱权限。若诊断后的源表达式变化,修复拒绝写盘。
258
+
259
+ `compileProject()` 现在默认在存在 error 时保留已有产物。仅诊断/迁移工具可以显式
260
+ 设置 `writeOnError: true` 导出错误版本;这些产物不应被部署或视为可执行成功产物。
241
261
 
242
262
  ## 编译基准
243
263
 
package/dist/cli.js CHANGED
@@ -668,9 +668,10 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
668
668
  className: classInfo.decl.name?.text ?? name,
669
669
  name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
670
670
  permission: stringLiteralProp(meta, "permission"),
671
- transaction: commandModeProp(meta, "transaction") ?? "none",
671
+ rpc: checkedRpc(meta, ctx),
672
+ transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
672
673
  audit: stringLiteralProp(meta, "audit"),
673
- idempotency: commandModeProp(meta, "idempotency") ?? "none",
674
+ idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
674
675
  standalone: true,
675
676
  aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
676
677
  });
@@ -960,9 +961,10 @@ function parseModule(candidate, nameByNode, ctx) {
960
961
  className: cls.name?.text ?? "<anonymous>",
961
962
  name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
962
963
  permission: stringLiteralProp(meta, "permission"),
963
- transaction: commandModeProp(meta, "transaction") ?? "none",
964
+ rpc: checkedRpc(meta, ctx),
965
+ transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
964
966
  audit: stringLiteralProp(meta, "audit"),
965
- idempotency: commandModeProp(meta, "idempotency") ?? "none",
967
+ idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
966
968
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
967
969
  ...aspects2.length > 0 ? { aspects: aspects2 } : {}
968
970
  });
@@ -1092,6 +1094,31 @@ function commandModeProp(object, name) {
1092
1094
  const value = stringLiteralProp(object, name);
1093
1095
  return value === "required" || value === "none" ? value : undefined;
1094
1096
  }
1097
+ function checkedCommandMode(object, property, ctx, command) {
1098
+ const expression = getProp(object, property);
1099
+ const value = commandModeProp(object, property);
1100
+ if (expression && value === undefined) {
1101
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1102
+ ctx.diagnostics.push({
1103
+ severity: "error",
1104
+ code: "invalid-command-mode",
1105
+ errorCode: "SC4012",
1106
+ docsUrl: "https://supacloud.dev/errors/SC4012",
1107
+ message: `${command}.${property} must be the explicit literal "required" or "none"; invalid governance cannot be disabled silently.`,
1108
+ file,
1109
+ line: lineOf(expression),
1110
+ suggestion: `Choose the intended ${property} policy explicitly; use set_command_mode with value "required" or "none".`,
1111
+ fix: {
1112
+ type: "set_command_mode",
1113
+ targetFile: file,
1114
+ command,
1115
+ property,
1116
+ expectedExpression: nodeText(expression)
1117
+ }
1118
+ });
1119
+ }
1120
+ return value;
1121
+ }
1095
1122
  function parseProvider(el, exportsSet, ctx) {
1096
1123
  const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
1097
1124
  const line = lineOf(el);
@@ -1552,6 +1579,10 @@ function parseController(input, ctx) {
1552
1579
  path: routePath,
1553
1580
  handler: propertyName(method.name)
1554
1581
  };
1582
+ const signature = ctx.checker.getSignatureFromDeclaration(method);
1583
+ const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
1584
+ if (resultType && /\bResponse\b/.test(resultType))
1585
+ route.nativeResponse = true;
1555
1586
  const pathParams = [];
1556
1587
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
1557
1588
  let match;
@@ -1700,6 +1731,23 @@ function parseController(input, ctx) {
1700
1731
  }
1701
1732
  const optionsArg = args[1];
1702
1733
  if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
1734
+ const contract = getProp(optionsArg, "contract");
1735
+ if (contract && ts3.isObjectLiteralExpression(contract)) {
1736
+ route.contract = {};
1737
+ for (const field of ["body", "response", "evidence"]) {
1738
+ const value = stringLiteralProp(contract, field);
1739
+ const allowed = field === "body" ? ["framework", "domain"] : field === "response" ? ["framework", "native-json", "binary", "stream"] : undefined;
1740
+ if (getProp(contract, field) && (!value || allowed && !allowed.includes(value))) {
1741
+ ctx.diagnostics.push({
1742
+ severity: "error",
1743
+ code: "invalid-route-contract",
1744
+ file,
1745
+ message: `Invalid contract.${field} on ${route.handler}; use an explicit supported string literal.`
1746
+ });
1747
+ } else if (value)
1748
+ Object.assign(route.contract, { [field]: value });
1749
+ }
1750
+ }
1703
1751
  for (const field of ["body", "params", "query", "response"]) {
1704
1752
  const schemaExpr = getProp(optionsArg, field);
1705
1753
  if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
@@ -1707,6 +1755,11 @@ function parseController(input, ctx) {
1707
1755
  const importPath = importPathOf(schemaExpr, ctx);
1708
1756
  if (importPath)
1709
1757
  schemaImports[schemaExpr.text] = importPath;
1758
+ const declaration = resolveDeclaration(schemaExpr, ctx)[0];
1759
+ const typeName = ctx.checker.typeToString(ctx.checker.getTypeAtLocation(schemaExpr));
1760
+ const initializer = declaration && ts3.isVariableDeclaration(declaration) ? declaration.initializer : undefined;
1761
+ const opaque = /\bT(?:Unknown|Any)\b/.test(typeName) || initializer && ts3.isCallExpression(initializer) && ts3.isPropertyAccessExpression(initializer.expression) && ["Unknown", "Any"].includes(initializer.expression.name.text);
1762
+ (route.schemaKinds ??= {})[field] = opaque ? "opaque" : "declared";
1710
1763
  }
1711
1764
  }
1712
1765
  const commandExpr = getProp(optionsArg, "command");
@@ -1801,6 +1854,23 @@ function parseController(input, ctx) {
1801
1854
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
1802
1855
  };
1803
1856
  }
1857
+ function checkedRpc(meta, ctx) {
1858
+ const expression = getProp(meta, "rpc");
1859
+ if (!expression)
1860
+ return;
1861
+ const value = stringLiteralProp(meta, "rpc");
1862
+ if (value?.trim())
1863
+ return value;
1864
+ ctx.diagnostics.push({
1865
+ severity: "error",
1866
+ code: "invalid-command-rpc",
1867
+ file: sourcePath(ctx.rootDir, meta.getSourceFile().fileName),
1868
+ line: lineOf(meta),
1869
+ message: "Command rpc must be a non-empty string literal identifying a configured adapter.",
1870
+ suggestion: "Declare a named RPC adapter and its tested persistence capabilities."
1871
+ });
1872
+ return;
1873
+ }
1804
1874
  function classDeps(cls, ctx) {
1805
1875
  const injectable = parseInjectableOptions(cls, ctx);
1806
1876
  const ctor = cls.members.find(ts3.isConstructorDeclaration);
@@ -2156,6 +2226,7 @@ function parseAspectRefs(expression, ctx, owner) {
2156
2226
  const declaredFile = declaration.getSourceFile().fileName;
2157
2227
  const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2158
2228
  refs.push({
2229
+ ...projectLocal ? { file: sourcePath(ctx.rootDir, declaredFile) } : {},
2159
2230
  name,
2160
2231
  expression: element.text,
2161
2232
  importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
@@ -2298,6 +2369,7 @@ var INTERFACES = `export interface CompiledRoute {
2298
2369
  }
2299
2370
 
2300
2371
  export interface CompiledCommand {
2372
+ rpc?: string;
2301
2373
  className: string;
2302
2374
  name: string;
2303
2375
  permission: string;
@@ -2823,6 +2895,7 @@ ${indent(item, 2)}`).join(",")}
2823
2895
  `transaction: ${JSON.stringify(command.transaction)}`,
2824
2896
  ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
2825
2897
  `idempotency: ${JSON.stringify(command.idempotency)}`,
2898
+ ...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
2826
2899
  ...command.standalone ? ["standalone: true"] : [],
2827
2900
  ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
2828
2901
  ];
@@ -3690,6 +3763,10 @@ var COMPILER_DIAGNOSTIC_CODES = {
3690
3763
  "invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
3691
3764
  "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3692
3765
  "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
3766
+ "invalid-command-mode": { code: "SC4012", docsUrl: "https://supacloud.dev/errors/SC4012" },
3767
+ "invalid-command-rpc": { code: "SC4013", docsUrl: "https://supacloud.dev/errors/SC4013" },
3768
+ "command-rpc-unavailable": { code: "SC4014", docsUrl: "https://supacloud.dev/errors/SC4014" },
3769
+ "invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
3693
3770
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" },
3694
3771
  "invalid-feature-states": { code: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
3695
3772
  "duplicate-feature-transition": { code: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
@@ -4119,8 +4196,18 @@ function validateGraph(graph, options = false) {
4119
4196
  permission: `${module.name}.${command.name}`
4120
4197
  });
4121
4198
  }
4199
+ if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
4200
+ error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
4201
+ }
4122
4202
  if (typeof options === "object" && options.commandCapabilities) {
4123
- const caps = options.commandCapabilities;
4203
+ const hostCaps = options.commandCapabilities;
4204
+ const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
4205
+ const caps = command.rpc ? {
4206
+ permission: hostCaps.permission,
4207
+ audit: rpcCaps?.audit === true,
4208
+ transaction: rpcCaps?.transaction === true,
4209
+ idempotency: rpcCaps?.idempotency === true
4210
+ } : hostCaps;
4124
4211
  const location = `${command.className} (${module.file})`;
4125
4212
  if (command.permission && caps.permission === false) {
4126
4213
  error("command-permission-unsupported", `Command ${command.name} declares permission, but runtime permission checks are unavailable (${location}).`, module.file, module.line);
@@ -4692,8 +4779,10 @@ function inspectRouteContracts(graph) {
4692
4779
  missing.push("params");
4693
4780
  if ((route.queryBindings?.length || route.handlerParams?.some((param) => param.kind === "query")) && !route.query)
4694
4781
  missing.push("query");
4695
- if (!route.response)
4782
+ if (!route.response && !["binary", "stream"].includes(route.contract?.response ?? ""))
4696
4783
  missing.push("response");
4784
+ const body = !route.body ? "missing" : route.contract?.body === "domain" ? "domain" : route.schemaKinds?.body === "opaque" ? "opaque" : "framework-declared";
4785
+ const response = route.contract?.response ?? (route.nativeResponse ? "native-response-unclassified" : route.schemaKinds?.response === "opaque" ? "opaque" : !route.response ? "missing" : "framework-declared");
4697
4786
  return {
4698
4787
  module: module.name,
4699
4788
  controller: controller.className,
@@ -4701,7 +4790,21 @@ function inspectRouteContracts(graph) {
4701
4790
  method: route.method,
4702
4791
  path: `${controller.path}${route.path}`,
4703
4792
  file: controller.file,
4704
- missing
4793
+ missing,
4794
+ validation: {
4795
+ body,
4796
+ response,
4797
+ schemas: route.schemaKinds ?? {},
4798
+ evidence: route.contract?.evidence ?? null,
4799
+ verified: false,
4800
+ obligations: [
4801
+ "Exercise actual HTTP request and response boundaries.",
4802
+ ...response === "opaque" ? ["Decode the response's business fields; an opaque schema accepts unvalidated output."] : [],
4803
+ ...body === "domain" || body === "opaque" ? ["Prove invalid input is rejected by the domain before writes."] : [],
4804
+ ...response === "native-json" || response === "native-response-unclassified" ? ["Validate serialized JSON explicitly; native Response bypasses framework response schemas."] : [],
4805
+ ...["binary", "stream"].includes(response) ? ["Test transport headers, access and bytes without JSON decoding."] : []
4806
+ ]
4807
+ }
4705
4808
  };
4706
4809
  })));
4707
4810
  }
@@ -4771,7 +4874,7 @@ async function compileProject(options) {
4771
4874
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4772
4875
  artifactHashes: options.cache?.generatedHashes
4773
4876
  };
4774
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
4877
+ const written = !hasErrors || options.writeOnError === true ? await generateApplication(graph, generatedOptions) : [];
4775
4878
  const stats = graph.cacheStats ? {
4776
4879
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
4777
4880
  changedFiles: [],
@@ -4910,22 +5013,23 @@ function createContextPack(graph, subject) {
4910
5013
  }
4911
5014
  const byName = new Map(graph.modules.map((module) => [module.name, module]));
4912
5015
  const selected = new Set([subjectModule.name]);
4913
- const queue = [subjectModule.name];
4914
- while (queue.length > 0) {
4915
- const current = queue.shift();
4916
- if (!current)
4917
- continue;
4918
- const module = byName.get(current);
4919
- if (!module)
4920
- continue;
4921
- const neighbors = [
4922
- ...module.imports,
4923
- ...graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name)
4924
- ];
4925
- for (const neighbor of neighbors) {
4926
- if (!selected.has(neighbor) && byName.has(neighbor)) {
4927
- selected.add(neighbor);
4928
- queue.push(neighbor);
5016
+ for (const direction of ["imports", "dependents"]) {
5017
+ const visited = new Set([subjectModule.name]);
5018
+ const queue = [subjectModule.name];
5019
+ while (queue.length > 0) {
5020
+ const current = queue.shift();
5021
+ if (!current)
5022
+ continue;
5023
+ const module = byName.get(current);
5024
+ if (!module)
5025
+ continue;
5026
+ const neighbors = direction === "imports" ? module.imports : graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
5027
+ for (const neighbor of neighbors) {
5028
+ if (!visited.has(neighbor) && byName.has(neighbor)) {
5029
+ visited.add(neighbor);
5030
+ selected.add(neighbor);
5031
+ queue.push(neighbor);
5032
+ }
4929
5033
  }
4930
5034
  }
4931
5035
  }
@@ -4933,7 +5037,8 @@ function createContextPack(graph, subject) {
4933
5037
  const files = [...new Set(modules.flatMap((module) => [
4934
5038
  module.file,
4935
5039
  ...module.providers.map((provider) => provider.file),
4936
- ...module.controllers.map((controller) => controller.file)
5040
+ ...module.controllers.map((controller) => controller.file),
5041
+ ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
4937
5042
  ]))].sort();
4938
5043
  const referencedTokens = new Set;
4939
5044
  for (const module of modules) {
@@ -4952,12 +5057,76 @@ function createContextPack(graph, subject) {
4952
5057
  modules,
4953
5058
  files,
4954
5059
  externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
5060
+ executionPlans: createExecutionPlans({ ...graph, modules }),
5061
+ routeContracts: inspectRouteContracts({ ...graph, modules }),
5062
+ diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
4955
5063
  relatedModules: {
4956
5064
  imports: subjectModule.imports.filter((name) => selected.has(name)),
4957
5065
  importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
4958
5066
  }
4959
5067
  };
4960
5068
  }
5069
+ function allAspects(module) {
5070
+ return [
5071
+ ...module.aspects ?? [],
5072
+ ...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
5073
+ ...module.commands.flatMap((command) => command.aspects ?? []),
5074
+ ...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
5075
+ ];
5076
+ }
5077
+ function createExecutionPlans(graph) {
5078
+ const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
5079
+ return graph.modules.flatMap((module) => [
5080
+ ...module.controllers.flatMap((controller) => controller.routes.map((route) => {
5081
+ const command = module.commands.find((item) => item.className === route.command);
5082
+ const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
5083
+ return {
5084
+ module: module.name,
5085
+ kind: "route",
5086
+ name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
5087
+ ...command ? { command: command.name } : {},
5088
+ stages: [
5089
+ ...aspects(`module:${module.name}`, module.aspects),
5090
+ ...aspects("route", route.aspects),
5091
+ ...aspects("command", command?.aspects),
5092
+ ...command ? [
5093
+ "commandExecutor",
5094
+ "authorize",
5095
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
5096
+ ...command.idempotency === "required" ? ["idempotency"] : [],
5097
+ ...command.transaction === "required" ? ["transaction"] : []
5098
+ ]
5099
+ ] : [],
5100
+ "handler",
5101
+ ...command?.audit && !command.rpc ? ["audit"] : []
5102
+ ]
5103
+ };
5104
+ })),
5105
+ ...module.commands.map((command) => ({
5106
+ module: module.name,
5107
+ kind: "command",
5108
+ name: command.name,
5109
+ command: command.name,
5110
+ stages: [
5111
+ "authorize",
5112
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
5113
+ ...command.idempotency === "required" ? ["idempotency"] : [],
5114
+ ...command.transaction === "required" ? ["transaction"] : []
5115
+ ],
5116
+ ...aspects(`module:${module.name}`, module.aspects),
5117
+ ...aspects("command", command.aspects),
5118
+ "handler",
5119
+ ...command.audit && !command.rpc ? ["audit"] : []
5120
+ ]
5121
+ })),
5122
+ ...(module.jobs ?? []).map((job) => ({
5123
+ module: module.name,
5124
+ kind: "job",
5125
+ name: job.name,
5126
+ stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
5127
+ }))
5128
+ ]);
5129
+ }
4961
5130
  function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
4962
5131
  const checks = [
4963
5132
  {
@@ -4997,7 +5166,8 @@ function explainModule(graph, module) {
4997
5166
  ` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
4998
5167
  ` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
4999
5168
  ` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
5000
- ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`
5169
+ ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
5170
+ ...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
5001
5171
  ].join(`
5002
5172
  `);
5003
5173
  }
@@ -5514,6 +5684,18 @@ async function applyDiagnosticFix(fix, options = {}) {
5514
5684
  let source = parse(file, original);
5515
5685
  let content;
5516
5686
  switch (fix.type) {
5687
+ case "set_command_mode": {
5688
+ if (fix.property !== "transaction" && fix.property !== "idempotency" || fix.value !== "required" && fix.value !== "none") {
5689
+ throw new Error("Command mode fix requires an explicit property and policy value");
5690
+ }
5691
+ const object = decoratorObject(findClass(source, fix.command), "Command");
5692
+ const current = property(object, fix.property);
5693
+ if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
5694
+ throw new Error("Command mode changed since diagnosis; analyze the project again");
5695
+ }
5696
+ content = replaceProperty(source, object, fix.property, ts5.factory.createStringLiteral(fix.value));
5697
+ break;
5698
+ }
5517
5699
  case "add_module_import": {
5518
5700
  if (!fix.importPath || !fix.symbol)
5519
5701
  throw new Error("Module fix requires importPath and symbol");
@@ -5816,7 +5998,7 @@ async function run() {
5816
5998
  if (!query)
5817
5999
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
5818
6000
  const fix = JSON.parse(await readFile3(resolve7(process.cwd(), query), "utf8"));
5819
- const result = await applyDiagnosticFix(fix, { rootDir: process.cwd(), dryRun });
6001
+ const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
5820
6002
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
5821
6003
  } else if (command === "compile") {
5822
6004
  const result = await compileProject(compileDefaults);
@@ -5934,8 +6116,8 @@ Source change detected; compiling...`);
5934
6116
  process.exit(1);
5935
6117
  }
5936
6118
  try {
5937
- const graph = await analyzeProject(resolvedRoot);
5938
- const pack = createContextPack(graph, query);
6119
+ const result = await checkProject(compileDefaults);
6120
+ const pack = createContextPack({ ...result.graph, diagnostics: result.diagnostics }, query);
5939
6121
  if (json) {
5940
6122
  console.log(JSON.stringify(pack, null, 2));
5941
6123
  } else {
@@ -5945,7 +6127,9 @@ Source change detected; compiling...`);
5945
6127
  ` files: ${pack.files.join(", ") || "-"}`,
5946
6128
  ` external tokens: ${pack.externalTokens.join(", ") || "-"}`,
5947
6129
  ` imports: ${pack.relatedModules.imports.join(", ") || "-"}`,
5948
- ` imported by: ${pack.relatedModules.importedBy.join(", ") || "-"}`
6130
+ ` imported by: ${pack.relatedModules.importedBy.join(", ") || "-"}`,
6131
+ ...pack.executionPlans.map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`),
6132
+ ...pack.diagnostics.map((diagnostic) => ` ${diagnostic.code}: ${diagnostic.message}`)
5949
6133
  ].join(`
5950
6134
  `));
5951
6135
  }
package/dist/compile.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CheckProjectResult, CompileOptions, CompileResult } from "./types";
2
2
  /**
3
3
  * Complete compilation pipeline: AST analysis -> validation -> generate static factory code and manifest.
4
- * Files are emitted even when error-level diagnostics exist; caller decides adoption based on diagnostics.
4
+ * Errors preserve the last working artifacts unless writeOnError is explicitly enabled.
5
5
  */
6
6
  export declare function compileProject(options: CompileOptions): Promise<CompileResult>;
7
7
  /**
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { applyDiagnosticFix } from "./fixes";
4
4
  export type { AppliedDiagnosticFix, ApplyDiagnosticFixOptions } from "./fixes";
5
5
  export { checkProject, compileProject } from "./compile";
6
6
  export { watchProject } from "./watch";
7
- export { createContextPack, doctorProject, explainGraph, formatGraph, exportGraphDot, exportGraphMermaid, } from "./inspect";
7
+ export { createContextPack, createExecutionPlans, doctorProject, explainGraph, formatGraph, exportGraphDot, exportGraphMermaid, } from "./inspect";
8
8
  export { createIncrementalCompiler } from "./incremental";
9
9
  export { createDependencyGraphCache } from "./incremental";
10
10
  export { ModuleDependencyGraph } from "./incremental";
@@ -15,7 +15,7 @@ export { TraitCompiler } from "./traits";
15
15
  export type { TraitCompilation, TraitHandler, TraitKind, TraitRecord } from "./traits";
16
16
  export { generateApplication, renderApplication } from "./generate";
17
17
  export type { GenerateOptions, RenderedArtifacts } from "./generate";
18
- export type { ContextPack, DoctorResult } from "./inspect";
18
+ export type { ContextPack, DoctorResult, ExecutionPlan } from "./inspect";
19
19
  export { validateGraph, COMPILER_DIAGNOSTIC_CODES } from "./validate";
20
20
  export { scanGeneratedArtifacts, scanProductionSource } from "./type-safety";
21
21
  export type { TypeSafetyScanOptions } from "./type-safety";
package/dist/index.js CHANGED
@@ -662,9 +662,10 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
662
662
  className: classInfo.decl.name?.text ?? name,
663
663
  name: stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name,
664
664
  permission: stringLiteralProp(meta, "permission"),
665
- transaction: commandModeProp(meta, "transaction") ?? "none",
665
+ rpc: checkedRpc(meta, ctx),
666
+ transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
666
667
  audit: stringLiteralProp(meta, "audit"),
667
- idempotency: commandModeProp(meta, "idempotency") ?? "none",
668
+ idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
668
669
  standalone: true,
669
670
  aspects: parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${classInfo.decl.name?.text ?? name}`)
670
671
  });
@@ -954,9 +955,10 @@ function parseModule(candidate, nameByNode, ctx) {
954
955
  className: cls.name?.text ?? "<anonymous>",
955
956
  name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
956
957
  permission: stringLiteralProp(meta, "permission"),
957
- transaction: commandModeProp(meta, "transaction") ?? "none",
958
+ rpc: checkedRpc(meta, ctx),
959
+ transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
958
960
  audit: stringLiteralProp(meta, "audit"),
959
- idempotency: commandModeProp(meta, "idempotency") ?? "none",
961
+ idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
960
962
  ...booleanProp(meta, "standalone") ? { standalone: true } : {},
961
963
  ...aspects2.length > 0 ? { aspects: aspects2 } : {}
962
964
  });
@@ -1086,6 +1088,31 @@ function commandModeProp(object, name) {
1086
1088
  const value = stringLiteralProp(object, name);
1087
1089
  return value === "required" || value === "none" ? value : undefined;
1088
1090
  }
1091
+ function checkedCommandMode(object, property, ctx, command) {
1092
+ const expression = getProp(object, property);
1093
+ const value = commandModeProp(object, property);
1094
+ if (expression && value === undefined) {
1095
+ const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
1096
+ ctx.diagnostics.push({
1097
+ severity: "error",
1098
+ code: "invalid-command-mode",
1099
+ errorCode: "SC4012",
1100
+ docsUrl: "https://supacloud.dev/errors/SC4012",
1101
+ message: `${command}.${property} must be the explicit literal "required" or "none"; invalid governance cannot be disabled silently.`,
1102
+ file,
1103
+ line: lineOf(expression),
1104
+ suggestion: `Choose the intended ${property} policy explicitly; use set_command_mode with value "required" or "none".`,
1105
+ fix: {
1106
+ type: "set_command_mode",
1107
+ targetFile: file,
1108
+ command,
1109
+ property,
1110
+ expectedExpression: nodeText(expression)
1111
+ }
1112
+ });
1113
+ }
1114
+ return value;
1115
+ }
1089
1116
  function parseProvider(el, exportsSet, ctx) {
1090
1117
  const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
1091
1118
  const line = lineOf(el);
@@ -1546,6 +1573,10 @@ function parseController(input, ctx) {
1546
1573
  path: routePath,
1547
1574
  handler: propertyName(method.name)
1548
1575
  };
1576
+ const signature = ctx.checker.getSignatureFromDeclaration(method);
1577
+ const resultType = signature && ctx.checker.typeToString(signature.getReturnType());
1578
+ if (resultType && /\bResponse\b/.test(resultType))
1579
+ route.nativeResponse = true;
1549
1580
  const pathParams = [];
1550
1581
  const paramRegex = /:([a-zA-Z0-9_]+)/g;
1551
1582
  let match;
@@ -1694,6 +1725,23 @@ function parseController(input, ctx) {
1694
1725
  }
1695
1726
  const optionsArg = args[1];
1696
1727
  if (optionsArg && ts3.isObjectLiteralExpression(optionsArg)) {
1728
+ const contract = getProp(optionsArg, "contract");
1729
+ if (contract && ts3.isObjectLiteralExpression(contract)) {
1730
+ route.contract = {};
1731
+ for (const field of ["body", "response", "evidence"]) {
1732
+ const value = stringLiteralProp(contract, field);
1733
+ const allowed = field === "body" ? ["framework", "domain"] : field === "response" ? ["framework", "native-json", "binary", "stream"] : undefined;
1734
+ if (getProp(contract, field) && (!value || allowed && !allowed.includes(value))) {
1735
+ ctx.diagnostics.push({
1736
+ severity: "error",
1737
+ code: "invalid-route-contract",
1738
+ file,
1739
+ message: `Invalid contract.${field} on ${route.handler}; use an explicit supported string literal.`
1740
+ });
1741
+ } else if (value)
1742
+ Object.assign(route.contract, { [field]: value });
1743
+ }
1744
+ }
1697
1745
  for (const field of ["body", "params", "query", "response"]) {
1698
1746
  const schemaExpr = getProp(optionsArg, field);
1699
1747
  if (schemaExpr && ts3.isIdentifier(schemaExpr)) {
@@ -1701,6 +1749,11 @@ function parseController(input, ctx) {
1701
1749
  const importPath = importPathOf(schemaExpr, ctx);
1702
1750
  if (importPath)
1703
1751
  schemaImports[schemaExpr.text] = importPath;
1752
+ const declaration = resolveDeclaration(schemaExpr, ctx)[0];
1753
+ const typeName = ctx.checker.typeToString(ctx.checker.getTypeAtLocation(schemaExpr));
1754
+ const initializer = declaration && ts3.isVariableDeclaration(declaration) ? declaration.initializer : undefined;
1755
+ const opaque = /\bT(?:Unknown|Any)\b/.test(typeName) || initializer && ts3.isCallExpression(initializer) && ts3.isPropertyAccessExpression(initializer.expression) && ["Unknown", "Any"].includes(initializer.expression.name.text);
1756
+ (route.schemaKinds ??= {})[field] = opaque ? "opaque" : "declared";
1704
1757
  }
1705
1758
  }
1706
1759
  const commandExpr = getProp(optionsArg, "command");
@@ -1795,6 +1848,23 @@ function parseController(input, ctx) {
1795
1848
  schemaImports: Object.keys(schemaImports).length > 0 ? schemaImports : undefined
1796
1849
  };
1797
1850
  }
1851
+ function checkedRpc(meta, ctx) {
1852
+ const expression = getProp(meta, "rpc");
1853
+ if (!expression)
1854
+ return;
1855
+ const value = stringLiteralProp(meta, "rpc");
1856
+ if (value?.trim())
1857
+ return value;
1858
+ ctx.diagnostics.push({
1859
+ severity: "error",
1860
+ code: "invalid-command-rpc",
1861
+ file: sourcePath(ctx.rootDir, meta.getSourceFile().fileName),
1862
+ line: lineOf(meta),
1863
+ message: "Command rpc must be a non-empty string literal identifying a configured adapter.",
1864
+ suggestion: "Declare a named RPC adapter and its tested persistence capabilities."
1865
+ });
1866
+ return;
1867
+ }
1798
1868
  function classDeps(cls, ctx) {
1799
1869
  const injectable = parseInjectableOptions(cls, ctx);
1800
1870
  const ctor = cls.members.find(ts3.isConstructorDeclaration);
@@ -2150,6 +2220,7 @@ function parseAspectRefs(expression, ctx, owner) {
2150
2220
  const declaredFile = declaration.getSourceFile().fileName;
2151
2221
  const projectLocal = isProjectSourcePath(declaredFile, ctx.rootDir);
2152
2222
  refs.push({
2223
+ ...projectLocal ? { file: sourcePath(ctx.rootDir, declaredFile) } : {},
2153
2224
  name,
2154
2225
  expression: element.text,
2155
2226
  importPath: projectLocal ? modulePath(ctx.rootDir, declaredFile) : undefined,
@@ -2370,6 +2441,18 @@ async function applyDiagnosticFix(fix, options = {}) {
2370
2441
  let source = parse(file, original);
2371
2442
  let content;
2372
2443
  switch (fix.type) {
2444
+ case "set_command_mode": {
2445
+ if (fix.property !== "transaction" && fix.property !== "idempotency" || fix.value !== "required" && fix.value !== "none") {
2446
+ throw new Error("Command mode fix requires an explicit property and policy value");
2447
+ }
2448
+ const object = decoratorObject(findClass(source, fix.command), "Command");
2449
+ const current = property(object, fix.property);
2450
+ if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
2451
+ throw new Error("Command mode changed since diagnosis; analyze the project again");
2452
+ }
2453
+ content = replaceProperty(source, object, fix.property, ts4.factory.createStringLiteral(fix.value));
2454
+ break;
2455
+ }
2373
2456
  case "add_module_import": {
2374
2457
  if (!fix.importPath || !fix.symbol)
2375
2458
  throw new Error("Module fix requires importPath and symbol");
@@ -2579,6 +2662,7 @@ var INTERFACES = `export interface CompiledRoute {
2579
2662
  }
2580
2663
 
2581
2664
  export interface CompiledCommand {
2665
+ rpc?: string;
2582
2666
  className: string;
2583
2667
  name: string;
2584
2668
  permission: string;
@@ -3104,6 +3188,7 @@ ${indent(item, 2)}`).join(",")}
3104
3188
  `transaction: ${JSON.stringify(command.transaction)}`,
3105
3189
  ...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
3106
3190
  `idempotency: ${JSON.stringify(command.idempotency)}`,
3191
+ ...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
3107
3192
  ...command.standalone ? ["standalone: true"] : [],
3108
3193
  ...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`] : []
3109
3194
  ];
@@ -3871,6 +3956,10 @@ var COMPILER_DIAGNOSTIC_CODES = {
3871
3956
  "invalid-job-scope": { code: "SC4007", docsUrl: "https://supacloud.dev/errors/SC4007" },
3872
3957
  "dynamic-aspect-reference": { code: "SC4010", docsUrl: "https://supacloud.dev/errors/SC4010" },
3873
3958
  "invalid-aspect-reference": { code: "SC4011", docsUrl: "https://supacloud.dev/errors/SC4011" },
3959
+ "invalid-command-mode": { code: "SC4012", docsUrl: "https://supacloud.dev/errors/SC4012" },
3960
+ "invalid-command-rpc": { code: "SC4013", docsUrl: "https://supacloud.dev/errors/SC4013" },
3961
+ "command-rpc-unavailable": { code: "SC4014", docsUrl: "https://supacloud.dev/errors/SC4014" },
3962
+ "invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
3874
3963
  "unused-root-provider": { code: "SC5001", docsUrl: "https://supacloud.dev/errors/SC5001" },
3875
3964
  "invalid-feature-states": { code: "SC6001", docsUrl: "https://supacloud.dev/errors/SC6001" },
3876
3965
  "duplicate-feature-transition": { code: "SC6002", docsUrl: "https://supacloud.dev/errors/SC6002" },
@@ -4300,8 +4389,18 @@ function validateGraph(graph, options = false) {
4300
4389
  permission: `${module.name}.${command.name}`
4301
4390
  });
4302
4391
  }
4392
+ if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
4393
+ error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
4394
+ }
4303
4395
  if (typeof options === "object" && options.commandCapabilities) {
4304
- const caps = options.commandCapabilities;
4396
+ const hostCaps = options.commandCapabilities;
4397
+ const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
4398
+ const caps = command.rpc ? {
4399
+ permission: hostCaps.permission,
4400
+ audit: rpcCaps?.audit === true,
4401
+ transaction: rpcCaps?.transaction === true,
4402
+ idempotency: rpcCaps?.idempotency === true
4403
+ } : hostCaps;
4305
4404
  const location = `${command.className} (${module.file})`;
4306
4405
  if (command.permission && caps.permission === false) {
4307
4406
  error("command-permission-unsupported", `Command ${command.name} declares permission, but runtime permission checks are unavailable (${location}).`, module.file, module.line);
@@ -4873,8 +4972,10 @@ function inspectRouteContracts(graph) {
4873
4972
  missing.push("params");
4874
4973
  if ((route.queryBindings?.length || route.handlerParams?.some((param) => param.kind === "query")) && !route.query)
4875
4974
  missing.push("query");
4876
- if (!route.response)
4975
+ if (!route.response && !["binary", "stream"].includes(route.contract?.response ?? ""))
4877
4976
  missing.push("response");
4977
+ const body = !route.body ? "missing" : route.contract?.body === "domain" ? "domain" : route.schemaKinds?.body === "opaque" ? "opaque" : "framework-declared";
4978
+ const response = route.contract?.response ?? (route.nativeResponse ? "native-response-unclassified" : route.schemaKinds?.response === "opaque" ? "opaque" : !route.response ? "missing" : "framework-declared");
4878
4979
  return {
4879
4980
  module: module.name,
4880
4981
  controller: controller.className,
@@ -4882,7 +4983,21 @@ function inspectRouteContracts(graph) {
4882
4983
  method: route.method,
4883
4984
  path: `${controller.path}${route.path}`,
4884
4985
  file: controller.file,
4885
- missing
4986
+ missing,
4987
+ validation: {
4988
+ body,
4989
+ response,
4990
+ schemas: route.schemaKinds ?? {},
4991
+ evidence: route.contract?.evidence ?? null,
4992
+ verified: false,
4993
+ obligations: [
4994
+ "Exercise actual HTTP request and response boundaries.",
4995
+ ...response === "opaque" ? ["Decode the response's business fields; an opaque schema accepts unvalidated output."] : [],
4996
+ ...body === "domain" || body === "opaque" ? ["Prove invalid input is rejected by the domain before writes."] : [],
4997
+ ...response === "native-json" || response === "native-response-unclassified" ? ["Validate serialized JSON explicitly; native Response bypasses framework response schemas."] : [],
4998
+ ...["binary", "stream"].includes(response) ? ["Test transport headers, access and bytes without JSON decoding."] : []
4999
+ ]
5000
+ }
4886
5001
  };
4887
5002
  })));
4888
5003
  }
@@ -4952,7 +5067,7 @@ async function compileProject(options) {
4952
5067
  treeShakeUnusedProviders: options.treeShakeUnusedProviders,
4953
5068
  artifactHashes: options.cache?.generatedHashes
4954
5069
  };
4955
- const written = !hasErrors || options.writeOnError !== false ? await generateApplication(graph, generatedOptions) : [];
5070
+ const written = !hasErrors || options.writeOnError === true ? await generateApplication(graph, generatedOptions) : [];
4956
5071
  const stats = graph.cacheStats ? {
4957
5072
  cacheHit: graph.cacheStats.reanalyzedModules.length === 0,
4958
5073
  changedFiles: [],
@@ -5461,22 +5576,23 @@ function createContextPack(graph, subject) {
5461
5576
  }
5462
5577
  const byName = new Map(graph.modules.map((module) => [module.name, module]));
5463
5578
  const selected = new Set([subjectModule.name]);
5464
- const queue = [subjectModule.name];
5465
- while (queue.length > 0) {
5466
- const current = queue.shift();
5467
- if (!current)
5468
- continue;
5469
- const module = byName.get(current);
5470
- if (!module)
5471
- continue;
5472
- const neighbors = [
5473
- ...module.imports,
5474
- ...graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name)
5475
- ];
5476
- for (const neighbor of neighbors) {
5477
- if (!selected.has(neighbor) && byName.has(neighbor)) {
5478
- selected.add(neighbor);
5479
- queue.push(neighbor);
5579
+ for (const direction of ["imports", "dependents"]) {
5580
+ const visited = new Set([subjectModule.name]);
5581
+ const queue = [subjectModule.name];
5582
+ while (queue.length > 0) {
5583
+ const current = queue.shift();
5584
+ if (!current)
5585
+ continue;
5586
+ const module = byName.get(current);
5587
+ if (!module)
5588
+ continue;
5589
+ const neighbors = direction === "imports" ? module.imports : graph.modules.filter((candidate) => candidate.imports.includes(module.name)).map((candidate) => candidate.name);
5590
+ for (const neighbor of neighbors) {
5591
+ if (!visited.has(neighbor) && byName.has(neighbor)) {
5592
+ visited.add(neighbor);
5593
+ selected.add(neighbor);
5594
+ queue.push(neighbor);
5595
+ }
5480
5596
  }
5481
5597
  }
5482
5598
  }
@@ -5484,7 +5600,8 @@ function createContextPack(graph, subject) {
5484
5600
  const files = [...new Set(modules.flatMap((module) => [
5485
5601
  module.file,
5486
5602
  ...module.providers.map((provider) => provider.file),
5487
- ...module.controllers.map((controller) => controller.file)
5603
+ ...module.controllers.map((controller) => controller.file),
5604
+ ...allAspects(module).flatMap((aspect) => aspect.file ? [aspect.file] : [])
5488
5605
  ]))].sort();
5489
5606
  const referencedTokens = new Set;
5490
5607
  for (const module of modules) {
@@ -5503,12 +5620,76 @@ function createContextPack(graph, subject) {
5503
5620
  modules,
5504
5621
  files,
5505
5622
  externalTokens: graph.externalTokens.filter((token) => referencedTokens.has(token)),
5623
+ executionPlans: createExecutionPlans({ ...graph, modules }),
5624
+ routeContracts: inspectRouteContracts({ ...graph, modules }),
5625
+ diagnostics: (graph.diagnostics ?? []).filter((diagnostic) => diagnostic.file === undefined || files.includes(diagnostic.file)),
5506
5626
  relatedModules: {
5507
5627
  imports: subjectModule.imports.filter((name) => selected.has(name)),
5508
5628
  importedBy: graph.modules.filter((module) => module.imports.includes(subjectModule.name)).map((module) => module.name).sort()
5509
5629
  }
5510
5630
  };
5511
5631
  }
5632
+ function allAspects(module) {
5633
+ return [
5634
+ ...module.aspects ?? [],
5635
+ ...module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.aspects ?? [])),
5636
+ ...module.commands.flatMap((command) => command.aspects ?? []),
5637
+ ...(module.jobs ?? []).flatMap((job) => job.aspects ?? [])
5638
+ ];
5639
+ }
5640
+ function createExecutionPlans(graph) {
5641
+ const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
5642
+ return graph.modules.flatMap((module) => [
5643
+ ...module.controllers.flatMap((controller) => controller.routes.map((route) => {
5644
+ const command = module.commands.find((item) => item.className === route.command);
5645
+ const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
5646
+ return {
5647
+ module: module.name,
5648
+ kind: "route",
5649
+ name: `${route.method} ${path.length > 1 ? path.replace(/\/+$/, "") : path}`,
5650
+ ...command ? { command: command.name } : {},
5651
+ stages: [
5652
+ ...aspects(`module:${module.name}`, module.aspects),
5653
+ ...aspects("route", route.aspects),
5654
+ ...aspects("command", command?.aspects),
5655
+ ...command ? [
5656
+ "commandExecutor",
5657
+ "authorize",
5658
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
5659
+ ...command.idempotency === "required" ? ["idempotency"] : [],
5660
+ ...command.transaction === "required" ? ["transaction"] : []
5661
+ ]
5662
+ ] : [],
5663
+ "handler",
5664
+ ...command?.audit && !command.rpc ? ["audit"] : []
5665
+ ]
5666
+ };
5667
+ })),
5668
+ ...module.commands.map((command) => ({
5669
+ module: module.name,
5670
+ kind: "command",
5671
+ name: command.name,
5672
+ command: command.name,
5673
+ stages: [
5674
+ "authorize",
5675
+ ...command.rpc ? [`rpc:${command.rpc}`] : [
5676
+ ...command.idempotency === "required" ? ["idempotency"] : [],
5677
+ ...command.transaction === "required" ? ["transaction"] : []
5678
+ ],
5679
+ ...aspects(`module:${module.name}`, module.aspects),
5680
+ ...aspects("command", command.aspects),
5681
+ "handler",
5682
+ ...command.audit && !command.rpc ? ["audit"] : []
5683
+ ]
5684
+ })),
5685
+ ...(module.jobs ?? []).map((job) => ({
5686
+ module: module.name,
5687
+ kind: "job",
5688
+ name: job.name,
5689
+ stages: [...aspects(`module:${module.name}`, module.aspects), ...aspects("job", job.aspects), "jobExecutor", "handler"]
5690
+ }))
5691
+ ]);
5692
+ }
5512
5693
  function doctorProject(rootDir, outDir, graph, upToDate, diagnostics = []) {
5513
5694
  const checks = [
5514
5695
  {
@@ -5548,7 +5729,8 @@ function explainModule(graph, module) {
5548
5729
  ` imported by: ${dependents.length > 0 ? dependents.join(", ") : "-"}`,
5549
5730
  ` providers: ${module.providers.length > 0 ? module.providers.map((provider) => provider.token).join(", ") : "-"}`,
5550
5731
  ` controllers: ${module.controllers.length > 0 ? module.controllers.map((controller) => controller.className).join(", ") : "-"}`,
5551
- ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`
5732
+ ` commands: ${module.commands.length > 0 ? module.commands.map((command) => command.name).join(", ") : "-"}`,
5733
+ ...createExecutionPlans({ ...graph, modules: [module] }).map((plan) => ` execution ${plan.name}: ${plan.stages.join(" -> ")}`)
5552
5734
  ].join(`
5553
5735
  `);
5554
5736
  }
@@ -5689,6 +5871,7 @@ export {
5689
5871
  compileTraits,
5690
5872
  createContextPack,
5691
5873
  createDependencyGraphCache,
5874
+ createExecutionPlans,
5692
5875
  createIncrementalCompiler,
5693
5876
  createIncrementalProgramSession,
5694
5877
  defineSupacloudConfig,
package/dist/inspect.d.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  import type { ApplicationGraph, Diagnostic, ModuleNode } from "./types";
2
+ import { inspectRouteContracts } from "./route-contracts";
3
+ export interface ExecutionPlan {
4
+ module: string;
5
+ kind: "route" | "command" | "job";
6
+ name: string;
7
+ command?: string;
8
+ /** Standard governance contract. Audit success follows the handler; custom executors may short-circuit. */
9
+ stages: string[];
10
+ }
2
11
  export interface ContextPack {
3
12
  version: 1;
4
13
  subject: string;
5
14
  modules: ModuleNode[];
6
15
  files: string[];
7
16
  externalTokens: string[];
17
+ executionPlans: ExecutionPlan[];
18
+ routeContracts: ReturnType<typeof inspectRouteContracts>;
19
+ diagnostics: Diagnostic[];
8
20
  relatedModules: {
9
21
  importedBy: string[];
10
22
  imports: string[];
@@ -26,6 +38,8 @@ export declare function explainGraph(graph: ApplicationGraph, subject: string):
26
38
  * editing one feature: the subject module, its imports, and its dependents.
27
39
  */
28
40
  export declare function createContextPack(graph: ApplicationGraph, subject: string): ContextPack;
41
+ /** Static plan, not runtime discovery; custom executors remain explicit boundaries. */
42
+ export declare function createExecutionPlans(graph: ApplicationGraph): ExecutionPlan[];
29
43
  export declare function doctorProject(rootDir: string, outDir: string, graph: ApplicationGraph, upToDate: boolean, diagnostics?: Diagnostic[]): DoctorResult;
30
44
  /**
31
45
  * Exports the application module architecture as a Mermaid graph diagram.
@@ -8,5 +8,13 @@ export declare function inspectRouteContracts(graph: ApplicationGraph): {
8
8
  path: string;
9
9
  file: string;
10
10
  missing: ("query" | "body" | "params" | "response")[];
11
+ validation: {
12
+ body: string;
13
+ response: string;
14
+ schemas: Partial<Record<"query" | "body" | "params" | "response", "opaque" | "declared">>;
15
+ evidence: string | null;
16
+ verified: false;
17
+ obligations: string[];
18
+ };
11
19
  }[];
12
20
  export declare function validateRouteContracts(graph: ApplicationGraph): Diagnostic[];
package/dist/types.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface FunctionalInjectNode {
21
21
  host?: boolean;
22
22
  }
23
23
  export interface AspectRefNode {
24
+ /** Exact project-local source path for context packs. */
25
+ file?: string;
24
26
  /** Exported symbol name used in the generated static import. */
25
27
  name: string;
26
28
  /** Source expression retained for diagnostics and manifest inspection. */
@@ -50,6 +52,14 @@ export interface Diagnostic {
50
52
  * than raw text offsets so fixes remain valid after unrelated edits.
51
53
  */
52
54
  export type DiagnosticFix = {
55
+ type: "set_command_mode";
56
+ targetFile: string;
57
+ command: string;
58
+ property: "transaction" | "idempotency";
59
+ expectedExpression: string;
60
+ /** Must be selected by the caller; never infer weaker governance. */
61
+ value?: "required" | "none";
62
+ } | {
53
63
  type: "add_module_import";
54
64
  targetFile: string;
55
65
  module: string;
@@ -136,6 +146,13 @@ export interface HandlerParamNode {
136
146
  default?: unknown;
137
147
  }
138
148
  export interface RouteNode {
149
+ contract?: {
150
+ body?: "framework" | "domain";
151
+ response?: "framework" | "native-json" | "binary" | "stream";
152
+ evidence?: string;
153
+ };
154
+ schemaKinds?: Partial<Record<"body" | "params" | "query" | "response", "opaque" | "declared">>;
155
+ nativeResponse?: boolean;
139
156
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
140
157
  path: string;
141
158
  handler: string;
@@ -206,6 +223,7 @@ export interface ControllerNode {
206
223
  schemaImports?: Record<string, string>;
207
224
  }
208
225
  export interface CommandNode {
226
+ rpc?: string;
209
227
  className: string;
210
228
  name: string;
211
229
  permission?: string;
@@ -312,7 +330,7 @@ export interface CompileOptions {
312
330
  disallowControllerDirectDb?: boolean;
313
331
  /** Detect modules declared in the project that are unreachable from any root module. */
314
332
  detectOrphanModules?: boolean;
315
- /** Write generated artifacts even when error-level diagnostics exist (default: true). */
333
+ /** Explicit unsafe opt-in to emit artifacts with errors (default: false). */
316
334
  writeOnError?: boolean;
317
335
  /** Generate typed API client in client.ts (default: false). */
318
336
  generateClient?: boolean;
@@ -368,6 +386,12 @@ export interface ValidateOptions {
368
386
  }
369
387
  /** Runtime capabilities declared by the Command executor. */
370
388
  export interface CommandExecutionCapabilities {
389
+ /** Explicit named adapters; declarations must also be tested against the database. */
390
+ rpc?: Record<string, {
391
+ audit?: boolean;
392
+ idempotency?: boolean;
393
+ transaction?: boolean;
394
+ }>;
371
395
  /** Whether runtime permission checks are supported. */
372
396
  permission?: boolean;
373
397
  /** Whether runtime audit persistence is supported. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",