@xano/xanoscript-language-server 12.17.0 → 12.19.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,6 +1,7 @@
1
1
  import { lexDocument } from "../lexer/lexer.js";
2
- import { multidocParser } from "../parser/multidoc.js";
2
+ import { multidocParser, splitMultidoc } from "../parser/multidoc.js";
3
3
  import { xanoscriptParser } from "../parser/parser.js";
4
+ import { policyCatalogueVersion } from "../parser/policy/catalogue.js";
4
5
  import { getSchemeFromContent } from "../utils.js";
5
6
 
6
7
  /**
@@ -28,12 +29,15 @@ class DocumentCache {
28
29
  getOrParse(uri, version, text) {
29
30
  const cached = this.cache.get(uri);
30
31
  const textLength = text.length;
32
+ // A policy's diagnostics depend on the check catalogue as well as the text.
33
+ const catalogueVersion = policyCatalogueVersion();
31
34
 
32
35
  // Check version and text length to detect same URI with different content (e.g., in tests)
33
36
  if (
34
37
  cached &&
35
38
  cached.version === version &&
36
- cached.textLength === textLength
39
+ cached.textLength === textLength &&
40
+ cached.catalogueVersion === catalogueVersion
37
41
  ) {
38
42
  return {
39
43
  lexResult: cached.scheme === "multidoc" ? { tokens: [] } : lexDocument(text),
@@ -42,12 +46,14 @@ class DocumentCache {
42
46
  };
43
47
  }
44
48
 
45
- // Multidoc detection — split on \n---\n separator
46
- if (text.includes("\n---\n")) {
47
- const parserState = multidocParser(text);
49
+ // Multidoc detection — split with splitMultidoc (a `---` line outside a triple-quoted string)
50
+ const segments = splitMultidoc(text);
51
+ if (segments.length > 1) {
52
+ const parserState = multidocParser(text, segments);
48
53
  const cacheEntry = {
49
54
  version,
50
55
  textLength,
56
+ catalogueVersion,
51
57
  parserState,
52
58
  scheme: "multidoc",
53
59
  };
@@ -124,6 +130,7 @@ class DocumentCache {
124
130
  const cacheEntry = {
125
131
  version,
126
132
  textLength,
133
+ catalogueVersion,
127
134
  parserState,
128
135
  scheme,
129
136
  };
@@ -12,6 +12,12 @@ import { onHover } from "./onHover/onHover.js";
12
12
  import { onSemanticCheck } from "./onSemanticCheck/onSemanticCheck.js";
13
13
  import { TOKEN_TYPES } from "./onSemanticCheck/tokens.js";
14
14
  import pkg from "./package.json" with { type: "json" };
15
+ import { setPolicyCatalogue } from "./parser/policy/catalogue.js";
16
+ import policyCatalogueSnapshot from "./parser/policy/catalogueSnapshot.js";
17
+
18
+ // Policy rules are checked against the check catalogue this package was built with (see
19
+ // scripts/gen-policy-catalogue.mjs); what it finds is a warning, since the instance may be newer.
20
+ setPolicyCatalogue(policyCatalogueSnapshot, { source: "snapshot" });
15
21
 
16
22
  const messageReader = new BrowserMessageReader(self);
17
23
  const messageWriter = new BrowserMessageWriter(self);
package/lexer/mcp.js CHANGED
@@ -3,7 +3,8 @@ import { createTokenByName } from "./utils.js";
3
3
 
4
4
  // MCP 2026-07-28 authoring keywords: the `prompt` and `resource` object kinds,
5
5
  // the tool/prompt/resource metadata keys, and the `mcp.elicit` / `mcp.progress`
6
- // statements. Grounded against cloud-client's schema DSL
6
+ // statements, plus the per-server `oauth` block and the `mcp.oauth.complete` /
7
+ // `mcp.oauth.request` / `mcp.oauth.revoke` statements. Grounded against cloud-client's schema DSL
7
8
  // (`script/kind/schema/core/{prompt,resource,tool,mcp_server}.yaml` and
8
9
  // `script/kind/schema/statement/mcp.{elicit,progress}.yaml`).
9
10
  //
@@ -71,6 +72,24 @@ export const ProgressToken = createTokenByName("progress", {
71
72
  categories: [Identifier],
72
73
  });
73
74
 
75
+ // "oauth" (the mcp_server `oauth` block and the `mcp.oauth.*` statements)
76
+ export const OauthToken = createTokenByName("oauth", {
77
+ longer_alt: Identifier,
78
+ categories: [Identifier],
79
+ });
80
+
81
+ // "complete" (mcp.oauth.complete)
82
+ export const CompleteToken = createTokenByName("complete", {
83
+ longer_alt: Identifier,
84
+ categories: [Identifier],
85
+ });
86
+
87
+ // "revoke" (mcp.oauth.revoke)
88
+ export const RevokeToken = createTokenByName("revoke", {
89
+ longer_alt: Identifier,
90
+ categories: [Identifier],
91
+ });
92
+
74
93
  export const McpObjectTokens = [
75
94
  PromptsToken,
76
95
  PromptToken,
@@ -82,6 +101,9 @@ export const McpObjectTokens = [
82
101
  MimeTypeToken,
83
102
  ElicitToken,
84
103
  ProgressToken,
104
+ OauthToken,
105
+ CompleteToken,
106
+ RevokeToken,
85
107
  ];
86
108
 
87
109
  /**
@@ -99,9 +121,12 @@ export function mapTokenToType(token) {
99
121
  case IconsToken.name:
100
122
  case UriToken.name:
101
123
  case MimeTypeToken.name:
124
+ case OauthToken.name:
102
125
  return "variable";
103
126
  case ElicitToken.name:
104
127
  case ProgressToken.name:
128
+ case CompleteToken.name:
129
+ case RevokeToken.name:
105
130
  return "function";
106
131
  default:
107
132
  return null;
@@ -0,0 +1,30 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // "policy"
5
+ export const PolicyToken = createTokenByName("policy", {
6
+ longer_alt: Identifier,
7
+ categories: [Identifier],
8
+ });
9
+
10
+ // "rule"
11
+ export const RuleToken = createTokenByName("rule", {
12
+ longer_alt: Identifier,
13
+ categories: [Identifier],
14
+ });
15
+
16
+ export const PolicyTokens = [PolicyToken, RuleToken];
17
+
18
+ /**
19
+ * Maps a token name to a type
20
+ * @param {string} token the token name
21
+ */
22
+ export function mapTokenToType(token) {
23
+ switch (token) {
24
+ case PolicyToken.name:
25
+ case RuleToken.name:
26
+ return "keyword";
27
+ default:
28
+ return null;
29
+ }
30
+ }
package/lexer/tokens.js CHANGED
@@ -116,6 +116,10 @@ import {
116
116
  mapTokenToType as mapObjectTokenToType,
117
117
  ObjectTokens,
118
118
  } from "./object.js";
