@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
package/src/schema-projection.ts
CHANGED
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
* derivation can reach.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
39
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
40
|
+
import { isInjectedDeclaration } from "./resource-input.js";
|
|
41
|
+
|
|
38
42
|
/** How a kind's entry collection projects to an object schema. */
|
|
39
43
|
export interface SchemaProjection {
|
|
40
44
|
/** JSON Pointer, from the resource root, to the entries. */
|
|
@@ -48,6 +52,33 @@ export interface SchemaProjection {
|
|
|
48
52
|
readonly nullable?: string;
|
|
49
53
|
/** Entry field that wraps the mapped node in an array. */
|
|
50
54
|
readonly array?: string;
|
|
55
|
+
/**
|
|
56
|
+
* How an entry whose keyed field holds a REFERENCE projects.
|
|
57
|
+
*
|
|
58
|
+
* The map is keyed on the field's VALUE, and a reference is not a key, so a
|
|
59
|
+
* `type:` holding one falls through to this path. It is declared as data by the
|
|
60
|
+
* backend, which is what keeps the analyzer from learning that an enum exists:
|
|
61
|
+
* `from` names the field of the target declaration to read, `keyword` the
|
|
62
|
+
* schema keyword its values become, and `base` / `baseFrom` where the node's
|
|
63
|
+
* own type comes from — a literal for an engine whose named type IS its own
|
|
64
|
+
* base, a field of the target for one that declares a storage class.
|
|
65
|
+
*
|
|
66
|
+
* A backend that declares none projects exactly as it did before.
|
|
67
|
+
*/
|
|
68
|
+
readonly reference?: ProjectionReference;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The reference path of a projection — see {@link SchemaProjection.reference}. */
|
|
72
|
+
export interface ProjectionReference {
|
|
73
|
+
/** Field of the TARGET declaration whose value the keyword takes. */
|
|
74
|
+
readonly from: string;
|
|
75
|
+
/** The JSON Schema keyword those values become (`enum`). */
|
|
76
|
+
readonly keyword: string;
|
|
77
|
+
/** The node the keyword is added to, written literally. */
|
|
78
|
+
readonly base?: Record<string, unknown>;
|
|
79
|
+
/** Field of the target declaration naming a value in the kind's own
|
|
80
|
+
* `x-telo-schema-map`, whose mapped node is the base. */
|
|
81
|
+
readonly baseFrom?: string;
|
|
51
82
|
}
|
|
52
83
|
|
|
53
84
|
export type SchemaMap = Readonly<Record<string, Record<string, unknown>>>;
|
|
@@ -71,6 +102,19 @@ export function readSchemaProjection(definition: unknown): SchemaProjection | un
|
|
|
71
102
|
nameField: typeof raw.name === "string" ? raw.name : undefined,
|
|
72
103
|
nullable: typeof raw.nullable === "string" ? raw.nullable : undefined,
|
|
73
104
|
array: typeof raw.array === "string" ? raw.array : undefined,
|
|
105
|
+
reference: readProjectionReference(raw.reference),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readProjectionReference(raw: unknown): ProjectionReference | undefined {
|
|
110
|
+
if (!isObject(raw)) return undefined;
|
|
111
|
+
const { from, keyword, base, baseFrom } = raw;
|
|
112
|
+
if (typeof from !== "string" || typeof keyword !== "string") return undefined;
|
|
113
|
+
return {
|
|
114
|
+
from,
|
|
115
|
+
keyword,
|
|
116
|
+
base: isObject(base) ? (base as Record<string, unknown>) : undefined,
|
|
117
|
+
baseFrom: typeof baseFrom === "string" ? baseFrom : undefined,
|
|
74
118
|
};
|
|
75
119
|
}
|
|
76
120
|
|
|
@@ -94,7 +138,35 @@ export function schemaProjectionIsMisplaced(definition: unknown): boolean {
|
|
|
94
138
|
return isObject(schema) && schema["x-telo-schema-projection"] !== undefined;
|
|
95
139
|
}
|
|
96
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The schema node that CARRIES the value vocabulary — the node itself, or the
|
|
143
|
+
* branch of a union that declares the map.
|
|
144
|
+
*
|
|
145
|
+
* A slot unioning a closed value vocabulary with a reference keeps its map on the
|
|
146
|
+
* value branch, exactly as the ref-slot reader peels the same union for its
|
|
147
|
+
* constraint. Exported because the strict half checks the map against the same
|
|
148
|
+
* branch's `enum`, and two implementations of "which branch is the value one"
|
|
149
|
+
* would eventually disagree — silently, since the failure of missing one is a
|
|
150
|
+
* completeness check that quietly stops running.
|
|
151
|
+
*/
|
|
152
|
+
export function schemaMapBranch(node: unknown): Record<string, unknown> | undefined {
|
|
153
|
+
if (!isObject(node)) return undefined;
|
|
154
|
+
if (node["x-telo-schema-map"] !== undefined) return node;
|
|
155
|
+
for (const key of ["oneOf", "anyOf"] as const) {
|
|
156
|
+
const branches = node[key];
|
|
157
|
+
if (!Array.isArray(branches)) continue;
|
|
158
|
+
for (const branch of branches) {
|
|
159
|
+
if (isObject(branch) && branch["x-telo-schema-map"] !== undefined) return branch;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
97
165
|
export function readSchemaMap(node: unknown): SchemaMap | undefined {
|
|
166
|
+
return ownSchemaMap(schemaMapBranch(node));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ownSchemaMap(node: unknown): SchemaMap | undefined {
|
|
98
170
|
if (!isObject(node)) return undefined;
|
|
99
171
|
const raw = node["x-telo-schema-map"];
|
|
100
172
|
if (!isObject(raw)) return undefined;
|
|
@@ -146,6 +218,76 @@ export function projectionKeyMap(
|
|
|
146
218
|
return readSchemaMap(entry.properties[projection.key]);
|
|
147
219
|
}
|
|
148
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The node an entry whose keyed field holds a REFERENCE projects to.
|
|
223
|
+
*
|
|
224
|
+
* This is the one place a projection crosses to another declaration, and it is
|
|
225
|
+
* a deliberate exception to the projection's lossiness: length, precision and
|
|
226
|
+
* collation stop at the boundary because the database enforces them, while a
|
|
227
|
+
* domain crosses because it IS the type at the granularity a consumer acts on —
|
|
228
|
+
* the enum in a CRUD model's OpenAPI operation, a completion list in the editor,
|
|
229
|
+
* a filter a repository can reject before the query.
|
|
230
|
+
*
|
|
231
|
+
* **A reference that cannot be read projects OPEN, never to nothing**, and that
|
|
232
|
+
* is the opposite of the rule an unmapped VALUE follows. The two failures are
|
|
233
|
+
* not the same failure: an unmapped value is a gap in the kind's own vocabulary,
|
|
234
|
+
* so there is no entry to speak of, while an unreadable reference names an entry
|
|
235
|
+
* the declaration plainly HAS and only leaves its type unknown. Dropping it made
|
|
236
|
+
* the projection deny the entry exists — a table whose enum reference had a typo
|
|
237
|
+
* reported `'status' is not allowed` against a column declared three lines up,
|
|
238
|
+
* blaming the seed row for the reference's mistake. Open is the honest
|
|
239
|
+
* under-approximation, and the reason is reported alongside.
|
|
240
|
+
*/
|
|
241
|
+
function referencedNode(
|
|
242
|
+
value: unknown,
|
|
243
|
+
entryName: string,
|
|
244
|
+
projection: SchemaProjection,
|
|
245
|
+
map: SchemaMap,
|
|
246
|
+
options?: {
|
|
247
|
+
readonly scope?: ProjectionScope;
|
|
248
|
+
readonly pointer?: string;
|
|
249
|
+
readonly failures?: ProjectionFailure[];
|
|
250
|
+
},
|
|
251
|
+
): Record<string, unknown> | undefined {
|
|
252
|
+
const reference = projection.reference;
|
|
253
|
+
if (!reference || !isObject(value)) return undefined;
|
|
254
|
+
// Through the single reader, so the name in the diagnostic is the one the
|
|
255
|
+
// author wrote whichever shape the slot holds — reading `value.name` here
|
|
256
|
+
// reported `<unnamed>` for an unresolved `!ref`, which is precisely the case
|
|
257
|
+
// that produces the diagnostic.
|
|
258
|
+
const name = readProjectionRef(value)?.name ?? "<unnamed>";
|
|
259
|
+
const report = (): Record<string, unknown> => {
|
|
260
|
+
options?.failures?.push({
|
|
261
|
+
reason: "entry-reference",
|
|
262
|
+
// The EMPTY pointer means the projected declaration is the one carrying
|
|
263
|
+
// the diagnostic, so the entry's own path is a real anchor in that file —
|
|
264
|
+
// the column, not the document root. Any other pointer names a slot
|
|
265
|
+
// holding a reference to a DIFFERENT manifest, whose entry paths mean
|
|
266
|
+
// nothing here, so the slot stays the anchor.
|
|
267
|
+
pointer:
|
|
268
|
+
options?.pointer === ""
|
|
269
|
+
? `${projection.entries}/${entryName}`
|
|
270
|
+
: (options?.pointer ?? projection.entries),
|
|
271
|
+
entry: entryName,
|
|
272
|
+
name,
|
|
273
|
+
});
|
|
274
|
+
return {};
|
|
275
|
+
};
|
|
276
|
+
const found = options?.scope?.resolveManifest(value);
|
|
277
|
+
if (!found || "ambiguous" in found) return report();
|
|
278
|
+
|
|
279
|
+
const values = (found.manifest as Record<string, unknown>)[reference.from];
|
|
280
|
+
if (!Array.isArray(values) || values.length === 0) return report();
|
|
281
|
+
|
|
282
|
+
let base: Record<string, unknown> | undefined = reference.base;
|
|
283
|
+
if (reference.baseFrom !== undefined) {
|
|
284
|
+
const declared = (found.manifest as Record<string, unknown>)[reference.baseFrom];
|
|
285
|
+
base = typeof declared === "string" ? map[declared] : undefined;
|
|
286
|
+
}
|
|
287
|
+
if (!base) return report();
|
|
288
|
+
return { ...base, [reference.keyword]: values };
|
|
289
|
+
}
|
|
290
|
+
|
|
149
291
|
/**
|
|
150
292
|
* Project one declaration to an object schema.
|
|
151
293
|
*
|
|
@@ -164,6 +306,15 @@ export function projectEntries(
|
|
|
164
306
|
manifest: unknown,
|
|
165
307
|
projection: SchemaProjection,
|
|
166
308
|
map: SchemaMap,
|
|
309
|
+
/** What a REFERENCE at the keyed field is resolved through, and where a
|
|
310
|
+
* failure to resolve one is reported. A caller with no scope cannot resolve
|
|
311
|
+
* one, so such an entry projects OPEN — present, untyped — rather than
|
|
312
|
+
* vanishing from the row. */
|
|
313
|
+
options?: {
|
|
314
|
+
readonly scope?: ProjectionScope;
|
|
315
|
+
readonly pointer?: string;
|
|
316
|
+
readonly failures?: ProjectionFailure[];
|
|
317
|
+
},
|
|
167
318
|
): Record<string, unknown> | undefined {
|
|
168
319
|
const entries = navigate(manifest, projection.entries);
|
|
169
320
|
if (entries === undefined) return undefined;
|
|
@@ -172,7 +323,10 @@ export function projectEntries(
|
|
|
172
323
|
const consider = (name: string | undefined, entry: unknown): void => {
|
|
173
324
|
if (!isObject(entry) || name === undefined) return;
|
|
174
325
|
const key = entry[projection.key];
|
|
175
|
-
const mapped =
|
|
326
|
+
const mapped =
|
|
327
|
+
typeof key === "string"
|
|
328
|
+
? map[key]
|
|
329
|
+
: referencedNode(key, name, projection, map, options);
|
|
176
330
|
// A value with no map entry projects to nothing rather than to `any`: the
|
|
177
331
|
// vocabulary is the kind's own enum, so an unmapped value is a gap in the
|
|
178
332
|
// kind's declaration, not a shape to guess at.
|
|
@@ -215,10 +369,25 @@ export interface ProjectionRef {
|
|
|
215
369
|
readonly alias?: string;
|
|
216
370
|
}
|
|
217
371
|
|
|
218
|
-
/**
|
|
219
|
-
*
|
|
372
|
+
/**
|
|
373
|
+
* The `{kind, name, alias?}` reference a value holds, or undefined. Exported so
|
|
374
|
+
* a host whose slot may hold EITHER shape can fall back to this reading.
|
|
375
|
+
*
|
|
376
|
+
* The unresolved `!ref` SENTINEL is read too. `resolveRefSentinels` normally
|
|
377
|
+
* rewrites one before this pass, but not when the reference names nothing — and
|
|
378
|
+
* that is exactly when a projection failure is reported, so reading only the
|
|
379
|
+
* resolved shape made the diagnostic name the target `<unnamed>`, which is the
|
|
380
|
+
* one fact the author needed from it. A round-trip host (`compile` off) carries
|
|
381
|
+
* the sentinel for every reference, resolved or not.
|
|
382
|
+
*/
|
|
220
383
|
export function readProjectionRef(value: unknown): ProjectionRef | undefined {
|
|
221
384
|
if (!isObject(value)) return undefined;
|
|
385
|
+
if (isRefSentinel(value)) {
|
|
386
|
+
const dot = value.source.indexOf(".");
|
|
387
|
+
return dot > 0
|
|
388
|
+
? { name: value.source.slice(dot + 1), alias: value.source.slice(0, dot) }
|
|
389
|
+
: { name: value.source };
|
|
390
|
+
}
|
|
222
391
|
const name = value.name;
|
|
223
392
|
if (typeof name !== "string") return undefined;
|
|
224
393
|
return {
|
|
@@ -306,11 +475,51 @@ export function manifestListScope(
|
|
|
306
475
|
};
|
|
307
476
|
}
|
|
308
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Why a slot could not be typed from a projection.
|
|
480
|
+
*
|
|
481
|
+
* Each reason is a DIFFERENT repair, which is why the three ways a target can
|
|
482
|
+
* carry no usable projection are kept apart rather than collapsed into
|
|
483
|
+
* `no-projection`: that one message ("declares no 'x-telo-schema-projection'")
|
|
484
|
+
* was printed for a kind that declares one whose key field carries no map, and
|
|
485
|
+
* for a declaration whose entry collection is simply absent — accusing the wrong
|
|
486
|
+
* author of the wrong omission in both.
|
|
487
|
+
*/
|
|
309
488
|
export type ProjectionFailure =
|
|
310
489
|
| { readonly reason: "no-ref"; readonly pointer: string }
|
|
311
490
|
| { readonly reason: "unresolved"; readonly pointer: string; readonly name: string }
|
|
312
491
|
| { readonly reason: "ambiguous"; readonly pointer: string; readonly name: string }
|
|
313
|
-
|
|
492
|
+
/** The target's KIND declares no `x-telo-schema-projection` at all. */
|
|
493
|
+
| { readonly reason: "no-projection"; readonly pointer: string; readonly kind: string }
|
|
494
|
+
/** It declares one, but the field it keys on carries no `x-telo-schema-map`. */
|
|
495
|
+
| { readonly reason: "no-projection-map"; readonly pointer: string; readonly kind: string }
|
|
496
|
+
/** Both are declared and the DECLARATION holds no entry collection to project
|
|
497
|
+
* — an absent `columns:`, or a value that is not a collection. */
|
|
498
|
+
| {
|
|
499
|
+
readonly reason: "no-entries";
|
|
500
|
+
readonly pointer: string;
|
|
501
|
+
readonly kind: string;
|
|
502
|
+
readonly entries: string;
|
|
503
|
+
}
|
|
504
|
+
/** The slot names a resource the module does not DECLARE — a library's
|
|
505
|
+
* `resources:` input, standing in for an instance its importer supplies. A
|
|
506
|
+
* projection is DECLARATION-derived, so it cannot be answered here at all;
|
|
507
|
+
* the stand-in has no entries, and reporting that would tell the library
|
|
508
|
+
* author their block is wrong when it is correct. Resolution moves to the
|
|
509
|
+
* injection site, where the real declaration is. Carried as its own reason
|
|
510
|
+
* rather than as `no-entries` so a consumer can tell "unanswerable here"
|
|
511
|
+
* from "answered, and empty". */
|
|
512
|
+
| { readonly reason: "injected"; readonly pointer: string; readonly name: string }
|
|
513
|
+
/** An ENTRY of the projected declaration references a shape that could not be
|
|
514
|
+
* read. Reported rather than dropped: the entry would silently vanish from
|
|
515
|
+
* the projected row, so a consumer naming it would be told the property does
|
|
516
|
+
* not exist. */
|
|
517
|
+
| {
|
|
518
|
+
readonly reason: "entry-reference";
|
|
519
|
+
readonly pointer: string;
|
|
520
|
+
readonly entry: string;
|
|
521
|
+
readonly name: string;
|
|
522
|
+
};
|
|
314
523
|
|
|
315
524
|
function refTarget(
|
|
316
525
|
value: unknown,
|
|
@@ -325,18 +534,41 @@ function refTarget(
|
|
|
325
534
|
if (!found) return { reason: "unresolved", pointer, name };
|
|
326
535
|
if ("ambiguous" in found) return { reason: "ambiguous", pointer, name };
|
|
327
536
|
const manifest = found.manifest;
|
|
537
|
+
if (isInjectedDeclaration(manifest as ResourceManifest)) {
|
|
538
|
+
return { reason: "injected", pointer, name };
|
|
539
|
+
}
|
|
328
540
|
if (typeof manifest.kind !== "string") return { reason: "unresolved", pointer, name };
|
|
329
541
|
const definition = scope.resolveDefinition(manifest.kind);
|
|
330
542
|
if (!definition) return { reason: "no-projection", pointer, kind: manifest.kind };
|
|
331
543
|
return { manifest, definition };
|
|
332
544
|
}
|
|
333
545
|
|
|
546
|
+
/** The declaration the annotation is written on, as a projection target. */
|
|
547
|
+
function ownTarget(
|
|
548
|
+
manifest: Record<string, any>,
|
|
549
|
+
scope: ProjectionScope,
|
|
550
|
+
): { manifest: Record<string, any>; definition: Record<string, any> } | ProjectionFailure {
|
|
551
|
+
if (typeof manifest.kind !== "string") {
|
|
552
|
+
return { reason: "no-ref", pointer: "" };
|
|
553
|
+
}
|
|
554
|
+
const definition = scope.resolveDefinition(manifest.kind);
|
|
555
|
+
if (!definition) return { reason: "no-projection", pointer: "", kind: manifest.kind };
|
|
556
|
+
return { manifest, definition };
|
|
557
|
+
}
|
|
558
|
+
|
|
334
559
|
export function describeProjectionFailure(failure: ProjectionFailure): string {
|
|
335
560
|
switch (failure.reason) {
|
|
336
561
|
case "no-ref":
|
|
337
|
-
return
|
|
562
|
+
return failure.pointer === ""
|
|
563
|
+
? "this resource declares no 'kind:', so there is no definition to project it through."
|
|
564
|
+
: `'${failure.pointer}' does not hold a reference, so there is no declaration to project.`;
|
|
338
565
|
case "unresolved":
|
|
339
566
|
return `'${failure.pointer}' references '${failure.name}', which resolves to no resource.`;
|
|
567
|
+
case "injected":
|
|
568
|
+
return (
|
|
569
|
+
`'${failure.pointer}' references '${failure.name}', a resource input this module does ` +
|
|
570
|
+
`not declare — its entries belong to whoever supplies it.`
|
|
571
|
+
);
|
|
340
572
|
case "ambiguous":
|
|
341
573
|
return (
|
|
342
574
|
`'${failure.pointer}' references '${failure.name}', which matches more than one resource ` +
|
|
@@ -347,6 +579,23 @@ export function describeProjectionFailure(failure: ProjectionFailure): string {
|
|
|
347
579
|
`'${failure.pointer}' references a resource of kind '${failure.kind}', which declares no ` +
|
|
348
580
|
`'x-telo-schema-projection' — so there is nothing for this slot to be typed from.`
|
|
349
581
|
);
|
|
582
|
+
case "no-projection-map":
|
|
583
|
+
return (
|
|
584
|
+
`kind '${failure.kind}' declares an 'x-telo-schema-projection' whose key field carries ` +
|
|
585
|
+
`no 'x-telo-schema-map', so there is no vocabulary to project its entries through and ` +
|
|
586
|
+
`'${failure.pointer || "this declaration"}' cannot be typed from it.`
|
|
587
|
+
);
|
|
588
|
+
case "no-entries":
|
|
589
|
+
return (
|
|
590
|
+
`'${failure.entries}' holds no entry collection on this ${failure.kind}, so the ` +
|
|
591
|
+
`projection has nothing to type '${failure.pointer || "this declaration"}' from.`
|
|
592
|
+
);
|
|
593
|
+
case "entry-reference":
|
|
594
|
+
return (
|
|
595
|
+
`entry '${failure.entry}' at '${failure.pointer}' references '${failure.name}', which ` +
|
|
596
|
+
`resolves to no declaration this analysis can read — so that entry is projected as an ` +
|
|
597
|
+
`open value and nothing typed from it is checked against the shape it was meant to have.`
|
|
598
|
+
);
|
|
350
599
|
}
|
|
351
600
|
}
|
|
352
601
|
|
|
@@ -360,6 +609,16 @@ export function describeProjectionFailure(failure: ProjectionFailure): string {
|
|
|
360
609
|
* `failures`, because degrading SILENTLY is the failure this whole mechanism
|
|
361
610
|
* exists to move earlier: the consumer's contract quietly reopens and a
|
|
362
611
|
* misspelled field passes `telo check` exactly as it did before.
|
|
612
|
+
*
|
|
613
|
+
* **A node that projected NOTHING is returned by IDENTITY**, and that is a
|
|
614
|
+
* correctness property of the caller rather than a micro-optimization:
|
|
615
|
+
* `DefinitionRegistry` memoizes a compiled AJV validator per schema OBJECT,
|
|
616
|
+
* because every resource of a kind is checked against the same one at keystroke
|
|
617
|
+
* time. Rebuilding each node unconditionally — which this did — misses that memo
|
|
618
|
+
* for every resource in the analysis, so AJV recompiled the whole kind schema
|
|
619
|
+
* once per resource: on `apps/hub` that was 197 compiles instead of 54, and 723
|
|
620
|
+
* ms instead of 97. Returning the input where nothing changed restores it for
|
|
621
|
+
* every kind that declares no projection at all, which is nearly all of them.
|
|
363
622
|
*/
|
|
364
623
|
export function resolveSchemaProjections(
|
|
365
624
|
schema: unknown,
|
|
@@ -368,36 +627,59 @@ export function resolveSchemaProjections(
|
|
|
368
627
|
failures?: ProjectionFailure[],
|
|
369
628
|
): unknown {
|
|
370
629
|
if (Array.isArray(schema)) {
|
|
371
|
-
|
|
630
|
+
let moved = false;
|
|
631
|
+
const items = schema.map((item) => {
|
|
632
|
+
const next = resolveSchemaProjections(item, manifest, scope, failures);
|
|
633
|
+
if (next !== item) moved = true;
|
|
634
|
+
return next;
|
|
635
|
+
});
|
|
636
|
+
return moved ? items : schema;
|
|
372
637
|
}
|
|
373
638
|
if (!isObject(schema)) return schema;
|
|
374
639
|
|
|
375
640
|
const pointer = readProjectionFrom(schema);
|
|
376
|
-
if (pointer && manifest) {
|
|
377
|
-
|
|
641
|
+
if (pointer !== undefined && manifest) {
|
|
642
|
+
// The EMPTY pointer names the declaration this annotation is written on,
|
|
643
|
+
// rather than a slot holding a reference to another. Resolution is skipped
|
|
644
|
+
// because the declaration is already in hand — which is what lets a kind
|
|
645
|
+
// type its own data against its own entries (a table's seed rows against its
|
|
646
|
+
// columns), something no reference could reach.
|
|
647
|
+
const target =
|
|
648
|
+
pointer === ""
|
|
649
|
+
? ownTarget(manifest, scope)
|
|
650
|
+
: refTarget(navigate(manifest, pointer), scope, pointer);
|
|
378
651
|
if ("reason" in target) {
|
|
379
652
|
failures?.push(target);
|
|
380
653
|
} else {
|
|
654
|
+
const kind = String(target.manifest.kind ?? "<unknown>");
|
|
381
655
|
const projection = readSchemaProjection(target.definition);
|
|
382
656
|
const map = projection && projectionKeyMap(target.definition.schema, projection);
|
|
383
657
|
const projected =
|
|
384
|
-
projection && map
|
|
658
|
+
projection && map
|
|
659
|
+
? projectEntries(target.manifest, projection, map, { scope, pointer, failures })
|
|
660
|
+
: undefined;
|
|
385
661
|
if (projected) {
|
|
386
662
|
const { ["x-telo-schema-projection-from"]: _dropped, ...rest } = schema;
|
|
387
663
|
return { ...rest, ...projected };
|
|
388
664
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
});
|
|
665
|
+
// Three distinct omissions, three repairs by three different authors: the
|
|
666
|
+
// kind declares no projection, the kind declares one the key field has no
|
|
667
|
+
// vocabulary for, or this DECLARATION simply lists no entries.
|
|
668
|
+
if (!projection) failures?.push({ reason: "no-projection", pointer, kind });
|
|
669
|
+
else if (!map) failures?.push({ reason: "no-projection-map", pointer, kind });
|
|
670
|
+
else {
|
|
671
|
+
failures?.push({ reason: "no-entries", pointer, kind, entries: projection.entries });
|
|
672
|
+
}
|
|
394
673
|
}
|
|
395
674
|
}
|
|
396
675
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
676
|
+
let moved = false;
|
|
677
|
+
const entries = Object.entries(schema).map(([key, value]) => {
|
|
678
|
+
const next = key.startsWith("x-telo-")
|
|
679
|
+
? value
|
|
680
|
+
: resolveSchemaProjections(value, manifest, scope, failures);
|
|
681
|
+
if (next !== value) moved = true;
|
|
682
|
+
return [key, next] as const;
|
|
683
|
+
});
|
|
684
|
+
return moved ? Object.fromEntries(entries) : schema;
|
|
403
685
|
}
|
package/src/system-kinds.ts
CHANGED
|
@@ -28,8 +28,13 @@ export const DEPENDENCY_GRAPH_SKIP_KINDS: ReadonlySet<string> = new Set([
|
|
|
28
28
|
/** Skipped by `!ref` sentinel resolution: kinds whose bodies are
|
|
29
29
|
* blueprints or import-time metadata, not resource instances with
|
|
30
30
|
* user-referenced ref slots. Mirrors `REF_VALIDATION_SKIP_KINDS` but
|
|
31
|
-
* also
|
|
32
|
-
*
|
|
31
|
+
* also lists Telo.Import, because walking its field map is pointless —
|
|
32
|
+
* there is no registered kind behind it.
|
|
33
|
+
*
|
|
34
|
+
* `resolveRefSentinels` reaches Telo.Import's `resources:` block AHEAD of this
|
|
35
|
+
* set, deliberately: those are the references an importer supplies for a
|
|
36
|
+
* library's declared inputs, and they resolve exactly like any other. Nothing
|
|
37
|
+
* else on the document is a reference slot, so only that subtree is walked. */
|
|
33
38
|
export const REF_RESOLUTION_SKIP_KINDS: ReadonlySet<string> = new Set([
|
|
34
39
|
"Telo.Definition",
|
|
35
40
|
"Telo.Abstract",
|
package/src/telo-version.ts
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
import type { ModuleScopes } from "./alias-resolver.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A `Telo.Definition`'s `resources:` entries — the bodies a kind writes for
|
|
8
|
+
* ANOTHER kind — read as what they are: declarations of that other kind.
|
|
9
|
+
*
|
|
10
|
+
* The consequence is the whole point. CEL inside such a body used to be typed in
|
|
11
|
+
* the ENCLOSING definition's scope, against one fixed permissive context
|
|
12
|
+
* (`self`, plus open `request` / `result` / `steps` / `error`). So `inputs` and
|
|
13
|
+
* `item` were undefined wherever the nested kind declares them, while `error`
|
|
14
|
+
* was offered everywhere regardless of whether a `catch:` was in scope — a body
|
|
15
|
+
* of more than one dispatch was unwritable, which is why no standard-library
|
|
16
|
+
* template has one.
|
|
17
|
+
*
|
|
18
|
+
* Resolving through the nested kind's OWN annotations — its `x-telo-context`
|
|
19
|
+
* regions, its step body, its error branches — makes a nested declaration answer
|
|
20
|
+
* exactly as the same declaration written at the top level does, with `self`
|
|
21
|
+
* merged in from the enclosing definition's `schema:`.
|
|
22
|
+
*
|
|
23
|
+
* Browser-safe.
|
|
24
|
+
*/
|
|
25
|
+
export interface TemplateBody {
|
|
26
|
+
/** Concrete path prefix of this entry (`resources[0]`), as CEL sites are
|
|
27
|
+
* addressed. */
|
|
28
|
+
prefix: string;
|
|
29
|
+
/** JSONPath prefix for context scopes (`$.resources[0]`). */
|
|
30
|
+
scopePrefix: string;
|
|
31
|
+
manifest: ResourceManifest;
|
|
32
|
+
definition: ResourceDefinition | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The template bodies a manifest declares — empty for anything that is not a
|
|
36
|
+
* `Telo.Definition` with a `resources:` array. */
|
|
37
|
+
export function templateBodies(
|
|
38
|
+
m: ResourceManifest,
|
|
39
|
+
registry: DefinitionRegistry,
|
|
40
|
+
aliases: AliasResolver | undefined,
|
|
41
|
+
scopes: ModuleScopes | undefined,
|
|
42
|
+
): TemplateBody[] {
|
|
43
|
+
if (m.kind !== "Telo.Definition") return [];
|
|
44
|
+
const bodies = (m as Record<string, unknown>).resources;
|
|
45
|
+
if (!Array.isArray(bodies)) return [];
|
|
46
|
+
|
|
47
|
+
// A nested kind is written through the alias scope of the module that DECLARED
|
|
48
|
+
// the definition, never the consumer's — the same rule every other kind
|
|
49
|
+
// resolution in a forwarded manifest follows.
|
|
50
|
+
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
51
|
+
const scope =
|
|
52
|
+
(ownModule && scopes && !scopes.rootModules.has(ownModule)
|
|
53
|
+
? scopes.aliasesByModule.get(ownModule)
|
|
54
|
+
: undefined) ?? aliases;
|
|
55
|
+
|
|
56
|
+
const out: TemplateBody[] = [];
|
|
57
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
58
|
+
const body = bodies[i];
|
|
59
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) continue;
|
|
60
|
+
const kind = (body as { kind?: unknown }).kind;
|
|
61
|
+
if (typeof kind !== "string") continue;
|
|
62
|
+
const canonical = scope?.resolveKind(kind);
|
|
63
|
+
const definition = registry.resolve(kind) ?? (canonical ? registry.resolve(canonical) : undefined);
|
|
64
|
+
out.push({
|
|
65
|
+
prefix: `resources[${i}]`,
|
|
66
|
+
scopePrefix: `$.resources[${i}]`,
|
|
67
|
+
manifest: body as ResourceManifest,
|
|
68
|
+
definition,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when a CEL path lies at or inside a body's own subtree. */
|
|
75
|
+
export function pathInBody(path: string, prefix: string): boolean {
|
|
76
|
+
return (
|
|
77
|
+
path === prefix || path.startsWith(`${prefix}.`) || path.startsWith(`${prefix}[`)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The body a CEL path belongs to, or undefined for a path in the definition's
|
|
82
|
+
* own fields. Generic over what the caller keyed to each body — the resolver
|
|
83
|
+
* carries a step context and an error map, the visitor carries the schema. */
|
|
84
|
+
export function bodyForPath<T extends { prefix: string }>(
|
|
85
|
+
bodies: readonly T[],
|
|
86
|
+
path: string,
|
|
87
|
+
): T | undefined {
|
|
88
|
+
return bodies.find((b) => pathInBody(path, b.prefix));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `self` is in scope throughout a template body — it is how the body reaches
|
|
92
|
+
* the configuration its enclosing kind was given — so it is merged into every
|
|
93
|
+
* context the nested kind declares, which knows nothing about it. Merged UNDER
|
|
94
|
+
* the nested kind's own properties: a name the nested kind declares wins, since
|
|
95
|
+
* that is what its controller will bind. */
|
|
96
|
+
export function withTemplateSelf(contextSchema: Record<string, any>): Record<string, any> {
|
|
97
|
+
return {
|
|
98
|
+
...contextSchema,
|
|
99
|
+
properties: {
|
|
100
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
101
|
+
...(contextSchema.properties ?? {}),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|