@telorun/analyzer 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +35 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -470
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +38 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +66 -22
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/invocation-contract.d.ts +11 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +15 -0
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/schema-compat.d.ts +59 -22
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +60 -75
- package/dist/schema-error-report.d.ts +68 -0
- package/dist/schema-error-report.d.ts.map +1 -0
- package/dist/schema-error-report.js +356 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-nested-inline.d.ts +22 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +17 -9
- package/dist/validate-step-inputs.d.ts +17 -0
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +108 -9
- package/package.json +2 -2
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +83 -587
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +79 -24
- package/src/find-manifest.ts +19 -0
- package/src/index.ts +23 -2
- package/src/invocation-contract.ts +22 -0
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-path.ts +34 -0
- package/src/schema-compat.ts +92 -79
- package/src/schema-error-report.ts +417 -0
- package/src/schema-walk.ts +144 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-nested-inline.ts +35 -14
- package/src/validate-step-inputs.ts +153 -11
package/src/cel-scope.ts
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **What is in scope for CEL at one expression site.**
|
|
3
|
+
*
|
|
4
|
+
* A CEL expression is typed against an environment and a resolved context
|
|
5
|
+
* schema that depend on WHERE it sits: the kind's step body contributes
|
|
6
|
+
* `steps.<name>.result`, an error-bearing branch contributes `error`, a
|
|
7
|
+
* `x-telo-bindings-from` field contributes its named bindings, an
|
|
8
|
+
* `x-telo-context` region contributes its own scope resolved against the
|
|
9
|
+
* enclosing array item, and the kernel globals are merged in per declaring
|
|
10
|
+
* module. That assembly used to live inline in the analysis pass, built for one
|
|
11
|
+
* `engine.analyze` call and discarded — so nothing outside the pass could ask
|
|
12
|
+
* what a cursor sees.
|
|
13
|
+
*
|
|
14
|
+
* It is a QUERY here, and the pass is one of its callers. The other is the IDE:
|
|
15
|
+
* completion, hover and signature help must offer exactly the names
|
|
16
|
+
* `telo check` accepts, and two implementations of an open, growing scope rule
|
|
17
|
+
* cannot be held in agreement by tests.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here reports a diagnostic. The pass keeps every check it ever had;
|
|
20
|
+
* what moved is the answer both halves need first.
|
|
21
|
+
*/
|
|
22
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
23
|
+
import type { Environment } from "@marcbachmann/cel-js";
|
|
24
|
+
import { AliasResolver, type ModuleScopes } from "./alias-resolver.js";
|
|
25
|
+
import {
|
|
26
|
+
bindingContextProperties,
|
|
27
|
+
bindingPathChain,
|
|
28
|
+
BINDINGS_ANNOTATION,
|
|
29
|
+
schemaAtChain,
|
|
30
|
+
} from "./cel-bindings.js";
|
|
31
|
+
import { buildImportInputCelEnvironment, buildTypedCelEnvironment } from "./cel-environment.js";
|
|
32
|
+
import { DefinitionRegistry } from "./definition-registry.js";
|
|
33
|
+
import { type ContractDirection, effectiveAuthorSchema } from "./extends-resolution.js";
|
|
34
|
+
import {
|
|
35
|
+
analyzerContractScope,
|
|
36
|
+
PERMISSIVE_CONTRACT,
|
|
37
|
+
resolveContract,
|
|
38
|
+
} from "./invocation-contract.js";
|
|
39
|
+
import {
|
|
40
|
+
mergeKernelGlobalsIntoContext,
|
|
41
|
+
type KernelGlobalsIndex,
|
|
42
|
+
} from "./kernel-globals.js";
|
|
43
|
+
import { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
|
|
44
|
+
import { readStepSlot } from "./step-slot.js";
|
|
45
|
+
import {
|
|
46
|
+
getManifestItem,
|
|
47
|
+
resolveContextAnnotations,
|
|
48
|
+
resolveTypeFieldToSchema,
|
|
49
|
+
} from "./validate-cel-context.js";
|
|
50
|
+
|
|
51
|
+
/** Build a closed JSON Schema for the `self` CEL variable available inside a
|
|
52
|
+
* `Telo.Definition` template body. Mirrors the runtime template controller's
|
|
53
|
+
* `const self = { ...resource, name: resource.metadata.name };` — every
|
|
54
|
+
* property the user declared in `schema:` plus synthetic `name` / `kind` and
|
|
55
|
+
* the metadata sub-object (kept open since metadata legitimately carries
|
|
56
|
+
* arbitrary user-added fields). */
|
|
57
|
+
function buildSelfSchema(
|
|
58
|
+
definition: Record<string, any>,
|
|
59
|
+
defs?: DefinitionRegistry,
|
|
60
|
+
aliases?: AliasResolver,
|
|
61
|
+
): Record<string, any> {
|
|
62
|
+
// The author-facing schema resolves inheritance: with `base:` the child's own
|
|
63
|
+
// schema (the parent's config is internal); without it, `merge(parent, own)`.
|
|
64
|
+
const userSchema = (
|
|
65
|
+
defs
|
|
66
|
+
? effectiveAuthorSchema(definition as unknown as ResourceDefinition, (k) =>
|
|
67
|
+
defs.resolve(aliases?.resolveKind(k) ?? k) ?? defs.resolve(k),
|
|
68
|
+
)
|
|
69
|
+
: (definition.schema ?? {})
|
|
70
|
+
) as Record<string, any>;
|
|
71
|
+
const userProps = (userSchema.properties ?? {}) as Record<string, any>;
|
|
72
|
+
const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
|
|
73
|
+
return {
|
|
74
|
+
type: "object",
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
properties: {
|
|
77
|
+
...userProps,
|
|
78
|
+
name: { type: "string" },
|
|
79
|
+
kind: { type: "string" },
|
|
80
|
+
metadata: {
|
|
81
|
+
type: "object",
|
|
82
|
+
additionalProperties: true,
|
|
83
|
+
properties: { name: { type: "string" } },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: [...userRequired, "name", "kind"],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
91
|
+
* invocable template body — the shared contract resolver applied to the
|
|
92
|
+
* definition itself, so a body is typed against the exact signature callers are
|
|
93
|
+
* checked against and dispatch enforces. Walks the whole `extends` chain rather
|
|
94
|
+
* than one hop, so a definition two levels below the declaration still gets
|
|
95
|
+
* typed inputs. Undefined when nothing in the chain declares a contract —
|
|
96
|
+
* the caller signals opaque `map<string, dyn>` upstream. */
|
|
97
|
+
function lookupTemplateInputsSchema(
|
|
98
|
+
definition: Record<string, any>,
|
|
99
|
+
defs: DefinitionRegistry,
|
|
100
|
+
aliases: AliasResolver,
|
|
101
|
+
allManifests: Record<string, any>[],
|
|
102
|
+
scopes: ModuleScopes,
|
|
103
|
+
): Record<string, any> | undefined {
|
|
104
|
+
return resolveContract(
|
|
105
|
+
"inputType",
|
|
106
|
+
undefined,
|
|
107
|
+
definition as unknown as ResourceDefinition,
|
|
108
|
+
analyzerContractScope(defs, aliases, scopes, allManifests),
|
|
109
|
+
)?.schema;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
113
|
+
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
114
|
+
* have been pre-augmented:
|
|
115
|
+
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
116
|
+
* - `inputType` → resolved through the shared contract resolver, so
|
|
117
|
+
* `x-telo-context-from-root: inputType` substitutes the
|
|
118
|
+
* real signature. Without it the annotation would replace
|
|
119
|
+
* the node verbatim with the inline `{kind, schema}` wrapper
|
|
120
|
+
* the standard library writes everywhere, typing `inputs` as
|
|
121
|
+
* `{kind, schema}` instead of the declared properties.
|
|
122
|
+
*
|
|
123
|
+
* For non-definition manifests the original object is returned. */
|
|
124
|
+
export function manifestRootForResolver(
|
|
125
|
+
m: Record<string, any>,
|
|
126
|
+
defs: DefinitionRegistry,
|
|
127
|
+
aliases: AliasResolver,
|
|
128
|
+
allManifests: Record<string, any>[],
|
|
129
|
+
scopes: ModuleScopes,
|
|
130
|
+
): Record<string, any> {
|
|
131
|
+
if (m.kind !== "Telo.Definition") return m;
|
|
132
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
|
|
133
|
+
return {
|
|
134
|
+
...m,
|
|
135
|
+
schema: buildSelfSchema(m, defs, aliases),
|
|
136
|
+
...(inputs ? { inputType: inputs } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a `steps` context schema for a kind's step body.
|
|
142
|
+
* Walks each step in the manifest array, resolves the invoked resource's output
|
|
143
|
+
* contract, and builds `steps.<name>.result` context entries.
|
|
144
|
+
*
|
|
145
|
+
* Resolution is the shared {@link resolveContract} — the invoked resource
|
|
146
|
+
* manifest's own declaration, then the kind's, resolved to the nearest
|
|
147
|
+
* declaration along `extends`, then permissive. Sharing it with the kernel is
|
|
148
|
+
* what stops `telo check` from typing `steps.X.result` against one contract
|
|
149
|
+
* while dispatch validates against another.
|
|
150
|
+
*
|
|
151
|
+
* The kind layer is what makes `x-telo-stream` properties on definitions
|
|
152
|
+
* actually govern step-result chain validation — without it, the validator falls
|
|
153
|
+
* back to permissive and the stream-opacity rule never fires.
|
|
154
|
+
*
|
|
155
|
+
* Recursion into nested step arrays is annotation-driven via
|
|
156
|
+
* `x-telo-topology-role`. The analyzer recognises three role values:
|
|
157
|
+
* - `branch` — value is an array of steps (e.g. then / else / do / catch).
|
|
158
|
+
* - `branch-list`— value is an array of objects each carrying further roled
|
|
159
|
+
* sub-properties (e.g. elseif: [{ if, then }]).
|
|
160
|
+
* - `case-map` — value is an object whose values are step arrays (e.g. cases).
|
|
161
|
+
* No specific Run.Sequence field name is hardcoded; any kind that uses
|
|
162
|
+
* a step body and tags its branch fields with these roles works.
|
|
163
|
+
*/
|
|
164
|
+
export function buildStepContextSchema(
|
|
165
|
+
manifest: Record<string, any>,
|
|
166
|
+
defSchema: Record<string, any>,
|
|
167
|
+
allManifests: Record<string, any>[],
|
|
168
|
+
defs: DefinitionRegistry,
|
|
169
|
+
aliases: AliasResolver,
|
|
170
|
+
scopes: ModuleScopes,
|
|
171
|
+
): Record<string, any> | undefined {
|
|
172
|
+
const props = defSchema.properties as Record<string, any> | undefined;
|
|
173
|
+
if (!props) return undefined;
|
|
174
|
+
|
|
175
|
+
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
176
|
+
const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
177
|
+
|
|
178
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
179
|
+
const stepCtx = readStepSlot(fieldSchema);
|
|
180
|
+
if (!stepCtx) continue;
|
|
181
|
+
|
|
182
|
+
const invokeField = stepCtx.invoke;
|
|
183
|
+
const outputTypeField = stepCtx.outputType;
|
|
184
|
+
// Optional: the field a step uses to produce a result without dispatching.
|
|
185
|
+
// Only a kind that declares one has pure steps at all.
|
|
186
|
+
const valueField = stepCtx.value;
|
|
187
|
+
if (!invokeField || !outputTypeField) continue;
|
|
188
|
+
|
|
189
|
+
const steps = manifest[fieldName];
|
|
190
|
+
if (!Array.isArray(steps)) continue;
|
|
191
|
+
|
|
192
|
+
const stepItemSchema = resolveLocalRef(
|
|
193
|
+
fieldSchema.items as Record<string, any> | undefined,
|
|
194
|
+
defSchema,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// The instance's own input contract, for typing a pure step that just
|
|
198
|
+
// forwards one of its values.
|
|
199
|
+
const ownInputs = resolveTypeFieldToSchema(
|
|
200
|
+
(manifest as Record<string, any>).inputType,
|
|
201
|
+
allManifests,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const stepProperties: Record<string, any> = {};
|
|
205
|
+
|
|
206
|
+
walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
|
|
207
|
+
const name = s.name;
|
|
208
|
+
const invoke = s[invokeField] as Record<string, any> | undefined;
|
|
209
|
+
// Only invoke steps register a `steps.<name>.result` entry — control-flow
|
|
210
|
+
// wrappers (try/if/while/switch/throw) don't produce a result and must
|
|
211
|
+
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
212
|
+
// or unknown step references slip through chain validation.
|
|
213
|
+
if (typeof name !== "string") return;
|
|
214
|
+
if (!invoke || typeof invoke !== "object") {
|
|
215
|
+
// A pure step dispatches nothing, so there is no contract to resolve.
|
|
216
|
+
// Where its expression is a plain chain into something already typed —
|
|
217
|
+
// an earlier step's result, or the kind's own inputs — that type carries
|
|
218
|
+
// through; anything else (arithmetic, a call, a comprehension) stays
|
|
219
|
+
// permissive rather than guessed. Same rule as a named binding's.
|
|
220
|
+
if (valueField && valueField in s) {
|
|
221
|
+
const scopeRoot = {
|
|
222
|
+
properties: {
|
|
223
|
+
steps: { type: "object", properties: { ...stepProperties } },
|
|
224
|
+
...(ownInputs ? { inputs: ownInputs } : {}),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const chained = schemaAtChain(bindingPathChain(s[valueField]), scopeRoot);
|
|
228
|
+
stepProperties[name] = {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: { result: chained ?? PERMISSIVE_CONTRACT },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const invokedKind = invoke.kind as string | undefined;
|
|
236
|
+
const invokedName = invoke.name as string | undefined;
|
|
237
|
+
// A named `!ref` carries the target's own manifest (which may narrow the
|
|
238
|
+
// contract for this one instance); an inline `{ kind, ... }` step IS the
|
|
239
|
+
// manifest. Either way the kind layer resolves through `extends`.
|
|
240
|
+
const invokedManifest = invokedName
|
|
241
|
+
? (allManifests.find(
|
|
242
|
+
(m) =>
|
|
243
|
+
(m.metadata as any)?.name === invokedName && (!invokedKind || m.kind === invokedKind),
|
|
244
|
+
) as Record<string, any> | undefined)
|
|
245
|
+
: (invoke as Record<string, any>);
|
|
246
|
+
const invokedDef = invokedKind
|
|
247
|
+
? contractScope.resolveIn(invokedKind, readingModule)
|
|
248
|
+
: undefined;
|
|
249
|
+
const outputSchema = resolveContract(
|
|
250
|
+
outputTypeField as ContractDirection,
|
|
251
|
+
invokedManifest,
|
|
252
|
+
invokedDef,
|
|
253
|
+
contractScope,
|
|
254
|
+
)?.schema;
|
|
255
|
+
stepProperties[name] = {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
result: outputSchema ?? PERMISSIVE_CONTRACT,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (Object.keys(stepProperties).length > 0) {
|
|
264
|
+
return {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: stepProperties,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function collectErrorContextScopes(
|
|
275
|
+
defSchema: Record<string, any> | undefined,
|
|
276
|
+
): Map<string, Record<string, any>> {
|
|
277
|
+
const out = new Map<string, Record<string, any>>();
|
|
278
|
+
if (!defSchema || typeof defSchema !== "object") return out;
|
|
279
|
+
const seen = new Set<Record<string, any>>();
|
|
280
|
+
|
|
281
|
+
const walk = (schema: Record<string, any> | undefined): void => {
|
|
282
|
+
if (!schema || typeof schema !== "object" || seen.has(schema)) return;
|
|
283
|
+
seen.add(schema);
|
|
284
|
+
|
|
285
|
+
const props = schema.properties as Record<string, any> | undefined;
|
|
286
|
+
if (props) {
|
|
287
|
+
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
288
|
+
if (fieldSchema && typeof fieldSchema === "object") {
|
|
289
|
+
const errCtx = (fieldSchema as Record<string, any>)["x-telo-error-context"];
|
|
290
|
+
if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
|
|
291
|
+
out.set(fieldName, errCtx as Record<string, any>);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
walk(resolveLocalRef(fieldSchema as Record<string, any>, defSchema));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (schema.items) walk(resolveLocalRef(schema.items as Record<string, any>, defSchema));
|
|
298
|
+
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
299
|
+
const arr = schema[key];
|
|
300
|
+
if (Array.isArray(arr)) for (const sub of arr) walk(resolveLocalRef(sub, defSchema));
|
|
301
|
+
}
|
|
302
|
+
if (schema.$defs && typeof schema.$defs === "object") {
|
|
303
|
+
for (const sub of Object.values(schema.$defs as Record<string, any>)) {
|
|
304
|
+
walk(sub as Record<string, any>);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
walk(defSchema);
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Return the error-context schema for a CEL `path` when the path lies within
|
|
315
|
+
* (any depth under) one of the error-bearing fields, else undefined. A path is
|
|
316
|
+
* "within" field `f` when it contains a segment `f[<index>]`. When multiple
|
|
317
|
+
* error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
|
|
318
|
+
* deepest — the one whose segment appears latest in the path — wins, so the
|
|
319
|
+
* innermost branch's schema governs.
|
|
320
|
+
*/
|
|
321
|
+
export function errorContextForPath(
|
|
322
|
+
path: string,
|
|
323
|
+
scopes: Map<string, Record<string, any>>,
|
|
324
|
+
): Record<string, any> | undefined {
|
|
325
|
+
let best: { index: number; schema: Record<string, any> } | undefined;
|
|
326
|
+
for (const [fieldName, schema] of scopes) {
|
|
327
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
328
|
+
for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
|
|
329
|
+
if (best === undefined || match.index > best.index) {
|
|
330
|
+
best = { index: match.index, schema };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return best?.schema;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Add a kind's named bindings to a resolved context, when the context declares
|
|
338
|
+
* a bindings region. They go UNDER the context's own properties: a scope
|
|
339
|
+
* variable wins over a same-named binding at runtime, so static typing has to
|
|
340
|
+
* agree (the collision itself is `BINDING_NAME_RESERVED`). */
|
|
341
|
+
export function withBindingNames(
|
|
342
|
+
contextSchema: Record<string, any>,
|
|
343
|
+
resource: Record<string, any>,
|
|
344
|
+
): Record<string, any> {
|
|
345
|
+
const field = contextSchema[BINDINGS_ANNOTATION];
|
|
346
|
+
if (typeof field !== "string") return contextSchema;
|
|
347
|
+
const bindings = resource[field];
|
|
348
|
+
if (bindings === null || typeof bindings !== "object" || Array.isArray(bindings)) {
|
|
349
|
+
return contextSchema;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
...contextSchema,
|
|
353
|
+
properties: {
|
|
354
|
+
...bindingContextProperties(bindings as Record<string, unknown>, contextSchema),
|
|
355
|
+
...(contextSchema.properties ?? {}),
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* What one CEL expression is typed against.
|
|
362
|
+
*
|
|
363
|
+
* Both halves, never a flattened name list: the environment answers "what
|
|
364
|
+
* names exist and what type does this expression have", the context schema
|
|
365
|
+
* answers "what shape does that name carry" — which is what a hover tooltip and
|
|
366
|
+
* a member completion are made of, and what a name list throws away.
|
|
367
|
+
*/
|
|
368
|
+
export interface CelScope {
|
|
369
|
+
/** The environment typed for this expression's path. */
|
|
370
|
+
env: Environment;
|
|
371
|
+
/** The resolved `x-telo-context` schema merged with the kernel globals, or
|
|
372
|
+
* null when no context applied (the environment alone types the site). */
|
|
373
|
+
contextSchema: Record<string, any> | null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The analyzer state a scope is resolved against — everything a manifest set
|
|
377
|
+
* contributes, gathered once per analysis. */
|
|
378
|
+
export interface CelScopeInputs {
|
|
379
|
+
/** The base (untyped) CEL environment. */
|
|
380
|
+
celEnv: Environment;
|
|
381
|
+
defs: DefinitionRegistry;
|
|
382
|
+
aliases: AliasResolver;
|
|
383
|
+
scopes: ModuleScopes;
|
|
384
|
+
allManifests: ResourceManifest[];
|
|
385
|
+
kernelGlobals: KernelGlobalsIndex;
|
|
386
|
+
/** The module doc carrying the Application-only `ports` namespace. */
|
|
387
|
+
moduleManifest?: ResourceManifest;
|
|
388
|
+
/** The observed-state-only context, used where no `x-telo-context` matched. */
|
|
389
|
+
observedStateContext: Record<string, any> | null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** One CEL site, as the manifest visitor reports it. Structural on purpose —
|
|
393
|
+
* the resolver takes the fields it reads, not the visitor's event type, so a
|
|
394
|
+
* caller that located a site some other way (a cursor in an editor buffer) can
|
|
395
|
+
* ask the same question. */
|
|
396
|
+
export interface CelSiteRef {
|
|
397
|
+
source: ResourceManifest;
|
|
398
|
+
path: string;
|
|
399
|
+
contextSchema?: Record<string, any>;
|
|
400
|
+
matchedScope?: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The CEL scope rule, applied per resource and then per expression.
|
|
405
|
+
*
|
|
406
|
+
* Stateful across a resource because the per-resource half — the step context,
|
|
407
|
+
* the error-bearing regions, the invocation context — is derived from the kind's
|
|
408
|
+
* schema once and read by every expression in that resource. `enterResource`
|
|
409
|
+
* establishes it; `scopeFor` answers for one path.
|
|
410
|
+
*/
|
|
411
|
+
export class CelScopeResolver {
|
|
412
|
+
private readonly typedEnvByManifest = new Map<ResourceManifest, Environment>();
|
|
413
|
+
|
|
414
|
+
/** Per-resource state, replaced at each `enterResource`. */
|
|
415
|
+
private stepContext: Record<string, any> | undefined;
|
|
416
|
+
private invocationContext: Record<string, any> | undefined;
|
|
417
|
+
private errorScopes: Map<string, Record<string, any>> = new Map();
|
|
418
|
+
|
|
419
|
+
constructor(private readonly inputs: CelScopeInputs) {}
|
|
420
|
+
|
|
421
|
+
/** The `steps` context schema for the current resource, or undefined when its
|
|
422
|
+
* kind declares no step body. Exposed because the step-inputs check needs the
|
|
423
|
+
* same schema this resource's expressions are typed against — recomputing it
|
|
424
|
+
* there is how the two would come to disagree. */
|
|
425
|
+
get stepContextSchema(): Record<string, any> | undefined {
|
|
426
|
+
return this.stepContext;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** The error-bearing regions the current resource's kind declares. */
|
|
430
|
+
get errorContextScopes(): ReadonlyMap<string, Record<string, any>> {
|
|
431
|
+
return this.errorScopes;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The resource-wide invocation context, when this resource is an inline
|
|
435
|
+
* declaration that carries one. Read by the "CEL in a non-eval field" check:
|
|
436
|
+
* such a resource's CEL is evaluated by the enclosing kind, not by an
|
|
437
|
+
* `x-telo-eval` annotation of its own. */
|
|
438
|
+
get invocationContextSchema(): Record<string, any> | undefined {
|
|
439
|
+
return this.invocationContext;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Establish the per-resource half of the scope. */
|
|
443
|
+
enterResource(m: ResourceManifest, definition: ResourceDefinition | undefined): void {
|
|
444
|
+
const { allManifests, defs, aliases, scopes } = this.inputs;
|
|
445
|
+
this.invocationContext = (m.metadata as any)?.xTeloInvocationContext as
|
|
446
|
+
| Record<string, any>
|
|
447
|
+
| undefined;
|
|
448
|
+
this.stepContext = definition?.schema
|
|
449
|
+
? buildStepContextSchema(
|
|
450
|
+
m as Record<string, any>,
|
|
451
|
+
definition.schema as Record<string, any>,
|
|
452
|
+
allManifests as Record<string, any>[],
|
|
453
|
+
defs,
|
|
454
|
+
aliases,
|
|
455
|
+
scopes,
|
|
456
|
+
)
|
|
457
|
+
: undefined;
|
|
458
|
+
this.errorScopes = collectErrorContextScopes(
|
|
459
|
+
definition?.schema as Record<string, any> | undefined,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* What the expression at `site` is typed against.
|
|
465
|
+
*
|
|
466
|
+
* The environment is cached per manifest when no context applied, which is
|
|
467
|
+
* most expressions: it then depends only on the manifest, so rebuilding it per
|
|
468
|
+
* expression is pure waste — a clone plus a re-registration of every variable,
|
|
469
|
+
* on every keystroke in the IDE. A matched context makes it path-specific (its
|
|
470
|
+
* schema is resolved against the enclosing array item), so those build fresh
|
|
471
|
+
* rather than risk one item's types leaking into another's.
|
|
472
|
+
*/
|
|
473
|
+
scopeFor(site: CelSiteRef): CelScope {
|
|
474
|
+
const m = site.source;
|
|
475
|
+
const contextSchema = this.resolveContextFor(site);
|
|
476
|
+
const {
|
|
477
|
+
celEnv,
|
|
478
|
+
allManifests,
|
|
479
|
+
moduleManifest,
|
|
480
|
+
} = this.inputs;
|
|
481
|
+
|
|
482
|
+
const cached = contextSchema === null ? this.typedEnvByManifest.get(m) : undefined;
|
|
483
|
+
const env =
|
|
484
|
+
cached ??
|
|
485
|
+
// A `Telo.Import`'s variables/secrets are a config-only contract evaluated
|
|
486
|
+
// in the IMPORTING module's scope, so they type from the owning module doc
|
|
487
|
+
// and drop `resources`/`env`, making a reference to either an error.
|
|
488
|
+
(m.kind === "Telo.Import"
|
|
489
|
+
? buildImportInputCelEnvironment(
|
|
490
|
+
celEnv,
|
|
491
|
+
allManifests.find(
|
|
492
|
+
(mm) =>
|
|
493
|
+
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
494
|
+
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
495
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
: buildTypedCelEnvironment(
|
|
499
|
+
celEnv,
|
|
500
|
+
m,
|
|
501
|
+
contextSchema ?? undefined,
|
|
502
|
+
moduleManifest,
|
|
503
|
+
));
|
|
504
|
+
if (contextSchema === null && !cached) this.typedEnvByManifest.set(m, env);
|
|
505
|
+
|
|
506
|
+
return { env, contextSchema };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The context schema in force at `site`: the matched `x-telo-context` (or the
|
|
510
|
+
* resource-wide invocation context), plus the step and error regions this
|
|
511
|
+
* path falls in, resolved and merged with the kernel globals. */
|
|
512
|
+
private resolveContextFor(site: CelSiteRef): Record<string, any> | null {
|
|
513
|
+
const { path } = site;
|
|
514
|
+
const m = site.source;
|
|
515
|
+
let matched: Record<string, any> | undefined = site.contextSchema ?? this.invocationContext;
|
|
516
|
+
|
|
517
|
+
if (this.stepContext) {
|
|
518
|
+
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
519
|
+
matched = {
|
|
520
|
+
...base,
|
|
521
|
+
properties: { ...(base.properties ?? {}), steps: this.stepContext },
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// `error` is only in scope inside an error-bearing branch (e.g. a
|
|
526
|
+
// `catch:` / `finally:`), so it's merged per-path, not resource-wide.
|
|
527
|
+
const errorSchema =
|
|
528
|
+
this.errorScopes.size > 0 ? errorContextForPath(path, this.errorScopes) : undefined;
|
|
529
|
+
if (errorSchema) {
|
|
530
|
+
const base = matched ?? { type: "object", properties: {}, additionalProperties: true };
|
|
531
|
+
matched = {
|
|
532
|
+
...base,
|
|
533
|
+
properties: { ...(base.properties ?? {}), error: errorSchema },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!matched) {
|
|
538
|
+
// No `x-telo-context` matched, so nothing was chain-validated here before.
|
|
539
|
+
// Validate the observed-state segment alone rather than merging the kernel
|
|
540
|
+
// globals, whose closed `variables` / `ports` nodes would newly reject
|
|
541
|
+
// reads that pass today.
|
|
542
|
+
return this.inputs.observedStateContext;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const { defs, aliases, scopes, allManifests, kernelGlobals } = this.inputs;
|
|
546
|
+
const manifestItem = site.matchedScope
|
|
547
|
+
? getManifestItem(path, site.matchedScope, m as Record<string, any>)
|
|
548
|
+
: (m as Record<string, any>);
|
|
549
|
+
const rootForResolver = manifestRootForResolver(
|
|
550
|
+
m as Record<string, any>,
|
|
551
|
+
defs,
|
|
552
|
+
aliases,
|
|
553
|
+
allManifests as Record<string, any>[],
|
|
554
|
+
scopes,
|
|
555
|
+
);
|
|
556
|
+
const resolved = resolveContextAnnotations(matched, manifestItem, {
|
|
557
|
+
manifestRoot: rootForResolver,
|
|
558
|
+
defs,
|
|
559
|
+
aliases,
|
|
560
|
+
allManifests: allManifests as Record<string, any>[],
|
|
561
|
+
});
|
|
562
|
+
return mergeKernelGlobalsIntoContext(
|
|
563
|
+
withBindingNames(resolved, m as Record<string, any>),
|
|
564
|
+
// Typed in the module that DECLARED this resource — for a manifest
|
|
565
|
+
// forwarded from an imported library, that is its `moduleGlobals` stamp,
|
|
566
|
+
// not the consuming application's block.
|
|
567
|
+
kernelGlobals.forResource(m),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|