@telorun/analyzer 0.62.1 → 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 (171) 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 +41 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +396 -476
  7. package/dist/call-graph.d.ts.map +1 -1
  8. package/dist/call-graph.js +2 -7
  9. package/dist/cel-scope-query.d.ts +109 -0
  10. package/dist/cel-scope-query.d.ts.map +1 -0
  11. package/dist/cel-scope-query.js +270 -0
  12. package/dist/cel-scope.d.ts +166 -0
  13. package/dist/cel-scope.d.ts.map +1 -0
  14. package/dist/cel-scope.js +377 -0
  15. package/dist/definition-registry.d.ts +15 -0
  16. package/dist/definition-registry.d.ts.map +1 -1
  17. package/dist/definition-registry.js +25 -9
  18. package/dist/find-manifest.d.ts +10 -0
  19. package/dist/find-manifest.d.ts.map +1 -0
  20. package/dist/find-manifest.js +12 -0
  21. package/dist/flatten-for-analyzer.js +1 -1
  22. package/dist/import-resolution-diagnostics.d.ts +9 -2
  23. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  24. package/dist/import-resolution-diagnostics.js +26 -7
  25. package/dist/index.d.ts +22 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +22 -1
  28. package/dist/invocation-contract.d.ts +17 -1
  29. package/dist/invocation-contract.d.ts.map +1 -1
  30. package/dist/invocation-contract.js +42 -4
  31. package/dist/loaded-types.d.ts +12 -3
  32. package/dist/loaded-types.d.ts.map +1 -1
  33. package/dist/manifest-analysis.d.ts +73 -0
  34. package/dist/manifest-analysis.d.ts.map +1 -0
  35. package/dist/manifest-analysis.js +78 -0
  36. package/dist/manifest-loader.d.ts +0 -1
  37. package/dist/manifest-loader.d.ts.map +1 -1
  38. package/dist/manifest-loader.js +62 -41
  39. package/dist/manifest-path.d.ts +18 -0
  40. package/dist/manifest-path.d.ts.map +1 -0
  41. package/dist/manifest-path.js +37 -0
  42. package/dist/manifest-schemas.d.ts +699 -0
  43. package/dist/manifest-schemas.d.ts.map +1 -1
  44. package/dist/manifest-schemas.js +306 -14
  45. package/dist/module-compatibility.d.ts +40 -0
  46. package/dist/module-compatibility.d.ts.map +1 -0
  47. package/dist/module-compatibility.js +52 -0
  48. package/dist/ref-slot-reading.d.ts +30 -0
  49. package/dist/ref-slot-reading.d.ts.map +1 -0
  50. package/dist/ref-slot-reading.js +139 -0
  51. package/dist/referrer-rule.d.ts +67 -0
  52. package/dist/referrer-rule.d.ts.map +1 -0
  53. package/dist/referrer-rule.js +96 -0
  54. package/dist/release/index.d.ts +1 -1
  55. package/dist/release/index.d.ts.map +1 -1
  56. package/dist/release/index.js +1 -1
  57. package/dist/release/version-stamp.d.ts +16 -0
  58. package/dist/release/version-stamp.d.ts.map +1 -1
  59. package/dist/release/version-stamp.js +52 -0
  60. package/dist/release/workspace-config.d.ts +10 -6
  61. package/dist/release/workspace-config.d.ts.map +1 -1
  62. package/dist/release/workspace-config.js +10 -6
  63. package/dist/resolve-schema-ref-kinds.d.ts +4 -3
  64. package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
  65. package/dist/resolve-schema-ref-kinds.js +16 -0
  66. package/dist/resolve-throws-union.d.ts.map +1 -1
  67. package/dist/resolve-throws-union.js +4 -3
  68. package/dist/resolve-zone-containment.d.ts +99 -0
  69. package/dist/resolve-zone-containment.d.ts.map +1 -0
  70. package/dist/resolve-zone-containment.js +212 -0
  71. package/dist/resolve-zone-requirements.d.ts +5 -0
  72. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  73. package/dist/resolve-zone-requirements.js +48 -7
  74. package/dist/resource-rule.d.ts +106 -0
  75. package/dist/resource-rule.d.ts.map +1 -0
  76. package/dist/resource-rule.js +236 -0
  77. package/dist/rule-condition.d.ts +50 -0
  78. package/dist/rule-condition.d.ts.map +1 -0
  79. package/dist/rule-condition.js +108 -0
  80. package/dist/schema-keywords.d.ts.map +1 -1
  81. package/dist/schema-keywords.js +20 -0
  82. package/dist/schema-projection.d.ts +171 -0
  83. package/dist/schema-projection.d.ts.map +1 -0
  84. package/dist/schema-projection.js +317 -0
  85. package/dist/schema-walk.d.ts +25 -0
  86. package/dist/schema-walk.d.ts.map +1 -0
  87. package/dist/schema-walk.js +126 -0
  88. package/dist/step-slot.d.ts +39 -0
  89. package/dist/step-slot.d.ts.map +1 -0
  90. package/dist/step-slot.js +49 -0
  91. package/dist/telo-version.d.ts +1 -1
  92. package/dist/telo-version.js +1 -1
  93. package/dist/validate-cel-context.d.ts.map +1 -1
  94. package/dist/validate-cel-context.js +17 -3
  95. package/dist/validate-durable-regions.d.ts +15 -0
  96. package/dist/validate-durable-regions.d.ts.map +1 -0
  97. package/dist/validate-durable-regions.js +336 -0
  98. package/dist/validate-references.d.ts.map +1 -1
  99. package/dist/validate-references.js +44 -2
  100. package/dist/validate-referrer-rules.d.ts +120 -0
  101. package/dist/validate-referrer-rules.d.ts.map +1 -0
  102. package/dist/validate-referrer-rules.js +255 -0
  103. package/dist/validate-resource-rules.d.ts +114 -0
  104. package/dist/validate-resource-rules.d.ts.map +1 -0
  105. package/dist/validate-resource-rules.js +283 -0
  106. package/dist/validate-schema-projection.d.ts +32 -0
  107. package/dist/validate-schema-projection.d.ts.map +1 -0
  108. package/dist/validate-schema-projection.js +110 -0
  109. package/dist/validate-step-inputs.d.ts +19 -1
  110. package/dist/validate-step-inputs.d.ts.map +1 -1
  111. package/dist/validate-step-inputs.js +102 -9
  112. package/dist/validate-throws-coverage.d.ts.map +1 -1
  113. package/dist/validate-throws-coverage.js +11 -5
  114. package/dist/validate-zone-slots.d.ts +2 -2
  115. package/dist/validate-zone-slots.d.ts.map +1 -1
  116. package/dist/validate-zone-slots.js +202 -6
  117. package/dist/validate-zone-violations.d.ts +12 -0
  118. package/dist/validate-zone-violations.d.ts.map +1 -0
  119. package/dist/validate-zone-violations.js +169 -0
  120. package/dist/value-type-keyword.d.ts +1 -1
  121. package/dist/value-type-keyword.d.ts.map +1 -1
  122. package/dist/value-type-keyword.js +4 -0
  123. package/dist/zone-slot.d.ts +61 -0
  124. package/dist/zone-slot.d.ts.map +1 -1
  125. package/dist/zone-slot.js +86 -5
  126. package/package.json +2 -2
  127. package/src/analysis-registry.ts +43 -0
  128. package/src/analyzer.ts +452 -595
  129. package/src/call-graph.ts +2 -8
  130. package/src/cel-scope-query.ts +337 -0
  131. package/src/cel-scope.ts +570 -0
  132. package/src/definition-registry.ts +31 -13
  133. package/src/find-manifest.ts +19 -0
  134. package/src/flatten-for-analyzer.ts +1 -1
  135. package/src/import-resolution-diagnostics.ts +25 -7
  136. package/src/index.ts +86 -0
  137. package/src/invocation-contract.ts +67 -2
  138. package/src/loaded-types.ts +12 -3
  139. package/src/manifest-analysis.ts +132 -0
  140. package/src/manifest-loader.ts +87 -55
  141. package/src/manifest-path.ts +34 -0
  142. package/src/manifest-schemas.ts +328 -14
  143. package/src/module-compatibility.ts +72 -0
  144. package/src/ref-slot-reading.ts +156 -0
  145. package/src/referrer-rule.ts +116 -0
  146. package/src/release/index.ts +1 -0
  147. package/src/release/version-stamp.ts +58 -0
  148. package/src/release/workspace-config.ts +10 -6
  149. package/src/resolve-schema-ref-kinds.ts +28 -4
  150. package/src/resolve-throws-union.ts +4 -3
  151. package/src/resolve-zone-containment.ts +329 -0
  152. package/src/resolve-zone-requirements.ts +67 -7
  153. package/src/resource-rule.ts +252 -0
  154. package/src/rule-condition.ts +126 -0
  155. package/src/schema-keywords.ts +24 -0
  156. package/src/schema-projection.ts +403 -0
  157. package/src/schema-walk.ts +144 -0
  158. package/src/step-slot.ts +64 -0
  159. package/src/telo-version.ts +1 -1
  160. package/src/validate-cel-context.ts +17 -3
  161. package/src/validate-durable-regions.ts +401 -0
  162. package/src/validate-references.ts +57 -2
  163. package/src/validate-referrer-rules.ts +373 -0
  164. package/src/validate-resource-rules.ts +401 -0
  165. package/src/validate-schema-projection.ts +184 -0
  166. package/src/validate-step-inputs.ts +148 -10
  167. package/src/validate-throws-coverage.ts +12 -5
  168. package/src/validate-zone-slots.ts +192 -8
  169. package/src/validate-zone-violations.ts +190 -0
  170. package/src/value-type-keyword.ts +4 -0
  171. package/src/zone-slot.ts +114 -5
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";
@@ -17,8 +17,23 @@ import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
17
17
  import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
