@telorun/analyzer 0.57.0 → 0.59.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.map +1 -1
- package/dist/analyzer.js +91 -9
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +6 -3
- package/dist/definition-registry.d.ts +5 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +9 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/invocation-contract.d.ts +58 -13
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +123 -46
- package/dist/loaded-types.d.ts +13 -0
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -6
- package/dist/migrations/driver.d.ts +69 -0
- package/dist/migrations/driver.d.ts.map +1 -0
- package/dist/migrations/driver.js +200 -0
- package/dist/migrations/entries/index.d.ts +3 -0
- package/dist/migrations/entries/index.d.ts.map +1 -0
- package/dist/migrations/entries/index.js +9 -0
- package/dist/migrations/entries/normalize-value-types.json +48 -0
- package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/dist/migrations/entry-data.d.ts +25 -0
- package/dist/migrations/entry-data.d.ts.map +1 -0
- package/dist/migrations/entry-data.js +171 -0
- package/dist/migrations/index.d.ts +22 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +19 -0
- package/dist/migrations/match.d.ts +162 -0
- package/dist/migrations/match.d.ts.map +1 -0
- package/dist/migrations/match.js +295 -0
- package/dist/migrations/patch.d.ts +68 -0
- package/dist/migrations/patch.d.ts.map +1 -0
- package/dist/migrations/patch.js +236 -0
- package/dist/migrations/provenance.d.ts +25 -0
- package/dist/migrations/provenance.d.ts.map +1 -0
- package/dist/migrations/provenance.js +118 -0
- package/dist/migrations/registry.d.ts +35 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +35 -0
- package/dist/migrations/report.d.ts +28 -0
- package/dist/migrations/report.d.ts.map +1 -0
- package/dist/migrations/report.js +144 -0
- package/dist/migrations/types.d.ts +115 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +24 -0
- package/dist/migrations/yaml-edit.d.ts +21 -0
- package/dist/migrations/yaml-edit.d.ts.map +1 -0
- package/dist/migrations/yaml-edit.js +245 -0
- package/dist/parse-loaded-file.d.ts +8 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +22 -13
- package/dist/resolve-schema-type-refs.d.ts +29 -12
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +89 -16
- package/dist/schema-compat.d.ts +53 -9
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +245 -66
- package/dist/schema-region.d.ts +33 -0
- package/dist/schema-region.d.ts.map +1 -0
- package/dist/schema-region.js +45 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +97 -15
- package/dist/validate-step-inputs.d.ts +8 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +47 -2
- package/dist/validate-value-type-slots.d.ts +43 -0
- package/dist/validate-value-type-slots.d.ts.map +1 -0
- package/dist/validate-value-type-slots.js +142 -0
- package/dist/value-type-keyword.d.ts +60 -0
- package/dist/value-type-keyword.d.ts.map +1 -0
- package/dist/value-type-keyword.js +119 -0
- package/dist/yaml-source-edit.d.ts +56 -0
- package/dist/yaml-source-edit.d.ts.map +1 -0
- package/dist/yaml-source-edit.js +93 -0
- package/package.json +3 -3
- package/src/analyzer.ts +99 -6
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +34 -3
- package/src/invocation-contract.ts +134 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/migrations/driver.ts +262 -0
- package/src/migrations/entries/index.ts +10 -0
- package/src/migrations/entries/normalize-value-types.json +48 -0
- package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/src/migrations/entry-data.ts +207 -0
- package/src/migrations/index.ts +22 -0
- package/src/migrations/match.ts +382 -0
- package/src/migrations/patch.ts +265 -0
- package/src/migrations/provenance.ts +167 -0
- package/src/migrations/registry.ts +40 -0
- package/src/migrations/report.ts +169 -0
- package/src/migrations/types.ts +144 -0
- package/src/migrations/yaml-edit.ts +260 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +277 -75
- package/src/schema-region.ts +46 -0
- package/src/types.ts +17 -0
- package/src/validate-cel-context.ts +117 -15
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-value-type-slots.ts +182 -0
- package/src/value-type-keyword.ts +135 -0
- package/src/yaml-source-edit.ts +106 -0
- package/dist/binary-slot.d.ts +0 -39
- package/dist/binary-slot.d.ts.map +0 -1
- package/dist/binary-slot.js +0 -58
- package/src/binary-slot.ts +0 -71
|
@@ -2,28 +2,51 @@ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
|
|
|
2
2
|
/** Schema-bearing fields on a Telo.Definition / Telo.Type resource. */
|
|
3
3
|
const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
|
|
4
4
|
/**
|
|
5
|
-
* Rewrites
|
|
6
|
-
* `telo://<authority>/<typeName>` names a `Type.JsonSchema` (or any `Telo.Type`)
|
|
7
|
-
* reached through an import: `telo://Self/<type>` for the declaring module's own
|
|
8
|
-
* type, `telo://<Alias>/<type>` for an imported module's. Each authority is
|
|
9
|
-
* resolved to the owning module's name and the ref is rewritten to the canonical
|
|
10
|
-
* `telo://<module>/<type>` the type registered its schema under.
|
|
5
|
+
* Rewrites schema references to their canonical, module-scoped form, in place.
|
|
11
6
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* A named shape is written with the reference tag — `!ref File` for the declaring
|
|
8
|
+
* module's own, `!ref <Alias>.<File>` for one an imported library exports — which
|
|
9
|
+
* is Telo's ONE reference grammar, and `use: schema` has been in the `x-telo-ref`
|
|
10
|
+
* vocabulary for exactly this relation all along (*names a shape; no runtime
|
|
11
|
+
* instance, no edge*). Phase 2.5 has already turned the tag into `{kind, name,
|
|
12
|
+
* alias?}`; this pass turns that into `{ $ref: "telo:<module>/<type>" }`, the id
|
|
13
|
+
* the type registered its schema under.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* WHY NORMALIZE RATHER THAN RESOLVE. Substituting the shape itself would inline
|
|
16
|
+
* it, which changes schema identity — the compiled-validator cache is keyed on it
|
|
17
|
+
* — and would make a self-recursive shape inexpressible. Keeping a REFERENCE is
|
|
18
|
+
* what every validator wants, and it is not an AJV concern: a Rust validator
|
|
19
|
+
* wants a registered id just as much. So the tag is the authoring surface and the
|
|
20
|
+
* canonical `$ref` is the internal form, exactly as `resolveRefSentinels` and
|
|
21
|
+
* `resolveSchemaRefKinds` already split authoring sugar from canonical form.
|
|
22
|
+
*
|
|
23
|
+
* ALIAS SCOPE IS WHAT THE CANONICAL FORM CARRIES. The authority is resolved in
|
|
24
|
+
* the DECLARING module's scope, so the id names the owning module and two
|
|
25
|
+
* libraries declaring a shape of the same name stay distinct. A downstream
|
|
26
|
+
* resolver reads the module off the id instead of matching a bare name, which is
|
|
27
|
+
* how an alias used to get silently dropped.
|
|
28
|
+
*
|
|
29
|
+
* The legacy authoring spelling `$ref: "telo://<authority>/<type>"` is resolved
|
|
30
|
+
* the same way, since a published artifact carries it. The version lives on the
|
|
31
|
+
* `imports:` entry, never the URI — only the pinned version is loaded, so the
|
|
32
|
+
* canonical id is version-free. Already-canonical refs and fragment-bearing
|
|
33
|
+
* built-ins (`telo://manifest#/$defs/ResourceRef`) are left untouched: the former
|
|
34
|
+
* because the authority resolves to nothing, the latter because they do not match
|
|
35
|
+
* the `authority/type` grammar.
|
|
19
36
|
*/
|
|
20
37
|
export function resolveSchemaTypeRefs(resources, aliases, aliasesByModule) {
|
|
21
|
-
const walk = (value, resolveAuthority
|
|
38
|
+
const walk = (value, resolveAuthority, ownModule,
|
|
39
|
+
// The type field's OWN value is a declared reference slot (`x-telo-ref:
|
|
40
|
+
// Telo.Type`), with its own reference validation and its own editor picker.
|
|
41
|
+
// Only what sits INSIDE it is schema, and only schema is this pass's to
|
|
42
|
+
// canonicalize — rewriting the slot itself would hand the reference checker
|
|
43
|
+
// a shape it is right to reject.
|
|
44
|
+
isSlotRoot) => {
|
|
22
45
|
if (value === null || typeof value !== "object")
|
|
23
46
|
return;
|
|
24
47
|
if (Array.isArray(value)) {
|
|
25
48
|
for (const item of value)
|
|
26
|
-
walk(item, resolveAuthority);
|
|
49
|
+
walk(item, resolveAuthority, ownModule, false);
|
|
27
50
|
return;
|
|
28
51
|
}
|
|
29
52
|
const obj = value;
|
|
@@ -33,15 +56,65 @@ export function resolveSchemaTypeRefs(resources, aliases, aliasesByModule) {
|
|
|
33
56
|
if (module)
|
|
34
57
|
obj.$ref = canonicalTypeSchemaId(module, parsed.typeName);
|
|
35
58
|
}
|
|
59
|
+
// A reference the tag left behind. Rewritten in place rather than replaced,
|
|
60
|
+
// so a node that carried siblings (a `title`, a `description`) keeps them —
|
|
61
|
+
// a `$ref` beside other keywords is ordinary JSON Schema.
|
|
62
|
+
const named = resolvedRefInSchema(obj);
|
|
63
|
+
if (named) {
|
|
64
|
+
const module = named.alias === undefined ? ownModule : resolveAuthority(named.alias);
|
|
65
|
+
// An unresolvable alias is left exactly as written: `validateReferences`
|
|
66
|
+
// is what reports a reference that names nothing, and guessing a module
|
|
67
|
+
// here would turn a typo into a `$ref` that resolves to the wrong shape.
|
|
68
|
+
if (module) {
|
|
69
|
+
const canonical = canonicalTypeSchemaId(module, named.name);
|
|
70
|
+
if (isSlotRoot) {
|
|
71
|
+
// At the slot root the reference STAYS a reference — the canonical id
|
|
72
|
+
// is stamped beside it. That is what makes the resolver alias-aware
|
|
73
|
+
// here: it reads the module off the id instead of matching a bare
|
|
74
|
+
// `metadata.name` across a flattened list, where two libraries
|
|
75
|
+
// declaring a shape of the same name collide silently.
|
|
76
|
+
obj.$ref = canonical;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
delete obj.kind;
|
|
80
|
+
delete obj.name;
|
|
81
|
+
delete obj.alias;
|
|
82
|
+
obj.$ref = canonical;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
36
86
|
for (const key of Object.keys(obj))
|
|
37
|
-
walk(obj[key], resolveAuthority);
|
|
87
|
+
walk(obj[key], resolveAuthority, ownModule, false);
|
|
38
88
|
};
|
|
39
89
|
for (const r of resources) {
|
|
40
90
|
const ownModule = r.metadata?.module;
|
|
41
91
|
const resolver = (ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
|
|
42
92
|
const resolveAuthority = (authority) => authority === "Self" ? ownModule : resolver?.moduleForAlias(authority);
|
|
43
93
|
for (const field of SCHEMA_FIELDS) {
|
|
44
|
-
walk(r[field], resolveAuthority);
|
|
94
|
+
walk(r[field], resolveAuthority, ownModule, true);
|
|
45
95
|
}
|
|
46
96
|
}
|
|
47
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* A resolved `!ref` sitting where a schema belongs, or null.
|
|
100
|
+
*
|
|
101
|
+
* Recognised structurally: Phase 2.5 writes `{kind, name, alias?}` and nothing
|
|
102
|
+
* else, and a node carrying JSON Schema keywords is a schema that happens to be
|
|
103
|
+
* beside one rather than a reference. Being conservative here is the safe
|
|
104
|
+
* direction — a node this declines to rewrite stays a reference for
|
|
105
|
+
* `validateReferences` to judge, while one it rewrote wrongly would silently
|
|
106
|
+
* become a different shape.
|
|
107
|
+
*/
|
|
108
|
+
function resolvedRefInSchema(obj) {
|
|
109
|
+
if (typeof obj.kind !== "string" || typeof obj.name !== "string")
|
|
110
|
+
return null;
|
|
111
|
+
for (const key of Object.keys(obj)) {
|
|
112
|
+
// `$ref` is allowed so the pass is idempotent at a slot root, where the id
|
|
113
|
+
// is stamped BESIDE the reference rather than replacing it.
|
|
114
|
+
if (key !== "kind" && key !== "name" && key !== "alias" && key !== "$ref")
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return typeof obj.alias === "string"
|
|
118
|
+
? { name: obj.name, alias: obj.alias }
|
|
119
|
+
: { name: obj.name };
|
|
120
|
+
}
|
package/dist/schema-compat.d.ts
CHANGED
|
@@ -12,10 +12,29 @@ export interface CompatibilityResult {
|
|
|
12
12
|
compatible: boolean;
|
|
13
13
|
issues: string[];
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Conservative structural JSON Schema compatibility check — is a value shaped
|
|
17
|
+
* like `source` acceptable where `target` is declared?
|
|
18
|
+
*
|
|
19
|
+
* COVARIANT, because the values this compares are consumed by reading: a
|
|
20
|
+
* narrower element satisfies a slot declaring a wider one. Only DEFINITE
|
|
21
|
+
* mismatches are flagged — a missing required field, a primitive type conflict,
|
|
22
|
+
* a disagreeing type argument. Anything ambiguous (`anyOf` / `oneOf` / `allOf`,
|
|
23
|
+
* an absent `type`, an undeclared argument) is treated as compatible, so an
|
|
24
|
+
* unmigrated producer and consumer keep checking exactly as they did.
|
|
25
|
+
*
|
|
26
|
+
* The traversal is written here rather than reused: the function this replaced
|
|
27
|
+
* compared only `type` for the names in `target.required` and descended only
|
|
28
|
+
* into objects, so a stream of arrays of strings and a stream of arrays of
|
|
29
|
+
* integers both read as `array` and passed — leaving argument checking inert on
|
|
30
|
+
* exactly the nested shapes it exists for. What survives from it is its posture.
|
|
31
|
+
*
|
|
32
|
+
* `resolveRef` sees through a named shape. Declaring a shape once and
|
|
33
|
+
* referencing it is the sanctioned way to reuse one, so without it two such
|
|
34
|
+
* arguments present as opaque nodes carrying no information — the same reason
|
|
35
|
+
* {@link withLiveValuesSkipped} takes one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkSchemaCompatibility(source: Record<string, any>, target: Record<string, any>, resolveRef?: (ref: string) => Record<string, any> | undefined): CompatibilityResult;
|
|
19
38
|
export declare function formatSingleError(err: any): string;
|
|
20
39
|
export declare function formatAjvErrors(errors: any[] | null | undefined): string;
|
|
21
40
|
/** A schema validation issue with a dotted-path pointer to the offending field. */
|
|
@@ -36,14 +55,21 @@ export declare function navigateJsonPointer(schema: unknown, pointer: string): u
|
|
|
36
55
|
* Returns `undefined` if any segment cannot be resolved. */
|
|
37
56
|
export declare function navigateSchemaToExprPath(schema: Record<string, any>, path: string): Record<string, any> | undefined;
|
|
38
57
|
/**
|
|
39
|
-
*
|
|
58
|
+
* Every `json`-represented value type's CEL brand → the primitive it refines.
|
|
59
|
+
*
|
|
40
60
|
* A brand is a nominal type the analyzer registers (see cel-environment.ts) so
|
|
41
|
-
* structurally-identical values (a `TcpPort` and a `UdpPort` are both
|
|
42
|
-
* stay distinct for static wiring checks. Brands carry no runtime
|
|
43
|
-
* value flows as its base type.
|
|
61
|
+
* structurally-identical values (a `Telo.TcpPort` and a `Telo.UdpPort` are both
|
|
62
|
+
* integers) stay distinct for static wiring checks. Brands carry no runtime
|
|
63
|
+
* effect — the value flows as its base type.
|
|
64
|
+
*
|
|
65
|
+
* DERIVED from the value-type vocabulary, never hand-written: a new brand is a
|
|
66
|
+
* new entry file, and a table here would be a second place to edit that could
|
|
67
|
+
* silently disagree with the one the runtime reads.
|
|
44
68
|
*/
|
|
45
69
|
export declare const VALUE_BRAND_BASE: Record<string, string>;
|
|
46
|
-
/** Read a
|
|
70
|
+
/** Read a `json`-represented value type's brand off a schema, or undefined.
|
|
71
|
+
* An `instance` type is not a brand — it replaces the JSON layer rather than
|
|
72
|
+
* refining it, so it carries its binding's CEL type instead. */
|
|
47
73
|
export declare function brandOfSchema(schema: Record<string, any> | undefined): string | undefined;
|
|
48
74
|
/** Map a JSON Schema type annotation to a CEL type string. */
|
|
49
75
|
export declare function jsonSchemaToCelType(schema: Record<string, any> | undefined): string;
|
|
@@ -52,6 +78,24 @@ export declare function celTypeSatisfiesJsonSchema(celType: string, schema: Reco
|
|
|
52
78
|
export declare function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown;
|
|
53
79
|
/** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
|
|
54
80
|
export declare function resolveRef(schema: Record<string, any>, root: Record<string, any>): Record<string, any>;
|
|
81
|
+
/** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
|
|
82
|
+
/**
|
|
83
|
+
* The `oneOf` / `anyOf` branch a value is written against, when exactly one fits.
|
|
84
|
+
*
|
|
85
|
+
* A union carries no `type` / `properties` / `items` of its own, so a walker that
|
|
86
|
+
* ignores it descends with an empty schema and hands every CEL leaf underneath a
|
|
87
|
+
* `null` placeholder — which then fails every branch and reports a pile of
|
|
88
|
+
* violations against a value that is perfectly valid. Picking the branch first
|
|
89
|
+
* is what lets the leaves be typed.
|
|
90
|
+
*
|
|
91
|
+
* Selection is structural and conservative: a branch must agree with the data's
|
|
92
|
+
* kind, and for an object every `required` key must be present (which is what
|
|
93
|
+
* separates a `{type, text}` part from a `{type, data, mediaType}` one). If that
|
|
94
|
+
* leaves anything other than exactly one branch, the union is returned unchanged
|
|
95
|
+
* — an ambiguous union is one the analyzer should not resolve on the author's
|
|
96
|
+
* behalf.
|
|
97
|
+
*/
|
|
98
|
+
export declare function selectUnionBranch(schema: Record<string, any>, data: unknown, root: Record<string, any>): Record<string, any>;
|
|
55
99
|
export declare function collectProperties(schema: Record<string, any>): Record<string, any>;
|
|
56
100
|
/** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
|
|
57
101
|
* schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"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"}
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { isRefSentinel, isTaggedSentinel, ManifestRootSchema, producedTypeOf, } from "@telorun/templating";
|
|
4
|
+
import { celBaseOfValueType, celTypeOfValueType, readValueTypeSlot, valueBrandBases, valueTypeOf, valueTypePlaceholder, } from "@telorun/sdk";
|
|
5
|
+
import { registerTeloKeywords } from "./value-type-keyword.js";
|
|
5
6
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
6
7
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
7
8
|
* Also registers the kernel manifest root schema under `telo://manifest` so
|
|
@@ -16,53 +17,181 @@ export function createAjv() {
|
|
|
16
17
|
addFormats.default
|
|
17
18
|
? addFormats.default(instance)
|
|
18
19
|
: addFormats(instance);
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// One registration site for every Telo keyword — the annotations as no-ops and
|
|
21
|
+
// `x-telo-type` as the one that checks. Registered here and in the kernel's
|
|
22
|
+
// validators from one definition, so a literal at an instance-typed slot is
|
|
23
|
+
// rejected statically and at dispatch by the identical rule.
|
|
24
|
+
registerTeloKeywords(instance);
|
|
23
25
|
instance.addSchema(ManifestRootSchema);
|
|
24
26
|
return instance;
|
|
25
27
|
}
|
|
26
28
|
const ajv = createAjv();
|
|
27
29
|
const compiledSchemaValidators = new WeakMap();
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
30
|
+
/** The alternatives a union node declares, or undefined when it is not one.
|
|
31
|
+
* `anyOf` and `oneOf` are one question here — which branches could accept this
|
|
32
|
+
* value — and their difference (exactly-one vs at-least-one) is a validation
|
|
33
|
+
* rule, not a compatibility one. */
|
|
34
|
+
function unionBranches(schema) {
|
|
35
|
+
const branches = schema.anyOf ?? schema.oneOf;
|
|
36
|
+
if (!Array.isArray(branches) || branches.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return branches.filter((b) => b && typeof b === "object");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Conservative structural JSON Schema compatibility check — is a value shaped
|
|
42
|
+
* like `source` acceptable where `target` is declared?
|
|
43
|
+
*
|
|
44
|
+
* COVARIANT, because the values this compares are consumed by reading: a
|
|
45
|
+
* narrower element satisfies a slot declaring a wider one. Only DEFINITE
|
|
46
|
+
* mismatches are flagged — a missing required field, a primitive type conflict,
|
|
47
|
+
* a disagreeing type argument. Anything ambiguous (`anyOf` / `oneOf` / `allOf`,
|
|
48
|
+
* an absent `type`, an undeclared argument) is treated as compatible, so an
|
|
49
|
+
* unmigrated producer and consumer keep checking exactly as they did.
|
|
50
|
+
*
|
|
51
|
+
* The traversal is written here rather than reused: the function this replaced
|
|
52
|
+
* compared only `type` for the names in `target.required` and descended only
|
|
53
|
+
* into objects, so a stream of arrays of strings and a stream of arrays of
|
|
54
|
+
* integers both read as `array` and passed — leaving argument checking inert on
|
|
55
|
+
* exactly the nested shapes it exists for. What survives from it is its posture.
|
|
56
|
+
*
|
|
57
|
+
* `resolveRef` sees through a named shape. Declaring a shape once and
|
|
58
|
+
* referencing it is the sanctioned way to reuse one, so without it two such
|
|
59
|
+
* arguments present as opaque nodes carrying no information — the same reason
|
|
60
|
+
* {@link withLiveValuesSkipped} takes one.
|
|
61
|
+
*/
|
|
62
|
+
export function checkSchemaCompatibility(source, target, resolveRef) {
|
|
32
63
|
const issues = [];
|
|
33
|
-
|
|
64
|
+
compare(source, target, "", issues, resolveRef, new Set());
|
|
34
65
|
return { compatible: issues.length === 0, issues };
|
|
35
66
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
67
|
+
function deref(schema, resolveRef) {
|
|
68
|
+
if (!resolveRef || typeof schema.$ref !== "string")
|
|
69
|
+
return schema;
|
|
70
|
+
return resolveRef(schema.$ref) ?? schema;
|
|
71
|
+
}
|
|
72
|
+
function compare(rawSource, rawTarget, path, issues, resolveRef, seen) {
|
|
73
|
+
if (!rawSource || !rawTarget || typeof rawSource !== "object" || typeof rawTarget !== "object") {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// A recursive shape reached through the same pair of references twice is the
|
|
77
|
+
// same question again; answering it once terminates and loses nothing.
|
|
78
|
+
//
|
|
79
|
+
// The key is the REFERENCE PAIR and deliberately not the path. A path grows on
|
|
80
|
+
// every descent, so a key containing it is new every time and the guard never
|
|
81
|
+
// fires — which is a stack overflow on the first self-referential shape, taking
|
|
82
|
+
// every other diagnostic in the file with it. It also has to be this way to be
|
|
83
|
+
// correct rather than merely terminating: comparing two schemas gives the same
|
|
84
|
+
// answer wherever they are reached from, so the second visit has nothing to add.
|
|
85
|
+
if (typeof rawSource.$ref === "string" && typeof rawTarget.$ref === "string") {
|
|
86
|
+
const key = `${rawSource.$ref}|${rawTarget.$ref}`;
|
|
87
|
+
if (seen.has(key))
|
|
88
|
+
return;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
}
|
|
91
|
+
const source = deref(rawSource, resolveRef);
|
|
92
|
+
const target = deref(rawTarget, resolveRef);
|
|
93
|
+
// A union is ALTERNATIVES, so it is compared by distributing over branches on
|
|
94
|
+
// both sides: a definite conflict is one where no source-branch/target-branch
|
|
95
|
+
// pair agrees. Returning silently the moment either side was a union — which
|
|
96
|
+
// is what this did — switched the whole comparison off for any slot that
|
|
97
|
+
// accepts more than one shape, and those are exactly the slots where a value
|
|
98
|
+
// type carries the only information distinguishing the branches.
|
|
99
|
+
//
|
|
100
|
+
// `allOf` is a conjunction rather than a choice, so it keeps the old posture:
|
|
101
|
+
// it says too little to judge and stays compatible.
|
|
102
|
+
if (source.allOf || target.allOf)
|
|
103
|
+
return;
|
|
104
|
+
const sourceBranches = unionBranches(source);
|
|
105
|
+
const targetBranches = unionBranches(target);
|
|
106
|
+
if (sourceBranches || targetBranches) {
|
|
107
|
+
const lefts = sourceBranches ?? [source];
|
|
108
|
+
const rights = targetBranches ?? [target];
|
|
109
|
+
const reasons = [];
|
|
110
|
+
for (const left of lefts) {
|
|
111
|
+
for (const right of rights) {
|
|
112
|
+
const probe = [];
|
|
113
|
+
// A fresh `seen` per probe: a pair rejected on one branch must not mark
|
|
114
|
+
// a reference pair visited for the next, which would silently pass it.
|
|
115
|
+
compare(left, right, path, probe, resolveRef, new Set(seen));
|
|
116
|
+
if (probe.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
reasons.push(...probe);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
issues.push(`${path || "/"}: no alternative matches — ${[...new Set(reasons)].join("; ")}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Value types first: an `instance` representation has no JSON `type` to
|
|
125
|
+
// compare, so its identity IS the comparison — and its arguments are where the
|
|
126
|
+
// real information lives.
|
|
127
|
+
const sourceType = readValueTypeSlot(source);
|
|
128
|
+
const targetType = readValueTypeSlot(target);
|
|
129
|
+
if (sourceType && targetType) {
|
|
130
|
+
if (sourceType.name !== targetType.name) {
|
|
131
|
+
issues.push(`${path || "/"}: value type mismatch — source is '${sourceType.name}', target expects '${targetType.name}'`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const [argument, targetArg] of Object.entries(targetType.args)) {
|
|
135
|
+
const sourceArg = sourceType.args[argument];
|
|
136
|
+
// An omitted argument is *any*, in BOTH directions. That is what keeps a
|
|
137
|
+
// bare `Telo.Stream` flowing into a typed slot and vice versa, so nothing
|
|
138
|
+
// that does not declare its element is forced to.
|
|
139
|
+
if (sourceArg === undefined)
|
|
140
|
+
continue;
|
|
141
|
+
compare(sourceArg, targetArg, `${path}<${argument}>`, issues, resolveRef, seen);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// One side declares a value type and the other does not. A `json`
|
|
146
|
+
// representation refines a base type, so it is compared through that base — a
|
|
147
|
+
// `Telo.TcpPort` into a plain `integer` slot is gradual typing working. An
|
|
148
|
+
// `instance` is not JSON at all, so ANY declared JSON type on the other side is
|
|
149
|
+
// a definite conflict; a side declaring no type at all is still saying nothing
|
|
150
|
+
// and stays compatible.
|
|
151
|
+
if (Boolean(sourceType) !== Boolean(targetType)) {
|
|
152
|
+
const declared = (sourceType ?? targetType);
|
|
153
|
+
const other = sourceType ? target : source;
|
|
154
|
+
if (declared.entry && typeof other.type === "string") {
|
|
155
|
+
const base = celBaseOfValueType(declared.entry);
|
|
156
|
+
const asJson = base === undefined ? undefined : declared.entry.base;
|
|
157
|
+
if (asJson !== other.type) {
|
|
158
|
+
issues.push(`${path || "/"}: value type mismatch — ${sourceType ? "source is" : "target expects"} '${declared.name}', ${sourceType ? "target expects" : "source is"} '${other.type}'`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Only flag definite primitive type clashes; an absent or union `type` says
|
|
164
|
+
// too little to judge.
|
|
165
|
+
if (typeof source.type === "string" &&
|
|
166
|
+
typeof target.type === "string" &&
|
|
167
|
+
source.type !== target.type) {
|
|
168
|
+
issues.push(`${path || "/"}: type mismatch — source is '${source.type}', target expects '${target.type}'`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// An array's element, which the old comparison never looked at — so every
|
|
172
|
+
// nested shape passed regardless of what it contained.
|
|
173
|
+
if (target.items && source.items) {
|
|
174
|
+
compare(source.items, target.items, `${path}[]`, issues, resolveRef, seen);
|
|
175
|
+
}
|
|
176
|
+
const targetRequired = Array.isArray(target.required) ? target.required : [];
|
|
38
177
|
const sourceProps = source.properties ?? {};
|
|
39
178
|
const targetProps = target.properties ?? {};
|
|
40
179
|
for (const field of targetRequired) {
|
|
41
180
|
if (!(field in sourceProps)) {
|
|
181
|
+
// Only when the source describes an object at all: a schema with no
|
|
182
|
+
// `properties` is saying nothing about its shape, not saying it is empty.
|
|
183
|
+
if (source.properties === undefined)
|
|
184
|
+
continue;
|
|
42
185
|
issues.push(`${path}/${field}: required by target but missing from source`);
|
|
43
186
|
continue;
|
|
44
187
|
}
|
|
45
188
|
const srcProp = sourceProps[field];
|
|
46
189
|
const tgtProp = targetProps[field];
|
|
47
190
|
if (tgtProp && srcProp) {
|
|
48
|
-
|
|
191
|
+
compare(srcProp, tgtProp, `${path}/${field}`, issues, resolveRef, seen);
|
|
49
192
|
}
|
|
50
193
|
}
|
|
51
194
|
}
|
|
52
|
-
function checkProperty(source, target, path, issues) {
|
|
53
|
-
// Only flag definite primitive type clashes; skip anyOf/oneOf/allOf
|
|
54
|
-
if (source.type &&
|
|
55
|
-
target.type &&
|
|
56
|
-
typeof source.type === "string" &&
|
|
57
|
-
typeof target.type === "string" &&
|
|
58
|
-
source.type !== target.type) {
|
|
59
|
-
issues.push(`${path}: type mismatch — source is '${source.type}', target expects '${target.type}'`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (target.type === "object" && source.type === "object") {
|
|
63
|
-
checkObject(source, target, path, issues);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
195
|
export function formatSingleError(err) {
|
|
67
196
|
const p = err.instancePath || "/";
|
|
68
197
|
const params = err.params ?? {};
|
|
@@ -120,13 +249,8 @@ function schemaCompiles(schema) {
|
|
|
120
249
|
export function validateAgainstSchema(data, schema) {
|
|
121
250
|
let validate = compiledSchemaValidators.get(schema);
|
|
122
251
|
if (!validate) {
|
|
123
|
-
// Normalize outside the try: a fault in our own ref-slot normalization must
|
|
124
|
-
// surface, never be mistaken for the module author's schema being malformed.
|
|
125
|
-
// Drop the legacy scalar `type` an older published module may still pin on
|
|
126
|
-
// its `x-telo-ref` slots so a resolved reference object validates.
|
|
127
|
-
const normalized = normalizeRefSlots(schema);
|
|
128
252
|
try {
|
|
129
|
-
validate = ajv.compile(
|
|
253
|
+
validate = ajv.compile(schema);
|
|
130
254
|
}
|
|
131
255
|
catch (err) {
|
|
132
256
|
// The normalized schema didn't compile. If the original schema is itself
|
|
@@ -198,28 +322,36 @@ export function navigateSchemaToExprPath(schema, path) {
|
|
|
198
322
|
return current;
|
|
199
323
|
}
|
|
200
324
|
/**
|
|
201
|
-
*
|
|
325
|
+
* Every `json`-represented value type's CEL brand → the primitive it refines.
|
|
326
|
+
*
|
|
202
327
|
* A brand is a nominal type the analyzer registers (see cel-environment.ts) so
|
|
203
|
-
* structurally-identical values (a `TcpPort` and a `UdpPort` are both
|
|
204
|
-
* stay distinct for static wiring checks. Brands carry no runtime
|
|
205
|
-
* value flows as its base type.
|
|
328
|
+
* structurally-identical values (a `Telo.TcpPort` and a `Telo.UdpPort` are both
|
|
329
|
+
* integers) stay distinct for static wiring checks. Brands carry no runtime
|
|
330
|
+
* effect — the value flows as its base type.
|
|
331
|
+
*
|
|
332
|
+
* DERIVED from the value-type vocabulary, never hand-written: a new brand is a
|
|
333
|
+
* new entry file, and a table here would be a second place to edit that could
|
|
334
|
+
* silently disagree with the one the runtime reads.
|
|
206
335
|
*/
|
|
207
|
-
export const VALUE_BRAND_BASE =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** Read a recognized `x-telo-type` brand off a schema, or undefined. */
|
|
336
|
+
export const VALUE_BRAND_BASE = valueBrandBases();
|
|
337
|
+
/** Read a `json`-represented value type's brand off a schema, or undefined.
|
|
338
|
+
* An `instance` type is not a brand — it replaces the JSON layer rather than
|
|
339
|
+
* refining it, so it carries its binding's CEL type instead. */
|
|
212
340
|
export function brandOfSchema(schema) {
|
|
213
|
-
const
|
|
214
|
-
return
|
|
341
|
+
const entry = valueTypeOf(schema);
|
|
342
|
+
return entry && entry.representation === "json" ? entry.name : undefined;
|
|
215
343
|
}
|
|
216
344
|
/** Map a JSON Schema type annotation to a CEL type string. */
|
|
217
345
|
export function jsonSchemaToCelType(schema) {
|
|
218
346
|
if (!schema || typeof schema !== "object")
|
|
219
347
|
return "dyn";
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
348
|
+
// A declared value type IS the type — for an `instance` representation it is
|
|
349
|
+
// the only thing that says so, since bytes and streams have no JSON Schema
|
|
350
|
+
// type at all. Before the three annotations were unified, a byte slot's
|
|
351
|
+
// expression typed as `dyn` because nothing here consulted `x-telo-binary`.
|
|
352
|
+
const entry = valueTypeOf(schema);
|
|
353
|
+
if (entry)
|
|
354
|
+
return celTypeOfValueType(entry);
|
|
223
355
|
if (schema.anyOf || schema.oneOf || schema.allOf)
|
|
224
356
|
return "dyn";
|
|
225
357
|
if (Array.isArray(schema.type))
|
|
@@ -263,10 +395,28 @@ export function celTypeSatisfiesJsonSchema(celType, schema) {
|
|
|
263
395
|
return fieldBrand === celType;
|
|
264
396
|
celType = sourceBase;
|
|
265
397
|
}
|
|
398
|
+
// An `instance` representation has no JSON Schema type to compare against, so
|
|
399
|
+
// an expression carrying its binding's CEL type is accepted on that ground
|
|
400
|
+
// alone. This ADDS a case and never removes one: a mismatch falls through to
|
|
401
|
+
// the rules below rather than being rejected here, so nothing that checks
|
|
402
|
+
// today stops checking, and the `bytes` row still accepts a byte expression at
|
|
403
|
+
// a plain `type: string` slot.
|
|
404
|
+
const slotEntry = valueTypeOf(schema);
|
|
405
|
+
if (slotEntry?.representation === "instance" && celTypeOfValueType(slotEntry) === celType) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
266
408
|
if (!schema.type && !schema.anyOf && !schema.oneOf && !schema.allOf)
|
|
267
409
|
return true;
|
|
268
|
-
|
|
410
|
+
// `allOf` is a conjunction and says too little to judge from a single CEL
|
|
411
|
+
// type. A union is a CHOICE, so it is satisfied by satisfying any branch —
|
|
412
|
+
// distributed for the same reason `compare` does it: accepting every union
|
|
413
|
+
// outright turns the check off for exactly the slots that admit more than one
|
|
414
|
+
// shape, and those are the ones where the branches carry the information.
|
|
415
|
+
if (schema.allOf)
|
|
269
416
|
return true;
|
|
417
|
+
const branches = unionBranches(schema);
|
|
418
|
+
if (branches)
|
|
419
|
+
return branches.some((branch) => celTypeSatisfiesJsonSchema(celType, branch));
|
|
270
420
|
const schemaTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
271
421
|
const accepted = {
|
|
272
422
|
int: ["integer", "number"],
|
|
@@ -357,12 +507,16 @@ function foldedConstraints(schema) {
|
|
|
357
507
|
}
|
|
358
508
|
export function celPlaceholderForSchema(rawSchema) {
|
|
359
509
|
const schema = foldedConstraints(rawSchema);
|
|
360
|
-
//
|
|
361
|
-
// and at dispatch, so a CEL leaf standing in for a runtime
|
|
362
|
-
// it. This is what keeps the rule single — a literal is
|
|
363
|
-
// literal is a
|
|
364
|
-
|
|
365
|
-
|
|
510
|
+
// An instance-typed slot's placeholder must BE an instance: the same keyword
|
|
511
|
+
// validates statically and at dispatch, so a CEL leaf standing in for a runtime
|
|
512
|
+
// value has to satisfy it. This is what keeps the rule single — a literal is
|
|
513
|
+
// rejected because no YAML literal is a byte buffer, while a value arriving by
|
|
514
|
+
// reference passes. The stand-in comes from the binding table, so a new
|
|
515
|
+
// instance type brings its own rather than adding a branch here; a `live` type
|
|
516
|
+
// declares none, because nothing validates it.
|
|
517
|
+
const placeholder = valueTypePlaceholder(schema);
|
|
518
|
+
if (placeholder !== undefined)
|
|
519
|
+
return placeholder;
|
|
366
520
|
if (schema.default !== undefined)
|
|
367
521
|
return schema.default;
|
|
368
522
|
// An enum-constrained field needs a placeholder drawn from the enum: the
|
|
@@ -373,6 +527,24 @@ export function celPlaceholderForSchema(rawSchema) {
|
|
|
373
527
|
// the real value is checked at runtime once the expression resolves.
|
|
374
528
|
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
375
529
|
return schema.enum[0];
|
|
530
|
+
// A UNION with no `type` of its own. Without this, a whole-field CEL leaf at
|
|
531
|
+
// such a slot gets `null`, which every branch then rejects — so a field
|
|
532
|
+
// declared `anyOf: [array, boolean]` could not be written as an expression at
|
|
533
|
+
// all, while one whose union happens to contain a `live` branch escaped by
|
|
534
|
+
// accident (nothing validates a live value, so `null` passed). The first
|
|
535
|
+
// branch that yields a placeholder wins: the same conservative posture
|
|
536
|
+
// `selectUnionBranch` takes, and enough for AJV, whose question is only
|
|
537
|
+
// whether SOME branch accepts the stand-in.
|
|
538
|
+
if (schema.type === undefined) {
|
|
539
|
+
const branches = unionBranches(schema);
|
|
540
|
+
if (branches) {
|
|
541
|
+
for (const branch of branches) {
|
|
542
|
+
const candidate = celPlaceholderForSchema(branch);
|
|
543
|
+
if (candidate !== null)
|
|
544
|
+
return candidate;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
376
548
|
switch (schema.type) {
|
|
377
549
|
case "integer":
|
|
378
550
|
case "number":
|
|
@@ -444,7 +616,7 @@ export function resolveRef(schema, root) {
|
|
|
444
616
|
* — an ambiguous union is one the analyzer should not resolve on the author's
|
|
445
617
|
* behalf.
|
|
446
618
|
*/
|
|
447
|
-
function selectUnionBranch(schema, data, root) {
|
|
619
|
+
export function selectUnionBranch(schema, data, root) {
|
|
448
620
|
const branches = (schema.oneOf ?? schema.anyOf);
|
|
449
621
|
if (!Array.isArray(branches) || branches.length === 0)
|
|
450
622
|
return schema;
|
|
@@ -537,16 +709,23 @@ onSubstitute, path = "") {
|
|
|
537
709
|
if (isRefSentinel(data)) {
|
|
538
710
|
return data;
|
|
539
711
|
}
|
|
540
|
-
// A
|
|
541
|
-
//
|
|
542
|
-
// produces
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// real type lets AJV and the `x-telo-
|
|
547
|
-
// statically, with no new diagnostic code.
|
|
548
|
-
|
|
549
|
-
|
|
712
|
+
// A tag whose produced type is a CONSTANT of the tag rather than a function of
|
|
713
|
+
// the slot substitutes a placeholder of THAT type: `!include-text` always
|
|
714
|
+
// produces a string and `!include-bytes` always produces bytes. Collapsing
|
|
715
|
+
// them to a slot-shaped placeholder like a CEL expression would make every
|
|
716
|
+
// slot accept both, so a byte embed at a `type: string` field passed
|
|
717
|
+
// `telo check` and failed at resource creation — and the reverse did too.
|
|
718
|
+
// Substituting the real type lets AJV and the `x-telo-type` keyword reject
|
|
719
|
+
// both directions statically, with no new diagnostic code.
|
|
720
|
+
//
|
|
721
|
+
// The engine is what says so. This used to branch on two tag names, which was
|
|
722
|
+
// the only place a tag's produced type was written down and it was written in
|
|
723
|
+
// the consumer — so a future tag producing bytes had to be added to a set here
|
|
724
|
+
// rather than declaring it.
|
|
725
|
+
if (isTaggedSentinel(data)) {
|
|
726
|
+
const produced = producedTypeOf(data.engine);
|
|
727
|
+
if (produced)
|
|
728
|
+
return celPlaceholderForSchema(produced);
|
|
550
729
|
}
|
|
551
730
|
if (isTaggedSentinel(data)) {
|
|
552
731
|
mark();
|