@telorun/analyzer 0.10.1 → 1.1.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/README.md +3 -3
- package/dist/analysis-registry.d.ts +7 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +38 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +198 -6
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +158 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +9 -11
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +26 -14
- package/dist/position-metadata.d.ts +5 -1
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +8 -1
- package/dist/reference-field-map.d.ts +21 -4
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +35 -19
- package/dist/residual-schema.d.ts +23 -0
- package/dist/residual-schema.d.ts.map +1 -0
- package/dist/residual-schema.js +45 -0
- package/dist/rewrite-synthetic-origins.d.ts +10 -0
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
- package/dist/rewrite-synthetic-origins.js +55 -0
- package/dist/validate-cel-context.d.ts +61 -7
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +90 -8
- package/dist/validate-provider-coherence.d.ts +23 -0
- package/dist/validate-provider-coherence.d.ts.map +1 -0
- package/dist/validate-provider-coherence.js +148 -0
- package/dist/validate-references.js +24 -24
- package/package.json +5 -3
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +240 -8
- package/src/builtins.ts +158 -1
- package/src/index.ts +1 -0
- package/src/kernel-globals.ts +9 -11
- package/src/normalize-inline-resources.ts +48 -13
- package/src/position-metadata.ts +8 -1
- package/src/reference-field-map.ts +46 -18
- package/src/residual-schema.ts +49 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/validate-cel-context.ts +111 -8
- package/src/validate-provider-coherence.ts +166 -0
- package/src/validate-references.ts +24 -24
|
@@ -82,7 +82,14 @@ export function normalizeInlineResources(
|
|
|
82
82
|
);
|
|
83
83
|
|
|
84
84
|
const invocationContext = isRefEntry(entry) ? entry.context : undefined;
|
|
85
|
-
const extracted = extractInlinesAtPath(
|
|
85
|
+
const extracted = extractInlinesAtPath(
|
|
86
|
+
resource,
|
|
87
|
+
fieldPath,
|
|
88
|
+
parentName,
|
|
89
|
+
resource.kind,
|
|
90
|
+
parentModule,
|
|
91
|
+
invocationContext,
|
|
92
|
+
);
|
|
86
93
|
for (const manifest of extracted) {
|
|
87
94
|
result.push(manifest);
|
|
88
95
|
queue.push(manifest as ResourceManifest & { metadata: { name: string } });
|
|
@@ -99,18 +106,42 @@ export function normalizeInlineResources(
|
|
|
99
106
|
* Walks `resource` following `fieldPath` (dot notation, `[]` = array traversal).
|
|
100
107
|
* Mutates the resource in-place: replaces each inline value with `{kind, name}`.
|
|
101
108
|
* Returns the extracted manifests.
|
|
109
|
+
*
|
|
110
|
+
* Each extracted manifest carries `metadata.xTeloOrigin` so downstream
|
|
111
|
+
* diagnostics can be rerouted back to the parent doc's YAML position:
|
|
112
|
+
* - `parentKind` / `parentName` — the resource that owned the inline slot
|
|
113
|
+
* - `pathFromParent` — concrete dotted path with `[N]` indices, matching
|
|
114
|
+
* `buildPositionIndex` keys (e.g. `routes[0].handler`)
|
|
102
115
|
*/
|
|
103
116
|
function extractInlinesAtPath(
|
|
104
117
|
resource: ResourceManifest,
|
|
105
118
|
fieldPath: string,
|
|
106
119
|
parentName: string,
|
|
120
|
+
parentKind: string,
|
|
107
121
|
parentModule: string | undefined,
|
|
108
122
|
invocationContext?: Record<string, any>,
|
|
109
123
|
): ResourceManifest[] {
|
|
110
124
|
const extracted: ResourceManifest[] = [];
|
|
111
125
|
const parts = fieldPath.split(".");
|
|
112
126
|
|
|
113
|
-
function
|
|
127
|
+
function emit(
|
|
128
|
+
inline: Record<string, unknown>,
|
|
129
|
+
nameSegments: string[],
|
|
130
|
+
concretePath: string,
|
|
131
|
+
): string {
|
|
132
|
+
const name = sanitizeName([parentName, ...nameSegments].join("_"));
|
|
133
|
+
extracted.push(
|
|
134
|
+
buildManifest(inline, name, parentKind, parentName, concretePath, parentModule, invocationContext),
|
|
135
|
+
);
|
|
136
|
+
return name;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function traverse(
|
|
140
|
+
obj: unknown,
|
|
141
|
+
partsLeft: string[],
|
|
142
|
+
nameParts: string[],
|
|
143
|
+
pathSoFar: string,
|
|
144
|
+
): void {
|
|
114
145
|
if (!obj || typeof obj !== "object" || partsLeft.length === 0) return;
|
|
115
146
|
|
|
116
147
|
const [head, ...rest] = partsLeft;
|
|
@@ -123,15 +154,15 @@ function extractInlinesAtPath(
|
|
|
123
154
|
const elem = container[mapKey];
|
|
124
155
|
if (!elem || typeof elem !== "object") continue;
|
|
125
156
|
const sanitizedKey = sanitizeName(mapKey);
|
|
157
|
+
const childPath = pathSoFar ? `${pathSoFar}.${mapKey}` : mapKey;
|
|
126
158
|
|
|
127
159
|
if (rest.length === 0) {
|
|
128
160
|
if (isInlineResource(elem as Record<string, unknown>)) {
|
|
129
|
-
const name =
|
|
130
|
-
extracted.push(buildManifest(elem as Record<string, unknown>, name, parentModule, invocationContext));
|
|
161
|
+
const name = emit(elem as Record<string, unknown>, [...nameParts, sanitizedKey], childPath);
|
|
131
162
|
container[mapKey] = { kind: (elem as Record<string, unknown>).kind, name };
|
|
132
163
|
}
|
|
133
164
|
} else {
|
|
134
|
-
traverse(elem, rest, [...nameParts, sanitizedKey]);
|
|
165
|
+
traverse(elem, rest, [...nameParts, sanitizedKey], childPath);
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
168
|
return;
|
|
@@ -142,6 +173,7 @@ function extractInlinesAtPath(
|
|
|
142
173
|
const container = obj as Record<string, unknown>;
|
|
143
174
|
const val = container[key];
|
|
144
175
|
if (val == null) return;
|
|
176
|
+
const keyPath = pathSoFar ? `${pathSoFar}.${key}` : key;
|
|
145
177
|
|
|
146
178
|
if (isArr) {
|
|
147
179
|
if (!Array.isArray(val)) return;
|
|
@@ -152,39 +184,41 @@ function extractInlinesAtPath(
|
|
|
152
184
|
typeof (elem as Record<string, unknown>).name === "string"
|
|
153
185
|
? ((elem as Record<string, unknown>).name as string)
|
|
154
186
|
: String(idx);
|
|
187
|
+
const childPath = `${keyPath}[${idx}]`;
|
|
155
188
|
|
|
156
189
|
if (rest.length === 0) {
|
|
157
190
|
// Array element itself is the ref slot
|
|
158
191
|
if (isInlineResource(elem as Record<string, unknown>)) {
|
|
159
|
-
const name =
|
|
160
|
-
extracted.push(buildManifest(elem as Record<string, unknown>, name, parentModule, invocationContext));
|
|
192
|
+
const name = emit(elem as Record<string, unknown>, [...nameParts, key, elemId], childPath);
|
|
161
193
|
val[idx] = { kind: (elem as Record<string, unknown>).kind, name };
|
|
162
194
|
}
|
|
163
195
|
} else {
|
|
164
|
-
traverse(elem, rest, [...nameParts, key, elemId]);
|
|
196
|
+
traverse(elem, rest, [...nameParts, key, elemId], childPath);
|
|
165
197
|
}
|
|
166
198
|
}
|
|
167
199
|
} else {
|
|
168
200
|
if (rest.length === 0) {
|
|
169
201
|
// val is the ref slot
|
|
170
202
|
if (val && typeof val === "object" && !Array.isArray(val) && isInlineResource(val as Record<string, unknown>)) {
|
|
171
|
-
const name =
|
|
172
|
-
extracted.push(buildManifest(val as Record<string, unknown>, name, parentModule, invocationContext));
|
|
203
|
+
const name = emit(val as Record<string, unknown>, [...nameParts, key], keyPath);
|
|
173
204
|
container[key] = { kind: (val as Record<string, unknown>).kind, name };
|
|
174
205
|
}
|
|
175
206
|
} else {
|
|
176
|
-
traverse(val, rest, [...nameParts, key]);
|
|
207
|
+
traverse(val, rest, [...nameParts, key], keyPath);
|
|
177
208
|
}
|
|
178
209
|
}
|
|
179
210
|
}
|
|
180
211
|
|
|
181
|
-
traverse(resource, parts, []);
|
|
212
|
+
traverse(resource, parts, [], "");
|
|
182
213
|
return extracted;
|
|
183
214
|
}
|
|
184
215
|
|
|
185
216
|
function buildManifest(
|
|
186
217
|
inline: Record<string, unknown>,
|
|
187
218
|
name: string,
|
|
219
|
+
parentKind: string,
|
|
220
|
+
parentName: string,
|
|
221
|
+
pathFromParent: string,
|
|
188
222
|
parentModule: string | undefined,
|
|
189
223
|
invocationContext?: Record<string, any>,
|
|
190
224
|
): ResourceManifest {
|
|
@@ -200,6 +234,7 @@ function buildManifest(
|
|
|
200
234
|
// Inherit parent module only if the inline doesn't already declare one
|
|
201
235
|
...(parentModule && !existingMeta.module ? { module: parentModule } : {}),
|
|
202
236
|
...(invocationContext ? { xTeloInvocationContext: invocationContext } : {}),
|
|
237
|
+
xTeloOrigin: { parentKind, parentName, pathFromParent },
|
|
203
238
|
},
|
|
204
|
-
} as ResourceManifest;
|
|
239
|
+
} as unknown as ResourceManifest;
|
|
205
240
|
}
|
package/src/position-metadata.ts
CHANGED
|
@@ -63,7 +63,11 @@ function offsetToPosition(offset: number, lineOffsets: number[]): Position {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** Walks the YAML AST and records source ranges for every field value, keyed
|
|
66
|
-
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path").
|
|
66
|
+
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path").
|
|
67
|
+
* Map keys are also recorded under the `@key:<path>` namespace so diagnostic
|
|
68
|
+
* resolvers can squiggle just the key identifier instead of the full value
|
|
69
|
+
* block — used when a diagnostic targets a missing child property and the
|
|
70
|
+
* resolver has to fall back to the parent. */
|
|
67
71
|
export function buildPositionIndex(doc: Document, lineOffsets: number[]): PositionIndex {
|
|
68
72
|
const index: PositionIndex = new Map();
|
|
69
73
|
|
|
@@ -83,6 +87,9 @@ export function buildPositionIndex(doc: Document, lineOffsets: number[]): Positi
|
|
|
83
87
|
const key = isScalar(pair.key) ? String(pair.key.value) : null;
|
|
84
88
|
if (key == null) continue;
|
|
85
89
|
const childPath = path ? `${path}.${key}` : key;
|
|
90
|
+
if (pair.key && (pair.key as any).range) {
|
|
91
|
+
recordNode(pair.key, `@key:${childPath}`);
|
|
92
|
+
}
|
|
86
93
|
if (pair.value != null) {
|
|
87
94
|
recordNode(pair.value, childPath);
|
|
88
95
|
walk(pair.value, childPath);
|
|
@@ -63,23 +63,39 @@ export function isInlineResource(val: Record<string, unknown>): boolean {
|
|
|
63
63
|
return true;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
/** A value found at a field-map path, paired with the concrete path that
|
|
67
|
+
* produced it. `path` has every `[]` substituted with `[N]` and every `{}`
|
|
68
|
+
* substituted with the actual map key, matching the format produced by
|
|
69
|
+
* `buildPositionIndex`. Used so diagnostics emitted against a specific
|
|
70
|
+
* array element / map entry can be resolved back to a YAML range. */
|
|
71
|
+
export interface ResolvedFieldEntry {
|
|
72
|
+
value: unknown;
|
|
73
|
+
path: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Resolves all `{value, path}` entries at a field map path in a resource
|
|
77
|
+
* config. The returned `path` is the concrete dotted path produced by the
|
|
78
|
+
* substitutions below, matching the format `buildPositionIndex` keys on.
|
|
79
|
+
* Path-segment markers accepted in the input `path`:
|
|
80
|
+
* - `[]` iterate array values at this key, substituting `[N]` per item
|
|
81
|
+
* (e.g. `routes[]` → `routes[0]`, `routes[1]`, …).
|
|
69
82
|
* - `{}` iterate map values (every value in an `additionalProperties`-typed
|
|
70
83
|
* object — used for fields like `content[mime]` whose schema declares
|
|
71
|
-
* a key-as-MIME map).
|
|
72
|
-
|
|
84
|
+
* a key-as-MIME map). Substituted with the literal map key joined by
|
|
85
|
+
* a dot, so the input `content.{}.encoder` yields concrete paths
|
|
86
|
+
* like `content.application/json.encoder`. */
|
|
87
|
+
export function resolveFieldEntries(obj: unknown, path: string): ResolvedFieldEntry[] {
|
|
73
88
|
const parts = path.split(".");
|
|
74
|
-
let current:
|
|
89
|
+
let current: ResolvedFieldEntry[] = [{ value: obj, path: "" }];
|
|
75
90
|
for (const part of parts) {
|
|
76
91
|
if (part === "{}") {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const next: ResolvedFieldEntry[] = [];
|
|
93
|
+
for (const entry of current) {
|
|
94
|
+
if (!entry.value || typeof entry.value !== "object") continue;
|
|
95
|
+
for (const [k, v] of Object.entries(entry.value as Record<string, unknown>)) {
|
|
96
|
+
if (v != null) {
|
|
97
|
+
next.push({ value: v, path: entry.path ? `${entry.path}.${k}` : k });
|
|
98
|
+
}
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
current = next;
|
|
@@ -87,19 +103,31 @@ export function resolveFieldValues(obj: unknown, path: string): unknown[] {
|
|
|
87
103
|
}
|
|
88
104
|
const isArray = part.endsWith("[]");
|
|
89
105
|
const key = isArray ? part.slice(0, -2) : part;
|
|
90
|
-
const next:
|
|
91
|
-
for (const
|
|
92
|
-
if (!
|
|
93
|
-
const val = (
|
|
106
|
+
const next: ResolvedFieldEntry[] = [];
|
|
107
|
+
for (const entry of current) {
|
|
108
|
+
if (!entry.value || typeof entry.value !== "object") continue;
|
|
109
|
+
const val = (entry.value as Record<string, unknown>)[key];
|
|
94
110
|
if (val == null) continue;
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
const basePath = entry.path ? `${entry.path}.${key}` : key;
|
|
112
|
+
if (isArray && Array.isArray(val)) {
|
|
113
|
+
for (let i = 0; i < val.length; i++) {
|
|
114
|
+
if (val[i] != null) next.push({ value: val[i], path: `${basePath}[${i}]` });
|
|
115
|
+
}
|
|
116
|
+
} else if (!isArray) {
|
|
117
|
+
next.push({ value: val, path: basePath });
|
|
118
|
+
}
|
|
97
119
|
}
|
|
98
120
|
current = next;
|
|
99
121
|
}
|
|
100
122
|
return current;
|
|
101
123
|
}
|
|
102
124
|
|
|
125
|
+
/** Backwards-compat wrapper that drops the concrete path. Prefer
|
|
126
|
+
* `resolveFieldEntries` for new code that wants positions. */
|
|
127
|
+
export function resolveFieldValues(obj: unknown, path: string): unknown[] {
|
|
128
|
+
return resolveFieldEntries(obj, path).map((e) => e.value);
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
/**
|
|
104
132
|
* Traverses a definition's JSON Schema once and returns a field map recording every
|
|
105
133
|
* x-telo-ref slot and every x-telo-scope slot.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the residual JSON Schema for a `variables` / `secrets` entry.
|
|
3
|
+
*
|
|
4
|
+
* For Telo.Application env-binding entries (those with an `env:` key), strips
|
|
5
|
+
* the kernel-specific wrapper keys `env` and `default` — `default` here is
|
|
6
|
+
* the *fallback host value* the kernel coerces when the env var is unset, not
|
|
7
|
+
* a JSON Schema annotation, so it must not leak into the validator.
|
|
8
|
+
*
|
|
9
|
+
* For Telo.Library entries (no `env:`), passes the entry through unchanged.
|
|
10
|
+
* Library `default:` is a standard JSON Schema annotation and stays.
|
|
11
|
+
*
|
|
12
|
+
* Single source of truth for "residual schema" referenced by both the
|
|
13
|
+
* analyzer's CEL globals normalization and the kernel's runtime env-var
|
|
14
|
+
* resolver — keeping them aligned prevents the two surfaces from drifting.
|
|
15
|
+
*/
|
|
16
|
+
export function residualEntrySchema(
|
|
17
|
+
entry: Record<string, unknown> | null | undefined,
|
|
18
|
+
): Record<string, unknown> {
|
|
19
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
20
|
+
return { type: "object", additionalProperties: true };
|
|
21
|
+
}
|
|
22
|
+
const isAppEnvBinding = "env" in entry;
|
|
23
|
+
const out: Record<string, unknown> = {};
|
|
24
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
25
|
+
if (isAppEnvBinding && (key === "env" || key === "default")) continue;
|
|
26
|
+
out[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply `residualEntrySchema` to every entry in a `variables` / `secrets` map.
|
|
33
|
+
* Returns a property-map suitable for use as the inner schema of CEL's
|
|
34
|
+
* `variables` / `secrets` namespaces.
|
|
35
|
+
*/
|
|
36
|
+
export function residualEntrySchemaMap(
|
|
37
|
+
entries: Record<string, unknown> | null | undefined,
|
|
38
|
+
): Record<string, Record<string, unknown>> {
|
|
39
|
+
const out: Record<string, Record<string, unknown>> = {};
|
|
40
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
44
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
45
|
+
out[name] = residualEntrySchema(value as Record<string, unknown>);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
|
|
4
|
+
interface XTeloOrigin {
|
|
5
|
+
parentKind: string;
|
|
6
|
+
parentName: string;
|
|
7
|
+
pathFromParent: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readOrigin(manifest: ResourceManifest | undefined): XTeloOrigin | undefined {
|
|
11
|
+
if (!manifest) return undefined;
|
|
12
|
+
const origin = (manifest.metadata as { xTeloOrigin?: XTeloOrigin } | undefined)?.xTeloOrigin;
|
|
13
|
+
if (
|
|
14
|
+
!origin ||
|
|
15
|
+
typeof origin.parentKind !== "string" ||
|
|
16
|
+
typeof origin.parentName !== "string" ||
|
|
17
|
+
typeof origin.pathFromParent !== "string"
|
|
18
|
+
) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return origin;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Diagnostics emitted on synthetic manifests (resources extracted by
|
|
25
|
+
* `normalizeInlineResources`) carry the synthetic's identity in
|
|
26
|
+
* `data.resource`, which has no YAML source. Rewrite each such diagnostic
|
|
27
|
+
* back to the chain root: walk up `metadata.xTeloOrigin` until a manifest
|
|
28
|
+
* with no origin is reached, and prepend each hop's `pathFromParent` to
|
|
29
|
+
* `data.path` so position-index lookups against the root doc resolve. */
|
|
30
|
+
export function rewriteSyntheticOrigins(
|
|
31
|
+
diagnostics: AnalysisDiagnostic[],
|
|
32
|
+
manifests: ResourceManifest[],
|
|
33
|
+
): AnalysisDiagnostic[] {
|
|
34
|
+
const byName = new Map<string, ResourceManifest>();
|
|
35
|
+
for (const m of manifests) {
|
|
36
|
+
const name = m.metadata?.name;
|
|
37
|
+
if (typeof name === "string") byName.set(name, m);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return diagnostics.map((d) => {
|
|
41
|
+
const data = d.data as
|
|
42
|
+
| { resource?: { kind?: string; name?: string }; path?: string; filePath?: string }
|
|
43
|
+
| undefined;
|
|
44
|
+
if (!data?.resource?.name) return d;
|
|
45
|
+
|
|
46
|
+
let current = byName.get(data.resource.name);
|
|
47
|
+
let origin = readOrigin(current);
|
|
48
|
+
if (!origin) return d;
|
|
49
|
+
|
|
50
|
+
let accumPath = typeof data.path === "string" ? data.path : "";
|
|
51
|
+
let rootKind: string = origin.parentKind;
|
|
52
|
+
let rootName: string = origin.parentName;
|
|
53
|
+
|
|
54
|
+
while (origin) {
|
|
55
|
+
accumPath = accumPath ? `${origin.pathFromParent}.${accumPath}` : origin.pathFromParent;
|
|
56
|
+
rootKind = origin.parentKind;
|
|
57
|
+
rootName = origin.parentName;
|
|
58
|
+
current = byName.get(origin.parentName);
|
|
59
|
+
origin = readOrigin(current);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rootFilePath =
|
|
63
|
+
(current?.metadata as { source?: string } | undefined)?.source ?? data.filePath;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...d,
|
|
67
|
+
data: {
|
|
68
|
+
...data,
|
|
69
|
+
resource: { kind: rootKind, name: rootName },
|
|
70
|
+
filePath: rootFilePath,
|
|
71
|
+
path: accumPath,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -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,66 @@ 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.
|
|
121
|
+
*
|
|
122
|
+
* Accepts either a single string or an array of strings. With an array, paths
|
|
123
|
+
* are tried in order and the first one that resolves to a usable schema wins —
|
|
124
|
+
* used by `result:` to find its dispatch target under whichever entry-point
|
|
125
|
+
* field (`provide:` or `invoke:`) the definition declares.
|
|
76
126
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
127
|
+
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
128
|
+
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
129
|
+
*
|
|
130
|
+
* **Fallback chain.** When both `x-telo-context-from-root` and
|
|
131
|
+
* `x-telo-context-from-ref-kind` are present on the same node, the resolver tries
|
|
132
|
+
* `from-root` first; if that produces no usable schema, it falls back to `from-ref-kind`.
|
|
133
|
+
* This lets a definition declare typing from its own field with a sibling-kind fallback
|
|
134
|
+
* (e.g. `inputType` direct → `extends`-declared abstract's `inputType`).
|
|
80
135
|
*/
|
|
81
136
|
export function resolveContextAnnotations(
|
|
82
137
|
schema: Record<string, any>,
|
|
83
138
|
manifestItem: Record<string, any>,
|
|
84
|
-
|
|
139
|
+
opts?: ContextResolveOpts | Record<string, any>[],
|
|
85
140
|
): Record<string, any> {
|
|
86
141
|
if (!schema || typeof schema !== "object") return schema;
|
|
87
142
|
|
|
143
|
+
// Back-compat: third positional arg used to be `allManifests: Record<string, any>[]`.
|
|
144
|
+
const normalizedOpts: ContextResolveOpts = Array.isArray(opts)
|
|
145
|
+
? { allManifests: opts }
|
|
146
|
+
: (opts ?? {});
|
|
147
|
+
const { manifestRoot = manifestItem, defs, aliases, allManifests } = normalizedOpts;
|
|
148
|
+
|
|
88
149
|
const from = schema["x-telo-context-from"] as string | undefined;
|
|
89
150
|
if (from) {
|
|
90
151
|
const resolved = navigatePath(manifestItem, from.split("/")) as Record<string, any> | undefined;
|
|
@@ -96,6 +157,48 @@ export function resolveContextAnnotations(
|
|
|
96
157
|
};
|
|
97
158
|
}
|
|
98
159
|
|
|
160
|
+
const fromRoot = schema["x-telo-context-from-root"] as string | undefined;
|
|
161
|
+
const fromRefKindRaw = schema["x-telo-context-from-ref-kind"] as
|
|
162
|
+
| string
|
|
163
|
+
| string[]
|
|
164
|
+
| undefined;
|
|
165
|
+
const fromRefKinds = fromRefKindRaw == null
|
|
166
|
+
? []
|
|
167
|
+
: Array.isArray(fromRefKindRaw)
|
|
168
|
+
? fromRefKindRaw
|
|
169
|
+
: [fromRefKindRaw];
|
|
170
|
+
if (fromRoot || fromRefKinds.length > 0) {
|
|
171
|
+
if (fromRoot) {
|
|
172
|
+
const resolved = navigatePath(manifestRoot, fromRoot.split("/")) as
|
|
173
|
+
| Record<string, any>
|
|
174
|
+
| undefined;
|
|
175
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
176
|
+
return resolved;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (defs) {
|
|
180
|
+
for (const fromRefKind of fromRefKinds) {
|
|
181
|
+
const hashIdx = fromRefKind.indexOf("#");
|
|
182
|
+
if (hashIdx <= 0) continue;
|
|
183
|
+
const refPath = fromRefKind.slice(0, hashIdx);
|
|
184
|
+
const field = fromRefKind.slice(hashIdx + 1);
|
|
185
|
+
const kindValue = navigatePath(manifestRoot, refPath.split("/"));
|
|
186
|
+
if (typeof kindValue !== "string" || kindValue.length === 0) continue;
|
|
187
|
+
const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
|
|
188
|
+
const def = defs.resolve(canonical);
|
|
189
|
+
const typeField = def
|
|
190
|
+
? (def as Record<string, unknown>)[field]
|
|
191
|
+
: undefined;
|
|
192
|
+
const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
|
|
193
|
+
if (resolved && typeof resolved === "object") {
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Open fallback so unresolved types never produce false-positive CEL diagnostics.
|
|
199
|
+
return { type: "object", additionalProperties: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
99
202
|
const refFrom = schema["x-telo-context-ref-from"] as string | undefined;
|
|
100
203
|
if (refFrom && allManifests) {
|
|
101
204
|
const slashIdx = refFrom.indexOf("/");
|
|
@@ -129,7 +232,7 @@ export function resolveContextAnnotations(
|
|
|
129
232
|
if (schema.properties) {
|
|
130
233
|
const props: Record<string, any> = {};
|
|
131
234
|
for (const [k, v] of Object.entries(schema.properties)) {
|
|
132
|
-
props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem,
|
|
235
|
+
props[k] = resolveContextAnnotations(v as Record<string, any>, manifestItem, normalizedOpts);
|
|
133
236
|
}
|
|
134
237
|
return { ...schema, properties: props };
|
|
135
238
|
}
|