@px-lsp/server 0.1.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 (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. package/src/wordAt.ts +49 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * paradox/dependencies: a generic dependency explorer for any indexed definition.
3
+ *
4
+ * Dependents — mod sites that reference the definition, grouped by the kind
5
+ * of their containing top-level definition (else by file). Schema-captured
6
+ * references come from the reference index (find-references semantics); bare
7
+ * `name = …` invocations of scripted effects/triggers, which the reference
8
+ * extractor does not record (they are keys, not schema field values), are
9
+ * found by a targeted scan of the mod's definition files. Vanilla references
10
+ * are not indexed (rework plan AD-4), so vanilla callers never appear here.
11
+ *
12
+ * Dependencies — named definitions referenced inside the definition's own block
13
+ * span, grouped by the referenced definition's kind. The file is parsed on
14
+ * demand: extractReferences (schema fields, prefixed values, loc keys) is
15
+ * filtered to the block's lines, then a scalar walk adds the bare scripted
16
+ * effect/trigger calls and script-value scalars the extractor misses.
17
+ *
18
+ * No `vscode` imports: unit-tested in plain Node.
19
+ */
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import type {
23
+ DependenciesResult,
24
+ DependencyGroup,
25
+ DependencyItem,
26
+ GuiUseSite,
27
+ } from "@px-lsp/protocol/protocol";
28
+ import type { Definition } from "@px-lsp/protocol/types";
29
+ import { pushAll } from "@px-lsp/protocol/arrays";
30
+ import type { ServerData } from "../serverData";
31
+ import type { SchemaData } from "../schema/loader";
32
+ import { extractReferences } from "../index/references";
33
+ import { decode, LineIndex, parseScript, walkStatements, type BlockNode, type Statement } from "../parser";
34
+
35
+ /** Definition kinds invoked as a bare `name = …` key (not captured as references). */
36
+ const CALL_KINDS = new Set(["scripted_effect", "scripted_trigger"]);
37
+
38
+ export function computeDependencies(
39
+ data: ServerData,
40
+ schema: SchemaData,
41
+ name: string,
42
+ kind?: string,
43
+ /** The GUI half of the answer, when the caller asked for it. Injected rather
44
+ * than imported so this module stays free of the gui store's file walk. */
45
+ guiUses?: (name: string) => GuiUseSite[]
46
+ ): DependenciesResult {
47
+ const resolved = data.index.lookup(name);
48
+ const candidates = resolved.length > 0 ? resolved : data.index.lookupAll(name);
49
+ const def = (kind ? candidates.find((d) => d.kind === kind) : undefined) ?? candidates[0] ?? null;
50
+ if (!def) return { def: null, dependents: [], dependencies: [], ...(guiUses ? { guiUses: [] } : {}) };
51
+
52
+ return {
53
+ def: { name: def.name, kind: def.kind, file: def.file, line: def.line },
54
+ dependents: collectDependents(data, def),
55
+ dependencies: collectDependencies(data, schema, def),
56
+ ...(guiUses ? { guiUses: guiUses(def.name) } : {}),
57
+ };
58
+ }
59
+
60
+ // ---- dependents -------------------------------------------------------------
61
+
62
+ interface Site {
63
+ file: string;
64
+ line: number;
65
+ }
66
+
67
+ function collectDependents(data: ServerData, def: Definition): DependencyGroup[] {
68
+ const sites: Site[] = data.refIndex.lookup(def.name).map((r) => ({ file: r.file, line: r.line }));
69
+ if (CALL_KINDS.has(def.kind)) pushAll(sites, scanCallSites(data, def));
70
+
71
+ const groups = new Map<string, Map<string, DependencyItem>>();
72
+ for (const site of sites) {
73
+ const container = containingDef(data, site.file, site.line);
74
+ const groupKind = container ? container.kind : "file";
75
+ const itemName = container ? container.name : path.basename(site.file);
76
+ const dedupe = `${itemName} ${site.file}`;
77
+ let byItem = groups.get(groupKind);
78
+ if (!byItem) groups.set(groupKind, (byItem = new Map()));
79
+ // First site wins the jump target: the actual usage line.
80
+ if (!byItem.has(dedupe)) byItem.set(dedupe, { name: itemName, file: site.file, line: site.line });
81
+ }
82
+ return toGroups(groups);
83
+ }
84
+
85
+ /**
86
+ * Bare `name = …` invocations across the mod's definition files. These are the
87
+ * scripted effect/trigger calls the reference extractor cannot record (the name
88
+ * is a key, and the extractor does not know which keys resolve to definitions).
89
+ * The definition's own site is excluded.
90
+ */
91
+ function scanCallSites(data: ServerData, def: Definition): Site[] {
92
+ const sites: Site[] = [];
93
+ const seen = new Set<string>();
94
+ for (const file of modFiles(data)) {
95
+ let text: string;
96
+ try {
97
+ text = decode(fs.readFileSync(file)).text;
98
+ } catch {
99
+ continue;
100
+ }
101
+ if (!text.includes(def.name)) continue;
102
+ const parse = parseScript(text);
103
+ const li = new LineIndex(text);
104
+ walkStatements(parse.root, (stmt: Statement) => {
105
+ if (stmt.kind !== "assignment" || stmt.key.quoted || stmt.key.text !== def.name) return;
106
+ const line = li.positionAt(stmt.key.range.start).line;
107
+ if (file === def.file && line === def.line) return; // the definition itself
108
+ const key = `${file} ${line}`;
109
+ if (seen.has(key)) return;
110
+ seen.add(key);
111
+ sites.push({ file, line });
112
+ });
113
+ }
114
+ return sites;
115
+ }
116
+
117
+ /** Unique mod definition files (where realistic caller sites live). */
118
+ function modFiles(data: ServerData): string[] {
119
+ const files = new Set<string>();
120
+ for (const d of data.index.allDefinitions()) {
121
+ if (d.source === "mod" && d.file.toLowerCase().endsWith(".txt")) files.add(d.file);
122
+ }
123
+ return [...files];
124
+ }
125
+
126
+ /**
127
+ * The top-level definition in `file` whose block encloses `line`: the one with
128
+ * the greatest start line at or before it. Implicit/nested sites (save_scope_as
129
+ * etc., which carry a container) are skipped so the real container is found.
130
+ */
131
+ function containingDef(data: ServerData, file: string, line: number): Definition | null {
132
+ let best: Definition | null = null;
133
+ for (const d of data.index.inFile(file)) {
134
+ if (d.container !== undefined) continue;
135
+ if (d.line <= line && (!best || d.line > best.line)) best = d;
136
+ }
137
+ return best;
138
+ }
139
+
140
+ // ---- dependencies -----------------------------------------------------------
141
+
142
+ function collectDependencies(data: ServerData, schema: SchemaData, def: Definition): DependencyGroup[] {
143
+ let text: string;
144
+ try {
145
+ text = decode(fs.readFileSync(def.file)).text;
146
+ } catch {
147
+ return [];
148
+ }
149
+ const parse = parseScript(text);
150
+ const li = new LineIndex(text);
151
+ const lineOf = (offset: number) => li.positionAt(offset).line;
152
+ const stmt = parse.root.statements.find(
153
+ (s): s is Statement & { kind: "assignment" } =>
154
+ s.kind === "assignment" && s.key.text === def.name && lineOf(s.key.range.start) === def.line
155
+ );
156
+ const block = stmt ? childBlock(stmt) : null;
157
+ if (!block) return [];
158
+ const startLine = lineOf(block.range.start);
159
+ const endLine = block.closeBrace !== null ? lineOf(block.closeBrace) : lineOf(block.range.end);
160
+
161
+ const groups = new Map<string, Map<string, DependencyItem>>();
162
+ const addTarget = (target: Definition) => {
163
+ const dedupe = `${target.kind} ${target.name}`;
164
+ let byItem = groups.get(target.kind);
165
+ if (!byItem) groups.set(target.kind, (byItem = new Map()));
166
+ if (!byItem.has(dedupe)) {
167
+ byItem.set(dedupe, { name: target.name, file: target.file, line: target.line });
168
+ }
169
+ };
170
+
171
+ // (a) Schema-captured references within the block: traits (has_trait), events
172
+ // (trigger_event), cultures/faiths, loc keys, variables/flags/scopes.
173
+ for (const ref of extractReferences(text, def.file, def.source, schema).references) {
174
+ if (ref.line < startLine || ref.line > endLine || ref.name === def.name) continue;
175
+ const target = resolveTarget(data, ref.name, ref.kinds);
176
+ if (target) addTarget(target);
177
+ }
178
+
179
+ // (b) Bare `name = …` calls to scripted effects/triggers and script-value
180
+ // scalars, which the reference extractor does not record. Same resolution
181
+ // as the event inspector (eventDetail.collectRefs); the group map dedupes
182
+ // any overlap with (a).
183
+ walkScalars(block, (word, isKey) => {
184
+ if (word === def.name) return;
185
+ const defs = data.index.lookup(word);
186
+ if (defs.length === 0) return;
187
+ const target = isKey
188
+ ? defs.find((d) => d.kind === "scripted_effect" || d.kind === "scripted_trigger")
189
+ : defs.find((d) => d.kind === "script_value");
190
+ if (target) addTarget(target);
191
+ });
192
+
193
+ return toGroups(groups);
194
+ }
195
+
196
+ /** Visit every unquoted scalar in a block: keys (isKey=true) and values. */
197
+ function walkScalars(block: BlockNode, cb: (word: string, isKey: boolean) => void): void {
198
+ for (const stmt of block.statements) {
199
+ if (stmt.kind === "assignment") {
200
+ if (!stmt.key.quoted) cb(stmt.key.text, true);
201
+ const v = stmt.value;
202
+ if (v?.kind === "scalar" && !v.quoted) cb(v.text, false);
203
+ const sub = childBlock(stmt);
204
+ if (sub) walkScalars(sub, cb);
205
+ } else if (stmt.value.kind === "scalar" && !stmt.value.quoted) {
206
+ cb(stmt.value.text, false);
207
+ } else if (stmt.value.kind === "block") {
208
+ walkScalars(stmt.value, cb);
209
+ } else if (stmt.value.kind === "tagged-block") {
210
+ walkScalars(stmt.value.block, cb);
211
+ }
212
+ }
213
+ }
214
+
215
+ /** Best index match for a reference: prefer a def whose kind the ref allows. */
216
+ function resolveTarget(data: ServerData, name: string, kinds: string[]): Definition | null {
217
+ const defs = data.index.lookup(name);
218
+ if (defs.length === 0) return null;
219
+ return defs.find((d) => kinds.includes(d.kind)) ?? defs[0];
220
+ }
221
+
222
+ // ---- shared -----------------------------------------------------------------
223
+
224
+ function childBlock(stmt: Statement): BlockNode | null {
225
+ if (stmt.kind !== "assignment") return null;
226
+ const v = stmt.value;
227
+ if (!v) return null;
228
+ if (v.kind === "block") return v;
229
+ if (v.kind === "tagged-block") return v.block;
230
+ return null;
231
+ }
232
+
233
+ function toGroups(groups: Map<string, Map<string, DependencyItem>>): DependencyGroup[] {
234
+ return [...groups.entries()]
235
+ .map(([kind, byItem]) => ({
236
+ kind,
237
+ items: [...byItem.values()].sort((a, b) => a.name.localeCompare(b.name)),
238
+ }))
239
+ .sort((a, b) => a.kind.localeCompare(b.kind));
240
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * paradox/eventBanner: the illustration an event theme puts behind its window.
3
+ *
4
+ * Two hops through the game's own files, no name table of our own:
5
+ *
6
+ * theme common/event_themes `background = { reference = throne_room }`
7
+ * bg common/event_backgrounds `background = { reference = "gfx/....dds" }`
8
+ *
9
+ * Both levels list several `background` blocks, all but the last gated by a
10
+ * `trigger` that only the running game can evaluate. The honest pick is the
11
+ * LAST block with no trigger: the file's own unconditional fallback (measured
12
+ * against the vanilla tree, where every background definition ends in one).
13
+ * A theme whose reference is already a path skips the second hop.
14
+ *
15
+ * Answering null is a real answer: the caller draws a labeled placeholder
16
+ * rather than a picture that is not the event's.
17
+ */
18
+ import * as fs from "fs";
19
+ import type { EventBannerResult } from "@px-lsp/protocol/protocol";
20
+ import type { ServerData } from "../serverData";
21
+ import { decode, parseScript, type BlockNode, type Statement } from "../parser";
22
+
23
+ /** A reference that already names a file rather than another definition. */
24
+ const TEXTURE_PATH = /\.(dds|tga|png)$/i;
25
+
26
+ export function computeEventBanner(data: ServerData, theme: string): EventBannerResult {
27
+ // The graph sends whatever the event itself names: an override_background
28
+ // reference (an event_background key, or already a texture path) wins over
29
+ // the theme, because that is the picture the game actually shows.
30
+ if (TEXTURE_PATH.test(theme)) return { theme, texture: theme };
31
+
32
+ const themeRef = referenceOf(data, theme, "event_theme");
33
+ if (themeRef !== null) {
34
+ if (TEXTURE_PATH.test(themeRef)) return { theme, texture: themeRef };
35
+ const backgroundRef = referenceOf(data, themeRef, "event_background");
36
+ if (backgroundRef === null) return { theme, reason: `no background definition named ${themeRef}` };
37
+ if (!TEXTURE_PATH.test(backgroundRef)) return { theme, reason: `${themeRef} names no texture` };
38
+ return { theme, texture: backgroundRef };
39
+ }
40
+
41
+ // Not a theme: an event_background named directly (override_background).
42
+ const direct = referenceOf(data, theme, "event_background");
43
+ if (direct === null) return { theme, reason: "no theme or background definition with a background" };
44
+ if (!TEXTURE_PATH.test(direct)) return { theme, reason: `${theme} names no texture` };
45
+ return { theme, texture: direct };
46
+ }
47
+
48
+ /**
49
+ * The `reference` of a definition's fallback `background` block: the last one
50
+ * that carries no `trigger`, else the last one there is.
51
+ */
52
+ function referenceOf(data: ServerData, name: string, kind: string): string | null {
53
+ const def = data.index.lookup(name).find((d) => d.kind === kind);
54
+ if (!def) return null;
55
+ let block: BlockNode | null = null;
56
+ try {
57
+ const text = decode(fs.readFileSync(def.file)).text;
58
+ const stmt = parseScript(text).root.statements.find(
59
+ (s): s is Statement & { kind: "assignment" } => s.kind === "assignment" && s.key.text === name
60
+ );
61
+ block = stmt ? childBlock(stmt) : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ if (!block) return null;
66
+
67
+ let fallback: string | null = null;
68
+ let last: string | null = null;
69
+ for (const stmt of block.statements) {
70
+ if (stmt.kind !== "assignment" || stmt.key.text.toLowerCase() !== "background") continue;
71
+ const body = childBlock(stmt);
72
+ if (!body) continue;
73
+ let reference: string | null = null;
74
+ let gated = false;
75
+ for (const child of body.statements) {
76
+ if (child.kind !== "assignment") continue;
77
+ const key = child.key.text.toLowerCase();
78
+ if (key === "trigger") gated = true;
79
+ else if (key === "reference" && child.value?.kind === "scalar") reference = child.value.text;
80
+ }
81
+ if (reference === null) continue;
82
+ last = reference;
83
+ if (!gated) fallback = reference;
84
+ }
85
+ return fallback ?? last;
86
+ }
87
+
88
+ function childBlock(stmt: Statement): BlockNode | null {
89
+ if (stmt.kind !== "assignment") return null;
90
+ const v = stmt.value;
91
+ if (!v) return null;
92
+ if (v.kind === "block") return v;
93
+ if (v.kind === "tagged-block") return v.block;
94
+ return null;
95
+ }