@telorun/ide-support 0.14.1 → 0.16.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 (85) hide show
  1. package/dist/cel/cursor-chain.d.ts +30 -0
  2. package/dist/cel/cursor-chain.d.ts.map +1 -0
  3. package/dist/cel/cursor-chain.js +30 -0
  4. package/dist/cel/symbols.d.ts +82 -0
  5. package/dist/cel/symbols.d.ts.map +1 -0
  6. package/dist/cel/symbols.js +147 -0
  7. package/dist/cel/tokens.d.ts +32 -0
  8. package/dist/cel/tokens.d.ts.map +1 -0
  9. package/dist/cel/tokens.js +162 -0
  10. package/dist/completions/build.d.ts +6 -2
  11. package/dist/completions/build.d.ts.map +1 -1
  12. package/dist/completions/build.js +52 -24
  13. package/dist/completions/call-inputs.d.ts +25 -0
  14. package/dist/completions/call-inputs.d.ts.map +1 -0
  15. package/dist/completions/call-inputs.js +78 -0
  16. package/dist/completions/cel-completions.d.ts +26 -0
  17. package/dist/completions/cel-completions.d.ts.map +1 -0
  18. package/dist/completions/cel-completions.js +78 -0
  19. package/dist/completions/detect-context.d.ts +47 -8
  20. package/dist/completions/detect-context.d.ts.map +1 -1
  21. package/dist/completions/detect-context.js +51 -15
  22. package/dist/completions/prop-keys.d.ts +5 -1
  23. package/dist/completions/prop-keys.d.ts.map +1 -1
  24. package/dist/completions/prop-keys.js +51 -3
  25. package/dist/completions/resolve-node.d.ts +9 -2
  26. package/dist/completions/resolve-node.d.ts.map +1 -1
  27. package/dist/completions/resolve-node.js +63 -21
  28. package/dist/definition/build-definition.d.ts +6 -2
  29. package/dist/definition/build-definition.d.ts.map +1 -1
  30. package/dist/definition/build-definition.js +16 -3
  31. package/dist/definition/locate-context-binding.d.ts +15 -0
  32. package/dist/definition/locate-context-binding.d.ts.map +1 -0
  33. package/dist/definition/locate-context-binding.js +35 -0
  34. package/dist/definition/locate-step.d.ts +13 -0
  35. package/dist/definition/locate-step.d.ts.map +1 -0
  36. package/dist/definition/locate-step.js +33 -0
  37. package/dist/definition/resolve-cel-target.d.ts +11 -1
  38. package/dist/definition/resolve-cel-target.d.ts.map +1 -1
  39. package/dist/definition/resolve-cel-target.js +14 -14
  40. package/dist/doc-identity.d.ts +17 -0
  41. package/dist/doc-identity.d.ts.map +1 -0
  42. package/dist/doc-identity.js +19 -0
  43. package/dist/hover/build-hover.d.ts +6 -2
  44. package/dist/hover/build-hover.d.ts.map +1 -1
  45. package/dist/hover/build-hover.js +64 -3
  46. package/dist/import-upgrades/build-import-upgrades.d.ts +57 -3
  47. package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -1
  48. package/dist/import-upgrades/build-import-upgrades.js +67 -10
  49. package/dist/import-upgrades/index.d.ts +5 -2
  50. package/dist/import-upgrades/index.d.ts.map +1 -1
  51. package/dist/import-upgrades/index.js +3 -1
  52. package/dist/import-upgrades/manifest-cache-url.d.ts +17 -0
  53. package/dist/import-upgrades/manifest-cache-url.d.ts.map +1 -0
  54. package/dist/import-upgrades/manifest-cache-url.js +30 -0
  55. package/dist/import-upgrades/version-compatibility.d.ts +92 -0
  56. package/dist/import-upgrades/version-compatibility.d.ts.map +1 -0
  57. package/dist/import-upgrades/version-compatibility.js +121 -0
  58. package/dist/semantic-tokens/build-semantic-tokens.d.ts +13 -8
  59. package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -1
  60. package/dist/semantic-tokens/build-semantic-tokens.js +81 -37
  61. package/dist/types.d.ts +25 -5
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +16 -2
  64. package/package.json +2 -2
  65. package/src/cel/cursor-chain.ts +58 -0
  66. package/src/cel/symbols.ts +189 -0
  67. package/src/cel/tokens.ts +169 -0
  68. package/src/completions/build.ts +85 -22
  69. package/src/completions/call-inputs.ts +92 -0
  70. package/src/completions/cel-completions.ts +108 -0
  71. package/src/completions/detect-context.ts +107 -13
  72. package/src/completions/prop-keys.ts +59 -2
  73. package/src/completions/resolve-node.ts +82 -17
  74. package/src/definition/build-definition.ts +30 -2
  75. package/src/definition/locate-context-binding.ts +53 -0
  76. package/src/definition/locate-step.ts +50 -0
  77. package/src/definition/resolve-cel-target.ts +25 -0
  78. package/src/doc-identity.ts +31 -0
  79. package/src/hover/build-hover.ts +67 -1
  80. package/src/import-upgrades/build-import-upgrades.ts +115 -11
  81. package/src/import-upgrades/index.ts +22 -1
  82. package/src/import-upgrades/manifest-cache-url.ts +40 -0
  83. package/src/import-upgrades/version-compatibility.ts +196 -0
  84. package/src/semantic-tokens/build-semantic-tokens.ts +84 -30
  85. package/src/types.ts +47 -6
