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}