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