@telorun/analyzer 0.63.0 → 0.64.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.
Files changed (52) hide show
  1. package/dist/analysis-registry.d.ts +24 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +35 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +57 -467
  7. package/dist/cel-scope-query.d.ts +109 -0
  8. package/dist/cel-scope-query.d.ts.map +1 -0
  9. package/dist/cel-scope-query.js +270 -0
  10. package/dist/cel-scope.d.ts +166 -0
  11. package/dist/cel-scope.d.ts.map +1 -0
  12. package/dist/cel-scope.js +377 -0
  13. package/dist/definition-registry.d.ts +15 -0
  14. package/dist/definition-registry.d.ts.map +1 -1
  15. package/dist/definition-registry.js +25 -9
  16. package/dist/find-manifest.d.ts +10 -0
  17. package/dist/find-manifest.d.ts.map +1 -0
  18. package/dist/find-manifest.js +12 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -0
  22. package/dist/invocation-contract.d.ts +11 -0
  23. package/dist/invocation-contract.d.ts.map +1 -1
  24. package/dist/invocation-contract.js +15 -0
  25. package/dist/manifest-analysis.d.ts +73 -0
  26. package/dist/manifest-analysis.d.ts.map +1 -0
  27. package/dist/manifest-analysis.js +78 -0
  28. package/dist/manifest-path.d.ts +18 -0
  29. package/dist/manifest-path.d.ts.map +1 -0
  30. package/dist/manifest-path.js +37 -0
  31. package/dist/schema-walk.d.ts +25 -0
  32. package/dist/schema-walk.d.ts.map +1 -0
  33. package/dist/schema-walk.js +126 -0
  34. package/dist/telo-version.d.ts +1 -1
  35. package/dist/telo-version.js +1 -1
  36. package/dist/validate-step-inputs.d.ts +17 -0
  37. package/dist/validate-step-inputs.d.ts.map +1 -1
  38. package/dist/validate-step-inputs.js +97 -6
  39. package/package.json +2 -2
  40. package/src/analysis-registry.ts +37 -0
  41. package/src/analyzer.ts +61 -585
  42. package/src/cel-scope-query.ts +337 -0
  43. package/src/cel-scope.ts +570 -0
  44. package/src/definition-registry.ts +31 -13
  45. package/src/find-manifest.ts +19 -0
  46. package/src/index.ts +13 -0
  47. package/src/invocation-contract.ts +22 -0
  48. package/src/manifest-analysis.ts +132 -0
  49. package/src/manifest-path.ts +34 -0
  50. package/src/schema-walk.ts +144 -0
  51. package/src/telo-version.ts +1 -1
  52. package/src/validate-step-inputs.ts +143 -7
package/dist/analyzer.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { canonicalTypeSchemaId, OBSERVED_STATE_KEY } from "@telorun/sdk";
2
2
  import { defaultRegistry, isRefSentinel, isTaggedSentinel, plainChainOf, } from "@telorun/templating";
3
- import { AliasResolver, moduleScopedDefResolver, scopeResolverForModule, } from "./alias-resolver.js";
4
- import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
3
+ import { AliasResolver, scopeResolverForModule, } from "./alias-resolver.js";
4
+ import { buildCelEnvironment, } from "./cel-environment.js";
5
5
  import { DefinitionRegistry } from "./definition-registry.js";
6
6
  import { effectiveAuthorSchema } from "./extends-resolution.js";
7
- import { PERMISSIVE_CONTRACT, resolveContract, } from "./invocation-contract.js";
7
+ import { analyzerContractScope, resolveContract, } from "./invocation-contract.js";
8
8
  import { buildCallGraph } from "./call-graph.js";
9
9
  import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
10
- import { buildKernelGlobalsIndex, KERNEL_GLOBAL_NAMES, mergeKernelGlobalsIntoContext, } from "./kernel-globals.js";
10
+ import { buildKernelGlobalsIndex, KERNEL_GLOBAL_NAMES, } from "./kernel-globals.js";
11
11
  import { buildObservedStateIndex, buildObservedStateResourcesSchema, collectRunReachableNames, observedStateRead, validateObservedStateDeclarations, } from "./validate-observed-state.js";
12
12
  import { computeSuggestKind } from "./kind-suggest.js";
