@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.
- package/dist/archive-render.d.ts +94 -0
- package/dist/archive-render.d.ts.map +1 -0
- package/dist/commands.d.ts +54 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +2 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +54 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +17 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/whm503.d.ts +34 -0
- package/dist/lint/post-synth/whm503.d.ts.map +1 -0
- package/dist/lint/post-synth/whm504.d.ts +20 -0
- package/dist/lint/post-synth/whm504.d.ts.map +1 -0
- package/dist/manifest.json +25 -13
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/WHM503.md +15 -0
- package/dist/okf/rules/WHM504.md +11 -0
- package/dist/okf/types/Values.md +1 -0
- package/dist/op/activities/cluster-probe.d.ts +44 -0
- package/dist/op/activities/cluster-probe.d.ts.map +1 -0
- package/dist/op/activities/helm.d.ts +179 -5
- package/dist/op/activities/helm.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +4 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/pinnability/actions.d.ts +76 -0
- package/dist/pinnability/actions.d.ts.map +1 -0
- package/dist/pinnability/classify.d.ts +129 -0
- package/dist/pinnability/classify.d.ts.map +1 -0
- package/dist/pinnability/conditions.d.ts +109 -0
- package/dist/pinnability/conditions.d.ts.map +1 -0
- package/dist/pinnability/index.d.ts +17 -0
- package/dist/pinnability/index.d.ts.map +1 -0
- package/dist/pinnability/localize.d.ts +124 -0
- package/dist/pinnability/localize.d.ts.map +1 -0
- package/dist/pinnability/render-stream.d.ts +27 -0
- package/dist/pinnability/render-stream.d.ts.map +1 -0
- package/dist/pinnability/values.d.ts +69 -0
- package/dist/pinnability/values.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/release-observe.d.ts +109 -0
- package/dist/release-observe.d.ts.map +1 -0
- package/dist/render-diff.d.ts +192 -0
- package/dist/render-diff.d.ts.map +1 -0
- package/dist/render-digest.d.ts +135 -0
- package/dist/render-digest.d.ts.map +1 -0
- package/dist/render-store.d.ts +310 -0
- package/dist/render-store.d.ts.map +1 -0
- package/dist/render-wrapper.d.ts +140 -0
- package/dist/render-wrapper.d.ts.map +1 -0
- package/dist/render.d.ts +81 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/rules/whm503.ts +101 -0
- package/dist/rules/whm504.ts +52 -0
- package/dist/skills/chant-helm.md +7 -0
- package/dist/values-probe.d.ts +177 -0
- package/dist/values-probe.d.ts.map +1 -0
- package/dist/wrapper-chart.d.ts +61 -0
- package/dist/wrapper-chart.d.ts.map +1 -0
- package/package.json +10 -4
- package/src/archive-render.test.ts +201 -0
- package/src/archive-render.ts +119 -0
- package/src/commands.test.ts +487 -0
- package/src/commands.ts +445 -0
- package/src/config.test.ts +130 -0
- package/src/config.ts +220 -0
- package/src/deep-observe-hooks.ts +20 -0
- package/src/deep-observe.test.ts +165 -0
- package/src/deep-observe.ts +121 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +198 -0
- package/src/index.ts +106 -2
- package/src/lifecycle-integration.test.ts +289 -0
- package/src/lint/audit-catalog.ts +2 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/whm503.test.ts +134 -0
- package/src/lint/post-synth/whm503.ts +101 -0
- package/src/lint/post-synth/whm504.test.ts +105 -0
- package/src/lint/post-synth/whm504.ts +52 -0
- package/src/op/activities/cluster-probe.test.ts +66 -0
- package/src/op/activities/cluster-probe.ts +148 -0
- package/src/op/activities/helm.test.ts +824 -0
- package/src/op/activities/helm.ts +628 -5
- package/src/op/activities/index.ts +17 -2
- package/src/op/builders.test.ts +70 -0
- package/src/op/builders.ts +66 -0
- package/src/pinnability/actions.ts +263 -0
- package/src/pinnability/classify.ts +579 -0
- package/src/pinnability/conditions.ts +0 -0
- package/src/pinnability/index.ts +77 -0
- package/src/pinnability/localize.test.ts +234 -0
- package/src/pinnability/localize.ts +644 -0
- package/src/pinnability/pinnability.test.ts +560 -0
- package/src/pinnability/render-stream.ts +59 -0
- package/src/pinnability/values.ts +205 -0
- package/src/plugin.test.ts +3 -1
- package/src/plugin.ts +97 -12
- package/src/release-observe.ts +392 -0
- package/src/render-diff.test.ts +366 -0
- package/src/render-diff.ts +407 -0
- package/src/render-digest.test.ts +186 -0
- package/src/render-digest.ts +207 -0
- package/src/render-store.test.ts +412 -0
- package/src/render-store.ts +621 -0
- package/src/render-wrapper.test.ts +259 -0
- package/src/render-wrapper.ts +274 -0
- package/src/render.test.ts +383 -4
- package/src/render.ts +319 -8
- package/src/skills/chant-helm.md +7 -0
- package/src/values-probe.test.ts +400 -0
- package/src/values-probe.ts +627 -0
- package/src/wrapper-chart.test.ts +110 -0
- 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
|
+
}
|