@intentius/chant-lexicon-helm 0.46.0 → 0.50.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 (122) hide show
  1. package/dist/archive-render.d.ts +94 -0
  2. package/dist/archive-render.d.ts.map +1 -0
  3. package/dist/commands.d.ts +54 -0
  4. package/dist/commands.d.ts.map +1 -0
  5. package/dist/config.d.ts +135 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/deep-observe-hooks.d.ts +2 -0
  8. package/dist/deep-observe-hooks.d.ts.map +1 -0
  9. package/dist/deep-observe.d.ts +54 -0
  10. package/dist/deep-observe.d.ts.map +1 -0
  11. package/dist/describe-resources.d.ts +17 -0
  12. package/dist/describe-resources.d.ts.map +1 -0
  13. package/dist/index.d.ts +19 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/integrity.json +5 -3
  16. package/dist/lint/audit-catalog.d.ts.map +1 -1
  17. package/dist/lint/post-synth/index.d.ts.map +1 -1
  18. package/dist/lint/post-synth/whm503.d.ts +34 -0
  19. package/dist/lint/post-synth/whm503.d.ts.map +1 -0
  20. package/dist/lint/post-synth/whm504.d.ts +20 -0
  21. package/dist/lint/post-synth/whm504.d.ts.map +1 -0
  22. package/dist/manifest.json +25 -13
  23. package/dist/okf/index.md +2 -0
  24. package/dist/okf/rules/WHM503.md +15 -0
  25. package/dist/okf/rules/WHM504.md +11 -0
  26. package/dist/okf/types/Values.md +1 -0
  27. package/dist/op/activities/cluster-probe.d.ts +44 -0
  28. package/dist/op/activities/cluster-probe.d.ts.map +1 -0
  29. package/dist/op/activities/helm.d.ts +179 -5
  30. package/dist/op/activities/helm.d.ts.map +1 -1
  31. package/dist/op/activities/index.d.ts +4 -2
  32. package/dist/op/activities/index.d.ts.map +1 -1
  33. package/dist/op/builders.d.ts +47 -0
  34. package/dist/op/builders.d.ts.map +1 -0
  35. package/dist/pinnability/actions.d.ts +76 -0
  36. package/dist/pinnability/actions.d.ts.map +1 -0
  37. package/dist/pinnability/classify.d.ts +129 -0
  38. package/dist/pinnability/classify.d.ts.map +1 -0
  39. package/dist/pinnability/conditions.d.ts +109 -0
  40. package/dist/pinnability/conditions.d.ts.map +1 -0
  41. package/dist/pinnability/index.d.ts +17 -0
  42. package/dist/pinnability/index.d.ts.map +1 -0
  43. package/dist/pinnability/localize.d.ts +124 -0
  44. package/dist/pinnability/localize.d.ts.map +1 -0
  45. package/dist/pinnability/render-stream.d.ts +27 -0
  46. package/dist/pinnability/render-stream.d.ts.map +1 -0
  47. package/dist/pinnability/values.d.ts +69 -0
  48. package/dist/pinnability/values.d.ts.map +1 -0
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/release-observe.d.ts +109 -0
  51. package/dist/release-observe.d.ts.map +1 -0
  52. package/dist/render-diff.d.ts +192 -0
  53. package/dist/render-diff.d.ts.map +1 -0
  54. package/dist/render-digest.d.ts +135 -0
  55. package/dist/render-digest.d.ts.map +1 -0
  56. package/dist/render-store.d.ts +310 -0
  57. package/dist/render-store.d.ts.map +1 -0
  58. package/dist/render-wrapper.d.ts +140 -0
  59. package/dist/render-wrapper.d.ts.map +1 -0
  60. package/dist/render.d.ts +81 -0
  61. package/dist/render.d.ts.map +1 -1
  62. package/dist/rules/whm503.ts +101 -0
  63. package/dist/rules/whm504.ts +52 -0
  64. package/dist/skills/chant-helm.md +7 -0
  65. package/dist/values-probe.d.ts +177 -0
  66. package/dist/values-probe.d.ts.map +1 -0
  67. package/dist/wrapper-chart.d.ts +61 -0
  68. package/dist/wrapper-chart.d.ts.map +1 -0
  69. package/package.json +10 -4
  70. package/src/archive-render.test.ts +201 -0
  71. package/src/archive-render.ts +119 -0
  72. package/src/commands.test.ts +487 -0
  73. package/src/commands.ts +445 -0
  74. package/src/config.test.ts +130 -0
  75. package/src/config.ts +220 -0
  76. package/src/deep-observe-hooks.ts +20 -0
  77. package/src/deep-observe.test.ts +165 -0
  78. package/src/deep-observe.ts +121 -0
  79. package/src/describe-resources.test.ts +255 -0
  80. package/src/describe-resources.ts +198 -0
  81. package/src/index.ts +106 -2
  82. package/src/lifecycle-integration.test.ts +289 -0
  83. package/src/lint/audit-catalog.ts +2 -0
  84. package/src/lint/post-synth/index.ts +4 -0
  85. package/src/lint/post-synth/whm503.test.ts +134 -0
  86. package/src/lint/post-synth/whm503.ts +101 -0
  87. package/src/lint/post-synth/whm504.test.ts +105 -0
  88. package/src/lint/post-synth/whm504.ts +52 -0
  89. package/src/op/activities/cluster-probe.test.ts +66 -0
  90. package/src/op/activities/cluster-probe.ts +148 -0
  91. package/src/op/activities/helm.test.ts +824 -0
  92. package/src/op/activities/helm.ts +628 -5
  93. package/src/op/activities/index.ts +17 -2
  94. package/src/op/builders.test.ts +70 -0
  95. package/src/op/builders.ts +66 -0
  96. package/src/pinnability/actions.ts +263 -0
  97. package/src/pinnability/classify.ts +579 -0
  98. package/src/pinnability/conditions.ts +0 -0
  99. package/src/pinnability/index.ts +77 -0
  100. package/src/pinnability/localize.test.ts +234 -0
  101. package/src/pinnability/localize.ts +644 -0
  102. package/src/pinnability/pinnability.test.ts +560 -0
  103. package/src/pinnability/render-stream.ts +59 -0
  104. package/src/pinnability/values.ts +205 -0
  105. package/src/plugin.test.ts +3 -1
  106. package/src/plugin.ts +97 -12
  107. package/src/release-observe.ts +392 -0
  108. package/src/render-diff.test.ts +366 -0
  109. package/src/render-diff.ts +407 -0
  110. package/src/render-digest.test.ts +186 -0
  111. package/src/render-digest.ts +207 -0
  112. package/src/render-store.test.ts +412 -0
  113. package/src/render-store.ts +621 -0
  114. package/src/render-wrapper.test.ts +259 -0
  115. package/src/render-wrapper.ts +274 -0
  116. package/src/render.test.ts +383 -4
  117. package/src/render.ts +319 -8
  118. package/src/skills/chant-helm.md +7 -0
  119. package/src/values-probe.test.ts +400 -0
  120. package/src/values-probe.ts +627 -0
  121. package/src/wrapper-chart.test.ts +110 -0
  122. package/src/wrapper-chart.ts +124 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Typed step-builder wrappers (chant #1288 Stage 2) — see
3
+ * `lexicons/k8s/src/op/builders.test.ts`'s module doc for what's asserted
4
+ * and why (backward compat against core's original, step-output-ref
5
+ * composition, compile-time-only `@ts-expect-error` checks walked by
6
+ * `npm run typecheck`).
7
+ */
8
+
9
+ import { describe, test, expect } from "vitest";
10
+ import {
11
+ helmInstall as helmInstallOld,
12
+ helmInstallPinned as helmInstallPinnedOld,
13
+ stepOutput,
14
+ type StepOutputRef,
15
+ } from "@intentius/chant/op";
16
+ import { helmInstall, helmInstallPinned } from "./builders";
17
+
18
+ describe("helm typed step builders (#1288 Stage 2)", () => {
19
+ test("helmInstall: identical ActivityStep to core's original for a minimal call", () => {
20
+ expect(helmInstall("api", "./chart")).toEqual(helmInstallOld("api", "./chart"));
21
+ });
22
+
23
+ test("helmInstall: identical ActivityStep with opts (namespace, values, set, capabilityProfile)", () => {
24
+ const opts = {
25
+ namespace: "prod",
26
+ values: "values.yaml",
27
+ set: { "image.tag": "v2" },
28
+ capabilityProfile: { kubeVersion: "1.29" },
29
+ profile: "fastIdempotent" as const,
30
+ };
31
+ expect(helmInstall("api", "./chart", opts)).toEqual(helmInstallOld("api", "./chart", opts));
32
+ });
33
+
34
+ test("helmInstallPinned: identical ActivityStep to core's original", () => {
35
+ const opts = { namespace: "prod" };
36
+ expect(helmInstallPinned("api", "sha256:abc", opts)).toEqual(helmInstallPinnedOld("api", "sha256:abc", opts));
37
+ });
38
+
39
+ test("helmInstall: accepts a StepOutputRef in a typed slot", () => {
40
+ const ref = stepOutput("render-step", "namespace");
41
+ const step = helmInstall("api", "./chart", { namespace: ref });
42
+ expect(step.args?.namespace).toBe(ref);
43
+ });
44
+
45
+ test("helmInstall: .out is reachable when an id is given", () => {
46
+ const step = helmInstall("api", "./chart", { id: "deploy" });
47
+ const ref: StepOutputRef = step.out.releaseName;
48
+ expect(ref.step).toBe("deploy");
49
+ expect(ref.path).toBe("releaseName");
50
+ });
51
+ });
52
+
53
+ // ── Compile-time-only: authoring-time type errors (never executed) ──────────
54
+ function _typeChecksOnly(): void {
55
+ // @ts-expect-error — "nameSpace" is not a key of HelmInstallArgs (the exact
56
+ // typo class chant #1288 names: helmInstall("api", "./chart", { nameSpace:
57
+ // "prod" }) silently no-opped before this).
58
+ helmInstall("api", "./chart", { nameSpace: "prod" });
59
+
60
+ // @ts-expect-error — set's values must be strings, not numbers.
61
+ helmInstall("api", "./chart", { set: { replicas: 3 } });
62
+
63
+ // @ts-expect-error — overrideProfileAssertion must be boolean.
64
+ helmInstall("api", "./chart", { overrideProfileAssertion: "true" });
65
+
66
+ // @ts-expect-error — contentDigest must be a string (the `sha256:...`
67
+ // digest), not a number.
68
+ helmInstall("api", "./chart", { contentDigest: 5 });
69
+ }
70
+ void _typeChecksOnly;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Typed step-builder wrappers for this lexicon's `helmInstall` activity
3
+ * (chant #1288 Stage 2 — "regenerate the step builders as fully typed
4
+ * wrappers"). Core cannot import a lexicon's `*Args` types (lexicons depend
5
+ * on core, not the other way around), so a fully typed `helmInstall` /
6
+ * `helmInstallPinned` has to live beside {@link HelmInstallArgs} — here,
7
+ * next to `./activities/helm.ts`. `opts`'s type in each wrapper below IS
8
+ * `HelmInstallArgs` itself (via `Omit`/`WithStepRefs`) — never a
9
+ * hand-restated mirror — so the option surface Stage 1's zod contracts
10
+ * deliberately skipped (`capabilityProfile`, `set`, `deleteMode`-adjacent
11
+ * cross-field options) is authoring-time typed for free.
12
+ *
13
+ * Every field also accepts a {@link StepOutputRef} in its place
14
+ * ({@link WithStepRefs}, chant #1950).
15
+ *
16
+ * `core`'s own `helmInstall`/`helmInstallPinned` (in `@intentius/chant/op`,
17
+ * re-exported from `@intentius/chant-lexicon-temporal`) are UNCHANGED and
18
+ * produce byte-identical `ActivityStep` output for the same inputs — these
19
+ * are purely additive. Deliberately not swapped into the temporal barrel
20
+ * (see `lexicons/k8s/src/op/builders.ts`'s module doc for why: it would make
21
+ * temporal depend on this package at runtime, undoing the product-agnostic
22
+ * split #809 did). An author who wants the typed surface imports it from
23
+ * here — `@intentius/chant-lexicon-helm` — which a project using
24
+ * `helmInstall` already depends on.
25
+ */
26
+
27
+ import {
28
+ activity,
29
+ takeProfileAndId,
30
+ type ActivityStep,
31
+ type NamedActivityStep,
32
+ type WithStepRefs,
33
+ } from "@intentius/chant/op";
34
+ import type { HelmInstallArgs } from "./activities/helm";
35
+
36
+ /** Extra opts every wrapper below accepts alongside its activity's own fields. */
37
+ type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
38
+
39
+ /**
40
+ * Deploy a helm release — the fully typed twin of core's `helmInstall`.
41
+ * `opts` is {@link HelmInstallArgs} itself, minus the positional `name`/
42
+ * `chart`. Defaults to the `longInfra` profile.
43
+ */
44
+ export const helmInstall = (
45
+ name: string,
46
+ chart: string,
47
+ opts?: WithStepRefs<Omit<HelmInstallArgs, "name" | "chart">> & StepOpts,
48
+ ): NamedActivityStep => {
49
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
50
+ return activity("helmInstall", { name, chart, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
51
+ };
52
+
53
+ /**
54
+ * Deploy a recorded pinned render by its `sha256:` content digest (#1242) —
55
+ * the fully typed twin of core's `helmInstallPinned`. `opts` is
56
+ * {@link HelmInstallArgs} itself, minus the positional `name`/
57
+ * `contentDigest`. Defaults to the `longInfra` profile.
58
+ */
59
+ export const helmInstallPinned = (
60
+ name: string,
61
+ contentDigest: string,
62
+ opts?: WithStepRefs<Omit<HelmInstallArgs, "name" | "contentDigest">> & StepOpts,
63
+ ): NamedActivityStep => {
64
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
65
+ return activity("helmInstall", { name, contentDigest, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
66
+ };
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Go template action extraction and control-flow scoping (#1234, epic #1228).
3
+ *
4
+ * The pinnability gate inspects template ACTIONS (`{{ ... }}`), never raw
5
+ * text. That distinction is finding 9: cert-manager carries 52 textual
6
+ * "lookup" occurrences, all comment prose, and a real template-action count
7
+ * of 0 — a text-matching gate refuses a chart that is perfectly pinnable.
8
+ *
9
+ * Beyond extraction, this module reconstructs the control-flow structure of a
10
+ * template file — which `if` / `with` / `range` blocks enclose each action —
11
+ * so the classifier can ask whether a construct is REACHABLE under the
12
+ * supplied values, not merely present. Reachability is what separates
13
+ * "unpinnable" from "pinnable with a recorded conditional hazard": bundled
14
+ * grafana's control-flow `lookup` sits behind `persistence.enabled`, off by
15
+ * default, so a default-values render never consults the cluster.
16
+ */
17
+
18
+ export type ActionKind =
19
+ | "if"
20
+ | "elseif"
21
+ | "else"
22
+ | "with"
23
+ | "range"
24
+ | "define"
25
+ | "block"
26
+ | "end"
27
+ | "other";
28
+
29
+ export interface TemplateAction {
30
+ /** Path of the file the action was found in (as given by the caller). */
31
+ file: string;
32
+ /** 1-based line the action starts on. */
33
+ line: number;
34
+ /** The action body between `{{` and `}}`, trim markers stripped. */
35
+ body: string;
36
+ /** Structural role of the action, from its leading keyword. */
37
+ kind: ActionKind;
38
+ }
39
+
40
+ /**
41
+ * One enclosing control-flow frame for an action. `conditions` are the
42
+ * expressions that must hold for the frame's branch to run; `negatedConditions`
43
+ * are prior branch conditions that must NOT hold (an `else` / `else if`
44
+ * branch runs only when every earlier branch condition was false).
45
+ */
46
+ export interface ScopeFrame {
47
+ kind: "if" | "with" | "range" | "define" | "block" | "dependency-condition";
48
+ conditions: string[];
49
+ negatedConditions: string[];
50
+ /**
51
+ * When a `with` block pins the dot to a `.Values` subtree, the path of that
52
+ * subtree — so bare `.enabled` inside `with .Values.persistence` resolves
53
+ * to `persistence.enabled`. `undefined` means the dot is unresolvable
54
+ * (a `range` element, a non-values `with`, a `define` invoked from an
55
+ * unknown context).
56
+ */
57
+ dotValuesPath?: string[];
58
+ /** Dot is rebound to something we cannot resolve (range element etc.). */
59
+ dotUnresolvable?: boolean;
60
+ }
61
+
62
+ export interface ScopedAction {
63
+ action: TemplateAction;
64
+ /** Enclosing frames, outermost first. Empty for top-level actions. */
65
+ frames: ScopeFrame[];
66
+ }
67
+
68
+ const KIND_RE =
69
+ /^(if\b|else\s+if\b|else\b|with\b|range\b|define\b|block\b|end\b)/;
70
+
71
+ function kindOf(body: string): ActionKind {
72
+ const m = body.match(KIND_RE);
73
+ if (!m) return "other";
74
+ const kw = m[1].replace(/\s+/g, " ").trim();
75
+ if (kw === "else if") return "elseif";
76
+ return kw.replace(/\b\s.*$/, "") as ActionKind;
77
+ }
78
+
79
+ /**
80
+ * Extract template actions from a source file, skipping comment actions
81
+ * (`{{/* ... *\/}}`). Text outside actions — including YAML `#` comments,
82
+ * where finding 9's 52 false positives lived — is never inspected by any
83
+ * scan built on this.
84
+ */
85
+ export function extractActions(source: string, file: string): TemplateAction[] {
86
+ const actions: TemplateAction[] = [];
87
+ let i = 0;
88
+ let line = 1;
89
+ let lineScanPos = 0;
90
+ const lineAt = (pos: number): number => {
91
+ // Incremental line counting: `pos` is monotonically increasing.
92
+ for (; lineScanPos < pos; lineScanPos++) {
93
+ if (source[lineScanPos] === "\n") line += 1;
94
+ }
95
+ return line;
96
+ };
97
+ while (i < source.length) {
98
+ const open = source.indexOf("{{", i);
99
+ if (open === -1) break;
100
+ const actionLine = lineAt(open);
101
+ // Body starts after `{{` and an optional trim marker.
102
+ let bodyStart = open + 2;
103
+ if (source[bodyStart] === "-") bodyStart += 1;
104
+ const rest = source.slice(bodyStart);
105
+ // Comment action: {{/* ... */}} — skip to its closing */}}.
106
+ if (/^\s*\/\*/.test(rest)) {
107
+ const closeComment = source.indexOf("*/", bodyStart);
108
+ if (closeComment === -1) break;
109
+ const close = source.indexOf("}}", closeComment);
110
+ i = close === -1 ? source.length : close + 2;
111
+ continue;
112
+ }
113
+ const close = source.indexOf("}}", bodyStart);
114
+ if (close === -1) break;
115
+ let body = source.slice(bodyStart, close);
116
+ if (body.endsWith("-")) body = body.slice(0, -1);
117
+ body = body.trim();
118
+ actions.push({ file, line: actionLine, body, kind: kindOf(body) });
119
+ i = close + 2;
120
+ }
121
+ return actions;
122
+ }
123
+
124
+ /**
125
+ * The pipeline of an action, structural keyword and variable assignment
126
+ * stripped: `if X` → `X`, `range $i, $v := X` → `X`, `$pw := X` → `X`.
127
+ * This is what condition evaluation and call-reachability parse.
128
+ */
129
+ export function actionPipeline(action: TemplateAction): string {
130
+ let expr = action.body.replace(KIND_RE, "").trim();
131
+ // `range $i, $v := .Values.x` / `$pw := .Values.x` — the pipeline is what
132
+ // follows the assignment.
133
+ const assign = expr.match(/^\$[\w]*\s*(?:,\s*\$[\w]*\s*)?:?=\s*(.*)$/);
134
+ if (assign) expr = assign[1].trim();
135
+ return expr;
136
+ }
137
+
138
+
139
+ /** `.Values.a.b` (or `$.Values.a.b`) as a path array, else undefined. */
140
+ function valuesPathOf(expr: string): string[] | undefined {
141
+ const m = expr.match(/^\$?\.Values((?:\.[A-Za-z0-9_-]+)+)$/);
142
+ return m ? m[1].slice(1).split(".") : undefined;
143
+ }
144
+
145
+ interface BranchState {
146
+ frame: ScopeFrame;
147
+ /** All branch conditions seen so far in this if-chain. */
148
+ chain: string[];
149
+ }
150
+
151
+ /**
152
+ * Walk a file's actions and attach the enclosing control-flow frames to each.
153
+ * `else` / `else if` branches carry the prior branch conditions as
154
+ * `negatedConditions`. Structure that cannot be tracked (an unbalanced
155
+ * `end`, template text split across files) degrades to fewer frames — which
156
+ * only ever makes the classifier MORE conservative, never less: a missing
157
+ * frame means a gate we cannot prove closed.
158
+ */
159
+ export function scopeActions(actions: TemplateAction[]): ScopedAction[] {
160
+ const scoped: ScopedAction[] = [];
161
+ const stack: BranchState[] = [];
162
+
163
+ for (const action of actions) {
164
+ switch (action.kind) {
165
+ case "if": {
166
+ const cond = actionPipeline(action);
167
+ // The `if` action itself is evaluated in the ENCLOSING scope.
168
+ scoped.push({ action, frames: stack.map((s) => s.frame) });
169
+ stack.push({
170
+ frame: { kind: "if", conditions: [cond], negatedConditions: [] },
171
+ chain: [cond],
172
+ });
173
+ break;
174
+ }
175
+ case "elseif": {
176
+ scoped.push({
177
+ action,
178
+ frames: stack.slice(0, -1).map((s) => s.frame),
179
+ });
180
+ const top = stack[stack.length - 1];
181
+ const cond = action.body.replace(/^else\s+if\b/, "").trim();
182
+ if (top) {
183
+ top.frame = {
184
+ kind: "if",
185
+ conditions: [cond],
186
+ negatedConditions: [...top.chain],
187
+ };
188
+ top.chain.push(cond);
189
+ }
190
+ break;
191
+ }
192
+ case "else": {
193
+ scoped.push({
194
+ action,
195
+ frames: stack.slice(0, -1).map((s) => s.frame),
196
+ });
197
+ const top = stack[stack.length - 1];
198
+ if (top) {
199
+ top.frame = {
200
+ kind: top.frame.kind === "if" ? "if" : top.frame.kind,
201
+ conditions: [],
202
+ negatedConditions: [...top.chain],
203
+ };
204
+ }
205
+ break;
206
+ }
207
+ case "with": {
208
+ const expr = actionPipeline(action);
209
+ scoped.push({ action, frames: stack.map((s) => s.frame) });
210
+ const path = valuesPathOf(expr);
211
+ stack.push({
212
+ frame: {
213
+ kind: "with",
214
+ conditions: [expr],
215
+ negatedConditions: [],
216
+ dotValuesPath: path,
217
+ dotUnresolvable: path === undefined,
218
+ },
219
+ chain: [expr],
220
+ });
221
+ break;
222
+ }
223
+ case "range": {
224
+ const expr = actionPipeline(action);
225
+ scoped.push({ action, frames: stack.map((s) => s.frame) });
226
+ stack.push({
227
+ frame: {
228
+ kind: "range",
229
+ conditions: [expr],
230
+ negatedConditions: [],
231
+ dotUnresolvable: true,
232
+ },
233
+ chain: [expr],
234
+ });
235
+ break;
236
+ }
237
+ case "define":
238
+ case "block": {
239
+ scoped.push({ action, frames: stack.map((s) => s.frame) });
240
+ // A define body runs with whatever context `include`/`template`
241
+ // passes. Most charts pass the root context; the frame records
242
+ // nothing to gate on, so constructs inside a define are treated as
243
+ // reachable unless gated within the define itself.
244
+ stack.push({
245
+ frame: { kind: action.kind, conditions: [], negatedConditions: [] },
246
+ chain: [],
247
+ });
248
+ break;
249
+ }
250
+ case "end": {
251
+ scoped.push({
252
+ action,
253
+ frames: stack.slice(0, -1).map((s) => s.frame),
254
+ });
255
+ stack.pop();
256
+ break;
257
+ }
258
+ default:
259
+ scoped.push({ action, frames: stack.map((s) => s.frame) });
260
+ }
261
+ }
262
+ return scoped;
263
+ }