13
13
  import { visitManifest } from "./manifest-visitor.js";
@@ -19,7 +19,14 @@ import { resolveSchemaRefKinds } from "./resolve-schema-ref-kinds.js";
19
19
  import { runZoneAnalysis } from "./resolve-zone-requirements.js";
20
20
  import { validateDurableRegions } from "./validate-durable-regions.js";
21
21
  import { validateZoneViolations } from "./validate-zone-violations.js";
22
- import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
22
+ import { resolveLocalRef, walkStepArray } from "./schema-walk.js";
23
+ import { CelScopeResolver } from "./cel-scope.js";
24
+ // The structural walks and the CEL scope rule moved out of this file — the
25
+ // first so both halves can reach them, the second so the IDE can ask what a
26
+ // cursor sees without pulling the analysis pass in behind it. Re-exported here
27
+ // because they were part of this module's surface before the split.
28
+ export { gatherPropertySchemas, resolveLocalRef, walkStepArray } from "./schema-walk.js";
29
+ export { analyzerContractScope } from "./invocation-contract.js";
23
30
  import { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
24
31
  import { validateSchemaProjection, } from "./validate-schema-projection.js";
25
32
  import { evaluateResourceRules, reportResourceRules, reportUnexercisedRule, ruleExercised, validateResourceRuleDeclarations, } from "./validate-resource-rules.js";
@@ -35,9 +42,9 @@ import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
35
42
  import { celTypeSatisfiesJsonSchema, checkSchemaCompatibility, navigateSchemaToExprPath, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
36
43
  import { collectValueSchemaIssues } from "./validate-value-schema.js";
37
44
  import { DiagnosticSeverity } from "./types.js";
38
- import { extractAccessChains, extractCelRegionScopes, getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
45
+ import { extractAccessChains, extractCelRegionScopes, pathMatchesScope, } from "./validate-cel-context.js";
39
46
  import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
40
- import { BINDINGS_ANNOTATION, bindingContextProperties, bindingPathChain, findBindingSites, resolveBindingOrder, schemaAtChain, } from "./cel-bindings.js";
47
+ import { BINDINGS_ANNOTATION, findBindingSites, resolveBindingOrder, } from "./cel-bindings.js";
41
48
  import { CEL_RESERVED_WORDS, checkName } from "./identifier-name.js";
42
49
  import { validateIdentifierNames } from "./validate-identifier-names.js";
43
50
  import { validateExtends } from "./validate-extends.js";
@@ -48,7 +55,7 @@ import { validateModuleMetadata } from "./validate-module-metadata.js";
48
55
  import { validateRequires } from "./validate-requires.js";
49
56
  import { validateBaseMapping } from "./validate-base-mapping.js";
50
57
  import { validateInvocationContract } from "./validate-invocation-contract.js";
51
- import { collectStepInputIssues } from "./validate-step-inputs.js";
58
+ import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
52
59
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
53
60
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
54
61
  import { validateReferences } from "./validate-references.js";
@@ -99,83 +106,7 @@ function resolveSelfOrAlias(value, ownModule, scopeResolver) {
99
106
  }
100
107
  return scopeResolver.resolveKind(value);
101
108
  }
102
- /** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
103
- * resolve in the module that declared the definition they were read off (so an
104
- * `extends` chain crossing module boundaries re-scopes at every hop), and named
105
- * `telo#Type` references resolve against the flattened manifest list. `resolveIn`
106
- * is the top-level entry point, where the kind was written by the READING
107
- * module and there is no declaring definition yet. */
108
- export function analyzerContractScope(defs, aliases, scopes, allManifests) {
109
- const resolve = moduleScopedDefResolver(defs, aliases, scopes);
110
- return {
111
- resolveDefinition: resolve,
112
- resolveIn: resolve.in,
113
- typeManifestsFor: () => allManifests,
114
- };
115
- }
116
109
  const SOURCE = "telo-analyzer";
117
- /** Build a closed JSON Schema for the `self` CEL variable available inside a
118
- * `Telo.Definition` template body. Mirrors the runtime template controller's
119
- * `const self = { ...resource, name: resource.metadata.name };` — every
120
- * property the user declared in `schema:` plus synthetic `name` / `kind` and
121
- * the metadata sub-object (kept open since metadata legitimately carries
122
- * arbitrary user-added fields). */
123
- function buildSelfSchema(definition, defs, aliases) {
124
- // The author-facing schema resolves inheritance: with `base:` the child's own
125
- // schema (the parent's config is internal); without it, `merge(parent, own)`.
126
- const userSchema = (defs
127
- ? effectiveAuthorSchema(definition, (k) => defs.resolve(aliases?.resolveKind(k) ?? k) ?? defs.resolve(k))
128
- : (definition.schema ?? {}));
129
- const userProps = (userSchema.properties ?? {});
130
- const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
131
- return {
132
- type: "object",
133
- additionalProperties: false,
134
- properties: {
135
- ...userProps,
136
- name: { type: "string" },
137
- kind: { type: "string" },
138
- metadata: {
139
- type: "object",
140
- additionalProperties: true,
141
- properties: { name: { type: "string" } },
142
- },
143
- },
144
- required: [...userRequired, "name", "kind"],
145
- };
146
- }
147
- /** Build the JSON Schema for the `inputs` CEL variable available inside an
148
- * invocable template body — the shared contract resolver applied to the
149
- * definition itself, so a body is typed against the exact signature callers are
150
- * checked against and dispatch enforces. Walks the whole `extends` chain rather
151
- * than one hop, so a definition two levels below the declaration still gets
152
- * typed inputs. Undefined when nothing in the chain declares a contract —
153
- * the caller signals opaque `map<string, dyn>` upstream. */
154
- function lookupTemplateInputsSchema(definition, defs, aliases, allManifests, scopes) {
155
- return resolveContract("inputType", undefined, definition, analyzerContractScope(defs, aliases, scopes, allManifests))?.schema;
156
- }
157
- /** Returns a "resolver-facing" view of the manifest where the fields used as
158
- * navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
159
- * have been pre-augmented:
160
- * - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
161
- * - `inputType` → resolved through the shared contract resolver, so
162
- * `x-telo-context-from-root: inputType` substitutes the
163
- * real signature. Without it the annotation would replace
164
- * the node verbatim with the inline `{kind, schema}` wrapper
165
- * the standard library writes everywhere, typing `inputs` as
166
- * `{kind, schema}` instead of the declared properties.
167
- *
168
- * For non-definition manifests the original object is returned. */
169
- function manifestRootForResolver(m, defs, aliases, allManifests, scopes) {
170
- if (m.kind !== "Telo.Definition")
171
- return m;
172
- const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
173
- return {
174
- ...m,
175
- schema: buildSelfSchema(m, defs, aliases),
176
- ...(inputs ? { inputType: inputs } : {}),
177
- };
178
- }
179
110
  /** True when an issue reports a property that is absent — its path points at a
180
111
  * node the manifest does not contain. */
181
112
  export const missingRequired = (issue) => /is missing required property/.test(issue.message);
@@ -202,228 +133,6 @@ function contractOwnerLabel(definition, contract) {
202
133
  }
203
134
  return canonical;
204
135
  }
205
- /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
206
- * Non-refs and unresolved refs pass through unchanged. */
207
- export function resolveLocalRef(schema, root) {
208
- if (!schema)
209
- return undefined;
210
- const ref = schema.$ref;
211
- if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
212
- const defName = ref.slice("#/$defs/".length);
213
- const resolved = root.$defs?.[defName];
214
- if (resolved && typeof resolved === "object")
215
- return resolved;
216
- }
217
- // A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
218
- // Resolved HERE rather than by each walker: this is the one chokepoint every
219
- // structural walk already goes through — the step-array walks, the call graph,
220
- // the zone projection, the eval-path collector — so a composer that points at a
221
- // shared shape stays legible to all of them at once. Nothing is inlined into
222
- // the stored schema, which keeps validator-cache identity stable and matches
223
- // what `resolveSchemaTypeRefs` does for a named user type.
224
- if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
225
- const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
226
- const resolved = ManifestRootSchema.$defs[defName];
227
- if (resolved && typeof resolved === "object")
228
- return resolved;
229
- }
230
- return schema;
231
- }
232
- const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
233
- /** Gather property schemas from a (possibly variant-bearing) object schema:
234
- * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
235
- *
236
- * Each branch is resolved through {@link resolveLocalRef} first, so a branch
237
- * that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
238
- * site — contributes its properties like an inline one. Without that, pointing a
239
- * composer at a shared shape would silently empty every role-driven lookup that
240
- * reads this (the inputs slot, the retry policy, the eval paths), which is a
241
- * failure with no diagnostic attached to it. */
242
- export function gatherPropertySchemas(schema, root) {
243
- const out = [];
244
- const base = resolveLocalRef(schema, root ?? schema) ?? schema;
245
- if (base.properties && typeof base.properties === "object") {
246
- for (const [k, v] of Object.entries(base.properties)) {
247
- out.push([k, v]);
248
- }
249
- }
250
- for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
251
- const arr = base[variantKey];
252
- if (!Array.isArray(arr))
253
- continue;
254
- for (const raw of arr) {
255
- if (!raw || typeof raw !== "object")
256
- continue;
257
- const variant = resolveLocalRef(raw, root ?? schema) ?? raw;
258
- if (variant.properties) {
259
- for (const [k, v] of Object.entries(variant.properties)) {
260
- out.push([k, v]);
261
- }
262
- }
263
- }
264
- }
265
- return out;
266
- }
267
- /**
268
- * Generic, role-driven walk over a step array. Calls
269
- * `visit(step, stepPath)` for every step — top-level and nested through the
270
- * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
271
- * the single definition of how steps nest, shared by `buildStepContextSchema`
272
- * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
273
- * checks invoke refs), so the topology contract lives in one place — adding a
274
- * role or nesting form updates both consumers at once. No resource kind is
275
- * hardcoded; recursion is driven entirely by the schema annotations.
276
- */
277
- export function walkStepArray(steps, stepItemSchema, rootSchema, basePath, visit) {
278
- const dispatchRole = (data, role, itemsSchema, path) => {
279
- if (role === "branch" && Array.isArray(data)) {
280
- walkStepArray(data, stepItemSchema, rootSchema, path, visit);
281
- }
282
- else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
283
- for (const [caseKey, arr] of Object.entries(data)) {
284
- if (Array.isArray(arr))
285
- walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
286
- }
287
- }
288
- else if (role === "branch-list" && Array.isArray(data)) {
289
- const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
290
- if (!entrySchema)
291
- return;
292
- data.forEach((entry, i) => {
293
- if (!entry || typeof entry !== "object")
294
- return;
295
- for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
296
- const subRole = subSchema["x-telo-topology-role"];
297
- if (typeof subRole !== "string")
298
- continue;
299
- dispatchRole(entry[subKey], subRole, subSchema.items, `${path}[${i}].${subKey}`);
300
- }
301
- });
302
- }
303
- };
304
- steps.forEach((step, i) => {
305
- if (!step || typeof step !== "object")
306
- return;
307
- const s = step;
308
- const stepPath = `${basePath}[${i}]`;
309
- visit(s, stepPath);
310
- if (!stepItemSchema)
311
- return;
312
- for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
313
- const role = propSchema["x-telo-topology-role"];
314
- if (typeof role !== "string")
315
- continue;
316
- dispatchRole(s[propKey], role, propSchema.items, `${stepPath}.${propKey}`);
317
- }
318
- });
319
- }
320
- /**
321
- * Build a `steps` context schema for a kind's step body.
322
- * Walks each step in the manifest array, resolves the invoked resource's output
323
- * contract, and builds `steps.<name>.result` context entries.
324
- *
325
- * Resolution is the shared {@link resolveContract} — the invoked resource
326
- * manifest's own declaration, then the kind's, resolved to the nearest
327
- * declaration along `extends`, then permissive. Sharing it with the kernel is
328
- * what stops `telo check` from typing `steps.X.result` against one contract
329
- * while dispatch validates against another.
330
- *
331
- * The kind layer is what makes `x-telo-stream` properties on definitions
332
- * actually govern step-result chain validation — without it, the validator falls
333
- * back to permissive and the stream-opacity rule never fires.
334
- *
335
- * Recursion into nested step arrays is annotation-driven via
336
- * `x-telo-topology-role`. The analyzer recognises three role values:
337
- * - `branch` — value is an array of steps (e.g. then / else / do / catch).
338
- * - `branch-list`— value is an array of objects each carrying further roled
339
- * sub-properties (e.g. elseif: [{ if, then }]).
340
- * - `case-map` — value is an object whose values are step arrays (e.g. cases).
341
- * No specific Run.Sequence field name is hardcoded; any kind that uses
342
- * a step body and tags its branch fields with these roles works.
343
- */
344
- function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases, scopes) {
345
- const props = defSchema.properties;
346
- if (!props)
347
- return undefined;
348
- const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
349
- const readingModule = manifest.metadata?.module;
350
- for (const [fieldName, fieldSchema] of Object.entries(props)) {
351
- const stepCtx = readStepSlot(fieldSchema);
352
- if (!stepCtx)
353
- continue;
354
- const invokeField = stepCtx.invoke;
355
- const outputTypeField = stepCtx.outputType;
356
- // Optional: the field a step uses to produce a result without dispatching.
357
- // Only a kind that declares one has pure steps at all.
358
- const valueField = stepCtx.value;
359
- if (!invokeField || !outputTypeField)
360
- continue;
361
- const steps = manifest[fieldName];
362
- if (!Array.isArray(steps))
363
- continue;
364
- const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
365
- // The instance's own input contract, for typing a pure step that just
366
- // forwards one of its values.
367
- const ownInputs = resolveTypeFieldToSchema(manifest.inputType, allManifests);
368
- const stepProperties = {};
369
- walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
370
- const name = s.name;
371
- const invoke = s[invokeField];
372
- // Only invoke steps register a `steps.<name>.result` entry — control-flow
373
- // wrappers (try/if/while/switch/throw) don't produce a result and must
374
- // not shadow real entries with a permissive `additionalProperties: true`,
375
- // or unknown step references slip through chain validation.
376
- if (typeof name !== "string")
377
- return;
378
- if (!invoke || typeof invoke !== "object") {
379
- // A pure step dispatches nothing, so there is no contract to resolve.
380
- // Where its expression is a plain chain into something already typed —
381
- // an earlier step's result, or the kind's own inputs — that type carries
382
- // through; anything else (arithmetic, a call, a comprehension) stays
383
- // permissive rather than guessed. Same rule as a named binding's.
384
- if (valueField && valueField in s) {
385
- const scopeRoot = {
386
- properties: {
387
- steps: { type: "object", properties: { ...stepProperties } },
388
- ...(ownInputs ? { inputs: ownInputs } : {}),
389
- },
390
- };
391
- const chained = schemaAtChain(bindingPathChain(s[valueField]), scopeRoot);
392
- stepProperties[name] = {
393
- type: "object",
394
- properties: { result: chained ?? PERMISSIVE_CONTRACT },
395
- };
396
- }
397
- return;
398
- }
399
- const invokedKind = invoke.kind;
400
- const invokedName = invoke.name;
401
- // A named `!ref` carries the target's own manifest (which may narrow the
402
- // contract for this one instance); an inline `{ kind, ... }` step IS the
403
- // manifest. Either way the kind layer resolves through `extends`.
404
- const invokedManifest = invokedName
405
- ? allManifests.find((m) => m.metadata?.name === invokedName && (!invokedKind || m.kind === invokedKind))
406
- : invoke;
407
- const invokedDef = invokedKind
408
- ? contractScope.resolveIn(invokedKind, readingModule)
409
- : undefined;
410
- const outputSchema = resolveContract(outputTypeField, invokedManifest, invokedDef, contractScope)?.schema;
411
- stepProperties[name] = {
412
- type: "object",
413
- properties: {
414
- result: outputSchema ?? PERMISSIVE_CONTRACT,
415
- },
416
- };
417
- });
418
- if (Object.keys(stepProperties).length > 0) {
419
- return {
420
- type: "object",
421
- properties: stepProperties,
422
- };
423
- }
424
- }
425
- return undefined;
426
- }
427
136
  /** The built-in namespace: globally resolvable, crossing no import boundary. */
