@telorun/analyzer 0.65.0 → 0.67.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/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +194 -65
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +7 -1
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -19
- package/dist/cel-scope.d.ts +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +89 -11
- package/dist/definition-registry.d.ts +41 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +76 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- package/dist/eval-paths.d.ts +68 -0
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +138 -0
- package/dist/flatten-for-analyzer.d.ts +37 -1
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +104 -5
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +36 -1
- package/dist/migrations/entries/index.d.ts.map +1 -1
- package/dist/migrations/entries/index.js +2 -0
- package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
- package/dist/peer-binding.d.ts +232 -0
- package/dist/peer-binding.d.ts.map +1 -0
- package/dist/peer-binding.js +418 -0
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -0
- package/dist/ref-slot.d.ts +13 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +26 -0
- package/dist/reference-field-map.d.ts +28 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +25 -0
- package/dist/referrer-rule.d.ts +17 -0
- package/dist/referrer-rule.d.ts.map +1 -1
- package/dist/referrer-rule.js +4 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +14 -1
- package/dist/resource-input.d.ts +75 -0
- package/dist/resource-input.d.ts.map +1 -0
- package/dist/resource-input.js +90 -0
- package/dist/resource-rule.d.ts +40 -27
- package/dist/resource-rule.d.ts.map +1 -1
- package/dist/resource-rule.js +61 -7
- package/dist/rule-condition.d.ts +11 -0
- package/dist/rule-condition.d.ts.map +1 -1
- package/dist/rule-condition.js +13 -0
- package/dist/schema-projection.d.ts +119 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +213 -18
- package/dist/system-kinds.d.ts +7 -2
- package/dist/system-kinds.d.ts.map +1 -1
- package/dist/system-kinds.js +7 -2
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +50 -0
- package/dist/template-body.d.ts.map +1 -0
- package/dist/template-body.js +58 -0
- package/dist/validate-cel-context.d.ts +1 -17
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +73 -78
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- package/dist/validate-reference-forms.d.ts +10 -0
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +23 -0
- package/dist/validate-references.d.ts +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +77 -18
- package/dist/validate-referrer-rules.d.ts +38 -4
- package/dist/validate-referrer-rules.d.ts.map +1 -1
- package/dist/validate-referrer-rules.js +139 -31
- package/dist/validate-resource-inputs.d.ts +35 -0
- package/dist/validate-resource-inputs.d.ts.map +1 -0
- package/dist/validate-resource-inputs.js +319 -0
- package/dist/validate-resource-rules.d.ts +11 -2
- package/dist/validate-resource-rules.d.ts.map +1 -1
- package/dist/validate-resource-rules.js +18 -12
- package/dist/validate-schema-projection.d.ts.map +1 -1
- package/dist/validate-schema-projection.js +33 -2
- package/dist/validate-template-dispatch.d.ts +27 -0
- package/dist/validate-template-dispatch.d.ts.map +1 -0
- package/dist/validate-template-dispatch.js +95 -0
- package/dist/with-synthetic-positions.d.ts +1 -1
- package/dist/with-synthetic-positions.js +1 -1
- package/package.json +3 -3
- package/src/analyzer.ts +236 -70
- package/src/builtins.ts +69 -16
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +113 -19
- package/src/definition-registry.ts +78 -0
- package/src/dependency-graph.ts +66 -0
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +117 -4
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +57 -4
- package/src/inline-imports.ts +1 -0
- package/src/manifest-visitor.ts +41 -1
- package/src/migrations/entries/index.ts +2 -0
- package/src/migrations/entries/schema-prepare-bucket.json +27 -0
- package/src/peer-binding.ts +641 -0
- package/src/precompile.ts +8 -0
- package/src/ref-slot.ts +36 -0
- package/src/reference-field-map.ts +42 -0
- package/src/referrer-rule.ts +20 -1
- package/src/release/index.ts +1 -1
- package/src/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-projection.ts +302 -20
- package/src/system-kinds.ts +7 -2
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +104 -0
- package/src/validate-cel-context.ts +73 -78
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +85 -16
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-template-dispatch.ts +99 -0
- package/src/with-synthetic-positions.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
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";
|
|
2
|
+
import { celSourceOf, findDynamicLeaf, isTaggedCondition, pointerSegments, pointerToPath, readNodes, } from "./resource-rule.js";
|
|
3
|
+
import { RULE_BUDGET_MS, UNTAGGED_CONDITION, compileRuleCondition, conditionRefusals, } from "./rule-condition.js";
|
|
4
4
|
function isObject(value) {
|
|
5
5
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
6
|
}
|
|
@@ -13,7 +13,7 @@ function isObject(value) {
|
|
|
13
13
|
* declaring module's scope by `resolveSchemaRefKinds`, the only pass holding
|
|
14
14
|
* that scope, and a name resolving to nothing is reported from there.
|
|
15
15
|
*/
|
|
16
|
-
export function validateReferrerRuleDeclarations(manifest) {
|
|
16
|
+
export function validateReferrerRuleDeclarations(manifest, context = {}) {
|
|
17
17
|
const own = manifest.schema;
|
|
18
18
|
const raw = readRawReferrerRules(own);
|
|
19
19
|
if (raw === undefined)
|
|
@@ -65,6 +65,10 @@ export function validateReferrerRuleDeclarations(manifest) {
|
|
|
65
65
|
if (entry.severity !== undefined && entry.severity !== "error" && entry.severity !== "warning") {
|
|
66
66
|
issue(`${at}.severity`, "'severity' must be 'error' or 'warning'.");
|
|
67
67
|
}
|
|
68
|
+
if (condition !== undefined && condition.length > 0 && !isTaggedCondition(entry.condition)) {
|
|
69
|
+
issue(`${at}.condition`, UNTAGGED_CONDITION);
|
|
70
|
+
}
|
|
71
|
+
validatePeersDeclaration(entry, at, issue, context);
|
|
68
72
|
if (condition) {
|
|
69
73
|
for (const refusal of conditionRefusals(condition))
|
|
70
74
|
issue(`${at}.condition`, refusal);
|
|
@@ -72,6 +76,40 @@ export function validateReferrerRuleDeclarations(manifest) {
|
|
|
72
76
|
});
|
|
73
77
|
return issues;
|
|
74
78
|
}
|
|
79
|
+
/** The strict half of `peers:` — the binding that lets a rule reach a referrer's
|
|
80
|
+
* OTHER entries, which is also the one that fails invisibly: a pointer naming a
|
|
81
|
+
* collection nothing in resolves would leave the rule seeing no declaration at
|
|
82
|
+
* all, and a check that never sees its subject reads as passing. */
|
|
83
|
+
function validatePeersDeclaration(entry, at, issue, context) {
|
|
84
|
+
const { peers } = entry;
|
|
85
|
+
if (peers === undefined)
|
|
86
|
+
return;
|
|
87
|
+
if (typeof peers !== "string") {
|
|
88
|
+
issue(`${at}.peers`, "'peers' is a JSON Pointer naming a collection OF THE REFERRER (e.g. /tables), whose " +
|
|
89
|
+
"other entries bind as 'peers' and whose own entry binds as 'entry'.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (pointerSegments(peers) === undefined || peers === "" || peers === "/") {
|
|
93
|
+
issue(`${at}.peers`, `'peers' must be a JSON Pointer to a collection, e.g. /tables — '${peers}' is not one.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (typeof entry.referrer !== "string" || entry.referrer.length === 0) {
|
|
97
|
+
issue(`${at}.peers`, "'peers' names a collection of the REFERRER, so the rule must also declare 'referrer:' — " +
|
|
98
|
+
"without a kind there is nothing to check the pointer against, and a pointer that " +
|
|
99
|
+
"resolves to nothing disables the rule in silence.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const target = context.peersTarget?.(entry.referrer, peers);
|
|
103
|
+
if (target === "absent") {
|
|
104
|
+
issue(`${at}.peers`, `'${entry.referrer}' declares no collection at '${peers}'. A pointer that resolves to ` +
|
|
105
|
+
"nothing binds no peers, so the rule would never see a sibling declaration.");
|
|
106
|
+
}
|
|
107
|
+
else if (target === "plain") {
|
|
108
|
+
issue(`${at}.peers`, `'${peers}' on '${entry.referrer}' holds plain data — nothing in it is a reference, so ` +
|
|
109
|
+
"no entry resolves to a declaration and the rule would run against values it cannot " +
|
|
110
|
+
"compare. Name a collection whose items are, or contain, a reference.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
75
113
|
/**
|
|
76
114
|
* Run a kind's referrer rules against every resource that references one of its
|
|
77
115
|
* resources.
|
|
@@ -81,19 +119,25 @@ export function validateReferrerRuleDeclarations(manifest) {
|
|
|
81
119
|
* manifest identity, since a name alone is module-scoped — because the condition
|
|
82
120
|
* reads the two manifests and nothing about the site, so a second site could only
|
|
83
121
|
* produce the identical verdict at a different path.
|
|
122
|
+
*
|
|
123
|
+
* **A rule declaring `peers:` is the exception, and evaluates once per ENTRY.**
|
|
124
|
+
* That is a deliberate departure from judge-once rather than an inconsistency: a
|
|
125
|
+
* rule that binds `entry` is about the entry, so a resource listed twice has two
|
|
126
|
+
* entries to answer for, and the two sites can genuinely disagree.
|
|
84
127
|
*/
|
|
85
128
|
export function evaluateReferrerRules(manifest, definitionSchema, referrers,
|
|
86
129
|
/** Whether a referrer of `kind` satisfies a rule's `referrer:` filter —
|
|
87
130
|
* Liskov-substitutable, so a child of the named kind matches. Supplied by the
|
|
88
131
|
* caller, which holds the definition registry. */
|
|
89
|
-
kindMatches) {
|
|
132
|
+
kindMatches, context = {}) {
|
|
90
133
|
const rules = readReferrerRules(definitionSchema);
|
|
91
134
|
if (rules.length === 0)
|
|
92
135
|
return [];
|
|
93
136
|
const self = manifest;
|
|
94
137
|
const findings = [];
|
|
95
138
|
for (const rule of rules) {
|
|
96
|
-
const
|
|
139
|
+
const perEntry = rule.peers !== undefined;
|
|
140
|
+
const compiled = compileRuleCondition(rule.condition, perEntry ? ["self", "referrer", "entry", "peers"] : ["self", "referrer"]);
|
|
97
141
|
if ("reason" in compiled) {
|
|
98
142
|
findings.push({ kind: "failed", rule, reason: compiled.reason });
|
|
99
143
|
continue;
|
|
@@ -108,29 +152,46 @@ kindMatches) {
|
|
|
108
152
|
for (const referrer of referrers) {
|
|
109
153
|
if (rule.referrer !== undefined && !kindMatches(rule.referrer, referrer.kind))
|
|
110
154
|
continue;
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
155
|
+
if (!perEntry) {
|
|
156
|
+
if (seen.has(referrer.manifest))
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(referrer.manifest);
|
|
159
|
+
}
|
|
160
|
+
const scope = {
|
|
161
|
+
self,
|
|
162
|
+
referrer: referrer.manifest,
|
|
163
|
+
};
|
|
164
|
+
if (perEntry) {
|
|
165
|
+
const bound = context.peerBinder
|
|
166
|
+
? context.peerBinder.bind(referrer.manifest, referrer.kind, rule.peers, referrer.path)
|
|
167
|
+
: {
|
|
168
|
+
ok: false,
|
|
169
|
+
failure: { reason: "unknown-shape", at: pointerToPath(rule.peers) },
|
|
170
|
+
};
|
|
171
|
+
if (!bound.ok) {
|
|
172
|
+
findings.push({ kind: "unbound", rule, referrer, failure: bound.failure });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
scope.peers = bound.binding.peers;
|
|
176
|
+
scope.entry = bound.binding.entry;
|
|
177
|
+
}
|
|
114
178
|
// Only the nodes this condition READS decide whether it can run — the
|
|
115
179
|
// resource-rule reasoning, and it bites harder here: the referrer is a
|
|
116
180
|
// whole manifest, so scanning all of it would disable the rule for any
|
|
117
181
|
// server carrying one unrelated expression.
|
|
118
|
-
let
|
|
119
|
-
for (const node of readNodes(chains, {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})) {
|
|
123
|
-
dynamicAt = findDynamicLeaf(node);
|
|
124
|
-
if (dynamicAt !== undefined)
|
|
182
|
+
let dynamic;
|
|
183
|
+
for (const node of readNodes(chains, scope)) {
|
|
184
|
+
dynamic = findDynamicLeaf(node);
|
|
185
|
+
if (dynamic !== undefined)
|
|
125
186
|
break;
|
|
126
187
|
}
|
|
127
|
-
if (
|
|
128
|
-
findings.push({ kind: "skipped", rule, referrer,
|
|
188
|
+
if (dynamic !== undefined) {
|
|
189
|
+
findings.push({ kind: "skipped", rule, referrer, dynamic });
|
|
129
190
|
continue;
|
|
130
191
|
}
|
|
131
192
|
let held;
|
|
132
193
|
try {
|
|
133
|
-
held = parsed(
|
|
194
|
+
held = parsed(scope);
|
|
134
195
|
}
|
|
135
196
|
catch (err) {
|
|
136
197
|
findings.push({
|
|
@@ -156,11 +217,38 @@ kindMatches) {
|
|
|
156
217
|
/** Whether a rule found any referrer to judge — the input to the never-exercised
|
|
157
218
|
* report, which is how a mistyped `referrer:` filter would otherwise disable a
|
|
158
219
|
* check in silence. */
|
|
159
|
-
export function referrerRuleExercised(rule, referrers, kindMatches) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
220
|
+
export function referrerRuleExercised(rule, referrers, kindMatches, context = {}) {
|
|
221
|
+
const matching = rule.referrer === undefined
|
|
222
|
+
? referrers
|
|
223
|
+
: referrers.filter((referrer) => kindMatches(rule.referrer, referrer.kind));
|
|
224
|
+
if (matching.length === 0)
|
|
225
|
+
return false;
|
|
226
|
+
if (rule.peers === undefined)
|
|
227
|
+
return true;
|
|
228
|
+
// A peer rule that ran against an EMPTY peer set proved nothing — and an empty
|
|
229
|
+
// set everywhere is exactly what a typo in `peers:` looks like from outside,
|
|
230
|
+
// which is silence that reads as passing. Asked through the binder, so it
|
|
231
|
+
// shares the resolution the evaluation already paid for.
|
|
232
|
+
const binder = context.peerBinder;
|
|
233
|
+
if (!binder)
|
|
234
|
+
return false;
|
|
235
|
+
return matching.some((referrer) => binder.hasPeers(referrer.manifest, referrer.kind, rule.peers, referrer.path));
|
|
236
|
+
}
|
|
237
|
+
/** Why a peer binding could not be produced, as the sentence a reader acts on. */
|
|
238
|
+
function peerBindingReason(failure) {
|
|
239
|
+
switch (failure.reason) {
|
|
240
|
+
case "no-collection":
|
|
241
|
+
return `'${failure.at}' holds no collection to bind 'peers' from.`;
|
|
242
|
+
case "unresolved":
|
|
243
|
+
return (`a reference at '${failure.at}' names a declaration this analysis does not hold, ` +
|
|
244
|
+
"so a peer would bind to nothing.");
|
|
245
|
+
case "dynamic":
|
|
246
|
+
return (`a value at '${failure.at}' holds ${failure.what ?? "a value"}, which is not known ` +
|
|
247
|
+
"until the resource is created, so the comparison would run against a placeholder.");
|
|
248
|
+
case "unknown-shape":
|
|
249
|
+
return (`which paths under '${failure.at}' hold references is not known here, so nothing ` +
|
|
250
|
+
"could be resolved into a declaration.");
|
|
251
|
+
}
|
|
164
252
|
}
|
|
165
253
|
const nameOf = (manifest) => manifest.metadata?.name ?? "<unnamed>";
|
|
166
254
|
/**
|
|
@@ -196,8 +284,8 @@ declarationIsOurs) {
|
|
|
196
284
|
severity: "information",
|
|
197
285
|
message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)}: rule ` +
|
|
198
286
|
`'${finding.rule.code}' from ${declaringKind} did not run at ` +
|
|
199
|
-
`'${finding.referrer.path}' — the value holds
|
|
200
|
-
`'${finding.
|
|
287
|
+
`'${finding.referrer.path}' — the value holds ${finding.dynamic.what} at ` +
|
|
288
|
+
`'${finding.dynamic.path}', which is not known until the resource is created. ` +
|
|
201
289
|
"Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
|
|
202
290
|
manifest: finding.referrer.manifest,
|
|
203
291
|
path: finding.referrer.path,
|
|
@@ -205,6 +293,20 @@ declarationIsOurs) {
|
|
|
205
293
|
});
|
|
206
294
|
continue;
|
|
207
295
|
}
|
|
296
|
+
if (finding.kind === "unbound") {
|
|
297
|
+
out.push({
|
|
298
|
+
code: "REFERRER_RULE_SKIPPED",
|
|
299
|
+
severity: "information",
|
|
300
|
+
message: `${finding.referrer.kind}/${nameOf(finding.referrer.manifest)}: rule ` +
|
|
301
|
+
`'${finding.rule.code}' from ${declaringKind} did not run at ` +
|
|
302
|
+
`'${finding.referrer.path}' — ${peerBindingReason(finding.failure)}` +
|
|
303
|
+
" Reported rather than dropped: a check whose coverage varies invisibly reads as passing.",
|
|
304
|
+
manifest: finding.referrer.manifest,
|
|
305
|
+
path: finding.referrer.path,
|
|
306
|
+
rule: finding.rule.code,
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
208
310
|
const because = finding.kind === "failed"
|
|
209
311
|
? `failed to evaluate: ${finding.reason}. Guard an optional field with 'in' or '.?'.`
|
|
210
312
|
: `exceeded its evaluation budget (${finding.elapsedMs}ms) and was stopped, so ` +
|
|
@@ -242,12 +344,18 @@ export function reportUnexercisedReferrerRule(definition, rule) {
|
|
|
242
344
|
return {
|
|
243
345
|
code: "REFERRER_RULE_UNEXERCISED",
|
|
244
346
|
severity: "information",
|
|
245
|
-
message:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
347
|
+
message: rule.peers !== undefined
|
|
348
|
+
? `Referrer rule '${rule.code}' never ran with peers to compare: nothing` +
|
|
349
|
+
(rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
|
|
350
|
+
` references a resource of this kind while '${rule.peers}' held another ` +
|
|
351
|
+
"declaration, so nothing has proven the condition. An empty peer set " +
|
|
352
|
+
"everywhere is what a typo in 'peers:' looks like from outside."
|
|
353
|
+
: `Referrer rule '${rule.code}' never ran: nothing` +
|
|
354
|
+
(rule.referrer === undefined ? "" : ` of kind '${rule.referrer}'`) +
|
|
355
|
+
" references a resource of this kind, so nothing has proven the condition." +
|
|
356
|
+
(rule.referrer === undefined
|
|
357
|
+
? ""
|
|
358
|
+
: " A 'referrer' naming a kind no manifest uses disables the rule in silence."),
|
|
251
359
|
manifest: definition,
|
|
252
360
|
path: `schema.${REFERRER_RULES_ANNOTATION}[${rule.index}]`,
|
|
253
361
|
rule: rule.code,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
import { type AnalysisDiagnostic } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
|
|
7
|
+
* split applied to the resource-input boundary, and not optional for the same
|
|
8
|
+
* reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
|
|
9
|
+
* leaves the library referencing a name nothing stands behind; an unchecked
|
|
10
|
+
* INJECTION hands a library an instance of a kind it never asked for, which
|
|
11
|
+
* surfaces as a method-missing failure inside someone else's module.
|
|
12
|
+
*
|
|
13
|
+
* Two surfaces, scoped the same way every other declaration check is — to the
|
|
14
|
+
* entry's own modules, since a published dependency's block is not the
|
|
15
|
+
* consumer's to fix:
|
|
16
|
+
*
|
|
17
|
+
* - **The library's own declaration.** Each entry's alias-qualified `kind:` must
|
|
18
|
+
* resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
|
|
19
|
+
* A constraint that resolves to nothing accepts anything, which is the same
|
|
20
|
+
* hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
|
|
21
|
+
* - **The injection site.** Every declared entry must be supplied
|
|
22
|
+
* (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
|
|
23
|
+
* variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
|
|
24
|
+
* must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
|
|
25
|
+
* kind must satisfy the declared constraint transitively
|
|
26
|
+
* (`RESOURCE_INPUT_KIND_MISMATCH`).
|
|
27
|
+
*
|
|
28
|
+
* Browser-safe.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateResourceInputs(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver, rootModules: Set<string>,
|
|
31
|
+
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
32
|
+
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
33
|
+
* two cannot disagree about what satisfies a constraint. */
|
|
34
|
+
acceptsKind: (suppliedKind: string, requiredKind: string) => boolean): AnalysisDiagnostic[];
|
|
35
|
+
//# sourceMappingURL=validate-resource-inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-resource-inputs.d.ts","sourceRoot":"","sources":["../src/validate-resource-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,YAAY,CAAC;AASzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC;AACxB;;6DAE6D;AAC7D,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,GACnE,kBAAkB,EAAE,CA6LtB"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
import { findDynamicLeaf } from "./resource-rule.js";
|
|
3
|
+
import { readResourceInputs, readSuppliedResources } from "./resource-input.js";
|
|
4
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
5
|
+
const SOURCE = "telo-analyzer";
|
|
6
|
+
/**
|
|
7
|
+
* The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
|
|
8
|
+
* split applied to the resource-input boundary, and not optional for the same
|
|
9
|
+
* reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
|
|
10
|
+
* leaves the library referencing a name nothing stands behind; an unchecked
|
|
11
|
+
* INJECTION hands a library an instance of a kind it never asked for, which
|
|
12
|
+
* surfaces as a method-missing failure inside someone else's module.
|
|
13
|
+
*
|
|
14
|
+
* Two surfaces, scoped the same way every other declaration check is — to the
|
|
15
|
+
* entry's own modules, since a published dependency's block is not the
|
|
16
|
+
* consumer's to fix:
|
|
17
|
+
*
|
|
18
|
+
* - **The library's own declaration.** Each entry's alias-qualified `kind:` must
|
|
19
|
+
* resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
|
|
20
|
+
* A constraint that resolves to nothing accepts anything, which is the same
|
|
21
|
+
* hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
|
|
22
|
+
* - **The injection site.** Every declared entry must be supplied
|
|
23
|
+
* (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
|
|
24
|
+
* variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
|
|
25
|
+
* must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
|
|
26
|
+
* kind must satisfy the declared constraint transitively
|
|
27
|
+
* (`RESOURCE_INPUT_KIND_MISMATCH`).
|
|
28
|
+
*
|
|
29
|
+
* Browser-safe.
|
|
30
|
+
*/
|
|
31
|
+
export function validateResourceInputs(manifests, registry, aliases, rootModules,
|
|
32
|
+
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
33
|
+
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
34
|
+
* two cannot disagree about what satisfies a constraint. */
|
|
35
|
+
acceptsKind) {
|
|
36
|
+
const out = [];
|
|
37
|
+
const isOwn = (module) => !module || rootModules.has(module);
|
|
38
|
+
// --- the library's own declaration -------------------------------------
|
|
39
|
+
for (const m of manifests) {
|
|
40
|
+
if (m.kind !== "Telo.Library")
|
|
41
|
+
continue;
|
|
42
|
+
const moduleName = m.metadata?.name;
|
|
43
|
+
// Only the entry's own modules, so the declaring scope is always the root
|
|
44
|
+
// alias table — a library analyzed here IS a root module.
|
|
45
|
+
if (!isOwn(moduleName))
|
|
46
|
+
continue;
|
|
47
|
+
const scope = aliases;
|
|
48
|
+
const exported = new Set((m.exports?.resources ?? []).filter((e) => typeof e === "string"));
|
|
49
|
+
for (const input of readResourceInputs(m)) {
|
|
50
|
+
// An input is the IMPORTER's instance, borrowed. Exporting it back out
|
|
51
|
+
// would forward a kind-only stand-in into the consumer's flattened set as
|
|
52
|
+
// a resource this library declares — a phantom the consumer can `!ref`
|
|
53
|
+
// and whose kind constraint is all there is behind it. Handing an
|
|
54
|
+
// instance straight back to whoever supplied it is also a relation
|
|
55
|
+
// nothing needs; if it ever does, it should be a decision rather than a
|
|
56
|
+
// name collision nobody noticed.
|
|
57
|
+
if (exported.has(input.name)) {
|
|
58
|
+
out.push({
|
|
59
|
+
severity: DiagnosticSeverity.Error,
|
|
60
|
+
code: "RESOURCE_INPUT_EXPORTED",
|
|
61
|
+
source: SOURCE,
|
|
62
|
+
message: `Resource input '${input.name}' is also listed in 'exports.resources'. An input is ` +
|
|
63
|
+
`an instance the importer supplies, not one this library declares, so there is ` +
|
|
64
|
+
`nothing to export — remove it from 'exports.resources', or rename the input.`,
|
|
65
|
+
data: {
|
|
66
|
+
resource: { kind: m.kind, name: moduleName },
|
|
67
|
+
filePath: m.metadata?.source,
|
|
68
|
+
path: `resources.${input.name}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (registry.resolve(input.kind) ?? registry.resolve(scope.resolveKind(input.kind) ?? "")) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out.push({
|
|
76
|
+
severity: DiagnosticSeverity.Error,
|
|
77
|
+
code: "RESOURCE_INPUT_KIND_UNRESOLVED",
|
|
78
|
+
source: SOURCE,
|
|
79
|
+
message: `Resource input '${input.name}' is constrained to kind '${input.kind}', which does not ` +
|
|
80
|
+
`resolve in this library's scope. Write it alias-qualified — '<Alias>.<Kind>' for an ` +
|
|
81
|
+
`import declared in this file, 'Self.<Kind>' for a kind this library owns, or ` +
|
|
82
|
+
`'Telo.<Kind>' for a built-in. An unresolvable constraint accepts anything.`,
|
|
83
|
+
data: {
|
|
84
|
+
resource: { kind: m.kind, name: moduleName },
|
|
85
|
+
filePath: m.metadata?.source,
|
|
86
|
+
path: `resources.${input.name}.kind`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// --- the injection site --------------------------------------------------
|
|
92
|
+
for (const m of manifests) {
|
|
93
|
+
if (m.kind !== "Telo.Import")
|
|
94
|
+
continue;
|
|
95
|
+
const meta = m.metadata;
|
|
96
|
+
const alias = meta?.name;
|
|
97
|
+
if (!alias || !isOwn(meta?.module))
|
|
98
|
+
continue;
|
|
99
|
+
// An import whose target identity was never established registers nothing —
|
|
100
|
+
// reporting a missing input against it would blame the author for a
|
|
101
|
+
// dependency the loader has already said it could not obtain.
|
|
102
|
+
if (!meta?.resolvedModuleName)
|
|
103
|
+
continue;
|
|
104
|
+
// A singleton has no room for a per-import override: `logging:` scopes a
|
|
105
|
+
// subtree that is no longer this import's subtree, and `runtime:` selects a
|
|
106
|
+
// controller backend for a library that is instantiated once. Whichever
|
|
107
|
+
// import happened to be created first would decide, so the field is
|
|
108
|
+
// rejected rather than silently applied to everyone or silently dropped.
|
|
109
|
+
if (meta.sharedLibrary === true) {
|
|
110
|
+
for (const field of ["logging", "runtime"]) {
|
|
111
|
+
if (m[field] === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
out.push({
|
|
114
|
+
severity: DiagnosticSeverity.Error,
|
|
115
|
+
code: "SHARED_LIBRARY_OVERRIDE",
|
|
116
|
+
source: SOURCE,
|
|
117
|
+
message: `Import '${alias}' declares '${field}:', but module ` +
|
|
118
|
+
`'${meta.resolvedModuleName}' is 'lifecycle: shared' — one instantiation for the ` +
|
|
119
|
+
`whole application, so a per-import override cannot apply to it. Remove it, or ` +
|
|
120
|
+
`make the library 'lifecycle: isolated'.`,
|
|
121
|
+
data: {
|
|
122
|
+
resource: { kind: m.kind, name: alias },
|
|
123
|
+
filePath: meta.source,
|
|
124
|
+
path: field,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const required = Object.entries(meta.requiredResources ?? {});
|
|
130
|
+
const supplied = readSuppliedResources(m);
|
|
131
|
+
const declared = new Map(required);
|
|
132
|
+
const resource = { kind: m.kind, name: alias };
|
|
133
|
+
const filePath = meta.source;
|
|
134
|
+
for (const [entryName, entryKind] of required) {
|
|
135
|
+
if (entryName in supplied)
|
|
136
|
+
continue;
|
|
137
|
+
out.push({
|
|
138
|
+
severity: DiagnosticSeverity.Error,
|
|
139
|
+
code: "RESOURCE_INPUT_MISSING",
|
|
140
|
+
source: SOURCE,
|
|
141
|
+
message: `Import '${alias}' does not supply the resource input '${entryName}', which module ` +
|
|
142
|
+
`'${meta.resolvedModuleName}' requires (kind '${entryKind}'). Add ` +
|
|
143
|
+
`\`resources: { ${entryName}: !ref <name> }\` to the import.`,
|
|
144
|
+
data: { resource, filePath, path: "resources" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const [name, value] of Object.entries(supplied)) {
|
|
148
|
+
const path = `resources.${name}`;
|
|
149
|
+
const entry = declared.get(name);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
const known = required.map(([n]) => n).join(", ") || "(none)";
|
|
152
|
+
out.push({
|
|
153
|
+
severity: DiagnosticSeverity.Error,
|
|
154
|
+
code: "RESOURCE_INPUT_UNKNOWN",
|
|
155
|
+
source: SOURCE,
|
|
156
|
+
message: `Import '${alias}' supplies a resource input '${name}', which module ` +
|
|
157
|
+
`'${meta.resolvedModuleName}' does not declare. Declared inputs: ${known}.`,
|
|
158
|
+
data: { resource, filePath, path },
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Phase 2.5 rewrote a resolvable `!ref` to `{kind, name}`; a sentinel that
|
|
163
|
+
// survived names nothing the importer can reach.
|
|
164
|
+
if (isRefSentinel(value)) {
|
|
165
|
+
out.push({
|
|
166
|
+
severity: DiagnosticSeverity.Error,
|
|
167
|
+
code: "RESOURCE_INPUT_UNRESOLVED",
|
|
168
|
+
source: SOURCE,
|
|
169
|
+
message: `Import '${alias}': resource input '${name}' → resource '${value.source}' not found`,
|
|
170
|
+
data: { resource, filePath, path },
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
175
|
+
out.push({
|
|
176
|
+
severity: DiagnosticSeverity.Error,
|
|
177
|
+
code: "RESOURCE_INPUT_UNRESOLVED",
|
|
178
|
+
source: SOURCE,
|
|
179
|
+
message: `Import '${alias}': resource input '${name}' must be a '!ref' to a resource this ` +
|
|
180
|
+
`module declares.`,
|
|
181
|
+
data: { resource, filePath, path },
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const suppliedKind = value.kind;
|
|
186
|
+
if (typeof suppliedKind !== "string")
|
|
187
|
+
continue;
|
|
188
|
+
const canonical = aliases.resolveKind(suppliedKind) ?? suppliedKind;
|
|
189
|
+
if (acceptsKind(canonical, entry))
|
|
190
|
+
continue;
|
|
191
|
+
out.push({
|
|
192
|
+
severity: DiagnosticSeverity.Error,
|
|
193
|
+
code: "RESOURCE_INPUT_KIND_MISMATCH",
|
|
194
|
+
source: SOURCE,
|
|
195
|
+
message: `Import '${alias}': resource input '${name}' is '${suppliedKind}' (resolved: ` +
|
|
196
|
+
`'${canonical}'), which does not satisfy the declared constraint '${entry}'.`,
|
|
197
|
+
data: { resource, filePath, path },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
out.push(...sharedLibraryConflicts(manifests, isOwn));
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
const SUPPLIED_BLOCKS = ["variables", "secrets", "resources"];
|
|
205
|
+
/**
|
|
206
|
+
* Two imports of ONE `lifecycle: shared` library supplying different values.
|
|
207
|
+
*
|
|
208
|
+
* The runtime half (`ERR_SHARED_LIBRARY_CONFLICT`) is authoritative — it holds
|
|
209
|
+
* resolved values and live instances, and a library reached through a
|
|
210
|
+
* programmatic load never passed `telo check`. But the overwhelmingly common
|
|
211
|
+
* shape is two literal scalars in one flattened manifest set, which is decidable
|
|
212
|
+
* here, and a conflict is a hard BOOT failure: finding it at `telo check` is the
|
|
213
|
+
* difference between a squiggle and a deployment that will not start.
|
|
214
|
+
*
|
|
215
|
+
* Only what is DECIDABLE is compared, and the two ways it is not are skipped
|
|
216
|
+
* rather than guessed:
|
|
217
|
+
*
|
|
218
|
+
* - A value holding a `!cel` (at any depth) is known only at load. Two
|
|
219
|
+
* different expressions may evaluate equal, and identical text in two
|
|
220
|
+
* different modules may not.
|
|
221
|
+
* - A `resources:` entry names a resource by name, which means the same
|
|
222
|
+
* instance only when both imports were DECLARED in the same module — so a
|
|
223
|
+
* cross-module pair is left to the runtime.
|
|
224
|
+
*
|
|
225
|
+
* Grouped by the target's RESOLVED SOURCE, the identity the kernel keys its
|
|
226
|
+
* singleton registry on; `resolvedModuleName` would collapse two versions of one
|
|
227
|
+
* module, which are two libraries.
|
|
228
|
+
*/
|
|
229
|
+
function sharedLibraryConflicts(manifests, isOwn) {
|
|
230
|
+
const out = [];
|
|
231
|
+
const byTarget = new Map();
|
|
232
|
+
for (const m of manifests) {
|
|
233
|
+
if (m.kind !== "Telo.Import")
|
|
234
|
+
continue;
|
|
235
|
+
const meta = m.metadata;
|
|
236
|
+
if (meta?.sharedLibrary !== true || !meta.name || !meta.resolvedSource)
|
|
237
|
+
continue;
|
|
238
|
+
const blocks = new Map();
|
|
239
|
+
for (const block of SUPPLIED_BLOCKS) {
|
|
240
|
+
const value = m[block];
|
|
241
|
+
blocks.set(block, value && typeof value === "object" && !Array.isArray(value)
|
|
242
|
+
? value
|
|
243
|
+
: {});
|
|
244
|
+
}
|
|
245
|
+
const bucket = byTarget.get(meta.resolvedSource);
|
|
246
|
+
const entry = {
|
|
247
|
+
alias: meta.name,
|
|
248
|
+
module: meta.module,
|
|
249
|
+
filePath: meta.source,
|
|
250
|
+
blocks,
|
|
251
|
+
};
|
|
252
|
+
if (bucket)
|
|
253
|
+
bucket.push(entry);
|
|
254
|
+
else
|
|
255
|
+
byTarget.set(meta.resolvedSource, [entry]);
|
|
256
|
+
}
|
|
257
|
+
for (const group of byTarget.values()) {
|
|
258
|
+
if (group.length < 2)
|
|
259
|
+
continue;
|
|
260
|
+
const [first, ...rest] = group;
|
|
261
|
+
for (const later of rest) {
|
|
262
|
+
for (const block of SUPPLIED_BLOCKS) {
|
|
263
|
+
// A reference means the same instance only within one module's scope.
|
|
264
|
+
if (block === "resources" && first.module !== later.module)
|
|
265
|
+
continue;
|
|
266
|
+
const a = first.blocks.get(block);
|
|
267
|
+
const b = later.blocks.get(block);
|
|
268
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
269
|
+
if (findDynamicLeaf(a[key]) || findDynamicLeaf(b[key]))
|
|
270
|
+
continue;
|
|
271
|
+
if (sameSuppliedValue(a[key], b[key]))
|
|
272
|
+
continue;
|
|
273
|
+
out.push({
|
|
274
|
+
severity: DiagnosticSeverity.Error,
|
|
275
|
+
code: "SHARED_LIBRARY_CONFLICT",
|
|
276
|
+
source: SOURCE,
|
|
277
|
+
message: `Import '${later.alias}' and import '${first.alias}' both reach a ` +
|
|
278
|
+
`'lifecycle: shared' library — one instantiation for the whole application — but ` +
|
|
279
|
+
`they supply different values for ${block}.${key}. Make the two imports agree, or ` +
|
|
280
|
+
`make the library 'lifecycle: isolated'.`,
|
|
281
|
+
data: isOwn(later.module)
|
|
282
|
+
? {
|
|
283
|
+
resource: { kind: "Telo.Import", name: later.alias },
|
|
284
|
+
filePath: later.filePath,
|
|
285
|
+
path: `${block}.${key}`,
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
resource: { kind: "Telo.Import", name: first.alias },
|
|
289
|
+
filePath: first.filePath,
|
|
290
|
+
path: `${block}.${key}`,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** Structural equality, key-order insensitive — the analyzer's half of the rule
|
|
300
|
+
* the kernel applies to resolved values. A secret's VALUE is compared but never
|
|
301
|
+
* printed: the key is what the author has to look at. */
|
|
302
|
+
function sameSuppliedValue(a, b) {
|
|
303
|
+
if (a === b)
|
|
304
|
+
return true;
|
|
305
|
+
if (a === undefined || b === undefined || a === null || b === null)
|
|
306
|
+
return false;
|
|
307
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
308
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
309
|
+
return false;
|
|
310
|
+
return a.every((item, i) => sameSuppliedValue(item, b[i]));
|
|
311
|
+
}
|
|
312
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
313
|
+
const left = a;
|
|
314
|
+
const right = b;
|
|
315
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
316
|
+
return [...keys].every((key) => sameSuppliedValue(left[key], right[key]));
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Browser-safe: no Node built-ins.
|
|
23
23
|
*/
|
|
24
24
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
25
|
-
import { type ResourceRule } from "./resource-rule.js";
|
|
25
|
+
import { type DynamicLeaf, type ResourceRule } from "./resource-rule.js";
|
|
26
26
|
/** Published name for the shared budget — see `rule-condition.ts`. */
|
|
27
27
|
export declare const RESOURCE_RULE_BUDGET_MS = 50;
|
|
28
28
|
export interface ResourceRuleIssue {
|
|
@@ -42,7 +42,7 @@ export type ResourceRuleFinding = {
|
|
|
42
42
|
kind: "skipped";
|
|
43
43
|
rule: ResourceRule;
|
|
44
44
|
path: string;
|
|
45
|
-
|
|
45
|
+
dynamic: DynamicLeaf;
|
|
46
46
|
} | {
|
|
47
47
|
kind: "failed";
|
|
48
48
|
rule: ResourceRule;
|
|
@@ -54,6 +54,15 @@ export type ResourceRuleFinding = {
|
|
|
54
54
|
path: string;
|
|
55
55
|
elapsedMs: number;
|
|
56
56
|
};
|
|
57
|
+
/** Navigate a kind's own schema to the node describing what a pointer names, so
|
|
58
|
+
* an `in:` naming a field the kind does not declare is caught at the kind.
|
|
59
|
+
* Exported because the peer-rule strict half asks the same question of the
|
|
60
|
+
* REFERRER's schema, and two navigators would eventually disagree about what a
|
|
61
|
+
* pointer names. */
|
|
62
|
+
export declare function schemaAtPointer(schema: unknown, pointer: string): unknown;
|
|
63
|
+
/** True when a schema node describes something a rule can iterate. Unknown or
|
|
64
|
+
* absent `type` passes: an open schema is not evidence of a defect. */
|
|
65
|
+
export declare function isIterableSchema(node: unknown): boolean;
|
|
57
66
|
/**
|
|
58
67
|
* Report every way a kind's rule declarations are malformed. Runs on the
|
|
59
68
|
* `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
|
|
@@ -1 +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;
|
|
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;AAOrD,OAAO,EAIL,KAAK,WAAW,EAOhB,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,OAAO,EAAE,WAAW,CAAA;CAAE,GAC3E;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;AAMjF;;;;qBAIqB;AACrB,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAezE;AAED;wEACwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAMvD;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,gBAAgB;AAC1B;;;;;;;;;GASG;AACH,eAAe,CAAC,EAAE,OAAO,GACxB,iBAAiB,EAAE,CAmGrB;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"}
|