18
18
  import { resolveSchemaRefKinds } from "./resolve-schema-ref-kinds.js";
19
19
  import { runZoneAnalysis } from "./resolve-zone-requirements.js";
20
- import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
20
+ import { validateDurableRegions } from "./validate-durable-regions.js";
21
+ import { validateZoneViolations } from "./validate-zone-violations.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";
21
30
  import { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
31
+ import { validateSchemaProjection, } from "./validate-schema-projection.js";
32
+ import { evaluateResourceRules, reportResourceRules, reportUnexercisedRule, ruleExercised, validateResourceRuleDeclarations, } from "./validate-resource-rules.js";
33
+ import { readResourceRules } from "./resource-rule.js";
34
+ import { readReferrerRules } from "./referrer-rule.js";
35
+ import { evaluateReferrerRules, referrerRuleExercised, reportReferrerRules, reportUnexercisedReferrerRule, validateReferrerRuleDeclarations, } from "./validate-referrer-rules.js";
36
+ import { describeProjectionFailure, } from "./schema-projection.js";
22
37
  import { validateDynamicSelectors, validateRefSlotDeclarations, } from "./validate-ref-slots.js";
23
38
  import { validateValueTypeSlots, } from "./validate-value-type-slots.js";
24
39
  import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
@@ -27,9 +42,9 @@ import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
27
42
  import { celTypeSatisfiesJsonSchema, checkSchemaCompatibility, navigateSchemaToExprPath, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
28
43
  import { collectValueSchemaIssues } from "./validate-value-schema.js";
29
44
  import { DiagnosticSeverity } from "./types.js";
30
- import { extractAccessChains, extractCelRegionScopes, getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
45
+ import { extractAccessChains, extractCelRegionScopes, pathMatchesScope, } from "./validate-cel-context.js";
31
46
  import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
32
- import { BINDINGS_ANNOTATION, bindingContextProperties, bindingPathChain, findBindingSites, resolveBindingOrder, schemaAtChain, } from "./cel-bindings.js";
47
+ import { BINDINGS_ANNOTATION, findBindingSites, resolveBindingOrder, } from "./cel-bindings.js";
33
48
  import { CEL_RESERVED_WORDS, checkName } from "./identifier-name.js";
34
49
  import { validateIdentifierNames } from "./validate-identifier-names.js";
35
50
  import { validateExtends } from "./validate-extends.js";
@@ -40,13 +55,14 @@ import { validateModuleMetadata } from "./validate-module-metadata.js";
40
55
  import { validateRequires } from "./validate-requires.js";
41
56
  import { validateBaseMapping } from "./validate-base-mapping.js";
42
57
  import { validateInvocationContract } from "./validate-invocation-contract.js";
43
- import { collectStepInputIssues } from "./validate-step-inputs.js";
58
+ import { collectRefInputIssues, collectStepInputIssues } from "./validate-step-inputs.js";
44
59
  import { validateNestedInlineResources } from "./validate-nested-inline.js";
45
60
  import { validateProviderCoherence } from "./validate-provider-coherence.js";
46
61
  import { validateReferences } from "./validate-references.js";
47
62
  import { validateReferenceForms } from "./validate-reference-forms.js";
48
63
  import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
49
64
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
65
+ import { readStepSlot } from "./step-slot.js";
50
66
  const SELF_PREFIX = "Self.";
51
67
  /**
52
68
  * `StaticAnalyzer.analyze()` requires `metadata.source` (non-empty) and
@@ -90,83 +106,7 @@ function resolveSelfOrAlias(value, ownModule, scopeResolver) {
90
106
  }
91
107
  return scopeResolver.resolveKind(value);
92
108
  }
93
- /** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
94
- * resolve in the module that declared the definition they were read off (so an
95
- * `extends` chain crossing module boundaries re-scopes at every hop), and named
96
- * `telo#Type` references resolve against the flattened manifest list. `resolveIn`
97
- * is the top-level entry point, where the kind was written by the READING
98
- * module and there is no declaring definition yet. */
99
- export function analyzerContractScope(defs, aliases, scopes, allManifests) {
100
- const resolve = moduleScopedDefResolver(defs, aliases, scopes);
101
- return {
102
- resolveDefinition: resolve,
103
- resolveIn: resolve.in,
104
- typeManifestsFor: () => allManifests,
105
- };
106
- }
107
109
  const SOURCE = "telo-analyzer";
108
- /** Build a closed JSON Schema for the `self` CEL variable available inside a
109
- * `Telo.Definition` template body. Mirrors the runtime template controller's
110
- * `const self = { ...resource, name: resource.metadata.name };` — every
111
- * property the user declared in `schema:` plus synthetic `name` / `kind` and
112
- * the metadata sub-object (kept open since metadata legitimately carries
113
- * arbitrary user-added fields). */
114
- function buildSelfSchema(definition, defs, aliases) {
115
- // The author-facing schema resolves inheritance: with `base:` the child's own
116
- // schema (the parent's config is internal); without it, `merge(parent, own)`.
117
- const userSchema = (defs
118
- ? effectiveAuthorSchema(definition, (k) => defs.resolve(aliases?.resolveKind(k) ?? k) ?? defs.resolve(k))
119
- : (definition.schema ?? {}));
120
- const userProps = (userSchema.properties ?? {});
121
- const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
122
- return {
123
- type: "object",
124
- additionalProperties: false,
125
- properties: {
126
- ...userProps,
127
- name: { type: "string" },
128
- kind: { type: "string" },
129
- metadata: {
130
- type: "object",
131
- additionalProperties: true,
132
- properties: { name: { type: "string" } },
133
- },
134
- },
135
- required: [...userRequired, "name", "kind"],
136
- };
137
- }
138
- /** Build the JSON Schema for the `inputs` CEL variable available inside an
139
- * invocable template body — the shared contract resolver applied to the
140
- * definition itself, so a body is typed against the exact signature callers are
141
- * checked against and dispatch enforces. Walks the whole `extends` chain rather
142
- * than one hop, so a definition two levels below the declaration still gets
143
- * typed inputs. Undefined when nothing in the chain declares a contract —
144
- * the caller signals opaque `map<string, dyn>` upstream. */
145
- function lookupTemplateInputsSchema(definition, defs, aliases, allManifests, scopes) {
146
- return resolveContract("inputType", undefined, definition, analyzerContractScope(defs, aliases, scopes, allManifests))?.schema;
147
- }
148
- /** Returns a "resolver-facing" view of the manifest where the fields used as
149
- * navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
150
- * have been pre-augmented:
151
- * - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
152
- * - `inputType` → resolved through the shared contract resolver, so
153
- * `x-telo-context-from-root: inputType` substitutes the
154
- * real signature. Without it the annotation would replace
155
- * the node verbatim with the inline `{kind, schema}` wrapper
156
- * the standard library writes everywhere, typing `inputs` as
157
- * `{kind, schema}` instead of the declared properties.
158
- *
159
- * For non-definition manifests the original object is returned. */
160
- function manifestRootForResolver(m, defs, aliases, allManifests, scopes) {
161
- if (m.kind !== "Telo.Definition")
162
- return m;
163
- const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
164
- return {
165
- ...m,
166
- schema: buildSelfSchema(m, defs, aliases),
167
- ...(inputs ? { inputType: inputs } : {}),
168
- };
169
- }
170
110
  /** True when an issue reports a property that is absent — its path points at a
171
111
  * node the manifest does not contain. */
172
112
  export const missingRequired = (issue) => /is missing required property/.test(issue.message);
@@ -193,228 +133,6 @@ function contractOwnerLabel(definition, contract) {
193
133
  }
194
134
  return canonical;
195
135
  }
196
- /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
197
- * Non-refs and unresolved refs pass through unchanged. */
198
- export function resolveLocalRef(schema, root) {
199
- if (!schema)
200
- return undefined;
201
- const ref = schema.$ref;
202
- if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
203
- const defName = ref.slice("#/$defs/".length);
204
- const resolved = root.$defs?.[defName];
205
- if (resolved && typeof resolved === "object")
206
- return resolved;
207
- }
208
- // A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
209
- // Resolved HERE rather than by each walker: this is the one chokepoint every
210
- // structural walk already goes through — the step-array walks, the call graph,
211
- // the zone projection, the eval-path collector — so a composer that points at a
212
- // shared shape stays legible to all of them at once. Nothing is inlined into
213
- // the stored schema, which keeps validator-cache identity stable and matches
214
- // what `resolveSchemaTypeRefs` does for a named user type.
215
- if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
216
- const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
217
- const resolved = ManifestRootSchema.$defs[defName];
218
- if (resolved && typeof resolved === "object")
219
- return resolved;
220
- }
221
- return schema;
222
- }
223
- const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
224
- /** Gather property schemas from a (possibly variant-bearing) object schema:
225
- * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
226
- *
227
- * Each branch is resolved through {@link resolveLocalRef} first, so a branch
228
- * that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
229
- * site — contributes its properties like an inline one. Without that, pointing a
230
- * composer at a shared shape would silently empty every role-driven lookup that
231
- * reads this (the inputs slot, the retry policy, the eval paths), which is a
232
- * failure with no diagnostic attached to it. */
233
- export function gatherPropertySchemas(schema, root) {
234
- const out = [];
235
- const base = resolveLocalRef(schema, root ?? schema) ?? schema;
236
- if (base.properties && typeof base.properties === "object") {
237
- for (const [k, v] of Object.entries(base.properties)) {
238
- out.push([k, v]);
239
- }
240
- }
241
- for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
242
- const arr = base[variantKey];
243
- if (!Array.isArray(arr))
244
- continue;
245
- for (const raw of arr) {
246
- if (!raw || typeof raw !== "object")
247
- continue;
248
- const variant = resolveLocalRef(raw, root ?? schema) ?? raw;
249
- if (variant.properties) {
250
- for (const [k, v] of Object.entries(variant.properties)) {
251
- out.push([k, v]);
252
- }
253
- }
254
- }
255
- }
256
- return out;
257
- }
258
- /**
259
- * Generic, role-driven walk over an `x-telo-step-context` step array. Calls
260
- * `visit(step, stepPath)` for every step — top-level and nested through the
261
- * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
262
- * the single definition of how steps nest, shared by `buildStepContextSchema`
263
- * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
264
- * checks invoke refs), so the topology contract lives in one place — adding a
265
- * role or nesting form updates both consumers at once. No resource kind is
266
- * hardcoded; recursion is driven entirely by the schema annotations.
267
- */
268
- export function walkStepArray(steps, stepItemSchema, rootSchema, basePath, visit) {
269
- const dispatchRole = (data, role, itemsSchema, path) => {
270
- if (role === "branch" && Array.isArray(data)) {
271
- walkStepArray(data, stepItemSchema, rootSchema, path, visit);
272
- }
273
- else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
274
- for (const [caseKey, arr] of Object.entries(data)) {
275
- if (Array.isArray(arr))
276
- walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
277
- }
278
- }
279
- else if (role === "branch-list" && Array.isArray(data)) {
280
- const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
281
- if (!entrySchema)
282
- return;
283
- data.forEach((entry, i) => {
284
- if (!entry || typeof entry !== "object")
285
- return;
286
- for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
287
- const subRole = subSchema["x-telo-topology-role"];
288
- if (typeof subRole !== "string")
289
- continue;
290
- dispatchRole(entry[subKey], subRole, subSchema.items, `${path}[${i}].${subKey}`);
291
- }
292
- });
293
- }
294
- };
295
- steps.forEach((step, i) => {
296
- if (!step || typeof step !== "object")
297
- return;
298
- const s = step;
299
- const stepPath = `${basePath}[${i}]`;
300
- visit(s, stepPath);
301
- if (!stepItemSchema)
302
- return;
303
- for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
304
- const role = propSchema["x-telo-topology-role"];
305
- if (typeof role !== "string")
306
- continue;
307
- dispatchRole(s[propKey], role, propSchema.items, `${stepPath}.${propKey}`);
308
- }
309
- });
310
- }
311
- /**
312
- * Build a `steps` context schema from `x-telo-step-context` annotation.
313
- * Walks each step in the manifest array, resolves the invoked resource's output
314
- * contract, and builds `steps.<name>.result` context entries.
315
- *
316
- * Resolution is the shared {@link resolveContract} — the invoked resource
317
- * manifest's own declaration, then the kind's, resolved to the nearest
318
- * declaration along `extends`, then permissive. Sharing it with the kernel is
319
- * what stops `telo check` from typing `steps.X.result` against one contract
320
- * while dispatch validates against another.
321
- *
322
- * The kind layer is what makes `x-telo-stream` properties on definitions
323
- * actually govern step-result chain validation — without it, the validator falls
324
- * back to permissive and the stream-opacity rule never fires.
325
- *
326
- * Recursion into nested step arrays is annotation-driven via
327
- * `x-telo-topology-role`. The analyzer recognises three role values:
328
- * - `branch` — value is an array of steps (e.g. then / else / do / catch).
329
- * - `branch-list`— value is an array of objects each carrying further roled
330
- * sub-properties (e.g. elseif: [{ if, then }]).
331
- * - `case-map` — value is an object whose values are step arrays (e.g. cases).
332
- * No specific Run.Sequence field name is hardcoded; any kind that uses
333
- * `x-telo-step-context` and tags its branch fields with these roles works.
334
- */
335
- function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases, scopes) {
336
- const props = defSchema.properties;
337
- if (!props)
338
- return undefined;
339
- const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
340
- const readingModule = manifest.metadata?.module;
341
- for (const [fieldName, fieldSchema] of Object.entries(props)) {
342
- const stepCtx = fieldSchema["x-telo-step-context"];
343
- if (!stepCtx)
344
- continue;
345
- const invokeField = stepCtx.invoke;
346
- const outputTypeField = stepCtx.outputType;
347
- // Optional: the field a step uses to produce a result without dispatching.
348
- // Only a kind that declares one has pure steps at all.
349
- const valueField = stepCtx.value;
350
- if (!invokeField || !outputTypeField)
351
- continue;
352
- const steps = manifest[fieldName];
353
- if (!Array.isArray(steps))
354
- continue;
355
- const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
356
- // The instance's own input contract, for typing a pure step that just
357
- // forwards one of its values.
358
- const ownInputs = resolveTypeFieldToSchema(manifest.inputType, allManifests);
359
- const stepProperties = {};
360
- walkStepArray(steps, stepItemSchema, defSchema, fieldName, (s) => {
361
- const name = s.name;
362
- const invoke = s[invokeField];
363
- // Only invoke steps register a `steps.<name>.result` entry — control-flow
364
- // wrappers (try/if/while/switch/throw) don't produce a result and must
365
- // not shadow real entries with a permissive `additionalProperties: true`,
366
- // or unknown step references slip through chain validation.
367
- if (typeof name !== "string")
368
- return;
369
- if (!invoke || typeof invoke !== "object") {
370
- // A pure step dispatches nothing, so there is no contract to resolve.
371
- // Where its expression is a plain chain into something already typed —
372
- // an earlier step's result, or the kind's own inputs — that type carries
373
- // through; anything else (arithmetic, a call, a comprehension) stays
374
- // permissive rather than guessed. Same rule as a named binding's.
375
- if (valueField && valueField in s) {
376
- const scopeRoot = {
377
- properties: {
378
- steps: { type: "object", properties: { ...stepProperties } },
379
- ...(ownInputs ? { inputs: ownInputs } : {}),
380
- },
381
- };
382
- const chained = schemaAtChain(bindingPathChain(s[valueField]), scopeRoot);
383
- stepProperties[name] = {
384
- type: "object",
385
- properties: { result: chained ?? PERMISSIVE_CONTRACT },
386
- };
387
- }
388
- return;
389
- }
390
- const invokedKind = invoke.kind;
391
- const invokedName = invoke.name;
392
- // A named `!ref` carries the target's own manifest (which may narrow the
393
- // contract for this one instance); an inline `{ kind, ... }` step IS the
394
- // manifest. Either way the kind layer resolves through `extends`.
395
- const invokedManifest = invokedName
396
- ? allManifests.find((m) => m.metadata?.name === invokedName && (!invokedKind || m.kind === invokedKind))
397
- : invoke;
398
- const invokedDef = invokedKind
399
- ? contractScope.resolveIn(invokedKind, readingModule)
400
- : undefined;
401
- const outputSchema = resolveContract(outputTypeField, invokedManifest, invokedDef, contractScope)?.schema;
402
- stepProperties[name] = {
403
- type: "object",
404
- properties: {
405
- result: outputSchema ?? PERMISSIVE_CONTRACT,
406
- },
407
- };
408
- });
409
- if (Object.keys(stepProperties).length > 0) {
410
- return {
411
- type: "object",
412
- properties: stepProperties,
413
- };
414
- }
415
- }
416
- return undefined;
417
- }
418
136
  /** The built-in namespace: globally resolvable, crossing no import boundary. */
