@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 +20 -0
- package/dist/cli.js +214 -30
- package/dist/compile.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +209 -26
- package/dist/inspect.d.ts +14 -0
- package/dist/route-contracts.d.ts +8 -0
- package/dist/types.d.ts +25 -1
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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
|
-
|
|
964
|
+
rpc: checkedRpc(meta, ctx),
|
|
965
|
+
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
964
966
|
audit: stringLiteralProp(meta, "audit"),
|
|
965
|
-
idempotency:
|
|
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
|
|
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
|
|
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
|
|
4914
|
-
|
|
4915
|
-
const
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
958
|
+
rpc: checkedRpc(meta, ctx),
|
|
959
|
+
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
958
960
|
audit: stringLiteralProp(meta, "audit"),
|
|
959
|
-
idempotency:
|
|
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
|
|
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
|
|
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
|
|
5465
|
-
|
|
5466
|
-
const
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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",
|