@telorun/ide-support 0.10.0 → 0.11.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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAe,MAAM,mBAAmB,CAAC;AACrG,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAuI3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/completions/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAe,MAAM,mBAAmB,CAAC;AACrG,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AA8I3E,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,qBAAqB,EAC/B,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
@@ -1,5 +1,5 @@
1
1
  import { parseToAst } from "@telorun/analyzer";
2
- import { detectContext, lookupRefConstraint } from "./detect-context.js";
2
+ import { detectContext, lookupRefConstraints } from "./detect-context.js";
3
3
  import { importSourceCompletions } from "./import-source.js";
4
4
  import { propKeyCompletions } from "./prop-keys.js";
5
5
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
@@ -36,16 +36,20 @@ function extractInFileResources(docs) {
36
36
  * kind match. Falls back to listing every in-file resource so the
37
37
  * user still sees something rather than nothing when the registry
38
38
  * doesn't recognize the kind yet. */
39
- function refNameCompletions(docs, refKind, refConstraint, registry, replaceRange) {
39
+ function refNameCompletions(docs, refKind, refConstraints, registry, replaceRange) {
40
40
  const resources = extractInFileResources(docs);
41
41
  let acceptable;
42
42
  if (refKind) {
43
43
  acceptable = new Set([refKind]);
44
44
  }
45
- else if (refConstraint && registry) {
46
- const kinds = registry.userFacingKindsForRef(refConstraint);
47
- if (kinds)
48
- acceptable = new Set(kinds);
45
+ else if (refConstraints.length > 0 && registry) {
46
+ // Union across the slot's accepted kinds: a resource satisfying any one of
47
+ // them fills the slot. A constraint the registry can't resolve contributes
48
+ // nothing rather than narrowing to the ones it could.
49
+ const resolved = refConstraints.map((c) => registry.userFacingKindsForRef(c));
50
+ if (resolved.some(Boolean)) {
51
+ acceptable = new Set(resolved.flatMap((kinds) => kinds ?? []));
52
+ }
49
53
  }
50
54
  const seen = new Set();
51
55
  const out = [];
@@ -74,10 +78,13 @@ function refConstrainedKinds(registry, parentDocKind, parentYamlPath) {
74
78
  const definition = registry.resolveDefinition(parentDocKind);
75
79
  if (!definition?.schema)
76
80
  return undefined;
77
- const refString = lookupRefConstraint(definition.schema, parentYamlPath);
78
- if (!refString)
81
+ const constraints = lookupRefConstraints(definition.schema, parentYamlPath);
82
+ if (constraints.length === 0)
83
+ return undefined;
84
+ const resolved = constraints.map((c) => registry.userFacingKindsForRef(c));
85
+ if (!resolved.some(Boolean))
79
86
  return undefined;
80
- return registry.userFacingKindsForRef(refString);
87
+ return [...new Set(resolved.flatMap((kinds) => kinds ?? []))];
81
88
  }
82
89
  function kindCompletions(registry, docKind, yamlPath, replaceRange) {
83
90
  let kinds;
@@ -126,10 +133,10 @@ export async function buildCompletions(text, line, character, registry, adapter,
126
133
  return capabilityCompletions();
127
134
  if (ctx.type === "ref-name") {
128
135
  const definition = registry?.resolveDefinition(ctx.docKind);
129
- const refConstraint = definition?.schema
130
- ? lookupRefConstraint(definition.schema, ctx.yamlPath)
131
- : undefined;
132
- return refNameCompletions(astDocs, ctx.refKind, refConstraint, registry, ctx.replaceRange);
136
+ const refConstraints = definition?.schema
137
+ ? lookupRefConstraints(definition.schema, ctx.yamlPath)
138
+ : [];
139
+ return refNameCompletions(astDocs, ctx.refKind, refConstraints, registry, ctx.replaceRange);
133
140
  }
134
141
  if (ctx.type === "field-value") {
135
142
  if (ctx.field === "import-source") {
@@ -56,12 +56,15 @@ export declare function navigateSchema(schema: Record<string, any>, path: string
56
56
  * completion to discover what kind of resource the user is targeting in an
57
57
  * object-form ref. Walking stops at the first line with a strictly smaller
58
58
  * indent (that's the parent's structural boundary). */
59
- /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
60
- * inside `definitionSchema`. Checks both the property node directly and its
61
- * peeled `anyOf` / `oneOf` branches, since some library schemas place the
62
- * annotation at the property level and others inside a branch. Returns
63
- * `undefined` when the path doesn't resolve or no ref constraint is declared. */
64
- export declare function lookupRefConstraint(definitionSchema: Record<string, any>, yamlPath: string[]): string | undefined;
59
+ /** Every kind the `x-telo-ref` slot at `yamlPath` accepts, or an empty array
60
+ * when the path doesn't resolve or declares no constraint.
61
+ *
62
+ * All of them, not the first: a slot accepting `Invocable | Runnable` used to
63
+ * complete only the invocables, because a single-constraint lookup stopped at
64
+ * the first branch. The analyzer's accessor unions a `kind:` list and the
65
+ * `anyOf` / `oneOf` branches alike, so completion now offers what the slot
66
+ * actually takes. */
67
+ export declare function lookupRefConstraints(definitionSchema: Record<string, any>, yamlPath: string[]): string[];
65
68
  /** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
66
69
  * structural resolution lives in `resolveNodeAtPosition`; this only maps a
67
70
  * resolved slot onto the completion the editor should offer. `docs` lets a
@@ -1 +1 @@
1
- {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,MAAM,CAAC;IACb;;;uEAGmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;+BAE2B;IAC3B,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GACpF;IACE;;;gDAG4C;IAC5C,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;6DACyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAuBN;;;;;sCAKsC;AACtC,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA4BjC;AAoCD;;;;;wDAKwD;AACxD;;;;kFAIkF;AAClF,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,SAAS,CAQpB;AAED;;;;oBAIoB;AACpB,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,aAAa,GAAG,SAAS,CAsE3B"}
1
+ {"version":3,"file":"detect-context.d.ts","sourceRoot":"","sources":["../../src/completions/detect-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,MAAM,CAAC;IACb;;;uEAGmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;+BAE2B;IAC3B,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,GACpF;IACE;;;gDAG4C;IAC5C,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;6DACyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAuBN;;;;;sCAKsC;AACtC,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,EAAE,GACb,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA4BjC;AAwCD;;;;;wDAKwD;AACxD;;;;;;;sBAOsB;AACtB,wBAAgB,oBAAoB,CAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrC,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,EAAE,CAIV;AAED;;;;oBAIoB;AACpB,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,aAAa,GAAG,SAAS,CAsE3B"}
@@ -1,4 +1,4 @@
1
- import { parseToAst } from "@telorun/analyzer";
1
+ import { parseToAst, readRefSlot, refSlotAnnotation } from "@telorun/analyzer";
2
2
  import { resolveNodeAtPosition } from "./resolve-node.js";
3
3
  /** Returns every schema branch reachable from `node` after peeling `anyOf` /
4
4
  * `oneOf` recursively. A branch with no combinators is its own only entry.
@@ -66,9 +66,12 @@ export function navigateSchema(schema, path) {
66
66
  /** Merge multiple peeled schema branches into one node for completion purposes.
67
67
  * Property maps are unioned (first branch wins on key collision). `required`
68
68
  * becomes the intersection so optional-in-any-branch keys still surface.
69
- * `x-telo-ref` from the unpeeled parent is preserved so ref-aware lookups
70
- * (`lookupRefConstraint`) still see the constraint when navigateSchema is
71
- * called on a property that places the annotation alongside `anyOf`/`oneOf`. */
69
+ *
70
+ * The parent's reference slot is re-stamped on the merged node, because peeling
71
+ * is exactly what destroys it: for the canonical multi-kind shape
72
+ * (`anyOf: [{x-telo-ref: A}, {x-telo-ref: B}]`) the constraint lives ONLY in the
73
+ * branches, and merging them left a node declaring no slot at all. Re-emitted
74
+ * through the analyzer's accessor so both annotation shapes survive the merge. */
72
75
  function unionLeaves(parent, leaves) {
73
76
  const properties = {};
74
77
  const requiredSets = [];
@@ -87,8 +90,9 @@ function unionLeaves(parent, leaves) {
87
90
  required = [...requiredSets[0]].filter((k) => requiredSets.every((s) => s.has(k)));
88
91
  }
89
92
  const out = { type: "object", properties, required };
90
- if (typeof parent["x-telo-ref"] === "string")
91
- out["x-telo-ref"] = parent["x-telo-ref"];
93
+ const slot = readRefSlot(parent);
94
+ if (slot && slot.kinds.length > 0)
95
+ out["x-telo-ref"] = refSlotAnnotation(slot);
92
96
  return out;
93
97
  }
94
98
  /** Walks up and down from `cursorLine` looking for a sibling line at the
@@ -97,22 +101,19 @@ function unionLeaves(parent, leaves) {
97
101
  * completion to discover what kind of resource the user is targeting in an
98
102
  * object-form ref. Walking stops at the first line with a strictly smaller
99
103
  * indent (that's the parent's structural boundary). */
100
- /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
101
- * inside `definitionSchema`. Checks both the property node directly and its
102
- * peeled `anyOf` / `oneOf` branches, since some library schemas place the
103
- * annotation at the property level and others inside a branch. Returns
104
- * `undefined` when the path doesn't resolve or no ref constraint is declared. */
105
- export function lookupRefConstraint(definitionSchema, yamlPath) {
104
+ /** Every kind the `x-telo-ref` slot at `yamlPath` accepts, or an empty array
105
+ * when the path doesn't resolve or declares no constraint.
106
+ *
107
+ * All of them, not the first: a slot accepting `Invocable | Runnable` used to
108
+ * complete only the invocables, because a single-constraint lookup stopped at
109
+ * the first branch. The analyzer's accessor unions a `kind:` list and the
110
+ * `anyOf` / `oneOf` branches alike, so completion now offers what the slot
111
+ * actually takes. */
112
+ export function lookupRefConstraints(definitionSchema, yamlPath) {
106
113
  const node = navigateSchema(definitionSchema, yamlPath);
107
114
  if (!node)
108
- return undefined;
109
- if (typeof node["x-telo-ref"] === "string")
110
- return node["x-telo-ref"];
111
- for (const branch of peelCombinators(node)) {
112
- if (typeof branch["x-telo-ref"] === "string")
113
- return branch["x-telo-ref"];
114
- }
115
- return undefined;
115
+ return [];
116
+ return readRefSlot(node)?.kinds ?? [];
116
117
  }
117
118
  /** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
118
119
  * structural resolution lives in `resolveNodeAtPosition`; this only maps a
@@ -1 +1 @@
1
- {"version":3,"file":"build-hover.d.ts","sourceRoot":"","sources":["../../src/hover/build-hover.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAoF/C,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,WAAW,GAAG,SAAS,CAOzB"}
1
+ {"version":3,"file":"build-hover.d.ts","sourceRoot":"","sources":["../../src/hover/build-hover.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAyF/C,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,WAAW,GAAG,SAAS,CAOzB"}
@@ -1,4 +1,4 @@
1
- import { parseToAst, } from "@telorun/analyzer";
1
+ import { parseToAst, readRefSlot, } from "@telorun/analyzer";
2
2
  import { navigateSchema } from "../completions/detect-context.js";
3
3
  import { resolveNodeAtPosition, scalarString, } from "../completions/resolve-node.js";
4
4
  import { CAPABILITY_DOCS } from "../completions/valid-capabilities.js";
@@ -57,9 +57,13 @@ function fieldHover(keyName, field) {
57
57
  if (typeof field.description === "string" && field.description) {
58
58
  lines.push("", field.description);
59
59
  }
60
- const ref = field["x-telo-ref"];
61
- if (typeof ref === "string")
62
- lines.push("", `Reference \`${ref}\``);
60
+ // Through the shared accessor, so a slot whose constraint sits in an `anyOf`
61
+ // branch or in a `kind:` list — hovers like any other. Reading the raw
62
+ // annotation here meant those slots showed no reference line at all.
63
+ const refKinds = readRefSlot(field)?.kinds ?? [];
64
+ if (refKinds.length > 0) {
65
+ lines.push("", `Reference → ${refKinds.map((k) => `\`${k}\``).join(" | ")}`);
66
+ }
63
67
  if (Array.isArray(field.enum) && field.enum.length > 0) {
64
68
  lines.push("", `Allowed: ${field.enum.map((v) => `\`${v}\``).join(", ")}`);
65
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/ide-support",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Editor-host-agnostic IDE support (completions, diagnostic normalization) for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -36,7 +36,7 @@
36
36
  "src/**"
37
37
  ],
38
38
  "dependencies": {
39
- "@telorun/analyzer": "0.52.0"
39
+ "@telorun/analyzer": "0.54.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^20.0.0",
@@ -1,7 +1,7 @@
1
1
  import { parseToAst, type AnalysisRegistry, type AstDocument, type AstMap } from "@telorun/analyzer";
2
2
  import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
3
3
  import type { ReplaceRange } from "./detect-context.js";
4
- import { detectContext, lookupRefConstraint } from "./detect-context.js";
4
+ import { detectContext, lookupRefConstraints } from "./detect-context.js";
5
5
  import { importSourceCompletions } from "./import-source.js";
6
6
  import { propKeyCompletions } from "./prop-keys.js";
7
7
  import { CAPABILITY_VALUES } from "./valid-capabilities.js";
@@ -47,7 +47,7 @@ function extractInFileResources(docs: AstDocument[]): ResourceRecord[] {
47
47
  function refNameCompletions(
48
48
  docs: AstDocument[],
49
49
  refKind: string | undefined,
50
- refConstraint: string | undefined,
50
+ refConstraints: string[],
51
51
  registry: AnalysisRegistry | undefined,
52
52
  replaceRange: ReplaceRange,
53
53
  ): CompletionResult[] {
@@ -56,9 +56,14 @@ function refNameCompletions(
56
56
 
57
57
  if (refKind) {
58
58
  acceptable = new Set([refKind]);
59
- } else if (refConstraint && registry) {
60
- const kinds = registry.userFacingKindsForRef(refConstraint);
61
- if (kinds) acceptable = new Set(kinds);
59
+ } else if (refConstraints.length > 0 && registry) {
60
+ // Union across the slot's accepted kinds: a resource satisfying any one of
61
+ // them fills the slot. A constraint the registry can't resolve contributes
62
+ // nothing rather than narrowing to the ones it could.
63
+ const resolved = refConstraints.map((c) => registry.userFacingKindsForRef(c));
64
+ if (resolved.some(Boolean)) {
65
+ acceptable = new Set(resolved.flatMap((kinds) => kinds ?? []));
66
+ }
62
67
  }
63
68
 
64
69
  const seen = new Set<string>();
@@ -90,12 +95,14 @@ function refConstrainedKinds(
90
95
  ): string[] | undefined {
91
96
  const definition = registry.resolveDefinition(parentDocKind);
92
97
  if (!definition?.schema) return undefined;
93
- const refString = lookupRefConstraint(
98
+ const constraints = lookupRefConstraints(
94
99
  definition.schema as Record<string, any>,
95
100
  parentYamlPath,
96
101
  );
97
- if (!refString) return undefined;
98
- return registry.userFacingKindsForRef(refString);
102
+ if (constraints.length === 0) return undefined;
103
+ const resolved = constraints.map((c) => registry.userFacingKindsForRef(c));
104
+ if (!resolved.some(Boolean)) return undefined;
105
+ return [...new Set(resolved.flatMap((kinds) => kinds ?? []))];
99
106
  }
100
107
 
101
108
  function kindCompletions(
@@ -154,10 +161,10 @@ export async function buildCompletions(
154
161
  if (ctx.type === "capability") return capabilityCompletions();
155
162
  if (ctx.type === "ref-name") {
156
163
  const definition = registry?.resolveDefinition(ctx.docKind);
157
- const refConstraint = definition?.schema
158
- ? lookupRefConstraint(definition.schema as Record<string, any>, ctx.yamlPath)
159
- : undefined;
160
- return refNameCompletions(astDocs, ctx.refKind, refConstraint, registry, ctx.replaceRange);
164
+ const refConstraints = definition?.schema
165
+ ? lookupRefConstraints(definition.schema as Record<string, any>, ctx.yamlPath)
166
+ : [];
167
+ return refNameCompletions(astDocs, ctx.refKind, refConstraints, registry, ctx.replaceRange);
161
168
  }
162
169
  if (ctx.type === "field-value") {
163
170
  if (ctx.field === "import-source") {
@@ -1,4 +1,4 @@
1
- import { parseToAst, type AstDocument } from "@telorun/analyzer";
1
+ import { parseToAst, readRefSlot, refSlotAnnotation, type AstDocument } from "@telorun/analyzer";
2
2
  import type { ReplaceRange } from "../types.js";
3
3
  import { resolveNodeAtPosition } from "./resolve-node.js";
4
4
 
@@ -108,9 +108,12 @@ export function navigateSchema(
108
108
  /** Merge multiple peeled schema branches into one node for completion purposes.
109
109
  * Property maps are unioned (first branch wins on key collision). `required`
110
110
  * becomes the intersection so optional-in-any-branch keys still surface.
111
- * `x-telo-ref` from the unpeeled parent is preserved so ref-aware lookups
112
- * (`lookupRefConstraint`) still see the constraint when navigateSchema is
113
- * called on a property that places the annotation alongside `anyOf`/`oneOf`. */
111
+ *
112
+ * The parent's reference slot is re-stamped on the merged node, because peeling
113
+ * is exactly what destroys it: for the canonical multi-kind shape
114
+ * (`anyOf: [{x-telo-ref: A}, {x-telo-ref: B}]`) the constraint lives ONLY in the
115
+ * branches, and merging them left a node declaring no slot at all. Re-emitted
116
+ * through the analyzer's accessor so both annotation shapes survive the merge. */
114
117
  function unionLeaves(
115
118
  parent: Record<string, any>,
116
119
  leaves: Record<string, any>[],
@@ -135,7 +138,8 @@ function unionLeaves(
135
138
  );
136
139
  }
137
140
  const out: Record<string, any> = { type: "object", properties, required };
138
- if (typeof parent["x-telo-ref"] === "string") out["x-telo-ref"] = parent["x-telo-ref"];
141
+ const slot = readRefSlot(parent);
142
+ if (slot && slot.kinds.length > 0) out["x-telo-ref"] = refSlotAnnotation(slot);
139
143
  return out;
140
144
  }
141
145
 
@@ -145,22 +149,21 @@ function unionLeaves(
145
149
  * completion to discover what kind of resource the user is targeting in an
146
150
  * object-form ref. Walking stops at the first line with a strictly smaller
147
151
  * indent (that's the parent's structural boundary). */
148
- /** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
149
- * inside `definitionSchema`. Checks both the property node directly and its
150
- * peeled `anyOf` / `oneOf` branches, since some library schemas place the
151
- * annotation at the property level and others inside a branch. Returns
152
- * `undefined` when the path doesn't resolve or no ref constraint is declared. */
153
- export function lookupRefConstraint(
152
+ /** Every kind the `x-telo-ref` slot at `yamlPath` accepts, or an empty array
153
+ * when the path doesn't resolve or declares no constraint.
154
+ *
155
+ * All of them, not the first: a slot accepting `Invocable | Runnable` used to
156
+ * complete only the invocables, because a single-constraint lookup stopped at
157
+ * the first branch. The analyzer's accessor unions a `kind:` list and the
158
+ * `anyOf` / `oneOf` branches alike, so completion now offers what the slot
159
+ * actually takes. */
160
+ export function lookupRefConstraints(
154
161
  definitionSchema: Record<string, any>,
155
162
  yamlPath: string[],
156
- ): string | undefined {
163
+ ): string[] {
157
164
  const node = navigateSchema(definitionSchema, yamlPath);
158
- if (!node) return undefined;
159
- if (typeof node["x-telo-ref"] === "string") return node["x-telo-ref"];
160
- for (const branch of peelCombinators(node)) {
161
- if (typeof branch["x-telo-ref"] === "string") return branch["x-telo-ref"];
162
- }
163
- return undefined;
165
+ if (!node) return [];
166
+ return readRefSlot(node)?.kinds ?? [];
164
167
  }
165
168
 
166
169
  /** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  parseToAst,
3
+ readRefSlot,
3
4
  type AnalysisRegistry,
4
5
  type AstDocument,
5
6
  } from "@telorun/analyzer";
@@ -65,8 +66,13 @@ function fieldHover(keyName: string, field: Record<string, any>): string {
65
66
  if (typeof field.description === "string" && field.description) {
66
67
  lines.push("", field.description);
67
68
  }
68
- const ref = field["x-telo-ref"];
69
- if (typeof ref === "string") lines.push("", `Reference \`${ref}\``);
69
+ // Through the shared accessor, so a slot whose constraint sits in an `anyOf`
70
+ // branch or in a `kind:` list — hovers like any other. Reading the raw
71
+ // annotation here meant those slots showed no reference line at all.
72
+ const refKinds = readRefSlot(field)?.kinds ?? [];
73
+ if (refKinds.length > 0) {
74
+ lines.push("", `Reference → ${refKinds.map((k) => `\`${k}\``).join(" | ")}`);
75
+ }
70
76
  if (Array.isArray(field.enum) && field.enum.length > 0) {
71
77
  lines.push("", `Allowed: ${field.enum.map((v: unknown) => `\`${v}\``).join(", ")}`);
72
78
  }