419
137
  const TELO_BUILTIN_MODULE = "Telo";
420
138
  /** One mapping from a ref-slot issue to a diagnostic — the shape is identical
@@ -488,7 +206,7 @@ const NO_ENTRY_POINT_CAPABILITIES = new Set([
488
206
  "Telo.Sink",
489
207
  ]);
490
208
  /**
491
- * Validate `x-telo-step-context` step `invoke` references (e.g. `Run.Sequence`
209
+ * Validate step `invoke` references (e.g. `Run.Sequence`
492
210
  * steps).
493
211
  *
494
212
  * The reference field map deliberately does NOT descend into step `invoke`
@@ -507,7 +225,7 @@ const NO_ENTRY_POINT_CAPABILITIES = new Set([
507
225
  * contract has no entry point → `REFERENCE_KIND_MISMATCH` (runtime
508
226
  * `ERR_RESOURCE_NOT_INVOKABLE`).
509
227
  *
510
- * Generic and topology-driven — it walks steps via the same `x-telo-step-context`
228
+ * Generic and topology-driven — it walks steps via the same step-slot
511
229
  * / `x-telo-topology-role` annotations `buildStepContextSchema` uses (through the
512
230
  * shared `walkStepArray`), so nested branches (then/else/do/catch/cases) are
513
231
  * covered and no `Run.Sequence` field name is hardcoded. The cross-module
@@ -626,8 +344,7 @@ function validateStepInvokeReferences(allManifests, defs, aliases) {
626
344
  const resource = { kind: m.kind, name: meta.name };
627
345
  const filePath = typeof meta.source === "string" ? meta.source : undefined;
628
346
  for (const [fieldName, fieldSchema] of Object.entries(defSchema.properties)) {
629
- const stepCtx = fieldSchema["x-telo-step-context"];
630
- const invokeField = stepCtx?.invoke;
347
+ const invokeField = readStepSlot(fieldSchema)?.invoke;
631
348
  if (!invokeField)
632
349
  continue;
633
350
  const steps = m[fieldName];
@@ -676,84 +393,6 @@ function pathCrossesNestedResource(root, path) {
676
393
  }
677
394
  return false;
678
395
  }
679
- function collectErrorContextScopes(defSchema) {
680
- const out = new Map();
681
- if (!defSchema || typeof defSchema !== "object")
682
- return out;
683
- const seen = new Set();
684
- const walk = (schema) => {
685
- if (!schema || typeof schema !== "object" || seen.has(schema))
686
- return;
687
- seen.add(schema);
688
- const props = schema.properties;
689
- if (props) {
690
- for (const [fieldName, fieldSchema] of Object.entries(props)) {
691
- if (fieldSchema && typeof fieldSchema === "object") {
692
- const errCtx = fieldSchema["x-telo-error-context"];
693
- if (errCtx && typeof errCtx === "object" && !out.has(fieldName)) {
694
- out.set(fieldName, errCtx);
695
- }
696
- }
697
- walk(resolveLocalRef(fieldSchema, defSchema));
698
- }
699
- }
700
- if (schema.items)
701
- walk(resolveLocalRef(schema.items, defSchema));
702
- for (const key of ["oneOf", "anyOf", "allOf"]) {
703
- const arr = schema[key];
704
- if (Array.isArray(arr))
705
- for (const sub of arr)
706
- walk(resolveLocalRef(sub, defSchema));
707
- }
708
- if (schema.$defs && typeof schema.$defs === "object") {
709
- for (const sub of Object.values(schema.$defs)) {
710
- walk(sub);
711
- }
712
- }
713
- };
714
- walk(defSchema);
715
- return out;
716
- }
717
- /**
718
- * Return the error-context schema for a CEL `path` when the path lies within
719
- * (any depth under) one of the error-bearing fields, else undefined. A path is
720
- * "within" field `f` when it contains a segment `f[<index>]`. When multiple
721
- * error-bearing fields match (e.g. a `finally` nested inside a `catch`), the
722
- * deepest — the one whose segment appears latest in the path — wins, so the
723
- * innermost branch's schema governs.
724
- */
725
- function errorContextForPath(path, scopes) {
726
- let best;
727
- for (const [fieldName, schema] of scopes) {
728
- const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
729
- for (const match of path.matchAll(new RegExp(`(^|\\.)${escaped}\\[\\d+\\]`, "g"))) {
730
- if (best === undefined || match.index > best.index) {
731
- best = { index: match.index, schema };
732
- }
733
- }
734
- }
735
- return best?.schema;
736
- }
737
- /** Add a kind's named bindings to a resolved context, when the context declares
738
- * a bindings region. They go UNDER the context's own properties: a scope
739
- * variable wins over a same-named binding at runtime, so static typing has to
740
- * agree (the collision itself is `BINDING_NAME_RESERVED`). */
741
- function withBindingNames(contextSchema, resource) {
742
- const field = contextSchema[BINDINGS_ANNOTATION];
743
- if (typeof field !== "string")
744
- return contextSchema;
745
- const bindings = resource[field];
746
- if (bindings === null || typeof bindings !== "object" || Array.isArray(bindings)) {
747
- return contextSchema;
748
- }
749
- return {
750
- ...contextSchema,
751
- properties: {
752
- ...bindingContextProperties(bindings, contextSchema),
753
- ...(contextSchema.properties ?? {}),
754
- },
755
- };
756
- }
757
396
  /** Member-access chains in a CEL expression, or none when it doesn't parse.
758
397
  * Best-effort: a syntax error is reported by the engine pass, not here. */