@@ -48,8 +48,15 @@ export interface ResolvedCursor {
48
48
  /** Key slot: number of `path` segments that reach `resourceKind`'s map, so
49
49
  * the schema-relative path is `path.slice(resourceDepth)`. */
50
50
  resourceDepth?: number;
51
- /** Set when the cursor sits inside a CEL body (closed or open). Populated for
52
- * a future CEL-completion feature; this refactor does not consume it. */
51
+ /** The path with sequence INDICES kept (`routes[0].handler.url`),
52
+ * as distinct from `path`, through which arrays are transparent. This is the
53
+ * address the analyzer speaks — an `x-telo-context` scope, an error-bearing
54
+ * region and a step's identity are all resolved per item — so it is what a
55
+ * CEL site is looked up by. */
56
+ concretePath?: string;
57
+ /** Set when the cursor sits inside a CEL body (closed or open). The CEL
58
+ * segment plus the cursor's document offset — what completion and hover
59
+ * hit-test the expression's chain against. */
53
60
  cel?: { segment: CelSegment; offset: number };
54
61
  }
55
62
 
@@ -109,6 +116,7 @@ type Descent =
109
116
  type: "key";
110
117
  container: AstMap;
111
118
  path: string[];
119
+ concretePath: string;
112
120
  keyNode: AstScalar;
113
121
  keyName?: string;
114
122
  scope: ResourceScope;
@@ -119,15 +127,22 @@ type Descent =
119
127
  * (which has no keyed siblings). */
120
128
  container: AstMap | undefined;
121
129
  path: string[];
130
+ concretePath: string;
122
131
  keyName?: string;
123
132
  keyEnd: number;
124
133
  valueNode: AstNode;
125
134
  }
126
135
  | { type: "empty" };
127
136
 
