@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/src/call-graph.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  type RefUse,
56
56
  type RefUseCases,
57
57
  } from "./ref-slot.js";
58
+ import { isStepSlot } from "./step-slot.js";
58
59
  import { isRefEntry, resolveFieldEntries, type RefFieldEntry } from "./reference-field-map.js";
59
60
  import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
60
61
 
@@ -292,12 +293,6 @@ function resolveUseAtSite(
292
293
  return { use: possibleUses(slot), unresolved: entry.useCases, unresolvedReason };
293
294
  }
294
295
 
295
- /** Names the step-list annotation on an array property, if any. */
296
- function stepContextOf(schema: Record<string, any> | undefined): Record<string, any> | undefined {
297
- const annotation = schema?.["x-telo-step-context"];
298
- return annotation && typeof annotation === "object" ? annotation : undefined;
299
- }
300
-
301
296
  /** A resolved plain reference value (`{kind, name}`, optionally `alias`) — the
302
297
  * shape `resolveRefSentinels` leaves at a ref site. NOT a step: a bare boot
303
298
  * target written `!ref X` must not mint a step node. */
@@ -545,8 +540,7 @@ export function buildCallGraph(
545
540
  if (!schema) return;
546
541
  const collected: StepGraphNode[] = [];
547
542
  for (const [key, propSchema] of propertySchemas(schema)) {
548
- const annotation = stepContextOf(propSchema);
549
- if (!annotation) continue;
543
+ if (!isStepSlot(propSchema)) continue;
550
544
  const value = (node.manifest as Record<string, unknown>)[key];
551
545
  if (!Array.isArray(value)) continue;
552
546
  walkSteps(value, key, undefined, {
@@ -0,0 +1,337 @@
1
+ /**
2
+ * **Ask what a CEL site sees, from outside the analysis pass.**
3
+ *
4
+ * The pass resolves a scope per expression as it walks. An IDE has no walk: it
5
+ * has a cursor, and needs the same answer for one address — often for an
6
+ * expression the last analysis never saw, because the user is typing it. So the
7
+ * query is driven by (manifest, path) rather than by a visitor event, and the
8
+ * `x-telo-context` match is recomputed here exactly as the visitor computes it
9
+ * (`extractContextsFromSchema` + `pathMatchesScope`, the same two functions).
10
+ *
11
+ * The scope RULE itself is not re-implemented — {@link CelScopeResolver} is the
12
+ * one that answers, here and in the pass. What this module adds is the way in.
13
+ */
14
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
15
+ import type { Environment } from "@marcbachmann/cel-js";
16
+ import { AliasResolver, type ModuleScopes } from "./alias-resolver.js";
17
+ import { buildCelEnvironment } from "./cel-environment.js";
18
+ import { CelScopeResolver, type CelScope } from "./cel-scope.js";
19
+ import { DefinitionRegistry } from "./definition-registry.js";
20
+ import { buildKernelGlobalsIndex } from "./kernel-globals.js";
21
+ import { isModuleKind } from "./module-kinds.js";
22
+ import { navigateConcretePath } from "./manifest-path.js";
23
+ import { findManifest } from "./find-manifest.js";
24
+ import { resolveLocalRef, walkStepArray } from "./schema-walk.js";
25
+ import { readStepSlot } from "./step-slot.js";
26
+ import {
27
+ buildObservedStateIndex,
28
+ buildObservedStateResourcesSchema,
29
+ } from "./validate-observed-state.js";
30
+ import {
31
+ extractContextsFromSchema,
32
+ getManifestItem,
33
+ pathMatchesScope,
34
+ } from "./validate-cel-context.js";
35
+
36
+ /** Where a CEL context binding was declared: the manifest that declares it,
37
+ * by identity, plus the concrete path within it. Identity rather than the
38
+ * object because a host locates a manifest in its own loaded files, which is
39
+ * what carries the source ranges. */
40
+ export interface ContextDeclarationSite {
41
+ kind: string;
42
+ name: string;
43
+ path: string;
44
+ }
45
+
46
+ /** Join a concrete path segment, tolerating an empty base (the manifest root). */
47
+ function joinPath(base: string, segment: string): string {
48
+ return base ? `${base}.${segment}` : segment;
49
+ }
50
+
51
+ /** The concrete path of the array ITEM an `x-telo-context` scope matched — the
52
+ * path half of `getManifestItem`, which returns only the value. Empty when the
53
+ * scope is not per-item. */
54
+ function manifestItemPath(exprPath: string, scope: string | undefined): string {
55
+ if (!scope) return "";
56
+ const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
57
+ const wildcard = stripped.indexOf("[*]");
58
+ if (wildcard === -1) return "";
59
+ const arrayProp = stripped.slice(0, wildcard);
60
+ const match = exprPath.match(new RegExp(`^${arrayProp}\\[(\\d+)\\]`));
61
+ return match ? `${arrayProp}[${match[1]}]` : "";
62
+ }
63
+
64
+ /** The analyzer state a query resolves against — what an `AnalysisRegistry`
65
+ * already holds, plus the manifest set the caller analyzed. */
66
+ export interface CelScopeQueryContext {
67
+ defs: DefinitionRegistry;
68
+ aliases: AliasResolver;
69
+ aliasesByModule: Map<string, AliasResolver>;
70
+ }
71
+
72
+ /**
73
+ * A reusable scope query over one manifest set.
74
+ *
75
+ * Built once per analysis rather than per keystroke: the observed-state index
76
+ * and the kernel globals are a function of the whole set, and rebuilding them
77
+ * for every cursor move would put a full-set walk on the hover path.
78
+ */
79
+ export class CelScopeQuery {
80
+ private readonly resolver: CelScopeResolver;
81
+ /** The resource the resolver is currently entered on — `enterResource` is the
82
+ * per-resource half of the rule, so re-entering per query is only needed when
83
+ * the cursor moves to a different resource. */
84
+ private entered: ResourceManifest | undefined;
85
+ /** Resolved scopes, keyed by resource and path. A context-matched site builds
86
+ * a fresh typed environment by design (a clone plus a re-registration of
87
+ * every variable), which is affordable once per site in a batch pass and not
88
+ * once per site per KEYSTROKE — which is what a whole-file colourizer asks
89
+ * for. The lifetime is this query's, which is the analysis's. */
90
+ private readonly scopeCache = new Map<ResourceManifest, Map<string, CelScope>>();
91
+
92
+ constructor(
93
+ private readonly manifests: ResourceManifest[],
94
+ ctx: CelScopeQueryContext,
95
+ celEnv?: Environment,
96
+ ) {
97
+ const { defs, aliases, aliasesByModule } = ctx;
98
+ const rootModules = new Set<string>();
99
+ for (const m of manifests) {
100
+ if (isModuleKind(m.kind) && m.metadata?.name) rootModules.add(m.metadata.name as string);
101
+ }
102
+ const scopes: ModuleScopes = { aliasesByModule, rootModules };
103
+ const observedState = buildObservedStateIndex(manifests, defs, aliases, scopes);
104
+ const reportsObservedState = [...observedState.values()].some((r) => r.status);
105
+
106
+ this.resolver = new CelScopeResolver({
107
+ celEnv: celEnv ?? buildCelEnvironment(),
108
+ defs,
109
+ aliases,
110
+ scopes,
111
+ allManifests: manifests,
112
+ kernelGlobals: buildKernelGlobalsIndex(manifests, observedState),
113
+ moduleManifest:
114
+ manifests.find((mm) => mm.kind === "Telo.Application") ??
115
+ manifests.find((mm) => mm.kind === "Telo.Library"),
116
+ observedStateContext: reportsObservedState
117
+ ? {
118
+ type: "object",
119
+ additionalProperties: true,
120
+ properties: { resources: buildObservedStateResourcesSchema(observedState, true) },
121
+ }
122
+ : null,
123
+ });
124
+ this.ctx = ctx;
125
+ }
126
+
127
+ private readonly ctx: CelScopeQueryContext;
128
+
129
+ /** The manifest a cursor's document addresses — {@link findManifest}, the one
130
+ * implementation `ManifestAnalysis` also answers from. */
131
+ resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined {
132
+ return findManifest(this.manifests, kind, name);
133
+ }
134
+
135
+ /**
136
+ * What CEL at `path` in `resource` is typed against.
137
+ *
138
+ * `path` is the CONCRETE path, indices and all (`routes[0].handler.url`) —
139
+ * an `x-telo-context` region, an error-bearing branch and a step's identity
140
+ * are each addressed per item, so an index-erased path resolves the wrong
141
+ * scope or none.
142
+ */
143
+ scopeAt(resource: ResourceManifest, path: string): CelScope {
144
+ let byPath = this.scopeCache.get(resource);
145
+ if (!byPath) this.scopeCache.set(resource, (byPath = new Map()));
146
+ const cached = byPath.get(path);
147
+ if (cached) return cached;
148
+ if (this.entered !== resource) {
149
+ this.resolver.enterResource(resource, this.definitionFor(resource));
150
+ this.entered = resource;
151
+ }
152
+ const { contextSchema, matchedScope } = this.matchContext(resource, path);
153
+ const scope = this.resolver.scopeFor({ source: resource, path, contextSchema, matchedScope });
154
+ byPath.set(path, scope);
155
+ return scope;
156
+ }
157
+
158
+ /**
159
+ * Where a CEL context binding was DECLARED — the manifest node the
160
+ * `x-telo-context-*` annotation derived it from.
161
+ *
162
+ * The mirror of {@link scopeAt}: that one resolves the annotation into a
163
+ * schema and the provenance is gone by the time it returns, while
164
+ * go-to-declaration wants the path and not the schema. Re-walking the
165
+ * annotation is what keeps `CelScope` free of an origin field every type
166
+ * consumer would have to ignore.
167
+ *
168
+ * Generic over the annotation, so `request.query` lands on the route's own
169
+ * `request.schema.query`, `self.<field>` on the definition's `schema`, and
170
+ * `result.<field>` on the INVOKED resource's `outputType` — one walk, no
171
+ * transport and no resource kind named here.
172
+ *
173
+ * Every candidate path is checked against the manifest before it is returned,
174
+ * so a binding the author never declared (a context annotation's static
175
+ * fallback properties) resolves to nothing rather than to a guessed node.
176
+ */
177
+ contextDeclarationSite(
178
+ resource: ResourceManifest,
179
+ sitePath: string,
180
+ parts: string[],
181
+ ): ContextDeclarationSite | undefined {
182
+ if (parts.length < 2) return undefined;
183
+ const { contextSchema, matchedScope } = this.matchContext(resource, sitePath);
184
+ const annotated = contextSchema?.properties?.[parts[0]] as Record<string, any> | undefined;
185
+ if (!annotated) return undefined;
186
+
187
+ const origin = this.originOf(annotated, resource, sitePath, matchedScope);
188
+ if (!origin) return undefined;
189
+
190
+ // The ORIGIN decides the shape rather than positional fallthrough. A
191
+ // property-map origin holds the names themselves, so its first hop is direct
192
+ // and ONLY direct — trying `properties` there would let an author's own
193
+ // `properties:` key win over the name they wrote. Every deeper hop, and
194
+ // every hop of a JSON-Schema origin, goes through `properties`, with the
195
+ // inline `{ kind, schema }` wrapper as the one alternative a type field is
196
+ // routinely written as.
197
+ let base = origin.path;
198
+ for (let i = 1; i < parts.length; i++) {
199
+ const candidates =
200
+ i === 1 && origin.propertyMap
201
+ ? [joinPath(base, parts[i])]
202
+ : [
203
+ joinPath(joinPath(base, "properties"), parts[i]),
204
+ joinPath(joinPath(joinPath(base, "schema"), "properties"), parts[i]),
205
+ ];
206
+ const hit = candidates.find(
207
+ (candidate) => navigateConcretePath(origin.manifest, candidate) !== undefined,
208
+ );
209
+ if (!hit) return undefined;
210
+ base = hit;
211
+ }
212
+
213
+ const metadata = origin.manifest.metadata as { name?: string } | undefined;
214
+ if (!origin.manifest.kind || !metadata?.name) return undefined;
215
+ return { kind: origin.manifest.kind, name: metadata.name, path: base };
216
+ }
217
+
218
+ /** The manifest and path an annotated context property is derived from, and
219
+ * whether that node holds a property MAP (names directly) or a JSON Schema. */
220
+ private originOf(
221
+ annotated: Record<string, any>,
222
+ resource: ResourceManifest,
223
+ sitePath: string,
224
+ matchedScope: string | undefined,
225
+ ): { manifest: Record<string, any>; path: string; propertyMap: boolean } | undefined {
226
+ const root = resource as Record<string, any>;
227
+
228
+ // Per-scope: the annotation navigates the enclosing ARRAY ITEM, so the path
229
+ // it yields is relative to that item rather than to the resource.
230
+ const from = annotated["x-telo-context-from"];
231
+ if (typeof from === "string") {
232
+ const itemPath = manifestItemPath(sitePath, matchedScope);
233
+ return { manifest: root, path: joinPath(itemPath, from.split("/").join(".")), propertyMap: true };
234
+ }
235
+
236
+ const fromRoot = annotated["x-telo-context-from-root"];
237
+ if (typeof fromRoot === "string") {
238
+ return { manifest: root, path: fromRoot.split("/").join("."), propertyMap: false };
239
+ }
240
+
241
+ // Cross-manifest: the binding is declared by whatever this slot REFERENCES,
242
+ // which is the node a reader wants when a result's members do not resolve.
243
+ const refFrom = annotated["x-telo-context-ref-from"];
244
+ if (typeof refFrom === "string") {
245
+ const slash = refFrom.indexOf("/");
246
+ if (slash === -1) return undefined;
247
+ const item = matchedScope
248
+ ? getManifestItem(sitePath, matchedScope, root)
249
+ : root;
250
+ const ref = item[refFrom.slice(0, slash)] as { kind?: string; name?: string } | undefined;
251
+ if (!ref?.kind || !ref.name) return undefined;
252
+ const target = this.manifests.find(
253
+ (m) => m.kind === ref.kind && (m.metadata as { name?: string } | undefined)?.name === ref.name,
254
+ ) as Record<string, any> | undefined;
255
+ if (!target) return undefined;
256
+ return { manifest: target, path: refFrom.slice(slash + 1).split("/").join("."), propertyMap: false };
257
+ }
258
+
259
+ // A kind's own declaration: the target is the `Telo.Definition` document,
260
+ // which is an ordinary manifest in the set.
261
+ const fromRefKind = annotated["x-telo-context-from-ref-kind"];
262
+ const first = Array.isArray(fromRefKind) ? fromRefKind[0] : fromRefKind;
263
+ if (typeof first === "string") {
264
+ const hash = first.indexOf("#");
265
+ if (hash <= 0) return undefined;
266
+ const kindValue = navigateConcretePath(root, first.slice(0, hash).split("/").join("."));
267
+ if (typeof kindValue !== "string") return undefined;
268
+ const canonical = this.ctx.aliases.resolveKind(kindValue) ?? kindValue;
269
+ const suffix = canonical.slice(canonical.indexOf(".") + 1);
270
+ const target = this.manifests.find(
271
+ (m) =>
272
+ (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") &&
273
+ (m.metadata as { name?: string } | undefined)?.name === suffix,
274
+ ) as Record<string, any> | undefined;
275
+ if (!target) return undefined;
276
+ return { manifest: target, path: first.slice(hash + 1), propertyMap: false };
277
+ }
278
+
279
+ // `x-telo-context-element-from` / `-collection-from` type a binding from an
280
+ // EXPRESSION, so there is no declaration to navigate to.
281
+ return undefined;
282
+ }
283
+
284
+ /**
285
+ * Where the step named `stepName` is declared in `resource`, as a concrete
286
+ * path (`steps[2]`), or undefined when the resource declares no step body or
287
+ * holds no such step.
288
+ *
289
+ * For go-to-declaration on `steps.<name>.result`, which is the one CEL scope
290
+ * whose members ARE written somewhere in the manifest but are reached through
291
+ * no reference slot. Driven by the kind's own step-body annotation and the
292
+ * shared nesting walk, so a step inside a `try:` inside a `catch:` is found
293
+ * and no resource kind is named here.
294
+ */
295
+ stepDeclarationPath(resource: ResourceManifest, stepName: string): string | undefined {
296
+ const schema = this.definitionFor(resource)?.schema as Record<string, any> | undefined;
297
+ const props = schema?.properties as Record<string, any> | undefined;
298
+ if (!schema || !props) return undefined;
299
+ for (const [fieldName, fieldSchema] of Object.entries(props)) {
300
+ if (!readStepSlot(fieldSchema)) continue;
301
+ const steps = (resource as Record<string, any>)[fieldName];
302
+ if (!Array.isArray(steps)) continue;
303
+ const itemSchema = resolveLocalRef(
304
+ (fieldSchema as Record<string, any>).items as Record<string, any> | undefined,
305
+ schema,
306
+ );
307
+ let found: string | undefined;
308
+ walkStepArray(steps, itemSchema, schema, fieldName, (step, stepPath) => {
309
+ if (found === undefined && step.name === stepName) found = stepPath;
310
+ });
311
+ if (found) return found;
312
+ }
313
+ return undefined;
314
+ }
315
+
316
+ private definitionFor(resource: ResourceManifest): ResourceDefinition | undefined {
317
+ const { defs, aliases } = this.ctx;
318
+ const canonical = aliases.resolveKind(resource.kind);
319
+ return defs.resolve(resource.kind) ?? (canonical ? defs.resolve(canonical) : undefined);
320
+ }
321
+
322
+ /** The `x-telo-context` region this path falls in, matched exactly as the
323
+ * manifest visitor matches it for an expression it walked onto. */
324
+ private matchContext(
325
+ resource: ResourceManifest,
326
+ path: string,
327
+ ): { contextSchema?: Record<string, any>; matchedScope?: string } {
328
+ const schema = this.definitionFor(resource)?.schema as Record<string, any> | undefined;
329
+ if (!schema) return {};
330
+ for (const ctx of extractContextsFromSchema(schema)) {
331
+ if (pathMatchesScope(path, ctx.scope)) {
332
+ return { contextSchema: ctx.schema, matchedScope: ctx.scope };
333
+ }
334
+ }
335
+ return {};
336
+ }
337
+ }