759
398
  function celAccessChains(env, expr) {
@@ -1032,13 +671,18 @@ export class StaticAnalyzer {
1032
671
  }
1033
672
  seen.add(alias);
1034
673
  }
1035
- if (alias && source) {
1036
- const targetModule = resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
674
+ // An import whose target identity was never established registers NO
675
+ // alias. The name is never guessed from the source string: a guess that
676
+ // is usually right is what turned "this import did not resolve" into an
677
+ // assertion that a published dependency was malformed, naming a module
678
+ // no registry could ever hold. With no alias, every use degrades to
679
+ // "cannot resolve alias '<X>'" — which points at the import the author
680
+ // has to fix, and which the loader has already reported on its own line.
681
+ if (alias && source && resolvedModuleName) {
682
+ const targetModule = resolvedModuleName;
1037
683
  // Module identity is registered globally so x-telo-ref resolution sees
1038
684
  // transitively-imported modules regardless of which scope brought them in.
1039
- if (resolvedModuleName) {
1040
- defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
1041
- }
685
+ defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
1042
686
  // `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
1043
687
  // maps an exported suffix to the true owning module's canonical kind for kinds this
1044
688
  // import transitively re-exports (`exports.kinds: [Alias.Kind]`).
@@ -1092,6 +736,55 @@ export class StaticAnalyzer {
1092
736
  const refConstraintIssues = [];
1093
737
  const refSlotIssues = [];
1094
738
  const zoneSlotIssues = [];
739
+ // One place a rule report becomes a diagnostic. The pass decided WHAT and
740
+ // WHERE; this only carries it across to the diagnostic shape.
741
+ const SEVERITY = {
742
+ error: DiagnosticSeverity.Error,
743
+ warning: DiagnosticSeverity.Warning,
744
+ information: DiagnosticSeverity.Information,
745
+ };
746
+ const resourceRuleDiagnostic = (report) => ({
747
+ severity: SEVERITY[report.severity],
748
+ code: report.code,
749
+ source: SOURCE,
750
+ message: report.message,
751
+ data: {
752
+ resource: {
753
+ kind: report.manifest.kind,
754
+ name: report.manifest.metadata?.name,
755
+ },
756
+ filePath: report.manifest.metadata?.source,
757
+ path: report.path,
758
+ rule: report.rule,
759
+ },
760
+ });
761
+ const referrerRuleDiagnostic = (report) => ({
762
+ severity: SEVERITY[report.severity],
763
+ code: report.code,
764
+ source: SOURCE,
765
+ message: report.message,
766
+ data: {
767
+ resource: {
768
+ kind: report.manifest.kind,
769
+ name: report.manifest.metadata?.name,
770
+ },
771
+ filePath: report.manifest.metadata?.source,
772
+ path: report.path,
773
+ rule: report.rule,
774
+ },
775
+ });
776
+ const projectionIssues = [];
777
+ const resourceRuleIssues = [];
778
+ const referrerRuleIssues = [];
779
+ // A rule that never had anything to iterate is never proven — the second way
780
+ // coverage varies invisibly, beside the dynamic-leaf skip. Tracked across the
781
+ // whole run and reported once, since "empty on every resource" is not a fact
782
+ // any single resource can establish.
783
+ const ruleExercise = new Map();
784
+ // Same for a referrer rule, where "never exercised" means nothing the
785
+ // `referrer:` filter matches ever referenced a resource of the kind — which
786
+ // is exactly what a typo in that filter looks like from the outside.
787
+ const referrerRuleExercise = new Map();
1095
788
  // `x-telo-type` is checked on EVERY manifest, not only on definition docs: a
1096
789
  // schema fragment is written wherever a kind declares a schema-valued field,
1097
790
  // so an inline `inputType:` on an ordinary resource carries one just as a
@@ -1124,6 +817,28 @@ export class StaticAnalyzer {
1124
817
  refConstraintIssues.push(...issues);
1125
818
  refSlotIssues.push(...validateRefSlotDeclarations(m));
1126
819
  zoneSlotIssues.push(...validateZoneSlotDeclarations(m));
820
+ projectionIssues.push(...validateSchemaProjection(m));
821
+ // Checked against the MERGED schema, so an `in:` pointer naming an
822
+ // inherited field resolves — which is what lets a rule shared by every
823
+ // backend be declared once on the abstract they extend.
824
+ resourceRuleIssues.push(...validateResourceRuleDeclarations(m, effectiveAuthorSchema(m, (k) => defs.resolve(aliases.resolveKind(k) ?? k) ?? defs.resolve(k))));
825
+ referrerRuleIssues.push(...validateReferrerRuleDeclarations(m));
826
+ for (const rule of readReferrerRules(m.schema)) {
827
+ referrerRuleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
828
+ manifest: m,
829
+ rule,
830
+ exercised: false,
831
+ seen: false,
832
+ });
833
+ }
834
+ for (const rule of readResourceRules(m.schema)) {
835
+ ruleExercise.set(`${m.metadata?.module}.${m.metadata?.name}#${rule.code}`, {
836
+ manifest: m,
837
+ rule,
838
+ exercised: false,
839
+ seen: false,
840
+ });
841
+ }
1127
842
  }
1128
843
  const resolvedCapability = def.capability
1129
844
  ? (scopeResolver.resolveKind(def.capability) ?? def.capability)
@@ -1177,6 +892,24 @@ export class StaticAnalyzer {
1177
892
  };
1178
893
  const filePath = issue.manifest.metadata?.source;
1179
894
  const data = { resource, filePath, path: issue.path };
895
+ if (issue.annotation === "referrer") {
896
+ // Mirrors ZONE_PROVIDER_UNRESOLVED: a filter naming no kind matches no
897
+ // referrer, so the rule would pass on every manifest while checking
898
+ // nothing — reported at the kind that wrote it, since the consumer
899
+ // cannot see that the check is inert.
900
+ diagnostics.push({
901
+ severity: DiagnosticSeverity.Error,
902
+ code: "REFERRER_RULE_INVALID",
903
+ source: SOURCE,
904
+ message: `x-telo-referrer-rules 'referrer: ${issue.ref}' at '${issue.path}' names no kind. ` +
905
+ `The prefix must be an import alias declared in this file's 'imports:' map, ` +
906
+ `'Self' for a kind in this library, or 'Telo' for a built-in. A filter that ` +
907
+ `matches nothing leaves the rule inert. Known aliases: ` +
908
+ `${issue.knownAliases?.join(", ") || "(none)"}.`,
909
+ data,
910
+ });
911
+ continue;
912
+ }
1180
913
  if (issue.annotation === "zone") {
1181
914
  // Mirrors X_TELO_REF_UNRESOLVED: an unresolvable provider kind would
1182
915
  // leave the requirement silently unenforced — no provider ever
@@ -1262,6 +995,57 @@ export class StaticAnalyzer {
1262
995
  // OPPOSITE directions — a dropped requirement is silently unenforced, a
1263
996
  // dropped provision invents failures — so neither can be left to
1264
997
  // leniency.
998
+ // A projection nothing can read does not fail — it stops typing the
999
+ // consumers counting on it, which puts a misspelled field back where the
1000
+ // projection exists to catch it earlier.
1001
+ for (const issue of resourceRuleIssues) {
1002
+ diagnostics.push({
1003
+ severity: DiagnosticSeverity.Error,
1004
+ code: issue.code,
1005
+ source: SOURCE,
1006
+ message: issue.message,
1007
+ data: {
1008
+ resource: {
1009
+ kind: issue.manifest.kind,
1010
+ name: issue.manifest.metadata?.name,
1011
+ },
1012
+ filePath: issue.manifest.metadata?.source,
1013
+ path: issue.path,
1014
+ },
1015
+ });
1016
+ }
1017
+ for (const issue of referrerRuleIssues) {
1018
+ diagnostics.push({
1019
+ severity: DiagnosticSeverity.Error,
1020
+ code: issue.code,
1021
+ source: SOURCE,
1022
+ message: issue.message,
1023
+ data: {
1024
+ resource: {
1025
+ kind: issue.manifest.kind,
1026
+ name: issue.manifest.metadata?.name,
1027
+ },
1028
+ filePath: issue.manifest.metadata?.source,
1029
+ path: issue.path,
1030
+ },
1031
+ });
1032
+ }
1033
+ for (const issue of projectionIssues) {
1034
+ diagnostics.push({
1035
+ severity: DiagnosticSeverity.Error,
1036
+ code: issue.code,
1037
+ source: SOURCE,
1038
+ message: issue.message,
1039
+ data: {
1040
+ resource: {
1041
+ kind: issue.manifest.kind,
1042
+ name: issue.manifest.metadata?.name,
1043
+ },
1044
+ filePath: issue.manifest.metadata?.source,
1045
+ path: issue.path,
1046
+ },
1047
+ });
1048
+ }
1265
1049
  for (const issue of zoneSlotIssues) {
1266
1050
  diagnostics.push({
1267
1051
  severity: DiagnosticSeverity.Error,
@@ -1296,6 +1080,57 @@ export class StaticAnalyzer {
1296
1080
  aliases,
1297
1081
  aliasesByModule,
1298
1082
  }));
1083
+ /**
1084
+ * Whether a referrer of `kind` satisfies a referrer rule's `referrer:`
1085
+ * filter, which is canonical by the time it gets here (`resolveSchemaRefKinds`
1086
+ * rewrote it in the DECLARING module's scope). The referring manifest's own
1087
+ * `kind:` is not — it is whatever alias its author imported the kind under —
1088
+ * so it is resolved the same way every other kind comparison in this pass
1089
+ * resolves one. Liskov-substitutable, matching `checkKind`: a child of the
1090
+ * named kind is one.
1091
+ */
1092
+ const kindMatches = (filter, kind) => {
1093
+ const resolved = aliases.resolveKind(kind) ?? kind;
1094
+ if (resolved === filter)
1095
+ return true;
1096
+ return defs
1097
+ .getByExtends(filter)
1098
+ .some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved);
1099
+ };
1100
+ /**
1101
+ * The resources that reach `manifest`, with the slot each one reaches it
1102
+ * through. A step's edge is attributed to the resource whose body declares
1103
+ * it: a step is not a manifest, and the requirement is about the resource
1104
+ * that has to declare something.
1105
+ *
1106
+ * Deduplication is the evaluation's, not this function's — a referrer
1107
+ * reaching one resource through two slots is two sites, and which one anchors
1108
+ * the diagnostic is a reporting decision.
1109
+ */
1110
+ const referrersOf = (manifest, graph) => {
1111
+ const name = manifest.metadata?.name;
1112
+ if (!name)
1113
+ return [];
1114
+ const node = graph.resource(manifest.kind, name) ?? graph.resourceByName(name);
1115
+ if (!node)
1116
+ return [];
1117
+ const out = [];
1118
+ for (const edge of graph.edgesTo(node.id)) {
1119
+ const from = graph.nodes.get(edge.from);
1120
+ if (!from)
1121
+ continue;
1122
+ const owner = from.type === "step" ? graph.nodes.get(from.owner) : from;
1123
+ if (!owner || owner.type !== "resource")
1124
+ continue;
1125
+ out.push({
1126
+ manifest: owner.manifest,
1127
+ kind: owner.kind,
1128
+ name: owner.name,
1129
+ path: edge.path,
1130
+ });
1131
+ }
1132
+ return out;
1133
+ };
1299
1134
  // A `use` case map's selector written in CEL is a hard diagnostic — a call
1300
1135
  // graph known only at runtime is not statically analyzable, and no fallback
1301
1136
  // is conservative for every consumer. Scoped to the entry's own modules:
@@ -1323,6 +1158,29 @@ export class StaticAnalyzer {
1323
1158
  moduleDocuments: options?.moduleDocuments,
1324
1159
  cache: zoneExportCache,
1325
1160
  }));
1161
+ // Durable regions — the SAME graph again, walked DOWNWARD this time.
1162
+ // Every rule here keys off a zone attribute rather than off any kind, so
1163
+ // a backend that ships its own workflow kind is covered without the
1164
+ // analyzer knowing it exists: going native costs a module, not a change
1165
+ // here.
1166
+ const resolveRegionDef = (kind, module) => {
1167
+ const scope = (module ? aliasesByModule.get(module) : undefined) ?? aliases;
1168
+ const canonical = scope.resolveKind(kind);
1169
+ return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
1170
+ };
1171
+ diagnostics.push(...validateDurableRegions({
1172
+ graph: getCallGraph(),
1173
+ resolveDef: resolveRegionDef,
1174
+ reportModules: rootModules,
1175
+ }),
1176
+ // The same walk once more, over EVERY attribute rather than the two
1177
+ // durability names — a region must not contain a resource that declares
1178
+ // it cannot honour what the region promises.
1179
+ ...validateZoneViolations({
1180
+ graph: getCallGraph(),
1181
+ resolveDef: resolveRegionDef,
1182
+ reportModules: rootModules,
1183
+ }));
1326
1184
  }
