@telorun/analyzer 0.63.0 → 0.65.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/analysis-registry.d.ts +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +35 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -470
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +38 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +66 -22
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/invocation-contract.d.ts +11 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +15 -0
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/schema-compat.d.ts +59 -22
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +60 -75
- package/dist/schema-error-report.d.ts +68 -0
- package/dist/schema-error-report.d.ts.map +1 -0
- package/dist/schema-error-report.js +356 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-nested-inline.d.ts +22 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +17 -9
- package/dist/validate-step-inputs.d.ts +17 -0
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +108 -9
- package/package.json +2 -2
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +83 -587
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +79 -24
- package/src/find-manifest.ts +19 -0
- package/src/index.ts +23 -2
- package/src/invocation-contract.ts +22 -0
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-path.ts +34 -0
- package/src/schema-compat.ts +92 -79
- package/src/schema-error-report.ts +417 -0
- package/src/schema-walk.ts +144 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-nested-inline.ts +35 -14
- package/src/validate-step-inputs.ts +153 -11
package/dist/schema-compat.js
CHANGED
|
@@ -3,6 +3,7 @@ import addFormats from "ajv-formats";
|
|
|
3
3
|
import { isRefSentinel, isTaggedSentinel, producedTypeOf, } from "@telorun/templating";
|
|
4
4
|
import { celBaseOfValueType, celTypeOfValueType, readValueTypeSlot, valueBrandBases, valueTypeOf, valueTypePlaceholder, } from "@telorun/sdk";
|
|
5
5
|
import { ManifestRootSchema } from "./manifest-schemas.js";
|
|
6
|
+
import { schemaIssues } from "./schema-error-report.js";
|
|
6
7
|
import { registerTeloKeywords } from "./value-type-keyword.js";
|
|
7
8
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
8
9
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
@@ -193,48 +194,7 @@ function compare(rawSource, rawTarget, path, issues, resolveRef, seen) {
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
|
-
export
|
|
197
|
-
const p = err.instancePath || "/";
|
|
198
|
-
const params = err.params ?? {};
|
|
199
|
-
switch (err.keyword) {
|
|
200
|
-
case "additionalProperties":
|
|
201
|
-
return `${p} must NOT have additional properties ('${params.additionalProperty}' is not allowed)`;
|
|
202
|
-
case "required":
|
|
203
|
-
return `${p} is missing required property '${params.missingProperty}'`;
|
|
204
|
-
case "enum":
|
|
205
|
-
return `${p} ${err.message ?? "is invalid"} (${params.allowedValues?.join(" | ")})`;
|
|
206
|
-
case "type":
|
|
207
|
-
return `${p} must be ${params.type} (got ${typeof err.data})`;
|
|
208
|
-
default:
|
|
209
|
-
return `${p} ${err.message ?? "is invalid"}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
export function formatAjvErrors(errors) {
|
|
213
|
-
if (!errors || errors.length === 0)
|
|
214
|
-
return "Unknown schema error";
|
|
215
|
-
return errors.map(formatSingleError).join("; ");
|
|
216
|
-
}
|
|
217
|
-
/** Converts an AJV error object to a dotted path string compatible with PositionIndex keys.
|
|
218
|
-
* e.g. instancePath "/config/routes/0/handler" → "config.routes[0].handler"
|
|
219
|
-
* For "required" keyword errors, appends the missing property to the parent path. */
|
|
220
|
-
function ajvErrorToPath(err) {
|
|
221
|
-
const instancePath = (err.instancePath ?? "");
|
|
222
|
-
const parts = instancePath.split("/").filter((p) => p !== "");
|
|
223
|
-
let result = "";
|
|
224
|
-
for (const part of parts) {
|
|
225
|
-
if (/^\d+$/.test(part)) {
|
|
226
|
-
result += `[${part}]`;
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
result += result ? `.${part}` : part;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
233
|
-
const missing = err.params.missingProperty;
|
|
234
|
-
result += result ? `.${missing}` : missing;
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
197
|
+
export { formatAjvErrors, formatSingleError } from "./schema-error-report.js";
|
|
238
198
|
/** Does `schema` compile as-authored? Used to tell a malformed module schema
|
|
239
199
|
* (the author's problem) apart from a fault we introduced while normalizing it. */
|
|
240
200
|
function schemaCompiles(schema) {
|
|
@@ -268,10 +228,7 @@ export function validateAgainstSchema(data, schema) {
|
|
|
268
228
|
}
|
|
269
229
|
if (validate(data))
|
|
270
230
|
return [];
|
|
271
|
-
return (validate.errors
|
|
272
|
-
message: formatSingleError(err),
|
|
273
|
-
path: ajvErrorToPath(err),
|
|
274
|
-
}));
|
|
231
|
+
return schemaIssues(validate.errors);
|
|
275
232
|
}
|
|
276
233
|
/** Resolves a JSON Pointer (RFC 6901, must start with "/") into a schema object.
|
|
277
234
|
* Returns undefined when any segment along the path is missing or not an object. */
|
|
@@ -590,15 +547,43 @@ function objectPlaceholder(schema) {
|
|
|
590
547
|
return out;
|
|
591
548
|
}
|
|
592
549
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
593
|
-
/**
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
550
|
+
/**
|
|
551
|
+
* Resolve a `$ref` — the document-local `#/$defs/...` form against `root`, and
|
|
552
|
+
* anything else through `external` when a caller supplies one.
|
|
553
|
+
*
|
|
554
|
+
* A named shape is addressed by a registered id (`telo:<module>/<Type>`), which
|
|
555
|
+
* lives in a schema store rather than in this document, so without the hook a
|
|
556
|
+
* walk stops at the reference and treats a described value as undescribed:
|
|
557
|
+
* every CEL leaf under it is handed the schema-unaware `""` placeholder and
|
|
558
|
+
* then rejected against a branch it was never measured against. The caller
|
|
559
|
+
* supplies the store because only the caller has one.
|
|
560
|
+
*/
|
|
561
|
+
export function resolveRef(schema, root, external) {
|
|
562
|
+
return resolveRefIn(schema, root, external).schema;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* {@link resolveRef}, reporting the ROOT the result's own `#/...` references
|
|
566
|
+
* resolve against.
|
|
567
|
+
*
|
|
568
|
+
* Following an external reference enters another document, and a `$ref` inside
|
|
569
|
+
* it is relative to THAT document — which is the whole of how a shape declares
|
|
570
|
+
* its own vocabulary (`anyOf: [{$ref: "#/$defs/Text"}, …]`). Resolving those
|
|
571
|
+
* against the referring document finds nothing, and a walker that then treats
|
|
572
|
+
* the branches as unconstrained accepts every one of them, resolves the union
|
|
573
|
+
* to nothing, and hands the values underneath an untyped stand-in. So the base
|
|
574
|
+
* travels with the schema.
|
|
575
|
+
*/
|
|
576
|
+
export function resolveRefIn(schema, root, external) {
|
|
577
|
+
if (!schema.$ref || typeof schema.$ref !== "string")
|
|
578
|
+
return { schema, root };
|
|
579
|
+
if (schema.$ref === "#")
|
|
580
|
+
return { schema: root, root };
|
|
581
|
+
if (schema.$ref.startsWith("#/$defs/")) {
|
|
582
|
+
const resolved = root.$defs?.[schema.$ref.slice("#/$defs/".length)];
|
|
583
|
+
return resolved ? { schema: resolved, root } : { schema, root };
|
|
584
|
+
}
|
|
585
|
+
const target = external?.(schema.$ref);
|
|
586
|
+
return target ? { schema: target, root: target } : { schema, root };
|
|
602
587
|
}
|
|
603
588
|
/** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
|
|
604
589
|
/**
|
|
@@ -617,7 +602,7 @@ export function resolveRef(schema, root) {
|
|
|
617
602
|
* — an ambiguous union is one the analyzer should not resolve on the author's
|
|
618
603
|
* behalf.
|
|
619
604
|
*/
|
|
620
|
-
export function selectUnionBranch(schema, data, root) {
|
|
605
|
+
export function selectUnionBranch(schema, data, root, external) {
|
|
621
606
|
const branches = (schema.oneOf ?? schema.anyOf);
|
|
622
607
|
if (!Array.isArray(branches) || branches.length === 0)
|
|
623
608
|
return schema;
|
|
@@ -639,7 +624,7 @@ export function selectUnionBranch(schema, data, root) {
|
|
|
639
624
|
if (!kind)
|
|
640
625
|
return schema;
|
|
641
626
|
const fits = branches
|
|
642
|
-
.map((b) => resolveRef(b, root))
|
|
627
|
+
.map((b) => resolveRef(b, root, external))
|
|
643
628
|
.filter((b) => {
|
|
644
629
|
const types = Array.isArray(b.type) ? b.type : b.type ? [b.type] : [];
|
|
645
630
|
if (types.length > 0 && !types.includes(kind))
|
|
@@ -680,21 +665,13 @@ export function collectProperties(schema) {
|
|
|
680
665
|
}
|
|
681
666
|
return props;
|
|
682
667
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
* against a value no author wrote. Some constraints cannot be satisfied by
|
|
691
|
-
* construction at all (`pattern`, `format`, a `oneOf` of unrelated shapes),
|
|
692
|
-
* so making every placeholder acceptable is not achievable in general —
|
|
693
|
-
* knowing where not to look is. Structural findings survive because they are
|
|
694
|
-
* located at the CONTAINER, not at the substituted leaf. */
|
|
695
|
-
onSubstitute, path = "") {
|
|
696
|
-
const root = rootSchema ?? schema;
|
|
697
|
-
const resolved = selectUnionBranch(resolveRef(schema, root), data, root);
|
|
668
|
+
export function substituteCelFields(data, schema, rootSchema, options = {}) {
|
|
669
|
+
const { onSubstitute, external } = options;
|
|
670
|
+
const path = options.path ?? "";
|
|
671
|
+
const base = rootSchema ?? schema;
|
|
672
|
+
const entered = resolveRefIn(schema, base, external);
|
|
673
|
+
const root = entered.root;
|
|
674
|
+
const resolved = selectUnionBranch(entered.schema, data, root, external);
|
|
698
675
|
const mark = () => onSubstitute?.(path);
|
|
699
676
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
700
677
|
mark();
|
|
@@ -733,8 +710,12 @@ onSubstitute, path = "") {
|
|
|
733
710
|
return celPlaceholderForSchema(resolved);
|
|
734
711
|
}
|
|
735
712
|
if (Array.isArray(data)) {
|
|
736
|
-
const
|
|
737
|
-
return data.map((
|
|
713
|
+
const item = resolveRefIn((resolved.items ?? {}), root, external);
|
|
714
|
+
return data.map((element, i) => substituteCelFields(element, item.schema, item.root, {
|
|
715
|
+
onSubstitute,
|
|
716
|
+
path: `${path}[${i}]`,
|
|
717
|
+
external,
|
|
718
|
+
}));
|
|
738
719
|
}
|
|
739
720
|
if (data !== null && typeof data === "object") {
|
|
740
721
|
const props = collectProperties(resolved);
|
|
@@ -743,7 +724,11 @@ onSubstitute, path = "") {
|
|
|
743
724
|
: undefined;
|
|
744
725
|
const result = {};
|
|
745
726
|
for (const [k, v] of Object.entries(data)) {
|
|
746
|
-
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}), root,
|
|
727
|
+
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}), root, {
|
|
728
|
+
onSubstitute,
|
|
729
|
+
path: path ? `${path}.${k}` : k,
|
|
730
|
+
external,
|
|
731
|
+
});
|
|
747
732
|
}
|
|
748
733
|
return result;
|
|
749
734
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one renderer for AJV validation failures.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe and re-imported by the kernel — the split `buildEvalPaths` and
|
|
5
|
+
* the redaction path parser already use — so a failure is phrased identically
|
|
6
|
+
* under `telo check` and at runtime. Three implementations used to answer this
|
|
7
|
+
* (the analyzer's keyword prose, the kernel's raw `instancePath + message`
|
|
8
|
+
* join, and observed state's own inline variant), so a developer who fixed what
|
|
9
|
+
* the analyzer told them met a different sentence describing the same thing.
|
|
10
|
+
*
|
|
11
|
+
* UNION REDUCTION is the second half. A union must attempt every branch, and
|
|
12
|
+
* AJV cannot know which one was intended — `discriminator: true` works only
|
|
13
|
+
* against an explicit OpenAPI-style discriminator property, which would mean
|
|
14
|
+
* changing what every module's authors write. So branch selection is a
|
|
15
|
+
* reporting concern and lives here.
|
|
16
|
+
*
|
|
17
|
+
* It narrows the error SET, never just the sentence: every consumer maps the
|
|
18
|
+
* surviving errors to manifest paths to anchor a diagnostic, so reducing at the
|
|
19
|
+
* prose layer alone would move the soup out of the message and into the
|
|
20
|
+
* problems list, one entry per branch on a different line.
|
|
21
|
+
*
|
|
22
|
+
* Selection is made from the ERRORS ALONE, never from the schema. A branch
|
|
23
|
+
* whose discriminating key is present emits no complaint at the union's own
|
|
24
|
+
* instancePath; one whose key is absent says `required`, and one that forbids a
|
|
25
|
+
* key the value carries says `additionalProperties`. That is the whole signal,
|
|
26
|
+
* and reading it off the errors is what lets reduction work across a `$ref`
|
|
27
|
+
* into another registered schema, where navigating to the branch subschema
|
|
28
|
+
* would mean re-implementing AJV's resolution.
|
|
29
|
+
*/
|
|
30
|
+
/** An AJV error object. Structurally typed — the analyzer and the kernel hand
|
|
31
|
+
* over errors from their own AJV instances. */
|
|
32
|
+
export interface AjvErrorLike {
|
|
33
|
+
keyword?: string;
|
|
34
|
+
instancePath?: string;
|
|
35
|
+
schemaPath?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
params?: Record<string, any>;
|
|
38
|
+
data?: unknown;
|
|
39
|
+
}
|
|
40
|
+
/** A schema validation issue with a dotted-path pointer to the offending field. */
|
|
41
|
+
export interface SchemaIssue {
|
|
42
|
+
message: string;
|
|
43
|
+
/** Dotted path to the field (e.g. "config.handler"). Empty string means root. */
|
|
44
|
+
path: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function formatSingleError(err: AjvErrorLike): string;
|
|
47
|
+
/**
|
|
48
|
+
* Replace each failing union with the errors of the branch the author plainly
|
|
49
|
+
* meant, recursively, outside in.
|
|
50
|
+
*
|
|
51
|
+
* Attribution runs to the DEEPEST occurrence that could own an error, which is
|
|
52
|
+
* what keeps a container's own complaint apart from its child's when both carry
|
|
53
|
+
* the same `schemaPath`. An occurrence reached through a branch becomes a
|
|
54
|
+
* candidate branch of its own — it raised nothing at the parent's node, so it is
|
|
55
|
+
* plausible exactly when the value really did take that shape and fail further
|
|
56
|
+
* in, and reducing it recursively is what stops an inner union's alternatives
|
|
57
|
+
* from surviving inside the outer one's selection.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reduceSchemaErrors(errors: AjvErrorLike[] | null | undefined): AjvErrorLike[];
|
|
60
|
+
/** Converts an AJV error to a dotted path compatible with PositionIndex keys.
|
|
61
|
+
* e.g. instancePath "/config/routes/0/handler" → "config.routes[0].handler"
|
|
62
|
+
* For "required" keyword errors, appends the missing property to the parent path. */
|
|
63
|
+
export declare function ajvErrorToPath(err: AjvErrorLike): string;
|
|
64
|
+
/** Reduced, path-anchored issues — what a diagnostic list is built from. */
|
|
65
|
+
export declare function schemaIssues(errors: AjvErrorLike[] | null | undefined): SchemaIssue[];
|
|
66
|
+
/** Reduced, rendered as one sentence — what a thrown runtime error carries. */
|
|
67
|
+
export declare function formatAjvErrors(errors: AjvErrorLike[] | null | undefined): string;
|
|
68
|
+
//# sourceMappingURL=schema-error-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-error-report.d.ts","sourceRoot":"","sources":["../src/schema-error-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;gDACgD;AAChD,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAWD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAe3D;AAkGD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,EAAE,CAsF5F;AAqHD;;sFAEsF;AACtF,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAaxD;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,EAAE,CAKrF;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAIjF"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one renderer for AJV validation failures.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe and re-imported by the kernel — the split `buildEvalPaths` and
|
|
5
|
+
* the redaction path parser already use — so a failure is phrased identically
|
|
6
|
+
* under `telo check` and at runtime. Three implementations used to answer this
|
|
7
|
+
* (the analyzer's keyword prose, the kernel's raw `instancePath + message`
|
|
8
|
+
* join, and observed state's own inline variant), so a developer who fixed what
|
|
9
|
+
* the analyzer told them met a different sentence describing the same thing.
|
|
10
|
+
*
|
|
11
|
+
* UNION REDUCTION is the second half. A union must attempt every branch, and
|
|
12
|
+
* AJV cannot know which one was intended — `discriminator: true` works only
|
|
13
|
+
* against an explicit OpenAPI-style discriminator property, which would mean
|
|
14
|
+
* changing what every module's authors write. So branch selection is a
|
|
15
|
+
* reporting concern and lives here.
|
|
16
|
+
*
|
|
17
|
+
* It narrows the error SET, never just the sentence: every consumer maps the
|
|
18
|
+
* surviving errors to manifest paths to anchor a diagnostic, so reducing at the
|
|
19
|
+
* prose layer alone would move the soup out of the message and into the
|
|
20
|
+
* problems list, one entry per branch on a different line.
|
|
21
|
+
*
|
|
22
|
+
* Selection is made from the ERRORS ALONE, never from the schema. A branch
|
|
23
|
+
* whose discriminating key is present emits no complaint at the union's own
|
|
24
|
+
* instancePath; one whose key is absent says `required`, and one that forbids a
|
|
25
|
+
* key the value carries says `additionalProperties`. That is the whole signal,
|
|
26
|
+
* and reading it off the errors is what lets reduction work across a `$ref`
|
|
27
|
+
* into another registered schema, where navigating to the branch subschema
|
|
28
|
+
* would mean re-implementing AJV's resolution.
|
|
29
|
+
*/
|
|
30
|
+
const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
|
|
31
|
+
/** Keywords a branch raises at the union's OWN instancePath when the value is
|
|
32
|
+
* not of that branch's shape at all — as opposed to being that shape and wrong
|
|
33
|
+
* further in. These are what make a branch implausible. */
|
|
34
|
+
const SHAPE_KEYWORDS = new Set(["required", "type", "additionalProperties", "enum", "const"]);
|
|
35
|
+
/* ------------------------------------------------------------------ prose */
|
|
36
|
+
export function formatSingleError(err) {
|
|
37
|
+
const p = err.instancePath || "/";
|
|
38
|
+
const params = err.params ?? {};
|
|
39
|
+
switch (err.keyword) {
|
|
40
|
+
case "additionalProperties":
|
|
41
|
+
return `${p} must NOT have additional properties ('${params.additionalProperty}' is not allowed)`;
|
|
42
|
+
case "required":
|
|
43
|
+
return `${p} is missing required property '${params.missingProperty}'`;
|
|
44
|
+
case "enum":
|
|
45
|
+
return `${p} ${err.message ?? "is invalid"} (${params.allowedValues?.join(" | ")})`;
|
|
46
|
+
case "type":
|
|
47
|
+
return `${p} must be ${params.type}${describeActual(err)}`;
|
|
48
|
+
default:
|
|
49
|
+
return `${p} ${err.message ?? "is invalid"}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** ` (got string)`, or nothing when the value is not in hand. AJV carries
|
|
53
|
+
* `data` only under `verbose`, and a reducer that navigated the root value
|
|
54
|
+
* would have to be given it at every call site; an absent actual type is worth
|
|
55
|
+
* less than a wrong one. */
|
|
56
|
+
function describeActual(err) {
|
|
57
|
+
if (!("data" in err))
|
|
58
|
+
return "";
|
|
59
|
+
const d = err.data;
|
|
60
|
+
if (d === null)
|
|
61
|
+
return " (got null)";
|
|
62
|
+
if (Array.isArray(d))
|
|
63
|
+
return " (got array)";
|
|
64
|
+
return ` (got ${typeof d})`;
|
|
65
|
+
}
|
|
66
|
+
/* -------------------------------------------------------------- reduction */
|
|
67
|
+
/** The branch index a `schemaPath` sits under, for a union whose own schemaPath
|
|
68
|
+
* is `unionPath` (`…/anyOf`): a child is `…/anyOf/<i>/…` and nothing else can
|
|
69
|
+
* collide with it. */
|
|
70
|
+
function branchIndexUnder(unionPath, schemaPath) {
|
|
71
|
+
if (!schemaPath || !schemaPath.startsWith(unionPath + "/"))
|
|
72
|
+
return undefined;
|
|
73
|
+
const rest = schemaPath.slice(unionPath.length + 1);
|
|
74
|
+
const slash = rest.indexOf("/");
|
|
75
|
+
const head = slash === -1 ? rest : rest.slice(0, slash);
|
|
76
|
+
const index = Number(head);
|
|
77
|
+
return Number.isInteger(index) ? index : undefined;
|
|
78
|
+
}
|
|
79
|
+
function instanceDepth(path) {
|
|
80
|
+
if (!path)
|
|
81
|
+
return 0;
|
|
82
|
+
return path.split("/").filter((s) => s !== "").length;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* How the alternatives at a union node are described, one phrase each.
|
|
86
|
+
*
|
|
87
|
+
* Read off the complaints made at the union's own node, and deliberately ONE
|
|
88
|
+
* PHRASE PER MISSING KEY rather than one per error group. AJV inlines most
|
|
89
|
+
* `$ref` branches and reports them all under the same bare `schemaPath`, so
|
|
90
|
+
* several branches are genuinely indistinguishable in the error set — joining
|
|
91
|
+
* their keys into a single phrase would read as one alternative demanding all
|
|
92
|
+
* of them, which is a claim about the schema that is simply false. Listing them
|
|
93
|
+
* separately under-specifies a branch that requires two keys at once, and each
|
|
94
|
+
* clause is still a true necessary condition; asserting a conjunction that does
|
|
95
|
+
* not exist is not.
|
|
96
|
+
*/
|
|
97
|
+
function describeAlternatives(errors, unionInstancePath) {
|
|
98
|
+
const own = errors.filter((e) => (e.instancePath || "") === unionInstancePath);
|
|
99
|
+
const phrases = [];
|
|
100
|
+
for (const e of own) {
|
|
101
|
+
if (e.keyword === "required")
|
|
102
|
+
phrases.push(`one with '${e.params?.missingProperty}'`);
|
|
103
|
+
else if (e.keyword === "type")
|
|
104
|
+
phrases.push(`a ${e.params?.type}`);
|
|
105
|
+
else if (e.keyword === "enum") {
|
|
106
|
+
phrases.push(`one of ${e.params?.allowedValues?.join(" | ")}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return phrases.length > 0 ? phrases : ["another shape"];
|
|
110
|
+
}
|
|
111
|
+
/** Is this branch a plausible reading of the value — does it accept the value's
|
|
112
|
+
* shape at the union node itself, and only disagree further in? */
|
|
113
|
+
function isPlausible(errors, unionInstancePath) {
|
|
114
|
+
return !errors.some((e) => (e.instancePath || "") === unionInstancePath && SHAPE_KEYWORDS.has(e.keyword ?? ""));
|
|
115
|
+
}
|
|
116
|
+
function isUnder(child, parent) {
|
|
117
|
+
return parent === "" ? child !== "" : child.startsWith(parent + "/");
|
|
118
|
+
}
|
|
119
|
+
/** The value path one level up, or undefined at the root. `""` is the root, so
|
|
120
|
+
* a non-empty path with no separator has the root as its parent. */
|
|
121
|
+
function parentPath(path) {
|
|
122
|
+
if (path === "")
|
|
123
|
+
return undefined;
|
|
124
|
+
const cut = path.lastIndexOf("/");
|
|
125
|
+
return cut <= 0 ? "" : path.slice(0, cut);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Replace each failing union with the errors of the branch the author plainly
|
|
129
|
+
* meant, recursively, outside in.
|
|
130
|
+
*
|
|
131
|
+
* Attribution runs to the DEEPEST occurrence that could own an error, which is
|
|
132
|
+
* what keeps a container's own complaint apart from its child's when both carry
|
|
133
|
+
* the same `schemaPath`. An occurrence reached through a branch becomes a
|
|
134
|
+
* candidate branch of its own — it raised nothing at the parent's node, so it is
|
|
135
|
+
* plausible exactly when the value really did take that shape and fail further
|
|
136
|
+
* in, and reducing it recursively is what stops an inner union's alternatives
|
|
137
|
+
* from surviving inside the outer one's selection.
|
|
138
|
+
*/
|
|
139
|
+
export function reduceSchemaErrors(errors) {
|
|
140
|
+
if (!errors || errors.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const occurrences = errors
|
|
143
|
+
.filter((e) => UNION_KEYWORDS.has(e.keyword ?? "") && typeof e.schemaPath === "string")
|
|
144
|
+
.map((e) => ({
|
|
145
|
+
error: e,
|
|
146
|
+
schemaPath: e.schemaPath,
|
|
147
|
+
instancePath: e.instancePath || "",
|
|
148
|
+
owned: [],
|
|
149
|
+
children: [],
|
|
150
|
+
}));
|
|
151
|
+
if (occurrences.length === 0)
|
|
152
|
+
return errors;
|
|
153
|
+
// The VALUE NODE is the claim, not the schemaPath. A branch written as a
|
|
154
|
+
// `$ref` is reported by AJV under the TARGET's schemaPath — and AJV inlines
|
|
155
|
+
// some of them, reporting several branches under one identical path — so
|
|
156
|
+
// nothing in such an error points back at the union that dispatched to it. A
|
|
157
|
+
// large union is written exactly that way, a branch per `$defs` entry, so
|
|
158
|
+
// claiming by schemaPath alone would leave the biggest unions unreduced.
|
|
159
|
+
//
|
|
160
|
+
// Indexed by instancePath rather than scanned: an error is claimed by the
|
|
161
|
+
// DEEPEST occurrence enclosing it, which is found by walking that error's own
|
|
162
|
+
// path upwards — bounded by the path's depth instead of by the number of
|
|
163
|
+
// unions. The scan this replaced was O(errors × occurrences), and both grow
|
|
164
|
+
// with nesting depth on a recursive shape, on a path the editor runs per
|
|
165
|
+
// keystroke.
|
|
166
|
+
const byPath = new Map();
|
|
167
|
+
const isOccurrence = new Set();
|
|
168
|
+
for (const o of occurrences) {
|
|
169
|
+
isOccurrence.add(o.error);
|
|
170
|
+
// Several unions can occur at ONE value node (a union inside a union
|
|
171
|
+
// branch); the first is kept, and the rest nest under it below.
|
|
172
|
+
if (!byPath.has(o.instancePath))
|
|
173
|
+
byPath.set(o.instancePath, o);
|
|
174
|
+
}
|
|
175
|
+
/** The nearest occurrence at or above `path`, excluding `path` itself when
|
|
176
|
+
* `strict` — which is how an occurrence finds its parent rather than itself. */
|
|
177
|
+
const enclosing = (path, strict) => {
|
|
178
|
+
let current = strict ? parentPath(path) : path;
|
|
179
|
+
while (current !== undefined) {
|
|
180
|
+
const hit = byPath.get(current);
|
|
181
|
+
if (hit)
|
|
182
|
+
return hit;
|
|
183
|
+
current = parentPath(current);
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
};
|
|
187
|
+
const owner = new Map();
|
|
188
|
+
for (const err of errors) {
|
|
189
|
+
if (isOccurrence.has(err))
|
|
190
|
+
continue;
|
|
191
|
+
const best = enclosing(err.instancePath || "", false);
|
|
192
|
+
if (best) {
|
|
193
|
+
best.owned.push(err);
|
|
194
|
+
owner.set(err, best);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Nest occurrences the same way: an occurrence deeper in the value was reached
|
|
198
|
+
// through some branch of the nearest one enclosing it.
|
|
199
|
+
const roots = [];
|
|
200
|
+
for (const o of occurrences) {
|
|
201
|
+
const parent = o === byPath.get(o.instancePath)
|
|
202
|
+
? enclosing(o.instancePath, true)
|
|
203
|
+
: byPath.get(o.instancePath);
|
|
204
|
+
if (parent && parent !== o)
|
|
205
|
+
parent.children.push(o);
|
|
206
|
+
else
|
|
207
|
+
roots.push(o);
|
|
208
|
+
}
|
|
209
|
+
const replaced = new Map();
|
|
210
|
+
for (const root of roots)
|
|
211
|
+
replaced.set(root.error, resolveOccurrence(root));
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const err of errors) {
|
|
214
|
+
const replacement = replaced.get(err);
|
|
215
|
+
if (replacement) {
|
|
216
|
+
out.push(...replacement);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// Everything an occurrence owns is spoken for by whichever branch survived,
|
|
220
|
+
// and a nested occurrence is carried inside its parent's selection.
|
|
221
|
+
if (owner.has(err))
|
|
222
|
+
continue;
|
|
223
|
+
if (occurrences.some((o) => o.error === err))
|
|
224
|
+
continue;
|
|
225
|
+
out.push(err);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
/** Groups one union's complaints into candidate readings of the value.
|
|
230
|
+
*
|
|
231
|
+
* The branch INDEX is used wherever the error carries it. It does not when the
|
|
232
|
+
* branch is a `$ref` — AJV reports under the target's schemaPath — so the
|
|
233
|
+
* fallback groups by the VALUE NODE each complaint is about: everything said
|
|
234
|
+
* about the union node itself is one candidate (those are the branches that
|
|
235
|
+
* rejected the value's shape outright), and each child node complained about is
|
|
236
|
+
* its own. That is the same question asked of the data instead of the schema,
|
|
237
|
+
* and it is what the ordering below actually reads. */
|
|
238
|
+
function groupCandidates(occurrence) {
|
|
239
|
+
const byIndex = new Map();
|
|
240
|
+
const byNode = new Map();
|
|
241
|
+
for (const err of occurrence.owned) {
|
|
242
|
+
const index = branchIndexUnder(occurrence.schemaPath, err.schemaPath);
|
|
243
|
+
const bucket = index === undefined
|
|
244
|
+
? mapBucket(byNode, childSegment(err.instancePath || "", occurrence.instancePath))
|
|
245
|
+
: mapBucket(byIndex, index);
|
|
246
|
+
bucket.push(err);
|
|
247
|
+
}
|
|
248
|
+
return [...byIndex]
|
|
249
|
+
.map(([index, errs]) => ({ index, errors: errs, nested: false }))
|
|
250
|
+
.concat([...byNode].map(([, errs]) => ({ index: Number.MAX_SAFE_INTEGER, errors: errs, nested: false })))
|
|
251
|
+
.concat(occurrence.children.map((child) => ({
|
|
252
|
+
index: Number.POSITIVE_INFINITY,
|
|
253
|
+
errors: resolveOccurrence(child),
|
|
254
|
+
nested: true,
|
|
255
|
+
})));
|
|
256
|
+
}
|
|
257
|
+
function mapBucket(map, key) {
|
|
258
|
+
const existing = map.get(key);
|
|
259
|
+
if (existing)
|
|
260
|
+
return existing;
|
|
261
|
+
const created = [];
|
|
262
|
+
map.set(key, created);
|
|
263
|
+
return created;
|
|
264
|
+
}
|
|
265
|
+
/** The first value-path segment below `parent`, or "" for the node itself. */
|
|
266
|
+
function childSegment(instancePath, parent) {
|
|
267
|
+
if (!isUnder(instancePath, parent))
|
|
268
|
+
return "";
|
|
269
|
+
const rest = instancePath.slice(parent.length + 1);
|
|
270
|
+
const slash = rest.indexOf("/");
|
|
271
|
+
return slash === -1 ? rest : rest.slice(0, slash);
|
|
272
|
+
}
|
|
273
|
+
function resolveOccurrence(occurrence) {
|
|
274
|
+
const candidates = groupCandidates(occurrence);
|
|
275
|
+
// `oneOf` matching SEVERAL branches emits the union error with no branch
|
|
276
|
+
// errors at all — nothing was rejected, so there is no branch to select.
|
|
277
|
+
if (candidates.length === 0)
|
|
278
|
+
return [occurrence.error];
|
|
279
|
+
const plausible = candidates.filter((c) => c.nested || isPlausible(c.errors, occurrence.instancePath));
|
|
280
|
+
if (plausible.length === 0) {
|
|
281
|
+
return [alternativesError(candidates, occurrence)];
|
|
282
|
+
}
|
|
283
|
+
// Deepest agreement first — a branch that matched further into the value is
|
|
284
|
+
// the one the author was writing — then the fewest complaints, then the
|
|
285
|
+
// declaration order, so the choice is stable.
|
|
286
|
+
plausible.sort((a, b) => {
|
|
287
|
+
const depth = maxDepth(b.errors) - maxDepth(a.errors);
|
|
288
|
+
if (depth !== 0)
|
|
289
|
+
return depth;
|
|
290
|
+
if (a.errors.length !== b.errors.length)
|
|
291
|
+
return a.errors.length - b.errors.length;
|
|
292
|
+
return a.index - b.index;
|
|
293
|
+
});
|
|
294
|
+
const winner = plausible[0];
|
|
295
|
+
return winner.nested ? winner.errors : reduceSchemaErrors(winner.errors);
|
|
296
|
+
}
|
|
297
|
+
function maxDepth(errors) {
|
|
298
|
+
let max = 0;
|
|
299
|
+
for (const e of errors)
|
|
300
|
+
max = Math.max(max, instanceDepth(e.instancePath));
|
|
301
|
+
return max;
|
|
302
|
+
}
|
|
303
|
+
/** One error anchored at the union node, listing what could have gone there.
|
|
304
|
+
* The honest fallback: a confident wrong message is worse than the
|
|
305
|
+
* concatenation this replaces, so when no branch is a plausible reading the
|
|
306
|
+
* reader is told what the alternatives are rather than shown one branch's
|
|
307
|
+
* complaints as if it were the intended one. */
|
|
308
|
+
function alternativesError(candidates, occurrence) {
|
|
309
|
+
const seen = new Set();
|
|
310
|
+
const described = [];
|
|
311
|
+
for (const text of describeAlternatives(candidates.flatMap((c) => c.errors), occurrence.instancePath)) {
|
|
312
|
+
if (seen.has(text))
|
|
313
|
+
continue;
|
|
314
|
+
seen.add(text);
|
|
315
|
+
described.push(text);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...occurrence.error,
|
|
319
|
+
instancePath: occurrence.instancePath,
|
|
320
|
+
message: `matches no alternative — expected ${described.join(", or ")}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/* --------------------------------------------------------------- rendering */
|
|
324
|
+
/** Converts an AJV error to a dotted path compatible with PositionIndex keys.
|
|
325
|
+
* e.g. instancePath "/config/routes/0/handler" → "config.routes[0].handler"
|
|
326
|
+
* For "required" keyword errors, appends the missing property to the parent path. */
|
|
327
|
+
export function ajvErrorToPath(err) {
|
|
328
|
+
const instancePath = err.instancePath ?? "";
|
|
329
|
+
const parts = instancePath.split("/").filter((p) => p !== "");
|
|
330
|
+
let result = "";
|
|
331
|
+
for (const part of parts) {
|
|
332
|
+
if (/^\d+$/.test(part))
|
|
333
|
+
result += `[${part}]`;
|
|
334
|
+
else
|
|
335
|
+
result += result ? `.${part}` : part;
|
|
336
|
+
}
|
|
337
|
+
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
338
|
+
const missing = err.params.missingProperty;
|
|
339
|
+
result += result ? `.${missing}` : missing;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
/** Reduced, path-anchored issues — what a diagnostic list is built from. */
|
|
344
|
+
export function schemaIssues(errors) {
|
|
345
|
+
return reduceSchemaErrors(errors).map((err) => ({
|
|
346
|
+
message: formatSingleError(err),
|
|
347
|
+
path: ajvErrorToPath(err),
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
/** Reduced, rendered as one sentence — what a thrown runtime error carries. */
|
|
351
|
+
export function formatAjvErrors(errors) {
|
|
352
|
+
const reduced = reduceSchemaErrors(errors);
|
|
353
|
+
if (reduced.length === 0)
|
|
354
|
+
return "Unknown schema error";
|
|
355
|
+
return reduced.map(formatSingleError).join("; ");
|
|
356
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
2
|
+
* Non-refs and unresolved refs pass through unchanged. */
|
|
3
|
+
export declare function resolveLocalRef(schema: Record<string, any> | undefined, root: Record<string, any>): Record<string, any> | undefined;
|
|
4
|
+
/** Gather property schemas from a (possibly variant-bearing) object schema:
|
|
5
|
+
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
|
|
6
|
+
*
|
|
7
|
+
* Each branch is resolved through {@link resolveLocalRef} first, so a branch
|
|
8
|
+
* that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
|
|
9
|
+
* site — contributes its properties like an inline one. Without that, pointing a
|
|
10
|
+
* composer at a shared shape would silently empty every role-driven lookup that
|
|
11
|
+
* reads this (the inputs slot, the retry policy, the eval paths), which is a
|
|
12
|
+
* failure with no diagnostic attached to it. */
|
|
13
|
+
export declare function gatherPropertySchemas(schema: Record<string, any>, root?: Record<string, any>): Array<[string, Record<string, any>]>;
|
|
14
|
+
/**
|
|
15
|
+
* Generic, role-driven walk over a step array. Calls
|
|
16
|
+
* `visit(step, stepPath)` for every step — top-level and nested through the
|
|
17
|
+
* `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
|
|
18
|
+
* the single definition of how steps nest, shared by `buildStepContextSchema`
|
|
19
|
+
* (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
|
|
20
|
+
* checks invoke refs), so the topology contract lives in one place — adding a
|
|
21
|
+
* role or nesting form updates both consumers at once. No resource kind is
|
|
22
|
+
* hardcoded; recursion is driven entirely by the schema annotations.
|
|
23
|
+
*/
|
|
24
|
+
export declare function walkStepArray(steps: unknown[], stepItemSchema: Record<string, any> | undefined, rootSchema: Record<string, any>, basePath: string, visit: (step: Record<string, any>, stepPath: string) => void): void;
|
|
25
|
+
//# sourceMappingURL=schema-walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-walk.d.ts","sourceRoot":"","sources":["../src/schema-walk.ts"],"names":[],"mappings":"AAYA;2DAC2D;AAC3D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EACvC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACxB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAqBjC;AAID;;;;;;;;iDAQiD;AACjD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAsBtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EAAE,EAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAC3D,IAAI,CAiDN"}
|