@telorun/analyzer 0.10.0 → 0.11.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 +162 -5
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +115 -1
- package/dist/validate-cel-context.d.ts +56 -7
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +78 -8
- package/package.json +3 -3
- package/src/analyzer.ts +205 -7
- package/src/builtins.ts +115 -1
- package/src/validate-cel-context.ts +98 -8
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;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA8c/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,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IA6avB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAUxF,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
|
@@ -37,12 +37,89 @@ function lookupDefinitionTypeField(invokedKind, fieldName, defs, aliases, allMan
|
|
|
37
37
|
return resolveTypeFieldToSchema(value, allManifests);
|
|
38
38
|
}
|
|
39
39
|
const SOURCE = "telo-analyzer";
|
|
40
|
+
/** Build a closed JSON Schema for the `self` CEL variable available inside a
|
|
41
|
+
* `Telo.Definition` template body. Mirrors the runtime template controller's
|
|
42
|
+
* `const self = { ...resource, name: resource.metadata.name };` — every
|
|
43
|
+
* property the user declared in `schema:` plus synthetic `name` / `kind` and
|
|
44
|
+
* the metadata sub-object (kept open since metadata legitimately carries
|
|
45
|
+
* arbitrary user-added fields). */
|
|
46
|
+
function buildSelfSchema(definition) {
|
|
47
|
+
const userSchema = (definition.schema ?? {});
|
|
48
|
+
const userProps = (userSchema.properties ?? {});
|
|
49
|
+
const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
|
|
50
|
+
return {
|
|
51
|
+
type: "object",
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
properties: {
|
|
54
|
+
...userProps,
|
|
55
|
+
name: { type: "string" },
|
|
56
|
+
kind: { type: "string" },
|
|
57
|
+
metadata: {
|
|
58
|
+
type: "object",
|
|
59
|
+
additionalProperties: true,
|
|
60
|
+
properties: { name: { type: "string" } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: [...userRequired, "name", "kind"],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
67
|
+
* invocable template body. Three-layer fallback mirroring the runtime's
|
|
68
|
+
* caller-supplied inputs:
|
|
69
|
+
* 1. The definition's own `inputType:` field (preferred).
|
|
70
|
+
* 2. The `extends:`-declared abstract's `inputType:` (so a concrete
|
|
71
|
+
* definition inheriting a contract gets typed inputs without
|
|
72
|
+
* redeclaring them).
|
|
73
|
+
* 3. Undefined — caller signals opaque `map<string, dyn>` upstream. */
|
|
74
|
+
function lookupTemplateInputsSchema(definition, defs, aliases, allManifests) {
|
|
75
|
+
const own = resolveTypeFieldToSchema(definition.inputType, allManifests);
|
|
76
|
+
if (own)
|
|
77
|
+
return own;
|
|
78
|
+
const ext = definition.extends;
|
|
79
|
+
if (typeof ext === "string" && ext.length > 0) {
|
|
80
|
+
const canonical = aliases.resolveKind(ext) ?? ext;
|
|
81
|
+
const abstractDef = defs.resolve(canonical);
|
|
82
|
+
if (abstractDef) {
|
|
83
|
+
const inherited = resolveTypeFieldToSchema(abstractDef.inputType, allManifests);
|
|
84
|
+
if (inherited)
|
|
85
|
+
return inherited;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
91
|
+
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
92
|
+
* have been pre-augmented:
|
|
93
|
+
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
94
|
+
* - `inputType` → resolved with extends fallback when the field isn't
|
|
95
|
+
* declared directly on the definition.
|
|
96
|
+
*
|
|
97
|
+
* For non-definition manifests the original object is returned. */
|
|
98
|
+
function manifestRootForResolver(m, defs, aliases, allManifests) {
|
|
99
|
+
if (m.kind !== "Telo.Definition")
|
|
100
|
+
return m;
|
|
101
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
|
|
102
|
+
return {
|
|
103
|
+
...m,
|
|
104
|
+
schema: buildSelfSchema(m),
|
|
105
|
+
...(inputs ? { inputType: inputs } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
40
108
|
/**
|
|
41
109
|
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
42
110
|
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
43
111
|
* the same format the analyzer uses for CEL context validation.
|
|
112
|
+
*
|
|
113
|
+
* Result is sorted by scope specificity (longer scope first) so that the
|
|
114
|
+
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
115
|
+
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
116
|
+
* could shadow a narrower descendant scope whose activation differs.
|
|
44
117
|
*/
|
|
45
118
|
function extractContextsFromSchema(schema, path = "$") {
|
|
119
|
+
const all = collectContexts(schema, path);
|
|
120
|
+
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
121
|
+
}
|
|
122
|
+
function collectContexts(schema, path) {
|
|
46
123
|
if (!schema || typeof schema !== "object")
|
|
47
124
|
return [];
|
|
48
125
|
const results = [];
|
|
@@ -51,16 +128,16 @@ function extractContextsFromSchema(schema, path = "$") {
|
|
|
51
128
|
}
|
|
52
129
|
if (schema.properties) {
|
|
53
130
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
54
|
-
results.push(...
|
|
131
|
+
results.push(...collectContexts(value, `${path}.${key}`));
|
|
55
132
|
}
|
|
56
133
|
}
|
|
57
134
|
if (schema.items && typeof schema.items === "object") {
|
|
58
|
-
results.push(...
|
|
135
|
+
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
59
136
|
}
|
|
60
137
|
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
61
138
|
if (Array.isArray(schema[key])) {
|
|
62
139
|
for (const subschema of schema[key]) {
|
|
63
|
-
results.push(...
|
|
140
|
+
results.push(...collectContexts(subschema, path));
|
|
64
141
|
}
|
|
65
142
|
}
|
|
66
143
|
}
|
|
@@ -436,7 +513,11 @@ export class StaticAnalyzer {
|
|
|
436
513
|
});
|
|
437
514
|
continue;
|
|
438
515
|
}
|
|
439
|
-
|
|
516
|
+
// Abstracts carry only inputType / outputType schema fields and no template
|
|
517
|
+
// body — nothing for the per-resource walk to validate. Definitions are now
|
|
518
|
+
// walked: their template bodies (`resources` / `invoke` / `run` / `provide`)
|
|
519
|
+
// contain CEL that must be checked against `self` / `inputs` / `result`.
|
|
520
|
+
if (m.kind === "Telo.Abstract") {
|
|
440
521
|
continue;
|
|
441
522
|
}
|
|
442
523
|
const resource = { kind: m.kind, name: m.metadata?.name };
|
|
@@ -489,6 +570,76 @@ export class StaticAnalyzer {
|
|
|
489
570
|
}
|
|
490
571
|
// (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
|
|
491
572
|
}
|
|
573
|
+
// Template-body structural validations: check that template entry-points produce
|
|
574
|
+
// values matching the contract of their dispatch target and (for `provide:`)
|
|
575
|
+
// the abstract this definition `extends`. CEL fields inside the templated
|
|
576
|
+
// values are replaced with type-appropriate placeholders before AJV runs —
|
|
577
|
+
// same pattern as the per-resource schema validation above.
|
|
578
|
+
for (const m of allManifests) {
|
|
579
|
+
if (m.kind !== "Telo.Definition")
|
|
580
|
+
continue;
|
|
581
|
+
const filePath = m.metadata?.source;
|
|
582
|
+
const name = m.metadata?.name;
|
|
583
|
+
if (!name)
|
|
584
|
+
continue;
|
|
585
|
+
const resource = { kind: m.kind, name };
|
|
586
|
+
const md = m;
|
|
587
|
+
const emitTargetMismatch = (targetKind, valueSchema, value, path) => {
|
|
588
|
+
const substituted = substituteCelFields(value, valueSchema);
|
|
589
|
+
const issues = validateAgainstSchema(substituted, valueSchema);
|
|
590
|
+
for (const issue of issues) {
|
|
591
|
+
diagnostics.push({
|
|
592
|
+
severity: DiagnosticSeverity.Error,
|
|
593
|
+
code: "TEMPLATE_TARGET_MISMATCH",
|
|
594
|
+
source: SOURCE,
|
|
595
|
+
message: `${m.kind}/${name}: ${path} does not satisfy ${targetKind}'s contract: ${issue.message}`,
|
|
596
|
+
data: { resource, filePath, path: issue.path ? `${path}.${issue.path}` : path },
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
// Resolve the dispatch target's kind, if statically known. Object-form
|
|
601
|
+
// `invoke: { kind, name }` and `provide: { kind, name }` carry it; the
|
|
602
|
+
// string-form `invoke: "name"` does not (the matching resource entry would
|
|
603
|
+
// need to be located by expanded name — out of scope here).
|
|
604
|
+
const invoke = md.invoke;
|
|
605
|
+
const provide = md.provide;
|
|
606
|
+
let dispatchKind;
|
|
607
|
+
if (invoke && typeof invoke === "object" && !Array.isArray(invoke) && typeof invoke.kind === "string") {
|
|
608
|
+
dispatchKind = invoke.kind;
|
|
609
|
+
}
|
|
610
|
+
else if (provide &&
|
|
611
|
+
typeof provide === "object" &&
|
|
612
|
+
!Array.isArray(provide) &&
|
|
613
|
+
typeof provide.kind === "string") {
|
|
614
|
+
dispatchKind = provide.kind;
|
|
615
|
+
}
|
|
616
|
+
// Top-level `inputs:` (sibling of `invoke:` / `provide:`) carries the
|
|
617
|
+
// values passed to the dispatch target's invoke(). Validate against the
|
|
618
|
+
// target's declared `inputType` when both sides have one.
|
|
619
|
+
if (dispatchKind && md.inputs && typeof md.inputs === "object") {
|
|
620
|
+
const targetSchema = lookupDefinitionTypeField(dispatchKind, "inputType", defs, aliases, allManifests);
|
|
621
|
+
if (targetSchema) {
|
|
622
|
+
emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Top-level `result:` (a sibling, only meaningful with `provide:`) is a
|
|
626
|
+
// post-call mapping that must satisfy the abstract this definition
|
|
627
|
+
// `extends` (`outputType`). The target's outputType lives on `provide.kind`
|
|
628
|
+
// and is what `result` is typed against *inside* CEL — separate role.
|
|
629
|
+
if (provide &&
|
|
630
|
+
typeof provide === "object" &&
|
|
631
|
+
!Array.isArray(provide) &&
|
|
632
|
+
md.result &&
|
|
633
|
+
typeof md.result === "object") {
|
|
634
|
+
const extendsValue = md.extends;
|
|
635
|
+
if (typeof extendsValue === "string" && extendsValue.length > 0) {
|
|
636
|
+
const abstractSchema = lookupDefinitionTypeField(extendsValue, "outputType", defs, aliases, allManifests);
|
|
637
|
+
if (abstractSchema) {
|
|
638
|
+
emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
492
643
|
// Validate CEL syntax and context variable access in all manifests
|
|
493
644
|
for (const m of allManifests) {
|
|
494
645
|
const resource = { kind: m.kind, name: m.metadata?.name };
|
|
@@ -533,7 +684,13 @@ export class StaticAnalyzer {
|
|
|
533
684
|
const manifestItem = matchedScope
|
|
534
685
|
? getManifestItem(path, matchedScope, m)
|
|
535
686
|
: m;
|
|
536
|
-
const
|
|
687
|
+
const rootForResolver = manifestRootForResolver(m, defs, aliases, allManifests);
|
|
688
|
+
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
689
|
+
manifestRoot: rootForResolver,
|
|
690
|
+
defs,
|
|
691
|
+
aliases,
|
|
692
|
+
allManifests: allManifests,
|
|
693
|
+
});
|
|
537
694
|
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
538
695
|
}
|
|
539
696
|
const engine = defaultRegistry().get(engineName);
|
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;AAEvD,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;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAwQ/C,CAAC"}
|
package/dist/builtins.js
CHANGED
|
@@ -38,7 +38,121 @@ export const KERNEL_BUILTINS = [
|
|
|
38
38
|
kind: "Telo.Definition",
|
|
39
39
|
metadata: { name: "Definition", module: "Telo" },
|
|
40
40
|
capability: "Telo.Template",
|
|
41
|
-
|
|
41
|
+
// Top-level shape stays open (`additionalProperties: true`) so this change
|
|
42
|
+
// attaches x-telo-context annotations to known template-body fields without
|
|
43
|
+
// tightening the Telo.Definition shape itself. The annotations drive
|
|
44
|
+
// static CEL validation of expressions inside `resources:` / `invoke:` /
|
|
45
|
+
// `run:` / `provide:` / top-level `inputs:` / top-level `result:` against
|
|
46
|
+
// `self` (typed from `schema:`) and `inputs` (typed from `inputType:`,
|
|
47
|
+
// falling back to the extends-declared abstract).
|
|
48
|
+
//
|
|
49
|
+
// `inputs:` and `result:` live as top-level siblings of `invoke:` / `provide:`,
|
|
50
|
+
// matching how Run.Sequence steps factor dispatch from data. The dispatch
|
|
51
|
+
// entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
|
|
52
|
+
// are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
|
|
53
|
+
schema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
additionalProperties: true,
|
|
56
|
+
properties: {
|
|
57
|
+
resources: {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: {
|
|
60
|
+
type: "object",
|
|
61
|
+
additionalProperties: true,
|
|
62
|
+
"x-telo-context": {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
properties: {
|
|
66
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
67
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
invoke: {
|
|
73
|
+
oneOf: [
|
|
74
|
+
{
|
|
75
|
+
type: "string",
|
|
76
|
+
"x-telo-context": {
|
|
77
|
+
type: "object",
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
properties: {
|
|
80
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "object",
|
|
86
|
+
additionalProperties: true,
|
|
87
|
+
properties: {
|
|
88
|
+
kind: { type: "string" },
|
|
89
|
+
name: {
|
|
90
|
+
type: "string",
|
|
91
|
+
"x-telo-context": {
|
|
92
|
+
type: "object",
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
properties: {
|
|
95
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
provide: {
|
|
104
|
+
type: "object",
|
|
105
|
+
additionalProperties: true,
|
|
106
|
+
properties: {
|
|
107
|
+
kind: { type: "string" },
|
|
108
|
+
name: {
|
|
109
|
+
type: "string",
|
|
110
|
+
"x-telo-context": {
|
|
111
|
+
type: "object",
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
properties: {
|
|
114
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
run: {
|
|
121
|
+
type: "string",
|
|
122
|
+
"x-telo-context": {
|
|
123
|
+
type: "object",
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
properties: {
|
|
126
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
inputs: {
|
|
131
|
+
type: "object",
|
|
132
|
+
additionalProperties: true,
|
|
133
|
+
"x-telo-context": {
|
|
134
|
+
type: "object",
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
properties: {
|
|
137
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
138
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
result: {
|
|
143
|
+
type: "object",
|
|
144
|
+
additionalProperties: true,
|
|
145
|
+
"x-telo-context": {
|
|
146
|
+
type: "object",
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
properties: {
|
|
149
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
150
|
+
result: { "x-telo-context-from-ref-kind": "provide/kind#outputType" },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
42
156
|
},
|
|
43
157
|
{
|
|
44
158
|
kind: "Telo.Definition",
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
|
+
export interface ContextResolveOpts {
|
|
3
|
+
/** When provided, used to resolve `x-telo-context-from-root` annotations against the
|
|
4
|
+
* root manifest. When omitted, defaults to `manifestItem`. */
|
|
5
|
+
manifestRoot?: Record<string, any>;
|
|
6
|
+
/** When provided alongside `aliases`, used to resolve `x-telo-context-from-ref-kind`
|
|
7
|
+
* annotations: read a kind name from a path on `manifestRoot` and return the
|
|
8
|
+
* declared definition's `<field>` schema. */
|
|
9
|
+
defs?: {
|
|
10
|
+
resolve(kind: string): Record<string, any> | undefined;
|
|
11
|
+
};
|
|
12
|
+
aliases?: {
|
|
13
|
+
resolveKind(kind: string): string | undefined;
|
|
14
|
+
};
|
|
15
|
+
allManifests?: Record<string, any>[];
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
|
|
4
19
|
* - String: look up the named type in allManifests (Type.JsonSchema resources)
|
|
@@ -15,15 +30,49 @@ export declare function resolveTypeFieldToSchema(value: unknown, allManifests: R
|
|
|
15
30
|
*/
|
|
16
31
|
export declare function pathMatchesScope(exprPath: string, scope: string): boolean;
|
|
17
32
|
/**
|
|
18
|
-
* Resolves `x-telo-context
|
|
19
|
-
* manifest item
|
|
20
|
-
* the result as named properties into the annotated node (locking additionalProperties: false).
|
|
33
|
+
* Resolves `x-telo-context-*` annotations in a context schema using the concrete
|
|
34
|
+
* manifest item (per-scope) and the manifest root.
|
|
21
35
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
36
|
+
* Annotation forms:
|
|
37
|
+
*
|
|
38
|
+
* - `x-telo-context-from`: navigates `manifestItem.<path>` and treats the resolved
|
|
39
|
+
* value as a **property map** (keys → sub-schemas) that is merged into the
|
|
40
|
+
* annotated node's properties. Used for HTTP-style scopes where the navigated
|
|
41
|
+
* value is itself a map of variable names.
|
|
42
|
+
*
|
|
43
|
+
* Example: `x-telo-context-from: "request/schema"` reads `manifestItem.request.schema`
|
|
44
|
+
* (= `{ query: {...}, body: {...}, … }`) and merges those keys as named properties
|
|
45
|
+
* of the context node.
|
|
46
|
+
*
|
|
47
|
+
* - `x-telo-context-from-root`: navigates `manifestRoot.<path>` and **replaces** the
|
|
48
|
+
* annotated node's schema with the resolved value. Used on individual property
|
|
49
|
+
* schemas (e.g. `properties.self`) where the resolved value is a single variable's
|
|
50
|
+
* full schema, not a property map.
|
|
51
|
+
*
|
|
52
|
+
* Example: `properties.self.x-telo-context-from-root: "schema"` reads
|
|
53
|
+
* `manifestRoot.schema` and uses it as the schema of the `self` CEL variable.
|
|
54
|
+
*
|
|
55
|
+
* - `x-telo-context-from-ref-kind`: reads a kind name from `manifestRoot.<refPath>`,
|
|
56
|
+
* resolves it via the definition registry, and returns that kind's `<field>` schema
|
|
57
|
+
* (e.g. `outputType`/`inputType`). Used to type `result` against the dispatch
|
|
58
|
+
* target's declared output shape.
|
|
59
|
+
*
|
|
60
|
+
* Syntax: `<refPath>#<field>` — slashes traverse the manifest tree.
|
|
61
|
+
*
|
|
62
|
+
* Example: `x-telo-context-from-ref-kind: "provide/kind#outputType"` reads
|
|
63
|
+
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
64
|
+
* and returns the `outputType` schema.
|
|
65
|
+
*
|
|
66
|
+
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
67
|
+
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
68
|
+
*
|
|
69
|
+
* **Fallback chain.** When both `x-telo-context-from-root` and
|
|
70
|
+
* `x-telo-context-from-ref-kind` are present on the same node, the resolver tries
|
|
71
|
+
* `from-root` first; if that produces no usable schema, it falls back to `from-ref-kind`.
|
|
72
|
+
* This lets a definition declare typing from its own field with a sibling-kind fallback
|
|
73
|
+
* (e.g. `inputType` direct → `extends`-declared abstract's `inputType`).
|
|
25
74
|
*/
|
|
26
|
-
export declare function resolveContextAnnotations(schema: Record<string, any>, manifestItem: Record<string, any>,
|
|
75
|
+
export declare function resolveContextAnnotations(schema: Record<string, any>, manifestItem: Record<string, any>, opts?: ContextResolveOpts | Record<string, any>[]): Record<string, any>;
|
|
27
76
|
/**
|
|
28
77
|
* Extracts the concrete manifest array item for a given expression path + scope.
|
|
29
78
|
* e.g. exprPath="routes[0].inputs.q", scope="$.routes[*].inputs" → manifest.routes[0]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CA6FrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB"}
|
|
@@ -61,17 +61,56 @@ export function pathMatchesScope(exprPath, scope) {
|
|
|
61
61
|
return remaining === "" || remaining[0] === "." || remaining[0] === "[";
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Resolves `x-telo-context
|
|
65
|
-
* manifest item
|
|
66
|
-
* the result as named properties into the annotated node (locking additionalProperties: false).
|
|
64
|
+
* Resolves `x-telo-context-*` annotations in a context schema using the concrete
|
|
65
|
+
* manifest item (per-scope) and the manifest root.
|
|
67
66
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `
|
|
67
|
+
* Annotation forms:
|
|
68
|
+
*
|
|
69
|
+
* - `x-telo-context-from`: navigates `manifestItem.<path>` and treats the resolved
|
|
70
|
+
* value as a **property map** (keys → sub-schemas) that is merged into the
|
|
71
|
+
* annotated node's properties. Used for HTTP-style scopes where the navigated
|
|
72
|
+
* value is itself a map of variable names.
|
|
73
|
+
*
|
|
74
|
+
* Example: `x-telo-context-from: "request/schema"` reads `manifestItem.request.schema`
|
|
75
|
+
* (= `{ query: {...}, body: {...}, … }`) and merges those keys as named properties
|
|
76
|
+
* of the context node.
|
|
77
|
+
*
|
|
78
|
+
* - `x-telo-context-from-root`: navigates `manifestRoot.<path>` and **replaces** the
|
|
79
|
+
* annotated node's schema with the resolved value. Used on individual property
|
|
80
|
+
* schemas (e.g. `properties.self`) where the resolved value is a single variable's
|
|
81
|
+
* full schema, not a property map.
|
|
82
|
+
*
|
|
83
|
+
* Example: `properties.self.x-telo-context-from-root: "schema"` reads
|
|
84
|
+
* `manifestRoot.schema` and uses it as the schema of the `self` CEL variable.
|
|
85
|
+
*
|
|
86
|
+
* - `x-telo-context-from-ref-kind`: reads a kind name from `manifestRoot.<refPath>`,
|
|
87
|
+
* resolves it via the definition registry, and returns that kind's `<field>` schema
|
|
88
|
+
* (e.g. `outputType`/`inputType`). Used to type `result` against the dispatch
|
|
89
|
+
* target's declared output shape.
|
|
90
|
+
*
|
|
91
|
+
* Syntax: `<refPath>#<field>` — slashes traverse the manifest tree.
|
|
92
|
+
*
|
|
93
|
+
* Example: `x-telo-context-from-ref-kind: "provide/kind#outputType"` reads
|
|
94
|
+
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
95
|
+
* and returns the `outputType` schema.
|
|
96
|
+
*
|
|
97
|
+
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
98
|
+
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
99
|
+
*
|
|
100
|
+
* **Fallback chain.** When both `x-telo-context-from-root` and
|
|
101
|
+
* `x-telo-context-from-ref-kind` are present on the same node, the resolver tries
|
|
102
|
+
* `from-root` first; if that produces no usable schema, it falls back to `from-ref-kind`.
|
|
103
|
+
* This lets a definition declare typing from its own field with a sibling-kind fallback
|
|
104
|
+
* (e.g. `inputType` direct → `extends`-declared abstract's `inputType`).
|
|
71
105
|
*/
|
|
72
|
-
export function resolveContextAnnotations(schema, manifestItem,
|
|
106
|
+
export function resolveContextAnnotations(schema, manifestItem, opts) {
|
|
73
107
|
if (!schema || typeof schema !== "object")
|
|
74
108
|
return schema;
|
|
109
|
+
// Back-compat: third positional arg used to be `allManifests: Record<string, any>[]`.
|
|
110
|
+
const normalizedOpts = Array.isArray(opts)
|
|
111
|
+
? { allManifests: opts }
|
|
112
|
+
: (opts ?? {});
|
|
113
|
+
const { manifestRoot = manifestItem, defs, aliases, allManifests } = normalizedOpts;
|
|
75
114
|
const from = schema["x-telo-context-from"];
|
|
76
115
|
if (from) {
|
|
77
116
|
const resolved = navigatePath(manifestItem, from.split("/"));
|
|
@@ -82,6 +121,37 @@ export function resolveContextAnnotations(schema, manifestItem, allManifests) {
|
|
|
82
121
|
additionalProperties: false,
|
|
83
122
|
};
|
|
84
123
|
}
|
|
124
|
+
const fromRoot = schema["x-telo-context-from-root"];
|
|
125
|
+
const fromRefKind = schema["x-telo-context-from-ref-kind"];
|
|
126
|
+
if (fromRoot || fromRefKind) {
|
|
127
|
+
if (fromRoot) {
|
|
128
|
+
const resolved = navigatePath(manifestRoot, fromRoot.split("/"));
|
|
129
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (fromRefKind && defs) {
|
|
134
|
+
const hashIdx = fromRefKind.indexOf("#");
|
|
135
|
+
if (hashIdx > 0) {
|
|
136
|
+
const refPath = fromRefKind.slice(0, hashIdx);
|
|
137
|
+
const field = fromRefKind.slice(hashIdx + 1);
|
|
138
|
+
const kindValue = navigatePath(manifestRoot, refPath.split("/"));
|
|
139
|
+
if (typeof kindValue === "string" && kindValue.length > 0) {
|
|
140
|
+
const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
|
|
141
|
+
const def = defs.resolve(canonical);
|
|
142
|
+
const typeField = def
|
|
143
|
+
? def[field]
|
|
144
|
+
: undefined;
|
|
145
|
+
const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
|
|
146
|
+
if (resolved && typeof resolved === "object") {
|
|
147
|
+
return resolved;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Open fallback so unresolved types never produce false-positive CEL diagnostics.
|
|
153
|
+
return { type: "object", additionalProperties: true };
|
|
154
|
+
}
|
|
85
155
|
const refFrom = schema["x-telo-context-ref-from"];
|
|
86
156
|
if (refFrom && allManifests) {
|
|
87
157
|
const slashIdx = refFrom.indexOf("/");
|
|
@@ -107,7 +177,7 @@ export function resolveContextAnnotations(schema, manifestItem, allManifests) {
|
|
|
107
177
|
if (schema.properties) {
|
|
108
178
|
const props = {};
|
|
109
179
|
for (const [k, v] of Object.entries(schema.properties)) {
|
|
110
|
-
props[k] = resolveContextAnnotations(v, manifestItem,
|
|
180
|
+
props[k] = resolveContextAnnotations(v, manifestItem, normalizedOpts);
|
|
111
181
|
}
|
|
112
182
|
return { ...schema, properties: props };
|
|
113
183
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/sdk": "0.
|
|
45
|
-
"@telorun/templating": "0.2.
|
|
44
|
+
"@telorun/sdk": "0.11.1",
|
|
45
|
+
"@telorun/templating": "0.2.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -68,14 +68,108 @@ function lookupDefinitionTypeField(
|
|
|
68
68
|
|
|
69
69
|
const SOURCE = "telo-analyzer";
|
|
70
70
|
|
|
71
|
+
/** Build a closed JSON Schema for the `self` CEL variable available inside a
|
|
72
|
+
* `Telo.Definition` template body. Mirrors the runtime template controller's
|
|
73
|
+
* `const self = { ...resource, name: resource.metadata.name };` — every
|
|
74
|
+
* property the user declared in `schema:` plus synthetic `name` / `kind` and
|
|
75
|
+
* the metadata sub-object (kept open since metadata legitimately carries
|
|
76
|
+
* arbitrary user-added fields). */
|
|
77
|
+
function buildSelfSchema(definition: Record<string, any>): Record<string, any> {
|
|
78
|
+
const userSchema = (definition.schema ?? {}) as Record<string, any>;
|
|
79
|
+
const userProps = (userSchema.properties ?? {}) as Record<string, any>;
|
|
80
|
+
const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
|
|
81
|
+
return {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
properties: {
|
|
85
|
+
...userProps,
|
|
86
|
+
name: { type: "string" },
|
|
87
|
+
kind: { type: "string" },
|
|
88
|
+
metadata: {
|
|
89
|
+
type: "object",
|
|
90
|
+
additionalProperties: true,
|
|
91
|
+
properties: { name: { type: "string" } },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: [...userRequired, "name", "kind"],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
99
|
+
* invocable template body. Three-layer fallback mirroring the runtime's
|
|
100
|
+
* caller-supplied inputs:
|
|
101
|
+
* 1. The definition's own `inputType:` field (preferred).
|
|
102
|
+
* 2. The `extends:`-declared abstract's `inputType:` (so a concrete
|
|
103
|
+
* definition inheriting a contract gets typed inputs without
|
|
104
|
+
* redeclaring them).
|
|
105
|
+
* 3. Undefined — caller signals opaque `map<string, dyn>` upstream. */
|
|
106
|
+
function lookupTemplateInputsSchema(
|
|
107
|
+
definition: Record<string, any>,
|
|
108
|
+
defs: DefinitionRegistry,
|
|
109
|
+
aliases: AliasResolver,
|
|
110
|
+
allManifests: Record<string, any>[],
|
|
111
|
+
): Record<string, any> | undefined {
|
|
112
|
+
const own = resolveTypeFieldToSchema(definition.inputType, allManifests);
|
|
113
|
+
if (own) return own;
|
|
114
|
+
const ext = definition.extends as string | undefined;
|
|
115
|
+
if (typeof ext === "string" && ext.length > 0) {
|
|
116
|
+
const canonical = aliases.resolveKind(ext) ?? ext;
|
|
117
|
+
const abstractDef = defs.resolve(canonical);
|
|
118
|
+
if (abstractDef) {
|
|
119
|
+
const inherited = resolveTypeFieldToSchema(
|
|
120
|
+
(abstractDef as unknown as Record<string, unknown>).inputType,
|
|
121
|
+
allManifests,
|
|
122
|
+
);
|
|
123
|
+
if (inherited) return inherited;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
130
|
+
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
131
|
+
* have been pre-augmented:
|
|
132
|
+
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
133
|
+
* - `inputType` → resolved with extends fallback when the field isn't
|
|
134
|
+
* declared directly on the definition.
|
|
135
|
+
*
|
|
136
|
+
* For non-definition manifests the original object is returned. */
|
|
137
|
+
function manifestRootForResolver(
|
|
138
|
+
m: Record<string, any>,
|
|
139
|
+
defs: DefinitionRegistry,
|
|
140
|
+
aliases: AliasResolver,
|
|
141
|
+
allManifests: Record<string, any>[],
|
|
142
|
+
): Record<string, any> {
|
|
143
|
+
if (m.kind !== "Telo.Definition") return m;
|
|
144
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
|
|
145
|
+
return {
|
|
146
|
+
...m,
|
|
147
|
+
schema: buildSelfSchema(m),
|
|
148
|
+
...(inputs ? { inputType: inputs } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
71
152
|
/**
|
|
72
153
|
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
73
154
|
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
74
155
|
* the same format the analyzer uses for CEL context validation.
|
|
156
|
+
*
|
|
157
|
+
* Result is sorted by scope specificity (longer scope first) so that the
|
|
158
|
+
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
159
|
+
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
160
|
+
* could shadow a narrower descendant scope whose activation differs.
|
|
75
161
|
*/
|
|
76
162
|
function extractContextsFromSchema(
|
|
77
163
|
schema: Record<string, any>,
|
|
78
164
|
path = "$",
|
|
165
|
+
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
166
|
+
const all = collectContexts(schema, path);
|
|
167
|
+
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectContexts(
|
|
171
|
+
schema: Record<string, any>,
|
|
172
|
+
path: string,
|
|
79
173
|
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
80
174
|
if (!schema || typeof schema !== "object") return [];
|
|
81
175
|
const results: Array<{ scope: string; schema: Record<string, any> }> = [];
|
|
@@ -86,18 +180,18 @@ function extractContextsFromSchema(
|
|
|
86
180
|
|
|
87
181
|
if (schema.properties) {
|
|
88
182
|
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
89
|
-
results.push(...
|
|
183
|
+
results.push(...collectContexts(value, `${path}.${key}`));
|
|
90
184
|
}
|
|
91
185
|
}
|
|
92
186
|
|
|
93
187
|
if (schema.items && typeof schema.items === "object") {
|
|
94
|
-
results.push(...
|
|
188
|
+
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
95
189
|
}
|
|
96
190
|
|
|
97
191
|
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
98
192
|
if (Array.isArray(schema[key])) {
|
|
99
193
|
for (const subschema of schema[key]) {
|
|
100
|
-
results.push(...
|
|
194
|
+
results.push(...collectContexts(subschema, path));
|
|
101
195
|
}
|
|
102
196
|
}
|
|
103
197
|
}
|
|
@@ -552,7 +646,11 @@ export class StaticAnalyzer {
|
|
|
552
646
|
});
|
|
553
647
|
continue;
|
|
554
648
|
}
|
|
555
|
-
|
|
649
|
+
// Abstracts carry only inputType / outputType schema fields and no template
|
|
650
|
+
// body — nothing for the per-resource walk to validate. Definitions are now
|
|
651
|
+
// walked: their template bodies (`resources` / `invoke` / `run` / `provide`)
|
|
652
|
+
// contain CEL that must be checked against `self` / `inputs` / `result`.
|
|
653
|
+
if (m.kind === "Telo.Abstract") {
|
|
556
654
|
continue;
|
|
557
655
|
}
|
|
558
656
|
|
|
@@ -612,6 +710,99 @@ export class StaticAnalyzer {
|
|
|
612
710
|
// (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
|
|
613
711
|
}
|
|
614
712
|
|
|
713
|
+
// Template-body structural validations: check that template entry-points produce
|
|
714
|
+
// values matching the contract of their dispatch target and (for `provide:`)
|
|
715
|
+
// the abstract this definition `extends`. CEL fields inside the templated
|
|
716
|
+
// values are replaced with type-appropriate placeholders before AJV runs —
|
|
717
|
+
// same pattern as the per-resource schema validation above.
|
|
718
|
+
for (const m of allManifests) {
|
|
719
|
+
if (m.kind !== "Telo.Definition") continue;
|
|
720
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
721
|
+
const name = (m.metadata as any)?.name as string | undefined;
|
|
722
|
+
if (!name) continue;
|
|
723
|
+
const resource = { kind: m.kind, name };
|
|
724
|
+
const md = m as Record<string, any>;
|
|
725
|
+
|
|
726
|
+
const emitTargetMismatch = (
|
|
727
|
+
targetKind: string,
|
|
728
|
+
valueSchema: Record<string, any>,
|
|
729
|
+
value: unknown,
|
|
730
|
+
path: string,
|
|
731
|
+
) => {
|
|
732
|
+
const substituted = substituteCelFields(value, valueSchema);
|
|
733
|
+
const issues = validateAgainstSchema(substituted, valueSchema);
|
|
734
|
+
for (const issue of issues) {
|
|
735
|
+
diagnostics.push({
|
|
736
|
+
severity: DiagnosticSeverity.Error,
|
|
737
|
+
code: "TEMPLATE_TARGET_MISMATCH",
|
|
738
|
+
source: SOURCE,
|
|
739
|
+
message: `${m.kind}/${name}: ${path} does not satisfy ${targetKind}'s contract: ${issue.message}`,
|
|
740
|
+
data: { resource, filePath, path: issue.path ? `${path}.${issue.path}` : path },
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// Resolve the dispatch target's kind, if statically known. Object-form
|
|
746
|
+
// `invoke: { kind, name }` and `provide: { kind, name }` carry it; the
|
|
747
|
+
// string-form `invoke: "name"` does not (the matching resource entry would
|
|
748
|
+
// need to be located by expanded name — out of scope here).
|
|
749
|
+
const invoke = md.invoke;
|
|
750
|
+
const provide = md.provide;
|
|
751
|
+
let dispatchKind: string | undefined;
|
|
752
|
+
if (invoke && typeof invoke === "object" && !Array.isArray(invoke) && typeof invoke.kind === "string") {
|
|
753
|
+
dispatchKind = invoke.kind;
|
|
754
|
+
} else if (
|
|
755
|
+
provide &&
|
|
756
|
+
typeof provide === "object" &&
|
|
757
|
+
!Array.isArray(provide) &&
|
|
758
|
+
typeof provide.kind === "string"
|
|
759
|
+
) {
|
|
760
|
+
dispatchKind = provide.kind;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Top-level `inputs:` (sibling of `invoke:` / `provide:`) carries the
|
|
764
|
+
// values passed to the dispatch target's invoke(). Validate against the
|
|
765
|
+
// target's declared `inputType` when both sides have one.
|
|
766
|
+
if (dispatchKind && md.inputs && typeof md.inputs === "object") {
|
|
767
|
+
const targetSchema = lookupDefinitionTypeField(
|
|
768
|
+
dispatchKind,
|
|
769
|
+
"inputType",
|
|
770
|
+
defs,
|
|
771
|
+
aliases,
|
|
772
|
+
allManifests as Record<string, any>[],
|
|
773
|
+
);
|
|
774
|
+
if (targetSchema) {
|
|
775
|
+
emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Top-level `result:` (a sibling, only meaningful with `provide:`) is a
|
|
780
|
+
// post-call mapping that must satisfy the abstract this definition
|
|
781
|
+
// `extends` (`outputType`). The target's outputType lives on `provide.kind`
|
|
782
|
+
// and is what `result` is typed against *inside* CEL — separate role.
|
|
783
|
+
if (
|
|
784
|
+
provide &&
|
|
785
|
+
typeof provide === "object" &&
|
|
786
|
+
!Array.isArray(provide) &&
|
|
787
|
+
md.result &&
|
|
788
|
+
typeof md.result === "object"
|
|
789
|
+
) {
|
|
790
|
+
const extendsValue = md.extends as string | undefined;
|
|
791
|
+
if (typeof extendsValue === "string" && extendsValue.length > 0) {
|
|
792
|
+
const abstractSchema = lookupDefinitionTypeField(
|
|
793
|
+
extendsValue,
|
|
794
|
+
"outputType",
|
|
795
|
+
defs,
|
|
796
|
+
aliases,
|
|
797
|
+
allManifests as Record<string, any>[],
|
|
798
|
+
);
|
|
799
|
+
if (abstractSchema) {
|
|
800
|
+
emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
615
806
|
// Validate CEL syntax and context variable access in all manifests
|
|
616
807
|
for (const m of allManifests) {
|
|
617
808
|
const resource = { kind: m.kind, name: m.metadata?.name as string };
|
|
@@ -670,11 +861,18 @@ export class StaticAnalyzer {
|
|
|
670
861
|
const manifestItem = matchedScope
|
|
671
862
|
? getManifestItem(path, matchedScope, m as Record<string, any>)
|
|
672
863
|
: (m as Record<string, any>);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
864
|
+
const rootForResolver = manifestRootForResolver(
|
|
865
|
+
m as Record<string, any>,
|
|
866
|
+
defs,
|
|
867
|
+
aliases,
|
|
676
868
|
allManifests as Record<string, any>[],
|
|
677
869
|
);
|
|
870
|
+
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
871
|
+
manifestRoot: rootForResolver,
|
|
872
|
+
defs,
|
|
873
|
+
aliases,
|
|
874
|
+
allManifests: allManifests as Record<string, any>[],
|
|
875
|
+
});
|
|
678
876
|
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
679
877
|
}
|
|
680
878
|
|
package/src/builtins.ts
CHANGED
|
@@ -40,7 +40,121 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
40
40
|
kind: "Telo.Definition",
|
|
41
41
|
metadata: { name: "Definition", module: "Telo" },
|
|
42
42
|
capability: "Telo.Template",
|
|
43
|
-
|
|
43
|
+
// Top-level shape stays open (`additionalProperties: true`) so this change
|
|
44
|
+
// attaches x-telo-context annotations to known template-body fields without
|
|
45
|
+
// tightening the Telo.Definition shape itself. The annotations drive
|
|
46
|
+
// static CEL validation of expressions inside `resources:` / `invoke:` /
|
|
47
|
+
// `run:` / `provide:` / top-level `inputs:` / top-level `result:` against
|
|
48
|
+
// `self` (typed from `schema:`) and `inputs` (typed from `inputType:`,
|
|
49
|
+
// falling back to the extends-declared abstract).
|
|
50
|
+
//
|
|
51
|
+
// `inputs:` and `result:` live as top-level siblings of `invoke:` / `provide:`,
|
|
52
|
+
// matching how Run.Sequence steps factor dispatch from data. The dispatch
|
|
53
|
+
// entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
|
|
54
|
+
// are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
|
|
55
|
+
schema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
additionalProperties: true,
|
|
58
|
+
properties: {
|
|
59
|
+
resources: {
|
|
60
|
+
type: "array",
|
|
61
|
+
items: {
|
|
62
|
+
type: "object",
|
|
63
|
+
additionalProperties: true,
|
|
64
|
+
"x-telo-context": {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
69
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
invoke: {
|
|
75
|
+
oneOf: [
|
|
76
|
+
{
|
|
77
|
+
type: "string",
|
|
78
|
+
"x-telo-context": {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: "object",
|
|
88
|
+
additionalProperties: true,
|
|
89
|
+
properties: {
|
|
90
|
+
kind: { type: "string" },
|
|
91
|
+
name: {
|
|
92
|
+
type: "string",
|
|
93
|
+
"x-telo-context": {
|
|
94
|
+
type: "object",
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
provide: {
|
|
106
|
+
type: "object",
|
|
107
|
+
additionalProperties: true,
|
|
108
|
+
properties: {
|
|
109
|
+
kind: { type: "string" },
|
|
110
|
+
name: {
|
|
111
|
+
type: "string",
|
|
112
|
+
"x-telo-context": {
|
|
113
|
+
type: "object",
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
properties: {
|
|
116
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
run: {
|
|
123
|
+
type: "string",
|
|
124
|
+
"x-telo-context": {
|
|
125
|
+
type: "object",
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
properties: {
|
|
128
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
inputs: {
|
|
133
|
+
type: "object",
|
|
134
|
+
additionalProperties: true,
|
|
135
|
+
"x-telo-context": {
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
properties: {
|
|
139
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
140
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
result: {
|
|
145
|
+
type: "object",
|
|
146
|
+
additionalProperties: true,
|
|
147
|
+
"x-telo-context": {
|
|
148
|
+
type: "object",
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
152
|
+
result: { "x-telo-context-from-ref-kind": "provide/kind#outputType" },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
44
158
|
},
|
|
45
159
|
{
|
|
46
160
|
kind: "Telo.Definition",
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
2
|
|
|
3
|
+
export interface ContextResolveOpts {
|
|
4
|
+
/** When provided, used to resolve `x-telo-context-from-root` annotations against the
|
|
5
|
+
* root manifest. When omitted, defaults to `manifestItem`. */
|
|
6
|
+
manifestRoot?: Record<string, any>;
|
|
7
|
+
/** When provided alongside `aliases`, used to resolve `x-telo-context-from-ref-kind`
|
|
8
|
+
* annotations: read a kind name from a path on `manifestRoot` and return the
|
|
9
|
+
* declared definition's `<field>` schema. */
|
|
10
|
+
defs?: {
|
|
11
|
+
resolve(kind: string): Record<string, any> | undefined;
|
|
12
|
+
};
|
|
13
|
+
aliases?: {
|
|
14
|
+
resolveKind(kind: string): string | undefined;
|
|
15
|
+
};
|
|
16
|
+
allManifests?: Record<string, any>[];
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
|
|
5
21
|
* - String: look up the named type in allManifests (Type.JsonSchema resources)
|
|
@@ -70,21 +86,61 @@ export function pathMatchesScope(exprPath: string, scope: string): boolean {
|
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
/**
|
|
73
|
-
* Resolves `x-telo-context
|
|
74
|
-
* manifest item
|
|
75
|
-
*
|
|
89
|
+
* Resolves `x-telo-context-*` annotations in a context schema using the concrete
|
|
90
|
+
* manifest item (per-scope) and the manifest root.
|
|
91
|
+
*
|
|
92
|
+
* Annotation forms:
|
|
93
|
+
*
|
|
94
|
+
* - `x-telo-context-from`: navigates `manifestItem.<path>` and treats the resolved
|
|
95
|
+
* value as a **property map** (keys → sub-schemas) that is merged into the
|
|
96
|
+
* annotated node's properties. Used for HTTP-style scopes where the navigated
|
|
97
|
+
* value is itself a map of variable names.
|
|
98
|
+
*
|
|
99
|
+
* Example: `x-telo-context-from: "request/schema"` reads `manifestItem.request.schema`
|
|
100
|
+
* (= `{ query: {...}, body: {...}, … }`) and merges those keys as named properties
|
|
101
|
+
* of the context node.
|
|
102
|
+
*
|
|
103
|
+
* - `x-telo-context-from-root`: navigates `manifestRoot.<path>` and **replaces** the
|
|
104
|
+
* annotated node's schema with the resolved value. Used on individual property
|
|
105
|
+
* schemas (e.g. `properties.self`) where the resolved value is a single variable's
|
|
106
|
+
* full schema, not a property map.
|
|
107
|
+
*
|
|
108
|
+
* Example: `properties.self.x-telo-context-from-root: "schema"` reads
|
|
109
|
+
* `manifestRoot.schema` and uses it as the schema of the `self` CEL variable.
|
|
110
|
+
*
|
|
111
|
+
* - `x-telo-context-from-ref-kind`: reads a kind name from `manifestRoot.<refPath>`,
|
|
112
|
+
* resolves it via the definition registry, and returns that kind's `<field>` schema
|
|
113
|
+
* (e.g. `outputType`/`inputType`). Used to type `result` against the dispatch
|
|
114
|
+
* target's declared output shape.
|
|
115
|
+
*
|
|
116
|
+
* Syntax: `<refPath>#<field>` — slashes traverse the manifest tree.
|
|
117
|
+
*
|
|
118
|
+
* Example: `x-telo-context-from-ref-kind: "provide/kind#outputType"` reads
|
|
119
|
+
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
120
|
+
* and returns the `outputType` schema.
|
|
76
121
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
122
|
+
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
123
|
+
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
124
|
+
*
|
|
125
|
+
* **Fallback chain.** When both `x-telo-context-from-root` and
|
|
126
|
+
* `x-telo-context-from-ref-kind` are present on the same node, the resolver tries
|
|
127
|
+
* `from-root` first; if that produces no usable schema, it falls back to `from-ref-kind`.
|
|
128
|
+
* This lets a definition declare typing from its own field with a sibling-kind fallback
|
|
129
|
+
* (e.g. `inputType` direct → `extends`-declared abstract's `inputType`).
|
|
80
130
|
*/
|
|
81
131
|
export function resolveContextAnnotations(
|
|
82
132
|
schema: Record<string, any>,
|
|
83
133
|
manifestItem: Record<string, any>,
|
|
84
|
-
|
|
134
|
+
opts?: ContextResolveOpts | Record<string, any>[],
|
|
85
135
|
): Record<string, any> {
|
|
86
136
|
if (!schema || typeof schema !== "object") return schema;
|
|
87
137
|
|
|
138
|
+
// Back-compat: third positional arg used to be `allManifests: Record<string, any>[]`.
|
|
139
|
+
const normalizedOpts: ContextResolveOpts = Array.isArray(opts)
|
|
140
|
+
? { allManifests: opts }
|
|
141
|
+
: (opts ?? {});
|
|
142
|
+
const { manifestRoot = manifestItem, defs, aliases, allManifests } = normalizedOpts;
|
|
143
|
+
|
|
88
144
|
const from = schema["x-telo-context-from"] as string | undefined;
|
|
89
145
|
if (from) {
|
|
90
146
|
const resolved = navigatePath(manifestItem, from.split("/")) as Record<string, any> | undefined;
|
|
@@ -96,6 +152,40 @@ export function resolveContextAnnotations(
|
|
|
96
152
|
};
|
|
97
153
|
}
|
|
98
154
|
|
|
155
|
+
const fromRoot = schema["x-telo-context-from-root"] as string | undefined;
|
|
156
|
+
const fromRefKind = schema["x-telo-context-from-ref-kind"] as string | undefined;
|
|
157
|
+
if (fromRoot || fromRefKind) {
|
|
158
|
+
if (fromRoot) {
|
|
159
|
+
const resolved = navigatePath(manifestRoot, fromRoot.split("/")) as
|
|
160
|
+
| Record<string, any>
|
|
161
|
+
| undefined;
|
|
162
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
163
|
+
return resolved;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (fromRefKind && defs) {
|
|
167
|
+
const hashIdx = fromRefKind.indexOf("#");
|
|
168
|
+
if (hashIdx > 0) {
|
|
169
|
+
const refPath = fromRefKind.slice(0, hashIdx);
|
|
170
|
+
const field = fromRefKind.slice(hashIdx + 1);
|
|
171
|
+
const kindValue = navigatePath(manifestRoot, refPath.split("/"));
|
|
172
|
+
if (typeof kindValue === "string" && kindValue.length > 0) {
|
|
173
|
+
const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
|
|
174
|
+
const def = defs.resolve(canonical);
|
|
175
|
+
const typeField = def
|
|
176
|
+
? (def as Record<string, unknown>)[field]
|
|
177
|
+
: undefined;
|
|
178
|
+
const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
|
|
179
|
+
if (resolved && typeof resolved === "object") {
|
|
180
|
+
return resolved;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Open fallback so unresolved types never produce false-positive CEL diagnostics.
|
|
186
|
+
return { type: "object", additionalProperties: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
99
189
|
const refFrom = schema["x-telo-context-ref-from"] as string | undefined;
|
|
100
190
|
if (refFrom && allManifests) {
|
|
101
191
|
const slashIdx = refFrom.indexOf("/");
|
|
@@ -129,7 +219,7 @@ export function resolveContextAnnotations(
|
|
|
129
219
|
if (schema.properties) {
|
|
130
220
|
const props: Record<string, any> = {};
|
|
131
221
|
for (const [k, v] of Object.entries(schema.properties)) {
|
|
132
|
-
props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem,
|
|
222
|
+
props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem, normalizedOpts);
|
|
133
223
|
}
|
|
134
224
|
return { ...schema, properties: props };
|
|
135
225
|
}
|