1327
1185
  // Phase 2.6: register each named `Telo.Type` resource's schema under its
1328
1186
  // canonical module-scoped id (`telo://<module>/<name>`), validate
@@ -1537,9 +1395,20 @@ export class StaticAnalyzer {
1537
1395
  // fit the slot at all", the schema answers "do their type arguments agree",
1538
1396
  // which cel-js cannot express because it types by constructor identity.
1539
1397
  const celSourceSchemaByPath = new Map();
1540
- // Context-free typed environments, one per manifest. Reused across every
1541
- // expression in it see the build site for why a matched context opts out.
1542
- 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
+ });
1543
1412
  // Validate each non-definition, non-system resource
1544
1413
  for (const m of allManifests) {
1545
1414
  const filePath = m.metadata?.source;
@@ -1672,6 +1541,56 @@ export class StaticAnalyzer {
1672
1541
  });
1673
1542
  }
1674
1543
  }
1544
+ // Resource rules — relationships between this resource's own fields that
1545
+ // JSON Schema cannot state, declared by the kind as CEL over `self` and
1546
+ // `this`. Read off the AUTHOR-FACING schema, so an `extends` child without
1547
+ // `base:` inherits its parent's rules and one that declares its own
1548
+ // replaces them, exactly as the rest of the config contract merges.
1549
+ // The finding→diagnostic mapping lives with the finding vocabulary in
1550
+ // `validate-resource-rules.ts`, the shape every neighbouring pass uses:
1551
+ // issues out, one emit here.
1552
+ const ruleDeclarer = definition.metadata?.module;
1553
+ for (const report of reportResourceRules(m, definition, evaluateResourceRules(m, authorSchema), !ruleDeclarer || rootModules.has(ruleDeclarer))) {
1554
+ diagnostics.push(resourceRuleDiagnostic(report));
1555
+ }
1556
+ for (const rule of readResourceRules(authorSchema)) {
1557
+ const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
1558
+ const tracked = ruleExercise.get(key);
1559
+ if (!tracked)
1560
+ continue;
1561
+ tracked.seen = true;
1562
+ if (ruleExercised(m, rule))
1563
+ tracked.exercised = true;
1564
+ }
1565
+ // Referrer rules — what must be true of whoever REFERENCES this resource,
1566
+ // declared by the kind that has the requirement rather than by the kind
1567
+ // that must satisfy it. The subject is chosen by the EDGE, so no kind
1568
+ // literal appears on the referring side, where the spelling would be the
1569
+ // consumer's import alias rather than anything the rule's author controls.
1570
+ // A consumer of the shared call graph, never a second traversal.
1571
+ const referrerRules = readReferrerRules(authorSchema);
1572
+ if (referrerRules.length > 0) {
1573
+ const referrers = referrersOf(m, getCallGraph());
1574
+ for (const report of reportReferrerRules(m, definition, evaluateReferrerRules(m, authorSchema, referrers, kindMatches), !ruleDeclarer || rootModules.has(ruleDeclarer))) {
1575
+ // A VIOLATION is the referrer's data, so it is reported only when that
1576
+ // manifest is the entry's own — the same direction a resource-rule
1577
+ // violation takes, one hop further out.
1578
+ const owner = report.manifest.metadata?.module;
1579
+ if (report.code === "REFERRER_RULE_VIOLATED" && owner && !rootModules.has(owner)) {
1580
+ continue;
1581
+ }
1582
+ diagnostics.push(referrerRuleDiagnostic(report));
1583
+ }
1584
+ for (const rule of referrerRules) {
1585
+ const key = `${definition.metadata?.module}.${definition.metadata?.name}#${rule.code}`;
1586
+ const tracked = referrerRuleExercise.get(key);
1587
+ if (!tracked)
1588
+ continue;
1589
+ tracked.seen = true;
1590
+ if (referrerRuleExercised(rule, referrers, kindMatches))
1591
+ tracked.exercised = true;
1592
+ }
1593
+ }
1675
1594
  // Validate inline resources nested inside this resource's body (e.g. a