428
137
  const TELO_BUILTIN_MODULE = "Telo";
429
138
  /** One mapping from a ref-slot issue to a diagnostic — the shape is identical
@@ -684,84 +393,6 @@ function pathCrossesNestedResource(root, path) {
684
393
  }
685
394
  return false;
686
395
  }
687
- function collectErrorContextScopes(defSchema) {
688
- const out = new Map();
689
- if (!defSchema || typeof defSchema !== "object")
690
- return out;
691
- const seen = new Set();
692
- const walk = (schema) => {
693
- if (!schema || typeof schema !== "object" || seen.has(schema))
694
- return;
695
- seen.add(schema);
696
- const props = schema.properties;
697
- if (props) {
698
- for (const [fieldName, fieldSchema] of Object.entries(props)) {
699
- if (fieldSchema && typeof fieldSchema === "object") {
700
- const errCtx = fieldSchema["x-telo-error-context"];
701
- if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
702
- out.set(fieldName, errCtx);
703
- }
704
- }
705
- walk(resolveLocalRef(fieldSchema, defSchema));
706
- }
707
- }
708
- if (schema.items)
709
- walk(resolveLocalRef(schema.items, defSchema));
710
- for (const key of ["oneOf", "anyOf", "allOf"]) {
711
- const arr = schema[key];
712
- if (Array.isArray(arr))
713
- for (const sub of arr)
714
- walk(resolveLocalRef(sub, defSchema));
715
- }
716
- if (schema.$defs && typeof schema.$defs === "object") {
717
- for (const sub of Object.values(schema.$defs)) {
718
- walk(sub);
719
- }
720
- }
721
- };
722
- walk(defSchema);
723
- return out;
724
- }
725
- /**
726
- * Return the error-context schema for a CEL `path` when the path lies within
727
- * (any depth under) one of the error-bearing fields, else undefined. A path is
728
- * "within" field `f` when it contains a segment `f[<index>]`. When multiple
729
- * error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
730
- * deepest — the one whose segment appears latest in the path — wins, so the
731
- * innermost branch's schema governs.
732
- */
733
- function errorContextForPath(path, scopes) {
734
- let best;
735
- for (const [fieldName, schema] of scopes) {
736
- const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
737
- for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
738
- if (best === undefined || match.index > best.index) {
739
- best = { index: match.index, schema };
740
- }
741
- }
742
- }
743
- return best?.schema;
744
- }
745
- /** Add a kind's named bindings to a resolved context, when the context declares
746
- * a bindings region. They go UNDER the context's own properties: a scope
747
- * variable wins over a same-named binding at runtime, so static typing has to
748
- * agree (the collision itself is `BINDING_NAME_RESERVED`). */
749
- function withBindingNames(contextSchema, resource) {
750
- const field = contextSchema[BINDINGS_ANNOTATION];
751
- if (typeof field !== "string")
752
- return contextSchema;
753
- const bindings = resource[field];
754
- if (bindings === null || typeof bindings !== "object" || Array.isArray(bindings)) {
755
- return contextSchema;
756
- }
757
- return {
758
- ...contextSchema,
759
- properties: {
760
- ...bindingContextProperties(bindings, contextSchema),
761
- ...(contextSchema.properties ?? {}),
762
- },
763
- };
764
- }
765
396
  /** Member-access chains in a CEL expression, or none when it doesn't parse.
766
397
  * Best-effort: a syntax error is reported by the engine pass, not here. */
