@telorun/analyzer 0.41.1 → 0.43.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 +8 -2
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +0 -1
- package/dist/manifest-visitor.d.ts +7 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +31 -5
- package/dist/reference-field-map.js +1 -1
- package/dist/validate-nested-inline.d.ts +3 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +14 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +16 -0
- package/dist/validate-value-schema.d.ts +13 -0
- package/dist/validate-value-schema.d.ts.map +1 -0
- package/dist/validate-value-schema.js +114 -0
- package/package.json +2 -2
- package/src/analyzer.ts +12 -1
- package/src/builtins.ts +0 -1
- package/src/manifest-visitor.ts +35 -4
- package/src/reference-field-map.ts +1 -1
- package/src/validate-nested-inline.ts +13 -1
- package/src/validate-references.ts +16 -0
- package/src/validate-value-schema.ts +126 -0
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAqB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA8xB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAw2BvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAmBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
|
|
|
16
16
|
import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
|
|
17
17
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
18
18
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
19
|
+
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
19
20
|
import { DiagnosticSeverity } from "./types.js";
|
|
20
21
|
import { extractCelRegionScopes, extractContextsFromSchema, getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
|
|
21
22
|
import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
|
|
@@ -1143,7 +1144,12 @@ export class StaticAnalyzer {
|
|
|
1143
1144
|
const celIssues = collectCelTypeIssues(m, schema, "", definition, m, baseTypedEnv, this.celEnv, moduleManifest);
|
|
1144
1145
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
1145
1146
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
1146
|
-
|
|
1147
|
+
// Phase 4: value slots that must satisfy a type declared elsewhere on
|
|
1148
|
+
// the resource (`x-telo-value-schema-from`) — e.g. every row of a
|
|
1149
|
+
// decision table against its declared `outputType`, so a mistyped branch
|
|
1150
|
+
// is caught here rather than on the one input that selects it.
|
|
1151
|
+
const valueSchemaIssues = collectValueSchemaIssues(m, schema, allManifests);
|
|
1152
|
+
const issues = [...celIssues, ...ajvIssues, ...valueSchemaIssues];
|
|
1147
1153
|
for (const issue of issues) {
|
|
1148
1154
|
diagnostics.push({
|
|
1149
1155
|
severity: DiagnosticSeverity.Error,
|
|
@@ -1178,7 +1184,7 @@ export class StaticAnalyzer {
|
|
|
1178
1184
|
}
|
|
1179
1185
|
const viaRoot = aliases.resolveKind(kind);
|
|
1180
1186
|
return viaRoot ? defs.resolve(viaRoot) : undefined;
|
|
1181
|
-
}));
|
|
1187
|
+
}, allManifests));
|
|
1182
1188
|
}
|
|
1183
1189
|
// (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
|
|
1184
1190
|
}
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AA2GvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AA2GvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EA6nB/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -49,6 +49,13 @@ export interface ScopeBoundaryEvent {
|
|
|
49
49
|
/** Names of every resource declared inside this resource's scopes. Used by
|
|
50
50
|
* the dependency graph to drop boot edges to scoped (on-demand) targets. */
|
|
51
51
|
enclosedNames: Set<string>;
|
|
52
|
+
/** Scope entries that are references (`!ref`) rather than inline resource
|
|
53
|
+
* definitions — a category error, since a scope declares its own resources.
|
|
54
|
+
* Each carries the concrete path (e.g. `with[0]`) for diagnostics. */
|
|
55
|
+
scopeRefEntries: {
|
|
56
|
+
path: string;
|
|
57
|
+
refName: string;
|
|
58
|
+
}[];
|
|
52
59
|
}
|
|
53
60
|
export interface RefSiteEvent {
|
|
54
61
|
source: ResourceManifest;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-visitor.d.ts","sourceRoot":"","sources":["../src/manifest-visitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,wEAAwE;IACxE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpD;iFAC6E;IAC7E,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest-visitor.d.ts","sourceRoot":"","sources":["../src/manifest-visitor.ts"],"names":[],"mappings":"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"}
|
package/dist/manifest-visitor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRefSentinel, isTaggedSentinel, walkCelExpressions } from "@telorun/templating";
|
|
2
|
-
import { isRefEntry, isSchemaFromEntry, isScopeEntry, resolveFieldEntries,
|
|
2
|
+
import { isRefEntry, isSchemaFromEntry, isScopeEntry, resolveFieldEntries, } from "./reference-field-map.js";
|
|
3
3
|
import { extractContextsFromSchema, pathMatchesScope } from "./validate-cel-context.js";
|
|
4
4
|
/** Synthetic entry for a value-tree-discovered ref — these carry no declared
|
|
5
5
|
* x-telo-ref constraint. */
|
|
@@ -77,12 +77,32 @@ export function visitManifest(resources, registry, visitor, options = {}) {
|
|
|
77
77
|
: baseMap;
|
|
78
78
|
if (refScopeMap && (wantsRefs || wantsScope)) {
|
|
79
79
|
const manifestsByPointer = new Map();
|
|
80
|
+
const scopeRefEntries = [];
|
|
80
81
|
for (const [fieldPath, entry] of refScopeMap) {
|
|
81
82
|
if (!isScopeEntry(entry))
|
|
82
83
|
continue;
|
|
83
|
-
const raw =
|
|
84
|
-
|
|
85
|
-
.
|
|
84
|
+
const raw = [];
|
|
85
|
+
for (const fe of resolveFieldEntries(r, fieldPath)) {
|
|
86
|
+
const items = Array.isArray(fe.value) ? fe.value : [fe.value];
|
|
87
|
+
items.forEach((v, i) => {
|
|
88
|
+
if (!v || typeof v !== "object")
|
|
89
|
+
return;
|
|
90
|
+
// A scope entry must be an inline resource definition; a `!ref`
|
|
91
|
+
// (tagged sentinel or resolved `{kind, name}`) is not — record it
|
|
92
|
+
// so a static diagnostic flags it instead of registering a
|
|
93
|
+
// config-less manifest.
|
|
94
|
+
const rec = v;
|
|
95
|
+
if (isRefSentinel(v) ||
|
|
96
|
+
(typeof rec.kind === "string" && typeof rec.name === "string")) {
|
|
97
|
+
scopeRefEntries.push({
|
|
98
|
+
path: Array.isArray(fe.value) ? `${fe.path}[${i}]` : fe.path,
|
|
99
|
+
refName: isRefSentinel(v) ? v.source : String(rec.name),
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
raw.push(v);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
86
106
|
const pointers = Array.isArray(entry.scope) ? entry.scope : [entry.scope];
|
|
87
107
|
for (const pointer of pointers)
|
|
88
108
|
manifestsByPointer.set(pointer, raw);
|
|
@@ -97,7 +117,13 @@ export function visitManifest(resources, registry, visitor, options = {}) {
|
|
|
97
117
|
enclosedNames.add(name);
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
|
-
visitor.onScope({
|
|
120
|
+
visitor.onScope({
|
|
121
|
+
source: r,
|
|
122
|
+
scopePrefixes,
|
|
123
|
+
manifestsByPointer,
|
|
124
|
+
enclosedNames,
|
|
125
|
+
scopeRefEntries,
|
|
126
|
+
});
|
|
101
127
|
}
|
|
102
128
|
if (wantsRefs) {
|
|
103
129
|
for (const [fieldPath, entry] of refScopeMap) {
|
|
@@ -142,7 +142,7 @@ function traverseNode(node, path, map, root, visitedRefs = new Set()) {
|
|
|
142
142
|
// the kernel's `runInvoke` emit-Invoked path. The walker fix and the
|
|
143
143
|
// dispatcher fix need to land together — see the follow-up in
|
|
144
144
|
// [kernel/nodejs/plans/reference-syntax-unification.md] and the
|
|
145
|
-
// stopgap in `resource-context.ts:
|
|
145
|
+
// stopgap in `resource-context.ts:ensureKindRef`. `visitedRefs`
|
|
146
146
|
// stays as a parameter so the recursive calls below thread the right
|
|
147
147
|
// signature; turning the descent back on is a single-branch change.
|
|
148
148
|
if (typeof node?.$ref === "string")
|
|
@@ -26,5 +26,7 @@ export interface InlineDefinitionLookup {
|
|
|
26
26
|
* `rewriteSyntheticOrigins` reroutes those on inline-extracted (synthetic)
|
|
27
27
|
* manifests back to the root doc.
|
|
28
28
|
*/
|
|
29
|
-
export declare function validateNestedInlineResources(manifest: ResourceManifest, rootSchema: Record<string, any>, lookupDefinition: InlineDefinitionLookup
|
|
29
|
+
export declare function validateNestedInlineResources(manifest: ResourceManifest, rootSchema: Record<string, any>, lookupDefinition: InlineDefinitionLookup,
|
|
30
|
+
/** Needed to resolve a `telo#Type` field a value slot is validated against. */
|
|
31
|
+
allManifests?: Record<string, any>[]): AnalysisDiagnostic[];
|
|
30
32
|
//# sourceMappingURL=validate-nested-inline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKzE,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,gBAAgB,EAAE,sBAAsB;AACxC,+EAA+E;AAC/E,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAO,GACvC,kBAAkB,EAAE,CA6HtB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectRefs, isInlineResource } from "./reference-field-map.js";
|
|
2
2
|
import { collectProperties, resolveRef, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
3
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
|
+
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
4
5
|
const SOURCE = "telo-analyzer";
|
|
5
6
|
/**
|
|
6
7
|
* Validates inline resources nested inside a resource body against their kind's
|
|
@@ -22,7 +23,9 @@ const SOURCE = "telo-analyzer";
|
|
|
22
23
|
* `rewriteSyntheticOrigins` reroutes those on inline-extracted (synthetic)
|
|
23
24
|
* manifests back to the root doc.
|
|
24
25
|
*/
|
|
25
|
-
export function validateNestedInlineResources(manifest, rootSchema, lookupDefinition
|
|
26
|
+
export function validateNestedInlineResources(manifest, rootSchema, lookupDefinition,
|
|
27
|
+
/** Needed to resolve a `telo#Type` field a value slot is validated against. */
|
|
28
|
+
allManifests = []) {
|
|
26
29
|
const diagnostics = [];
|
|
27
30
|
const resource = { kind: manifest.kind, name: manifest.metadata?.name };
|
|
28
31
|
const filePath = manifest.metadata?.source;
|
|
@@ -68,7 +71,16 @@ export function validateNestedInlineResources(manifest, rootSchema, lookupDefini
|
|
|
68
71
|
: {};
|
|
69
72
|
const data = { ...inline, metadata: { name: "__inline__", ...existingMeta } };
|
|
70
73
|
const substituted = substituteCelFields(data, effectiveSchema, effectiveSchema);
|
|
71
|
-
|
|
74
|
+
// The same two passes the top-level resource loop runs, so a kind's
|
|
75
|
+
// guarantees don't depend on whether the author wrote it standalone or
|
|
76
|
+
// inline (under a step's `invoke:`, or in a `with:` scope). `data` carries
|
|
77
|
+
// the synthesized metadata; `x-telo-value-schema-from` reads sibling fields
|
|
78
|
+
// off the resource, which are present either way.
|
|
79
|
+
const inlineIssues = [
|
|
80
|
+
...validateAgainstSchema(substituted, effectiveSchema),
|
|
81
|
+
...collectValueSchemaIssues(data, schema, allManifests),
|
|
82
|
+
];
|
|
83
|
+
for (const issue of inlineIssues) {
|
|
72
84
|
diagnostics.push({
|
|
73
85
|
severity: DiagnosticSeverity.Error,
|
|
74
86
|
code: "SCHEMA_VIOLATION",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA6dtB"}
|
|
@@ -341,6 +341,22 @@ export function validateReferences(resources, context) {
|
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
343
|
},
|
|
344
|
+
onScope: (e) => {
|
|
345
|
+
const r = e.source;
|
|
346
|
+
const resourceLabel = `${r.kind}/${r.metadata.name}`;
|
|
347
|
+
const resourceData = { kind: r.kind, name: r.metadata.name };
|
|
348
|
+
const filePath = r.metadata?.source;
|
|
349
|
+
for (const { path: concretePath, refName } of e.scopeRefEntries) {
|
|
350
|
+
const scopeField = concretePath.split(/[.[]/)[0];
|
|
351
|
+
diagnostics.push({
|
|
352
|
+
severity: DiagnosticSeverity.Error,
|
|
353
|
+
code: "SCOPE_ENTRY_NOT_INLINE",
|
|
354
|
+
source: SOURCE,
|
|
355
|
+
message: `${resourceLabel}: scope entry at '${concretePath}' is a reference (\`!ref ${refName}\`), but a scope declares inline resource definitions (a \`kind:\` with its config). Declare the resource inline under '${scopeField}:', or reference an outer resource from a sibling field such as 'targets:'.`,
|
|
356
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
},
|
|
344
360
|
}, { aliases, aliasesByModule, skipKinds: SYSTEM_KINDS, expand: true });
|
|
345
361
|
// Phase 3b — x-telo-schema-from validation.
|
|
346
362
|
// For each field with a schemaFrom path expression, resolve the anchor ref to get the
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SchemaIssue } from "./schema-compat.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate every `x-telo-value-schema-from` slot in one resource.
|
|
4
|
+
*
|
|
5
|
+
* CEL leaves are replaced with schema-shaped placeholders before AJV runs
|
|
6
|
+
* (`substituteCelFields`), so an expression is accepted wherever its slot's
|
|
7
|
+
* declared type would be — a static pass cannot know what it evaluates to. What
|
|
8
|
+
* this DOES catch is structural disagreement that no runtime value can fix: a
|
|
9
|
+
* missing required property, an unknown property under
|
|
10
|
+
* `additionalProperties: false`, or a literal of the wrong type.
|
|
11
|
+
*/
|
|
12
|
+
export declare function collectValueSchemaIssues(manifest: Record<string, any>, defSchema: Record<string, any> | undefined, allManifests: Record<string, any>[]): SchemaIssue[];
|
|
13
|
+
//# sourceMappingURL=validate-value-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-value-schema.d.ts","sourceRoot":"","sources":["../src/validate-value-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA2FlG;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC1C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,WAAW,EAAE,CAoBf"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { substituteCelFields, validateAgainstSchema } from "./schema-compat.js";
|
|
2
|
+
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* `x-telo-value-schema-from: "<field>"` — the value written at the annotated
|
|
5
|
+
* node must satisfy the type declared at the resource's `<field>`.
|
|
6
|
+
*
|
|
7
|
+
* The motivating shape is a kind with ONE declared output contract and SEVERAL
|
|
8
|
+
* places that must each produce it — a decision table's rows, a switch's arms.
|
|
9
|
+
* Only the branch that wins at runtime gets checked there, so a mistyped branch
|
|
10
|
+
* ships and fails on the one input that selects it. This annotation checks every
|
|
11
|
+
* branch statically instead.
|
|
12
|
+
*
|
|
13
|
+
* Generic and topology-driven: the analyzer hardcodes no kind. Any definition
|
|
14
|
+
* with a `telo#Type` field and value-producing slots opts in by annotating those
|
|
15
|
+
* slots. `<field>` is resolved with the same `telo#Type` semantics as
|
|
16
|
+
* `inputType` / `outputType` everywhere else, so an inline
|
|
17
|
+
* `{ kind: Type.JsonSchema, schema: … }` and a named type reference both work.
|
|
18
|
+
*
|
|
19
|
+
* A field that resolves to no schema — the common case of an optional
|
|
20
|
+
* `outputType` left undeclared — is skipped, not reported: declaring the
|
|
21
|
+
* contract is what opts into the check.
|
|
22
|
+
*/
|
|
23
|
+
const ANNOTATION = "x-telo-value-schema-from";
|
|
24
|
+
/** Walk a definition schema collecting every `x-telo-value-schema-from`, in the
|
|
25
|
+
* same scope form the CEL-context walker produces. */
|
|
26
|
+
function collectAnnotations(schema, path) {
|
|
27
|
+
if (!schema || typeof schema !== "object")
|
|
28
|
+
return [];
|
|
29
|
+
const out = [];
|
|
30
|
+
const from = schema[ANNOTATION];
|
|
31
|
+
if (typeof from === "string" && from.length > 0)
|
|
32
|
+
out.push({ scope: path, from });
|
|
33
|
+
if (schema.properties) {
|
|
34
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
35
|
+
out.push(...collectAnnotations(value, `${path}.${key}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (schema.items && typeof schema.items === "object") {
|
|
39
|
+
out.push(...collectAnnotations(schema.items, `${path}[*]`));
|
|
40
|
+
}
|
|
41
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
42
|
+
if (Array.isArray(schema[key])) {
|
|
43
|
+
for (const sub of schema[key])
|
|
44
|
+
out.push(...collectAnnotations(sub, path));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** Expand a scope into the concrete values present in this manifest, carrying
|
|
50
|
+
* each one's real path so a diagnostic points at the row the author wrote. */
|
|
51
|
+
function resolveScopeValues(manifest, scope) {
|
|
52
|
+
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
53
|
+
if (!stripped)
|
|
54
|
+
return [];
|
|
55
|
+
let frontier = [{ path: "", value: manifest }];
|
|
56
|
+
// Segments look like `choices[*]` or `default` — a name plus optional wildcard.
|
|
57
|
+
for (const segment of stripped.split(".")) {
|
|
58
|
+
const wildcard = segment.endsWith("[*]");
|
|
59
|
+
const name = wildcard ? segment.slice(0, -3) : segment;
|
|
60
|
+
const next = [];
|
|
61
|
+
for (const entry of frontier) {
|
|
62
|
+
const container = entry.value;
|
|
63
|
+
if (!container || typeof container !== "object")
|
|
64
|
+
continue;
|
|
65
|
+
const child = container[name];
|
|
66
|
+
if (child === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const childPath = entry.path ? `${entry.path}.${name}` : name;
|
|
69
|
+
if (!wildcard) {
|
|
70
|
+
next.push({ path: childPath, value: child });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(child))
|
|
74
|
+
continue;
|
|
75
|
+
child.forEach((item, i) => next.push({ path: `${childPath}[${i}]`, value: item }));
|
|
76
|
+
}
|
|
77
|
+
frontier = next;
|
|
78
|
+
if (frontier.length === 0)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return frontier;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate every `x-telo-value-schema-from` slot in one resource.
|
|
85
|
+
*
|
|
86
|
+
* CEL leaves are replaced with schema-shaped placeholders before AJV runs
|
|
87
|
+
* (`substituteCelFields`), so an expression is accepted wherever its slot's
|
|
88
|
+
* declared type would be — a static pass cannot know what it evaluates to. What
|
|
89
|
+
* this DOES catch is structural disagreement that no runtime value can fix: a
|
|
90
|
+
* missing required property, an unknown property under
|
|
91
|
+
* `additionalProperties: false`, or a literal of the wrong type.
|
|
92
|
+
*/
|
|
93
|
+
export function collectValueSchemaIssues(manifest, defSchema, allManifests) {
|
|
94
|
+
if (!defSchema)
|
|
95
|
+
return [];
|
|
96
|
+
const annotations = collectAnnotations(defSchema, "$");
|
|
97
|
+
if (annotations.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const issues = [];
|
|
100
|
+
for (const { scope, from } of annotations) {
|
|
101
|
+
const target = resolveTypeFieldToSchema(manifest[from], allManifests);
|
|
102
|
+
if (!target || typeof target !== "object")
|
|
103
|
+
continue;
|
|
104
|
+
for (const { path, value } of resolveScopeValues(manifest, scope)) {
|
|
105
|
+
for (const issue of validateAgainstSchema(substituteCelFields(value, target), target)) {
|
|
106
|
+
issues.push({
|
|
107
|
+
message: `\`${path}\` does not satisfy the type declared at \`${from}\`: ${issue.message}`,
|
|
108
|
+
path: issue.path ? `${path}.${issue.path}` : path,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return issues;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.54.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/analyzer.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
validateAgainstSchema,
|
|
30
30
|
type SchemaIssue,
|
|
31
31
|
} from "./schema-compat.js";
|
|
32
|
+
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
32
33
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
|
|
33
34
|
import {
|
|
34
35
|
extractCelRegionScopes,
|
|
@@ -1351,7 +1352,16 @@ export class StaticAnalyzer {
|
|
|
1351
1352
|
);
|
|
1352
1353
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
1353
1354
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
1354
|
-
|
|
1355
|
+
// Phase 4: value slots that must satisfy a type declared elsewhere on
|
|
1356
|
+
// the resource (`x-telo-value-schema-from`) — e.g. every row of a
|
|
1357
|
+
// decision table against its declared `outputType`, so a mistyped branch
|
|
1358
|
+
// is caught here rather than on the one input that selects it.
|
|
1359
|
+
const valueSchemaIssues = collectValueSchemaIssues(
|
|
1360
|
+
m as Record<string, any>,
|
|
1361
|
+
schema,
|
|
1362
|
+
allManifests as Record<string, any>[],
|
|
1363
|
+
);
|
|
1364
|
+
const issues = [...celIssues, ...ajvIssues, ...valueSchemaIssues];
|
|
1355
1365
|
for (const issue of issues) {
|
|
1356
1366
|
diagnostics.push({
|
|
1357
1367
|
severity: DiagnosticSeverity.Error,
|
|
@@ -1390,6 +1400,7 @@ export class StaticAnalyzer {
|
|
|
1390
1400
|
const viaRoot = aliases.resolveKind(kind);
|
|
1391
1401
|
return viaRoot ? defs.resolve(viaRoot) : undefined;
|
|
1392
1402
|
},
|
|
1403
|
+
allManifests as Record<string, any>[],
|
|
1393
1404
|
),
|
|
1394
1405
|
);
|
|
1395
1406
|
}
|
package/src/builtins.ts
CHANGED
package/src/manifest-visitor.ts
CHANGED
|
@@ -63,6 +63,10 @@ export interface ScopeBoundaryEvent {
|
|
|
63
63
|
/** Names of every resource declared inside this resource's scopes. Used by
|
|
64
64
|
* the dependency graph to drop boot edges to scoped (on-demand) targets. */
|
|
65
65
|
enclosedNames: Set<string>;
|
|
66
|
+
/** Scope entries that are references (`!ref`) rather than inline resource
|
|
67
|
+
* definitions — a category error, since a scope declares its own resources.
|
|
68
|
+
* Each carries the concrete path (e.g. `with[0]`) for diagnostics. */
|
|
69
|
+
scopeRefEntries: { path: string; refName: string }[];
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export interface RefSiteEvent {
|
|
@@ -235,11 +239,32 @@ export function visitManifest(
|
|
|
235
239
|
|
|
236
240
|
if (refScopeMap && (wantsRefs || wantsScope)) {
|
|
237
241
|
const manifestsByPointer = new Map<string, ResourceManifest[]>();
|
|
242
|
+
const scopeRefEntries: { path: string; refName: string }[] = [];
|
|
238
243
|
for (const [fieldPath, entry] of refScopeMap) {
|
|
239
244
|
if (!isScopeEntry(entry)) continue;
|
|
240
|
-
const raw =
|
|
241
|
-
|
|
242
|
-
.
|
|
245
|
+
const raw: ResourceManifest[] = [];
|
|
246
|
+
for (const fe of resolveFieldEntries(r, fieldPath)) {
|
|
247
|
+
const items = Array.isArray(fe.value) ? fe.value : [fe.value];
|
|
248
|
+
items.forEach((v, i) => {
|
|
249
|
+
if (!v || typeof v !== "object") return;
|
|
250
|
+
// A scope entry must be an inline resource definition; a `!ref`
|
|
251
|
+
// (tagged sentinel or resolved `{kind, name}`) is not — record it
|
|
252
|
+
// so a static diagnostic flags it instead of registering a
|
|
253
|
+
// config-less manifest.
|
|
254
|
+
const rec = v as Record<string, unknown>;
|
|
255
|
+
if (
|
|
256
|
+
isRefSentinel(v) ||
|
|
257
|
+
(typeof rec.kind === "string" && typeof rec.name === "string")
|
|
258
|
+
) {
|
|
259
|
+
scopeRefEntries.push({
|
|
260
|
+
path: Array.isArray(fe.value) ? `${fe.path}[${i}]` : fe.path,
|
|
261
|
+
refName: isRefSentinel(v) ? v.source : String(rec.name),
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
raw.push(v as ResourceManifest);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
243
268
|
const pointers = Array.isArray(entry.scope) ? entry.scope : [entry.scope];
|
|
244
269
|
for (const pointer of pointers) manifestsByPointer.set(pointer, raw);
|
|
245
270
|
}
|
|
@@ -253,7 +278,13 @@ export function visitManifest(
|
|
|
253
278
|
if (typeof name === "string") enclosedNames.add(name);
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
|
-
visitor.onScope!({
|
|
281
|
+
visitor.onScope!({
|
|
282
|
+
source: r,
|
|
283
|
+
scopePrefixes,
|
|
284
|
+
manifestsByPointer,
|
|
285
|
+
enclosedNames,
|
|
286
|
+
scopeRefEntries,
|
|
287
|
+
});
|
|
257
288
|
}
|
|
258
289
|
|
|
259
290
|
if (wantsRefs) {
|
|
@@ -210,7 +210,7 @@ function traverseNode(
|
|
|
210
210
|
// the kernel's `runInvoke` emit-Invoked path. The walker fix and the
|
|
211
211
|
// dispatcher fix need to land together — see the follow-up in
|
|
212
212
|
// [kernel/nodejs/plans/reference-syntax-unification.md] and the
|
|
213
|
-
// stopgap in `resource-context.ts:
|
|
213
|
+
// stopgap in `resource-context.ts:ensureKindRef`. `visitedRefs`
|
|
214
214
|
// stays as a parameter so the recursive calls below thread the right
|
|
215
215
|
// signature; turning the descent back on is a single-branch change.
|
|
216
216
|
if (typeof node?.$ref === "string") return;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
validateAgainstSchema,
|
|
8
8
|
} from "./schema-compat.js";
|
|
9
9
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
10
|
+
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
10
11
|
|
|
11
12
|
const SOURCE = "telo-analyzer";
|
|
12
13
|
|
|
@@ -39,6 +40,8 @@ export function validateNestedInlineResources(
|
|
|
39
40
|
manifest: ResourceManifest,
|
|
40
41
|
rootSchema: Record<string, any>,
|
|
41
42
|
lookupDefinition: InlineDefinitionLookup,
|
|
43
|
+
/** Needed to resolve a `telo#Type` field a value slot is validated against. */
|
|
44
|
+
allManifests: Record<string, any>[] = [],
|
|
42
45
|
): AnalysisDiagnostic[] {
|
|
43
46
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
44
47
|
const resource = { kind: manifest.kind, name: manifest.metadata?.name as string };
|
|
@@ -87,7 +90,16 @@ export function validateNestedInlineResources(
|
|
|
87
90
|
: {};
|
|
88
91
|
const data = { ...inline, metadata: { name: "__inline__", ...existingMeta } };
|
|
89
92
|
const substituted = substituteCelFields(data, effectiveSchema, effectiveSchema);
|
|
90
|
-
|
|
93
|
+
// The same two passes the top-level resource loop runs, so a kind's
|
|
94
|
+
// guarantees don't depend on whether the author wrote it standalone or
|
|
95
|
+
// inline (under a step's `invoke:`, or in a `with:` scope). `data` carries
|
|
96
|
+
// the synthesized metadata; `x-telo-value-schema-from` reads sibling fields
|
|
97
|
+
// off the resource, which are present either way.
|
|
98
|
+
const inlineIssues = [
|
|
99
|
+
...validateAgainstSchema(substituted, effectiveSchema),
|
|
100
|
+
...collectValueSchemaIssues(data, schema, allManifests),
|
|
101
|
+
];
|
|
102
|
+
for (const issue of inlineIssues) {
|
|
91
103
|
diagnostics.push({
|
|
92
104
|
severity: DiagnosticSeverity.Error,
|
|
93
105
|
code: "SCHEMA_VIOLATION",
|
|
@@ -357,6 +357,22 @@ export function validateReferences(
|
|
|
357
357
|
});
|
|
358
358
|
}
|
|
359
359
|
},
|
|
360
|
+
onScope: (e) => {
|
|
361
|
+
const r = e.source;
|
|
362
|
+
const resourceLabel = `${r.kind}/${r.metadata!.name as string}`;
|
|
363
|
+
const resourceData = { kind: r.kind, name: r.metadata!.name as string };
|
|
364
|
+
const filePath = (r.metadata as { source?: string } | undefined)?.source;
|
|
365
|
+
for (const { path: concretePath, refName } of e.scopeRefEntries) {
|
|
366
|
+
const scopeField = concretePath.split(/[.[]/)[0];
|
|
367
|
+
diagnostics.push({
|
|
368
|
+
severity: DiagnosticSeverity.Error,
|
|
369
|
+
code: "SCOPE_ENTRY_NOT_INLINE",
|
|
370
|
+
source: SOURCE,
|
|
371
|
+
message: `${resourceLabel}: scope entry at '${concretePath}' is a reference (\`!ref ${refName}\`), but a scope declares inline resource definitions (a \`kind:\` with its config). Declare the resource inline under '${scopeField}:', or reference an outer resource from a sibling field such as 'targets:'.`,
|
|
372
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
},
|
|
360
376
|
},
|
|
361
377
|
{ aliases, aliasesByModule, skipKinds: SYSTEM_KINDS, expand: true },
|
|
362
378
|
);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { substituteCelFields, validateAgainstSchema, type SchemaIssue } from "./schema-compat.js";
|
|
2
|
+
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `x-telo-value-schema-from: "<field>"` — the value written at the annotated
|
|
6
|
+
* node must satisfy the type declared at the resource's `<field>`.
|
|
7
|
+
*
|
|
8
|
+
* The motivating shape is a kind with ONE declared output contract and SEVERAL
|
|
9
|
+
* places that must each produce it — a decision table's rows, a switch's arms.
|
|
10
|
+
* Only the branch that wins at runtime gets checked there, so a mistyped branch
|
|
11
|
+
* ships and fails on the one input that selects it. This annotation checks every
|
|
12
|
+
* branch statically instead.
|
|
13
|
+
*
|
|
14
|
+
* Generic and topology-driven: the analyzer hardcodes no kind. Any definition
|
|
15
|
+
* with a `telo#Type` field and value-producing slots opts in by annotating those
|
|
16
|
+
* slots. `<field>` is resolved with the same `telo#Type` semantics as
|
|
17
|
+
* `inputType` / `outputType` everywhere else, so an inline
|
|
18
|
+
* `{ kind: Type.JsonSchema, schema: … }` and a named type reference both work.
|
|
19
|
+
*
|
|
20
|
+
* A field that resolves to no schema — the common case of an optional
|
|
21
|
+
* `outputType` left undeclared — is skipped, not reported: declaring the
|
|
22
|
+
* contract is what opts into the check.
|
|
23
|
+
*/
|
|
24
|
+
const ANNOTATION = "x-telo-value-schema-from";
|
|
25
|
+
|
|
26
|
+
interface Annotation {
|
|
27
|
+
/** JSONPath-ish scope into the manifest, e.g. `$.choices[*].value`. */
|
|
28
|
+
scope: string;
|
|
29
|
+
/** Resource field naming the type to validate against, e.g. `outputType`. */
|
|
30
|
+
from: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Walk a definition schema collecting every `x-telo-value-schema-from`, in the
|
|
34
|
+
* same scope form the CEL-context walker produces. */
|
|
35
|
+
function collectAnnotations(schema: Record<string, any>, path: string): Annotation[] {
|
|
36
|
+
if (!schema || typeof schema !== "object") return [];
|
|
37
|
+
const out: Annotation[] = [];
|
|
38
|
+
|
|
39
|
+
const from = schema[ANNOTATION];
|
|
40
|
+
if (typeof from === "string" && from.length > 0) out.push({ scope: path, from });
|
|
41
|
+
|
|
42
|
+
if (schema.properties) {
|
|
43
|
+
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
44
|
+
out.push(...collectAnnotations(value, `${path}.${key}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (schema.items && typeof schema.items === "object") {
|
|
48
|
+
out.push(...collectAnnotations(schema.items, `${path}[*]`));
|
|
49
|
+
}
|
|
50
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
51
|
+
if (Array.isArray(schema[key])) {
|
|
52
|
+
for (const sub of schema[key]) out.push(...collectAnnotations(sub, path));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Expand a scope into the concrete values present in this manifest, carrying
|
|
59
|
+
* each one's real path so a diagnostic points at the row the author wrote. */
|
|
60
|
+
function resolveScopeValues(
|
|
61
|
+
manifest: Record<string, any>,
|
|
62
|
+
scope: string,
|
|
63
|
+
): Array<{ path: string; value: unknown }> {
|
|
64
|
+
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
65
|
+
if (!stripped) return [];
|
|
66
|
+
|
|
67
|
+
let frontier: Array<{ path: string; value: unknown }> = [{ path: "", value: manifest }];
|
|
68
|
+
// Segments look like `choices[*]` or `default` — a name plus optional wildcard.
|
|
69
|
+
for (const segment of stripped.split(".")) {
|
|
70
|
+
const wildcard = segment.endsWith("[*]");
|
|
71
|
+
const name = wildcard ? segment.slice(0, -3) : segment;
|
|
72
|
+
const next: Array<{ path: string; value: unknown }> = [];
|
|
73
|
+
for (const entry of frontier) {
|
|
74
|
+
const container = entry.value as Record<string, unknown> | undefined;
|
|
75
|
+
if (!container || typeof container !== "object") continue;
|
|
76
|
+
const child = container[name];
|
|
77
|
+
if (child === undefined) continue;
|
|
78
|
+
const childPath = entry.path ? `${entry.path}.${name}` : name;
|
|
79
|
+
if (!wildcard) {
|
|
80
|
+
next.push({ path: childPath, value: child });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!Array.isArray(child)) continue;
|
|
84
|
+
child.forEach((item, i) => next.push({ path: `${childPath}[${i}]`, value: item }));
|
|
85
|
+
}
|
|
86
|
+
frontier = next;
|
|
87
|
+
if (frontier.length === 0) break;
|
|
88
|
+
}
|
|
89
|
+
return frontier;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate every `x-telo-value-schema-from` slot in one resource.
|
|
94
|
+
*
|
|
95
|
+
* CEL leaves are replaced with schema-shaped placeholders before AJV runs
|
|
96
|
+
* (`substituteCelFields`), so an expression is accepted wherever its slot's
|
|
97
|
+
* declared type would be — a static pass cannot know what it evaluates to. What
|
|
98
|
+
* this DOES catch is structural disagreement that no runtime value can fix: a
|
|
99
|
+
* missing required property, an unknown property under
|
|
100
|
+
* `additionalProperties: false`, or a literal of the wrong type.
|
|
101
|
+
*/
|
|
102
|
+
export function collectValueSchemaIssues(
|
|
103
|
+
manifest: Record<string, any>,
|
|
104
|
+
defSchema: Record<string, any> | undefined,
|
|
105
|
+
allManifests: Record<string, any>[],
|
|
106
|
+
): SchemaIssue[] {
|
|
107
|
+
if (!defSchema) return [];
|
|
108
|
+
const annotations = collectAnnotations(defSchema, "$");
|
|
109
|
+
if (annotations.length === 0) return [];
|
|
110
|
+
|
|
111
|
+
const issues: SchemaIssue[] = [];
|
|
112
|
+
for (const { scope, from } of annotations) {
|
|
113
|
+
const target = resolveTypeFieldToSchema(manifest[from], allManifests);
|
|
114
|
+
if (!target || typeof target !== "object") continue;
|
|
115
|
+
|
|
116
|
+
for (const { path, value } of resolveScopeValues(manifest, scope)) {
|
|
117
|
+
for (const issue of validateAgainstSchema(substituteCelFields(value, target), target)) {
|
|
118
|
+
issues.push({
|
|
119
|
+
message: `\`${path}\` does not satisfy the type declared at \`${from}\`: ${issue.message}`,
|
|
120
|
+
path: issue.path ? `${path}.${issue.path}` : path,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return issues;
|
|
126
|
+
}
|