1676
1595
  // Run.Sequence step's `invoke: { kind, ...config }`). These sit at
1677
1596
  // x-telo-ref slots reached only through local `$ref`s, which the
@@ -1700,6 +1619,23 @@ export class StaticAnalyzer {
1700
1619
  }
1701
1620
  // (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
1702
1621
  }
1622
+ // A rule whose collection was empty on every resource of its kind is a rule
1623
+ // nothing proved. `check()` types `self` only shallowly (cel-js takes a flat
1624
+ // field map, so `columns` is `map`), which means a typo below the first level
1625
+ // survives declaration validation and is caught only by evaluation — so a
1626
+ // rule that never evaluated has been verified by nothing at all. Reported
1627
+ // only when the kind HAS resources here: a kind nobody instantiated in this
1628
+ // workspace says nothing about the rule.
1629
+ for (const tracked of ruleExercise.values()) {
1630
+ if (!tracked.seen || tracked.exercised)
1631
+ continue;
1632
+ diagnostics.push(resourceRuleDiagnostic(reportUnexercisedRule(tracked.manifest, tracked.rule)));
1633
+ }
1634
+ for (const tracked of referrerRuleExercise.values()) {
1635
+ if (!tracked.seen || tracked.exercised)
1636
+ continue;
1637
+ diagnostics.push(referrerRuleDiagnostic(reportUnexercisedReferrerRule(tracked.manifest, tracked.rule)));
1638
+ }
1703
1639
  // Template-body structural validations: check that template entry-points produce
