@telorun/analyzer 0.64.0 → 0.66.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 +175 -65
- 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.map +1 -1
- package/dist/cel-scope.js +24 -4
- package/dist/definition-registry.d.ts +47 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +82 -13
- 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 +1 -1
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +8 -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/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/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-compat.d.ts +59 -22
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +60 -75
- package/dist/schema-error-report.d.ts +68 -0
- package/dist/schema-error-report.d.ts.map +1 -0
- package/dist/schema-error-report.js +356 -0
- package/dist/schema-projection.d.ts +106 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +206 -18
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- 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 +5 -70
- package/dist/validate-nested-inline.d.ts +22 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +17 -9
- 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.map +1 -1
- package/dist/validate-references.js +9 -2
- 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-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-step-inputs.js +11 -3
- 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 +211 -68
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +24 -6
- package/src/definition-registry.ts +90 -11
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +55 -6
- package/src/manifest-visitor.ts +8 -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/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/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-compat.ts +92 -79
- package/src/schema-error-report.ts +417 -0
- package/src/schema-projection.ts +283 -20
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +6 -71
- package/src/validate-nested-inline.ts +35 -14
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +15 -2
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-step-inputs.ts +10 -4
- package/src/with-synthetic-positions.ts +1 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { isRefEntry } from "./reference-field-map.js";
|
|
2
|
+
import { dynamicNode, findDynamicLeaf, pointerToPath, resolvePointer, } from "./resource-rule.js";
|
|
3
|
+
import { isIterableSchema, schemaAtPointer } from "./validate-resource-rules.js";
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/** The reference a value holds, in either shape, or `undefined`. Read only at a
|
|
8
|
+
* path the field map declares to be a reference slot. */
|
|
9
|
+
export function referenceValueOf(value) {
|
|
10
|
+
if (!isObject(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
if (value.__tagged === true && value.engine === "ref" && typeof value.source === "string") {
|
|
13
|
+
const dot = value.source.indexOf(".");
|
|
14
|
+
return dot > 0
|
|
15
|
+
? { alias: value.source.slice(0, dot), name: value.source.slice(dot + 1) }
|
|
16
|
+
: { name: value.source };
|
|
17
|
+
}
|
|
18
|
+
if (typeof value.name === "string" && typeof value.kind === "string") {
|
|
19
|
+
return {
|
|
20
|
+
name: value.name,
|
|
21
|
+
kind: value.kind,
|
|
22
|
+
...(typeof value.alias === "string" ? { alias: value.alias } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** True when a concrete path is an instance of a field-map shape — `tables[2]`
|
|
28
|
+
* of `tables[]`, `mounts[1].mount` of `mounts[].mount`, `tables.orders` of
|
|
29
|
+
* `tables.{}`. A map key containing a dot is not distinguishable here, the same
|
|
30
|
+
* ambiguity the concrete path itself carries. */
|
|
31
|
+
export function shapeMatches(concrete, shape) {
|
|
32
|
+
const c = concrete.split(".");
|
|
33
|
+
const s = shape.split(".");
|
|
34
|
+
if (c.length !== s.length)
|
|
35
|
+
return false;
|
|
36
|
+
return s.every((segment, i) => {
|
|
37
|
+
if (segment === "{}")
|
|
38
|
+
return true;
|
|
39
|
+
if (segment.endsWith("[]")) {
|
|
40
|
+
const base = segment.slice(0, -2);
|
|
41
|
+
return c[i].startsWith(`${base}[`) && /^\[\d+\]$/.test(c[i].slice(base.length));
|
|
42
|
+
}
|
|
43
|
+
return segment === c[i];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** The concrete path of the ENTRY a slot path sits in, truncated at the first
|
|
47
|
+
* `[]` / `{}` marker of the shape it matches — which is where an entry begins,
|
|
48
|
+
* everything after it being the entry's own shape. */
|
|
49
|
+
export function entryBoundary(concrete, shape) {
|
|
50
|
+
const c = concrete.split(".");
|
|
51
|
+
const s = shape.split(".");
|
|
52
|
+
for (let i = 0; i < s.length; i++) {
|
|
53
|
+
if (s[i] === "{}" || s[i].endsWith("[]"))
|
|
54
|
+
return c.slice(0, i + 1).join(".");
|
|
55
|
+
}
|
|
56
|
+
return concrete;
|
|
57
|
+
}
|
|
58
|
+
/** Navigate a dotted/bracketed path (`mounts[1].mount`) within a value — the
|
|
59
|
+
* spelling the call graph gives an edge, so an edge's own path is what reads
|
|
60
|
+
* the entry it came from. */
|
|
61
|
+
export function navigatePath(value, path) {
|
|
62
|
+
if (path === "")
|
|
63
|
+
return value;
|
|
64
|
+
let current = value;
|
|
65
|
+
for (const segment of path.split(".")) {
|
|
66
|
+
const parsed = /^([^[\]]*)((?:\[\d+\])*)$/.exec(segment);
|
|
67
|
+
if (!parsed)
|
|
68
|
+
return undefined;
|
|
69
|
+
const [, key, indices] = parsed;
|
|
70
|
+
if (key !== "") {
|
|
71
|
+
if (!isObject(current))
|
|
72
|
+
return undefined;
|
|
73
|
+
current = current[key];
|
|
74
|
+
}
|
|
75
|
+
for (const index of indices.match(/\d+/g) ?? []) {
|
|
76
|
+
if (!Array.isArray(current))
|
|
77
|
+
return undefined;
|
|
78
|
+
current = current[Number(index)];
|
|
79
|
+
}
|
|
80
|
+
if (current === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The entry shape a collection's items take — `tables[]` for an array,
|
|
87
|
+
* `tables.{}` for a map — as the field map spells it.
|
|
88
|
+
*
|
|
89
|
+
* Chosen by the collection's RUNTIME type rather than by whichever shape the map
|
|
90
|
+
* lists first: the two spell an entry's concrete path differently (`tables[2]`
|
|
91
|
+
* versus `tables.orders`), so taking the wrong one leaves every entry unable to
|
|
92
|
+
* recognise itself.
|
|
93
|
+
*/
|
|
94
|
+
function entryShapeOf(shapes, collectionPath, isArray) {
|
|
95
|
+
const wanted = isArray ? `${collectionPath}[]` : `${collectionPath}.{}`;
|
|
96
|
+
return shapes.some((shape) => shape === wanted || shape.startsWith(`${wanted}.`))
|
|
97
|
+
? wanted
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Which paths inside one entry hold a reference: the item itself, or the named
|
|
101
|
+
* properties one level in. */
|
|
102
|
+
function entryRefsOf(shapes, entryShape) {
|
|
103
|
+
const properties = new Set();
|
|
104
|
+
let itemIsRef = false;
|
|
105
|
+
for (const shape of shapes) {
|
|
106
|
+
if (shape === entryShape) {
|
|
107
|
+
itemIsRef = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!shape.startsWith(`${entryShape}.`))
|
|
111
|
+
continue;
|
|
112
|
+
const rest = shape.slice(entryShape.length + 1);
|
|
113
|
+
if (!rest.includes("."))
|
|
114
|
+
properties.add(rest);
|
|
115
|
+
}
|
|
116
|
+
return { itemIsRef, properties };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The first `!cel` among the fields of a resolved DECLARATION that a peer rule
|
|
120
|
+
* can actually COMPARE — its own top-level scalars.
|
|
121
|
+
*
|
|
122
|
+
* Which fields a condition reads off an element cannot be recovered from the
|
|
123
|
+
* parse: cel-js emits no access chain for a comprehension variable, so
|
|
124
|
+
* `peers.exists(p, p.table == …)` yields only `peers` and nothing about `table`.
|
|
125
|
+
* The bound is drawn structurally instead, and top-level scalars are the honest
|
|
126
|
+
* line: a physical name, a type name, a marker — the identities a peer rule
|
|
127
|
+
* compares. Reaching deeper needs a comprehension over nested config, and a rule
|
|
128
|
+
* of that shape has an unbounded subject anyway.
|
|
129
|
+
*
|
|
130
|
+
* Scanning the whole declaration was tried first and is what this replaced: it
|
|
131
|
+
* disabled every rule over a table the moment any nested field held an
|
|
132
|
+
* expression — a seed's `when:`, a column default — which is noise where the
|
|
133
|
+
* rule was about a physical name.
|
|
134
|
+
*/
|
|
135
|
+
function dynamicInDeclaration(declaration) {
|
|
136
|
+
for (const [key, value] of Object.entries(declaration)) {
|
|
137
|
+
if (key === "metadata")
|
|
138
|
+
continue;
|
|
139
|
+
const dynamic = dynamicNode(value, key);
|
|
140
|
+
if (dynamic)
|
|
141
|
+
return dynamic;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/** One entry with the references INSIDE it resolved — the declaration itself for
|
|
146
|
+
* a bare `!ref`, or the entry with each declared reference property replaced. */
|
|
147
|
+
function resolveEntry(value, at, refs, lookup) {
|
|
148
|
+
if (refs.itemIsRef) {
|
|
149
|
+
const reference = referenceValueOf(value);
|
|
150
|
+
// An inline declaration (`{kind, …config}` with no name) is not a reference;
|
|
151
|
+
// it binds as written, exactly as any other non-reference entry does.
|
|
152
|
+
if (reference) {
|
|
153
|
+
const declaration = lookup(reference);
|
|
154
|
+
if (!declaration)
|
|
155
|
+
return { ok: false, failure: { reason: "unresolved", at } };
|
|
156
|
+
const dynamic = dynamicInDeclaration(declaration);
|
|
157
|
+
if (dynamic) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
failure: { reason: "dynamic", at: `${at} → ${dynamic.path}`, what: dynamic.what },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, value: declaration };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!isObject(value))
|
|
167
|
+
return { ok: true, value };
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const [key, child] of Object.entries(value)) {
|
|
170
|
+
if (!refs.properties.has(key)) {
|
|
171
|
+
// The author's OWN entry data beside the reference (`prefix` next to
|
|
172
|
+
// `mount`) — small, and read directly by a rule, so it is scanned whole.
|
|
173
|
+
const dynamic = findDynamicLeaf(child, `${at}.${key}`);
|
|
174
|
+
if (dynamic !== undefined) {
|
|
175
|
+
return { ok: false, failure: { reason: "dynamic", at: dynamic.path, what: dynamic.what } };
|
|
176
|
+
}
|
|
177
|
+
out[key] = child;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const reference = referenceValueOf(child);
|
|
181
|
+
if (!reference) {
|
|
182
|
+
out[key] = child;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const declaration = lookup(reference);
|
|
186
|
+
if (!declaration)
|
|
187
|
+
return { ok: false, failure: { reason: "unresolved", at: `${at}.${key}` } };
|
|
188
|
+
const dynamic = dynamicInDeclaration(declaration);
|
|
189
|
+
if (dynamic) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
failure: { reason: "dynamic", at: `${at}.${key} → ${dynamic.path}`, what: dynamic.what },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
out[key] = declaration;
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, value: out };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Binds `peers` and `entry`, resolving each referrer's collection once.
|
|
201
|
+
*
|
|
202
|
+
* One binder per analysis: the cache is what keeps a rule over an n-entry
|
|
203
|
+
* collection linear in resolution work rather than quadratic, and it is shared
|
|
204
|
+
* by the evaluation and the exercised check, which ask the same question.
|
|
205
|
+
*/
|
|
206
|
+
export class PeerBinder {
|
|
207
|
+
env;
|
|
208
|
+
constructor(env) {
|
|
209
|
+
this.env = env;
|
|
210
|
+
}
|
|
211
|
+
collections = new WeakMap();
|
|
212
|
+
/**
|
|
213
|
+
* @param slotPath concrete path of the edge that reached the referenced
|
|
214
|
+
* resource, e.g. `tables[2]` or `mounts[1].mount`.
|
|
215
|
+
*/
|
|
216
|
+
bind(referrer, referrerKind, pointer, slotPath) {
|
|
217
|
+
const collectionPath = pointerToPath(pointer);
|
|
218
|
+
const shapes = this.env.refSlotsOf(referrerKind);
|
|
219
|
+
if (!shapes)
|
|
220
|
+
return { ok: false, failure: { reason: "unknown-shape", at: collectionPath } };
|
|
221
|
+
const resolved = this.collection(referrer, pointer, collectionPath, shapes);
|
|
222
|
+
if (!resolved.ok)
|
|
223
|
+
return resolved;
|
|
224
|
+
const mine = resolved.entryShape
|
|
225
|
+
? this.entryKey(slotPath, collectionPath, resolved.entryShape)
|
|
226
|
+
: undefined;
|
|
227
|
+
const at = mine === undefined ? -1 : resolved.keys.indexOf(mine);
|
|
228
|
+
const peers = at === -1 ? resolved.values : resolved.values.filter((_, i) => i !== at);
|
|
229
|
+
if (at !== -1)
|
|
230
|
+
return { ok: true, binding: { peers, entry: resolved.values[at] } };
|
|
231
|
+
// The edge runs through a DIFFERENT collection than the one `peers:` names
|
|
232
|
+
// (a rule over a schema's `enums:` while my own entry sits in `tables:`), so
|
|
233
|
+
// the entry is found through the shape this slot path matches.
|
|
234
|
+
const shape = shapes.find((candidate) => shapeMatches(slotPath, candidate));
|
|
235
|
+
if (!shape)
|
|
236
|
+
return { ok: false, failure: { reason: "unknown-shape", at: slotPath } };
|
|
237
|
+
const boundary = entryBoundary(slotPath, shape);
|
|
238
|
+
const entryShape = entryBoundary(shape, shape);
|
|
239
|
+
const entry = resolveEntry(navigatePath(referrer, boundary), boundary, entryRefsOf(shapes, entryShape), this.env.declarationOf);
|
|
240
|
+
if (!entry.ok)
|
|
241
|
+
return entry;
|
|
242
|
+
return { ok: true, binding: { peers, entry: entry.value } };
|
|
243
|
+
}
|
|
244
|
+
/** True when the rule has something to compare — the input to the
|
|
245
|
+
* never-exercised report, asked through the same cache. */
|
|
246
|
+
hasPeers(referrer, referrerKind, pointer, slotPath) {
|
|
247
|
+
const bound = this.bind(referrer, referrerKind, pointer, slotPath);
|
|
248
|
+
return bound.ok && bound.binding.peers.length > 0;
|
|
249
|
+
}
|
|
250
|
+
collection(referrer, pointer, collectionPath, shapes) {
|
|
251
|
+
let byPointer = this.collections.get(referrer);
|
|
252
|
+
if (!byPointer) {
|
|
253
|
+
byPointer = new Map();
|
|
254
|
+
this.collections.set(referrer, byPointer);
|
|
255
|
+
}
|
|
256
|
+
const cached = byPointer.get(pointer);
|
|
257
|
+
if (cached)
|
|
258
|
+
return cached;
|
|
259
|
+
const resolved = this.resolveCollection(referrer, pointer, collectionPath, shapes);
|
|
260
|
+
byPointer.set(pointer, resolved);
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
resolveCollection(referrer, pointer, collectionPath, shapes) {
|
|
264
|
+
const raw = resolvePointer(referrer, pointer);
|
|
265
|
+
// An ABSENT collection is an EMPTY one, not an unbindable one — the line
|
|
266
|
+
// `resolveRuleSubjects` already draws for a resource rule's `in:`. A resource
|
|
267
|
+
// that simply declares none is the loudest case a peer rule has (a column
|
|
268
|
+
// naming an enum its schema lists nowhere), so degrading it to a skip would
|
|
269
|
+
// silence exactly the manifest the rule exists for.
|
|
270
|
+
if (raw === undefined || raw === null)
|
|
271
|
+
return { ok: true, keys: [], values: [] };
|
|
272
|
+
if (!Array.isArray(raw) && !isObject(raw)) {
|
|
273
|
+
return { ok: false, failure: { reason: "no-collection", at: collectionPath } };
|
|
274
|
+
}
|
|
275
|
+
const entryShape = entryShapeOf(shapes, collectionPath, Array.isArray(raw));
|
|
276
|
+
if (!entryShape) {
|
|
277
|
+
return { ok: false, failure: { reason: "unknown-shape", at: collectionPath } };
|
|
278
|
+
}
|
|
279
|
+
const refs = entryRefsOf(shapes, entryShape);
|
|
280
|
+
const keys = Array.isArray(raw) ? raw.map((_, i) => String(i)) : Object.keys(raw);
|
|
281
|
+
const items = Array.isArray(raw) ? raw : Object.values(raw);
|
|
282
|
+
const values = [];
|
|
283
|
+
for (let i = 0; i < items.length; i++) {
|
|
284
|
+
const at = Array.isArray(raw) ? `${collectionPath}[${keys[i]}]` : `${collectionPath}.${keys[i]}`;
|
|
285
|
+
const resolved = resolveEntry(items[i], at, refs, this.env.declarationOf);
|
|
286
|
+
if (!resolved.ok)
|
|
287
|
+
return resolved;
|
|
288
|
+
values.push(resolved.value);
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, keys, values, entryShape };
|
|
291
|
+
}
|
|
292
|
+
/** The array index or map key `slotPath` occupies in the peers collection, or
|
|
293
|
+
* `undefined` when the path does not run through it at all. */
|
|
294
|
+
entryKey(slotPath, collectionPath, entryShape) {
|
|
295
|
+
if (entryShape.endsWith("[]")) {
|
|
296
|
+
if (!slotPath.startsWith(`${collectionPath}[`))
|
|
297
|
+
return undefined;
|
|
298
|
+
const match = /^\[(\d+)\]/.exec(slotPath.slice(collectionPath.length));
|
|
299
|
+
return match ? match[1] : undefined;
|
|
300
|
+
}
|
|
301
|
+
if (!slotPath.startsWith(`${collectionPath}.`))
|
|
302
|
+
return undefined;
|
|
303
|
+
const rest = slotPath.slice(collectionPath.length + 1);
|
|
304
|
+
const dot = rest.indexOf(".");
|
|
305
|
+
return dot === -1 ? rest : rest.slice(0, dot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The binder for one analysis run.
|
|
310
|
+
*
|
|
311
|
+
* ONE per run: it caches each referrer's resolved collection, which is what
|
|
312
|
+
* keeps a rule over an n-entry collection from re-resolving that collection once
|
|
313
|
+
* per entry — and both the evaluation and the exercised check ask for it.
|
|
314
|
+
*
|
|
315
|
+
* Built here rather than assembled at the analysis site, the
|
|
316
|
+
* `analyzerContractScope` precedent: both halves are the binding's own rule
|
|
317
|
+
* expressed against the analyzer's registry, and inline they were 40 lines of a
|
|
318
|
+
* 2700-line pass, using two different field-map accessors for one question.
|
|
319
|
+
*/
|
|
320
|
+
export function analyzerPeerBinder(registry, aliases, manifests) {
|
|
321
|
+
const byName = new Map();
|
|
322
|
+
const byModuleAndName = new Map();
|
|
323
|
+
for (const m of manifests) {
|
|
324
|
+
const name = m.metadata?.name;
|
|
325
|
+
if (!name)
|
|
326
|
+
continue;
|
|
327
|
+
byName.set(name, m);
|
|
328
|
+
const mod = m.metadata.module;
|
|
329
|
+
if (mod)
|
|
330
|
+
byModuleAndName.set(`${mod}\0${name}`, m);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* An alias RESOLVES OR NOTHING DOES. Resource names are module-scoped, so two
|
|
334
|
+
* libraries each exporting a `users` share one bucket in `byName`; falling
|
|
335
|
+
* back to it when the alias names a module this analysis cannot resolve would
|
|
336
|
+
* compare against a declaration from somewhere else entirely — a confident
|
|
337
|
+
* wrong verdict, which is worse than the reported skip an absent binding
|
|
338
|
+
* produces.
|
|
339
|
+
*/
|
|
340
|
+
const declarationOf = (ref) => {
|
|
341
|
+
if (ref.alias && ref.alias !== "Self") {
|
|
342
|
+
const module = aliases.moduleForAlias(ref.alias);
|
|
343
|
+
return module ? byModuleAndName.get(`${module}\0${ref.name}`) : undefined;
|
|
344
|
+
}
|
|
345
|
+
return byName.get(ref.name);
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Which paths of a referrer kind hold references — the field map, which is the
|
|
349
|
+
* authority. A binder that sniffed for `{kind, name}` instead would resolve
|
|
350
|
+
* author data carrying those two keys to an unrelated manifest.
|
|
351
|
+
*/
|
|
352
|
+
const refSlotsOf = (kind) => {
|
|
353
|
+
const map = registry.getFieldMapForKind(kind, aliases);
|
|
354
|
+
if (!map)
|
|
355
|
+
return undefined;
|
|
356
|
+
return [...map].filter(([, entry]) => isRefEntry(entry)).map(([path]) => path);
|
|
357
|
+
};
|
|
358
|
+
return new PeerBinder({ declarationOf, refSlotsOf });
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The strict half's resolver for a `peers:` pointer.
|
|
362
|
+
*
|
|
363
|
+
* Liskov in BOTH directions, which is what the check has to be: the filter is
|
|
364
|
+
* usually an abstract (`Sql.Schema`, so one rule serves every backend) while the
|
|
365
|
+
* collection is declared by the backends that implement it, so a pointer
|
|
366
|
+
* resolving on any candidate resolves the rule. Reported only when NO candidate
|
|
367
|
+
* declares it, or when every candidate that does holds plain data — the two
|
|
368
|
+
* shapes where the rule would see no declaration at all.
|
|
369
|
+
*
|
|
370
|
+
* Takes only the registry: it is answered after the registration loop and before
|
|
371
|
+
* the manifest list exists, which is also why it is a separate function rather
|
|
372
|
+
* than a method on one environment object.
|
|
373
|
+
*/
|
|
374
|
+
export function analyzerPeersTarget(registry) {
|
|
375
|
+
return (referrerKind, pointer) => {
|
|
376
|
+
const implementations = registry
|
|
377
|
+
.getByExtends(referrerKind)
|
|
378
|
+
.map((d) => d.metadata.module ? `${d.metadata.module}.${d.metadata.name}` : String(d.metadata.name));
|
|
379
|
+
const candidates = [referrerKind, ...implementations];
|
|
380
|
+
const path = pointerToPath(pointer);
|
|
381
|
+
let sawSchema = false;
|
|
382
|
+
let sawCollection = false;
|
|
383
|
+
for (const kind of candidates) {
|
|
384
|
+
const schema = registry.effectiveSchema(kind);
|
|
385
|
+
if (!schema)
|
|
386
|
+
continue;
|
|
387
|
+
sawSchema = true;
|
|
388
|
+
const node = schemaAtPointer(schema, pointer);
|
|
389
|
+
if (node === undefined || !isIterableSchema(node))
|
|
390
|
+
continue;
|
|
391
|
+
sawCollection = true;
|
|
392
|
+
const map = registry.getFieldMap(kind);
|
|
393
|
+
if (!map)
|
|
394
|
+
continue;
|
|
395
|
+
for (const [fieldPath, entry] of map) {
|
|
396
|
+
if (!isRefEntry(entry))
|
|
397
|
+
continue;
|
|
398
|
+
if (fieldPath === `${path}[]` ||
|
|
399
|
+
fieldPath.startsWith(`${path}[].`) ||
|
|
400
|
+
fieldPath === `${path}.{}` ||
|
|
401
|
+
fieldPath.startsWith(`${path}.{}.`)) {
|
|
402
|
+
return "ok";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!sawSchema)
|
|
407
|
+
return "unknown";
|
|
408
|
+
// An ABSTRACT filter with no implementations in this analysis says nothing:
|
|
409
|
+
// the abstract declares the collection structurally and each backend
|
|
410
|
+
// restates the entry as a reference to its OWN kind, so checking a library
|
|
411
|
+
// on its own would report every such rule as pointing at plain data. The
|
|
412
|
+
// consumer's app analysis holds the backends and answers properly there.
|
|
413
|
+
const filter = registry.resolve(referrerKind);
|
|
414
|
+
if (implementations.length === 0 && filter?.kind === "Telo.Abstract")
|
|
415
|
+
return "unknown";
|
|
416
|
+
return sawCollection ? "plain" : "absent";
|
|
417
|
+
};
|
|
418
|
+
}
|
package/dist/ref-slot.d.ts
CHANGED
|
@@ -80,6 +80,19 @@ export interface RefSlot {
|
|
|
80
80
|
/** `x-telo-inline: true` on the slot or any `anyOf` branch — accepts an inline
|
|
81
81
|
* `{kind, ...config}` definition, not only a `!ref`. */
|
|
82
82
|
inline: boolean;
|
|
83
|
+
/** The slot's VALUE branches — `anyOf` / `oneOf` alternatives that carry no
|
|
84
|
+
* `x-telo-ref`, present only when the reference constraint is itself a
|
|
85
|
+
* *branch* rather than the node's own annotation.
|
|
86
|
+
*
|
|
87
|
+
* That narrowing is the whole point. A node-level `x-telo-ref` with branches
|
|
88
|
+
* beneath it (an Application `targets` entry) uses those branches to describe
|
|
89
|
+
* the POST-RESOLUTION structural forms a reference takes — a bare string
|
|
90
|
+
* there is the removed string-reference spelling, and admitting it as a value
|
|
91
|
+
* would retire `INVALID_REFERENCE_FORM` exactly where it still applies. A
|
|
92
|
+
* branch-level constraint says something different: this slot holds either a
|
|
93
|
+
* value of one shape or a reference, and a scalar is then a value, not a
|
|
94
|
+
* malformed reference. */
|
|
95
|
+
valueBranches: Record<string, any>[];
|
|
83
96
|
}
|
|
84
97
|
/** Every use a slot can take, flattening a case map. The conservative reading
|
|
85
98
|
* for a consumer that does not resolve the selector. */
|
package/dist/ref-slot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ref-slot.d.ts","sourceRoot":"","sources":["../src/ref-slot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;mDAEmD;AACnD,MAAM,MAAM,MAAM;AAChB,sEAAsE;AACpE,QAAQ;AACV,oEAAoE;GAClE,YAAY;AACd,gEAAgE;GAC9D,MAAM;AACR;oCACoC;GAClC,UAAU;AACZ;8EAC8E;GAC5E,iBAAiB;AACnB;kDACkD;GAChD,kBAAkB,CAAC;AAEvB,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAOrC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED;qCACqC;AACrC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;0DAE0D;AAC1D,MAAM,WAAW,WAAW;IAC1B;;8DAE0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,OAAO;IACtB;;;yDAGqD;IACrD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;6DAIyD;IACzD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB;;0CAEsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6DACyD;IACzD,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ref-slot.d.ts","sourceRoot":"","sources":["../src/ref-slot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;mDAEmD;AACnD,MAAM,MAAM,MAAM;AAChB,sEAAsE;AACpE,QAAQ;AACV,oEAAoE;GAClE,YAAY;AACd,gEAAgE;GAC9D,MAAM;AACR;oCACoC;GAClC,UAAU;AACZ;8EAC8E;GAC5E,iBAAiB;AACnB;kDACkD;GAChD,kBAAkB,CAAC;AAEvB,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAOrC,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED;qCACqC;AACrC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;0DAE0D;AAC1D,MAAM,WAAW,WAAW;IAC1B;;8DAE0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,OAAO;IACtB;;;yDAGqD;IACrD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;6DAIyD;IACzD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB;;0CAEsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6DACyD;IACzD,MAAM,EAAE,OAAO,CAAC;IAChB;;;;;;;;;;;+BAW2B;IAC3B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;yDACyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAOpD;AAED;kDACkD;AAClD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAErD;AAiFD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CA8BtF;AAED;+DAC+D;AAC/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,CAExE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACxC,IAAI,CAiBN"}
|
package/dist/ref-slot.js
CHANGED
|
@@ -121,6 +121,31 @@ function annotationNodes(node) {
|
|
|
121
121
|
}
|
|
122
122
|
return out;
|
|
123
123
|
}
|
|
124
|
+
/** The branches of a value-or-reference union — see {@link RefSlot.valueBranches}.
|
|
125
|
+
* Empty unless the node delegates its reference constraint to a branch. */
|
|
126
|
+
function valueBranchesOf(node) {
|
|
127
|
+
if (!node || typeof node !== "object")
|
|
128
|
+
return [];
|
|
129
|
+
if (node["x-telo-ref"] !== undefined)
|
|
130
|
+
return [];
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
133
|
+
const branches = node[key];
|
|
134
|
+
if (!Array.isArray(branches))
|
|
135
|
+
continue;
|
|
136
|
+
const carriesRef = branches.some((b) => b && typeof b === "object" && b["x-telo-ref"] !== undefined);
|
|
137
|
+
if (!carriesRef)
|
|
138
|
+
continue;
|
|
139
|
+
for (const branch of branches) {
|
|
140
|
+
if (!branch || typeof branch !== "object")
|
|
141
|
+
continue;
|
|
142
|
+
if (branch["x-telo-ref"] !== undefined)
|
|
143
|
+
continue;
|
|
144
|
+
out.push(branch);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
124
149
|
/**
|
|
125
150
|
* Reads a schema node as a reference slot, or `undefined` when it declares none.
|
|
126
151
|
*
|
|
@@ -160,6 +185,7 @@ export function readRefSlot(node) {
|
|
|
160
185
|
kinds,
|
|
161
186
|
uses: [...uses],
|
|
162
187
|
inline: node?.["x-telo-inline"] === true || nodes.some((n) => n["x-telo-inline"] === true),
|
|
188
|
+
valueBranches: valueBranchesOf(node),
|
|
163
189
|
};
|
|
164
190
|
if (useCases)
|
|
165
191
|
slot.useCases = useCases;
|
|
@@ -17,6 +17,10 @@ export interface RefFieldEntry {
|
|
|
17
17
|
inputs?: string;
|
|
18
18
|
/** True when the field path traversed through at least one array (path contains "[]"). */
|
|
19
19
|
isArray: boolean;
|
|
20
|
+
/** The slot's non-reference branches, when the reference constraint is a
|
|
21
|
+
* branch of a union — see {@link RefSlot.valueBranches}. A value satisfying
|
|
22
|
+
* one of these is a value, not a malformed reference. */
|
|
23
|
+
valueBranches?: Record<string, any>[];
|
|
20
24
|
/** x-telo-context schema declared on this ref slot, if any. Describes the CEL invocation
|
|
21
25
|
* context available to resources placed in this slot. */
|
|
22
26
|
context?: Record<string, any>;
|
|
@@ -52,6 +56,30 @@ export type FieldMapEntry = RefFieldEntry | ScopeFieldEntry | SchemaFromFieldEnt
|
|
|
52
56
|
* Paths use dot notation; array traversal is denoted by `[]` (e.g. "steps[].invoke"). */
|
|
53
57
|
export type ReferenceFieldMap = Map<string, FieldMapEntry>;
|
|
54
58
|
export declare function isRefEntry(entry: FieldMapEntry): entry is RefFieldEntry;
|
|
59
|
+
/** The half of a definition registry this question needs — structural, so the
|
|
60
|
+
* field map keeps depending on nothing. */
|
|
61
|
+
export interface ValueBranchValidator {
|
|
62
|
+
schemaCompileError(schema: Record<string, any>): string | undefined;
|
|
63
|
+
validateWithRefs(data: unknown, schema: Record<string, any>): string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* True when a value at a ref slot satisfies one of the slot's VALUE branches —
|
|
67
|
+
* a storage class beside a `!ref`, so it is a value and not a malformed
|
|
68
|
+
* reference.
|
|
69
|
+
*
|
|
70
|
+
* One implementation, because BOTH reference passes have to narrow the same way:
|
|
71
|
+
* `validateReferenceForms` would otherwise call it a removed string reference,
|
|
72
|
+
* and `validateReferences` a reference missing `kind` and `name`. Two copies of
|
|
73
|
+
* the rule would eventually disagree about which of the two reported a value.
|
|
74
|
+
*
|
|
75
|
+
* A branch AJV cannot COMPILE is not a branch the value satisfies.
|
|
76
|
+
* `validateWithRefs` returns no issues for one — it swallows the compile failure
|
|
77
|
+
* by design, so one bad schema does not abort the pass — and reading that as
|
|
78
|
+
* "no issues, therefore a value" would switch the reference-form rule off for
|
|
79
|
+
* the slot silently. The uncompilable schema is reported on its own definition
|
|
80
|
+
* by `schemaCompileError`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function satisfiesValueBranch(value: unknown, branches: readonly Record<string, any>[] | undefined, registry: ValueBranchValidator): boolean;
|
|
55
83
|
export declare function isScopeEntry(entry: FieldMapEntry): entry is ScopeFieldEntry;
|
|
56
84
|
export declare function isSchemaFromEntry(entry: FieldMapEntry): entry is SchemaFromFieldEntry;
|
|
57
85
|
/** Keys that a named reference object may have. Values beyond these indicate an inline resource. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,WAAW,EAAe,MAAM,eAAe,CAAC;AAEzF,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACvE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAElE,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;;6EAEyE;IACzE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;0DACsD;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB;0CACsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B;;;;;;;;;;kCAU8B;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;sEAIsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;wDAUwD;AACxD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAoCpF;AAED;+DAC+D;AAC/D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAExE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF;AAED;;6CAE6C;AAC7C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CAE/D;AAED;;;8CAG8C;AAC9C,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAInB"}
|
|
1
|
+
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,29 @@ export { readRefSlot, isRefSlot, hasDeclaredUse } from "./ref-slot.js";
|
|
|
3
3
|
export function isRefEntry(entry) {
|
|
4
4
|
return "refs" in entry;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* True when a value at a ref slot satisfies one of the slot's VALUE branches —
|
|
8
|
+
* a storage class beside a `!ref`, so it is a value and not a malformed
|
|
9
|
+
* reference.
|
|
10
|
+
*
|
|
11
|
+
* One implementation, because BOTH reference passes have to narrow the same way:
|
|
12
|
+
* `validateReferenceForms` would otherwise call it a removed string reference,
|
|
13
|
+
* and `validateReferences` a reference missing `kind` and `name`. Two copies of
|
|
14
|
+
* the rule would eventually disagree about which of the two reported a value.
|
|
15
|
+
*
|
|
16
|
+
* A branch AJV cannot COMPILE is not a branch the value satisfies.
|
|
17
|
+
* `validateWithRefs` returns no issues for one — it swallows the compile failure
|
|
18
|
+
* by design, so one bad schema does not abort the pass — and reading that as
|
|
19
|
+
* "no issues, therefore a value" would switch the reference-form rule off for
|
|
20
|
+
* the slot silently. The uncompilable schema is reported on its own definition
|
|
21
|
+
* by `schemaCompileError`.
|
|
22
|
+
*/
|
|
23
|
+
export function satisfiesValueBranch(value, branches, registry) {
|
|
24
|
+
if (!branches?.length)
|
|
25
|
+
return false;
|
|
26
|
+
return branches.some((branch) => registry.schemaCompileError(branch) === undefined &&
|
|
27
|
+
registry.validateWithRefs(value, branch).length === 0);
|
|
28
|
+
}
|
|
6
29
|
export function isScopeEntry(entry) {
|
|
7
30
|
return "scope" in entry;
|
|
8
31
|
}
|
|
@@ -155,6 +178,8 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
|
|
|
155
178
|
entry.useCases = slot.useCases;
|
|
156
179
|
if (slot.inputs !== undefined)
|
|
157
180
|
entry.inputs = slot.inputs;
|
|
181
|
+
if (slot.valueBranches.length > 0)
|
|
182
|
+
entry.valueBranches = slot.valueBranches;
|
|
158
183
|
if (node["x-telo-context"])
|
|
159
184
|
entry.context = node["x-telo-context"];
|
|
160
185
|
if (slot.inline)
|
package/dist/referrer-rule.d.ts
CHANGED
|
@@ -35,6 +35,23 @@ export interface ReferrerRule {
|
|
|
35
35
|
* relation the rule is about, so a kind should write it.
|
|
36
36
|
*/
|
|
37
37
|
readonly referrer?: string;
|
|
38
|
+
/**
|
|
39
|
+
* JSON Pointer to a collection OF THE REFERRER to resolve — the binding that
|
|
40
|
+
* lets a rule state a relation between SIBLING declarations, which neither a
|
|
41
|
+
* resource rule (one resource) nor a plain referrer rule (a pair joined by one
|
|
42
|
+
* reference) can reach.
|
|
43
|
+
*
|
|
44
|
+
* Declaring it binds `peers` (the collection's OTHER entries) and `entry` (my
|
|
45
|
+
* own), and makes the rule evaluate once per entry rather than once per
|
|
46
|
+
* referrer: a rule reading `entry` is about the entry, and a resource listed
|
|
47
|
+
* twice has two entries to answer for.
|
|
48
|
+
*
|
|
49
|
+
* Entries bind AS WRITTEN with the references *inside them* resolved one level
|
|
50
|
+
* — `p` is the declaration where the entry is a bare `!ref`, `p.mount` is the
|
|
51
|
+
* declaration with `p.prefix` beside it where it is not. Nothing is guessed
|
|
52
|
+
* from the item schema and there is no second pointer to write.
|
|
53
|
+
*/
|
|
54
|
+
readonly peers?: string;
|
|
38
55
|
/** CEL source. TRUE when the rule holds. */
|
|
39
56
|
readonly condition: string;
|
|
40
57
|
/** The rule's own name, carried in `data.rule`. Never a diagnostic code —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"referrer-rule.d.ts","sourceRoot":"","sources":["../src/referrer-rule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAe,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;qEACiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAMD;;6CAE6C;AAC7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAG7D;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"referrer-rule.d.ts","sourceRoot":"","sources":["../src/referrer-rule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAe,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;qEACiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAMD;;6CAE6C;AAC7C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAG7D;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,EAAE,CA0BjE;AAED;0EAC0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAEvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC5C,IAAI,CAQN"}
|
package/dist/referrer-rule.js
CHANGED
|
@@ -47,18 +47,21 @@ export function readReferrerRules(schema) {
|
|
|
47
47
|
if (!isObject(entry))
|
|
48
48
|
return;
|
|
49
49
|
const condition = celSourceOf(entry.condition);
|
|
50
|
-
const { code, message, referrer } = entry;
|
|
50
|
+
const { code, message, referrer, peers } = entry;
|
|
51
51
|
if (!condition || typeof code !== "string" || typeof message !== "string")
|
|
52
52
|
return;
|
|
53
53
|
if (code.length === 0 || message.length === 0)
|
|
54
54
|
return;
|
|
55
55
|
if (referrer !== undefined && typeof referrer !== "string")
|
|
56
56
|
return;
|
|
57
|
+
if (peers !== undefined && typeof peers !== "string")
|
|
58
|
+
return;
|
|
57
59
|
if (entry.severity !== undefined && entry.severity !== "warning" && entry.severity !== "error") {
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
rules.push({
|
|
61
63
|
...(referrer === undefined ? {} : { referrer }),
|
|
64
|
+
...(peers === undefined ? {} : { peers }),
|
|
62
65
|
condition,
|
|
63
66
|
code,
|
|
64
67
|
message,
|
package/dist/release/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The release model: module identity, fragments, the ledger, the edge graph,
|
|
3
3
|
* level propagation and version planning.
|
|
4
4
|
*
|
|
5
|
-
* Browser-safe by construction — pure data in, plan out — so
|
|
5
|
+
* Browser-safe by construction — pure data in, plan out — so telo studio can
|
|
6
6
|
* answer "what does changing this library bump?" from the same model the CLI
|
|
7
7
|
* releases from. Everything Node-shaped (finding the workspace, running the
|
|
8
8
|
* controller builder, building payloads, reading git, writing files) is
|
package/dist/release/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The release model: module identity, fragments, the ledger, the edge graph,
|
|
3
3
|
* level propagation and version planning.
|
|
4
4
|
*
|
|
5
|
-
* Browser-safe by construction — pure data in, plan out — so
|
|
5
|
+
* Browser-safe by construction — pure data in, plan out — so telo studio can
|
|
6
6
|
* answer "what does changing this library bump?" from the same model the CLI
|
|
7
7
|
* releases from. Everything Node-shaped (finding the workspace, running the
|
|
8
8
|
* controller builder, building payloads, reading git, writing files) is
|