767
398
  function celAccessChains(env, expr) {
@@ -1764,9 +1395,20 @@ export class StaticAnalyzer {
1764
1395
  // fit the slot at all", the schema answers "do their type arguments agree",
1765
1396
  // which cel-js cannot express because it types by constructor identity.
1766
1397
  const celSourceSchemaByPath = new Map();
1767
- // Context-free typed environments, one per manifest. Reused across every
1768
- // expression in it see the build site for why a matched context opts out.
1769
- const typedEnvByManifest = new Map();
1398
+ // What every CEL expression in this set is typed against. The rule lives in
1399
+ // `cel-scope.ts` so the IDE asks the same question the pass does a
1400
+ // completion list is a claim that the name it offers will pass this check,
1401
+ // and two implementations of it could not be held in agreement.
1402
+ const celScope = new CelScopeResolver({
1403
+ celEnv: this.celEnv,
1404
+ defs,
1405
+ aliases,
1406
+ scopes: { aliasesByModule, rootModules },
1407
+ allManifests,
1408
+ kernelGlobals,
1409
+ moduleManifest,
1410
+ observedStateContext,
1411
+ });
1770
1412
  // Validate each non-definition, non-system resource
1771
1413
  for (const m of allManifests) {
1772
1414
  const filePath = m.metadata?.source;
@@ -2106,10 +1748,10 @@ export class StaticAnalyzer {
2106
1748
  // state (definitions, aliases, the typed CEL env).
2107
1749
  // Per-resource state computed at enter and read by that resource's CEL
2108
1750
  // sites. The manifest / resource / filePath come straight off each CelSite's
2109
- // `source` (no need to capture them); only the derived step / invocation
2110
- // context which require analyzer state to build are stashed here.
1751
+ // `source` (no need to capture them); the derived step / invocation / error
1752
+ // context is the scope resolver's, read back from it where a CHECK needs the
1753
+ // same schema an expression is typed against.
2111
1754
  let celStepContextSchema;
2112
- let celInvocationContext;
2113
1755
  let celErrorScopes = new Map();
2114
1756
  // Region coverage for the "CEL in a non-eval field" check: the union of
2115
1757
  // `x-telo-eval` paths (own + capability) and `x-telo-context` /
@@ -2127,14 +1769,24 @@ export class StaticAnalyzer {
2127
1769
  visitManifest(allManifests, defs, {
2128
1770
  onResourceEnter: (e) => {
2129
1771
  const m = e.source;
2130
- celInvocationContext = m.metadata?.xTeloInvocationContext;
2131
- celStepContextSchema = e.definition?.schema
2132
- ? buildStepContextSchema(m, e.definition.schema, allManifests, defs, aliases, { aliasesByModule, rootModules })
2133
- : undefined;
1772
+ celScope.enterResource(m, e.definition);
1773
+ // Read back rather than recomputed: the step-inputs check has to see
1774
+ // the same `steps` schema this resource's expressions are typed
1775
+ // against, and a second computation is how the two come to disagree.
1776
+ celStepContextSchema = celScope.stepContextSchema;
1777
+ celErrorScopes = celScope.errorContextScopes;
2134
1778
  if (e.definition?.schema) {
2135
1779
  const stepName = m.metadata?.name;
2136
1780
  const stepFile = m.metadata?.source;
2137
- for (const issue of collectStepInputIssues(m, e.definition.schema, allManifests, defs, aliases, { aliasesByModule, rootModules }, celStepContextSchema)) {
1781
+ // Both drivers of the SAME check: a step's `inputs:` found through
1782
+ // the step grammar, and a reference slot's found through the
1783
+ // `x-telo-ref` `inputs:` pointer. A call site the editor can
1784
+ // complete is a call site `telo check` validates.
1785
+ const inputIssues = [
1786
+ ...collectRefInputIssues(m, defs.expandedFieldMapForResource(m, aliases, aliasesByModule), allManifests, defs, aliases, { aliasesByModule, rootModules }),
1787
+ ...collectStepInputIssues(m, e.definition.schema, allManifests, defs, aliases, { aliasesByModule, rootModules }, celStepContextSchema),
1788
+ ];
1789
+ for (const issue of inputIssues) {
2138
1790
  diagnostics.push({
2139
1791
  severity: DiagnosticSeverity.Error,
2140
1792
  code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
@@ -2152,7 +1804,6 @@ export class StaticAnalyzer {
2152
1804
  });
2153
1805
  }
2154
1806
  }
2155
- celErrorScopes = collectErrorContextScopes(e.definition?.schema);
2156
1807
  celBindingSites = findBindingSites(e.definition?.schema);
2157
1808
  if (celBindingSites) {
2158
1809
  const declared = m[celBindingSites.field];
@@ -2276,7 +1927,7 @@ export class StaticAnalyzer {
2276
1927
  // (resource-wide invocation context) carry CEL the kernel evaluates.
2277
1928
  if (celRuleApplies &&
2278
1929
  engineName === "cel" &&
2279
- celInvocationContext === undefined &&
1930
+ celScope.invocationContextSchema === undefined &&
2280
1931
  !evalPathsCover(celEvalPaths, path) &&
2281
1932
  !celRegionScopes.some((scope) => pathMatchesScope(path, scope)) &&
2282
1933
  !pathCrossesNestedResource(m, path)) {
@@ -2323,55 +1974,6 @@ export class StaticAnalyzer {
2323
1974
  }
2324
1975
  }
2325
1976
  }
2326
- let matchedContext = e.contextSchema ?? celInvocationContext;
2327
- if (celStepContextSchema) {
2328
- const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
2329
- matchedContext = {
2330
- ...base,
2331
- properties: {
2332
- ...(base.properties ?? {}),
2333
- steps: celStepContextSchema,
2334
- },
2335
- };
2336
- }
2337
- // `error` is only in scope inside an error-bearing branch (e.g. a
2338
- // `catch:` / `finally:`), so it's merged per-path, not resource-wide.
2339
- const errorSchema = celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
2340
- if (errorSchema) {
2341
- const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
2342
- matchedContext = {
2343
- ...base,
2344
- properties: {
2345
- ...(base.properties ?? {}),
2346
- error: errorSchema,
2347
- },
2348
- };
2349
- }
2350
- let effectiveContext = null;
2351
- if (matchedContext) {
2352
- const manifestItem = matchedScope
2353
- ? getManifestItem(path, matchedScope, m)
2354
- : m;
2355
- const rootForResolver = manifestRootForResolver(m, defs, aliases, allManifests, { aliasesByModule, rootModules });
2356
- const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
2357
- manifestRoot: rootForResolver,
2358
- defs,
2359
- aliases,
2360
- allManifests: allManifests,
2361
- });
2362
- effectiveContext = mergeKernelGlobalsIntoContext(withBindingNames(resolvedContext, m),
2363
- // Typed in the module that DECLARED this resource — for a manifest
2364
- // forwarded from an imported library, that is its `moduleGlobals`
2365
- // stamp, not the consuming application's block.
2366
- kernelGlobals.forResource(m));
2367
- }
2368
- else if (observedStateContext) {
2369
- // No `x-telo-context` matched, so nothing was chain-validated here
2370
- // before. Validate the observed-state segment alone rather than
2371
- // merging the kernel globals, whose closed `variables` / `ports`
2372
- // nodes would newly reject reads that pass today.
2373
- effectiveContext = observedStateContext;
2374
- }
2375
1977
  const engine = defaultRegistry().get(engineName);
2376
1978
  if (!engine) {
2377
1979
  // No registered engine owns this tag — the expression would go
@@ -2386,27 +1988,15 @@ export class StaticAnalyzer {
2386
1988
  return;
2387
1989
  }
2388
1990
  // The engine type-checks, so it gets the environment typed for THIS
2389
- // path — not the bare base one. A `Telo.Import`'s variables/secrets
2390
- // are a config-only contract evaluated in the IMPORTING module's
2391
- // scope, so they type from the owning module doc and drop
2392
- // `resources`/`env`, making a reference to either an error.
2393
- //
2394
- // Cached per manifest when no `x-telo-context` applied, which is most
2395
- // expressions: the environment then depends only on the manifest, so
2396
- // rebuilding it per expression is pure waste — a clone plus a
2397
- // re-registration of every variable, on every keystroke in the IDE.
2398
- // A matched context makes the environment path-specific (its schema is
2399
- // resolved against the enclosing array item), so those still build
2400
- // fresh rather than risk one item's types leaking into another's.
2401
- const cached = effectiveContext === null ? typedEnvByManifest.get(m) : undefined;
2402
- const typedEnv = cached ??
2403
- (m.kind === "Telo.Import"
2404
- ? buildImportInputCelEnvironment(this.celEnv, allManifests.find((mm) => (mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
2405
- mm.metadata?.name ===
2406
- m.metadata?.module))
2407
- : buildTypedCelEnvironment(this.celEnv, m, effectiveContext ?? undefined, moduleManifest));
2408
- if (effectiveContext === null && !cached)
2409
- typedEnvByManifest.set(m, typedEnv);
1991
+ // path — not the bare base one. Both halves come from the one scope
1992
+ // rule, which is what makes the IDE's answer and this check the same
1993
+ // answer rather than two that agree today.
1994
+ const { env: typedEnv, contextSchema: effectiveContext } = celScope.scopeFor({
1995
+ source: m,
1996
+ path,
1997
+ contextSchema: e.contextSchema,
1998
+ matchedScope,
1999
+ });
2410
2000
  const result = engine.analyze(expr, { celEnv: typedEnv, contextSchema: effectiveContext });
2411
2001
  if (result.type !== undefined) {
2412
2002
  let byPath = celTypeByPath.get(m);