1704
1640
  // values matching the contract of their dispatch target and (for `provide:`)
1705
1641
  // the abstract this definition `extends`. CEL fields inside the templated
@@ -1768,6 +1704,42 @@ export class StaticAnalyzer {
1768
1704
  }
1769
1705
  }
1770
1706
  }
1707
+ // A consumer's slot typed from a referenced declaration
1708
+ // (`x-telo-schema-projection-from`) fails in the direction that is hardest
1709
+ // to notice: the contract silently reopens, so a misspelled field passes
1710
+ // `telo check` exactly as it did before the projection existed. That is the
1711
+ // failure the projection exists to move earlier, so it is reported here
1712
+ // rather than left to degrade. Entry-module-scoped, like
1713
+ // `X_TELO_REF_UNRESOLVED` — a published dependency's slot is not the
1714
+ // consumer's to fix.
1715
+ for (const m of allManifests) {
1716
+ const md = m;
1717
+ if (typeof md.kind !== "string" || md.kind.startsWith("Telo."))
1718
+ continue;
1719
+ const ownModule = md.metadata?.module;
1720
+ if (ownModule && !rootModules.has(ownModule))
1721
+ continue;
1722
+ const definition = contractScope.resolveIn(md.kind, ownModule);
1723
+ if (!definition)
1724
+ continue;
1725
+ const failures = [];
1726
+ for (const direction of ["inputType", "outputType"]) {
1727
+ resolveContract(direction, md, definition, contractScope, failures);
1728
+ }
1729
+ for (const failure of failures) {
1730
+ diagnostics.push({
1731
+ severity: DiagnosticSeverity.Error,
1732
+ code: "SCHEMA_PROJECTION_FROM_UNRESOLVED",
1733
+ source: SOURCE,
1734
+ message: `${md.kind}: ${describeProjectionFailure(failure)}`,
1735
+ data: {
1736
+ resource: { kind: md.kind, name: md.metadata?.name },
1737
+ filePath: md.metadata?.source,
1738
+ path: failure.pointer.replace(/^\//, ""),
1739
+ },
1740
+ });
1741
+ }
1742
+ }
1771
1743
  // Validate CEL syntax and context variable access in all manifests. The
1772
1744
  // walker discovers every compiled CEL node by scanning the value tree and
1773
1745
  // hands back the `x-telo-context` schema matched at the enclosing path; the
@@ -1776,10 +1748,10 @@ export class StaticAnalyzer {
1776
1748
  // state (definitions, aliases, the typed CEL env).
1777
1749
  // Per-resource state computed at enter and read by that resource's CEL
1778
1750
  // sites. The manifest / resource / filePath come straight off each CelSite's
1779
- // `source` (no need to capture them); only the derived step / invocation
1780
- // 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.
1781
1754
  let celStepContextSchema;
1782
- let celInvocationContext;
1783
1755
  let celErrorScopes = new Map();
1784
1756
  // Region coverage for the "CEL in a non-eval field" check: the union of
1785
1757
  // `x-telo-eval` paths (own + capability) and `x-telo-context` /
@@ -1797,14 +1769,24 @@ export class StaticAnalyzer {
1797
1769
  visitManifest(allManifests, defs, {
1798
1770
  onResourceEnter: (e) => {
1799
1771
  const m = e.source;
1800
- celInvocationContext = m.metadata?.xTeloInvocationContext;
1801
- celStepContextSchema = e.definition?.schema
1802
- ? buildStepContextSchema(m, e.definition.schema, allManifests, defs, aliases, { aliasesByModule, rootModules })
1803
- : 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;
1804
1778
  if (e.definition?.schema) {
1805
1779
  const stepName = m.metadata?.name;
1806
1780
  const stepFile = m.metadata?.source;
1807
- 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) {
1808
1790
  diagnostics.push({
1809
1791
  severity: DiagnosticSeverity.Error,
1810
1792
  code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
@@ -1822,7 +1804,6 @@ export class StaticAnalyzer {
1822
1804
  });
1823
1805
  }
1824
1806
  }
1825
- celErrorScopes = collectErrorContextScopes(e.definition?.schema);
1826
1807
  celBindingSites = findBindingSites(e.definition?.schema);
1827
1808
  if (celBindingSites) {
1828
1809
  const declared = m[celBindingSites.field];
@@ -1946,7 +1927,7 @@ export class StaticAnalyzer {
1946
1927
  // (resource-wide invocation context) carry CEL the kernel evaluates.
1947
1928
  if (celRuleApplies &&
1948
1929
  engineName === "cel" &&
1949
- celInvocationContext === undefined &&
1930
+ celScope.invocationContextSchema === undefined &&
1950
1931
  !evalPathsCover(celEvalPaths, path) &&
1951
1932
  !celRegionScopes.some((scope) => pathMatchesScope(path, scope)) &&
1952
1933
  !pathCrossesNestedResource(m, path)) {
@@ -1993,55 +1974,6 @@ export class StaticAnalyzer {
1993
1974
  }
1994
1975
  }
1995
1976
  }
1996
- let matchedContext = e.contextSchema ?? celInvocationContext;
1997
- if (celStepContextSchema) {
1998
- const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
1999
- matchedContext = {
2000
- ...base,
2001
- properties: {
2002
- ...(base.properties ?? {}),
2003
- steps: celStepContextSchema,
2004
- },
2005
- };
2006
- }
2007
- // `error` is only in scope inside an error-bearing branch (e.g. a
2008
- // `catch:` / `finally:`), so it's merged per-path, not resource-wide.
2009
- const errorSchema = celErrorScopes.size > 0 ? errorContextForPath(path, celErrorScopes) : undefined;
2010
- if (errorSchema) {
2011
- const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
2012
- matchedContext = {
2013
- ...base,
2014
- properties: {
2015
- ...(base.properties ?? {}),
2016
- error: errorSchema,
2017
- },
2018
- };
2019
- }
2020
- let effectiveContext = null;
2021
- if (matchedContext) {
2022
- const manifestItem = matchedScope
2023
- ? getManifestItem(path, matchedScope, m)
2024
- : m;
2025
- const rootForResolver = manifestRootForResolver(m, defs, aliases, allManifests, { aliasesByModule, rootModules });
2026
- const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
2027
- manifestRoot: rootForResolver,
2028
- defs,
2029
- aliases,
2030
- allManifests: allManifests,
2031
- });
2032
- effectiveContext = mergeKernelGlobalsIntoContext(withBindingNames(resolvedContext, m),
2033
- // Typed in the module that DECLARED this resource — for a manifest
2034
- // forwarded from an imported library, that is its `moduleGlobals`
2035
- // stamp, not the consuming application's block.
2036
- kernelGlobals.forResource(m));
2037
- }
2038
- else if (observedStateContext) {
2039
- // No `x-telo-context` matched, so nothing was chain-validated here
2040
- // before. Validate the observed-state segment alone rather than
2041
- // merging the kernel globals, whose closed `variables` / `ports`
2042
- // nodes would newly reject reads that pass today.
2043
- effectiveContext = observedStateContext;
2044
- }
2045
1977
  const engine = defaultRegistry().get(engineName);
2046
1978
  if (!engine) {
2047
1979
  // No registered engine owns this tag — the expression would go
@@ -2056,27 +1988,15 @@ export class StaticAnalyzer {
2056
1988
  return;
2057
1989
  }
2058
1990
  // The engine type-checks, so it gets the environment typed for THIS
2059
- // path — not the bare base one. A `Telo.Import`'s variables/secrets
2060
- // are a config-only contract evaluated in the IMPORTING module's
2061
- // scope, so they type from the owning module doc and drop
2062
- // `resources`/`env`, making a reference to either an error.
2063
- //
2064
- // Cached per manifest when no `x-telo-context` applied, which is most
2065
- // expressions: the environment then depends only on the manifest, so
2066
- // rebuilding it per expression is pure waste — a clone plus a
2067
- // re-registration of every variable, on every keystroke in the IDE.
2068
- // A matched context makes the environment path-specific (its schema is
2069
- // resolved against the enclosing array item), so those still build
2070
- // fresh rather than risk one item's types leaking into another's.
2071
- const cached = effectiveContext === null ? typedEnvByManifest.get(m) : undefined;
2072
- const typedEnv = cached ??
2073
- (m.kind === "Telo.Import"
2074
- ? buildImportInputCelEnvironment(this.celEnv, allManifests.find((mm) => (mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
2075
- mm.metadata?.name ===
2076
- m.metadata?.module))
2077
- : buildTypedCelEnvironment(this.celEnv, m, effectiveContext ?? undefined, moduleManifest));
2078
- if (effectiveContext === null && !cached)
2079
- 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
+ });
2080
2000
  const result = engine.analyze(expr, { celEnv: typedEnv, contextSchema: effectiveContext });
2081
2001
  if (result.type !== undefined) {
2082
2002
  let byPath = celTypeByPath.get(m);