@telorun/analyzer 0.62.1 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +6 -0
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +343 -13
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +2 -7
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +9 -2
- package/dist/import-resolution-diagnostics.d.ts.map +1 -1
- package/dist/import-resolution-diagnostics.js +26 -7
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/invocation-contract.d.ts +6 -1
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +27 -4
- package/dist/loaded-types.d.ts +12 -3
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +0 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +62 -41
- package/dist/manifest-schemas.d.ts +699 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +306 -14
- package/dist/module-compatibility.d.ts +40 -0
- package/dist/module-compatibility.d.ts.map +1 -0
- package/dist/module-compatibility.js +52 -0
- package/dist/ref-slot-reading.d.ts +30 -0
- package/dist/ref-slot-reading.d.ts.map +1 -0
- package/dist/ref-slot-reading.js +139 -0
- package/dist/referrer-rule.d.ts +67 -0
- package/dist/referrer-rule.d.ts.map +1 -0
- package/dist/referrer-rule.js +96 -0
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/version-stamp.d.ts +16 -0
- package/dist/release/version-stamp.d.ts.map +1 -1
- package/dist/release/version-stamp.js +52 -0
- package/dist/release/workspace-config.d.ts +10 -6
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +10 -6
- package/dist/resolve-schema-ref-kinds.d.ts +4 -3
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +16 -0
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +4 -3
- package/dist/resolve-zone-containment.d.ts +99 -0
- package/dist/resolve-zone-containment.d.ts.map +1 -0
- package/dist/resolve-zone-containment.js +212 -0
- package/dist/resolve-zone-requirements.d.ts +5 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +48 -7
- package/dist/resource-rule.d.ts +106 -0
- package/dist/resource-rule.d.ts.map +1 -0
- package/dist/resource-rule.js +236 -0
- package/dist/rule-condition.d.ts +50 -0
- package/dist/rule-condition.d.ts.map +1 -0
- package/dist/rule-condition.js +108 -0
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +20 -0
- package/dist/schema-projection.d.ts +171 -0
- package/dist/schema-projection.d.ts.map +1 -0
- package/dist/schema-projection.js +317 -0
- package/dist/step-slot.d.ts +39 -0
- package/dist/step-slot.d.ts.map +1 -0
- package/dist/step-slot.js +49 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -3
- package/dist/validate-durable-regions.d.ts +15 -0
- package/dist/validate-durable-regions.d.ts.map +1 -0
- package/dist/validate-durable-regions.js +336 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +44 -2
- package/dist/validate-referrer-rules.d.ts +120 -0
- package/dist/validate-referrer-rules.d.ts.map +1 -0
- package/dist/validate-referrer-rules.js +255 -0
- package/dist/validate-resource-rules.d.ts +114 -0
- package/dist/validate-resource-rules.d.ts.map +1 -0
- package/dist/validate-resource-rules.js +283 -0
- package/dist/validate-schema-projection.d.ts +32 -0
- package/dist/validate-schema-projection.d.ts.map +1 -0
- package/dist/validate-schema-projection.js +110 -0
- package/dist/validate-step-inputs.d.ts +2 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +5 -3
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +11 -5
- package/dist/validate-zone-slots.d.ts +2 -2
- package/dist/validate-zone-slots.d.ts.map +1 -1
- package/dist/validate-zone-slots.js +202 -6
- package/dist/validate-zone-violations.d.ts +12 -0
- package/dist/validate-zone-violations.d.ts.map +1 -0
- package/dist/validate-zone-violations.js +169 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +4 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -1
- package/dist/zone-slot.js +86 -5
- package/package.json +2 -2
- package/src/analysis-registry.ts +6 -0
- package/src/analyzer.ts +395 -14
- package/src/call-graph.ts +2 -8
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +25 -7
- package/src/index.ts +73 -0
- package/src/invocation-contract.ts +45 -2
- package/src/loaded-types.ts +12 -3
- package/src/manifest-loader.ts +87 -55
- package/src/manifest-schemas.ts +328 -14
- package/src/module-compatibility.ts +72 -0
- package/src/ref-slot-reading.ts +156 -0
- package/src/referrer-rule.ts +116 -0
- package/src/release/index.ts +1 -0
- package/src/release/version-stamp.ts +58 -0
- package/src/release/workspace-config.ts +10 -6
- package/src/resolve-schema-ref-kinds.ts +28 -4
- package/src/resolve-throws-union.ts +4 -3
- package/src/resolve-zone-containment.ts +329 -0
- package/src/resolve-zone-requirements.ts +67 -7
- package/src/resource-rule.ts +252 -0
- package/src/rule-condition.ts +126 -0
- package/src/schema-keywords.ts +24 -0
- package/src/schema-projection.ts +403 -0
- package/src/step-slot.ts +64 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +17 -3
- package/src/validate-durable-regions.ts +401 -0
- package/src/validate-references.ts +57 -2
- package/src/validate-referrer-rules.ts +373 -0
- package/src/validate-resource-rules.ts +401 -0
- package/src/validate-schema-projection.ts +184 -0
- package/src/validate-step-inputs.ts +5 -3
- package/src/validate-throws-coverage.ts +12 -5
- package/src/validate-zone-slots.ts +192 -8
- package/src/validate-zone-violations.ts +190 -0
- package/src/value-type-keyword.ts +4 -0
- package/src/zone-slot.ts +114 -5
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { REFERRER_RULES_ANNOTATION, readRawReferrerRules, readReferrerRules, } from "./referrer-rule.js";
|
|
2
|
+
import { celSourceOf, findDynamicLeaf, readNodes } from "./resource-rule.js";
|
|
3
|
+
import { RULE_BUDGET_MS, compileRuleCondition, conditionRefusals } from "./rule-condition.js";
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Report every way a kind's referrer-rule declarations are malformed. Runs on
|
|
9
|
+
* the `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
|
|
10
|
+
* kind's author wrote rather than on a consumer's resource.
|
|
11
|
+
*
|
|
12
|
+
* The `referrer:` filter is NOT resolved here: it is canonicalized in the
|
|
13
|
+
* declaring module's scope by `resolveSchemaRefKinds`, the only pass holding
|
|
14
|
+
* that scope, and a name resolving to nothing is reported from there.
|
|
15
|
+
*/
|
|
16
|
+
export function validateReferrerRuleDeclarations(manifest) {
|
|
17
|
+
const own = manifest.schema;
|
|
18
|
+
const raw = readRawReferrerRules(own);
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return [];
|
|
21
|
+
const base = `schema.${REFERRER_RULES_ANNOTATION}`;
|
|
22
|
+
const issues = [];
|
|
23
|
+
const issue = (path, message) => {
|
|
24
|
+
issues.push({ code: "REFERRER_RULE_INVALID", manifest, path, message });
|
|
25
|
+
};
|
|
26
|
+
if (!Array.isArray(raw)) {
|
|
27
|
+
issue(base, `'${REFERRER_RULES_ANNOTATION}' must be an array of rules.`);
|
|
28
|
+
return issues;
|
|
29
|
+
}
|
|
30
|
+
const seen = new Map();
|
|
31
|
+
raw.forEach((entry, index) => {
|
|
32
|
+
const at = `${base}[${index}]`;
|
|
33
|
+
if (!isObject(entry)) {
|
|
34
|
+
issue(at, "A rule must be an object with 'condition', 'code' and 'message'.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const condition = celSourceOf(entry.condition);
|
|
38
|
+
if (condition === undefined || condition.length === 0) {
|
|
39
|
+
issue(`${at}.condition`, "A rule needs a 'condition' — a CEL expression over 'referrer' (the resource that " +
|
|
40
|
+
"references this one) and 'self', TRUE when the rule holds. Write it with the !cel tag.");
|
|
41
|
+
}
|
|
42
|
+
if (typeof entry.code !== "string" || entry.code.length === 0) {
|
|
43
|
+
issue(`${at}.code`, "A rule needs a 'code' naming it. It is reported in the diagnostic's data.rule, " +
|
|
44
|
+
"not as a diagnostic code — every violation reports under REFERRER_RULE_VIOLATED.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const first = seen.get(entry.code);
|
|
48
|
+
if (first !== undefined) {
|
|
49
|
+
issue(`${at}.code`, `Rule code '${entry.code}' is already used by rule ${first}. A code names one rule, ` +
|
|
50
|
+
"so two rules sharing it are indistinguishable in data.rule.");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
seen.set(entry.code, index);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (typeof entry.message !== "string" || entry.message.length === 0) {
|
|
57
|
+
issue(`${at}.message`, "A rule needs a 'message' saying what the referrer must do — only the kind's author " +
|
|
58
|
+
"knows that, and the analyzer supplies only where and what.");
|
|
59
|
+
}
|
|
60
|
+
if (entry.referrer !== undefined && typeof entry.referrer !== "string") {
|
|
61
|
+
issue(`${at}.referrer`, "'referrer' names the kind a referring resource must be, in the same alias-qualified " +
|
|
62
|
+
"grammar as extends: (Self.Server, Http.Server, Telo.Something). Omit it to apply the " +
|
|
63
|
+
"rule to every referrer.");
|
|
64
|
+
}
|
|
65
|
+
if (entry.severity !== undefined && entry.severity !== "error" && entry.severity !== "warning") {
|
|
66
|
+
issue(`${at}.severity`, "'severity' must be 'error' or 'warning'.");
|
|
67
|
+
}
|
|
68
|
+
if (condition) {
|
|
69
|
+
for (const refusal of conditionRefusals(condition))
|
|
70
|
+
issue(`${at}.condition`, refusal);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return issues;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Run a kind's referrer rules against every resource that references one of its
|
|
77
|
+
* resources.
|
|
78
|
+
*
|
|
79
|
+
* `self` binds the referenced resource, `referrer` the one that reached it. A
|
|
80
|
+
* referrer reaching the same resource through several slots is judged ONCE — by
|
|
81
|
+
* manifest identity, since a name alone is module-scoped — because the condition
|
|
82
|
+
* reads the two manifests and nothing about the site, so a second site could only
|
|
83
|
+
* produce the identical verdict at a different path.
|
|
84
|
+
*/
|
|
85
|
+
export function evaluateReferrerRules(manifest, definitionSchema, referrers,
|
|
86
|
+
/** Whether a referrer of `kind` satisfies a rule's `referrer:` filter —
|
|
87
|
+
* Liskov-substitutable, so a child of the named kind matches. Supplied by the
|
|
88
|
+
* caller, which holds the definition registry. */
|
|
89
|
+
kindMatches) {
|
|
90
|
+
const rules = readReferrerRules(definitionSchema);
|
|
91
|
+
if (rules.length === 0)
|
|
92
|
+
return [];
|
|
93
|
+
const self = manifest;
|
|
94
|
+
const findings = [];
|
|
95
|
+
for (const rule of rules) {
|
|
96
|
+
const compiled = compileRuleCondition(rule.condition, ["self", "referrer"]);
|
|
97
|
+
if ("reason" in compiled) {
|
|
98
|
+
findings.push({ kind: "failed", rule, reason: compiled.reason });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const { parsed, chains } = compiled;
|
|
102
|
+
// Identity is the MANIFEST, never `(kind, name)`: resource names are
|
|
103
|
+
// module-scoped, so two libraries each declaring a `server` of the same kind
|
|
104
|
+
// would share one bucket and the second violation would be dropped in
|
|
105
|
+
// silence — the trap the migration provenance index names.
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const started = Date.now();
|
|
108
|
+
for (const referrer of referrers) {
|
|
109
|
+
if (rule.referrer !== undefined && !kindMatches(rule.referrer, referrer.kind))
|
|
110
|
+
continue;
|
|
111
|
+
if (seen.has(referrer.manifest))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(referrer.manifest);
|
|
114
|
+
// Only the nodes this condition READS decide whether it can run — the
|
|
115
|
+
// resource-rule reasoning, and it bites harder here: the referrer is a
|
|
116
|
+
// whole manifest, so scanning all of it would disable the rule for any
|
|
117
|
+
// server carrying one unrelated expression.
|
|
118
|
+
let dynamicAt;
|
|
119
|
+
for (const node of readNodes(chains, {
|
|
120
|
+
self,
|
|
121
|
+
referrer: referrer.manifest,
|
|
122
|
+
})) {
|
|
123
|
+
dynamicAt = findDynamicLeaf(node);
|
|
124
|
+
if (dynamicAt !== undefined)
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (dynamicAt !== undefined) {
|
|
128
|
+
findings.push({ kind: "skipped", rule, referrer, dynamicAt });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let held;
|
|
132
|
+
try {
|
|
133
|
+
held = parsed({ self, referrer: referrer.manifest });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
findings.push({
|
|
137
|
+
kind: "failed",
|
|
138
|
+
rule,
|
|
139
|
+
referrer,
|
|
140
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
141
|
+
});
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (held !== true) {
|
|
145
|
+
findings.push({ kind: "violation", rule, referrer, message: rule.message });
|
|
146
|
+
}
|
|
147
|
+
const elapsed = Date.now() - started;
|
|
148
|
+
if (elapsed > RULE_BUDGET_MS) {
|
|
149
|
+
findings.push({ kind: "over-budget", rule, referrer, elapsedMs: elapsed });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
/** Whether a rule found any referrer to judge — the input to the never-exercised
|
|
157
|
+
* report, which is how a mistyped `referrer:` filter would otherwise disable a
|
|
158
|
+
* check in silence. */
|
|
159
|
+
export function referrerRuleExercised(rule, referrers, kindMatches) {
|
|
160
|
+
if (rule.referrer === undefined)
|
|
161
|
+
return referrers.length > 0;
|
|
162
|
+
const filter = rule.referrer;
|
|
163
|
+
return referrers.some((referrer) => kindMatches(filter, referrer.kind));
|
|
164
|
+
}
|
|
165
|
+
const nameOf = (manifest) => manifest.metadata?.name ?? "<unnamed>";
|
|
166
|
+
/**
|
|
167
|
+
* Map one resource's findings to what should be reported.
|
|
168
|
+
*
|
|
169
|
+
* A violation names the declaring kind in the message. For a resource rule the
|
|
170
|
+
* reported resource IS of the kind that declared it, so the origin is implicit;
|
|
171
|
+
* here it is not — the diagnostic lands on an `Http.Server` for a rule
|
|
172
|
+
* `HttpServer.Reference` wrote — and leaving that to the author's prose would
|
|
173
|
+
* make the trail depend on remembering to write it.
|
|
174
|
+
*/
|
|
175
|
+
export function reportReferrerRules(manifest, definition, findings,
|
|
176
|
+
/** Whether the DECLARING definition is one of the entry's own modules. */
|
|
177
|
+
declarationIsOurs) {
|
|
178
|
+
const declaringKind = manifest.kind;
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const finding of findings) {
|
|
181
|
+
if (finding.kind === "violation") {
|
|
182
|
+
out.push({
|
|
183
|
+
code: "REFERRER_RULE_VIOLATED",
|
|
184
|
+
severity: finding.rule.severity,
|
|
185
|
+
message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)} at ` +
|
|
186
|
+
`'${finding.referrer.path}': required by ${declaringKind} — ${finding.message}`,
|
|
187
|
+
manifest: finding.referrer.manifest,
|
|
188
|
+
path: finding.referrer.path,
|
|
189
|
+
rule: finding.rule.code,
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (finding.kind === "skipped") {
|
|
194
|
+
out.push({
|
|
195
|
+
code: "REFERRER_RULE_SKIPPED",
|
|
196
|
+
severity: "information",
|
|
197
|
+
message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)}: rule ` +
|
|
198
|
+
`'${finding.rule.code}' from ${declaringKind} did not run at ` +
|
|
199
|
+
`'${finding.referrer.path}' — the value holds a CEL expression at ` +
|
|
200
|
+
`'${finding.dynamicAt}', which is not known until the resource is created. ` +
|
|
201
|
+
"Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
|
|
202
|
+
manifest: finding.referrer.manifest,
|
|
203
|
+
path: finding.referrer.path,
|
|
204
|
+
rule: finding.rule.code,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const because = finding.kind === "failed"
|
|
209
|
+
? `failed to evaluate: ${finding.reason}. Guard an optional field with 'in' or '.?'.`
|
|
210
|
+
: `exceeded its evaluation budget (${finding.elapsedMs}ms) and was stopped, so ` +
|
|
211
|
+
"coverage from here on is incomplete. Simplify the condition.";
|
|
212
|
+
out.push({
|
|
213
|
+
code: "REFERRER_RULE_INVALID",
|
|
214
|
+
severity: declarationIsOurs ? "error" : "warning",
|
|
215
|
+
message: `Referrer rule '${finding.rule.code}' on kind '${declaringKind}' ${because} ` +
|
|
216
|
+
`This is a defect in the rule, not in ${nameOf(manifest)}` +
|
|
217
|
+
(declarationIsOurs ? "." : " — it is declared by a module this workspace does not own."),
|
|
218
|
+
// The two halves of the anchor move together or they name a node that
|
|
219
|
+
// does not exist: the rule's own declaration site is a path in the
|
|
220
|
+
// DEFINITION, while a referrer's slot path is a path in the REFERRER. A
|
|
221
|
+
// dependency's broken rule therefore lands on the referrer it was checking
|
|
222
|
+
// — a manifest the reader owns and where the path resolves — and on the
|
|
223
|
+
// referenced resource only when there is no referrer to name (a condition
|
|
224
|
+
// that failed to compile at all).
|
|
225
|
+
...(declarationIsOurs && definition
|
|
226
|
+
? {
|
|
227
|
+
manifest: definition,
|
|
228
|
+
path: `schema.${REFERRER_RULES_ANNOTATION}[${finding.rule.index}]`,
|
|
229
|
+
}
|
|
230
|
+
: finding.referrer
|
|
231
|
+
? { manifest: finding.referrer.manifest, path: finding.referrer.path }
|
|
232
|
+
: { manifest }),
|
|
233
|
+
rule: finding.rule.code,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** The report for a rule nothing ever exercised — here that means no resource of
|
|
239
|
+
* the kind was referenced by anything the filter matches, which is what a typo
|
|
240
|
+
* in `referrer:` looks like from the outside. */
|
|
241
|
+
export function reportUnexercisedReferrerRule(definition, rule) {
|
|
242
|
+
return {
|
|
243
|
+
code: "REFERRER_RULE_UNEXERCISED",
|
|
244
|
+
severity: "information",
|
|
245
|
+
message: `Referrer rule '${rule.code}' never ran: nothing` +
|
|
246
|
+
(rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
|
|
247
|
+
" references a resource of this kind, so nothing has proven the condition." +
|
|
248
|
+
(rule.referrer === undefined
|
|
249
|
+
? ""
|
|
250
|
+
: " A 'referrer' naming a kind no manifest uses disables the rule in silence."),
|
|
251
|
+
manifest: definition,
|
|
252
|
+
path: `schema.${REFERRER_RULES_ANNOTATION}[${rule.index}]`,
|
|
253
|
+
rule: rule.code,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strict half of the `x-telo-resource-rules` accessor split
|
|
3
|
+
* (`validate-ref-slots.ts` / `validate-zone-slots.ts` precedent), plus the
|
|
4
|
+
* evaluation pass that runs the rules against each resource.
|
|
5
|
+
*
|
|
6
|
+
* Both halves live here because they fail in opposite directions and must agree
|
|
7
|
+
* about what a rule MEANS: a declaration the reader cannot parse is silently
|
|
8
|
+
* unenforced — the check reads as passing when it never ran — while a rule that
|
|
9
|
+
* throws would otherwise be reported against the consumer's manifest, blaming an
|
|
10
|
+
* author for a defect in someone else's kind.
|
|
11
|
+
*
|
|
12
|
+
* Scoping follows `X_TELO_REF_UNRESOLVED`, and it splits by WHOSE defect it is.
|
|
13
|
+
* A declaration defect belongs to the kind, so it is reported only for
|
|
14
|
+
* definitions in the entry's own modules. A VIOLATION belongs to the data, so it
|
|
15
|
+
* is reported wherever the offending resource is the entry's — the opposite
|
|
16
|
+
* direction, for the same reason. A rule that THROWS or runs out of budget is a
|
|
17
|
+
* defect in the rule found while checking someone else's data: the caller
|
|
18
|
+
* anchors it on the declaring definition when that is ours, and downgrades it to
|
|
19
|
+
* a warning when it is a published dependency's, so it is never an error on a
|
|
20
|
+
* line the author cannot change.
|
|
21
|
+
*
|
|
22
|
+
* Browser-safe: no Node built-ins.
|
|
23
|
+
*/
|
|
24
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
25
|
+
import { type ResourceRule } from "./resource-rule.js";
|
|
26
|
+
/** Published name for the shared budget — see `rule-condition.ts`. */
|
|
27
|
+
export declare const RESOURCE_RULE_BUDGET_MS = 50;
|
|
28
|
+
export interface ResourceRuleIssue {
|
|
29
|
+
code: "RESOURCE_RULE_INVALID";
|
|
30
|
+
manifest: ResourceManifest;
|
|
31
|
+
path: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
/** One rule's verdict on one resource. */
|
|
35
|
+
export type ResourceRuleFinding = {
|
|
36
|
+
kind: "violation";
|
|
37
|
+
rule: ResourceRule;
|
|
38
|
+
/** Path of the offending element, or "" for a rule with no `in:`. */
|
|
39
|
+
path: string;
|
|
40
|
+
message: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "skipped";
|
|
43
|
+
rule: ResourceRule;
|
|
44
|
+
path: string;
|
|
45
|
+
dynamicAt: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: "failed";
|
|
48
|
+
rule: ResourceRule;
|
|
49
|
+
path: string;
|
|
50
|
+
reason: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "over-budget";
|
|
53
|
+
rule: ResourceRule;
|
|
54
|
+
path: string;
|
|
55
|
+
elapsedMs: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Report every way a kind's rule declarations are malformed. Runs on the
|
|
59
|
+
* `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
|
|
60
|
+
* kind's author wrote rather than on a consumer's resource.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateResourceRuleDeclarations(manifest: ResourceManifest,
|
|
63
|
+
/**
|
|
64
|
+
* The schema an `in:` pointer is checked against — the SAME merged schema the
|
|
65
|
+
* evaluation pass reads, so the two halves cannot disagree about which fields
|
|
66
|
+
* a kind has. Reading the doc's own `schema:` here was correct only for a leaf
|
|
67
|
+
* kind: a rule declared on a parent and evaluated against a child's field
|
|
68
|
+
* would be reported invalid at the parent, and a child's rule naming an
|
|
69
|
+
* INHERITED field would be told the kind does not declare it. Both arrive as
|
|
70
|
+
* soon as rules are declared on an abstract, which is where a rule shared by
|
|
71
|
+
* every backend belongs.
|
|
72
|
+
*/
|
|
73
|
+
effectiveSchema?: unknown): ResourceRuleIssue[];
|
|
74
|
+
/**
|
|
75
|
+
* Run a kind's rules against one resource.
|
|
76
|
+
*
|
|
77
|
+
* `self` binds the resource, `this` the element under test — the two coexist in
|
|
78
|
+
* cel-js, which is what lets a rule correlate an element against the whole
|
|
79
|
+
* (`c in self.columns`) with no path language.
|
|
80
|
+
*/
|
|
81
|
+
export declare function evaluateResourceRules(manifest: ResourceManifest, definitionSchema: unknown): ResourceRuleFinding[];
|
|
82
|
+
/** Whether a rule found anything to iterate on this resource — the input to the
|
|
83
|
+
* never-exercised report, which is the second way coverage varies invisibly. */
|
|
84
|
+
export declare function ruleExercised(manifest: ResourceManifest, rule: ResourceRule): boolean;
|
|
85
|
+
/** Where a rule finding is reported, and how loudly. Plain data, so the caller
|
|
86
|
+
* pushes it exactly as it does for `zoneSlotIssues` / `refSlotIssues` rather
|
|
87
|
+
* than composing severity, ownership and prose inline in the resource loop. */
|
|
88
|
+
export interface ResourceRuleDiagnostic {
|
|
89
|
+
code: "RESOURCE_RULE_VIOLATED" | "RESOURCE_RULE_SKIPPED" | "RESOURCE_RULE_INVALID" | "RESOURCE_RULE_UNEXERCISED";
|
|
90
|
+
severity: "error" | "warning" | "information";
|
|
91
|
+
message: string;
|
|
92
|
+
/** The resource the finding is reported ON — the offending one for a
|
|
93
|
+
* violation, the DECLARING definition for a defect in the rule itself. */
|
|
94
|
+
manifest: ResourceManifest;
|
|
95
|
+
path?: string;
|
|
96
|
+
rule: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Map one resource's findings to what should be reported.
|
|
100
|
+
*
|
|
101
|
+
* The split by whose defect it is lives here, with the finding vocabulary,
|
|
102
|
+
* rather than in the analyzer's resource loop: a violation belongs to the DATA
|
|
103
|
+
* and is reported on the resource, while a rule that throws or exhausts its
|
|
104
|
+
* budget is a defect in the RULE and belongs on the definition that declared it
|
|
105
|
+
* — downgraded to a warning when that definition is a published dependency's,
|
|
106
|
+
* since an error there blocks `telo check` on a line the consumer cannot change.
|
|
107
|
+
*/
|
|
108
|
+
export declare function reportResourceRules(manifest: ResourceManifest, definition: ResourceManifest | undefined, findings: readonly ResourceRuleFinding[],
|
|
109
|
+
/** Whether the DECLARING definition is one of the entry's own modules. */
|
|
110
|
+
declarationIsOurs: boolean): ResourceRuleDiagnostic[];
|
|
111
|
+
/** The report for a rule nothing ever exercised — the second way coverage varies
|
|
112
|
+
* invisibly, beside the dynamic-leaf skip. */
|
|
113
|
+
export declare function reportUnexercisedRule(definition: ResourceManifest, rule: ResourceRule): ResourceRuleDiagnostic;
|
|
114
|
+
//# sourceMappingURL=validate-resource-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-resource-rules.d.ts","sourceRoot":"","sources":["../src/validate-resource-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAE5B,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,KAAiB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,YAAY,CAAC;IACnB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAmCjF;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,gBAAgB;AAC1B;;;;;;;;;GASG;AACH,eAAe,CAAC,EAAE,OAAO,GACxB,iBAAiB,EAAE,CA+FrB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,gBAAgB,EAAE,OAAO,GACxB,mBAAmB,EAAE,CA4DvB;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAIrF;AAED;;gFAEgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,GACvB,2BAA2B,CAAC;IAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;+EAC2E;IAC3E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,gBAAgB,GAAG,SAAS,EACxC,QAAQ,EAAE,SAAS,mBAAmB,EAAE;AACxC,0EAA0E;AAC1E,iBAAiB,EAAE,OAAO,GACzB,sBAAsB,EAAE,CA2D1B;AAED;+CAC+C;AAC/C,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,gBAAgB,EAC5B,IAAI,EAAE,YAAY,GACjB,sBAAsB,CAYxB"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { RULE_BUDGET_MS, compileRuleCondition, conditionRefusals } from "./rule-condition.js";
|
|
2
|
+
import { RESOURCE_RULES_ANNOTATION, celSourceOf, findDynamicLeaf, pointerSegments, readRawResourceRules, readResourceRules, readNodes, resolveRuleSubjects, } from "./resource-rule.js";
|
|
3
|
+
/** Published name for the shared budget — see `rule-condition.ts`. */
|
|
4
|
+
export const RESOURCE_RULE_BUDGET_MS = RULE_BUDGET_MS;
|
|
5
|
+
function isObject(value) {
|
|
6
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
/** Navigate a kind's own schema to the node describing what a pointer names, so
|
|
9
|
+
* an `in:` naming a field the kind does not declare is caught at the kind. */
|
|
10
|
+
function schemaAtPointer(schema, pointer) {
|
|
11
|
+
const segments = pointerSegments(pointer);
|
|
12
|
+
if (!segments)
|
|
13
|
+
return undefined;
|
|
14
|
+
let node = schema;
|
|
15
|
+
for (const segment of segments) {
|
|
16
|
+
if (!isObject(node))
|
|
17
|
+
return undefined;
|
|
18
|
+
const properties = isObject(node.properties) ? node.properties : undefined;
|
|
19
|
+
const next = properties?.[segment] ??
|
|
20
|
+
(node.type === "array" ? node.items : undefined) ??
|
|
21
|
+
(isObject(node.additionalProperties) ? node.additionalProperties : undefined);
|
|
22
|
+
if (next === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
node = next;
|
|
25
|
+
}
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
/** True when a schema node describes something a rule can iterate. Unknown or
|
|
29
|
+
* absent `type` passes: an open schema is not evidence of a defect. */
|
|
30
|
+
function isIterableSchema(node) {
|
|
31
|
+
if (!isObject(node))
|
|
32
|
+
return true;
|
|
33
|
+
const type = node.type;
|
|
34
|
+
if (type === undefined)
|
|
35
|
+
return true;
|
|
36
|
+
const types = Array.isArray(type) ? type : [type];
|
|
37
|
+
return types.some((t) => t === "array" || t === "object");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Report every way a kind's rule declarations are malformed. Runs on the
|
|
41
|
+
* `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
|
|
42
|
+
* kind's author wrote rather than on a consumer's resource.
|
|
43
|
+
*/
|
|
44
|
+
export function validateResourceRuleDeclarations(manifest,
|
|
45
|
+
/**
|
|
46
|
+
* The schema an `in:` pointer is checked against — the SAME merged schema the
|
|
47
|
+
* evaluation pass reads, so the two halves cannot disagree about which fields
|
|
48
|
+
* a kind has. Reading the doc's own `schema:` here was correct only for a leaf
|
|
49
|
+
* kind: a rule declared on a parent and evaluated against a child's field
|
|
50
|
+
* would be reported invalid at the parent, and a child's rule naming an
|
|
51
|
+
* INHERITED field would be told the kind does not declare it. Both arrive as
|
|
52
|
+
* soon as rules are declared on an abstract, which is where a rule shared by
|
|
53
|
+
* every backend belongs.
|
|
54
|
+
*/
|
|
55
|
+
effectiveSchema) {
|
|
56
|
+
const own = manifest.schema;
|
|
57
|
+
const schema = effectiveSchema ?? own;
|
|
58
|
+
const raw = readRawResourceRules(own);
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return [];
|
|
61
|
+
const base = `schema.${RESOURCE_RULES_ANNOTATION}`;
|
|
62
|
+
const issues = [];
|
|
63
|
+
const issue = (path, message) => {
|
|
64
|
+
issues.push({ code: "RESOURCE_RULE_INVALID", manifest, path, message });
|
|
65
|
+
};
|
|
66
|
+
if (!Array.isArray(raw)) {
|
|
67
|
+
issue(base, `'${RESOURCE_RULES_ANNOTATION}' must be an array of rules.`);
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
const seen = new Map();
|
|
71
|
+
raw.forEach((entry, index) => {
|
|
72
|
+
const at = `${base}[${index}]`;
|
|
73
|
+
if (!isObject(entry)) {
|
|
74
|
+
issue(at, "A rule must be an object with 'condition', 'code' and 'message'.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const condition = celSourceOf(entry.condition);
|
|
78
|
+
if (condition === undefined || condition.length === 0) {
|
|
79
|
+
issue(`${at}.condition`, "A rule needs a 'condition' — a CEL expression that is TRUE when the rule holds " +
|
|
80
|
+
"(the polarity Telo.JsonSchema rules use). Write it with the !cel tag.");
|
|
81
|
+
}
|
|
82
|
+
if (typeof entry.code !== "string" || entry.code.length === 0) {
|
|
83
|
+
issue(`${at}.code`, "A rule needs a 'code' naming it. It is reported in the diagnostic's data.rule, " +
|
|
84
|
+
"not as a diagnostic code — every violation reports under RESOURCE_RULE_VIOLATED.");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const first = seen.get(entry.code);
|
|
88
|
+
if (first !== undefined) {
|
|
89
|
+
issue(`${at}.code`, `Rule code '${entry.code}' is already used by rule ${first}. A code names one rule, ` +
|
|
90
|
+
"so two rules sharing it are indistinguishable in data.rule.");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
seen.set(entry.code, index);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (typeof entry.message !== "string" || entry.message.length === 0) {
|
|
97
|
+
issue(`${at}.message`, "A rule needs a 'message' saying what the relationship means — only the kind's " +
|
|
98
|
+
"author knows that, and the analyzer supplies only where and what.");
|
|
99
|
+
}
|
|
100
|
+
if (entry.severity !== undefined &&
|
|
101
|
+
entry.severity !== "error" &&
|
|
102
|
+
entry.severity !== "warning") {
|
|
103
|
+
issue(`${at}.severity`, "'severity' must be 'error' or 'warning'.");
|
|
104
|
+
}
|
|
105
|
+
if (entry.in !== undefined) {
|
|
106
|
+
if (typeof entry.in !== "string") {
|
|
107
|
+
issue(`${at}.in`, "'in' must be a JSON Pointer to the collection the rule iterates.");
|
|
108
|
+
}
|
|
109
|
+
else if (!pointerSegments(entry.in)) {
|
|
110
|
+
issue(`${at}.in`, `'in' must be a JSON Pointer starting with '/', got '${entry.in}'.`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const node = schemaAtPointer(schema, entry.in);
|
|
114
|
+
if (node === undefined) {
|
|
115
|
+
issue(`${at}.in`, `'in' points at '${entry.in}', which this kind's schema does not declare. ` +
|
|
116
|
+
"The pointer is the diagnostic's anchor, so it must name a field of this kind.");
|
|
117
|
+
}
|
|
118
|
+
else if (!isIterableSchema(node)) {
|
|
119
|
+
issue(`${at}.in`, `'in' points at '${entry.in}', which is not a collection. A rule iterates an ` +
|
|
120
|
+
"array or a map; omit 'in' for a rule about the resource as a whole.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (condition) {
|
|
125
|
+
for (const refusal of conditionRefusals(condition))
|
|
126
|
+
issue(`${at}.condition`, refusal);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return issues;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run a kind's rules against one resource.
|
|
133
|
+
*
|
|
134
|
+
* `self` binds the resource, `this` the element under test — the two coexist in
|
|
135
|
+
* cel-js, which is what lets a rule correlate an element against the whole
|
|
136
|
+
* (`c in self.columns`) with no path language.
|
|
137
|
+
*/
|
|
138
|
+
export function evaluateResourceRules(manifest, definitionSchema) {
|
|
139
|
+
const rules = readResourceRules(definitionSchema);
|
|
140
|
+
if (rules.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const self = manifest;
|
|
143
|
+
const findings = [];
|
|
144
|
+
for (const rule of rules) {
|
|
145
|
+
const subjects = rule.in === undefined ? [{ path: "", value: self }] : resolveRuleSubjects(self, rule.in);
|
|
146
|
+
// `undefined` means the pointer resolved to a scalar — a declaration defect
|
|
147
|
+
// the strict half reports at the kind. Evaluating anyway would report it
|
|
148
|
+
// against the consumer instead.
|
|
149
|
+
if (subjects === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const compiled = compileRuleCondition(rule.condition, ["self", "this"]);
|
|
152
|
+
if ("reason" in compiled) {
|
|
153
|
+
findings.push({ kind: "failed", rule, path: "", reason: compiled.reason });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const { parsed, chains } = compiled;
|
|
157
|
+
const started = Date.now();
|
|
158
|
+
for (const subject of subjects) {
|
|
159
|
+
// Only the nodes this condition READS decide whether it can run — see
|
|
160
|
+
// `readNodes`. Scanning the whole subject would disable every
|
|
161
|
+
// resource-wide rule on any manifest containing one unrelated expression.
|
|
162
|
+
let dynamicAt;
|
|
163
|
+
for (const node of readNodes(chains, { self, this: subject.value })) {
|
|
164
|
+
dynamicAt = findDynamicLeaf(node);
|
|
165
|
+
if (dynamicAt !== undefined)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (dynamicAt !== undefined) {
|
|
169
|
+
findings.push({ kind: "skipped", rule, path: subject.path, dynamicAt });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let held;
|
|
173
|
+
try {
|
|
174
|
+
held = parsed({ self, this: subject.value, key: subject.key ?? null });
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
findings.push({
|
|
178
|
+
kind: "failed",
|
|
179
|
+
rule,
|
|
180
|
+
path: subject.path,
|
|
181
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
182
|
+
});
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (held !== true) {
|
|
186
|
+
findings.push({ kind: "violation", rule, path: subject.path, message: rule.message });
|
|
187
|
+
}
|
|
188
|
+
const elapsed = Date.now() - started;
|
|
189
|
+
if (elapsed > RESOURCE_RULE_BUDGET_MS) {
|
|
190
|
+
findings.push({ kind: "over-budget", rule, path: subject.path, elapsedMs: elapsed });
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return findings;
|
|
196
|
+
}
|
|
197
|
+
/** Whether a rule found anything to iterate on this resource — the input to the
|
|
198
|
+
* never-exercised report, which is the second way coverage varies invisibly. */
|
|
199
|
+
export function ruleExercised(manifest, rule) {
|
|
200
|
+
if (rule.in === undefined)
|
|
201
|
+
return true;
|
|
202
|
+
const subjects = resolveRuleSubjects(manifest, rule.in);
|
|
203
|
+
return subjects !== undefined && subjects.length > 0;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Map one resource's findings to what should be reported.
|
|
207
|
+
*
|
|
208
|
+
* The split by whose defect it is lives here, with the finding vocabulary,
|
|
209
|
+
* rather than in the analyzer's resource loop: a violation belongs to the DATA
|
|
210
|
+
* and is reported on the resource, while a rule that throws or exhausts its
|
|
211
|
+
* budget is a defect in the RULE and belongs on the definition that declared it
|
|
212
|
+
* — downgraded to a warning when that definition is a published dependency's,
|
|
213
|
+
* since an error there blocks `telo check` on a line the consumer cannot change.
|
|
214
|
+
*/
|
|
215
|
+
export function reportResourceRules(manifest, definition, findings,
|
|
216
|
+
/** Whether the DECLARING definition is one of the entry's own modules. */
|
|
217
|
+
declarationIsOurs) {
|
|
218
|
+
const name = manifest.metadata?.name ?? "<unnamed>";
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const finding of findings) {
|
|
221
|
+
const at = finding.path === "" ? undefined : finding.path;
|
|
222
|
+
if (finding.kind === "violation") {
|
|
223
|
+
out.push({
|
|
224
|
+
// One analyzer-owned envelope: surfaces branch on `code`, so a published
|
|
225
|
+
// module free to emit any string could shadow machinery that never
|
|
226
|
+
// expected a third party in that space. The rule's own name rides in
|
|
227
|
+
// `data.rule`.
|
|
228
|
+
code: "RESOURCE_RULE_VIOLATED",
|
|
229
|
+
severity: finding.rule.severity,
|
|
230
|
+
message: `${manifest.kind}/${name}${at ? ` at '${at}'` : ""}: ${finding.message}`,
|
|
231
|
+
manifest,
|
|
232
|
+
path: at,
|
|
233
|
+
rule: finding.rule.code,
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (finding.kind === "skipped") {
|
|
238
|
+
out.push({
|
|
239
|
+
code: "RESOURCE_RULE_SKIPPED",
|
|
240
|
+
severity: "information",
|
|
241
|
+
message: `${manifest.kind}/${name}: rule '${finding.rule.code}' did not run` +
|
|
242
|
+
`${at ? ` at '${at}'` : ""} — the value holds a CEL expression at ` +
|
|
243
|
+
`'${finding.dynamicAt}', which is not known until the resource is created. ` +
|
|
244
|
+
"Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
|
|
245
|
+
manifest,
|
|
246
|
+
path: at,
|
|
247
|
+
rule: finding.rule.code,
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const because = finding.kind === "failed"
|
|
252
|
+
? `failed to evaluate: ${finding.reason}. Guard an optional field with \`in\` or \`.?\`.`
|
|
253
|
+
: `exceeded its evaluation budget (${finding.elapsedMs}ms) and was stopped, so ` +
|
|
254
|
+
"coverage from here on is incomplete. Simplify the condition.";
|
|
255
|
+
out.push({
|
|
256
|
+
code: "RESOURCE_RULE_INVALID",
|
|
257
|
+
severity: declarationIsOurs ? "error" : "warning",
|
|
258
|
+
message: `Rule '${finding.rule.code}' on kind '${manifest.kind}' ${because} ` +
|
|
259
|
+
`This is a defect in the rule, not in ${name}` +
|
|
260
|
+
(declarationIsOurs ? "." : " — it is declared by a module this workspace does not own."),
|
|
261
|
+
manifest: declarationIsOurs && definition ? definition : manifest,
|
|
262
|
+
path: declarationIsOurs && definition
|
|
263
|
+
? `schema.${RESOURCE_RULES_ANNOTATION}[${finding.rule.index}]`
|
|
264
|
+
: at,
|
|
265
|
+
rule: finding.rule.code,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
/** The report for a rule nothing ever exercised — the second way coverage varies
|
|
271
|
+
* invisibly, beside the dynamic-leaf skip. */
|
|
272
|
+
export function reportUnexercisedRule(definition, rule) {
|
|
273
|
+
return {
|
|
274
|
+
code: "RESOURCE_RULE_UNEXERCISED",
|
|
275
|
+
severity: "information",
|
|
276
|
+
message: `Rule '${rule.code}' never ran: '${rule.in}' was empty on every resource of this kind, ` +
|
|
277
|
+
"so nothing has proven the condition. A nested typo in the condition is caught only at " +
|
|
278
|
+
"evaluation.",
|
|
279
|
+
manifest: definition,
|
|
280
|
+
path: `schema.${RESOURCE_RULES_ANNOTATION}[${rule.index}]`,
|
|
281
|
+
rule: rule.code,
|
|
282
|
+
};
|
|
283
|
+
}
|