Skip to main content

tatara_process/
matrix.rs

1//! `EnvMatrixSpec` — the ephemeral-environment *permutation generator*.
2//!
3//! One `(defenvmatrix …)` declaration fans a single `EphemeralSpec` base out
4//! across a set of named axes into the whole permutation set of environments,
5//! spawned together. This is generation-over-composition (Pillar 12) applied
6//! to environments: author the matrix once, get every variant.
7//!
8//! Each permutation overlays its axis values into the base's
9//! `aplicacao.values_overlay` (or a well-known `@`-target like `@version`),
10//! yielding a distinct canonical spec — so each variant gets its own
11//! deterministic `EphemeralEnvId` (`blake3(spec)[:8]`) and FQDN
12//! (`{app}.{envId}.{cluster}.{location}.{domain}`) for free, via the existing
13//! [`crate::hostname`] machinery. The matrix is workload-agnostic: the base
14//! can install *any* OCI chart, so the same primitive sweeps echo servers,
15//! gateways, migrations, or test suites.
16//!
17//! Lisp authoring:
18//! ```lisp
19//! (defenvmatrix echo-sweep
20//!   :base (:aplicacao (:chart-ref "oci://ghcr.io/pleme-io/charts/echo"
21//!                      :version "0.1.0" :profile "minimal" :values-overlay ())
22//!          :ttl "2h" :teardown Always)
23//!   :axes ((:name "version"  :path "@version"     :values ("0.1.0" "0.2.0"))
24//!          (:name "replicas" :path "replicaCount" :values (1 3))
25//!          (:name "flag"     :path "feature.flag" :values ("on" "off")))
26//!   :select Cartesian
27//!   :budget (:max-envs 12 :cost-ceiling "$5/h")
28//!   :breathe (:dimensions ((:kind "memory" :floor "128Mi" :ceiling "1Gi")
29//!                          (:kind "cpu"    :floor "100m"  :ceiling "1"))
30//!             :cooldown-seconds 60 :dry-run #t))
31//! ```
32//! → 2×2×2 = 8 named `EphemeralSpec`s. `tatara-lispc` renders each as a
33//! `Process` CR plus its breathe Band CRs (one per dimension), so the whole
34//! sweep is cost-bounded under the shared `:budget` and auto-scales within the
35//! `:breathe` floor/ceiling limits.
36
37use std::fmt;
38
39use schemars::JsonSchema;
40use serde::{Deserialize, Serialize};
41use tatara_lisp::DeriveTataraDomain;
42
43use crate::ephemeral::EphemeralSpec;
44
45/// `EnvMatrixSpec` — authors `(defenvmatrix …)`. Expands to a set of named
46/// [`EphemeralSpec`] values via [`EnvMatrixSpec::expand`].
47#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize, JsonSchema)]
48#[serde(rename_all = "camelCase")]
49#[tatara(keyword = "defenvmatrix")]
50pub struct EnvMatrixSpec {
51    /// The base ephemeral environment every permutation is derived from.
52    pub base: EphemeralSpec,
53
54    /// The permutation axes. Each axis ranges over a list of values; the
55    /// generated set is the selection (cartesian by default) over all axes.
56    pub axes: Vec<MatrixAxis>,
57
58    /// Selection strategy over the axes' product. Defaults to `Cartesian`.
59    #[serde(default)]
60    pub select: SelectStrategy,
61
62    /// Shared cost / concurrency budget across the whole sweep. The
63    /// `cost_ceiling` is the envelope handed to breathe so the entire
64    /// permutation set stays cost-bounded.
65    #[serde(default)]
66    pub budget: MatrixBudget,
67
68    /// Optional breathe envelope. When set, [`EnvMatrixSpec::breathe_bands`]
69    /// emits one breathe Band CR per dimension per env, so each generated
70    /// environment auto-scales inside cost-bounded floor/ceiling limits
71    /// (idle-shrink → near-floor, breathe-up on demand). This is how the
72    /// sweep "fully leverages breathability": author the envelope once, every
73    /// permutation inherits it.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub breathe: Option<BreatheEnvelope>,
76}
77
78/// The breathe envelope inherited by every env in a sweep — the per-dimension
79/// homeostasis bounds plus dev-loop-tuned cadence.
80#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
81#[serde(rename_all = "camelCase")]
82pub struct BreatheEnvelope {
83    /// The resource dimensions to band. Each yields a breathe Band CR
84    /// (`MemoryBand` / `CpuBand` / `StorageBand`) per env.
85    pub dimensions: Vec<BreatheDimension>,
86
87    /// Band cooldown in seconds between carves — low (e.g. 60s) for fast
88    /// dev-loop breathe-up/shrink, vs the fleet default.
89    #[serde(default = "default_breathe_cooldown")]
90    pub cooldown_seconds: u64,
91
92    /// Start observe-only (`dryRun`) — breathe reports what it WOULD carve
93    /// without mutating, until the cost SLA is validated. Default `true`
94    /// (safe by default for a fresh sweep).
95    #[serde(default = "crate::serde_defaults::default_true")]
96    pub dry_run: bool,
97
98    /// The workload kind the bands target (default `Deployment`). The band's
99    /// `targetRef.name` is the env's Helm release name (the per-env release).
100    #[serde(default = "default_target_kind")]
101    pub target_kind: String,
102}
103
104/// One banded resource dimension: which breathe Band kind and its bounds.
105#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
106#[serde(rename_all = "camelCase")]
107pub struct BreatheDimension {
108    /// The breathe dimension keyword — `memory`/`mem` → `MemoryBand`,
109    /// `cpu` → `CpuBand`, `storage`/`disk` → `StorageBand`. Case-
110    /// insensitive. Decoded through the typed [`BreatheDimensionKind`]
111    /// closed-set projection: aliases (`mem` / `disk`) decode to the
112    /// SAME variant as the primary keyword, then [`BreatheDimensionKind::
113    /// band_kind`] projects the CR kind and [`BreatheDimensionKind::
114    /// name_segment`] projects the canonical band-name segment so the
115    /// emitted band name (`<env>-{name-segment}`) does NOT depend on
116    /// which alias the operator wrote. Unrecognized keywords drop the
117    /// dimension (no band emitted) — the closed set IS the substrate's
118    /// supported axis set.
119    pub kind: String,
120    /// Floor quantity (the never-shrink-below limit, in the dimension's unit:
121    /// bytes-quantity like `128Mi` for memory/storage, millicores like `100m`
122    /// for cpu). Idle envs shrink toward this.
123    pub floor: String,
124    /// Ceiling quantity (the never-grow-above limit) — the cost-bounding wall.
125    pub ceiling: String,
126}
127
128fn default_breathe_cooldown() -> u64 {
129    60
130}
131fn default_target_kind() -> String {
132    "Deployment".to_string()
133}
134
135/// One permutation axis: a named dimension and the values it ranges over.
136#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
137#[serde(rename_all = "camelCase")]
138pub struct MatrixAxis {
139    /// Axis name — used in the generated env name and as a `matrix-axis/<name>`
140    /// label. Must be a DNS-label-safe token.
141    pub name: String,
142
143    /// Where each value is written on the base spec:
144    /// - A typed [`MatrixTarget`] marker ([`MatrixTarget::Version`] /
145    ///   [`MatrixTarget::Profile`] / [`MatrixTarget::ChartRef`]) → the
146    ///   matching `AplicacaoIntent` field (value must be a string). The
147    ///   path-string surface for each variant is canonicalized by
148    ///   [`MatrixTarget::marker`] (`"@version"` / `"@profile"` /
149    ///   `"@chart-ref"`) and decoded by [`MatrixTarget::from_path`].
150    /// - any other string → a dot-path into `aplicacao.values_overlay`
151    ///   (e.g. `replicaCount`, `image.tag`, `feature.flag`); intermediate
152    ///   objects are created as needed,
153    /// - empty → the value (which must be a JSON object) is merged at the
154    ///   overlay root.
155    #[serde(default, skip_serializing_if = "String::is_empty")]
156    pub path: String,
157
158    /// The values this axis ranges over, as a JSON array (e.g.
159    /// `["v1" "v2"]`, `[1 3]`, `[#t #f]`). Each element is overlaid at
160    /// `path`. A non-array (or empty array) contributes no permutations.
161    pub values: serde_json::Value,
162}
163
164impl MatrixAxis {
165    /// The axis values as a slice (empty if `values` is not a JSON array).
166    fn vals(&self) -> &[serde_json::Value] {
167        self.values.as_array().map(Vec::as_slice).unwrap_or(&[])
168    }
169}
170
171/// How the permutation set is drawn from the axes.
172///
173/// Closed-set discriminator: see [`SelectStrategyKind`] for the
174/// payload-stripped view that drives `as_str` / `Display` / `FromStr`
175/// over [`SelectStrategyKind::ALL`]. Adding a third strategy (e.g.
176/// `Latin` for a Latin-hypercube sweep or `Random { seed, count }`
177/// for a seeded random sample) lands at one variant here + one
178/// [`SelectStrategyKind`] entry + one `kind()` arm + one
179/// [`SelectStrategy::selection_size_for`] arm, exhaustively checked
180/// by the compiler AND by the per-variant truth-table tests below.
181///
182/// Sibling closed-set algebra to every other typed surface on the
183/// `tatara-process` matrix axis: [`MatrixTarget::ALL`],
184/// [`BreatheDimensionKind::ALL`], [`crate::phase::ProcessPhase::ALL`],
185/// [`crate::lifetime::TeardownPolicy::ALL`],
186/// [`crate::lifetime_clock::TerminateReasonKind::ALL`].
187#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
188pub enum SelectStrategy {
189    /// The full cartesian product of every axis. N envs = Π |axisᵢ|.
190    #[default]
191    Cartesian,
192    /// An explicit list of coordinate tuples — one entry per axis, each a
193    /// 0-based index into that axis's `values`. Lets the operator hand-pick
194    /// a sparse subset instead of the full product.
195    Explicit(Vec<Vec<usize>>),
196}
197
198impl SelectStrategy {
199    /// Discriminator projection — strips the payload, yielding the
200    /// closed-set kind. Used by the kind-sweep tests and by any future
201    /// consumer that wants to group strategies by category without
202    /// pattern-matching the full payload (e.g. metrics labels,
203    /// `tatara-check` enumerators, future `status.conditions[].reason`
204    /// reason-keys, LSP completion lists). Sibling shape to
205    /// [`crate::lifetime_clock::TerminateReason::kind`].
206    #[must_use]
207    pub const fn kind(&self) -> SelectStrategyKind {
208        match self {
209            Self::Cartesian => SelectStrategyKind::Cartesian,
210            Self::Explicit(_) => SelectStrategyKind::Explicit,
211        }
212    }
213
214    /// Count the selection size against `axes_lengths` (one entry per
215    /// matrix axis, the count of `values` for that axis), WITHOUT
216    /// materializing the coordinate set. The closed-set dispatch IS
217    /// the lift: for [`Self::Cartesian`] the answer is the product
218    /// of axis lengths (so a 10×10×… sweep counts in O(N) instead of
219    /// allocating the whole product just to call `.len()`); for
220    /// [`Self::Explicit`] it's the count of in-bounds coordinate
221    /// tuples (one in-bounds index per axis).
222    ///
223    /// Routes through [`coords_in_bounds_against`] for the Explicit
224    /// case so the in-bounds check binds at ONE site that the
225    /// [`EnvMatrixSpec::coord_in_bounds`] adapter and the
226    /// [`EnvMatrixSpec::coordinates`] filter also project through —
227    /// a regression that drifts ONE site's bounds-check semantics
228    /// (e.g. starts admitting equal indices) lands at the shared
229    /// helper, not at three byte-identical sites.
230    ///
231    /// Aligns with the pre-lift `cartesian` empty-axes semantics:
232    /// empty `axes_lengths` ⇒ 1 (one empty coord), any zero length
233    /// ⇒ 0 (no env can range over no values), otherwise the product.
234    ///
235    /// Saturating fold so a sweep whose product exceeds `usize::MAX`
236    /// saturates to `usize::MAX` rather than panicking in debug
237    /// builds or silently wrapping in release — either of which
238    /// would mis-cap downstream when [`EnvMatrixSpec::expand`] reads
239    /// the size as a budget hint. Saturation gives the operator a
240    /// deterministic "as many as fit" signal at the top of the
241    /// `usize` range, NOT a wraparound to a small number that would
242    /// silently under-spawn.
243    #[must_use]
244    pub fn selection_size_for(&self, axes_lengths: &[usize]) -> usize {
245        match self {
246            Self::Cartesian => {
247                if axes_lengths.is_empty() {
248                    1
249                } else if axes_lengths.iter().any(|&l| l == 0) {
250                    0
251                } else {
252                    axes_lengths
253                        .iter()
254                        .copied()
255                        .fold(1_usize, usize::saturating_mul)
256                }
257            }
258            Self::Explicit(coords) => coords
259                .iter()
260                .filter(|c| coord_in_bounds_against(axes_lengths, c))
261                .count(),
262        }
263    }
264}
265
266/// True iff `coord` has one in-bounds index per axis (length equals
267/// `axes_lengths.len()`, and every `coord[i] < axes_lengths[i]`).
268/// Free function so the [`SelectStrategy::selection_size_for`] count
269/// path and the [`EnvMatrixSpec::coord_in_bounds`] filter path share
270/// ONE bounds-check definition — pre-lift these would have drifted
271/// independently if either grew an off-by-one or an inclusive-bound
272/// regression.
273#[must_use]
274fn coord_in_bounds_against(axes_lengths: &[usize], coord: &[usize]) -> bool {
275    coord.len() == axes_lengths.len() && coord.iter().zip(axes_lengths).all(|(&i, &l)| i < l)
276}
277
278/// The closed set of [`SelectStrategy`] kinds — the discriminator
279/// view, payload-stripped, that sibling closed-set enums in this
280/// crate carry (see [`crate::lifetime_clock::TerminateReasonKind`],
281/// [`MatrixTarget`], [`BreatheDimensionKind`]).
282///
283/// Drives the `as_str` / `Display` / `FromStr` triad over
284/// [`Self::ALL`] so a new variant added with an `ALL` entry
285/// automatically extends the parser, the canonical wire-format
286/// projection, and any future kind-keyed enumeration that needs
287/// to list the strategy categories (metrics labels, sweep
288/// dashboards, `status.conditions[].reason` keys, LSP completion).
289///
290/// The `as_str` projection pins the PascalCase serde external-tag
291/// form (`"Cartesian"` / `"Explicit"`) so a round-trip through the
292/// JSON wire stays bit-identical with the typed kind name — a
293/// future consumer that reads `serde_json::Value::String(s)` off
294/// the wire and routes through `SelectStrategyKind::from_str(&s)`
295/// gets back the same kind the typed authoring site composed.
296#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
297#[closed_set(via = "as_str", display, generate_unknown)]
298pub enum SelectStrategyKind {
299    /// Full cartesian product of every axis.
300    Cartesian,
301    /// Operator-supplied explicit list of coordinate tuples.
302    Explicit,
303}
304
305impl SelectStrategyKind {
306    /// The closed set — single source of truth for `as_str` / Display /
307    /// `FromStr`. The `[Self; 2]` array literal forces the arity so a
308    /// third variant added without an `ALL` entry fails at the type
309    /// level before the test sweep below runs.
310    pub const ALL: [Self; 2] = [Self::Cartesian, Self::Explicit];
311
312    /// Canonical PascalCase wire-format projection. Mirrors the
313    /// `tatara-process` PascalCase idiom used by every other
314    /// closed-set enum's `as_str` projection (e.g.
315    /// [`crate::lifetime_clock::TerminateReasonKind::as_str`],
316    /// [`crate::lifetime::TeardownPolicy::as_str`]) AND the serde
317    /// external-tag form `SelectStrategy` already serializes through
318    /// — so a round-trip through the JSON wire stays bit-identical
319    /// with the typed kind name.
320    #[must_use]
321    pub const fn as_str(self) -> &'static str {
322        match self {
323            Self::Cartesian => "Cartesian",
324            Self::Explicit => "Explicit",
325        }
326    }
327}
328
329// `impl fmt::Display for SelectStrategyKind` + `impl FromStr for
330// SelectStrategyKind` + `impl tatara_lisp::ClosedSet for
331// SelectStrategyKind` + `pub struct UnknownSelectStrategyKind(pub
332// String)` are generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
333// `#[closed_set(via = "as_str", display, generate_unknown)]` on the
334// enum declaration above. The auto-derived label `"select strategy
335// kind"` matches the prior hand-rolled `#[error("unknown select
336// strategy kind: {0}")]` verbatim. The inherent `as_str` projection
337// stays load-bearing — the PascalCase wire-format that matches the
338// serde external-tag form on `SelectStrategy` verbatim — while the
339// trait method `label` gives generic consumers a STABLE name across
340// the workspace-wide closed-set implementors.
341
342/// Shared budget across the sweep.
343#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)]
344#[serde(rename_all = "camelCase")]
345pub struct MatrixBudget {
346    /// Hard cap on the number of envs the sweep spawns (`0` = no cap). When
347    /// the selection exceeds this, the first `max_envs` (in selection order)
348    /// are kept and the rest are dropped — callers should `log` the drop so
349    /// truncation is never silent.
350    #[serde(default)]
351    pub max_envs: u32,
352
353    /// Cost ceiling for the whole sweep — a free-form budget string (e.g.
354    /// `"$5/h"`). Surfaced to breathe as the shared envelope cost SLA; the
355    /// controller gates how many permutations run concurrently against
356    /// `band.status.observedCostRemaining`.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub cost_ceiling: Option<String>,
359
360    /// Per-env `max_concurrent` override. When set, replaces the base's
361    /// value on every generated spec; when `None`, each variant keeps the
362    /// base's `max_concurrent`.
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub max_concurrent: Option<u32>,
365}
366
367/// A single generated environment: a deterministic name plus the lowered
368/// [`EphemeralSpec`]. The name is `{matrix}-{axis-value}…`; the env's
369/// `EphemeralEnvId` is derived downstream from the spec's canonical hash, so
370/// distinct overlays ⇒ distinct ids ⇒ distinct FQDNs automatically.
371#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
372pub struct NamedEphemeral {
373    /// DNS-label-safe instance name, `{matrix}-{axis-slug}…`.
374    pub name: String,
375    /// The concrete ephemeral spec for this permutation.
376    pub spec: EphemeralSpec,
377}
378
379impl EnvMatrixSpec {
380    /// The selection coordinates (one Vec per generated env; each is one
381    /// 0-based value index per axis), before the `max_envs` cap.
382    pub fn coordinates(&self) -> Vec<Vec<usize>> {
383        match &self.select {
384            SelectStrategy::Cartesian => {
385                let lengths: Vec<usize> = self.axes.iter().map(|a| a.vals().len()).collect();
386                cartesian(&lengths)
387            }
388            SelectStrategy::Explicit(coords) => coords
389                .iter()
390                .filter(|c| self.coord_in_bounds(c))
391                .cloned()
392                .collect(),
393        }
394    }
395
396    /// True iff `coord` has one in-bounds index per axis. Routes
397    /// through [`coord_in_bounds_against`] so the bounds check binds
398    /// at ONE site that [`SelectStrategy::selection_size_for`] also
399    /// projects through — a regression that drifts the bounds-check
400    /// semantics (e.g. an off-by-one or an inclusive-bound flip) lands
401    /// at the shared helper rather than at two byte-identical sites.
402    fn coord_in_bounds(&self, coord: &[usize]) -> bool {
403        let lengths: Vec<usize> = self.axes.iter().map(|a| a.vals().len()).collect();
404        coord_in_bounds_against(&lengths, coord)
405    }
406
407    /// How many environments this matrix *would* generate before the
408    /// `max_envs` cap (the full selection size). Routes through
409    /// [`SelectStrategy::selection_size_for`] so the count is read
410    /// off the typed strategy projection WITHOUT materializing the
411    /// coordinate set — a 10-axis Cartesian sweep over 10 values
412    /// each counts in O(N) instead of allocating a 10-billion-entry
413    /// `Vec<Vec<usize>>` just to call `.len()`. Pinned by
414    /// `selection_size_avoids_materializing_cartesian_product`.
415    pub fn selection_size(&self) -> usize {
416        let lengths: Vec<usize> = self.axes.iter().map(|a| a.vals().len()).collect();
417        self.select.selection_size_for(&lengths)
418    }
419
420    /// Expand the matrix into the concrete, capped set of named ephemeral
421    /// specs. `matrix_name` is the `(defenvmatrix <name> …)` name — the prefix
422    /// for every generated env name.
423    pub fn expand(&self, matrix_name: &str) -> Vec<NamedEphemeral> {
424        let coords = self.coordinates();
425        let capped: Vec<Vec<usize>> = match self.budget.max_envs {
426            0 => coords,
427            n => coords.into_iter().take(n as usize).collect(),
428        };
429        capped
430            .into_iter()
431            .map(|coord| self.materialize(matrix_name, &coord))
432            .collect()
433    }
434
435    /// Build one variant from a coordinate.
436    fn materialize(&self, matrix_name: &str, coord: &[usize]) -> NamedEphemeral {
437        let mut spec = self.base.clone();
438        let mut suffix = Vec::with_capacity(coord.len());
439        for (axis_idx, &val_idx) in coord.iter().enumerate() {
440            let axis = &self.axes[axis_idx];
441            let val = axis
442                .vals()
443                .get(val_idx)
444                .cloned()
445                .unwrap_or(serde_json::Value::Null);
446            apply_axis(&mut spec, &axis.path, val.clone());
447            suffix.push(format!("{}-{}", slug(&axis.name), slug_value(&val)));
448        }
449        if let Some(mc) = self.budget.max_concurrent {
450            spec.max_concurrent = mc;
451        }
452        let name = if suffix.is_empty() {
453            matrix_name.to_string()
454        } else {
455            format!("{}-{}", matrix_name, suffix.join("-"))
456        };
457        // Each permutation is a distinct Helm release named for the env, so the
458        // variants coexist and breathe bands can target each one by name.
459        spec.aplicacao.release_name = Some(name.clone());
460        NamedEphemeral { name, spec }
461    }
462
463    /// Emit the breathe Band CRs for one generated env — one per envelope
464    /// dimension (`MemoryBand` / `CpuBand` / `StorageBand`). Empty when no
465    /// `:breathe` envelope is declared. Each band targets the env's Helm
466    /// release (a `Deployment` by default) and inherits the sweep's
467    /// `:budget :cost-ceiling` as a `breathe.pleme.io/cost-ceiling` annotation,
468    /// so the controller gates the whole sweep against one cost budget.
469    pub fn breathe_bands(&self, env: &NamedEphemeral) -> Vec<serde_json::Value> {
470        let Some(envelope) = &self.breathe else {
471            return vec![];
472        };
473        let target_name = env.spec.aplicacao.release_name_or(&env.name);
474        let namespace = env.spec.aplicacao.target_namespace_or(&env.name);
475        let mut annotations = serde_json::Map::new();
476        if let Some(ceiling) = &self.budget.cost_ceiling {
477            annotations.insert(
478                "breathe.pleme.io/cost-ceiling".to_string(),
479                serde_json::Value::String(ceiling.clone()),
480            );
481        }
482        envelope
483            .dimensions
484            .iter()
485            .filter_map(|dim| {
486                let kind = BreatheDimensionKind::from_keyword(&dim.kind)?;
487                Some(serde_json::json!({
488                    "apiVersion": "breathe.pleme.io/v1",
489                    "kind": kind.band_kind(),
490                    "metadata": {
491                        "name": format!("{}-{}", env.name, kind.name_segment()),
492                        "namespace": namespace,
493                        "labels": { "matrix-env": env.name },
494                        "annotations": annotations,
495                    },
496                    "spec": {
497                        "targetRef": { "kind": envelope.target_kind, "name": target_name },
498                        "floor": dim.floor,
499                        "ceiling": dim.ceiling,
500                        "cooldownSeconds": envelope.cooldown_seconds,
501                        "dryRun": envelope.dry_run,
502                    },
503                }))
504            })
505            .collect()
506    }
507}
508
509/// Closed-set typed identifier for the three reachable breathe Band CR
510/// kinds a [`BreatheDimension::kind`] keyword can target — [`Self::Memory`]
511/// → `MemoryBand`, [`Self::Cpu`] → `CpuBand`, [`Self::Storage`] →
512/// `StorageBand` — as a Rust enum, so the (keyword-set, CR-kind,
513/// name-segment) triple binds at ONE site on the typed algebra rather
514/// than at three byte-identical string-literal sites scattered across
515/// [`EnvMatrixSpec::breathe_bands`] and the deleted `band_kind_for`
516/// helper.
517///
518/// Pre-lift the dispatch lived as a string-input / `&'static str`-output
519/// `band_kind_for` helper paired with an inline
520/// `dim.kind.to_ascii_lowercase()` composing the band's metadata-name
521/// segment. The two arms of the pairing did NOT canonicalize together:
522/// `band_kind_for("mem")` and `band_kind_for("memory")` both projected
523/// to `"MemoryBand"`, but the inline name-segment site echoed the
524/// operator's raw alias (`<env>-mem` vs `<env>-memory`), so a single
525/// matrix sweep that wrote one dimension as `"mem"` and another as
526/// `"memory"` produced two bands with drift-shaped names and no compile
527/// or runtime signal that the names depended on operator-side alias
528/// choice. Post-lift the pairing binds at ONE typed projection
529/// ([`Self::band_kind`] + [`Self::name_segment`]) — both the CR kind
530/// AND the name segment derive from the same closed-set variant, so
531/// every alias canonicalizes to ONE band-name shape regardless of how
532/// the operator spelled the dimension keyword.
533///
534/// Adding a fourth dimension (e.g. `Network` → `NetworkBand`,
535/// name-segment `"network"`) extends the enum AND the three projection
536/// arms ([`Self::from_keyword`], [`Self::band_kind`],
537/// [`Self::name_segment`]) in lockstep — rustc binds the extension
538/// through exhaustiveness over the closed enum so a partial extension
539/// that forgets ONE projection becomes a compile error rather than a
540/// runtime drift where the new band-kind projects but the name-segment
541/// falls back to the raw keyword.
542///
543/// Sibling closed-set lift to this file's [`MatrixTarget`]
544/// (three-of-three magic-target identifier on the same `EphemeralSpec`
545/// algebra) and to tatara-lisp's `QuoteForm` (four-of-four homoiconic
546/// prefix-wrappers), `UnquoteForm` (two-of-four template-marker
547/// subset), `MacroDefHead` (two-of-two macro-definition heads), and
548/// `CompilerSpecIoStage` (disk-persistence surface) closed-set
549/// algebras: those enums key their respective dispatch / projection
550/// variants on a typed identity carried inside the variant; this enum
551/// keys the three reachable breathe-dimension CR-kind / name-segment
552/// pairs on a typed marker identity.
553///
554/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set
555/// of breathe-dimension keywords becomes a TYPE rather than three
556/// `&'static str` literals at one site and a raw-keyword `to_ascii_
557/// lowercase()` at another. A typo in any arm becomes a compile error
558/// against the typed projection. THEORY.md §VI.1 — generation over
559/// composition; the (keyword-set, CR-kind, name-segment) triple was
560/// load-bearing across two sites yet enforced by per-site call-site
561/// discipline — past the ≥2 PRIME-DIRECTIVE trigger once the
562/// structural shape is named.
563#[derive(Clone, Copy, Debug, PartialEq, Eq)]
564pub enum BreatheDimensionKind {
565    /// `memory` / `mem` → `MemoryBand`, name-segment `"memory"`.
566    Memory,
567    /// `cpu` → `CpuBand`, name-segment `"cpu"`.
568    Cpu,
569    /// `storage` / `disk` → `StorageBand`, name-segment `"storage"`.
570    Storage,
571}
572
573impl BreatheDimensionKind {
574    /// The closed set of breathe dimensions — single source of truth
575    /// that drives the [`Self::from_keyword`] decode sweep AND the
576    /// [`Self::name_segment`] projection through [`Self::aliases`].
577    /// Adding a fourth dimension (e.g. `Network` → `NetworkBand`,
578    /// name-segment `"network"`) lands at one `ALL` entry + one
579    /// `aliases` arm + one `band_kind` arm, exhaustively checked by
580    /// the compiler (the `[Self; 3]` array literal forces the arity)
581    /// AND by the per-variant truth-table tests below. Sibling
582    /// closed-set lift to every other `ALL`-keyed enum in this crate
583    /// including [`MatrixTarget::ALL`] (the file-local closed-set
584    /// peer), [`crate::phase::ProcessPhase::ALL`],
585    /// [`crate::intent::IntentKind::ALL`],
586    /// [`crate::signal::ProcessSignal::ALL`],
587    /// [`crate::boundary::ConditionKind::ALL`],
588    /// [`crate::lifetime::TeardownPolicy::ALL`], and
589    /// [`crate::classification::SubstrateType::ALL`]; having ONE
590    /// enumeration site means future LSP-completion, `tatara-check`
591    /// breathe-dimension enumeration, and exhaustive bidirection
592    /// sweeps project through this constant rather than re-listing
593    /// the variants at every consumer.
594    pub const ALL: [Self; 3] = [Self::Memory, Self::Cpu, Self::Storage];
595
596    /// The closed alias set this variant accepts at the
597    /// [`BreatheDimension::kind`] boundary (lowercase). Slot 0 IS the
598    /// canonical name segment — [`Self::name_segment`] reads
599    /// `aliases()[0]` so the canonical-name and alias-set pairing
600    /// binds at ONE site rather than at TWO sites (a `from_keyword`
601    /// alias-union arm AND a `name_segment` literal arm per variant).
602    /// Pre-lift the (alias-set, canonical-name) pairing was load-
603    /// bearing across two methods yet enforced by per-site call-site
604    /// discipline — a regression that renames the [`Self::Memory`]
605    /// canonical from `"memory"` → `"ram"` in [`Self::name_segment`]
606    /// without updating the [`Self::from_keyword`] arm silently
607    /// desynchronizes the two sites (band names track the canonical
608    /// but decode keeps accepting only `"memory"` / `"mem"`). Post-
609    /// lift the rename lands at ONE [`Self::aliases`] arm and
610    /// [`Self::name_segment`] + [`Self::from_keyword`] automatically
611    /// track because both project through this method.
612    ///
613    /// Every alias is lowercase; callers MUST lowercase their input
614    /// before comparing (the [`Self::from_keyword`] sweep does this
615    /// once at the top of its loop). The slice MUST be non-empty —
616    /// `name_segment()` panics on empty; the
617    /// `breathe_dimension_kind_aliases_nonempty_for_every_variant`
618    /// truth-table test pins the contract.
619    #[must_use]
620    pub fn aliases(self) -> &'static [&'static str] {
621        match self {
622            Self::Memory => &["memory", "mem"],
623            Self::Cpu => &["cpu"],
624            Self::Storage => &["storage", "disk"],
625        }
626    }
627
628    /// Decode a [`BreatheDimension::kind`] keyword (case-insensitive)
629    /// into the typed marker, or `None` for keywords that aren't in
630    /// the closed set (they fall through to the `filter_map` drop in
631    /// [`EnvMatrixSpec::breathe_bands`] — the dimension contributes
632    /// no band). Closed-set primary inverse of [`Self::band_kind`]
633    /// and [`Self::name_segment`]: every primary / alias keyword for
634    /// a variant decodes back to that variant. Lifted onto a linear
635    /// search across [`Self::ALL`] keyed on [`Self::aliases`] so the
636    /// canonical lowercase alias literals live at ONE site (the
637    /// `aliases` arms) rather than at TWO sites (a `from_keyword`
638    /// alias-union arm AND a `name_segment` literal arm per variant)
639    /// — adding a fourth dimension extends only `ALL` + `aliases` +
640    /// `band_kind`, NOT a third per-variant literal site.
641    #[must_use]
642    pub fn from_keyword(kw: &str) -> Option<Self> {
643        let lower = kw.to_ascii_lowercase();
644        Self::ALL
645            .into_iter()
646            .find(|t| t.aliases().iter().any(|a| *a == lower))
647    }
648
649    /// Canonical breathe Band CR kind — the `kind:` field on the
650    /// emitted Band CR (`MemoryBand` / `CpuBand` / `StorageBand`).
651    /// Projects through `&'static str` (no allocation) so consumers
652    /// (the `breathe_bands` emitter, future CRD discovery, future
653    /// LSP completion lists) compose with the same shape
654    /// `tatara_lisp`'s `QuoteForm::prefix` / `MatrixTarget::marker`
655    /// closed-set surfaces use.
656    #[must_use]
657    pub fn band_kind(self) -> &'static str {
658        match self {
659            Self::Memory => "MemoryBand",
660            Self::Cpu => "CpuBand",
661            Self::Storage => "StorageBand",
662        }
663    }
664
665    /// Canonical lower-case keyword used as the band metadata-name
666    /// segment (`{env-name}-{name-segment}`). Pinned to the variant
667    /// rather than echoed from the operator-side alias: a sweep that
668    /// declares `(:kind "mem" …)` produces `<env>-memory`, NOT
669    /// `<env>-mem` — every alias funnels to ONE deterministic band
670    /// name so two dimensions written with two different aliases
671    /// (`"mem"` and `"memory"`) cannot collide-by-shape into
672    /// indistinguishable band names; the typed projection is the
673    /// canonical-name boundary the substrate's deterministic-output
674    /// posture relies on. Projects through [`Self::aliases`] slot 0
675    /// so the canonical name + accepted-alias set live at ONE source
676    /// of truth — a future variant adds ONE `aliases` arm and the
677    /// canonical name is automatically the first entry.
678    #[must_use]
679    pub fn name_segment(self) -> &'static str {
680        self.aliases()[0]
681    }
682}
683
684/// Closed-set typed identifier for the `@`-prefixed magic targets a
685/// [`MatrixAxis::path`] can write into on the base [`EphemeralSpec`] — the
686/// three reachable aplicacao-field write targets ([`Self::Version`] →
687/// `aplicacao.version`, [`Self::Profile`] → `aplicacao.profile`,
688/// [`Self::ChartRef`] → `aplicacao.chart_ref`) — as a Rust enum, so the
689/// three-way (path-string, aplicacao-field) pairing binds at ONE site on
690/// the typed algebra rather than at three byte-identical inline arms in
691/// [`apply_axis`].
692///
693/// Pre-lift the magic-target dispatch lived as three arms in
694/// [`apply_axis`], each opening its own `val.as_str().to_string() →
695/// field = …` skeleton paired with its own `&'static str` literal arm
696/// label. The (path-literal, aplicacao-field) pairing was load-bearing
697/// across three sites yet only enforced by call-site discipline — a
698/// regression that swapped two assignment targets (e.g. routed
699/// `"@version"` to `chart_ref`) type-checked but silently mis-applied
700/// every operator's matrix sweep. Post-lift the pairing binds at ONE
701/// typed projection ([`Self::apply`]) the substrate's invariant relies on:
702/// rustc's closed-set match across [`Self`] enforces that every variant
703/// has exactly one apply arm and exactly one [`Self::marker`] arm, and
704/// the bidirectional contract `from_path(t.marker()) == Some(t)` makes the
705/// decode + canonical-marker round-trip a TYPE rather than three string
706/// literals scattered across the file.
707///
708/// Adding a fourth magic-target (e.g. `@release-name` →
709/// `aplicacao.release_name`, `@target-namespace` →
710/// `aplicacao.target_namespace`) extends the enum AND the three
711/// projection arms ([`Self::from_path`], [`Self::marker`], [`Self::apply`])
712/// in lockstep — rustc binds the extension through exhaustiveness over
713/// the closed enum so a partial extension that forgets ONE projection
714/// becomes a compile error rather than a runtime drift.
715///
716/// Sibling closed-set lift to tatara-lisp's `QuoteForm` (four-of-four
717/// homoiconic prefix-wrappers), `UnquoteForm` (two-of-four template-
718/// marker subset), `MacroDefHead` (two-of-two macro-definition heads),
719/// and `CompilerSpecIoStage` (disk-persistence surface) closed-set
720/// algebras: those enums key their respective dispatch / projection
721/// variants on a typed identity carried inside the variant; this enum
722/// keys the three reachable matrix-axis aplicacao-field write targets
723/// on a typed marker identity.
724///
725/// Theory anchor: THEORY.md §V.1 — knowable platform; the closed set of
726/// `@`-prefixed magic targets becomes a TYPE rather than three
727/// `&'static str` literals scattered across [`apply_axis`]. A typo in
728/// any arm becomes a compile error against the typed projection.
729/// THEORY.md §VI.1 — generation over composition; the (path-literal,
730/// aplicacao-field) pairing appeared at three arms — past the ≥2
731/// PRIME-DIRECTIVE trigger once the structural shape is named.
732/// THEORY.md §II.1 invariant 1 — typed entry; the matrix-axis
733/// path-string to typed-target decoding IS the typed-entry gate at the
734/// `MatrixAxis::path` boundary, and naming the closed-set identity
735/// lifts the gate from per-site literal discipline to ONE method
736/// the substrate's diagnostic promotions hang off of.
737#[derive(Clone, Copy, Debug, PartialEq, Eq)]
738pub enum MatrixTarget {
739    /// `@version` → `aplicacao.version`.
740    Version,
741    /// `@profile` → `aplicacao.profile`.
742    Profile,
743    /// `@chart-ref` → `aplicacao.chart_ref`.
744    ChartRef,
745}
746
747impl MatrixTarget {
748    /// The closed set of magic targets — single source of truth that
749    /// drives the `marker` / Display / `from_path` triad AND the
750    /// per-variant `apply` arm. Adding a fourth magic target (e.g.
751    /// `@release-name` → `aplicacao.release_name`, `@target-namespace`
752    /// → `aplicacao.target_namespace`) lands at one `ALL` entry + one
753    /// `marker` arm + one `apply` arm, exhaustively checked by the
754    /// compiler (the `[Self; 3]` array literal forces the arity) AND
755    /// by the per-variant bidirection + apply truth-table tests
756    /// below. Sibling closed-set lift to every other `ALL`-keyed enum
757    /// in this crate including [`crate::phase::ProcessPhase::ALL`],
758    /// [`crate::intent::IntentKind::ALL`],
759    /// [`crate::signal::ProcessSignal::ALL`],
760    /// [`crate::boundary::ConditionKind::ALL`],
761    /// [`crate::lifetime::TeardownPolicy::ALL`], and
762    /// [`crate::classification::SubstrateType::ALL`]; having ONE
763    /// enumeration site means future LSP-completion, `tatara-check`
764    /// magic-target enumeration, and exhaustive bidirection sweeps
765    /// project through this constant rather than re-listing the
766    /// variants at every consumer.
767    pub const ALL: [Self; 3] = [Self::Version, Self::Profile, Self::ChartRef];
768
769    /// Decode a [`MatrixAxis::path`] string into the typed marker, or
770    /// `None` for paths that aren't reserved `@`-prefixed magic targets
771    /// (they fall through to [`overlay_at_path`] semantics inside
772    /// [`apply_axis`]). Closed-set dual of [`Self::marker`]: for every
773    /// variant `t`, `from_path(t.marker()) == Some(t)`. Lifted onto a
774    /// linear search across [`Self::ALL`] keyed on [`Self::marker`] so
775    /// the canonical `@`-prefixed string literals live at ONE site
776    /// (the `marker` arms) rather than at TWO sites (a `from_path`
777    /// arm AND a `marker` arm per variant) — adding a fourth magic
778    /// target extends only `ALL` + the `marker` arm + the `apply`
779    /// arm, NOT a third per-variant literal site.
780    #[must_use]
781    pub fn from_path(path: &str) -> Option<Self> {
782        Self::ALL.into_iter().find(|t| t.marker() == path)
783    }
784
785    /// Canonical `&'static str` marker — the `@`-prefixed path literal
786    /// each variant decodes from. Bidirectional dual of [`Self::from_path`]:
787    /// for every variant `t`, `from_path(t.marker()) == Some(t)`. The
788    /// `&'static str` lifetime lets consumers (axis-path docstrings,
789    /// future `tatara-check` typed-target enumerators, future LSP
790    /// completion lists) project through this method without an
791    /// allocation, parallel to how `tatara_lisp`'s `QuoteForm::prefix`
792    /// / `UnquoteForm::marker` / `CompilerSpecIoStage::operation`
793    /// project their closed-set surfaces.
794    #[must_use]
795    pub fn marker(self) -> &'static str {
796        match self {
797            Self::Version => "@version",
798            Self::Profile => "@profile",
799            Self::ChartRef => "@chart-ref",
800        }
801    }
802
803    /// Apply a string value into the targeted [`AplicacaoIntent`] field
804    /// on `spec.aplicacao`. Non-string values are silently ignored —
805    /// matching the pre-lift [`apply_axis`] posture (the magic-target
806    /// arms only acted when `val.as_str()` succeeded; non-string axis
807    /// values for a magic target are dropped, NOT routed to the
808    /// overlay). The (variant, field-assignment) pairing binds at ONE
809    /// match arm rather than three byte-identical sites — a regression
810    /// that drifts ONE arm's field target (e.g. routes
811    /// [`Self::Version`] to `chart_ref`) becomes a compile error
812    /// against the typed projection.
813    pub fn apply(self, spec: &mut EphemeralSpec, val: &serde_json::Value) {
814        let Some(s) = val.as_str() else {
815            return;
816        };
817        let s = s.to_string();
818        match self {
819            Self::Version => spec.aplicacao.version = s,
820            Self::Profile => spec.aplicacao.profile = s,
821            Self::ChartRef => spec.aplicacao.chart_ref = s,
822        }
823    }
824}
825
826impl fmt::Display for MatrixTarget {
827    /// Project the variant through [`Self::marker`] — the canonical
828    /// `@`-prefixed path literal each variant decodes from. Pinned by
829    /// `matrix_target_display_matches_marker_for_every_variant` so a
830    /// future Display impl can't drift from the canonical marker (the
831    /// posture every sibling-closed-set Display in this crate carries,
832    /// e.g. [`crate::lifetime_clock::TerminateReason`],
833    /// [`crate::classification::Arity`]).
834    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
835        f.write_str(self.marker())
836    }
837}
838
839/// Apply one axis value to a spec at the axis's path. Routes through the
840/// substrate's [`MatrixTarget`] closed-set dispatch — `@`-prefixed magic
841/// targets bind through [`MatrixTarget::from_path`] + [`MatrixTarget::apply`]
842/// at ONE typed site rather than three inline match arms; everything
843/// else falls through to [`overlay_at_path`].
844fn apply_axis(spec: &mut EphemeralSpec, path: &str, val: serde_json::Value) {
845    if let Some(target) = MatrixTarget::from_path(path) {
846        target.apply(spec, &val);
847    } else {
848        overlay_at_path(&mut spec.aplicacao.values_overlay, path, val);
849    }
850}
851
852/// Cartesian product of axis lengths → mixed-radix coordinate list. Any
853/// zero-length axis yields the empty set (no env can range over no values).
854fn cartesian(lengths: &[usize]) -> Vec<Vec<usize>> {
855    if lengths.is_empty() {
856        return vec![vec![]];
857    }
858    if lengths.iter().any(|&l| l == 0) {
859        return vec![];
860    }
861    let total: usize = lengths.iter().product();
862    (0..total)
863        .map(|n| {
864            let mut rem = n;
865            lengths
866                .iter()
867                .map(|&l| {
868                    let d = rem % l;
869                    rem /= l;
870                    d
871                })
872                .collect()
873        })
874        .collect()
875}
876
877/// Set `val` into a JSON object at a dot-path, creating intermediate objects.
878/// Empty path merges an object value at the root.
879fn overlay_at_path(root: &mut serde_json::Value, path: &str, val: serde_json::Value) {
880    if !root.is_object() {
881        *root = serde_json::Value::Object(Default::default());
882    }
883    if path.is_empty() {
884        if let Some(obj) = val.as_object() {
885            let r = root.as_object_mut().expect("root is object");
886            for (k, v) in obj {
887                r.insert(k.clone(), v.clone());
888            }
889        }
890        return;
891    }
892    let parts: Vec<&str> = path.split('.').collect();
893    let mut cur = root;
894    for part in &parts[..parts.len() - 1] {
895        if !cur.is_object() {
896            *cur = serde_json::Value::Object(Default::default());
897        }
898        cur = cur
899            .as_object_mut()
900            .expect("object")
901            .entry((*part).to_string())
902            .or_insert_with(|| serde_json::Value::Object(Default::default()));
903    }
904    if !cur.is_object() {
905        *cur = serde_json::Value::Object(Default::default());
906    }
907    cur.as_object_mut()
908        .expect("object")
909        .insert(parts[parts.len() - 1].to_string(), val);
910}
911
912/// DNS-label-safe slug of a token: lowercase, non-`[a-z0-9-]` → `-`, collapse
913/// runs, trim leading/trailing `-`. Empty → `"x"`.
914fn slug(s: &str) -> String {
915    let mut out = String::with_capacity(s.len());
916    let mut prev_dash = false;
917    for c in s.chars() {
918        let c = c.to_ascii_lowercase();
919        if c.is_ascii_alphanumeric() {
920            out.push(c);
921            prev_dash = false;
922        } else if !prev_dash {
923            out.push('-');
924            prev_dash = true;
925        }
926    }
927    let trimmed = out.trim_matches('-');
928    if trimmed.is_empty() {
929        "x".to_string()
930    } else {
931        trimmed.to_string()
932    }
933}
934
935/// Slug of a JSON value (string content, number, or bool) for env names.
936fn slug_value(v: &serde_json::Value) -> String {
937    match v {
938        serde_json::Value::String(s) => slug(s),
939        serde_json::Value::Bool(b) => b.to_string(),
940        serde_json::Value::Number(n) => slug(&n.to_string()),
941        other => slug(&other.to_string()),
942    }
943}
944
945/// Compile a `(defenvmatrix …)` Lisp source into named `EnvMatrixSpec` values.
946pub fn compile_env_matrix_source(
947    src: &str,
948) -> tatara_lisp::Result<Vec<tatara_lisp::NamedDefinition<EnvMatrixSpec>>> {
949    tatara_lisp::compile_named::<EnvMatrixSpec>(src)
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955    use crate::crd::ProcessSpec;
956    use crate::intent::{AplicacaoIntent, IntentVariant};
957    use crate::lifetime::TeardownPolicy;
958
959    fn base() -> EphemeralSpec {
960        EphemeralSpec {
961            aplicacao: AplicacaoIntent {
962                chart_ref: "oci://ghcr.io/pleme-io/charts/echo".into(),
963                version: "0.1.0".into(),
964                profile: "minimal".into(),
965                values_overlay: serde_json::json!({}),
966                release_name: None,
967                target_namespace: None,
968                install_timeout: None,
969            },
970            ttl: "2h".into(),
971            teardown: TeardownPolicy::Always,
972            max_concurrent: 1,
973            postconditions: vec![],
974            preconditions: vec![],
975            verify_timeout: None,
976            classification: None,
977            parent: None,
978            exports: vec![],
979            routing: None,
980        }
981    }
982
983    fn matrix() -> EnvMatrixSpec {
984        EnvMatrixSpec {
985            base: base(),
986            axes: vec![
987                MatrixAxis {
988                    name: "version".into(),
989                    path: "@version".into(),
990                    values: serde_json::json!(["0.1.0", "0.2.0"]),
991                },
992                MatrixAxis {
993                    name: "replicas".into(),
994                    path: "replicaCount".into(),
995                    values: serde_json::json!([1, 3]),
996                },
997                MatrixAxis {
998                    name: "flag".into(),
999                    path: "feature.flag".into(),
1000                    values: serde_json::json!(["on", "off"]),
1001                },
1002            ],
1003            select: SelectStrategy::Cartesian,
1004            budget: MatrixBudget::default(),
1005            breathe: None,
1006        }
1007    }
1008
1009    #[test]
1010    fn cartesian_count_is_product_of_axes() {
1011        let m = matrix();
1012        assert_eq!(m.selection_size(), 2 * 2 * 2);
1013        let envs = m.expand("echo-sweep");
1014        assert_eq!(envs.len(), 8);
1015    }
1016
1017    #[test]
1018    fn each_permutation_overlays_its_axis_values() {
1019        let envs = matrix().expand("echo-sweep");
1020        // Find the v0.2.0 / replicas=3 / flag=off variant.
1021        let target = envs
1022            .iter()
1023            .find(|e| {
1024                e.spec.aplicacao.version == "0.2.0"
1025                    && e.spec.aplicacao.values_overlay["replicaCount"] == 3
1026                    && e.spec.aplicacao.values_overlay["feature"]["flag"] == "off"
1027            })
1028            .expect("the v0.2.0/3/off permutation exists");
1029        // Name carries the axis slugs.
1030        assert!(target.name.starts_with("echo-sweep-"));
1031        assert!(target.name.contains("version-0-2-0"));
1032        assert!(target.name.contains("replicas-3"));
1033        assert!(target.name.contains("flag-off"));
1034    }
1035
1036    #[test]
1037    fn names_are_unique_and_dns_safe() {
1038        let envs = matrix().expand("echo-sweep");
1039        let names: std::collections::BTreeSet<_> = envs.iter().map(|e| e.name.as_str()).collect();
1040        assert_eq!(names.len(), envs.len(), "all names distinct");
1041        for e in &envs {
1042            assert!(
1043                e.name
1044                    .chars()
1045                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
1046                "name {} is DNS-safe",
1047                e.name
1048            );
1049        }
1050    }
1051
1052    #[test]
1053    fn max_envs_caps_and_overrides_concurrency() {
1054        let mut m = matrix();
1055        m.budget.max_envs = 3;
1056        m.budget.max_concurrent = Some(5);
1057        let envs = m.expand("echo-sweep");
1058        assert_eq!(envs.len(), 3);
1059        assert!(envs.iter().all(|e| e.spec.max_concurrent == 5));
1060    }
1061
1062    #[test]
1063    fn explicit_selection_picks_a_subset() {
1064        let mut m = matrix();
1065        // Just two hand-picked corners of the cube.
1066        m.select = SelectStrategy::Explicit(vec![vec![0, 0, 0], vec![1, 1, 1]]);
1067        let envs = m.expand("echo-sweep");
1068        assert_eq!(envs.len(), 2);
1069        assert_eq!(envs[0].spec.aplicacao.version, "0.1.0");
1070        assert_eq!(envs[1].spec.aplicacao.version, "0.2.0");
1071    }
1072
1073    #[test]
1074    fn each_variant_lowers_to_a_process_spec() {
1075        let envs = matrix().expand("echo-sweep");
1076        for e in &envs {
1077            let ps: ProcessSpec = e.spec.clone().into();
1078            assert!(matches!(
1079                ps.intent.variant().unwrap(),
1080                IntentVariant::Aplicacao(_)
1081            ));
1082        }
1083    }
1084
1085    #[test]
1086    fn breathe_envelope_emits_bands_per_env() {
1087        let mut m = matrix();
1088        m.budget.cost_ceiling = Some("$5/h".into());
1089        m.breathe = Some(BreatheEnvelope {
1090            dimensions: vec![
1091                BreatheDimension {
1092                    kind: "memory".into(),
1093                    floor: "128Mi".into(),
1094                    ceiling: "1Gi".into(),
1095                },
1096                BreatheDimension {
1097                    kind: "cpu".into(),
1098                    floor: "100m".into(),
1099                    ceiling: "1".into(),
1100                },
1101            ],
1102            cooldown_seconds: 60,
1103            dry_run: true,
1104            target_kind: "Deployment".into(),
1105        });
1106        let envs = m.expand("echo-sweep");
1107        let env0 = &envs[0];
1108        let bands = m.breathe_bands(env0);
1109        assert_eq!(bands.len(), 2, "one band per dimension");
1110        let mem = &bands[0];
1111        assert_eq!(mem["kind"], "MemoryBand");
1112        assert_eq!(mem["spec"]["targetRef"]["kind"], "Deployment");
1113        // The band targets the env's per-env Helm release (= env name).
1114        assert_eq!(mem["spec"]["targetRef"]["name"], env0.name.as_str());
1115        assert_eq!(mem["spec"]["floor"], "128Mi");
1116        assert_eq!(mem["spec"]["ceiling"], "1Gi");
1117        assert_eq!(mem["spec"]["dryRun"], true);
1118        // The sweep's cost budget rides on every band as an annotation.
1119        assert_eq!(
1120            mem["metadata"]["annotations"]["breathe.pleme.io/cost-ceiling"],
1121            "$5/h"
1122        );
1123        assert_eq!(bands[1]["kind"], "CpuBand");
1124
1125        // No envelope ⇒ no bands.
1126        let m2 = matrix();
1127        assert!(m2.breathe_bands(&m2.expand("x")[0]).is_empty());
1128    }
1129
1130    #[test]
1131    fn overlay_at_nested_path_creates_intermediate_objects() {
1132        let mut root = serde_json::json!({"existing": 1});
1133        overlay_at_path(&mut root, "a.b.c", serde_json::json!("v"));
1134        assert_eq!(root["a"]["b"]["c"], "v");
1135        assert_eq!(root["existing"], 1, "existing keys preserved");
1136    }
1137
1138    // ── MatrixTarget: closed-set magic-target dispatch ──────────────
1139    //
1140    // The three `@`-prefixed magic-target arms (`@version` /
1141    // `@profile` / `@chart-ref`) inside the pre-lift `apply_axis` body
1142    // collapse onto the typed `MatrixTarget` closed-set enum. The
1143    // tests below pin three structural contracts the lift establishes
1144    // — bidirection (`from_path ↔ marker`), per-variant apply
1145    // semantics (each variant writes exactly its named field), and
1146    // the soft-projection posture (`from_path` returns `None` for
1147    // non-magic paths so they cascade to `overlay_at_path`).
1148
1149    #[test]
1150    fn matrix_target_from_path_round_trips_through_marker_for_every_variant() {
1151        // BIDIRECTION CONTRACT: for every `MatrixTarget` variant,
1152        // decoding its canonical marker through `from_path` yields
1153        // the same variant. Sibling-arm sweep over `MatrixTarget::ALL`
1154        // so the three pairings stay load-bearing under reordering
1155        // refactors — a regression that drifts ONE arm's `from_path
1156        // → marker` round-trip (e.g. routes `@version` through to
1157        // `Profile`) fails loudly here. The `ALL` slice is closed so
1158        // a fourth variant automatically extends this sweep through
1159        // the closed-set constant rather than requiring a hand-edited
1160        // literal-array bump.
1161        for variant in MatrixTarget::ALL {
1162            assert_eq!(
1163                MatrixTarget::from_path(variant.marker()),
1164                Some(variant),
1165                "from_path(marker) must round-trip to {variant:?}"
1166            );
1167        }
1168    }
1169
1170    #[test]
1171    fn matrix_target_marker_renders_canonical_at_prefixed_path_for_every_variant() {
1172        // CANONICAL-MARKER CONTRACT: each variant's `marker()` projects
1173        // to its canonical `@`-prefixed path literal. Pins the literal
1174        // identity at the typed projection rather than at the inline
1175        // arms in pre-lift `apply_axis` so a future renaming (e.g.
1176        // hyphenated `@chart-ref` → `@chartRef` to match camelCase
1177        // serde rename) lands at ONE method body.
1178        assert_eq!(MatrixTarget::Version.marker(), "@version");
1179        assert_eq!(MatrixTarget::Profile.marker(), "@profile");
1180        assert_eq!(MatrixTarget::ChartRef.marker(), "@chart-ref");
1181    }
1182
1183    #[test]
1184    fn matrix_target_apply_writes_string_value_to_targeted_aplicacao_field() {
1185        // PER-VARIANT APPLY CONTRACT: each variant's `apply` writes
1186        // exclusively to its named `aplicacao` field. Pin BOTH the
1187        // target-field write AND the non-write of the two sibling
1188        // fields — a regression that drifts ONE arm's assignment
1189        // target (e.g. routes `Version → chart_ref`) silently
1190        // corrupts every operator's matrix sweep and would not
1191        // surface without an explicit per-arm pin.
1192        let mut spec = base();
1193        MatrixTarget::Version.apply(&mut spec, &serde_json::json!("9.9.9"));
1194        assert_eq!(spec.aplicacao.version, "9.9.9");
1195        assert_eq!(spec.aplicacao.profile, "minimal", "profile untouched");
1196        assert_eq!(
1197            spec.aplicacao.chart_ref, "oci://ghcr.io/pleme-io/charts/echo",
1198            "chart_ref untouched"
1199        );
1200
1201        let mut spec = base();
1202        MatrixTarget::Profile.apply(&mut spec, &serde_json::json!("airgapped"));
1203        assert_eq!(spec.aplicacao.profile, "airgapped");
1204        assert_eq!(spec.aplicacao.version, "0.1.0", "version untouched");
1205
1206        let mut spec = base();
1207        MatrixTarget::ChartRef.apply(
1208            &mut spec,
1209            &serde_json::json!("oci://example.com/charts/other"),
1210        );
1211        assert_eq!(spec.aplicacao.chart_ref, "oci://example.com/charts/other");
1212        assert_eq!(spec.aplicacao.version, "0.1.0", "version untouched");
1213    }
1214
1215    #[test]
1216    fn matrix_target_apply_silently_ignores_non_string_values_for_every_variant() {
1217        // NON-STRING-VALUE CONTRACT: magic-target arms accept ONLY
1218        // string values; ints / bools / nulls / arrays / objects are
1219        // silently dropped (they don't route to `overlay_at_path` —
1220        // the path already matched a magic target so the fallthrough
1221        // never fires). Pin the drop-on-non-string posture across all
1222        // three variants × five non-string shapes so a regression
1223        // that starts routing through `val.to_string()` (which would
1224        // stringify `42` into `"42"` and silently mis-write the
1225        // field) fails loudly here.
1226        let non_string_values = [
1227            serde_json::json!(42),
1228            serde_json::json!(true),
1229            serde_json::json!(null),
1230            serde_json::json!([1, 2]),
1231            serde_json::json!({ "k": "v" }),
1232        ];
1233        for variant in MatrixTarget::ALL {
1234            for val in &non_string_values {
1235                let mut spec = base();
1236                let before = (
1237                    spec.aplicacao.version.clone(),
1238                    spec.aplicacao.profile.clone(),
1239                    spec.aplicacao.chart_ref.clone(),
1240                );
1241                variant.apply(&mut spec, val);
1242                let after = (
1243                    spec.aplicacao.version.clone(),
1244                    spec.aplicacao.profile.clone(),
1245                    spec.aplicacao.chart_ref.clone(),
1246                );
1247                assert_eq!(
1248                    before, after,
1249                    "{variant:?}.apply({val}) must NOT mutate aplicacao on non-string"
1250                );
1251            }
1252        }
1253    }
1254
1255    #[test]
1256    fn matrix_target_from_path_rejects_non_magic_path_strings_to_cascade_through_overlay() {
1257        // SOFT-PROJECTION CONTRACT: `from_path` returns `None` for
1258        // every shape that isn't an `@`-prefixed reserved magic
1259        // target — the empty path, plain dotted-paths, plain
1260        // identifiers, and even `@`-prefixed strings that aren't in
1261        // the closed set. The `None` return is load-bearing: it
1262        // signals `apply_axis` to cascade into `overlay_at_path`, so
1263        // a regression that starts admitting near-miss `@`-prefixes
1264        // (e.g. `@chart-Ref` via case-insensitive matching) would
1265        // silently route plain overlay paths through the magic-target
1266        // dispatch — fails loudly here.
1267        for non_magic in [
1268            "",
1269            "replicaCount",
1270            "feature.flag",
1271            "image.tag",
1272            "@versoin",  // typo → not a magic target
1273            "@chartRef", // missing hyphen → not a magic target
1274            "version",   // missing `@` prefix → not a magic target
1275        ] {
1276            assert_eq!(
1277                MatrixTarget::from_path(non_magic),
1278                None,
1279                "{non_magic:?} must NOT decode as a magic target"
1280            );
1281        }
1282    }
1283
1284    #[test]
1285    fn apply_axis_routes_magic_target_paths_through_matrix_target_apply() {
1286        // PATH-UNIFORMITY CONTRACT (apply_axis side): the lifted
1287        // `apply_axis` routes its three magic-target arms through
1288        // `MatrixTarget::from_path` + `MatrixTarget::apply`. Pin that
1289        // the legacy per-arm assignment and the typed-projection
1290        // composition AGREE bit-for-bit across every magic target —
1291        // a regression in `apply_axis` that bypasses the typed
1292        // projection (e.g. reverts to inline `match path { ... }`
1293        // arms) AND accidentally swaps two field targets silently
1294        // corrupts every operator's matrix sweep; this test catches
1295        // the drift via the typed-marker dispatch.
1296        for variant in MatrixTarget::ALL {
1297            let mut via_axis = base();
1298            apply_axis(
1299                &mut via_axis,
1300                variant.marker(),
1301                serde_json::json!("sentinel-VAL"),
1302            );
1303            let mut via_target = base();
1304            variant.apply(&mut via_target, &serde_json::json!("sentinel-VAL"));
1305            assert_eq!(
1306                via_axis.aplicacao.version, via_target.aplicacao.version,
1307                "{variant:?}: apply_axis.version drifted from MatrixTarget::apply"
1308            );
1309            assert_eq!(
1310                via_axis.aplicacao.profile, via_target.aplicacao.profile,
1311                "{variant:?}: apply_axis.profile drifted from MatrixTarget::apply"
1312            );
1313            assert_eq!(
1314                via_axis.aplicacao.chart_ref, via_target.aplicacao.chart_ref,
1315                "{variant:?}: apply_axis.chart_ref drifted from MatrixTarget::apply"
1316            );
1317        }
1318    }
1319
1320    #[test]
1321    fn matrix_target_all_enumerates_each_variant_exactly_once() {
1322        // CLOSED-SET ARITY CONTRACT: `MatrixTarget::ALL` covers every
1323        // variant exactly once. The `[Self; 3]` array literal forces
1324        // the arity at compile time (a fourth variant would fail to
1325        // construct the literal until `ALL` is bumped); this sweep
1326        // additionally pins that none of the three entries is a
1327        // duplicate (every variant's marker appears exactly once in
1328        // the projected marker set). Sibling closed-set contract to
1329        // every other `ALL`-keyed enum in this crate — see e.g.
1330        // `phase::tests::process_phase_all_covers_every_variant`,
1331        // `intent::tests::intent_kind_all_covers_every_variant`,
1332        // `signal::tests::process_signal_all_covers_every_variant`.
1333        let markers: Vec<&'static str> = MatrixTarget::ALL.iter().map(|t| t.marker()).collect();
1334        assert_eq!(markers.len(), 3, "ALL must enumerate exactly 3 variants");
1335        let mut sorted = markers.clone();
1336        sorted.sort_unstable();
1337        sorted.dedup();
1338        assert_eq!(
1339            sorted.len(),
1340            markers.len(),
1341            "ALL must contain no duplicate variants ({markers:?})"
1342        );
1343        // PER-VARIANT REACHABILITY: each named variant appears in
1344        // `ALL`. The `[Self; 3]` literal forces the arity; this sweep
1345        // pins that the three slots are NOT all filled with one
1346        // variant (a regression like `[Self::Version, Self::Version,
1347        // Self::Version]` would satisfy the arity check + the
1348        // marker-len check but still be silently wrong).
1349        for named in [
1350            MatrixTarget::Version,
1351            MatrixTarget::Profile,
1352            MatrixTarget::ChartRef,
1353        ] {
1354            assert!(
1355                MatrixTarget::ALL.contains(&named),
1356                "{named:?} missing from ALL"
1357            );
1358        }
1359    }
1360
1361    #[test]
1362    fn matrix_target_display_matches_marker_for_every_variant() {
1363        // DISPLAY-CANONICAL CONTRACT: `Display` projects through
1364        // `marker()` byte-for-byte. Pins the canonical-form posture
1365        // every sibling closed-set enum in this crate carries
1366        // (`TerminateReason`, `Arity`, `ProcessPhase`, …): a future
1367        // Display impl that re-derives from variant names (e.g.
1368        // `format!("{self:?}")`) drifts from the canonical
1369        // `@`-prefixed marker and breaks every consumer that reads
1370        // the Display form as a hint string. The sweep across `ALL`
1371        // makes the contract automatically cover any future variant.
1372        for variant in MatrixTarget::ALL {
1373            assert_eq!(
1374                variant.to_string(),
1375                variant.marker(),
1376                "Display({variant:?}) must equal marker()"
1377            );
1378        }
1379    }
1380
1381    #[test]
1382    fn matrix_target_from_path_is_derived_from_all_via_marker() {
1383        // LIFT-POSTURE CONTRACT: `from_path` is the closed-set inverse
1384        // of `marker` over `MatrixTarget::ALL` — for every path string,
1385        // `from_path(p)` equals the (at-most-one) variant in `ALL`
1386        // whose `marker()` equals `p`. Pins that adding a fourth
1387        // variant to `ALL` + its `marker` arm automatically makes
1388        // `from_path` recognize the new marker, with NO additional
1389        // edit to `from_path` required. A regression that reverts
1390        // `from_path` to an inline `match path { ... }` body would
1391        // pass for the three existing variants but silently fail this
1392        // closed-set inversion sweep the moment a new variant is
1393        // added to `ALL` without a paired `from_path` arm.
1394        let probes: &[&str] = &[
1395            "@version",
1396            "@profile",
1397            "@chart-ref",
1398            "@release-name", // not a magic target today; must decode as None
1399            "",
1400            "feature.flag",
1401            "version",
1402        ];
1403        for p in probes {
1404            let expected = MatrixTarget::ALL.iter().copied().find(|t| t.marker() == *p);
1405            assert_eq!(
1406                MatrixTarget::from_path(p),
1407                expected,
1408                "from_path({p:?}) must equal the unique ALL entry whose marker() == {p:?}"
1409            );
1410        }
1411    }
1412
1413    // ── BreatheDimensionKind: closed-set dimension dispatch ───────────
1414    //
1415    // The string-input `band_kind_for` helper paired with an inline
1416    // `dim.kind.to_ascii_lowercase()` name-segment site collapses onto
1417    // the typed `BreatheDimensionKind` closed-set enum. The tests
1418    // below pin the structural contracts the lift establishes —
1419    // `ALL` arity + uniqueness + reachability, `aliases` non-emptiness
1420    // / lowercase / no-cross-variant-collisions / slot-0-is-canonical,
1421    // primary-keyword decode, alias-equivalence (each alias decodes to
1422    // the SAME variant as the primary), per-variant `band_kind` /
1423    // `name_segment` projection, unknown-keyword drop, and the
1424    // canonical-name contract that `breathe_bands` emits the SAME
1425    // band-name regardless of which alias the operator wrote (the
1426    // load-bearing improvement over pre-lift's per-alias name drift).
1427
1428    #[test]
1429    fn breathe_dimension_kind_all_enumerates_each_variant_exactly_once() {
1430        // ALL CONTRACT: `BreatheDimensionKind::ALL` is the substrate's
1431        // closed-set source of truth — every consumer (`from_keyword`,
1432        // future `tatara-check` enumerators, sibling test sweeps)
1433        // projects through it. Three contracts: arity (the `[Self; 3]`
1434        // array literal pins it at compile time), no-duplicate (a slot
1435        // that re-lists `Memory` would pass arity but silently lose
1436        // `Storage`-shaped coverage from every sweep), and per-variant
1437        // reachability (every declared variant appears in `ALL`).
1438        // Sibling shape to every other `ALL`-keyed enum's truth-table
1439        // test in this crate.
1440        let segments: Vec<&'static str> = BreatheDimensionKind::ALL
1441            .iter()
1442            .map(|v| v.name_segment())
1443            .collect();
1444        assert_eq!(segments.len(), 3, "ALL must enumerate exactly 3 variants");
1445        let mut sorted = segments.clone();
1446        sorted.sort_unstable();
1447        sorted.dedup();
1448        assert_eq!(
1449            sorted.len(),
1450            segments.len(),
1451            "ALL must contain no duplicate variants ({segments:?})"
1452        );
1453        // Per-variant reachability: every declared variant projects
1454        // through some `ALL` entry.
1455        for v in [
1456            BreatheDimensionKind::Memory,
1457            BreatheDimensionKind::Cpu,
1458            BreatheDimensionKind::Storage,
1459        ] {
1460            assert!(
1461                BreatheDimensionKind::ALL.contains(&v),
1462                "BreatheDimensionKind::ALL must enumerate {v:?}"
1463            );
1464        }
1465    }
1466
1467    #[test]
1468    fn breathe_dimension_kind_aliases_nonempty_for_every_variant() {
1469        // ALIASES NON-EMPTINESS CONTRACT: `aliases()` MUST return a
1470        // non-empty slice for every variant — `name_segment()` reads
1471        // `aliases()[0]` and an empty slice would panic at runtime.
1472        // The truth-table sweep over `ALL` pins the invariant at test
1473        // time so a future variant whose `aliases` arm returns `&[]`
1474        // fails loudly here rather than at the first operator-side
1475        // `name_segment()` call.
1476        for variant in BreatheDimensionKind::ALL {
1477            assert!(
1478                !variant.aliases().is_empty(),
1479                "BreatheDimensionKind::{variant:?}.aliases() must be non-empty"
1480            );
1481        }
1482    }
1483
1484    #[test]
1485    fn breathe_dimension_kind_aliases_are_all_lowercase() {
1486        // ALIAS LOWERCASE CONTRACT: every entry of `aliases()` is
1487        // pre-lowercased; `from_keyword` lowercases the input ONCE
1488        // and compares directly against the alias entries. An upper-
1489        // case alias literal (e.g. `&["Memory", "mem"]`) would silently
1490        // fail to decode the lowercase input `"memory"` despite
1491        // appearing to declare it. Pin the lowercase contract over
1492        // `ALL × aliases()` so the case-fold invariant is structural,
1493        // not per-arm-discipline.
1494        for variant in BreatheDimensionKind::ALL {
1495            for alias in variant.aliases() {
1496                assert_eq!(
1497                    *alias,
1498                    alias.to_ascii_lowercase(),
1499                    "BreatheDimensionKind::{variant:?} alias {alias:?} must be lowercase"
1500                );
1501            }
1502        }
1503    }
1504
1505    #[test]
1506    fn breathe_dimension_kind_name_segment_is_aliases_slot_zero() {
1507        // CANONICAL-SLOT CONTRACT: `name_segment()` projects through
1508        // `aliases()[0]`. Pin the slot-0 binding so a future refactor
1509        // that reorders an `aliases` arm (e.g. swaps `"memory"` and
1510        // `"mem"` for Memory) immediately reshapes the canonical
1511        // band-name and the test fails — preventing a silent
1512        // canonical-name drift from `<env>-memory` to `<env>-mem` that
1513        // would otherwise type-check.
1514        for variant in BreatheDimensionKind::ALL {
1515            assert_eq!(
1516                variant.name_segment(),
1517                variant.aliases()[0],
1518                "BreatheDimensionKind::{variant:?}.name_segment() must be aliases()[0]"
1519            );
1520        }
1521    }
1522
1523    #[test]
1524    fn breathe_dimension_kind_aliases_have_no_cross_variant_collisions() {
1525        // CROSS-VARIANT UNIQUENESS CONTRACT: no alias appears in two
1526        // variants' `aliases()` lists. Two variants accepting the same
1527        // alias would make `from_keyword` non-deterministic — the
1528        // linear search across `ALL` would return whichever variant
1529        // came first in `ALL`. Sibling shape to every other closed-set
1530        // round-trip-uniqueness sweep in this crate.
1531        let mut pairs: Vec<(&'static str, BreatheDimensionKind)> = Vec::new();
1532        for variant in BreatheDimensionKind::ALL {
1533            for alias in variant.aliases() {
1534                if let Some((_, prior)) = pairs.iter().find(|(a, _)| a == alias) {
1535                    panic!(
1536                        "alias {alias:?} appears in both {prior:?} and {variant:?} \
1537                         — `from_keyword` would be non-deterministic"
1538                    );
1539                }
1540                pairs.push((*alias, variant));
1541            }
1542        }
1543    }
1544
1545    #[test]
1546    fn breathe_dimension_kind_from_keyword_decodes_every_alias_for_every_variant() {
1547        // ALL-ALIAS DECODE SWEEP: for every (variant, alias) pair in
1548        // `ALL × aliases()`, `from_keyword(alias)` MUST decode to that
1549        // variant. Subsumes the prior pinned-pairs table by deriving
1550        // the sweep from the typed source of truth — adding a fourth
1551        // dimension automatically extends the sweep through `ALL`
1552        // rather than requiring a hand-edited literal table.
1553        for variant in BreatheDimensionKind::ALL {
1554            for alias in variant.aliases() {
1555                assert_eq!(
1556                    BreatheDimensionKind::from_keyword(alias),
1557                    Some(variant),
1558                    "from_keyword({alias:?}) must decode as {variant:?}"
1559                );
1560            }
1561        }
1562    }
1563
1564    #[test]
1565    fn breathe_dimension_kind_from_keyword_round_trips_through_band_kind_for_every_variant() {
1566        // PRIMARY-KEYWORD CONTRACT: for every `BreatheDimensionKind`
1567        // variant, decoding its `name_segment` (the canonical primary
1568        // keyword `memory` / `cpu` / `storage`) through `from_keyword`
1569        // yields the same variant. Sibling-arm sweep so the three
1570        // pairings stay load-bearing under reordering refactors — a
1571        // regression that drifts ONE arm's `from_keyword → name_segment`
1572        // round-trip (e.g. routes `"cpu"` through to `Memory`) fails
1573        // loudly here.
1574        for variant in BreatheDimensionKind::ALL {
1575            assert_eq!(
1576                BreatheDimensionKind::from_keyword(variant.name_segment()),
1577                Some(variant),
1578                "from_keyword(name_segment) must round-trip to {variant:?}"
1579            );
1580        }
1581    }
1582
1583    #[test]
1584    fn breathe_dimension_kind_aliases_decode_to_the_same_variant_as_the_primary_keyword() {
1585        // ALIAS-EQUIVALENCE CONTRACT: aliases (`mem` for Memory,
1586        // `disk` for Storage) decode to the SAME variant as the
1587        // primary keyword. Cpu has no alias so the (variant,
1588        // alias-set) table is asymmetric — pin every (alias, variant)
1589        // pair explicitly so a regression that adds a wrong alias
1590        // (e.g. `"hdd" → Cpu`) fails loudly here.
1591        let pairs: &[(&str, BreatheDimensionKind)] = &[
1592            ("mem", BreatheDimensionKind::Memory),
1593            ("memory", BreatheDimensionKind::Memory),
1594            ("MEM", BreatheDimensionKind::Memory),
1595            ("Memory", BreatheDimensionKind::Memory),
1596            ("cpu", BreatheDimensionKind::Cpu),
1597            ("CPU", BreatheDimensionKind::Cpu),
1598            ("Cpu", BreatheDimensionKind::Cpu),
1599            ("disk", BreatheDimensionKind::Storage),
1600            ("storage", BreatheDimensionKind::Storage),
1601            ("DISK", BreatheDimensionKind::Storage),
1602            ("Storage", BreatheDimensionKind::Storage),
1603        ];
1604        for (keyword, expected) in pairs {
1605            assert_eq!(
1606                BreatheDimensionKind::from_keyword(keyword),
1607                Some(*expected),
1608                "from_keyword({keyword:?}) must decode as {expected:?}"
1609            );
1610        }
1611    }
1612
1613    #[test]
1614    fn breathe_dimension_kind_band_kind_projects_canonical_cr_kind_for_every_variant() {
1615        // CANONICAL-CR-KIND CONTRACT: each variant's `band_kind()`
1616        // projects to its canonical breathe Band CR kind literal
1617        // (`MemoryBand` / `CpuBand` / `StorageBand`) — the wire-format
1618        // string the `kind:` field on the emitted CR carries. Pins
1619        // the literal identity at the typed projection rather than
1620        // at the inline arms in pre-lift `band_kind_for` so a future
1621        // rename (e.g. `MemoryBand` → `MemBand`) lands at ONE method
1622        // body.
1623        assert_eq!(BreatheDimensionKind::Memory.band_kind(), "MemoryBand");
1624        assert_eq!(BreatheDimensionKind::Cpu.band_kind(), "CpuBand");
1625        assert_eq!(BreatheDimensionKind::Storage.band_kind(), "StorageBand");
1626    }
1627
1628    #[test]
1629    fn breathe_dimension_kind_name_segment_canonicalizes_aliases_to_the_primary_keyword() {
1630        // CANONICAL-NAME-SEGMENT CONTRACT: for every alias of a
1631        // variant, `from_keyword(alias).name_segment()` MUST equal
1632        // the primary-keyword name segment — NOT the alias the
1633        // operator wrote. Pre-lift the band metadata name echoed
1634        // `dim.kind.to_ascii_lowercase()` so an operator who wrote
1635        // `(:kind "mem" …)` got a band named `<env>-mem` while
1636        // another who wrote `(:kind "memory" …)` got `<env>-memory`;
1637        // two semantically-identical sweeps produced two different
1638        // band-name surfaces and no test caught the drift. Post-lift
1639        // the name segment binds to the typed variant so EVERY alias
1640        // funnels to ONE canonical band name.
1641        let pairs: &[(&str, &str)] = &[
1642            ("mem", "memory"),
1643            ("memory", "memory"),
1644            ("MEM", "memory"),
1645            ("cpu", "cpu"),
1646            ("CPU", "cpu"),
1647            ("disk", "storage"),
1648            ("storage", "storage"),
1649            ("DISK", "storage"),
1650        ];
1651        for (alias, canonical) in pairs {
1652            let kind = BreatheDimensionKind::from_keyword(alias)
1653                .expect("alias must decode to a known dimension");
1654            assert_eq!(
1655                kind.name_segment(),
1656                *canonical,
1657                "from_keyword({alias:?}).name_segment() must canonicalize to {canonical:?}"
1658            );
1659        }
1660    }
1661
1662    #[test]
1663    fn breathe_dimension_kind_from_keyword_rejects_unknown_keywords() {
1664        // UNKNOWN-KEYWORD CONTRACT: `from_keyword` returns `None` for
1665        // every shape outside the closed set — the empty string,
1666        // near-miss typos, and dimension keywords the substrate does
1667        // not (yet) support. The `None` return is load-bearing: it
1668        // signals `breathe_bands` to drop the dimension via
1669        // `filter_map`, so a regression that starts admitting
1670        // near-miss keywords (e.g. case-fold matching `"net" →
1671        // Network` against a Network variant that doesn't exist)
1672        // would silently route unrelated dimensions through the
1673        // dispatch — fails loudly here.
1674        for unknown in [
1675            "", "network", // not yet a supported dimension
1676            "gpu",     // not yet a supported dimension
1677            "memoryy", // typo
1678            "cp",      // truncated
1679            "diskz",   // suffix
1680        ] {
1681            assert_eq!(
1682                BreatheDimensionKind::from_keyword(unknown),
1683                None,
1684                "{unknown:?} must NOT decode as a breathe dimension"
1685            );
1686        }
1687    }
1688
1689    #[test]
1690    fn breathe_bands_emits_canonical_name_segment_regardless_of_operator_alias() {
1691        // END-TO-END CANONICAL-NAME CONTRACT (breathe_bands side):
1692        // two sweeps that declare the SAME dimension under two
1693        // different aliases (`(:kind "mem" …)` vs `(:kind "memory"
1694        // …)`) emit Band CRs with the SAME band metadata name. Pre-
1695        // lift this assertion FAILED — the `"mem"` sweep produced
1696        // `<env>-mem` and the `"memory"` sweep produced `<env>-
1697        // memory`. Post-lift the name segment routes through
1698        // `BreatheDimensionKind::name_segment` so every alias funnels
1699        // to one canonical name. A regression that reverts to
1700        // echoing `dim.kind.to_ascii_lowercase()` would silently
1701        // re-introduce the drift; this test catches it.
1702        let envelope = |kind: &str| BreatheEnvelope {
1703            dimensions: vec![BreatheDimension {
1704                kind: kind.into(),
1705                floor: "128Mi".into(),
1706                ceiling: "1Gi".into(),
1707            }],
1708            cooldown_seconds: 60,
1709            dry_run: true,
1710            target_kind: "Deployment".into(),
1711        };
1712        let mut m_mem = matrix();
1713        m_mem.breathe = Some(envelope("mem"));
1714        let mut m_memory = matrix();
1715        m_memory.breathe = Some(envelope("memory"));
1716        let mut m_upper = matrix();
1717        m_upper.breathe = Some(envelope("MEMORY"));
1718
1719        let envs = matrix().expand("echo-sweep");
1720        let env0 = &envs[0];
1721
1722        let bands_mem = m_mem.breathe_bands(env0);
1723        let bands_memory = m_memory.breathe_bands(env0);
1724        let bands_upper = m_upper.breathe_bands(env0);
1725
1726        assert_eq!(bands_mem.len(), 1);
1727        assert_eq!(bands_memory.len(), 1);
1728        assert_eq!(bands_upper.len(), 1);
1729
1730        let expected_name = format!("{}-memory", env0.name);
1731        assert_eq!(bands_mem[0]["metadata"]["name"], expected_name);
1732        assert_eq!(bands_memory[0]["metadata"]["name"], expected_name);
1733        assert_eq!(bands_upper[0]["metadata"]["name"], expected_name);
1734
1735        // The CR kind also canonicalizes — every alias projects to
1736        // `MemoryBand` (the wire-format kind).
1737        assert_eq!(bands_mem[0]["kind"], "MemoryBand");
1738        assert_eq!(bands_memory[0]["kind"], "MemoryBand");
1739        assert_eq!(bands_upper[0]["kind"], "MemoryBand");
1740    }
1741
1742    #[test]
1743    fn breathe_bands_drops_dimensions_with_unknown_kind_keywords() {
1744        // UNKNOWN-DIMENSION DROP CONTRACT (breathe_bands side): a
1745        // dimension whose keyword `from_keyword` doesn't recognize
1746        // drops out via `filter_map` — the sweep continues with the
1747        // remaining recognized dimensions. Pin the drop here so a
1748        // regression that starts emitting bands with raw / unmapped
1749        // `kind:` values (e.g. an inline fallback that bypasses the
1750        // typed projection) fails loudly.
1751        let mut m = matrix();
1752        m.breathe = Some(BreatheEnvelope {
1753            dimensions: vec![
1754                BreatheDimension {
1755                    kind: "memory".into(),
1756                    floor: "128Mi".into(),
1757                    ceiling: "1Gi".into(),
1758                },
1759                BreatheDimension {
1760                    kind: "network".into(), // unrecognized — must drop
1761                    floor: "1Mbps".into(),
1762                    ceiling: "100Mbps".into(),
1763                },
1764                BreatheDimension {
1765                    kind: "cpu".into(),
1766                    floor: "100m".into(),
1767                    ceiling: "1".into(),
1768                },
1769            ],
1770            cooldown_seconds: 60,
1771            dry_run: true,
1772            target_kind: "Deployment".into(),
1773        });
1774        let envs = m.expand("echo-sweep");
1775        let bands = m.breathe_bands(&envs[0]);
1776        assert_eq!(bands.len(), 2, "unknown dimension `network` must drop");
1777        assert_eq!(bands[0]["kind"], "MemoryBand");
1778        assert_eq!(bands[1]["kind"], "CpuBand");
1779    }
1780
1781    // ── SelectStrategyKind: closed-set strategy discriminator ────────
1782    //
1783    // `SelectStrategy` carries data on `Explicit(Vec<Vec<usize>>)`, so
1784    // the closed-set view lives on the payload-stripped
1785    // `SelectStrategyKind` (same shape as `TerminateReason` →
1786    // `TerminateReasonKind` in `lifetime_clock`). The tests below pin
1787    // the structural contracts the lift establishes — `ALL` arity +
1788    // uniqueness + reachability, `as_str` canonical PascalCase pin +
1789    // uniqueness, `Display` IS `as_str`, `FromStr` round-trips through
1790    // `ALL`, `kind()` agrees with each variant exhaustively, and the
1791    // `selection_size_for` count agrees with `coordinates().len()` on
1792    // every probe shape (the load-bearing perf lift: count without
1793    // materializing the cartesian product).
1794
1795    /// Structural well-formedness of [`SelectStrategyKind`] as a
1796    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1797    /// testkit lift that pins all three structural invariants (`ALL`
1798    /// is non-empty, every variant round-trips through
1799    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1800    /// outside the closed set) at ONE call site. Replaces the hand-
1801    /// derived `select_strategy_kind_all_enumerates_each_variant_exactly_once`
1802    /// + `select_strategy_kind_from_str_round_trips_canonical_names` +
1803    /// the empty-input arm of
1804    /// `select_strategy_kind_from_str_rejects_unknown_with_verbatim_input`.
1805    /// `FromStr` delegates to
1806    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1807    /// exercises the same code path the matrix sweep hits when reading
1808    /// a `SelectStrategy` external-tag back to the typed kind.
1809    #[test]
1810    fn select_strategy_kind_is_well_formed_closed_set() {
1811        tatara_closed_set::assert_closed_set_well_formed::<SelectStrategyKind>();
1812    }
1813
1814    #[test]
1815    fn select_strategy_kind_as_str_canonical_pascal_case_pinned() {
1816        // CANONICAL-NAME CONTRACT: each variant's `as_str()` projects
1817        // to its serde external-tag PascalCase form — so a typed kind
1818        // round-trips bit-identically with the wire-format discriminator
1819        // that `SelectStrategy`'s `Serialize`/`Deserialize` derive
1820        // already uses. Pinning the literal at the typed projection
1821        // means a future rename of the wire form (e.g. PascalCase →
1822        // kebab-case via `#[serde(rename_all = "kebab-case")]`) lands
1823        // here AND at the serde rename in lockstep — diverging the
1824        // two would break the round-trip the closed-set view promises.
1825        assert_eq!(SelectStrategyKind::Cartesian.as_str(), "Cartesian");
1826        assert_eq!(SelectStrategyKind::Explicit.as_str(), "Explicit");
1827    }
1828
1829    #[test]
1830    fn select_strategy_kind_display_matches_as_str() {
1831        // DISPLAY-CANONICAL CONTRACT: `Display` projects through
1832        // `as_str()` byte-for-byte. Pins the canonical-form posture
1833        // every sibling closed-set enum in this crate carries
1834        // (`TerminateReasonKind`, `ProcessPhase`, `TeardownPolicy`,
1835        // …): a future Display impl that re-derives from variant
1836        // names (e.g. `format!("{self:?}")`) drifts from the canonical
1837        // wire form and breaks every consumer that reads the Display
1838        // form as a wire string.
1839        crate::tagged_union::assert_display_matches_label::<SelectStrategyKind>();
1840    }
1841
1842    #[test]
1843    fn select_strategy_kind_from_str_rejects_unknown_with_verbatim_input() {
1844        use std::str::FromStr;
1845        // UNKNOWN-INPUT CONTRACT: parsing a non-canonical string fails
1846        // with a typed `UnknownSelectStrategyKind` carrying the input
1847        // verbatim — operators see the bad value, not a normalized
1848        // form. Sibling shape to every `Unknown*` parse error in this
1849        // crate (e.g. `UnknownPhase`, `UnknownTeardownPolicy`,
1850        // `UnknownTerminateReasonKind`). The empty-input arm is pinned
1851        // by [`select_strategy_kind_is_well_formed_closed_set`] via the
1852        // `tatara_lisp::ClosedSet` testkit; the cases here pin the
1853        // verbatim-echo contract on the [`UnknownSelectStrategyKind`]
1854        // newtype, which the trait's `make_unknown` can't see.
1855        for bad in [
1856            "cartesian", // wrong case
1857            "explicit",  // wrong case
1858            "Latin",
1859            "Random",
1860            "Cartesian ", // trailing whitespace
1861        ] {
1862            let err = SelectStrategyKind::from_str(bad).unwrap_err();
1863            assert_eq!(
1864                err,
1865                UnknownSelectStrategyKind(bad.to_string()),
1866                "from_str({bad:?}) must surface the offending input verbatim"
1867            );
1868        }
1869    }
1870
1871    #[test]
1872    fn select_strategy_kind_projection_agrees_per_variant() {
1873        // PROJECTION CONTRACT: `SelectStrategy::kind()` strips the
1874        // payload to the typed discriminator exhaustively. Pin per
1875        // variant so a future regression that drifts ONE arm (e.g.
1876        // routes `Explicit(_)` through to `Cartesian`) — which would
1877        // silently make the typed discriminator non-injective — fails
1878        // loudly here. The `Explicit` probe additionally pins that
1879        // the projection is `const` over the payload shape (an empty
1880        // coord list and a populated one project to the SAME kind).
1881        assert_eq!(
1882            SelectStrategy::Cartesian.kind(),
1883            SelectStrategyKind::Cartesian
1884        );
1885        assert_eq!(
1886            SelectStrategy::Explicit(vec![]).kind(),
1887            SelectStrategyKind::Explicit
1888        );
1889        assert_eq!(
1890            SelectStrategy::Explicit(vec![vec![0, 1, 2]]).kind(),
1891            SelectStrategyKind::Explicit
1892        );
1893        // And the Default IS Cartesian — pin the default's kind
1894        // projection so a future `#[default]` move silently breaks
1895        // any consumer that assumed Cartesian.
1896        assert_eq!(
1897            SelectStrategy::default().kind(),
1898            SelectStrategyKind::Cartesian
1899        );
1900    }
1901
1902    #[test]
1903    fn selection_size_for_cartesian_handles_empty_and_zero_length_axes() {
1904        // CARTESIAN EDGE-CASE CONTRACT: aligns with pre-lift `cartesian`
1905        // semantics — empty axes ⇒ 1 (the single empty coord), any
1906        // zero-length axis ⇒ 0 (no env can range over no values).
1907        // Pin both edges so a future refactor that drops the empty /
1908        // zero-length cases (and silently bumps the size to `product`
1909        // of an empty iter = 1, or skips the zero-length short-circuit)
1910        // fails here BEFORE any operator-facing matrix sweeps drift.
1911        let strat = SelectStrategy::Cartesian;
1912        assert_eq!(
1913            strat.selection_size_for(&[]),
1914            1,
1915            "no axes ⇒ one empty coord"
1916        );
1917        assert_eq!(strat.selection_size_for(&[0]), 0, "any zero-length ⇒ 0");
1918        assert_eq!(
1919            strat.selection_size_for(&[2, 0, 3]),
1920            0,
1921            "zero-length anywhere ⇒ 0"
1922        );
1923        assert_eq!(strat.selection_size_for(&[3]), 3);
1924        assert_eq!(strat.selection_size_for(&[2, 3, 4]), 24);
1925    }
1926
1927    #[test]
1928    fn selection_size_for_explicit_filters_out_of_bounds_coords() {
1929        // EXPLICIT FILTER CONTRACT: in-bounds coords are counted;
1930        // mis-length and out-of-bounds coords are dropped — matching
1931        // the pre-lift `coord_in_bounds` filter on `coordinates()`.
1932        // Routes through the shared `coord_in_bounds_against` helper,
1933        // so a regression on the bounds-check lands at ONE site.
1934        let strat = SelectStrategy::Explicit(vec![
1935            vec![0, 0, 0],
1936            vec![1, 1, 1],
1937            vec![2, 0, 0],    // axis-0 out of bounds (lengths[0]=2)
1938            vec![0, 0],       // mis-length
1939            vec![0, 0, 0, 0], // mis-length
1940            vec![1, 1, 0],
1941        ]);
1942        assert_eq!(strat.selection_size_for(&[2, 2, 2]), 3);
1943    }
1944
1945    #[test]
1946    fn selection_size_matches_coordinates_len_on_every_probe() {
1947        // EQUIVALENCE CONTRACT: `EnvMatrixSpec::selection_size()` —
1948        // which now routes through the typed `selection_size_for`
1949        // projection — must agree with the pre-lift definition
1950        // `self.coordinates().len()` on every shape. Pin a battery of
1951        // probes covering Cartesian / Explicit / empty-axes / zero-
1952        // length-axis / mixed-bounds-coord shapes so a future
1953        // performance refactor of EITHER path that diverges its count
1954        // from the materialized-coords reference fails here.
1955        let mut m = matrix();
1956        // Cartesian over the 2×2×2 default.
1957        assert_eq!(m.selection_size(), m.coordinates().len());
1958        // Cartesian with one zero-length axis (no values at all).
1959        m.axes[1].values = serde_json::json!([]);
1960        assert_eq!(m.selection_size(), 0);
1961        assert_eq!(m.selection_size(), m.coordinates().len());
1962        // Cartesian with all axes empty: still no axes worth zeroing,
1963        // and the product over no-axes is the single empty coord.
1964        let mut m2 = matrix();
1965        m2.axes.clear();
1966        assert_eq!(m2.selection_size(), 1);
1967        assert_eq!(m2.selection_size(), m2.coordinates().len());
1968        // Explicit with mixed in-bounds / out-of-bounds / mis-length
1969        // coords against the 2×2×2 axes.
1970        let mut m3 = matrix();
1971        m3.select = SelectStrategy::Explicit(vec![
1972            vec![0, 0, 0],
1973            vec![1, 1, 1],
1974            vec![2, 0, 0],
1975            vec![0, 0],
1976            vec![1, 0, 1],
1977        ]);
1978        assert_eq!(m3.selection_size(), 3);
1979        assert_eq!(m3.selection_size(), m3.coordinates().len());
1980    }
1981
1982    #[test]
1983    fn selection_size_avoids_materializing_cartesian_product() {
1984        // PERF LIFT CONTRACT: a sweep over axes whose product exceeds
1985        // any reasonable test allocation budget (10 axes × 100 values
1986        // = 10^20 coords) must still resolve `selection_size()` in
1987        // O(N) time WITHOUT materializing the coordinate set. Pre-lift
1988        // `selection_size` called `coordinates().len()` which would
1989        // either OOM or wedge on this probe; post-lift the closed-set
1990        // dispatch routes through a saturating product over the axis
1991        // lengths.
1992        //
1993        // 10^20 overflows `usize`, so the assertion pins the saturated
1994        // sentinel `usize::MAX` — the deterministic "as many as fit"
1995        // signal the fold promises rather than a debug-build panic or
1996        // a release-build wraparound to a small number.
1997        let strat = SelectStrategy::Cartesian;
1998        let lengths = vec![100_usize; 10];
1999        assert_eq!(
2000            strat.selection_size_for(&lengths),
2001            usize::MAX,
2002            "10^20 must saturate to usize::MAX, not panic or wrap"
2003        );
2004        // And the saturation is sticky once reached: appending more
2005        // non-trivial axes never reduces the count.
2006        let mut deeper = lengths.clone();
2007        deeper.push(2);
2008        assert_eq!(strat.selection_size_for(&deeper), usize::MAX);
2009    }
2010
2011    #[test]
2012    fn env_matrix_lisp_round_trip() {
2013        let src = r#"
2014            (defenvmatrix echo-sweep
2015              :base (:aplicacao (:chart-ref "oci://ghcr.io/pleme-io/charts/echo"
2016                                 :version "0.1.0" :profile "minimal" :values-overlay ())
2017                     :ttl "2h" :teardown Always)
2018              :axes ((:name "version"  :path "@version"     :values ("0.1.0" "0.2.0"))
2019                     (:name "replicas" :path "replicaCount" :values (1 3)))
2020              :select Cartesian
2021              :budget (:max-envs 12 :cost-ceiling "$5/h"))
2022        "#;
2023        let defs = compile_env_matrix_source(src).expect("compile");
2024        assert_eq!(defs.len(), 1);
2025        let d = &defs[0];
2026        assert_eq!(d.name, "echo-sweep");
2027        assert_eq!(d.spec.axes.len(), 2);
2028        assert_eq!(d.spec.budget.max_envs, 12);
2029        assert_eq!(d.spec.budget.cost_ceiling.as_deref(), Some("$5/h"));
2030        let envs = d.spec.expand(&d.name);
2031        assert_eq!(envs.len(), 4); // 2 × 2
2032    }
2033}