119
+ import {
120
+ mapTokenToType as mapPolicyTokenToType,
121
+ PolicyTokens,
122
+ } from "./policy.js";
119
123
  import { mapTokenToType as mapQueryTokenToType, QueryTokens } from "./query.js";
120
124
  import {
121
125
  mapTokenToType as mapRealtimeTokenToType,
@@ -478,6 +482,7 @@ export const allTokens = uniq([
478
482
  ...AddonTokens,
479
483
  ...ActionTokens,
480
484
  ...BranchTokens,
485
+ ...PolicyTokens,
481
486
  ...WorkspaceTokens,
482
487
  ViewToken,
483
488
  JsonInToken,
@@ -518,6 +523,7 @@ const tokenMappers = [
518
523
  mapMiddlewareTokenToType,
519
524
  mapMultilineTokenToType,
520
525
  mapObjectTokenToType,
526
+ mapPolicyTokenToType,
521
527
  mapQueryTokenToType,
522
528
  mapRealtimeTokenToType,
523
529
  mapRealtimeServerTriggerTokenToType,
@@ -5,6 +5,7 @@ import { FilterMessageProvider } from "../onHover/FilterMessageProvider.js";
5
5
  import { encodeTokenType } from "../onSemanticCheck/tokens.js";
6
6
  import { filterNames } from "../parser/generic/filterNames.js";
7
7
  import { parser } from "../parser/parser.js";
8
+ import { getPolicyCompletions } from "./policyCompletion.js";
8
9
 
9
10
  // FIXME: this should be imported from the onHover/FilterMessageProvider.js
10
11
  const filterDocsMarkdown = `
@@ -133,6 +134,11 @@ const filterSuggestions = Object.freeze(
133
134
  const MAX_CONTENT_ASSIST_TOKENS = 40;
134
135
 
135
136
  export function getContentAssistSuggestions(text, scheme) {
137
+ // A policy is read with a tolerant scanner and the check catalogue, not content assist.
138
+ if (scheme === "policy") {
139
+ return getPolicyCompletions(text);
140
+ }
141
+
136
142
  try {
137
143
  const lexResult = lexDocument(text);
138
144
 
@@ -43,6 +43,7 @@ export function onCompletion(params, documents) {
43
43
 
44
44
  let scheme;
45
45
  let prefix;
46
+ let lineOffset = 0;
46
47
 
47
48
  if (parser.isMultidoc) {
48
49
  const match = findSegmentAtOffset(parser, offset);
@@ -52,15 +53,21 @@ export function onCompletion(params, documents) {
52
53
  // parses backwards from the cursor to find context, so it needs the segment
53
54
  // text up to the local offset
54
55
  prefix = match.segment.text.slice(0, match.localOffset);
56
+ // A textEdit (policy completion) is in segment coordinates; a segment starts a line.
57
+ lineOffset = document.positionAt(match.segment.globalOffset).line;
55
58
  } else {
56
59
  scheme = getSchemeFromContent(text);
57
60
  prefix = text.slice(0, offset);
58
61
  }
59
62
 
60
- const suggestions = getContentAssistSuggestions(prefix, scheme);
63
+ const suggestions = shiftTextEdits(
64
+ getContentAssistSuggestions(prefix, scheme),
65
+ lineOffset
66
+ );
61
67
 
62
- // Add workspace object name completions
63
- const workspaceCompletions = getWorkspaceCompletions(prefix);
68
+ // Add workspace object name completions; a policy names no workspace object (its prose would
69
+ // match the reference patterns).
70
+ const workspaceCompletions = scheme === "policy" ? null : getWorkspaceCompletions(prefix);
64
71
  if (workspaceCompletions) {
65
72
  const existing = suggestions || [];
66
73
  return [...existing, ...workspaceCompletions];
@@ -69,6 +76,29 @@ export function onCompletion(params, documents) {
69
76
  return suggestions;
70
77
  }
71
78
 
79
+ /**
80
+ * Move each item's textEdit down by `lines`.
81
+ * @param {Array|null} items
82
+ * @param {number} lines
83
+ */
84
+ function shiftTextEdits(items, lines) {
85
+ if (!items || !lines) return items;
86
+ return items.map((item) => {
87
+ if (!item.textEdit) return item;
88
+ const { start, end } = item.textEdit.range;
89
+ return {
90
+ ...item,
91
+ textEdit: {
92
+ ...item.textEdit,
93
+ range: {
94
+ start: { line: start.line + lines, character: start.character },
95
+ end: { line: end.line + lines, character: end.character },
96
+ },
97
+ },
98
+ };
99
+ });
100
+ }
101
+
72
102
  /**
73
103
  * Get completions for cross-file references based on cursor context.
74
104
  * @param {string} prefix - Text before cursor
@@ -0,0 +1,339 @@
1
+ import { getPolicyCatalogue } from "../parser/policy/catalogue.js";
2
+ import {
3
+ checkMarkdown,
4
+ paramMarkdown,
5
+ paramValues,
6
+ } from "../parser/policy/policyDocs.js";
7
+ import { own, policyStringValue } from "../parser/policy/policyLiteral.js";
8
+
9
+ /**
10
+ * Completion inside a `policy` document, all of it read from the check catalogue: the policy's and
11
+ * a rule's fields and their closed values (the catalogue's `document`), `rule`, check ids, a
12
+ * check's params, an object param's fields and a param's closed values.
13
+ *
14
+ * The text before the cursor is usually unfinished (an open string, a half-typed key), which the
15
+ * grammar cannot parse, so this reads it with a small tolerant scanner: strings, words,
16
+ * brackets, `=`, `:`, `,` and line breaks, tracking which block each `{` or `[` opened.
17
+ *
18
+ * Items use LSP CompletionItemKind numbers and carry a `textEdit` whose range is in the
19
+ * coordinates of `prefix`, so a check id with dots replaces what was typed as a whole.
20
+ */
21
+
22
+ /** LSP CompletionItemKind. */
23
+ const KIND = {
24
+ Field: 5,
25
+ Property: 10,
26
+ Value: 12,
27
+ Keyword: 14,
28
+ EnumMember: 20,
29
+ };
30
+
31
+ /** A document field's closed values, as completion offers them (`bool` is true and false). */
32
+ function fieldValues(spec) {
33
+ if (spec?.type === "bool") return ["true", "false"];
34
+ return spec?.values ?? null;
35
+ }
36
+
37
+ /** Completion documentation for a document field. */
38
+ function fieldDoc(name, spec) {
39
+ if (!spec?.description) return undefined;
40
+ const values = fieldValues(spec);
41
+ const lines = [`**${name}** (${spec.type ?? "string"})`, "", spec.description];
42
+ if (values) lines.push("", `One of: ${values.map((v) => "`" + v + "`").join(", ")}.`);
43
+ if (spec.default !== undefined) lines.push("", `Default: \`${spec.default}\``);
44
+ return { kind: "markdown", value: lines.join("\n") };
45
+ }
46
+
47
+ /**
48
+ * Split the text before the cursor into the few token kinds completion needs.
49
+ * @param {string} text
50
+ */
51
+ function scan(text) {
52
+ const tokens = [];
53
+ let i = 0;
54
+ while (i < text.length) {
55
+ const c = text[i];
56
+ if (c === "\n") {
57
+ tokens.push({ t: "nl", start: i, end: i + 1 });
58
+ i++;
59
+ } else if (c === " " || c === "\t" || c === "\r") {
60
+ i++;
61
+ } else if (c === "/" && text[i + 1] === "/") {
62
+ while (i < text.length && text[i] !== "\n") i++;
63
+ } else if (text.startsWith('"""', i) || text.startsWith("'''", i)) {
64
+ const close = text.indexOf(text.slice(i, i + 3), i + 3);
65
+ const end = close === -1 ? text.length : close + 3;
66
+ tokens.push({
67
+ t: "str",
68
+ start: i,
69
+ end,
70
+ quote: 3,
71
+ open: close === -1,
72
+ value: text.slice(i + 3, close === -1 ? text.length : close),
73
+ });
74
+ i = end;
75
+ } else if (c === '"' || c === "'") {
76
+ let j = i + 1;
77
+ while (j < text.length && text[j] !== c && text[j] !== "\n") {
78
+ j += text[j] === "\\" ? 2 : 1;
79
+ }
80
+ const open = j >= text.length;
81
+ const end = open ? text.length : j + 1;
82
+ tokens.push({
83
+ t: "str",
84
+ start: i,
85
+ end,
86
+ quote: 1,
87
+ open,
88
+ value: text.slice(i + 1, open ? text.length : j),
89
+ });
90
+ i = end;
91
+ } else if ("{}[]=:,".includes(c)) {
92
+ tokens.push({ t: c, start: i, end: i + 1 });
93
+ i++;
94
+ } else if (/[\w$.-]/.test(c)) {
95
+ let j = i;
96
+ while (j < text.length && /[\w$.-]/.test(text[j])) j++;
97
+ tokens.push({ t: "word", start: i, end: j, value: text.slice(i, j) });
98
+ i = j;
99
+ } else {
100
+ tokens.push({ t: "other", start: i, end: i + 1 });
101
+ i++;
102
+ }
103
+ }
104
+ return tokens;
105
+ }
106
+
107
+ /** The first word on the line holding token `k` (`rule` in `rule {`, `policy` in `policy KEY {`). */
108
+ function lineHead(tokens, k) {
109
+ if (tokens[k - 1]?.t === "word" && tokens[k - 1].value === "rule" && tokens[k - 2]?.value !== "policy") return "rule";
110
+ let j = k;
111
+ while (j > 0 && tokens[j - 1].t !== "nl") j--;
112
+ return tokens[j]?.t === "word" ? tokens[j].value : null;
113
+ }
114
+
115
+ /** The key a value at token `k` belongs to: the word before the `=` or `:` just before it. */
116
+ function keyBefore(tokens, k) {
117
+ const sep = tokens[k - 1];
118
+ if (sep && (sep.t === "=" || sep.t === ":") && tokens[k - 2]?.t === "word") {
119
+ return tokens[k - 2].value;
120
+ }
121
+ return null;
122
+ }
123
+
124
+ /**
125
+ * Where the cursor is: the stack of open blocks, and what is being typed.
126
+ * @param {string} prefix
127
+ */
128
+ function contextOf(prefix) {
129
+ const tokens = scan(prefix);
130
+ const frames = [];
131
+ const top = () => frames[frames.length - 1];
132
+
133
+ tokens.forEach((token, k) => {
134
+ if (token.t === "{" || token.t === "[") {
135
+ frames.push({
136
+ open: token.t,
137
+ owner: keyBefore(tokens, k) ?? (token.t === "{" ? lineHead(tokens, k) : null),
138
+ keys: new Set(),
139
+ check: null,
140
+ });
141
+ } else if (token.t === "}" || token.t === "]") {
142
+ frames.pop();
143
+ } else if (token.t === "word" && (tokens[k + 1]?.t === "=" || tokens[k + 1]?.t === ":")) {
144
+ top()?.keys.add(token.value);
145
+ } else if (token.t === "str" && !token.open && keyBefore(tokens, k) === "check") {
146
+ if (top()) top().check = policyStringValue(prefix.slice(token.start, token.end)).trim();
147
+ }
148
+ });
149
+
150
+ const last = tokens[tokens.length - 1];
151
+ const touching = last && last.end === prefix.length;
152
+
153
+ // Typing inside a string: a value.
154
+ if (touching && last.t === "str" && last.open) {
155
+ const k = tokens.length - 1;
156
+ const prev = tokens[k - 1];
157
+ const inList = prev && (prev.t === "[" || prev.t === ",") && top()?.open === "[";
158
+ return {
159
+ frames,
160
+ kind: "value",
161
+ key: inList ? top().owner : keyBefore(tokens, k),
162
+ inList,
163
+ quoted: true,
164
+ start: last.start + last.quote,
165
+ };
166
+ }
167
+
168
+ // A bare word being typed: a key, or an unquoted value after `=` / `:`.
169
+ const typing = touching && last.t === "word" ? last : null;
170
+ const k = typing ? tokens.length - 1 : tokens.length;
171
+ const prev = tokens[k - 1];
172
+ if (!prev || prev.t === "nl" || prev.t === "{" || prev.t === ",") {
173
+ if (top()?.open === "[") return null;
174
+ return {
175
+ frames,
176
+ kind: "key",
177
+ start: typing ? typing.start : prefix.length,
178
+ };
179
+ }
180
+ if (prev.t === "=" || prev.t === ":") {
181
+ return {
182
+ frames,
183
+ kind: "value",
184
+ key: keyBefore(tokens, k),
185
+ inList: false,
186
+ quoted: false,
187
+ start: typing ? typing.start : prefix.length,
188
+ };
189
+ }
190
+ if (prev.t === "[" && top()?.open === "[") {
191
+ return {
192
+ frames,
193
+ kind: "value",
194
+ key: top().owner,
195
+ inList: true,
196
+ quoted: false,
197
+ start: typing ? typing.start : prefix.length,
198
+ };
199
+ }
200
+ return null;
201
+ }
202
+
203
+ /** An LSP position within `text`. */
204
+ function positionAt(text, offset) {
205
+ const before = text.slice(0, offset);
206
+ const line = before.split("\n").length - 1;
207
+ return { line, character: offset - (before.lastIndexOf("\n") + 1) };
208
+ }
209
+
210
+ /**
211
+ * @param {string} prefix the document text before the cursor
212
+ * @returns {Object[]} LSP completion items; empty when there is nothing to offer here
213
+ */
214
+ export function getPolicyCompletions(prefix) {
215
+ const context = contextOf(prefix);
216
+ if (!context) return [];
217
+ const { frames } = context;
218
+ const catalogue = getPolicyCatalogue();
219
+ const range = {
220
+ start: positionAt(prefix, context.start),
221
+ end: positionAt(prefix, prefix.length),
222
+ };
223
+ const item = (label, kind, extra = {}) => ({
224
+ label,
225
+ kind,
226
+ ...extra,
227
+ textEdit: { range, newText: extra.newText ?? label },
228
+ });
229
+ const value = (label, kind, extra = {}) =>
230
+ item(label, kind, {
231
+ ...extra,
232
+ newText: context.quoted ? label : `"${label}"`,
233
+ filterText: context.quoted ? label : `"${label}"`,
234
+ });
235
+
236
+ const [policy, rule, params, nested] = frames;
237
+ if (!policy || policy.owner !== "policy") return [];
238
+ const ruleOpen = rule && rule.open === "{" && rule.owner === "rule";
239
+ const check =
240
+ ruleOpen && rule.check ? (catalogue.checks.get(rule.check) ?? null) : null;
241
+ const policyFields = catalogue.document?.fields ?? {};
242
+ const ruleFields = catalogue.document?.rule ?? {};
243
+ const fieldItem = (name, spec) =>
244
+ item(name, KIND.Property, {
245
+ detail: spec?.type,
246
+ documentation: fieldDoc(name, spec),
247
+ });
248
+
249
+ if (context.kind === "key") {
250
+ if (frames.length === 1) {
251
+ return [
252
+ ...Object.entries(policyFields)
253
+ .filter(([name]) => !policy.keys.has(name))
254
+ .map(([name, spec]) => fieldItem(name, spec)),
255
+ item("rule", KIND.Keyword, { detail: "rule { check = \"...\" }" }),
256
+ ];
257
+ }
258
+ if (frames.length === 2 && ruleOpen) {
259
+ return Object.entries(ruleFields)
260
+ .filter(([name]) => !rule.keys.has(name))
261
+ .map(([name, spec]) => fieldItem(name, spec));
262
+ }
263
+ if (frames.length === 3 && ruleOpen && params.owner === "params" && check) {
264
+ const applicable = new Set((check.scope_fields ?? []).map((field) => field.name));
265
+ return Object.entries(check.params ?? {})
266
+ .filter(([name]) => !params.keys.has(name))
267
+ .filter(([name]) => !catalogue.scopeFields.has(name) || applicable.has(name))
268
+ .map(([name, spec]) =>
269
+ item(name, KIND.Property, {
270
+ detail: `${spec.type ?? "string"}${spec.required ? ", required" : ""}`,
271
+ documentation: { kind: "markdown", value: paramMarkdown(check.id, name, spec) },
272
+ sortText: `${spec.required ? 0 : 1}${catalogue.scopeFields.has(name) ? 1 : 0}${name}`,
273
+ })
274
+ );
275
+ }
276
+ if (frames.length === 4 && ruleOpen && params.owner === "params" && check && nested.open === "{") {
277
+ const spec = own(check.params, nested.owner);
278
+ return Object.entries(spec?.properties ?? {})
279
+ .filter(([name]) => !name.startsWith("<") && !nested.keys.has(name))
280
+ .map(([name, property]) =>
281
+ item(name, KIND.Field, {
282
+ detail: property.type ?? "string",
283
+ documentation: {
284
+ kind: "markdown",
285
+ value: paramMarkdown(check.id, `${nested.owner}.${name}`, property),
286
+ },
287
+ })
288
+ );
289
+ }
290
+ return [];
291
+ }
292
+
293
+ // A value.
294
+ const closed = frames.length === 1 ? fieldValues(own(policyFields, context.key)) : null;
295
+ if (closed) {
296
+ const bool = policyFields[context.key].type === "bool";
297
+ if (bool && context.quoted) return [];
298
+ return closed.map((name) =>
299
+ bool
300
+ ? item(name, KIND.EnumMember, { newText: name, filterText: name })
301
+ : value(name, KIND.EnumMember)
302
+ );
303
+ }
304
+ if (frames.length === 2 && ruleOpen && context.key === "check") {
305
+ return catalogue.ids.map((id) => {
306
+ const entry = catalogue.checks.get(id);
307
+ return value(id, KIND.Value, {
308
+ detail: entry.label,
309
+ documentation: { kind: "markdown", value: checkMarkdown(entry, catalogue) },
310
+ });
311
+ });
312
+ }
313
+ if (!check || params?.owner !== "params") return [];
314
+
315
+ // Inside params: the param (or its list items, or an object param's field) with closed values.
316
+ let spec = null;
317
+ let path = null;
318
+ if (frames.length === 3 && !context.inList) {
319
+ spec = own(check.params, context.key);
320
+ path = context.key;
321
+ } else if (frames.length === 4 && context.inList) {
322
+ spec = own(check.params, context.key);
323
+ path = context.key;
324
+ } else if (frames.length === 4 && nested.open === "{" && !context.inList) {
325
+ spec = own(own(check.params, nested.owner)?.properties, context.key);
326
+ path = `${nested.owner}.${context.key}`;
327
+ }
328
+ const bool = spec?.type === "bool";
329
+ if (bool && context.quoted) return [];
330
+ const values = bool ? ["true", "false"] : paramValues(spec);
331
+ if (!values) return [];
332
+ return values
333
+ .filter((option) => option !== "")
334
+ .map((option) =>
335
+ (bool ? item : value)(String(option), KIND.EnumMember, {
336
+ documentation: { kind: "markdown", value: paramMarkdown(check.id, path, spec) },
337
+ })
338
+ );
339
+ }