@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.
Files changed (114) hide show
  1. package/dist/analyzer.d.ts +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +175 -65
  4. package/dist/call-graph.d.ts.map +1 -1
  5. package/dist/call-graph.js +7 -1
  6. package/dist/cel-environment.d.ts +12 -0
  7. package/dist/cel-environment.d.ts.map +1 -1
  8. package/dist/cel-environment.js +35 -19
  9. package/dist/cel-scope.d.ts.map +1 -1
  10. package/dist/cel-scope.js +24 -4
  11. package/dist/definition-registry.d.ts +47 -6
  12. package/dist/definition-registry.d.ts.map +1 -1
  13. package/dist/definition-registry.js +82 -13
  14. package/dist/eval-paths.d.ts +68 -0
  15. package/dist/eval-paths.d.ts.map +1 -1
  16. package/dist/eval-paths.js +138 -0
  17. package/dist/flatten-for-analyzer.d.ts +1 -1
  18. package/dist/flatten-for-analyzer.js +1 -1
  19. package/dist/import-resolution-diagnostics.d.ts +1 -1
  20. package/dist/import-resolution-diagnostics.js +1 -1
  21. package/dist/index.d.ts +13 -9
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -6
  24. package/dist/manifest-visitor.d.ts.map +1 -1
  25. package/dist/manifest-visitor.js +8 -1
  26. package/dist/migrations/entries/index.d.ts.map +1 -1
  27. package/dist/migrations/entries/index.js +2 -0
  28. package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
  29. package/dist/peer-binding.d.ts +232 -0
  30. package/dist/peer-binding.d.ts.map +1 -0
  31. package/dist/peer-binding.js +418 -0
  32. package/dist/ref-slot.d.ts +13 -0
  33. package/dist/ref-slot.d.ts.map +1 -1
  34. package/dist/ref-slot.js +26 -0
  35. package/dist/reference-field-map.d.ts +28 -0
  36. package/dist/reference-field-map.d.ts.map +1 -1
  37. package/dist/reference-field-map.js +25 -0
  38. package/dist/referrer-rule.d.ts +17 -0
  39. package/dist/referrer-rule.d.ts.map +1 -1
  40. package/dist/referrer-rule.js +4 -1
  41. package/dist/release/index.d.ts +1 -1
  42. package/dist/release/index.js +1 -1
  43. package/dist/resource-rule.d.ts +40 -27
  44. package/dist/resource-rule.d.ts.map +1 -1
  45. package/dist/resource-rule.js +61 -7
  46. package/dist/rule-condition.d.ts +11 -0
  47. package/dist/rule-condition.d.ts.map +1 -1
  48. package/dist/rule-condition.js +13 -0
  49. package/dist/schema-compat.d.ts +59 -22
  50. package/dist/schema-compat.d.ts.map +1 -1
  51. package/dist/schema-compat.js +60 -75
  52. package/dist/schema-error-report.d.ts +68 -0
  53. package/dist/schema-error-report.d.ts.map +1 -0
  54. package/dist/schema-error-report.js +356 -0
  55. package/dist/schema-projection.d.ts +106 -4
  56. package/dist/schema-projection.d.ts.map +1 -1
  57. package/dist/schema-projection.js +206 -18
  58. package/dist/telo-version.d.ts +1 -1
  59. package/dist/telo-version.js +1 -1
  60. package/dist/validate-cel-context.d.ts +1 -17
  61. package/dist/validate-cel-context.d.ts.map +1 -1
  62. package/dist/validate-cel-context.js +5 -70
  63. package/dist/validate-nested-inline.d.ts +22 -1
  64. package/dist/validate-nested-inline.d.ts.map +1 -1
  65. package/dist/validate-nested-inline.js +17 -9
  66. package/dist/validate-reference-forms.d.ts +10 -0
  67. package/dist/validate-reference-forms.d.ts.map +1 -1
  68. package/dist/validate-reference-forms.js +23 -0
  69. package/dist/validate-references.d.ts.map +1 -1
  70. package/dist/validate-references.js +9 -2
  71. package/dist/validate-referrer-rules.d.ts +38 -4
  72. package/dist/validate-referrer-rules.d.ts.map +1 -1
  73. package/dist/validate-referrer-rules.js +139 -31
  74. package/dist/validate-resource-rules.d.ts +11 -2
  75. package/dist/validate-resource-rules.d.ts.map +1 -1
  76. package/dist/validate-resource-rules.js +18 -12
  77. package/dist/validate-schema-projection.d.ts.map +1 -1
  78. package/dist/validate-schema-projection.js +33 -2
  79. package/dist/validate-step-inputs.js +11 -3
  80. package/dist/with-synthetic-positions.d.ts +1 -1
  81. package/dist/with-synthetic-positions.js +1 -1
  82. package/package.json +3 -3
  83. package/src/analyzer.ts +211 -68
  84. package/src/call-graph.ts +7 -1
  85. package/src/cel-environment.ts +48 -19
  86. package/src/cel-scope.ts +24 -6
  87. package/src/definition-registry.ts +90 -11
  88. package/src/eval-paths.ts +152 -0
  89. package/src/flatten-for-analyzer.ts +1 -1
  90. package/src/import-resolution-diagnostics.ts +1 -1
  91. package/src/index.ts +55 -6
  92. package/src/manifest-visitor.ts +8 -1
  93. package/src/migrations/entries/index.ts +2 -0
  94. package/src/migrations/entries/schema-prepare-bucket.json +27 -0
  95. package/src/peer-binding.ts +641 -0
  96. package/src/ref-slot.ts +36 -0
  97. package/src/reference-field-map.ts +42 -0
  98. package/src/referrer-rule.ts +20 -1
  99. package/src/release/index.ts +1 -1
  100. package/src/resource-rule.ts +69 -7
  101. package/src/rule-condition.ts +15 -0
  102. package/src/schema-compat.ts +92 -79
  103. package/src/schema-error-report.ts +417 -0
  104. package/src/schema-projection.ts +283 -20
  105. package/src/telo-version.ts +1 -1
  106. package/src/validate-cel-context.ts +6 -71
  107. package/src/validate-nested-inline.ts +35 -14
  108. package/src/validate-reference-forms.ts +22 -0
  109. package/src/validate-references.ts +15 -2
  110. package/src/validate-referrer-rules.ts +215 -28
  111. package/src/validate-resource-rules.ts +26 -12
  112. package/src/validate-schema-projection.ts +50 -1
  113. package/src/validate-step-inputs.ts +10 -4
  114. 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
+ }
@@ -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. */
@@ -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;CACjB;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;AA2DD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CA6BtF;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"}
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)
@@ -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,CAwBjE;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"}
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"}
@@ -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,
@@ -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 the telo editor can
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
@@ -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 the telo editor can
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