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