137
+ /** Append one key segment to a concrete path (`routes[0]` + `handler`). */
138
+ function joinKey(concrete: string, key: string): string {
139
+ return concrete ? `${concrete}.${key}` : key;
140
+ }
141
+
128
142
  function descend(
129
143
  node: AstNode,
130
144
  ancestors: string[],
145
+ concrete: string,
131
146
  offset: number,
132
147
  scope: ResourceScope,
133
148
  ): Descent | undefined {
@@ -140,6 +155,7 @@ function descend(
140
155
  type: "key",
141
156
  container: node,
142
157
  path: ancestors,
158
+ concretePath: concrete,
143
159
  keyNode: pair.key as AstScalar,
144
160
  keyName,
145
161
  scope: mapScope,
@@ -147,13 +163,15 @@ function descend(
147
163
  }
148
164
  if (pair.value && within(pair.value.range, offset)) {
149
165
  const childAncestors = keyName != null ? [...ancestors, keyName] : ancestors;
166
+ const childConcrete = keyName != null ? joinKey(concrete, keyName) : concrete;
150
167
  if (pair.value.kind === "map" || pair.value.kind === "seq") {
151
- return descend(pair.value, childAncestors, offset, mapScope) ?? { type: "empty" };
168
+ return descend(pair.value, childAncestors, childConcrete, offset, mapScope) ?? { type: "empty" };
152
169
  }
153
170
  return {
154
171
  type: "value",
155
172
  container: node,
156
173
  path: ancestors,
174
+ concretePath: childConcrete,
157
175
  keyName,
158
176
  keyEnd: pair.key.range[1],
159
177
  valueNode: pair.value,
@@ -164,16 +182,26 @@ function descend(
164
182
  }
165
183
  if (node.kind === "seq") {
166
184
  // Sequence items are transparent to the key path (mirrors the schema
167
- // walker, which auto-descends arrays).
168
- for (const item of node.items) {
185
+ // walker, which auto-descends arrays) but NOT to the concrete path: an
186
+ // `x-telo-context` scope, an error-bearing region and a step's identity are
187
+ // all addressed per item, so a CEL site is unreachable without the index.
188
+ for (const [index, item] of node.items.entries()) {
169
189
  if (within(item.range, offset)) {
190
+ const itemConcrete = `${concrete}[${index}]`;
170
191
  if (item.kind === "map" || item.kind === "seq") {
171
- return descend(item, ancestors, offset, scope) ?? { type: "empty" };
192
+ return descend(item, ancestors, itemConcrete, offset, scope) ?? { type: "empty" };
172
193
  }
173
194
  // A bare scalar list item (`targets:\n - One`) has no enclosing map of
174
195
  // keyed siblings — leave `container` undefined rather than treating the
175
196
  // seq as a map.
176
- return { type: "value", container: undefined, path: ancestors, keyEnd: item.range[0], valueNode: item };
197
+ return {
198
+ type: "value",
199
+ container: undefined,
200
+ path: ancestors,
201
+ concretePath: itemConcrete,
202
+ keyEnd: item.range[0],
203
+ valueNode: item,
204
+ };
177
205
  }
178
206
  }
179
207
  return undefined;
@@ -187,6 +215,9 @@ function descend(
187
215
 
188
216
  interface MapScope {
189
217
  path: string[];
218
+ /** The same location with sequence indices kept — what addresses a manifest
219
+ * node, as distinct from `path`, which addresses a schema node. */
220
+ concrete: string;
190
221
  childColumn: number;
191
222
  keys: Set<string>;
192
223
  rangeStart: number;
@@ -195,6 +226,7 @@ interface MapScope {
195
226
 
196
227
  interface PairScope {
197
228
  path: string[]; // full key path to this pair
229
+ concrete: string;
198
230
  keyColumn: number;
199
231
  keyOffset: number;
200
232
  childKeys: Set<string>;
@@ -204,6 +236,7 @@ interface PairScope {
204
236
  function collectScopes(
205
237
  node: AstNode,
206
238
  ancestors: string[],
239
+ concrete: string,
207
240
  scope: ResourceScope,
208
241
  lineOffsets: number[],
209
242
  maps: MapScope[],
@@ -219,11 +252,19 @@ function collectScopes(
219
252
  if (childColumn < 0) childColumn = offsetToPosition(pair.key.range[0], lineOffsets).character;
220
253
  }
221
254
  if (childColumn >= 0) {
222
- maps.push({ path: ancestors, childColumn, keys, rangeStart: node.range[0], scope: mapScope });
255
+ maps.push({
256
+ path: ancestors,
257
+ concrete,
258
+ childColumn,
259
+ keys,
260
+ rangeStart: node.range[0],
261
+ scope: mapScope,
262
+ });
223
263
  }
224
264
  for (const pair of node.entries) {
225
265
  const keyName = scalarString(pair.key);
226
266
  const fullPath = keyName != null ? [...ancestors, keyName] : ancestors;
267
+ const fullConcrete = keyName != null ? joinKey(concrete, keyName) : concrete;
227
268
  const childKeys = new Set<string>();
228
269
  if (pair.value?.kind === "map") {
229
270
  for (const p of pair.value.entries) {
@@ -233,20 +274,26 @@ function collectScopes(
233
274
  }
234
275
  pairs.push({
235
276
  path: fullPath,
277
+ concrete: fullConcrete,
236
278
  keyColumn: offsetToPosition(pair.key.range[0], lineOffsets).character,
237
279
  keyOffset: pair.key.range[0],
238
280
  childKeys,
239
281
  scope: mapScope,
240
282
  });
241
- if (pair.value) collectScopes(pair.value, fullPath, mapScope, lineOffsets, maps, pairs);
283
+ if (pair.value) {
284
+ collectScopes(pair.value, fullPath, fullConcrete, mapScope, lineOffsets, maps, pairs);
285
+ }
242
286
  }
243
287
  } else if (node.kind === "seq") {
244
- for (const item of node.items) collectScopes(item, ancestors, scope, lineOffsets, maps, pairs);
288
+ node.items.forEach((item, index) =>
289
+ collectScopes(item, ancestors, `${concrete}[${index}]`, scope, lineOffsets, maps, pairs),
290
+ );
245
291
  }
246
292
  }
247
293
 
248
294
  interface KeyResolution {
249
295
  path: string[];
296
+ concrete: string;
250
297
  existingKeys: Set<string>;
251
298
  scope: ResourceScope;
252
299
  }
@@ -262,7 +309,7 @@ function columnSearch(
262
309
  ): KeyResolution {
263
310
  const maps: MapScope[] = [];
264
311
  const pairs: PairScope[] = [];
265
- collectScopes(root, [], { depth: 0 }, lineOffsets, maps, pairs);
312
+ collectScopes(root, [], "", { depth: 0 }, lineOffsets, maps, pairs);
266
313
 
267
314
  // Sibling level: a map whose children already sit at the cursor's column.
268
315
  let sibling: MapScope | undefined;
@@ -271,7 +318,14 @@ function columnSearch(
271
318
  if (!sibling || m.rangeStart > sibling.rangeStart) sibling = m;
272
319
  }
273
320
  }
274
- if (sibling) return { path: sibling.path, existingKeys: sibling.keys, scope: sibling.scope };
321
+ if (sibling) {
322
+ return {
323
+ path: sibling.path,
324
+ concrete: sibling.concrete,
325
+ existingKeys: sibling.keys,
326
+ scope: sibling.scope,
327
+ };
328
+ }
275
329
 
276
330
  // Nest under the nearest-preceding key shallower than the cursor.
277
331
  let nest: PairScope | undefined;
@@ -286,9 +340,16 @@ function columnSearch(
286
340
  }
287
341
  }
288
342
  }
289
- if (nest) return { path: nest.path, existingKeys: nest.childKeys, scope: nest.scope };
343
+ if (nest) {
344
+ return {
345
+ path: nest.path,
346
+ concrete: nest.concrete,
347
+ existingKeys: nest.childKeys,
348
+ scope: nest.scope,
349
+ };
350
+ }
290
351
 
291
- return { path: [], existingKeys: new Set(), scope: { depth: 0 } };
352
+ return { path: [], concrete: "", existingKeys: new Set(), scope: { depth: 0 } };
292
353
  }
293
354
 
294
355
  // ---------------------------------------------------------------------------
@@ -326,7 +387,7 @@ export function resolveNodeAtPosition(
326
387
  const doc = docs[docIndex];
327
388
  const docKind = docKindOf(doc);
328
389
 
329
- const found = doc.root ? descend(doc.root, [], offset, { depth: 0 }) : undefined;
390
+ const found = doc.root ? descend(doc.root, [], "", offset, { depth: 0 }) : undefined;
330
391
 
331
392
  // Cursor sits on an existing map key → key/prop-key position.
332
393
  if (found?.type === "key") {
@@ -341,6 +402,7 @@ export function resolveNodeAtPosition(
341
402
  docKind,
342
403
  slot: "key",
343
404
  path: found.path,
405
+ concretePath: found.concretePath,
344
406
  node: found.keyNode,
345
407
  replaceRange: { start: toPos(found.keyNode.range[0]), end: toPos(found.keyNode.range[1]) },
346
408
  container: found.container,
@@ -365,13 +427,14 @@ export function resolveNodeAtPosition(
365
427
  toPos(value.range[0]).line !== toPos(found.keyEnd).line;
366
428
  if (isPartialKey && doc.root) {
367
429
  const col = toPos(value.range[0]).character;
368
- const { path, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
430
+ const { path, concrete, existingKeys, scope } = columnSearch(doc.root, col, offset, lineOffsets);
369
431
  return {
370
432
  docIndex,
371
433
  offset,
372
434
  docKind,
373
435
  slot: "key",
374
436
  path,
437
+ concretePath: concrete,
375
438
  container: found.container,
376
439
  existingKeys,
377
440
  resourceKind: scope.kind,
@@ -386,6 +449,7 @@ export function resolveNodeAtPosition(
386
449
  docKind,
387
450
  slot: "value",
388
451
  path: found.keyName != null ? [...found.path, found.keyName] : found.path,
452
+ concretePath: found.concretePath,
389
453
  node: value,
390
454
  container: found.container,
391
455
  prefix: text.slice(value.range[0], clampedEnd),
@@ -400,13 +464,14 @@ export function resolveNodeAtPosition(
400
464
  // resolved by cursor column.
401
465
  const resolution: KeyResolution = doc.root
402
466
  ? columnSearch(doc.root, character, offset, lineOffsets)
403
- : { path: [], existingKeys: new Set<string>(), scope: { depth: 0 } };
467
+ : { path: [], concrete: "", existingKeys: new Set<string>(), scope: { depth: 0 } };
404
468
  return {
405
469
  docIndex,
406
470
  offset,
407
471
  docKind,
408
472
  slot: "key",
409
473
  path: resolution.path,
474
+ concretePath: resolution.concrete,
410
475
  existingKeys: resolution.existingKeys,
411
476
  resourceKind: resolution.scope.kind,
412
477
  resourceDepth: resolution.scope.depth,
@@ -1,7 +1,15 @@
1
- import { parseToAst, type AstDocument, type AstScalar, type LoadedGraph } from "@telorun/analyzer";
1
+ import {
2
+ parseToAst,
3
+ type AstDocument,
4
+ type AstScalar,
5
+ type ManifestAnalysis,
6
+ type LoadedGraph,
7
+ } from "@telorun/analyzer";
2
8
  import type { DefinitionResult } from "../types.js";
3
9
  import { resolveNodeAtPosition } from "../completions/resolve-node.js";
4
10
  import { splitAliasQualified } from "./alias-qualified-value.js";
11
+ import { locateContextBinding } from "./locate-context-binding.js";
12
+ import { locateStepDeclaration } from "./locate-step.js";
5
13
  import { moduleForFile } from "./manifest-navigation.js";
6
14
  import { resolveCelTarget } from "./resolve-cel-target.js";
7
15
  import { isKindSlot, resolveKindTarget } from "./resolve-kind-target.js";
@@ -30,6 +38,10 @@ export function buildDefinition(
30
38
  graph: LoadedGraph,
31
39
  currentFilePath: string,
32
40
  docs?: AstDocument[],
41
+ /** Lets a `steps.<name>` read navigate to the step that produced it, and a
42
+ * context binding to its declaration — both need the declaring kind's own
43
+ * annotations, which only the analysis can read. */
44
+ analysis?: ManifestAnalysis,
33
45
  ): DefinitionResult | undefined {
34
46
  const astDocs = docs ?? parseToAst(text);
35
47
  const resolved = resolveNodeAtPosition(text, astDocs, line, character);
@@ -38,7 +50,23 @@ export function buildDefinition(
38
50
  const currentModule = moduleForFile(graph, currentFilePath) ?? graph.entry;
39
51
 
40
52
  if (resolved.cel) {
41
- return resolveCelTarget(graph, currentModule, resolved.cel.segment, resolved.cel.offset);
53
+ return resolveCelTarget(graph, currentModule, resolved.cel.segment, resolved.cel.offset, {
54
+ // A step is resolved in the CURRENT document — `steps.<name>.result` is
55
+ // readable only inside the resource that declares it, which is one
56
+ // document. Supplied as a closure so the chain resolver stays free of the
57
+ // AST and the scope query alike.
58
+ locateStep: (stepName) =>
59
+ locateStepDeclaration(graph, currentFilePath, astDocs, resolved.docIndex, stepName, analysis?.celScope),
60
+ locateContextBinding: (parts) =>
61
+ locateContextBinding(
62
+ graph,
63
+ astDocs,
64
+ resolved.docIndex,
65
+ resolved.concretePath ?? "",
66
+ parts,
67
+ analysis?.celScope,
68
+ ),
69
+ });
42
70
  }
43
71
 
44
72
  const node = resolved.node;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Where a CEL context binding was declared.
3
+ *
4
+ * `request.query` is written in the route's own `request.schema`, `self.<field>`
5
+ * in the definition's `schema`, `result.<field>` in the INVOKED resource's
6
+ * `outputType` — each derived by an `x-telo-context-*` annotation rather than
7
+ * reached through a reference slot, so none of them is navigable by the graph
8
+ * walk the other CEL roots use. The scope query resolves the annotation to a
9
+ * manifest identity plus a path; locating that path in the loaded files is what
10
+ * this adds.
11
+ */
12
+ import type { AstDocument, CelScopeQuery, LoadedFile, LoadedGraph } from "@telorun/analyzer";
13
+ import type { DefinitionResult } from "../types.js";
14
+ import { docIdentity } from "../doc-identity.js";
15
+
16
+ /** Every file of every module in the graph — a binding's declaration may sit in
17
+ * another module (an invoked handler's `outputType`), so the search is not
18
+ * confined to the current file. */
19
+ function allFiles(graph: LoadedGraph): LoadedFile[] {
20
+ const out: LoadedFile[] = [];
21
+ for (const mod of graph.modules.values()) out.push(mod.owner, ...mod.partials);
22
+ return out;
23
+ }
24
+
25
+ export function locateContextBinding(
26
+ graph: LoadedGraph,
27
+ docs: AstDocument[],
28
+ docIndex: number,
29
+ sitePath: string,
30
+ parts: string[],
31
+ scopeQuery: CelScopeQuery | undefined,
32
+ ): DefinitionResult | undefined {
33
+ if (!scopeQuery) return undefined;
34
+ const identity = docIdentity(docs[docIndex]);
35
+ const resource = scopeQuery.resourceFor(identity.kind, identity.name);
36
+ if (!resource) return undefined;
37
+
38
+ const site = scopeQuery.contextDeclarationSite(resource, sitePath, parts);
39
+ if (!site) return undefined;
40
+
41
+ for (const file of allFiles(graph)) {
42
+ for (let i = 0; i < file.manifests.length; i++) {
43
+ const manifest = file.manifests[i] as { kind?: string; metadata?: { name?: string } } | null;
44
+ if (manifest?.kind !== site.kind || manifest.metadata?.name !== site.name) continue;
45
+ const index = file.positions[i]?.positionIndex;
46
+ // The KEY span, so the jump underlines `query:` rather than the block that
47
+ // follows it — a declaration, not a selection.
48
+ const range = index?.get(`@key:${site.path}`) ?? index?.get(site.path);
49
+ if (range) return { uri: file.source, range };
50
+ }
51
+ }
52
+ return undefined;
53
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Where a `Run` step is declared.
3
+ *
4
+ * The one CEL scope whose members really are written in the manifest but are
5
+ * reached through no reference slot — `steps.encode.result` names something a
6
+ * `!ref` resolver has never heard of. Finding it needs the declaring kind's own
7
+ * step-body annotation, which is why it goes through the scope query rather
8
+ * than through the graph navigation the other CEL roots use.
9
+ */
10
+ import type { AstDocument, CelScopeQuery, LoadedFile, LoadedGraph } from "@telorun/analyzer";
11
+ import type { DefinitionResult } from "../types.js";
12
+ import { docIdentity } from "../doc-identity.js";
13
+
14
+ /** The loaded file for `filePath`, whose position index maps a manifest path to
15
+ * a source range. */
16
+ function loadedFile(graph: LoadedGraph, filePath: string): LoadedFile | undefined {
17
+ for (const mod of graph.modules.values()) {
18
+ if (mod.owner.source === filePath) return mod.owner;
19
+ const partial = mod.partials.find((p) => p.source === filePath);
20
+ if (partial) return partial;
21
+ }
22
+ return undefined;
23
+ }
24
+
25
+ export function locateStepDeclaration(
26
+ graph: LoadedGraph,
27
+ filePath: string,
28
+ docs: AstDocument[],
29
+ docIndex: number,
30
+ stepName: string,
31
+ scopeQuery: CelScopeQuery | undefined,
32
+ ): DefinitionResult | undefined {
33
+ if (!scopeQuery) return undefined;
34
+ const identity = docIdentity(docs[docIndex]);
35
+ const resource = scopeQuery.resourceFor(identity.kind, identity.name);
36
+ if (!resource) return undefined;
37
+
38
+ const stepPath = scopeQuery.stepDeclarationPath(resource, stepName);
39
+ if (!stepPath) return undefined;
40
+
41
+ const file = loadedFile(graph, filePath);
42
+ const index = file?.positions[docIndex]?.positionIndex;
43
+ if (!index) return undefined;
44
+ // The step's own `name:` value is what the jump underlines — the step object's
45
+ // range would highlight the whole block, which reads as a selection rather
46
+ // than as a declaration.
47
+ const range =
48
+ index.get(`${stepPath}.name`) ?? index.get(`@key:${stepPath}.name`) ?? index.get(stepPath);
49
+ return range ? { uri: file!.source, range } : undefined;
50
+ }
@@ -55,11 +55,23 @@ function resolveResourceChain(
55
55
  * analyzer reports the syntax error itself. Only that failure is tolerated: a
56
56
  * defect in the CEL wrapper propagates rather than reading as "nothing to
57
57
  * navigate to". */
58
+ export interface CelTargetResolvers {
59
+ /** Where a step of the CURRENT resource is declared. Supplied by the caller
60
+ * because finding one needs the declaring kind's step-body annotation, which
61
+ * the graph alone does not carry. */
62
+ locateStep?(stepName: string): DefinitionResult | undefined;
63
+ /** Where a context binding (`request.query`, `self.<field>`,
64
+ * `result.<field>`) was declared. Same reason: the site is derived by an
65
+ * `x-telo-context-*` annotation, which only the scope query can read. */
66
+ locateContextBinding?(parts: string[]): DefinitionResult | undefined;
67
+ }
68
+
58
69
  export function resolveCelTarget(
59
70
  graph: LoadedGraph,
60
71
  currentModule: LoadedModule,
61
72
  segment: CelSegment,
62
73
  offset: number,
74
+ resolvers?: CelTargetResolvers,
63
75
  ): DefinitionResult | undefined {
64
76
  let ast: CelNode;
65
77
  try {
@@ -80,5 +92,18 @@ export function resolveCelTarget(
80
92
  return undefined;
81
93
  }
82
94
  if (root === "resources") return resolveResourceChain(graph, currentModule, parts, index);
95
+ // `steps.<name>` navigates to the step; `steps` itself and `.result` do not —
96
+ // the first names no one step, the second is the contract's output, which is
97
+ // declared by the invoked target rather than at the read site.
98
+ if (root === "steps" && index === 1 && resolvers?.locateStep) {
99
+ return resolvers.locateStep(parts[1].name);
100
+ }
101
+ // Anything else in scope came from an `x-telo-context-*` annotation, which
102
+ // names a real manifest node for `request` / `self` / `result` and friends.
103
+ // The chain UP TO the cursor is what resolves — hovering `query` in
104
+ // `request.query.lastEventId` navigates to `query`, not to the leaf.
105
+ if (index >= 1 && resolvers?.locateContextBinding) {
106
+ return resolvers.locateContextBinding(parts.slice(0, index + 1).map((p) => p.name));
107
+ }
83
108
  return undefined;
84
109
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A document's `kind` + `metadata.name` — the pair that names the resource an
3
+ * analyzed manifest set holds it under.
4
+ *
5
+ * Its own module because it is a document-identity primitive, not a completion
6
+ * one: hover, semantic tokens and both declaration locators need it, and
7
+ * reaching it through the completion entry point dragged that whole module
8
+ * graph — CEL completion, call-input resolution, the import-source machinery —
9
+ * into surfaces that use none of it.
10
+ */
11
+ import type { AstDocument, AstMap } from "@telorun/analyzer";
12
+
13
+ const scalar = (node: { kind: string; value?: unknown } | undefined): string | undefined =>
14
+ node?.kind === "scalar" && typeof node.value === "string" ? node.value : undefined;
15
+
16
+ /** Either half may be absent while the author is still writing the document. */
17
+ export function docIdentity(doc: AstDocument | undefined): { kind?: string; name?: string } {
18
+ if (doc?.root?.kind !== "map") return {};
19
+ let kind: string | undefined;
20
+ let name: string | undefined;
21
+ for (const pair of doc.root.entries) {
22
+ const key = scalar(pair.key);
23
+ if (key === "kind") kind = scalar(pair.value);
24
+ else if (key === "metadata" && pair.value?.kind === "map") {
25
+ const meta = pair.value as AstMap;
26
+ const nameEntry = meta.entries.find((e) => scalar(e.key) === "name");
27
+ name = scalar(nameEntry?.value);
28
+ }
29
+ }
30
+ return { kind, name };
31
+ }
@@ -1,10 +1,17 @@
1
1
  import {
2
+ CelParseError,
2
3
  parseToAst,
3
4
  readRefSlot,
4
5
  type AnalysisRegistry,
5
6
  type AstDocument,
7
+ type CelNode,
8
+ type CelScopeQuery,
9
+ type ManifestAnalysis,
6
10
  } from "@telorun/analyzer";
7
11
  import type { HoverResult } from "../types.js";
12
+ import { chainAt } from "../cel-chain.js";
13
+ import { celSymbolAt } from "../cel/symbols.js";
14
+ import { docIdentity } from "../doc-identity.js";
8
15
  import { navigateSchema } from "../completions/detect-context.js";
9
16
  import {
10
17
  resolveNodeAtPosition,
@@ -90,7 +97,9 @@ function fieldSchemaFor(
90
97
  if (!resourceKind || !registry) return undefined;
91
98
  const def = registry.resolveDefinition(resourceKind);
92
99
  if (!def?.schema) return undefined;
93
- return navigateSchema(def.schema as Record<string, any>, relativePath);
100
+ return navigateSchema(def.schema as Record<string, any>, relativePath, (from) =>
101
+ registry.resolveSchemaFrom(from, resourceKind),
102
+ );
94
103
  }
95
104
 
96
105
  export function buildHover(
@@ -99,15 +108,72 @@ export function buildHover(
99
108
  character: number,
100
109
  registry: AnalysisRegistry | undefined,
101
110
  docs?: AstDocument[],
111
+ /** The host's analysis. Without it a CEL identifier hovers as nothing — its
112
+ * type is a property of the resolved scope, and there is no second source
113
+ * for it. */
114
+ analysis?: ManifestAnalysis,
102
115
  ): HoverResult | undefined {
103
116
  const astDocs = docs ?? parseToAst(text);
104
117
  const resolved = resolveNodeAtPosition(text, astDocs, line, character);
105
118
  if (!resolved) return undefined;
106
119
 
120
+ if (resolved.cel) {
121
+ const hover = hoverForCel(resolved, astDocs, analysis?.celScope);
122
+ // A CEL body is still a field value; when the cursor is on nothing
123
+ // nameable inside it (an operator, a literal), fall through to the field's
124
+ // own hover rather than reporting nothing.
125
+ if (hover) return hover;
126
+ }
107
127
  if (resolved.slot === "value") return hoverForValue(resolved, registry);
108
128
  return hoverForKey(resolved, registry);
109
129
  }
110
130
 
131
+ /**
132
+ * Hover for one identifier of a CEL chain.
133
+ *
134
+ * The TYPE comes from the resolved scope; a DESCRIPTION comes from whatever
135
+ * schema node declared the name. Where to jump is the other half's answer
136
+ * (`resolveCelTarget`) and is deliberately not consulted here — hover must
137
+ * still say what `steps.encode.result` IS even though nothing in the manifest
138
+ * declares it.
139
+ */
140
+ function hoverForCel(
141
+ resolved: ResolvedCursor,
142
+ docs: AstDocument[],
143
+ scopeQuery: CelScopeQuery | undefined,
144
+ ): HoverResult | undefined {
145
+ if (!resolved.cel || !scopeQuery) return undefined;
146
+ const identity = docIdentity(docs[resolved.docIndex]);
147
+ const resource = scopeQuery.resourceFor(identity.kind, identity.name);
148
+ if (!resource) return undefined;
149
+
150
+ let ast: CelNode;
151
+ try {
152
+ ast = resolved.cel.segment.ast();
153
+ } catch (error) {
154
+ // An expression the author is still writing does not parse. That means
155
+ // there is no chain to hit-test, not an error to report from a hover — the
156
+ // analyzer reports the syntax error itself. Only that failure is tolerated.
157
+ if (!(error instanceof CelParseError)) throw error;
158
+ return undefined;
159
+ }
160
+
161
+ const hit = chainAt(ast, resolved.cel.offset);
162
+ if (!hit) return undefined;
163
+ // The chain UP TO the cursor, not the whole chain: hovering `resources` in
164
+ // `resources.db.url` describes `resources`.
165
+ const parts = hit.parts.slice(0, hit.index + 1).map((p) => p.name);
166
+ const scope = scopeQuery.scopeAt(resource, resolved.concretePath ?? "");
167
+ const symbol = celSymbolAt(scope, parts);
168
+ if (!symbol) return undefined;
169
+
170
+ const lines = [symbol.type ? `**${symbol.name}**: \`${symbol.type}\`` : `**${symbol.name}**`];
171
+ if (symbol.description) lines.push("", symbol.description);
172
+ const chainText = parts.join(".");
173
+ if (chainText !== symbol.name) lines.push("", `\`${chainText}\``);
174
+ return { contents: lines.join("\n") };
175
+ }
176
+
111
177
  function hoverForValue(
112
178
  resolved: ResolvedCursor,
113
179
  registry: AnalysisRegistry | undefined,