Skip to main content

tatara_process/
pool.rs

1//! `EphemeralPool` CRD — a population of warm, pre-attested ephemeral
2//! Processes that get *allocated* to requestors (e.g., a GitHub PR
3//! flow) on demand and *returned* (per a typed policy) when the
4//! requestor releases them.
5//!
6//! Compounding move: the pool is a population manager **over the
7//! existing Process algebra**, not a parallel runtime. A pool member
8//! is just a `Process` with `Lifetime::Permanent` while in the free
9//! list; allocation is "the operator (the pool reconciler) flips
10//! that Process's lifetime slot to Ephemeral with the requestor's
11//! TTL." Zero new compute primitive.
12//!
13//! Topology:
14//!
15//! ```text
16//! EphemeralPool       (this CRD)
17//!   ├── PoolSpec      (desired_size, template (EphemeralSpec), return_policy, selector)
18//!   ├── PoolStatus    (phase, free / allocated / spawning / returning counts, members)
19//!   └── owns N Processes via ownerReferences (one per pool slot)
20//!
21//! EphemeralAllocation (see allocation.rs)
22//!   ├── AllocationSpec (pool_ref, requestor, requested_at, lifetime override)
23//!   └── AllocationStatus (phase, assigned_process_ref, allocated_at, expires_at)
24//! ```
25
26use chrono::{DateTime, Utc};
27use kube::CustomResource;
28use schemars::JsonSchema;
29use serde::{Deserialize, Serialize};
30
31use crate::ephemeral::EphemeralSpec;
32
33/// `EphemeralPool` CRD spec — typed pool of warm Processes.
34///
35/// ```yaml
36/// apiVersion: tatara.pleme.io/v1alpha1
37/// kind: EphemeralPool
38/// metadata:
39///   name: attest-pool
40///   namespace: ephemeral-pools
41/// spec:
42///   desiredSize: 3
43///   minSize: 1
44///   maxSize: 5
45///   returnPolicy: Reset
46///   selector:
47///     repos: ["pleme-io/demo-*"]
48///     branches: ["main", "release-*"]
49///     prLabels: ["needs-ephemeral"]
50///   template:
51///     aplicacao:
52///       chartRef: "oci://ghcr.io/pleme-io/charts/lareira-demo-app"
53///       version: "0.5.5"
54///       profile: "all-in-one"
55///       …
56///     ttl: "2h"
57///     teardown: OnAttested
58///     postconditions: [ … ]
59/// ```
60#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
61#[kube(
62    group = "tatara.pleme.io",
63    version = "v1alpha1",
64    kind = "EphemeralPool",
65    plural = "ephemeralpools",
66    shortname = "epool",
67    namespaced,
68    status = "PoolStatus",
69    printcolumn = r#"{"name":"Desired","type":"integer","jsonPath":".spec.desiredSize"}"#,
70    printcolumn = r#"{"name":"Ready","type":"integer","jsonPath":".status.readyCount"}"#,
71    printcolumn = r#"{"name":"Allocated","type":"integer","jsonPath":".status.allocatedCount"}"#,
72    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
73    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
74)]
75#[serde(rename_all = "camelCase")]
76pub struct PoolSpec {
77    /// Target number of warm Processes the pool maintains in `Free`
78    /// state (sum of Free + Spawning targets `desired_size`).
79    pub desired_size: u32,
80
81    /// Hard floor on the free count. The reconciler refuses to scale
82    /// below this even on cost-pressure signals. Default = 0.
83    #[serde(default)]
84    pub min_size: u32,
85
86    /// Hard ceiling on total pool members (free + allocated + spawning).
87    /// `0` = no cap. Default = 0.
88    #[serde(default)]
89    pub max_size: u32,
90
91    /// What to do when an allocation releases.
92    #[serde(default)]
93    pub return_policy: ReturnPolicy,
94
95    /// Routing selector — which allocation requests this pool serves.
96    /// The reconciler matches incoming `EphemeralAllocation` CRs
97    /// against this selector (most-specific wins across pools sharing
98    /// a namespace).
99    #[serde(default)]
100    pub selector: PoolSelector,
101
102    /// Template for each pool member — a typed `EphemeralSpec` that
103    /// the reconciler lowers to `ProcessSpec` and instantiates.
104    /// While in the free list each member's lifetime is overridden
105    /// to `Permanent`; allocation flips it back to `Ephemeral` with
106    /// the requestor's TTL.
107    pub template: EphemeralSpec,
108
109    /// How long a pool member may sit in `Free` before the reconciler
110    /// recycles it (humantime). Defends against drift / stale state.
111    /// Default `"24h"`.
112    #[serde(default = "default_free_ttl")]
113    pub free_ttl: String,
114
115    /// Max time the reconciler allows a single allocation to hold a
116    /// member before forcibly returning it (humantime). Hard cap
117    /// independent of the allocation's own TTL. Default `"4h"`.
118    #[serde(default = "default_max_allocation_ttl")]
119    pub max_allocation_ttl: String,
120
121    /// **R5 desired-count loop** — when set non-zero, the pool
122    /// reconciler maintains exactly this many *healthy* (Running or
123    /// Attested) Processes regardless of allocation pressure. Drives
124    /// the "always seeking stability" property: failed members are
125    /// replaced per `replacement_policy`. `0` keeps the legacy
126    /// allocation-driven sizing (desired = floor of free + allocated).
127    ///
128    /// Operator usage: `desired: 5` means "always have 5 of these
129    /// running"; failures auto-replace.
130    #[serde(default)]
131    pub desired: u32,
132
133    /// **R5** — what the pool reconciler does when a member reaches
134    /// `Failed` phase.
135    #[serde(default)]
136    pub replacement_policy: ReplacementPolicy,
137
138    /// **R5** — when true, exactly one healthy member of the pool
139    /// holds the unprefixed-form DNS hostnames declared in
140    /// `template.routing` at any moment. The claim arbiter (see
141    /// `tatara-reconciler::claim`) transfers atomically when the
142    /// holder fails.
143    #[serde(default)]
144    pub stable_name_claim: bool,
145}
146
147impl PoolSpec {
148    /// Humantime-parsed [`std::time::Duration`] projection of the
149    /// [`Self::free_ttl`] slot — the ONE-line collapse of the paired
150    /// `humantime::parse_duration(&<pool>.spec.free_ttl).ok()`
151    /// incantation the pool reconciler's stale-free bucket loop
152    /// hand-authored pre-lift, sibling to
153    /// [`crate::lifetime::EphemeralLifetime::ttl_duration`] on the
154    /// SAME `(humantime string field × Option<Duration>) → Option<
155    /// Duration>` substrate axis.
156    ///
157    /// Pre-lift the `humantime::parse_duration(&<field>).ok()` shape
158    /// was owned at ONE substrate primitive on
159    /// [`crate::lifetime::EphemeralLifetime`] (the `spec.lifetime
160    /// .ephemeral.ttl` axis, feeding
161    /// [`crate::lifetime_clock::evaluate`]'s TTL-expiry gate + the
162    /// `requeue_with_ttl` sleep-budget picker) AND hand-authored at
163    /// ONE peer consumer site — `tatara-pool-reconciler::pool_decide
164    /// ::decide_pool`, which parses `pool.spec.free_ttl` with the
165    /// byte-identical shape (`humantime::parse_duration(&spec
166    /// .free_ttl).unwrap_or_default()`) and gates the stale-free
167    /// bucket loop on the result. That's ONE substrate owner + ONE
168    /// hand-authored chain on a peer humantime field of a peer spec
169    /// type past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger —
170    /// two surfaces spelling the SAME projection with the SAME drift
171    /// risk (a per-fleet minimum TTL floor before the humantime cast,
172    /// a canonical unit-normalization pass, a warn-log on
173    /// unparseable strings would have had to land at every surface
174    /// plus stay coherent between them).
175    ///
176    /// Post-lift both peer humantime fields
177    /// ([`crate::lifetime::EphemeralLifetime::ttl`] +
178    /// [`Self::free_ttl`]) publish the SAME shape at TWO peer
179    /// inherent methods on peer spec types — the tatara-pool-
180    /// reconciler's stale-free bucket loop reads `pool.spec.free_ttl
181    /// _duration().unwrap_or_default()` and the produced [`std::time
182    /// ::Duration`] feeds the same `!free_ttl.is_zero()` guard +
183    /// `elapsed > free_ttl` comparator unchanged. A future
184    /// normalization (per-fleet minimum floor, canonical unit
185    /// normalization, warn-log on unparseable strings) lands at TWO
186    /// substrate methods here + on
187    /// [`crate::lifetime::EphemeralLifetime::ttl_duration`], reachable
188    /// via ONE workspace-wide sweep across the peer axis rather than
189    /// as a per-callsite hand-edit at every downstream humantime-ttl
190    /// consumer.
191    ///
192    /// Return-form axis: `Option<std::time::Duration>` matches the
193    /// peer primitive on
194    /// [`crate::lifetime::EphemeralLifetime::ttl_duration`] and the
195    /// downstream comparator's type. The peer projection
196    /// [`crate::time::elapsed_since`] returns the SAME `Option<std
197    /// ::time::Duration>` shape, so the stale-free gate's `elapsed >
198    /// free_ttl` comparator lands with both operands on the same
199    /// axis without a per-consumer conversion step.
200    ///
201    /// The `None` arm is the "operator's `free_ttl` string doesn't
202    /// parse" corner — a typo (`"1our"`), an unsupported unit, a
203    /// non-humantime literal that reached the field. The pool
204    /// reconciler's stale-free bucket loop collapses the corner via
205    /// `.unwrap_or_default()`, yielding the `Duration::ZERO` value
206    /// that already gates its follow-on `!free_ttl.is_zero()` check
207    /// — post-lift semantics is byte-identical to the pre-lift
208    /// hand-authored `humantime::parse_duration(&spec.free_ttl)
209    /// .unwrap_or_default()` shape.
210    ///
211    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
212    /// the `humantime::parse_duration(&<field>).ok()` shape recurred
213    /// at ONE substrate owner + ONE hand-authored peer site past the
214    /// ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted onto
215    /// TWO peer inherent methods on peer spec types here + on
216    /// [`crate::lifetime::EphemeralLifetime::ttl_duration`]).
217    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
218    /// the pins below bind the parse-failure corner, the empty-ttl
219    /// corner, the humantime edge shapes, the return-form parity with
220    /// [`crate::lifetime::EphemeralLifetime::ttl_duration`], and the
221    /// byte-identical parity with the pre-lift `.ok()` chain on the
222    /// SAME `spec.free_ttl` value, so a regression that drifts any
223    /// surface fails at `tests::pool_spec_free_ttl_duration_*` here
224    /// rather than as silent operator-facing skew between the pool
225    /// stale-free bucket loop and the ephemeral TTL-expiry gate on
226    /// the two peer humantime-string fields).
227    #[must_use]
228    pub fn free_ttl_duration(&self) -> Option<std::time::Duration> {
229        humantime::parse_duration(&self.free_ttl).ok()
230    }
231
232    /// Compose a [`PoolSpec`] for the given member `template`, stamping
233    /// every non-template slot at the `#[serde(default …)]` value the
234    /// wire-schema publishes above — the ONE substrate composer that
235    /// closes the 11-slot `PoolSpec { desired_size: 1, min_size: 0,
236    /// max_size: 0, return_policy: ReturnPolicy::Replace, selector:
237    /// PoolSelector::default(), template, free_ttl: "24h".into(),
238    /// max_allocation_ttl: "4h".into(), desired: 0, replacement_policy:
239    /// Default::default(), stable_name_claim: false }` struct-literal
240    /// every test-side + reconciler-side seed hand-authored pre-lift.
241    ///
242    /// Sibling to [`crate::crd::ProcessSpec::gate_compute_defaults`] on
243    /// the (spec-type × full-baseline-composer) axis — that primitive
244    /// owns the 11-slot [`crate::crd::ProcessSpec`] baseline composer;
245    /// this one owns the peer 11-slot [`PoolSpec`] baseline composer.
246    /// Both take a caller-supplied slot (there: the classification
247    /// baseline via `Classification::gate_compute()`; here: the
248    /// `template` [`EphemeralSpec`], which has no natural default) and
249    /// fill every other slot at its wire-published default so a caller
250    /// composes with struct-update syntax (`PoolSpec { desired_size: 1,
251    /// ..PoolSpec::with_template(empty_template()) }`) rather than
252    /// re-spelling the 10 defaulted slots at every seed. A future
253    /// promotion of a defaulted slot to a non-default (a per-fleet
254    /// minimum `min_size` floor, a shifted `default_free_ttl`,
255    /// a widened `ReturnPolicy` default) lands at ONE substrate
256    /// composer here and every downstream seed inherits the upgrade
257    /// mechanically.
258    ///
259    /// Pre-lift the 11-slot struct-literal was hand-authored at EIGHT
260    /// sites across TWO crates past the ★★ PRIME-DIRECTIVE ≥ 2
261    /// duplication trigger:
262    /// * `tatara-process::lib::tests::pool_fixture` — the
263    ///   `qualified_process_ref` + trait-pin fixture seed;
264    /// * `tatara-process::lib::tests::empty_pool_spec` (×2) — the two
265    ///   sibling fixtures inside separate pin modules;
266    /// * `tatara-process::pool::tests::pool_spec` — the `name_or_empty`
267    ///   / `namespace_or_empty` pin fixture;
268    /// * `tatara-pool-reconciler::router::tests::pool` — the router-
269    ///   candidate-arbiter pin fixture (overrides `selector`);
270    /// * `tatara-pool-reconciler::desired::tests::pool_with_desired` —
271    ///   the desired-count-loop pin fixture (overrides `desired` +
272    ///   `replacement_policy`);
273    /// * `tatara-pool-reconciler::pool_decide::tests::pool` — the
274    ///   pure-decision pin fixture (overrides sizes);
275    /// * `tatara-pool-reconciler::allocation_decide::tests::pool` —
276    ///   the allocation-router pin fixture (overrides `selector`).
277    ///
278    /// The three fields the wire-schema does NOT default (`desired_size`
279    /// carries no `#[serde(default)]` above; `template` is the caller-
280    /// supplied slot) are stamped at their operator-friendly seed
281    /// values here — `desired_size = 0` matches every other reset
282    /// slot's `0` / `false` / `Default` stamp, so a caller can compose
283    /// `PoolSpec { desired_size: 1, ..PoolSpec::with_template(t) }` for
284    /// the single-slot pool the majority of pre-lift seeds spelled, or
285    /// `PoolSpec { desired_size: 0, desired: 5, ..with_template(t) }`
286    /// for the desired-count-loop shape one seed spelled.
287    ///
288    /// Theory anchor: THEORY.md §VI.1 (generation over composition — the
289    /// 11-slot [`PoolSpec`] struct-literal recurred at EIGHT hand-
290    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
291    /// trigger and is lifted onto ONE workspace-wide owner here).
292    /// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
293    /// regression that drifted a wire-published default at only one
294    /// consumer, or that broke the sibling-default correspondence with
295    /// [`crate::crd::ProcessSpec::gate_compute_defaults`], surfaces at
296    /// this primitive's tests rather than as silent operator-visible
297    /// skew across the eight fixtures whose assertions key on the
298    /// shape).
299    #[must_use]
300    pub fn with_template(template: EphemeralSpec) -> Self {
301        Self {
302            desired_size: 0,
303            min_size: 0,
304            max_size: 0,
305            return_policy: ReturnPolicy::default(),
306            selector: PoolSelector::default(),
307            template,
308            free_ttl: default_free_ttl(),
309            max_allocation_ttl: default_max_allocation_ttl(),
310            desired: 0,
311            replacement_policy: ReplacementPolicy::default(),
312            stable_name_claim: false,
313        }
314    }
315}
316
317impl EphemeralPool {
318    /// Borrow-form metadata-projection primitive on the `metadata.name`
319    /// axis of `EphemeralPool`: returns the K8s object name slice with
320    /// the missing-name corner collapsed to the load-bearing empty-string
321    /// sentinel — the ONE-liner collapse of the paired
322    /// `self.metadata.name.as_deref().unwrap_or("")` incantation every
323    /// pool-side consumer restated by hand pre-lift.
324    ///
325    /// Pre-lift the `.metadata.name.as_deref().unwrap_or("")` chain
326    /// was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
327    /// duplication threshold in `tatara-pool-reconciler`, both keyed
328    /// by the pool's own name slot:
329    /// * `router::pool_name` — the tie-break comparator inside
330    ///   `best_match`; a deterministic lexicographic-min-name arbiter
331    ///   across two pool candidates whose specificity scores tie.
332    /// * `controller_allocation::reconcile_inner` — the `HashMap<
333    ///   pool-name, Vec<PoolMember>>` lookup closure fed into
334    ///   `decide_allocation_reconcile`; keys the "which pool members
335    ///   back this allocation candidate?" projection at every
336    ///   allocation-reconcile pass.
337    ///
338    /// Both sites walked the SAME `.as_deref().unwrap_or("")` chain
339    /// and both wanted the `&str` form the primitive returns — as a
340    /// borrow suitable for lexicographic `str::cmp` in the tie-break
341    /// AND for the `HashMap<String, _>::get(&str)` lookup. Post-lift
342    /// each caller reaches for `pool.name_or_empty()` and the produced
343    /// slice feeds the same downstream comparator / lookup unchanged.
344    ///
345    /// The empty-string fallback is the SAME sentinel the sibling
346    /// borrow-form primitive [`crate::crd::Process::uid_or_empty`]
347    /// returns AND the SAME sentinel the owned-form sibling
348    /// [`crate::crd::Process::owned_name_or_empty`] returns on the
349    /// `metadata.name` axis of the sister CRD — the three primitives
350    /// partition the (borrow-form × owned-form) × (uid × name) corner
351    /// of the metadata-slot family on identical fallback semantics
352    /// (empty string means "the slot is unset"), so a consumer that
353    /// switches between the CRD surfaces based on downstream keying
354    /// requirements never sees a different missing-slot spelling as
355    /// a side effect.
356    ///
357    /// Return-form axis: `&str` mirrors the borrow-first discipline
358    /// of the peer metadata primitives on `Process`
359    /// ([`crate::crd::Process::namespace_or_default`],
360    /// [`crate::crd::Process::name_or_placeholder`],
361    /// [`crate::crd::Process::uid_or_empty`]). The one missing-slot
362    /// corner the chain swallowed pre-lift (missing `metadata.name`)
363    /// collapses to the empty-string sentinel so `str::is_empty` /
364    /// `HashMap::get` on an unnamed pool behaves identically to what
365    /// the pre-lift `.as_deref().unwrap_or("")` chain produced.
366    ///
367    /// A future normalization step (a name-canonicalization pass, a
368    /// case-fold key builder, a per-cluster prefix stripper for
369    /// cross-cluster pool-name aliasing) lands at ONE substrate
370    /// method here and both downstream consumers pick up the upgrade
371    /// mechanically — no per-callsite hand-edit at `pool_name` /
372    /// `reconcile_inner`.
373    ///
374    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
375    /// the `.metadata.name.as_deref().unwrap_or("")` chain recurred
376    /// at two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
377    /// duplication trigger, and is lifted to ONE owner here).
378    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
379    /// the pins bind the missing-name corner + the empty-string
380    /// sentinel byte-shape + the borrow-form `&str` lifetime + the
381    /// byte-identical parity with the pre-lift chain + the fallback-
382    /// value coherence with `Process::uid_or_empty` /
383    /// `Process::owned_name_or_empty` on the metadata-slot × empty-
384    /// sentinel axis, so a regression that drifted any surface at
385    /// `tests::name_or_empty_*` here rather than as silent operator-
386    /// facing skew between the router tie-break and the allocation
387    /// member-lookup on the SAME pool candidate).
388    pub fn name_or_empty(&self) -> &str {
389        self.metadata.name.as_deref().unwrap_or("")
390    }
391
392    /// Owned-form metadata-projection primitive on the `metadata.name`
393    /// axis of `EphemeralPool`: returns an owned `String` copy of the K8s
394    /// object name with the missing-name corner collapsed to the load-
395    /// bearing empty-string sentinel — the ONE-liner collapse of the
396    /// paired `self.metadata.name.clone().unwrap_or_default()` incantation
397    /// every pool-side consumer restated by hand pre-lift.
398    ///
399    /// Pre-lift the `.metadata.name.clone().unwrap_or_default()` chain
400    /// was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
401    /// duplication threshold in `tatara-pool-reconciler`, both keyed by
402    /// the pool's own name slot in an `owned String` context:
403    /// * `controller_allocation::reconcile_inner` — the
404    ///   `HashMap<String, Vec<PoolMember>>` key seed inside a
405    ///   `pools.iter().map(|p| ...).collect()` fanout; the map key is
406    ///   the owned `String` form because the produced `HashMap<String, _>`
407    ///   outlives the pool-list borrow that generated it and the
408    ///   downstream `pool_members.get(pool.name_or_empty())` closure
409    ///   consumes it as `&str`.
410    /// * `allocation_decide::AllocationConvergenceCtx::observe` — the
411    ///   `AllocationRef::name` slot seed stamped on the matched-pool
412    ///   handle; the struct literal is `AllocationRef { name: String,
413    ///   namespace: String }` and the produced value is threaded through
414    ///   the `Decision::decide` transition rule downstream.
415    ///
416    /// Both sites walked the SAME `.clone().unwrap_or_default()` chain
417    /// and both wanted the `String` form the primitive returns — as the
418    /// owned key of a `HashMap<String, _>` and as the `String` slot of
419    /// an `AllocationRef` struct literal. Post-lift each callsite reads
420    /// `pool.owned_name_or_empty()` and the produced value feeds the
421    /// same downstream key / struct-literal slot unchanged.
422    ///
423    /// The empty-string fallback is the SAME sentinel the sibling
424    /// borrow-form primitive [`Self::name_or_empty`] returns AND the
425    /// SAME sentinel the sibling owned-form primitive
426    /// [`crate::crd::Process::owned_name_or_empty`] returns on the
427    /// `metadata.name` axis of the sister CRD — the three primitives
428    /// partition the (borrow-form × owned-form) corner of the metadata-
429    /// name family across BOTH tatara-process CRDs on identical missing-
430    /// slot semantics (empty string means "the slot is unset"), so a
431    /// consumer that switches between the CRD surfaces based on
432    /// downstream ownership requirements never sees a different
433    /// missing-slot spelling as a side effect.
434    ///
435    /// Peer to [`Self::name_or_empty`] on the (return-form × ownership)
436    /// axis pair — closes the corner the pool-side family previously
437    /// left open:
438    ///
439    /// * borrow + empty sentinel → [`Self::name_or_empty`] (router tie-
440    ///   break comparator, `HashMap<String, _>::get(&str)` lookup —
441    ///   consumers whose downstream keys by `&str` and allocates
442    ///   nothing);
443    /// * owned + empty sentinel → **this method** (HashMap-key seed in
444    ///   an outliving-borrow context, `AllocationRef::name` struct-
445    ///   literal slot — consumers whose downstream requires the owned
446    ///   `String` form because the produced value outlives the source-
447    ///   pool borrow).
448    ///
449    /// A future normalization step (a name-canonicalization pass, a
450    /// case-fold key builder, a per-cluster prefix stripper for cross-
451    /// cluster pool-name aliasing) lands at ONE substrate method here
452    /// and both downstream consumers pick up the upgrade mechanically —
453    /// no per-callsite hand-edit at `reconcile_inner` /
454    /// `AllocationConvergenceCtx::observe`.
455    ///
456    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
457    /// the `.metadata.name.clone().unwrap_or_default()` chain recurred
458    /// at two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
459    /// duplication trigger, and is lifted to ONE owner here).
460    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
461    /// the pins bind the missing-name corner + the empty-string
462    /// sentinel byte-shape + the owned-form `String` return type + the
463    /// byte-identical parity with the pre-lift chain + the fallback-
464    /// value coherence with [`Self::name_or_empty`] +
465    /// [`crate::crd::Process::owned_name_or_empty`] on the metadata-
466    /// slot × empty-sentinel axis, so a regression that drifted any
467    /// surface at `tests::owned_name_or_empty_*` here rather than as
468    /// silent operator-facing skew between the pool-members lookup key
469    /// and the AllocationRef seed on the SAME pool candidate).
470    pub fn owned_name_or_empty(&self) -> String {
471        self.metadata.name.clone().unwrap_or_default()
472    }
473
474    /// Copy-form metadata-projection primitive on the deletion-tombstone
475    /// axis of `EphemeralPool`: returns `true` iff the K8s API server
476    /// has stamped a `metadata.deletionTimestamp` on this pool (the
477    /// moment the object entered the "being deleted" corner of its
478    /// lifecycle, after which further mutating writes are refused and
479    /// finalizers are drained before the object is actually removed) —
480    /// the ONE-liner collapse of the paired
481    /// `self.metadata.deletion_timestamp.is_some()` incantation every
482    /// pool-side consumer restated by hand pre-lift.
483    ///
484    /// Pre-lift the `.metadata.deletion_timestamp.is_some()` chain was
485    /// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
486    /// duplication threshold in `tatara-pool-reconciler`, both
487    /// projecting the SAME tombstone-presence predicate on an
488    /// `EphemeralPool` value:
489    /// * `pool_decide::decide_pool_reconcile` — the pure decision
490    ///   function's deletion-preempt gate that forces
491    ///   [`PoolDecision::Drain`] as soon as the API server stamps
492    ///   the tombstone, before the (desired vs actual) supply-arithmetic
493    ///   branches get a chance to run. Wired at the very top of the
494    ///   decision so a draining pool never spawns / reaps / expires
495    ///   through the normal replenishment arithmetic while the
496    ///   deletion is in flight.
497    /// * `controller_pool::pool_phase_from_members` — the observed-
498    ///   phase composer's tombstone-first arm that returns
499    ///   [`PoolPhase::Draining`] regardless of the supply / demand
500    ///   arithmetic that would otherwise pick `Ready` / `Scaling` /
501    ///   `Degraded`. Keeps the reported phase honest during the
502    ///   finalizer drain so operators reading `kubectl get
503    ///   ephemeralpools` see the tombstone-present state as
504    ///   `Draining`, not as a stale `Ready`.
505    ///
506    /// Both sites walked the SAME `.metadata.deletion_timestamp
507    /// .is_some()` chain and both wanted the `bool` form the primitive
508    /// returns — the `decide_pool_reconcile` site to gate the
509    /// `→ Drain` short-circuit and the `pool_phase_from_members` site
510    /// to gate the `→ Draining` short-circuit. Post-lift each callsite
511    /// reads `pool.is_being_deleted()` and the produced `bool` feeds
512    /// the same downstream short-circuit unchanged.
513    ///
514    /// Sibling to [`crate::crd::Process::is_being_deleted`] on the
515    /// deletion-tombstone axis of the sister CRD — the two primitives
516    /// now partition the tombstone-presence probe across BOTH
517    /// tatara-process CRDs on identical missing-slot semantics
518    /// (present timestamp means "the API server has begun deletion"),
519    /// so an operator or reconciler that switches between the CRD
520    /// surfaces never sees a different tombstone-detection spelling
521    /// as a side effect.
522    ///
523    /// Return-form axis: `bool` matches the copy-form discipline of
524    /// the sibling [`crate::crd::Process::is_being_deleted`] and of
525    /// the pool-side [`crate::phase::ProcessPhase::is_alive`] +
526    /// [`Self::name_or_empty`]-family primitives — the underlying
527    /// slot is a wire-format `Option<Time>` that carries only
528    /// presence information at this axis (the RFC-3339 timestamp
529    /// payload itself is not what the two consumers read; both only
530    /// probe presence to detect the tombstone-stamped state).
531    /// Returning the raw `Option<&Time>` would push the `.is_some()`
532    /// probe back to every callsite, restating the pre-lift chain
533    /// one link shorter without collapsing the primitive.
534    ///
535    /// Peer to [`Self::name_or_empty`] and [`Self::owned_name_or_empty`]
536    /// on the metadata-projection axis for `EphemeralPool`; this method
537    /// opens the presence-probe corner for the tombstone slot. Future
538    /// metadata-presence projections on the pool CRD (an
539    /// `is_being_finalized` projection on
540    /// `metadata.finalizers.is_empty()`'s negation, a `has_owner`
541    /// projection on `metadata.owner_references.is_empty()`'s
542    /// negation) land as peer methods on this same axis.
543    ///
544    /// A future normalization step (a per-tombstone staleness gate
545    /// that returns `false` for a tombstone older than the reconciler's
546    /// grace-period budget, a canonicalization pass that treats a
547    /// tombstone from a paused controller as absent, a cross-cluster
548    /// tombstone-observation clock skew guard) lands at ONE substrate
549    /// method here and both downstream consumers pick up the upgrade
550    /// mechanically — no per-callsite hand-edit at
551    /// `decide_pool_reconcile` / `pool_phase_from_members`.
552    ///
553    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
554    /// the `.metadata.deletion_timestamp.is_some()` chain recurred at
555    /// two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
556    /// duplication trigger, and is lifted to ONE owner here).
557    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
558    /// the pins bind the missing-tombstone corner + the present-
559    /// tombstone corner + the copy-form `bool` return + the byte-
560    /// identical parity with the pre-lift `.is_some()` chain + the
561    /// cross-CRD coherence with `crate::crd::Process::is_being_deleted`
562    /// on the tombstone axis, so a regression that drifted any surface
563    /// at `tests::is_being_deleted_*` rather than as silent operator-
564    /// facing skew between the pool-reconciler's `→ Drain` decision
565    /// and the observed-phase composer's `→ Draining` report on the
566    /// SAME `EphemeralPool` within one reconcile pass).
567    pub fn is_being_deleted(&self) -> bool {
568        self.metadata.deletion_timestamp.is_some()
569    }
570
571    /// Owned-form metadata-projection primitive on the `metadata.namespace`
572    /// axis of `EphemeralPool`: returns an owned `String` copy of the K8s
573    /// namespace with the missing-namespace corner collapsed to the load-
574    /// bearing empty-string sentinel — the ONE-liner collapse of the
575    /// paired `self.metadata.namespace.clone().unwrap_or_default()`
576    /// incantation every pool-side consumer restated by hand pre-lift.
577    ///
578    /// Pre-lift the `.metadata.namespace.clone().unwrap_or_default()`
579    /// chain was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE
580    /// ≥ 2 duplication threshold, both stamping the `AllocationRef
581    /// { namespace: String, .. }` slot inside an owned-`String` context:
582    /// * `tatara-pool-reconciler::allocation_decide::AllocationConvergenceCtx
583    ///   ::observe` — the matched-pool seed's `AllocationRef.namespace`
584    ///   slot, right beside the peer [`Self::owned_name_or_empty`] call
585    ///   that owns the paired name half. This is the exact site the
586    ///   pre-existing peer-primitive doc-comment forecast (`"a future
587    ///   run may lift owned_namespace_or_empty as the sibling axis
588    ///   peer"`).
589    /// * `crate::pool::tests::allocation_ref_new_composes_with_owned_name_or_empty_pool_projection`
590    ///   — the composition pin that seeded an `AllocationRef` from the
591    ///   same paired-primitive-half construction the production consumer
592    ///   in `allocation_decide::observe` performs. Post-lift the pin
593    ///   composes two peer primitives (`owned_name_or_empty` +
594    ///   `owned_namespace_or_empty`) rather than one primitive plus the
595    ///   pre-lift chain, sharpening it from a mixed-form composition
596    ///   check into a paired-primitive-family composition check.
597    ///
598    /// Both sites walked the SAME `.clone().unwrap_or_default()` chain
599    /// and both wanted the `String` form the primitive returns — as the
600    /// `String` slot of an `AllocationRef` struct literal built through
601    /// [`crate::pool::AllocationRef::new`]. Post-lift each callsite reads
602    /// `pool.owned_namespace_or_empty()` and the produced value feeds
603    /// the same downstream `AllocationRef` slot unchanged.
604    ///
605    /// The empty-string fallback is the SAME sentinel the sibling owned-
606    /// form primitive [`Self::owned_name_or_empty`] returns on the
607    /// `metadata.name` axis of the same CRD — the two primitives now
608    /// partition the (owned `String` × `metadata.<slot>`) corner of the
609    /// pool CRD's metadata family across BOTH object-coordinate slots
610    /// on identical missing-slot semantics (empty string means "the
611    /// slot is unset"), so the [`crate::pool::AllocationRef::new`]
612    /// composer sees a coherent owned-empty pair regardless of which
613    /// slot is absent on the source pool. Coherent with the workspace-
614    /// wide owned-empty sentinel that the peer primitives
615    /// [`crate::crd::Process::uid_or_empty`],
616    /// [`crate::crd::Process::owned_name_or_empty`],
617    /// [`Self::name_or_empty`], and [`Self::owned_name_or_empty`]
618    /// already share on the metadata-slot × empty-sentinel axis.
619    ///
620    /// Peer to [`Self::owned_name_or_empty`] on the
621    /// (`metadata.name` × `metadata.namespace`) axis of the owned-form
622    /// projection family — closes the corner the pool-side family
623    /// previously left open:
624    ///
625    /// * owned + name + empty sentinel → [`Self::owned_name_or_empty`]
626    ///   (`AllocationRef.name` seed, `HashMap<String, _>` key seed);
627    /// * owned + namespace + empty sentinel → **this method**
628    ///   (`AllocationRef.namespace` seed — the paired half the same
629    ///   `AllocationRef::new(name, namespace)` constructor consumes);
630    /// * copy + deletion + tombstone probe → [`Self::is_being_deleted`]
631    ///   (the presence-probe corner of the same metadata axis, already
632    ///   opened).
633    ///
634    /// A future normalization step (a namespace-canonicalization pass,
635    /// a case-fold key builder, a per-cluster prefix stripper, or the
636    /// canonical-namespace default lift that would substitute
637    /// [`crate::crd::Process::DEFAULT_NAMESPACE`] on the missing-slot
638    /// corner rather than the empty-string sentinel) lands at ONE
639    /// substrate method here and both downstream consumers pick up the
640    /// upgrade mechanically — no per-callsite hand-edit at
641    /// `AllocationConvergenceCtx::observe` / the composition pin.
642    ///
643    /// The empty-string fallback (rather than
644    /// [`crate::crd::Process::DEFAULT_NAMESPACE`]) is DELIBERATELY
645    /// pinned: the sole downstream consumer
646    /// (`AllocationConvergenceCtx::observe`'s matched-pool seed) feeds
647    /// the produced value into `AllocationRef.namespace`, which is then
648    /// matched byte-identically against `spec.pool_ref.namespace` at
649    /// [`crate::pool::allocation_decide::resolve_pool`]-style comparators.
650    /// A silent substitution of `"default"` at this primitive would
651    /// alias every namespace-absent pool to the `"default"` bucket at
652    /// the matcher, hiding the missing-slot corner from an operator
653    /// who explicitly authored an allocation against a namespace-
654    /// unset pool. The load-bearing empty-string sentinel keeps the
655    /// pre-lift `.clone().unwrap_or_default()` shape verbatim so the
656    /// downstream matcher's byte-comparison stays honest.
657    ///
658    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
659    /// the `.metadata.namespace.clone().unwrap_or_default()` chain
660    /// recurred at two hand-authored sites past the ★★ PRIME-DIRECTIVE
661    /// ≥ 2 duplication trigger, and is lifted to ONE owner here).
662    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
663    /// the pins bind the missing-namespace corner + the empty-string
664    /// sentinel byte-shape + the owned-form `String` return type + the
665    /// byte-identical parity with the pre-lift chain + the fallback-
666    /// value coherence with [`Self::owned_name_or_empty`] on the
667    /// paired-slot axis, so a regression that drifted any surface at
668    /// `tests::owned_namespace_or_empty_*` rather than as silent
669    /// operator-facing skew between the paired name / namespace halves
670    /// of the SAME `AllocationRef` seed).
671    pub fn owned_namespace_or_empty(&self) -> String {
672        self.metadata.namespace.clone().unwrap_or_default()
673    }
674
675    /// Compound owned-form metadata-projection primitive on the paired
676    /// `(metadata.uid, metadata.name)` axis of `EphemeralPool`: returns
677    /// a stable owned `String` seed for slot-slug derivation, PREFERRING
678    /// the K8s-assigned uid, FALLING BACK to the pool's own name, then
679    /// SINKING to the load-bearing empty-string sentinel when both slots
680    /// are absent — the ONE-liner collapse of the paired
681    /// `pool.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
682    /// incantation every pool-slot-name-composing consumer restated by
683    /// hand pre-lift.
684    ///
685    /// Pre-lift the `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
686    /// chain was hand-authored at TWO production sites past the ★★
687    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
688    /// `tatara-pool-reconciler::controller_pool`, both feeding the SAME
689    /// `member_process_name(&pool_name, &pool_uid_or_name_fallback, slot)`
690    /// composer:
691    /// * `reconcile_inner` — the desired-count `PoolDecision::Spawn`
692    ///   arm's spawn-loop slot-slug seed (fallback bound as
693    ///   `|| name.clone()` from the extracted-earlier owned `name`
694    ///   half of `owned_coordinates_required()`).
695    /// * `apply_convergence_actions` — the legacy allocation-driven
696    ///   `ConvergenceAction::CreateMember` arm's slot-slug seed
697    ///   (fallback bound as `|| name.to_string()` from the borrowed
698    ///   `name: &str` parameter that the same
699    ///   `owned_coordinates_required()`-extracted `String` was passed
700    ///   through by reference).
701    ///
702    /// Both sites computed the SAME "prefer the k8s uid; fall back to
703    /// the pool's own name" projection on the SAME `EphemeralPool`
704    /// value, differing only in the surface syntax of the fallback
705    /// (`.clone()` vs `.to_string()`) — a per-callsite typing artefact
706    /// of the enclosing scope's `name` binding rather than a semantic
707    /// distinction. Post-lift each callsite reads
708    /// `pool.owned_uid_or_name_or_empty()` and the produced owned
709    /// `String` feeds the same `member_process_name(&name, &_, slot)`
710    /// composer verbatim; the caller no longer threads its own local
711    /// `name` handle through as the fallback, since the primitive
712    /// reaches through the same `self.metadata.name` slot the caller
713    /// extracted from earlier — coherent by construction with the
714    /// sibling primitive [`Self::owned_name_or_empty`] on the missing-
715    /// name corner.
716    ///
717    /// The compound (uid-preferred, name-fallback, empty-sentinel)
718    /// precedence is DELIBERATELY pinned: the K8s API server stamps
719    /// `metadata.uid` on every persisted object at admission time, so
720    /// the reachable state at both callsites (each already gated by
721    /// `owned_coordinates_required()?`) has `uid = Some(_)`. The name
722    /// fallback is a load-bearing safety net for the vanishingly rare
723    /// pre-admission-uid corner + the unit-test path that constructs
724    /// an `EphemeralPool` value in-memory without stamping a uid; the
725    /// empty-string sink is the sentinel-coherent complement of the
726    /// missing-both corner (both slots `None`) so a regression that
727    /// dropped either fallback surfaces as a compiler-visible test
728    /// failure rather than as an operator-facing skew between spawn
729    /// slots derived from mixed-fallback seeds within one reconcile
730    /// pass. Coherent with the workspace-wide owned-empty sentinel
731    /// that the peer primitives [`Self::owned_name_or_empty`],
732    /// [`Self::owned_namespace_or_empty`],
733    /// [`crate::crd::Process::owned_name_or_empty`], and
734    /// [`crate::crd::Process::uid_or_empty`] already share on the
735    /// metadata-slot × empty-sentinel axis.
736    ///
737    /// A future normalization step (a per-cluster uid-prefix stripper,
738    /// a case-fold key builder, canonicalization of a suspiciously-
739    /// empty uid to the name fallback, a namespace-scoped hashing pass
740    /// that mixes cluster identity into the seed) lands at ONE
741    /// substrate method here and both downstream `spawn` /
742    /// `apply_convergence_actions` consumers pick up the upgrade
743    /// mechanically — no per-callsite hand-edit at `controller_pool`.
744    ///
745    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
746    /// the `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
747    /// chain recurred at two hand-authored sites past the ★★ PRIME-
748    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
749    /// here). THEORY.md §II.1 invariant 5 (composition preserves
750    /// proofs — the pins bind the uid-present corner + the uid-absent
751    /// name-fallback corner + the both-absent empty-sentinel corner +
752    /// the owned-form `String` return type + the byte-identical parity
753    /// with each pre-lift callsite's fallback surface, so a regression
754    /// that drifted any surface at `tests::owned_uid_or_name_or_empty_*`
755    /// rather than as silent operator-facing skew between the two
756    /// slot-slug seeds within ONE reconcile pass).
757    pub fn owned_uid_or_name_or_empty(&self) -> String {
758        self.metadata
759            .uid
760            .clone()
761            .unwrap_or_else(|| self.owned_name_or_empty())
762    }
763
764    /// Copy-form metadata-projection primitive on the `metadata.name`
765    /// axis of `EphemeralPool` in its `presence-and-equal` corner:
766    /// returns `true` iff the K8s object name slot is BOTH `Some(_)`
767    /// AND byte-identical to the supplied candidate — the ONE-liner
768    /// collapse of the paired
769    /// `self.metadata.name.as_deref() == Some(candidate)` incantation
770    /// every pool-side lookup consumer restated by hand pre-lift.
771    ///
772    /// Pre-lift the `.metadata.name.as_deref() == Some(<candidate>)`
773    /// chain was hand-authored at TWO production sites past the ★★
774    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
775    /// `tatara-pool-reconciler`, both keyed by the `EphemeralPool`'s
776    /// own name slot inside a `candidate_pools.iter().find(|p| ...)`
777    /// closure that resolves a pool from an `AllocationRef.name` half:
778    /// * `allocation_decide::resolve_pool` — the explicit-`pool_ref`
779    ///   half of the pool-resolution ladder, one of two conjuncts in
780    ///   the `(name == X && namespace == Y)` byte-comparison against
781    ///   `AllocationSpec::pool_ref`. Pairs with the sibling namespace
782    ///   comparison (a future run may lift `has_namespace` as the
783    ///   paired-axis peer once a second namespace-probe site opens).
784    /// * `controller_allocation::reconcile_inner` — the TTL-inheritance
785    ///   fallback path's pool-lookup by `AllocationDecision::Bind::pool
786    ///   .name`, feeding the matched pool's `spec.template.ttl` into
787    ///   the just-bound member Process's lifetime overlay.
788    ///
789    /// Both sites walked the SAME `.as_deref() == Some(<x>.as_str())`
790    /// chain against a `&str` candidate held by an [`AllocationRef`]
791    /// or a similar owned-name handle, and both wanted the `bool`
792    /// form the primitive returns — the transition rule's discriminant
793    /// on either the `find(|p| p.has_name(&pool_ref.name))` closure
794    /// (which either matches ONE candidate pool or none) or the
795    /// TTL-inheritance closure's short-circuit through
796    /// `.map(...).unwrap_or_else(...)`. Post-lift each callsite reads
797    /// `p.has_name(&candidate)` and the produced `bool` feeds the same
798    /// downstream `find` / `map` closure unchanged.
799    ///
800    /// Distinct in semantics from the sibling primitive
801    /// [`Self::name_or_empty`] on the SAME `metadata.name` axis: the
802    /// `_or_empty` family folds the missing-slot corner to the load-
803    /// bearing empty-string sentinel (so `None` and `Some("")` both
804    /// project to `""`), whereas this primitive keeps `None` distinct
805    /// from `Some("")` at the `==` operator — a `None` slot returns
806    /// `false` even when the candidate is the empty string. That
807    /// discipline is load-bearing at both consumer sites: pre-lift
808    /// they compared `Option<&str>` against `Some(<candidate>)`, so a
809    /// substitution through `Self::name_or_empty` would silently
810    /// promote a namespace-absent pool with a `""` candidate into a
811    /// spurious match at the `find` closure, aliasing every unnamed
812    /// pool to the same lookup bucket at the resolver. Preserving the
813    /// `None ⇒ false` corner keeps the resolver's byte-comparison
814    /// honest.
815    ///
816    /// Peer to the sibling substrate primitives already opened on the
817    /// pool-side (`metadata.name` × return-form) axis:
818    /// * borrow-form + empty sentinel → [`Self::name_or_empty`] (`&str`
819    ///   projection with a `""` fallback for missing / explicitly-empty
820    ///   name slots; router tie-break comparator);
821    /// * owned-form + empty sentinel → [`Self::owned_name_or_empty`]
822    ///   (`String` projection with a `""` fallback; `AllocationRef.name`
823    ///   seed);
824    /// * **presence-and-equal probe → this method** (`bool` projection
825    ///   with `None`-preserving semantics; pool-lookup closure
826    ///   discriminant).
827    ///
828    /// A future normalization step (a name-canonicalization pass, a
829    /// case-fold key builder, a per-cluster prefix stripper for cross-
830    /// cluster pool-name aliasing, or a canonical-namespace default
831    /// lift) lands at ONE substrate method here and both downstream
832    /// consumers pick up the upgrade mechanically — no per-callsite
833    /// hand-edit at `resolve_pool` / `controller_allocation
834    /// ::reconcile_inner`.
835    ///
836    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
837    /// the `.metadata.name.as_deref() == Some(<candidate>)` chain
838    /// recurred at two hand-authored sites past the ★★ PRIME-
839    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
840    /// here). THEORY.md §II.1 invariant 5 (composition preserves
841    /// proofs — the pins bind the missing-slot corner (`None ⇒
842    /// false`, even against a `""` candidate) + the populated-slot
843    /// equal corner + the populated-slot unequal corner + the
844    /// byte-identical parity with the pre-lift `.as_deref() == Some
845    /// (<candidate>)` chain + the disjoint semantics vs. the
846    /// `_or_empty` sibling family, so a regression that drifted any
847    /// surface at `tests::has_name_*` here rather than as silent
848    /// operator-facing skew between the two `find` closures the
849    /// primitive owns).
850    #[must_use]
851    pub fn has_name(&self, candidate: &str) -> bool {
852        self.metadata.name.as_deref() == Some(candidate)
853    }
854
855    /// The namespaced-CRD constructor composer on the `EphemeralPool`
856    /// axis: forwards `(name, spec)` to the kube-derived
857    /// [`Self::new`] constructor + stamps `metadata.namespace` with
858    /// the caller-supplied slot in ONE step. The ONE-liner collapse
859    /// of the paired `let mut p = EphemeralPool::new(<name>, <spec>);
860    /// p.meta_mut().namespace = Some(<ns>.into());` incantation every
861    /// pool-side test fixture restated by hand pre-lift.
862    ///
863    /// Pre-lift the 2-line construct-then-set-namespace chain was
864    /// hand-authored at FOUR sites past the ★★ PRIME-DIRECTIVE ≥ 2
865    /// duplication threshold in `tatara-pool-reconciler`, all
866    /// composing a namespaced `EphemeralPool` fixture from a `name`
867    /// slot and a `PoolSpec`:
868    /// * `router::pool` — the selector-routing test fixture pinned
869    ///   to `"ephemeral-pools"`.
870    /// * `pool_decide::pool` — the desired-count-loop test fixture
871    ///   pinned to `"pools"`.
872    /// * `desired::pool` — the replacement-policy test fixture
873    ///   pinned to `"pools"`.
874    /// * `allocation_decide::pool` — the allocation-decision test
875    ///   fixture pinned to the caller-supplied `ns` slot.
876    ///
877    /// All four sites walked the SAME 2-line chain and all four
878    /// wanted the `EphemeralPool` back with `metadata.namespace`
879    /// stamped as `Some(<ns>.into())`. Post-lift each callsite reads
880    /// `EphemeralPool::new_in(<name>, <ns>, <spec>)` and the produced
881    /// value feeds the same downstream reconciler-input `Vec<
882    /// EphemeralPool>` unchanged.
883    ///
884    /// The `impl Into<String>` at the `namespace` slot matches the
885    /// sibling `impl Into<String>`-widening discipline the workspace's
886    /// other namespaced-CRD-adjacent composers walk
887    /// ([`crate::pool::PoolMember::unallocated`] on the
888    /// `process_name` slot, [`crate::pool::AllocationRef::new`] on the
889    /// `(name, namespace)` slot pair, [`crate::allocation::
890    /// Requestor::kind_only`] on the `kind` slot) and accepts BOTH
891    /// `&'static str` (the majority pre-lift caller shape) AND owned
892    /// `String` at the SAME signature.
893    ///
894    /// Peer to [`crate::allocation::EphemeralAllocation::new_in`] on
895    /// the sister `EphemeralAllocation` CRD — the two primitives
896    /// partition the namespaced-CRD-constructor family axis for the
897    /// two pool-adjacent CRDs the workspace stamps at reconciler
898    /// fixture / GitHub-webhook-emitter time. A future normalization
899    /// (a per-fleet virtual-cluster prefix rewrite on the `namespace`
900    /// slot, a per-cluster canonical case-fold pass, a
901    /// `generateName` fallback on the `name` slot, an operator-scoped
902    /// default namespace for cluster-local test rigs, an audit-tag
903    /// stamped on every fixture-emitted CRD for post-hoc grep
904    /// discipline) lands at ONE primitive body per CRD and every
905    /// downstream fixture consumer inherits the upgrade mechanically.
906    ///
907    /// `#[must_use]` on the return keeps a caller from composing the
908    /// namespaced value and dropping it un-passed to a reconciler-
909    /// input slot or an assertion helper.
910    ///
911    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
912    /// the 2-line construct-then-set-namespace chain recurred at
913    /// FOUR hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
914    /// duplication trigger, spanning one crate but four modules, and
915    /// is lifted to ONE substrate owner here). THEORY.md §II.1
916    /// invariant 5 (composition preserves proofs — the pins below
917    /// bind the (name-slot → metadata.name, ns-slot → metadata.
918    /// namespace, spec-slot → spec) slot-projection triple + the
919    /// byte-identical parity with the pre-lift 2-line chain across
920    /// the two representative `impl Into<String>` value shapes
921    /// (`&'static str` and owned `String`) + the sibling-composer
922    /// coherence with [`Self::new`]).
923    #[must_use]
924    pub fn new_in(name: &str, namespace: impl Into<String>, spec: PoolSpec) -> Self {
925        // Routes through the ONE substrate owner of the
926        // `metadata.namespace` stamp — the [`crate::PlacedInNamespace`]
927        // blanket-impl trait over `kube::Resource<DynamicType = ()>`.
928        // Byte-identical to the pre-lift 3-line body
929        // (`Self::new(name, spec); metadata.namespace = Some(namespace
930        // .into())`); the trait-forwarding form collapses the mutation
931        // duplication with the sibling per-CRD composer
932        // [`crate::allocation::EphemeralAllocation::new_in`] and with
933        // the render-fixture site on `Process` that has no per-CRD
934        // `new_in` sibling.
935        use crate::PlacedInNamespace;
936        Self::new(name, spec).in_namespace(namespace)
937    }
938
939    /// Observed pool phase from live member observations — the pure
940    /// typed projection every pool-reconciler status-patch site needs
941    /// before it stamps [`PoolStatus`] on the wire.
942    ///
943    /// # Why it exists
944    ///
945    /// Pre-lift the phase computation lived at
946    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
947    /// as a private free function — a repo-local closed-set match over
948    /// the (tombstone-first, empty-members, min-floor, supply-vs-desired)
949    /// gate ladder. Every downstream that wanted to know a pool's
950    /// observed phase from its members had to reach into the
951    /// reconciler crate for the helper, so the projection stayed a
952    /// controller-internal detail even though `EphemeralPool` +
953    /// `PoolMember` + `PoolPhase` all live in this substrate crate.
954    /// Post-lift the projection lives at the ONE typed accessor on
955    /// `EphemeralPool` — the natural owner, since the four gate slots
956    /// the ladder reads (`is_being_deleted()`, `spec.desired_size`,
957    /// `spec.min_size`, and the members supply computed via
958    /// [`crate::pool::MemberState::counts_toward_supply`]) all belong
959    /// to `EphemeralPool` or to the shared substrate. Any future
960    /// consumer (a feira `pool status --phase` command, an MCP tool
961    /// that renders pool health, a dashboard SSE feed, a peer
962    /// controller that mirrors pool state into an external system)
963    /// reads the phase through this ONE substrate method rather than
964    /// re-implementing the gate ladder against the pre-lift
965    /// controller-internal shape.
966    ///
967    /// # Gate ladder
968    ///
969    /// The ladder walks in strict priority order — the first gate
970    /// that fires returns:
971    ///
972    /// 1. **Tombstone-first** — `is_being_deleted()` → `Draining`.
973    ///    Keeps the reported phase honest during the finalizer drain
974    ///    so operators reading `kubectl get ephemeralpools` see the
975    ///    tombstone-present state as `Draining`, not as a stale
976    ///    `Steady` derived from the pre-tombstone supply arithmetic.
977    /// 2. **Empty-members** — `members.is_empty()` → `Initializing`.
978    ///    A pool with zero members is either fresh (never had a
979    ///    spawn) or fully reaped without the tombstone; either way
980    ///    the supply arithmetic below has no signal to work with,
981    ///    so the ladder short-circuits.
982    /// 3. **Min-floor** — `spec.min_size > 0 && supply < spec.min_size`
983    ///    → `Degraded`. Hard floor breach: the pool has members but
984    ///    not enough Free/Spawning capacity to serve requestors
985    ///    without dipping below the operator-declared floor.
986    /// 4. **Supply-vs-desired** — `supply < spec.desired_size` →
987    ///    `ScalingUp`; `supply > spec.desired_size` → `ScalingDown`.
988    ///    The standard replenishment arithmetic that the reconciler's
989    ///    convergence loop drives toward zero.
990    /// 5. **Steady** — the terminal arm. Supply matches desired, no
991    ///    scaling pending, no tombstone, no floor breach.
992    ///
993    /// The supply computation rides through the closed-set predicate
994    /// [`crate::pool::MemberState::counts_toward_supply`] — a
995    /// `MemberState` variant that should also count toward supply
996    /// (e.g. a "Warming" state between Spawning and Free) lands at
997    /// ONE predicate arm and this method inherits the new bucketing
998    /// automatically. The
999    /// `member_state_failed_implies_no_supply` contract test on
1000    /// [`crate::pool::MemberState`] pins that a `Failed` member can
1001    /// never inflate this count and pollute the ladder's gate
1002    /// decisions.
1003    ///
1004    /// # Invariants
1005    ///
1006    /// - **Priority order:** the ladder walks tombstone → empty →
1007    ///   floor → supply → steady; a regression that reordered any two
1008    ///   gates would surface at `tests::observed_phase_from_*` as a
1009    ///   truth-table mismatch across the gate corners.
1010    /// - **Pure projection:** two consecutive calls on the same
1011    ///   `(pool, members)` pair return the same `PoolPhase` — no
1012    ///   hidden state, no per-tick clock read (the tombstone probe
1013    ///   is a metadata slot presence check, not a wall-clock
1014    ///   comparison).
1015    /// - **Byte-identical to the pre-lift chain:** the ladder
1016    ///   collapses to the same `PoolPhase` the reconciler's private
1017    ///   `pool_phase_from_members` free function produced pre-lift
1018    ///   at every gate corner — pinned by
1019    ///   `tests::observed_phase_from_matches_pre_lift_reconciler_chain`.
1020    ///
1021    /// Sibling to the peer typed-projection primitives on
1022    /// `EphemeralPool` ([`Self::is_being_deleted`],
1023    /// [`Self::name_or_empty`], [`Self::owned_name_or_empty`],
1024    /// [`Self::owned_namespace_or_empty`]) — closes the corner the
1025    /// pool-side family previously left open on the (observation →
1026    /// derived-phase) axis. Peer to
1027    /// [`crate::pool::PoolStatus::observed_from`] on the (pure typed
1028    /// projection, compound-composer) axis; the peer chains this
1029    /// projection with the wall-clock-anchored
1030    /// [`crate::pool::PoolStatus::observed_now`] stamp so both
1031    /// pool-reconciler status-patch sites route the observation
1032    /// through ONE substrate composer rather than through the pre-
1033    /// lift 3-line (phase-compute + observed_now + merge_status)
1034    /// chain.
1035    ///
1036    /// # `#[must_use]`
1037    ///
1038    /// The returned [`PoolPhase`] is a pure typed projection; every
1039    /// consumer feeds it into a downstream status-patch composer or
1040    /// operator-facing diagnostic. Dropping the return means the
1041    /// projection was computed for no observable reason.
1042    ///
1043    /// Theory anchor: THEORY.md §II.1 invariant 3 (typed exit — the
1044    /// gate ladder's five arms partition the observed-phase space
1045    /// exhaustively and the closed-set match at each arm keeps the
1046    /// exhaustiveness under compiler control). THEORY.md §III (the
1047    /// typescape — this projection is a typed accessor on
1048    /// `EphemeralPool`, coherent with the workspace-wide typed
1049    /// projection family the pool-side primitives already anchor).
1050    #[must_use]
1051    pub fn observed_phase_from(&self, members: &[PoolMember]) -> PoolPhase {
1052        if self.is_being_deleted() {
1053            return PoolPhase::Draining;
1054        }
1055        if members.is_empty() {
1056            return PoolPhase::Initializing;
1057        }
1058        let supply = members
1059            .iter()
1060            .filter(|m| m.state.counts_toward_supply())
1061            .count() as u32;
1062        if self.spec.min_size > 0 && supply < self.spec.min_size {
1063            return PoolPhase::Degraded;
1064        }
1065        let want = self.spec.desired_size;
1066        if supply < want {
1067            return PoolPhase::ScalingUp;
1068        }
1069        if supply > want {
1070            return PoolPhase::ScalingDown;
1071        }
1072        PoolPhase::Steady
1073    }
1074}
1075
1076/// What the pool reconciler does when a member reaches `Failed`.
1077///
1078/// Sibling closed-set lifts on the same `tatara-process` axis:
1079/// [`crate::compliance::VerificationPhase::ALL`],
1080/// [`crate::signal::SighupStrategy::ALL`],
1081/// [`crate::spec::MustReachPhase::ALL`],
1082/// [`crate::intent::WorkloadKind::ALL`],
1083/// [`crate::export::ReportFormat::ALL`],
1084/// [`crate::encapsulates::EncapsulationMode::ALL`],
1085/// [`crate::export::ExportTrigger::ALL`],
1086/// [`crate::lifetime::TeardownPolicy::ALL`],
1087/// [`crate::boundary::ConditionKind::ALL`],
1088/// [`crate::lifetime::LifetimeKind::ALL`],
1089/// [`crate::intent::IntentKind::ALL`],
1090/// [`crate::phase::ProcessPhase::ALL`],
1091/// [`crate::signal::ProcessSignal::ALL`].
1092#[derive(
1093    Clone,
1094    Copy,
1095    Debug,
1096    Default,
1097    Serialize,
1098    Deserialize,
1099    JsonSchema,
1100    PartialEq,
1101    Eq,
1102    Hash,
1103    tatara_closed_set::DeriveClosedSet,
1104)]
1105#[serde(rename_all = "PascalCase")]
1106#[closed_set(via = "as_str", generate_unknown, display)]
1107pub enum ReplacementPolicy {
1108    /// **Default** — Failed member is reaped + replaced immediately
1109    /// (pool stays at `desired` count). Most production-like.
1110    #[default]
1111    ReplaceImmediate,
1112    /// Failed member stays for inspection; pool runs short until the
1113    /// operator manually reaps it. Useful for debugging.
1114    HoldFailed,
1115    /// Failed member triggers pool-wide pause: `desired` is
1116    /// effectively 0 until the operator manually resumes via a
1117    /// pool-status patch. Used for "halt on any failure" workflows.
1118    PausePool,
1119}
1120
1121impl ReplacementPolicy {
1122    /// The closed set of replacement policies — single source of truth
1123    /// that drives the `as_str` / Display / `FromStr` triad and the
1124    /// `replaces_failed` / `pauses_on_failure` predicate pair. Adding a
1125    /// fourth variant lands at one `ALL` entry + one `as_str` arm + one
1126    /// predicate arm per projection — exhaustively checked by the
1127    /// compiler (the `[Self; 3]` array literal forces the arity) and by
1128    /// the predicate-pair injectivity test below (a new variant must
1129    /// land in its own (replaces_failed, pauses_on_failure) bucket or
1130    /// the author has to extend the consumer dispatch in
1131    /// `tatara-pool-reconciler::desired::PoolConvergence::decide`).
1132    pub const ALL: [Self; 3] = [Self::ReplaceImmediate, Self::HoldFailed, Self::PausePool];
1133
1134    /// Canonical PascalCase wire-format projection — matches the serde
1135    /// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
1136    /// enumeration the pool reconciler stamps on the
1137    /// `ephemeralpools.tatara.pleme.io` schema. Pinned by
1138    /// `replacement_policy_as_str_matches_serde` so a variant rename
1139    /// can't drift between the typed surface, the CRD enum, the YAML
1140    /// wire format AND the operator-facing diagnostic (the
1141    /// `desired.rs` Pause reason composes `policy={policy}` via
1142    /// Display, not a hard-coded `"PausePool"` literal that would
1143    /// silently rot).
1144    pub const fn as_str(self) -> &'static str {
1145        match self {
1146            Self::ReplaceImmediate => "ReplaceImmediate",
1147            Self::HoldFailed => "HoldFailed",
1148            Self::PausePool => "PausePool",
1149        }
1150    }
1151
1152    /// Should the pool auto-spawn a replacement for a Failed member?
1153    /// Closed-set match (not `matches!`) so a future variant triggers
1154    /// the compiler's exhaustiveness check at this site rather than
1155    /// silently defaulting to `false`. Paired with
1156    /// `pauses_on_failure` they form the two-axis projection
1157    /// consumers in `tatara-pool-reconciler::desired::PoolConvergence`
1158    /// pattern-match against — `replaces_failed` true ⇒ emit
1159    /// `ReapFailed` per failure; `pauses_on_failure` true with any
1160    /// failure ⇒ emit `Pause` and short-circuit. The pair is
1161    /// `(true, false) | (false, false) | (false, true)` — pinned
1162    /// injective by `replacement_policy_predicate_pair_is_injective`.
1163    pub const fn replaces_failed(self) -> bool {
1164        match self {
1165            Self::ReplaceImmediate => true,
1166            Self::HoldFailed | Self::PausePool => false,
1167        }
1168    }
1169
1170    /// Should reaching Failed on any member pause the whole pool?
1171    /// See `replaces_failed` for the closed-match rationale + the
1172    /// predicate-pair contract.
1173    pub const fn pauses_on_failure(self) -> bool {
1174        match self {
1175            Self::PausePool => true,
1176            Self::ReplaceImmediate | Self::HoldFailed => false,
1177        }
1178    }
1179}
1180
1181// `impl FromStr for ReplacementPolicy` + `impl tatara_lisp::ClosedSet for
1182// ReplacementPolicy` + `impl fmt::Display for ReplacementPolicy` are
1183// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
1184// declaration above. `label` delegates to the inherent
1185// `ReplacementPolicy::as_str` via `#[closed_set(via = "as_str")]` so the
1186// PascalCase wire-format projection stays load-bearing (matches the
1187// serde `rename_all = "PascalCase"` output AND the
1188// `tatara-pool-reconciler::desired::PoolConvergence` Pause reason
1189// emission verbatim) while generic `T: ClosedSet` consumers reach the
1190// STABLE workspace-wide name (`label`); Display delegates to the same
1191// inherent projection via `#[closed_set(display)]` so the
1192// `Pause` reason emitter's `policy={policy}` composition stays
1193// pinned on the closed-set algebra rather than on a hand-rolled
1194// `fmt::Display` block per implementor.
1195
1196// `pub struct UnknownReplacementPolicy(pub String)` is generated by
1197// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1198// on the enum declaration above. The auto-derived label
1199// `"replacement policy"` matches the prior hand-rolled
1200// `#[error("unknown replacement policy: {0}")]` verbatim. Symmetric to
1201// [`UnknownMemberState`], [`UnknownPoolPhase`], [`UnknownReturnPolicy`],
1202// [`crate::export::UnknownReportFormat`],
1203// [`crate::export::UnknownChannelKind`],
1204// [`crate::export::UnknownExportTrigger`],
1205// [`crate::lifetime::UnknownTeardownPolicy`],
1206// [`crate::boundary::UnknownConditionKind`], and
1207// [`crate::phase::UnknownPhase`].
1208
1209fn default_free_ttl() -> String {
1210    "24h".to_string()
1211}
1212fn default_max_allocation_ttl() -> String {
1213    "4h".to_string()
1214}
1215
1216/// `EphemeralPool.status` — observed pool population state.
1217#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
1218#[serde(rename_all = "camelCase")]
1219pub struct PoolStatus {
1220    /// Pool lifecycle phase.
1221    #[serde(default)]
1222    pub phase: PoolPhase,
1223
1224    /// When the pool entered the current phase.
1225    #[serde(default, skip_serializing_if = "Option::is_none")]
1226    pub phase_since: Option<DateTime<Utc>>,
1227
1228    /// Number of members currently in `Free` state (ready for allocation).
1229    #[serde(default)]
1230    pub ready_count: u32,
1231
1232    /// Number of members currently `Allocated`.
1233    #[serde(default)]
1234    pub allocated_count: u32,
1235
1236    /// Number of members currently `Spawning` (not yet Attested).
1237    #[serde(default)]
1238    pub spawning_count: u32,
1239
1240    /// Number of members currently `Returning` (reset or replace
1241    /// in progress).
1242    #[serde(default)]
1243    pub returning_count: u32,
1244
1245    /// Member ledger — one entry per pool slot.
1246    #[serde(default)]
1247    pub members: Vec<PoolMember>,
1248
1249    /// Operator-visible message (e.g., "scaled down to floor").
1250    #[serde(default, skip_serializing_if = "Option::is_none")]
1251    pub message: Option<String>,
1252
1253    /// Standard Kubernetes Conditions.
1254    #[serde(default)]
1255    pub conditions: Vec<PoolCondition>,
1256}
1257
1258/// One pool slot's state.
1259#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1260#[serde(rename_all = "camelCase")]
1261pub struct PoolMember {
1262    /// `metadata.name` of the backing Process.
1263    pub process_name: String,
1264    /// Pool member's current slot state.
1265    pub state: MemberState,
1266    /// When the member entered the current state.
1267    pub entered_state_at: DateTime<Utc>,
1268    /// If allocated: the AllocationRef holding this slot.
1269    #[serde(default, skip_serializing_if = "Option::is_none")]
1270    pub allocation_ref: Option<AllocationRef>,
1271}
1272
1273impl PoolStatus {
1274    /// Substrate constructor for the observed [`PoolStatus`] seed:
1275    /// composes the `(phase, phase_since, ready/allocated/spawning
1276    /// /returning counts, members, message, conditions)` 9-slot record
1277    /// every pool-reconciler status-patch site restated by hand pre-
1278    /// lift. The four counters ride a SINGLE closed-set-driven fold
1279    /// over the members list (one pass rather than four independent
1280    /// filter-and-count passes); the `message` + `conditions` slots
1281    /// stay at their invariant `None` / `vec![]` defaults every pre-
1282    /// lift caller stamped verbatim, and `phase_since` is derived from
1283    /// the caller-supplied `now` timestamp so the constructor stays
1284    /// clock-injectable rather than implicitly reading wall time.
1285    ///
1286    /// Pre-lift the 11-line
1287    /// ```rust,ignore
1288    /// PoolStatus {
1289    ///     phase,
1290    ///     phase_since: Some(Utc::now()),
1291    ///     ready_count: count_state(&members, MemberState::Free),
1292    ///     allocated_count: count_state(&members, MemberState::Allocated),
1293    ///     spawning_count: count_state(&members, MemberState::Spawning),
1294    ///     returning_count: count_state(&members, MemberState::Returning),
1295    ///     members: members.clone(),
1296    ///     message: None,
1297    ///     conditions: vec![],
1298    /// }
1299    /// ```
1300    /// incantation was hand-authored at TWO sites past the ★★ PRIME-
1301    /// DIRECTIVE ≥ 2 duplication threshold in
1302    /// `tatara-pool-reconciler::controller_pool::reconcile_inner`,
1303    /// both restating the same 4-slot count fanout + defaults:
1304    /// * The `desired > 0` path — status patch after the
1305    ///   convergence-action loop when the operator drives the pool
1306    ///   through the R11 desired-count invariant.
1307    /// * The legacy allocation-driven path (`desired == 0`) — status
1308    ///   patch after the [`crate::pool::PoolDecision`] apply loop.
1309    ///
1310    /// Both sites walked the SAME 4-slot count fanout on the SAME
1311    /// four `MemberState` variants (Free/Allocated/Spawning/Returning)
1312    /// and stamped the SAME defaults (`message: None`, `conditions:
1313    /// vec![]`), even though the four counters walked the members list
1314    /// four independent times pre-lift when a single pass suffices.
1315    /// Post-lift both callers write
1316    /// `PoolStatus::observed(phase, members, Utc::now())` and share
1317    /// ONE substrate owner; a future counter slot (e.g., a
1318    /// `warming_count` for a `MemberState::Warming` variant between
1319    /// Spawning and Free) plugs into the fold at ONE match arm and
1320    /// both status-patch sites inherit the new slot mechanically.
1321    ///
1322    /// The `Failed` variant is deliberately absent from the fold — no
1323    /// `PoolStatus` slot counts failed members (they surface via
1324    /// `pool_phase_from_members`'s `PoolPhase::Degraded` transition
1325    /// instead), and the closed-set match on
1326    /// [`MemberState`] pins that a future variant which SHOULD count
1327    /// toward one of the four buckets triggers the compiler's
1328    /// exhaustiveness check at this fold rather than silently sinking
1329    /// into `Failed`'s no-op arm.
1330    ///
1331    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1332    /// the 11-line status-seed incantation recurred at two hand-
1333    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
1334    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
1335    /// invariant 5 (composition preserves proofs — the pins bind the
1336    /// 4-slot count fanout + the closed-set exhaustiveness on
1337    /// `MemberState` + the invariant defaults, so a regression that
1338    /// dropped a counter slot or swapped a variant surfaces at
1339    /// `tests::pool_status_observed_*` rather than as silent operator-
1340    /// facing skew between the two status-patch sites on the SAME
1341    /// pool).
1342    #[must_use]
1343    pub fn observed(phase: PoolPhase, members: Vec<PoolMember>, now: DateTime<Utc>) -> Self {
1344        let (ready_count, allocated_count, spawning_count, returning_count) =
1345            PoolMember::state_count_fanout(&members);
1346        Self {
1347            phase,
1348            phase_since: Some(now),
1349            ready_count,
1350            allocated_count,
1351            spawning_count,
1352            returning_count,
1353            members,
1354            message: None,
1355            conditions: vec![],
1356        }
1357    }
1358
1359    /// Wall-clock-anchored peer of [`Self::observed`] — the ONE
1360    /// substrate owner of the 4-arg `PoolStatus::observed(phase,
1361    /// members, Utc::now())` composition every pool-reconciler
1362    /// status-patch site that reads the wall clock at tick-time
1363    /// hand-authored pre-lift.
1364    ///
1365    /// # Why it exists
1366    ///
1367    /// Pre-lift the 4-arg `PoolStatus::observed(phase, members.clone(),
1368    /// chrono::Utc::now())` chain was hand-authored at TWO sites past the
1369    /// ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold in
1370    /// `tatara-pool-reconciler::controller_pool::reconcile_inner`, each
1371    /// pairing the 3-arg [`Self::observed`] composer with a
1372    /// `chrono::Utc::now()` third argument at the status-patch stamp:
1373    ///
1374    /// * The `desired > 0` path — status patch after the
1375    ///   convergence-action loop when the operator drives the pool
1376    ///   through the R11 desired-count invariant.
1377    /// * The legacy allocation-driven path (`desired == 0`) — status
1378    ///   patch after the [`crate::pool::PoolDecision`] apply loop.
1379    ///
1380    /// Both sites walked the SAME 4-arg call with the SAME
1381    /// `chrono::Utc::now()` third argument — the wall-clock projection
1382    /// had no per-callsite variation. Post-lift both consumers share ONE
1383    /// substrate owner for the wall-clock-at-tick projection; a future
1384    /// clock swap (a monotonic clock cross-check, a per-reconciler
1385    /// injected time source, a test-only override at the production
1386    /// callsite via feature flag) lands at ONE substrate function and
1387    /// every pool-reconciler status-patch site inherits the upgrade
1388    /// mechanically.
1389    ///
1390    /// The 3-arg [`Self::observed`] peer stays load-bearing for test
1391    /// callers — the injected-`now` shape is what unit tests use to
1392    /// drive the clock deterministically (every
1393    /// `PoolStatus::observed(phase, members, seeded_now)` in this
1394    /// module's own test suite reads that surface). This peer is
1395    /// production-only: pinning the wall-clock at the substrate site
1396    /// means no test can accidentally consume it without the
1397    /// deterministic-clock injection that makes the test meaningful.
1398    ///
1399    /// Sibling of
1400    /// [`crate::lifetime_clock::evaluate_now`] on the (typed
1401    /// pure-fn, wall-clock-anchored peer) axis — both primitives own
1402    /// the "read the wall clock at tick-time" projection on a peer
1403    /// clock-injectable primitive so the workspace's timed-decision
1404    /// family stays uniform across `EphemeralLifetime` TTL expiry and
1405    /// `PoolStatus` observed-state stamp.
1406    ///
1407    /// # Invariants
1408    ///
1409    /// - **Same shape:** returns the SAME [`PoolStatus`] the 3-arg
1410    ///   [`Self::observed`] returns when passed `chrono::Utc::now()` as
1411    ///   the third argument. This is a delegation, not a
1412    ///   re-implementation.
1413    /// - **Wall-clock read once:** `Utc::now()` is called exactly ONCE
1414    ///   per invocation, at the primitive's body, so a future consumer
1415    ///   that chains two `observed_now` calls back-to-back still sees
1416    ///   monotonic `now` reads (each call reads a fresh instant, not a
1417    ///   cached one) — matches the pre-lift shape where each of the two
1418    ///   status-patch sites computed its own `chrono::Utc::now()` at its
1419    ///   own line.
1420    ///
1421    /// # `#[must_use]`
1422    ///
1423    /// Every consumer feeds the returned [`PoolStatus`] into
1424    /// `tatara_process::patch::merge_status(&pool_api, &name, &<status>)`
1425    /// or a peer status-patch call. Dropping the return means the
1426    /// observation composed for no observable reason — the attribute
1427    /// surfaces that as a warning at every call site.
1428    ///
1429    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1430    /// the 4-arg call with `chrono::Utc::now()` as the third argument
1431    /// recurred at 2 hand-authored sites past the ★★ PRIME-DIRECTIVE
1432    /// ≥ 2 duplication trigger, lifted onto the ONE workspace-wide
1433    /// substrate owner here). THEORY.md §II.1 invariant 5 (composition
1434    /// preserves proofs — the wall-clock projection lives at ONE site
1435    /// so a future clock swap reaches both consumers through one edit).
1436    #[must_use]
1437    pub fn observed_now(phase: PoolPhase, members: Vec<PoolMember>) -> Self {
1438        Self::observed(phase, members, Utc::now())
1439    }
1440
1441    /// Compound composer peer of [`Self::observed_now`] that derives
1442    /// the [`PoolPhase`] from `(pool, members)` via the typed
1443    /// projection [`crate::pool::EphemeralPool::observed_phase_from`]
1444    /// — the ONE substrate owner of the (phase-compute + observed_now
1445    /// wall-clock stamp) 2-link chain every pool-reconciler status-
1446    /// patch site walked pre-lift.
1447    ///
1448    /// # Why it exists
1449    ///
1450    /// Pre-lift the 2-link `let phase = pool_phase_from_members(&pool,
1451    /// &members); PoolStatus::observed_now(phase, members.clone())`
1452    /// chain was hand-authored at TWO sites past the ★★ PRIME-
1453    /// DIRECTIVE ≥ 2 duplication threshold in
1454    /// `tatara-pool-reconciler::controller_pool::reconcile_inner`,
1455    /// both keyed against the same `(pool, members)` observations:
1456    ///
1457    /// * The `desired > 0` path — status patch after the
1458    ///   `apply_convergence_actions` walk when the operator drives
1459    ///   the pool through the R11 desired-count invariant.
1460    /// * The legacy allocation-driven path (`desired == 0`) — status
1461    ///   patch after the [`crate::pool::PoolDecision`] apply loop.
1462    ///
1463    /// Both sites walked the SAME 2-link chain — compute the observed
1464    /// phase from the (tombstone-first, empty, floor, supply-vs-
1465    /// desired) gate ladder against the borrowed `(pool, members)`
1466    /// pair, then hand the produced phase + owned members clone to
1467    /// the wall-clock-anchored [`Self::observed_now`] composer. Both
1468    /// keyed the SAME projection through a repo-internal free
1469    /// function (`pool_phase_from_members`) that shadowed the natural
1470    /// substrate owner. Post-lift each callsite reads
1471    /// `PoolStatus::observed_from(&pool, members.clone())` and the
1472    /// compose+dispatch sink lives at ONE substrate owner —
1473    /// [`crate::pool::EphemeralPool::observed_phase_from`] +
1474    /// [`Self::observed_now`] compose here, at the exact substrate
1475    /// site where the pool + status types both live.
1476    ///
1477    /// # Invariants
1478    ///
1479    /// - **Same shape:** returns the SAME [`PoolStatus`] the 2-link
1480    ///   chain `observed_now(pool.observed_phase_from(&members),
1481    ///   members)` returns. This is a delegation, not a re-
1482    ///   implementation — the underlying wall-clock stamp still lives
1483    ///   at [`Self::observed_now`] and the phase projection still
1484    ///   lives at [`crate::pool::EphemeralPool::observed_phase_from`].
1485    /// - **Members ride through by owned value:** the members `Vec`
1486    ///   is consumed by [`Self::observed_now`] verbatim (no defensive
1487    ///   `.clone()` at the composer boundary); the phase projection
1488    ///   borrows the same slice through `&members[..]` inside the
1489    ///   delegation so the underlying single-pass fold in
1490    ///   [`crate::pool::MemberState::counts_toward_supply`]-family
1491    ///   composers still gets the same borrowed view it did pre-lift.
1492    /// - **Wall-clock read once:** `Utc::now()` is called exactly ONCE
1493    ///   per invocation (inherited from [`Self::observed_now`]),
1494    ///   preserving the pre-lift shape where each of the two status-
1495    ///   patch sites computed its own `chrono::Utc::now()` at its own
1496    ///   line.
1497    ///
1498    /// # `#[must_use]`
1499    ///
1500    /// Every consumer feeds the returned [`PoolStatus`] into
1501    /// `tatara_process::patch::merge_status(&pool_api, &name,
1502    /// &<status>)` or a peer status-patch call. Dropping the return
1503    /// means the observation composed for no observable reason — the
1504    /// attribute surfaces that as a warning at every call site.
1505    ///
1506    /// Sibling of [`Self::observed_now`] on the (pure phase argument,
1507    /// pool-derived phase) axis pair: both compose atop the 3-arg
1508    /// [`Self::observed`] primitive, differing only in whether the
1509    /// caller has already computed the phase (`observed_now`) or
1510    /// hands the pool + members observations to the composer to
1511    /// derive the phase in one shot (`observed_from`). Peer of
1512    /// [`crate::pool::EphemeralPool::observed_phase_from`] on the
1513    /// (pure typed projection, compound-composer) axis.
1514    ///
1515    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1516    /// the 2-link `pool_phase_from_members + observed_now` chain
1517    /// recurred at 2 hand-authored sites past the ★★ PRIME-DIRECTIVE
1518    /// ≥ 2 duplication trigger, and is lifted to ONE substrate owner
1519    /// here). THEORY.md §II.1 invariant 5 (composition preserves
1520    /// proofs — the compound composer inherits the two component
1521    /// composers' invariants mechanically, so a regression at either
1522    /// component surfaces at the pinned tests here rather than as
1523    /// silent skew at either status-patch site).
1524    #[must_use]
1525    pub fn observed_from(pool: &EphemeralPool, members: Vec<PoolMember>) -> Self {
1526        let phase = pool.observed_phase_from(&members);
1527        Self::observed_now(phase, members)
1528    }
1529}
1530
1531impl PoolMember {
1532    /// Substrate primitive: single-pass closed-set fold over a
1533    /// `[PoolMember]` slice producing the `(ready, allocated,
1534    /// spawning, returning)` 4-tuple every `PoolStatus` seed stamps at
1535    /// its four counter slots. The `Failed` arm is a no-op (no
1536    /// `PoolStatus` counter tracks failed members — they surface via
1537    /// [`PoolPhase::Degraded`] instead), pinned by the closed-set
1538    /// match so a future variant that SHOULD count toward one of the
1539    /// four buckets triggers the compiler's exhaustiveness check here
1540    /// rather than silently falling through.
1541    ///
1542    /// Consumed by [`PoolStatus::observed`]. A caller that needs a
1543    /// single per-variant count outside the status-seed fanout should
1544    /// keep spelling `members.iter().filter(...).count()` rather than
1545    /// walking this 4-tuple — the fanout is shaped for the
1546    /// `PoolStatus` fill, not for arbitrary per-variant queries.
1547    #[must_use]
1548    pub fn state_count_fanout(members: &[Self]) -> (u32, u32, u32, u32) {
1549        let mut ready = 0u32;
1550        let mut allocated = 0u32;
1551        let mut spawning = 0u32;
1552        let mut returning = 0u32;
1553        for m in members {
1554            match m.state {
1555                MemberState::Free => ready += 1,
1556                MemberState::Allocated => allocated += 1,
1557                MemberState::Spawning => spawning += 1,
1558                MemberState::Returning => returning += 1,
1559                MemberState::Failed => {}
1560            }
1561        }
1562        (ready, allocated, spawning, returning)
1563    }
1564
1565    /// Substrate primitive: single-pass closed-set collection of the
1566    /// `process_name` axis over a `[PoolMember]` slice into an owned
1567    /// `HashSet<String>` — the O(1)-lookup shape every spawn-arm on
1568    /// the workspace builds pre-collision-check against a candidate
1569    /// [`crate::pool::PoolMember::process_name`] produced by
1570    /// [`tatara-pool-reconciler::naming::member_process_name`].
1571    ///
1572    /// Pre-lift the 2-line
1573    /// `members.iter().map(|m| m.process_name.clone()).collect()`
1574    /// chain was hand-authored at TWO sites past the ★★ PRIME-
1575    /// DIRECTIVE ≥ 2 duplication threshold in
1576    /// `tatara-pool-reconciler::controller_pool`, both restating the
1577    /// SAME `process_name` projection through the SAME
1578    /// `iter → map → collect` shape and both feeding a `.contains
1579    /// (&candidate)` probe:
1580    /// * `reconcile_inner`'s legacy allocation-driven
1581    ///   `PoolDecision::Spawn` arm (`desired == 0` path) —
1582    ///   collision-set for
1583    ///   `member_process_name(&pool_name, &pool_uid, slot)` per spawn
1584    ///   slot.
1585    /// * `apply_convergence_actions` — collision-set for the SAME
1586    ///   composer inside the R11 desired-count
1587    ///   `ConvergenceAction::CreateMember` loop.
1588    ///
1589    /// Post-lift both consumers share ONE substrate owner; the
1590    /// composed `HashSet<String>` still feeds the same
1591    /// `HashSet::<String>::contains(&candidate)` probe at each
1592    /// callsite unchanged. A future normalization step on the
1593    /// occupied-name axis (case-fold before insertion, a per-cluster
1594    /// prefix strip, deduplication against a sibling stale-name
1595    /// registry, exclusion of `Returning`/`Failed` members that no
1596    /// longer own their slot) lands at ONE substrate method rather
1597    /// than being restated at each callsite.
1598    ///
1599    /// Sibling to [`Self::state_count_fanout`] on the `(collection
1600    /// shape × slice-owned fold)` axis: both primitives fold a
1601    /// `[PoolMember]` slice into one caller-shaped aggregate in a
1602    /// single pass, both are `#[must_use]`, both take the slice by
1603    /// reference so no caller has to reshape its `Vec<PoolMember>` or
1604    /// `Vec<PoolMember>` slice upstream.
1605    ///
1606    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1607    /// the `HashSet<String>` collision-set shape recurred at TWO
1608    /// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
1609    /// duplication trigger, and is lifted to ONE owner here).
1610    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
1611    /// the pins bind the axis (`process_name`), the aggregate shape
1612    /// (`HashSet<String>`), the empty-slice corner, and the
1613    /// duplicate-name deduplication semantics `HashSet` provides
1614    /// implicitly, so a regression at any of those surfaces at
1615    /// `tests::process_names_set_*` rather than as silent occupied-
1616    /// slot skew at either spawn arm).
1617    #[must_use]
1618    pub fn process_names_set(members: &[Self]) -> std::collections::HashSet<String> {
1619        members.iter().map(|m| m.process_name.clone()).collect()
1620    }
1621
1622    /// Substrate composer for the unallocated `PoolMember` seed: the
1623    /// 4-slot `{ process_name, state, entered_state_at, allocation_ref:
1624    /// None }` fixture literal every non-`Allocated`-role callsite
1625    /// stamped by hand pre-lift.
1626    ///
1627    /// Pre-lift the 4-slot struct literal `PoolMember { process_name,
1628    /// state, entered_state_at, allocation_ref: None }` was hand-authored
1629    /// at FIVE workspace-wide sites past the ★★ PRIME-DIRECTIVE ≥ 2
1630    /// duplication threshold across TWO crates:
1631    /// * `tatara-pool-reconciler::controller_pool::reconcile_inner` —
1632    ///   the production per-owned-Process seed built inside the
1633    ///   `for p in all_processes.items` walk; `entered_state_at` rides
1634    ///   in from [`crate::prelude::Process::observed_phase_since`] with
1635    ///   the `Utc::now` fallback at the callsite.
1636    /// * `tatara-pool-reconciler::pool_decide::tests::member` — test
1637    ///   helper for the pool decision suite; `entered_state_at` rides
1638    ///   in from [`crate::time::seconds_ago`].
1639    /// * `tatara-pool-reconciler::allocation_decide::tests::member` —
1640    ///   test helper for the allocation decision suite;
1641    ///   `entered_state_at` rides in from `Utc::now`.
1642    /// * `tatara-process::pool::tests::member` — test helper for the
1643    ///   fanout / status suite; `entered_state_at` rides in from the
1644    ///   epoch anchor `DateTime::<Utc>::from_timestamp(0, 0)`.
1645    /// * `tatara-process::pool::tests::named_member` — test helper for
1646    ///   the `process_names_set` suite; same epoch anchor.
1647    ///
1648    /// Every one of those FIVE sites pinned `allocation_ref: None`
1649    /// verbatim — no `PoolMember` construction site in the workspace
1650    /// pairs `allocation_ref: Some(<ref>)` with a hand-authored 4-slot
1651    /// struct literal, so this composer's `None` slot is safe by
1652    /// construction (the compiler exhaustiveness check on the struct's
1653    /// four fields catches a future 5th slot addition here rather than
1654    /// at any of the callsites).
1655    ///
1656    /// Post-lift every consumer writes
1657    /// `PoolMember::unallocated(<name>, <state>, <anchor>)` and shares
1658    /// ONE substrate owner; a future promotion of the unallocated shape
1659    /// (a per-cluster clock-skew guard on the `entered_state_at`
1660    /// anchor, a canonical rename of the None-slot to a typed
1661    /// `Unallocated` marker, a lint-friendly closed-set restriction to
1662    /// the four `MemberState` variants that legitimately carry no
1663    /// `allocation_ref`) lands at ONE substrate site and every downstream
1664    /// consumer inherits the upgrade mechanically.
1665    ///
1666    /// `impl Into<String>` accepts both `&str` literals (every test
1667    /// helper site) and owned `String` produced by
1668    /// [`crate::prelude::Process::owned_name_or_empty`] (the production
1669    /// controller-pool site) without widening the signature.
1670    ///
1671    /// Sibling to [`AllocationRef::new`] on the substrate-composer
1672    /// axis: both take `impl Into<String>`-gated identity slots and
1673    /// return their owner-type by value; [`AllocationRef::new`] owns
1674    /// the (name, namespace) pair, this composer owns the four-slot
1675    /// unallocated-member seed.
1676    ///
1677    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1678    /// the 4-slot unallocated-`PoolMember` seed recurred at FIVE hand-
1679    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
1680    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
1681    /// invariant 5 (composition preserves proofs — the pins bind the
1682    /// four-slot fill AND the `allocation_ref: None` invariant AND the
1683    /// caller-clock-injectability of `entered_state_at`, so a
1684    /// regression that drifts any surface fails at
1685    /// `tests::pool_member_unallocated_*` rather than as silent
1686    /// operator-facing skew between the production controller-pool seed
1687    /// and the three test-suite helpers on the SAME `PoolMember`
1688    /// shape).
1689    #[must_use]
1690    pub fn unallocated(
1691        process_name: impl Into<String>,
1692        state: MemberState,
1693        entered_state_at: DateTime<Utc>,
1694    ) -> Self {
1695        Self {
1696            process_name: process_name.into(),
1697            state,
1698            entered_state_at,
1699            allocation_ref: None,
1700        }
1701    }
1702}
1703
1704/// Light reference to an `EphemeralAllocation`.
1705#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
1706#[serde(rename_all = "camelCase")]
1707pub struct AllocationRef {
1708    pub name: String,
1709    pub namespace: String,
1710}
1711
1712impl AllocationRef {
1713    /// Substrate constructor for [`AllocationRef`]: composes the
1714    /// `(name, namespace)` pair through ONE `impl Into<String>`-gated
1715    /// entry point — the ONE-liner collapse of the paired
1716    /// `AllocationRef { name: n.into(), namespace: ns.into() }`
1717    /// struct-literal incantation every downstream consumer restated
1718    /// by hand pre-lift.
1719    ///
1720    /// Pre-lift the `AllocationRef { name, namespace }` struct-literal
1721    /// was hand-authored at FOUR production sites past the ★★ PRIME-
1722    /// DIRECTIVE ≥ 2 duplication threshold across the workspace, all
1723    /// composing an owned `(name: String, namespace: String)` pair
1724    /// under one of two roles:
1725    /// * `tatara-pool-reconciler::controller_allocation::reconcile_inner`
1726    ///   Bind path — the `assignedProcess` status slot's ref, pairing
1727    ///   the just-bound member Process name with the allocation's
1728    ///   containing namespace.
1729    /// * `tatara-pool-reconciler::controller_allocation::reconcile_inner`
1730    ///   Release path — the same `assignedProcess` slot shape, stamped
1731    ///   at the release-side status patch alongside the (unchanged)
1732    ///   `boundPool` ref.
1733    /// * `tatara-pool-reconciler::allocation_decide::AllocationConvergenceCtx::observe`
1734    ///   pool-matched handle — the `matched_pool` slot's ref, pairing
1735    ///   [`EphemeralPool::owned_name_or_empty`] with the pool's
1736    ///   containing namespace.
1737    /// * `tatara-github-watcher::allocation_factory::allocation_from_pr`
1738    ///   — the `pool_ref` slot on the `AllocationSpec` emitted from a
1739    ///   PullRequestEvent, pairing the operator-configured pool name
1740    ///   with the watcher's target namespace.
1741    ///
1742    /// All FOUR sites walked the SAME two-field struct-literal shape
1743    /// — an owned name half, an owned namespace half — differing only
1744    /// in provenance. Post-lift each callsite reads
1745    /// `AllocationRef::new(name, ns)` and the produced value feeds the
1746    /// same downstream slot (`assignedProcess` / `bound_pool` /
1747    /// `matched_pool` / `spec.pool_ref`) unchanged. The `impl Into<String>`
1748    /// signature accepts every provenance the pre-lift sites carried —
1749    /// owned `String` (the reconciler's owned-form projections), `&str`
1750    /// (the factory's `n.to_string()` / `namespace.to_string()`
1751    /// borrow-to-owned promotions), `Cow<str>`, and every other
1752    /// `Into<String>` implementor — so no callsite has to change its
1753    /// upstream provenance to route through the primitive.
1754    ///
1755    /// Return-form axis: owned [`AllocationRef`] — the wire-format
1756    /// shape [`crate::pool::AllocationRef`]'s serde `rename_all =
1757    /// "camelCase"` produces on both spec (`poolRef`) and status
1758    /// (`boundPool` / `assignedProcess`) slots. The primitive owns
1759    /// the axis-order `(name, namespace)` — the same order the four
1760    /// consumers spelled — so a slot swap surfaces at the
1761    /// `allocation_ref_new_positional_axis_order` pin below rather
1762    /// than as silent `<namespace>/<name>` inversion downstream.
1763    ///
1764    /// Peer to the sibling substrate primitives already opened on the
1765    /// pool-side (name, namespace) axis pair:
1766    /// [`EphemeralPool::name_or_empty`] (borrow-form name),
1767    /// [`EphemeralPool::owned_name_or_empty`] (owned-form name); this
1768    /// constructor is the composer that folds the owned-form projections
1769    /// into the wire-format ref shape.
1770    ///
1771    /// A future refactor of [`AllocationRef`]'s field set (a
1772    /// `resource_kind: String` field for cross-CRD refs, an
1773    /// `api_version: String` field for FQN references, a
1774    /// canonicalization pass over the namespace half, a non-empty-name
1775    /// gate) lands at ONE substrate constructor site here and every
1776    /// downstream consumer inherits the upgrade mechanically — no per-
1777    /// callsite hand-edit at the FOUR reconciler + factory sites.
1778    ///
1779    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1780    /// the `AllocationRef { name, namespace }` struct-literal shape
1781    /// recurred at four hand-authored sites past the ★★ PRIME-
1782    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
1783    /// here). THEORY.md §II.1 invariant 5 (composition preserves
1784    /// proofs — the pins bind the positional axis-order + the
1785    /// `Into<String>` provenance closure + byte-identical parity with
1786    /// the pre-lift struct-literal + `PartialEq` coherence with the
1787    /// hand-authored form, so a regression that reshaped any surface
1788    /// at `tests::allocation_ref_new_*` rather than as silent
1789    /// operator-facing skew between the assignedProcess / bound_pool
1790    /// / matched_pool / spec.pool_ref slots on the SAME allocation).
1791    #[must_use]
1792    pub fn new(name: impl Into<String>, namespace: impl Into<String>) -> Self {
1793        Self {
1794            name: name.into(),
1795            namespace: namespace.into(),
1796        }
1797    }
1798}
1799
1800/// Per-slot state in the pool's free list.
1801///
1802/// Sibling closed-sets on the `EphemeralPool` axis: [`ReplacementPolicy::ALL`]
1803/// (the on-failure policy that the pool reconciler dispatches against
1804/// the [`Self::is_failed`] projection), [`ReturnPolicy::ALL`] (the
1805/// release-time disposition that transitions an [`Self::Allocated`]
1806/// member into [`Self::Returning`] before it either re-enters
1807/// [`Self::Free`] or gets [`Self::Spawning`]'d as a fresh slot).
1808#[derive(
1809    Clone,
1810    Copy,
1811    Debug,
1812    PartialEq,
1813    Eq,
1814    Hash,
1815    Serialize,
1816    Deserialize,
1817    JsonSchema,
1818    tatara_closed_set::DeriveClosedSet,
1819)]
1820#[serde(rename_all = "PascalCase")]
1821#[closed_set(via = "as_str", generate_unknown, display)]
1822pub enum MemberState {
1823    /// Pool reconciler is creating/converging the backing Process.
1824    Spawning,
1825    /// Process is `Attested`; ready for allocation.
1826    Free,
1827    /// Held by an `EphemeralAllocation`.
1828    Allocated,
1829    /// Return policy is being applied (Reset → reset Job; Replace →
1830    /// Process is being torn down and recreated).
1831    Returning,
1832    /// Permanent failure — the member needs operator attention.
1833    Failed,
1834}
1835
1836impl MemberState {
1837    /// The closed set of member states — single source of truth that
1838    /// drives the `as_str` / Display / `FromStr` triad AND the
1839    /// `is_failed` / `counts_toward_supply` predicate pair. Adding a
1840    /// sixth variant lands at one `ALL` entry + one `as_str` arm + one
1841    /// arm per predicate — exhaustively checked by the compiler (the
1842    /// `[Self; 5]` array literal forces the arity) and by the
1843    /// per-variant truth-table contract test (a new variant must
1844    /// declare its own `(is_failed, counts_toward_supply)` projection
1845    /// or the consumer dispatch in
1846    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
1847    /// and `tatara-pool-reconciler::pool_decide::decide_pool_reconcile`
1848    /// will silently bucket it into the wrong lifecycle column).
1849    pub const ALL: [Self; 5] = [
1850        Self::Spawning,
1851        Self::Free,
1852        Self::Allocated,
1853        Self::Returning,
1854        Self::Failed,
1855    ];
1856
1857    /// Canonical PascalCase wire-format projection — matches the serde
1858    /// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
1859    /// enumeration that `ephemeralpools.tatara.pleme.io` stamps on
1860    /// `status.members[].state`. Pinned by
1861    /// `member_state_as_str_matches_serde` so a variant rename can't
1862    /// drift between the typed surface, the CRD enum, the YAML wire
1863    /// format AND any future operator-facing diagnostic that composes
1864    /// `state={state}` via Display rather than a hard-coded literal
1865    /// that would silently rot.
1866    pub const fn as_str(self) -> &'static str {
1867        match self {
1868            Self::Spawning => "Spawning",
1869            Self::Free => "Free",
1870            Self::Allocated => "Allocated",
1871            Self::Returning => "Returning",
1872            Self::Failed => "Failed",
1873        }
1874    }
1875
1876    /// Is this member in a permanent-failure state — needs operator
1877    /// attention? Closed-set match (not `matches!`) so a future variant
1878    /// triggers the compiler's exhaustiveness check at this site rather
1879    /// than silently defaulting to `false`. Consumed by
1880    /// `tatara-pool-reconciler::pool_decide::decide_pool_reconcile` to
1881    /// gate the highest-priority `ReplaceMembers` decision branch — a
1882    /// future variant that should also trigger replacement (e.g.
1883    /// `MemberState::Quarantined`) flips this predicate at one site
1884    /// and inherits the priority-1 dispatch without touching the
1885    /// consumer match arm.
1886    pub const fn is_failed(self) -> bool {
1887        match self {
1888            Self::Failed => true,
1889            Self::Spawning | Self::Free | Self::Allocated | Self::Returning => false,
1890        }
1891    }
1892
1893    /// Does this member contribute to the pool's *available supply*
1894    /// (current ready slots + slots coming online)? Closed-set match so
1895    /// a future variant triggers the compiler's exhaustiveness check.
1896    /// Consumed by
1897    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
1898    /// — the `(free + spawning)` supply calc collapses into one
1899    /// predicate-driven filter, so a future "warming-up" state
1900    /// (`MemberState::Warming` between Spawning and Free) plugs into
1901    /// the supply count at one site rather than three. Disjoint with
1902    /// `is_failed` — pinned by `member_state_failed_implies_no_supply`
1903    /// (a Failed member can never count toward supply; the pool
1904    /// reconciler would otherwise double-count failures as available
1905    /// capacity).
1906    pub const fn counts_toward_supply(self) -> bool {
1907        match self {
1908            Self::Free | Self::Spawning => true,
1909            Self::Allocated | Self::Returning | Self::Failed => false,
1910        }
1911    }
1912}
1913
1914// `impl FromStr for MemberState` + `impl tatara_lisp::ClosedSet for
1915// MemberState` + `impl fmt::Display for MemberState` are generated by
1916// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
1917// above. `label` delegates to the inherent `MemberState::as_str` via
1918// `#[closed_set(via = "as_str")]` so the
1919// `pool_phase_from_members` supply calc can keep keying on
1920// `counts_toward_supply` against the typed variant while a generic
1921// `T: ClosedSet` consumer reaches the STABLE workspace-wide name
1922// (`label`) without knowing this enum lives in `tatara-process::pool`;
1923// Display delegates to the same inherent projection via
1924// `#[closed_set(display)]` so the diagnostic emitter's
1925// `state={state}` composition stays pinned on the closed-set algebra.
1926
1927// `pub struct UnknownMemberState(pub String)` is generated by
1928// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1929// on the enum declaration above. The auto-derived label `"member state"`
1930// matches the prior hand-rolled `#[error("unknown member state: {0}")]`
1931// verbatim. Symmetric to [`UnknownReplacementPolicy`],
1932// [`UnknownPoolPhase`], [`UnknownReturnPolicy`],
1933// [`crate::lifetime::UnknownTeardownPolicy`],
1934// [`crate::boundary::UnknownConditionKind`], and
1935// [`crate::phase::UnknownPhase`].
1936
1937/// Pool lifecycle phase (observed across the whole pool population).
1938///
1939/// Sibling closed-set on the same `EphemeralPool` axis as
1940/// [`MemberState::ALL`] (the per-slot lifecycle this phase aggregates
1941/// over via [`MemberState::counts_toward_supply`]),
1942/// [`ReplacementPolicy::ALL`] (on-failure policy) and
1943/// [`ReturnPolicy::ALL`] (release-time disposition). Together with
1944/// `MemberState`, this closes the pool reconciler's
1945/// `(slot-state, pool-phase)` two-tier observation algebra on the
1946/// same closed-set discipline as the rest of `tatara-process`.
1947#[derive(
1948    Clone,
1949    Copy,
1950    Debug,
1951    PartialEq,
1952    Eq,
1953    Hash,
1954    Serialize,
1955    Deserialize,
1956    JsonSchema,
1957    tatara_closed_set::DeriveClosedSet,
1958)]
1959#[serde(rename_all = "PascalCase")]
1960#[closed_set(via = "as_str", generate_unknown, display)]
1961pub enum PoolPhase {
1962    /// Just admitted; no members yet.
1963    Initializing,
1964    /// `ready_count == desired_size`.
1965    Steady,
1966    /// `ready_count + spawning_count < desired_size` and reconciler
1967    /// is creating new members.
1968    ScalingUp,
1969    /// `ready_count > desired_size` and reconciler is reaping excess.
1970    ScalingDown,
1971    /// `min_size` constraint violated.
1972    Degraded,
1973    /// Pool is being deleted; reconciler is reaping all members.
1974    Draining,
1975}
1976
1977impl Default for PoolPhase {
1978    fn default() -> Self {
1979        Self::Initializing
1980    }
1981}
1982
1983impl PoolPhase {
1984    /// The closed set of pool phases — single source of truth that
1985    /// drives the `as_str` / Display / `FromStr` triad AND the
1986    /// `is_steady` / `is_terminal` predicate pair. Adding a seventh
1987    /// variant lands at one `ALL` entry + one `as_str` arm + one arm
1988    /// per predicate — exhaustively checked by the compiler (the
1989    /// `[Self; 6]` array literal forces the arity) AND by the
1990    /// per-variant truth-table contract test (a new variant must
1991    /// declare its own `(is_steady, is_terminal)` projection or any
1992    /// future status-aggregator surface — `feira pool list
1993    /// --healthy`, the operator-facing condition aggregator, the
1994    /// desired-loop heartbeat short-circuit — will silently bucket
1995    /// it into the wrong lifecycle column).
1996    pub const ALL: [Self; 6] = [
1997        Self::Initializing,
1998        Self::Steady,
1999        Self::ScalingUp,
2000        Self::ScalingDown,
2001        Self::Degraded,
2002        Self::Draining,
2003    ];
2004
2005    /// Canonical PascalCase wire-format projection — matches the
2006    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
2007    /// `enum:` enumeration that `ephemeralpools.tatara.pleme.io`
2008    /// stamps on `status.phase`. Pinned by
2009    /// `pool_phase_as_str_matches_serde` so a variant rename can't
2010    /// drift between the typed surface, the CRD enum, the YAML wire
2011    /// format AND any future operator-facing diagnostic that
2012    /// composes `phase={phase}` via Display rather than a hard-coded
2013    /// literal that would silently rot. Display + FromStr triad
2014    /// over `ALL` mirrors `MemberState` / `ReplacementPolicy` /
2015    /// `ReturnPolicy` / `AllocationPhase` / `TeardownPolicy` /
2016    /// `ConditionKind` / `ProcessPhase` / `ProcessSignal`.
2017    pub const fn as_str(self) -> &'static str {
2018        match self {
2019            Self::Initializing => "Initializing",
2020            Self::Steady => "Steady",
2021            Self::ScalingUp => "ScalingUp",
2022            Self::ScalingDown => "ScalingDown",
2023            Self::Degraded => "Degraded",
2024            Self::Draining => "Draining",
2025        }
2026    }
2027
2028    /// Is the pool fully converged — supply matches desired, no
2029    /// reconciler-driven population change pending? Closed-set match
2030    /// (not `matches!`) so a future variant triggers the compiler's
2031    /// exhaustiveness check at this site rather than silently
2032    /// defaulting to `false`. Paired with `is_terminal` they form
2033    /// the two-axis projection that future status aggregators
2034    /// (operator-facing fleet health, `feira pool list --healthy`,
2035    /// the SSE filter "show non-steady pools") dispatch against —
2036    /// `is_steady && !is_terminal` ⇒ converged (goal state);
2037    /// `!is_steady && is_terminal` ⇒ being deleted (no future
2038    /// spawn); `!is_steady && !is_terminal` ⇒ transient
2039    /// (Initializing | ScalingUp | ScalingDown | Degraded — pool
2040    /// is in motion toward desired). The impossible bucket
2041    /// `(true, true)` — a draining pool that's somehow also steady
2042    /// — is pinned empty by `pool_phase_steady_excludes_terminal`.
2043    pub const fn is_steady(self) -> bool {
2044        match self {
2045            Self::Steady => true,
2046            Self::Initializing
2047            | Self::ScalingUp
2048            | Self::ScalingDown
2049            | Self::Degraded
2050            | Self::Draining => false,
2051        }
2052    }
2053
2054    /// Is the pool in its absorbing exit state — deletion-stamped,
2055    /// reconciler is reaping every member, no spawn will ever
2056    /// happen again? Closed-set match so a future variant triggers
2057    /// the compiler's exhaustiveness check. See `is_steady` for the
2058    /// predicate-pair contract + bucket definitions.
2059    pub const fn is_terminal(self) -> bool {
2060        match self {
2061            Self::Draining => true,
2062            Self::Initializing
2063            | Self::Steady
2064            | Self::ScalingUp
2065            | Self::ScalingDown
2066            | Self::Degraded => false,
2067        }
2068    }
2069}
2070
2071// `impl FromStr for PoolPhase` + `impl tatara_lisp::ClosedSet for PoolPhase`
2072// + `impl fmt::Display for PoolPhase` are generated by
2073// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration above.
2074// `label` delegates to the inherent `PoolPhase::as_str` via
2075// `#[closed_set(via = "as_str")]` so the operator-facing
2076// `phase={phase}` Display composition keeps reading the same canonical
2077// PascalCase projection while a generic `T: ClosedSet` consumer (a
2078// status-aggregator filter, the `feira pool list --healthy` predicate, a
2079// future SSE event router) can walk every variant without knowing the
2080// closed set lives in `tatara-process::pool`; Display delegates to the
2081// same inherent projection via `#[closed_set(display)]` so the
2082// `phase={phase}` composition stays pinned on the closed-set algebra
2083// rather than a hand-rolled `fmt::Display` block.
2084
2085// `pub struct UnknownPoolPhase(pub String)` is generated by
2086// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
2087// on the enum declaration above. The auto-derived label `"pool phase"`
2088// matches the prior hand-rolled `#[error("unknown pool phase: {0}")]`
2089// verbatim. Symmetric to [`UnknownMemberState`],
2090// [`UnknownReplacementPolicy`], [`UnknownReturnPolicy`],
2091// [`crate::lifetime::UnknownTeardownPolicy`],
2092// [`crate::boundary::UnknownConditionKind`], and
2093// [`crate::phase::UnknownPhase`].
2094
2095/// Standard K8s Condition shape (kept local so tatara-process doesn't
2096/// depend on k8s_openapi types in its public schema).
2097#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
2098#[serde(rename_all = "camelCase")]
2099pub struct PoolCondition {
2100    pub type_: String,
2101    pub status: String,
2102    pub reason: String,
2103    pub message: String,
2104    pub last_transition_time: DateTime<Utc>,
2105}
2106
2107/// What the pool does when an allocation releases a member.
2108///
2109/// Sibling closed-set on the `EphemeralPool` axis:
2110/// [`ReplacementPolicy::ALL`]. Sibling closed-sets on the
2111/// `tatara-process` algebra: [`crate::lifetime::TeardownPolicy::ALL`]
2112/// (the *release*-time counterpart for non-pooled ephemeral envs),
2113/// [`crate::boundary::ConditionKind::ALL`],
2114/// [`crate::lifetime::LifetimeKind::ALL`],
2115/// [`crate::intent::IntentKind::ALL`],
2116/// [`crate::phase::ProcessPhase::ALL`],
2117/// [`crate::signal::ProcessSignal::ALL`].
2118#[derive(
2119    Clone,
2120    Copy,
2121    Debug,
2122    Hash,
2123    PartialEq,
2124    Eq,
2125    Serialize,
2126    Deserialize,
2127    JsonSchema,
2128    Default,
2129    tatara_closed_set::DeriveClosedSet,
2130)]
2131#[serde(rename_all = "PascalCase")]
2132#[closed_set(via = "as_str", generate_unknown, display)]
2133pub enum ReturnPolicy {
2134    /// Tear down the Process + create a fresh one. Safe but slow
2135    /// (1-2 min spin-up before the slot is Free again).
2136    #[default]
2137    Replace,
2138    /// Keep the Process running; run a typed `:reset` Job that wipes
2139    /// state (DB drop, secrets rotate). Fast (~5-10s) but depends on
2140    /// the reset Job being correct for the workload. API-authoritative
2141    /// systems are natural fits because the control API owns all state.
2142    Reset,
2143    /// Keep the Process indefinitely after release (debugging aid;
2144    /// operator must `feira pool reap NAME` to clean up). Useful for
2145    /// post-mortem of a flaky test.
2146    Keep,
2147}
2148
2149impl ReturnPolicy {
2150    /// The closed set of return policies — single source of truth that
2151    /// drives the `as_str` / Display / `FromStr` triad and the
2152    /// `keeps_process` / `runs_reset_job` predicate pair. Adding a
2153    /// fourth variant lands at one `ALL` entry + one `as_str` arm +
2154    /// one arm per predicate — exhaustively checked by the compiler
2155    /// (the `[Self; 3]` array literal forces the arity) and by the
2156    /// predicate-pair injectivity test (a new variant must land in
2157    /// its own (keeps_process, runs_reset_job) bucket or the author
2158    /// has to extend the consumer dispatch in
2159    /// `tatara-pool-reconciler::return_policy::plan_return`).
2160    pub const ALL: [Self; 3] = [Self::Replace, Self::Reset, Self::Keep];
2161
2162    /// Canonical PascalCase wire-format projection — matches the
2163    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
2164    /// `enum:` enumeration the pool reconciler stamps on the
2165    /// `ephemeralpools.tatara.pleme.io` schema. Pinned by
2166    /// `return_policy_as_str_matches_serde` so a variant rename can't
2167    /// drift between the typed surface, the CRD enum, the YAML wire
2168    /// format AND any future operator-facing diagnostic that composes
2169    /// `policy={policy}` via Display rather than a hard-coded literal.
2170    pub const fn as_str(self) -> &'static str {
2171        match self {
2172            Self::Replace => "Replace",
2173            Self::Reset => "Reset",
2174            Self::Keep => "Keep",
2175        }
2176    }
2177
2178    /// Does the pool keep the backing Process alive across release?
2179    /// Closed-set match (not `matches!`) so a future variant triggers
2180    /// the compiler's exhaustiveness check at this site rather than
2181    /// silently defaulting to `false`. Paired with `runs_reset_job`
2182    /// they form the two-axis projection that the consumer in
2183    /// `tatara-pool-reconciler::return_policy::plan_return` matches
2184    /// against — `keeps_process` false ⇒ `DeleteAndRespawn`;
2185    /// `keeps_process && runs_reset_job` ⇒ `ResetThenFree`;
2186    /// `keeps_process && !runs_reset_job` ⇒ `KeepForInspection`. The
2187    /// pair is `(false, false) | (true, true) | (true, false)` —
2188    /// pinned injective by
2189    /// `return_policy_predicate_pair_is_injective`.
2190    pub const fn keeps_process(self) -> bool {
2191        match self {
2192            Self::Replace => false,
2193            Self::Reset | Self::Keep => true,
2194        }
2195    }
2196
2197    /// Does the policy run a typed `:reset` Job to wipe state in
2198    /// place? See `keeps_process` for the closed-match rationale +
2199    /// the predicate-pair contract.
2200    pub const fn runs_reset_job(self) -> bool {
2201        match self {
2202            Self::Reset => true,
2203            Self::Replace | Self::Keep => false,
2204        }
2205    }
2206}
2207
2208// `impl FromStr for ReturnPolicy` + `impl tatara_lisp::ClosedSet for
2209// ReturnPolicy` + `impl fmt::Display for ReturnPolicy` are generated by
2210// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
2211// above. `label` delegates to the inherent `ReturnPolicy::as_str` via
2212// `#[closed_set(via = "as_str")]` so the
2213// `tatara-pool-reconciler::return_policy::plan_return` dispatch keeps
2214// reading the canonical PascalCase projection that matches the CRD
2215// `enum:` literal verbatim, while a generic `T: ClosedSet` consumer
2216// plugs in without knowing the enum lives in `tatara-process::pool`;
2217// Display delegates to the same inherent projection via
2218// `#[closed_set(display)]` so the `policy={policy}` diagnostic
2219// composition stays pinned on the closed-set algebra.
2220
2221// `pub struct UnknownReturnPolicy(pub String)` is generated by
2222// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
2223// on the enum declaration above. The auto-derived label `"return policy"`
2224// matches the prior hand-rolled `#[error("unknown return policy: {0}")]`
2225// verbatim. Symmetric to [`UnknownReplacementPolicy`],
2226// [`UnknownMemberState`], [`UnknownPoolPhase`],
2227// [`crate::lifetime::UnknownTeardownPolicy`],
2228// [`crate::boundary::UnknownConditionKind`], and
2229// [`crate::phase::UnknownPhase`].
2230
2231/// Routing selector — matches an `EphemeralAllocation`'s requestor
2232/// against pool-eligibility predicates.
2233#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
2234#[serde(rename_all = "camelCase")]
2235pub struct PoolSelector {
2236    /// Glob-matched against `EphemeralAllocation.spec.requestor.repo`.
2237    /// Empty = match every repo.
2238    #[serde(default)]
2239    pub repos: Vec<String>,
2240
2241    /// Glob-matched against `EphemeralAllocation.spec.requestor.branch`.
2242    /// Empty = match every branch.
2243    #[serde(default)]
2244    pub branches: Vec<String>,
2245
2246    /// PR labels (all-must-match, AND semantics). Empty = no label
2247    /// requirement.
2248    #[serde(default)]
2249    pub pr_labels: Vec<String>,
2250
2251    /// Allocation `kind` strings this pool can serve (e.g., "github-pr",
2252    /// "manual", "ci-run"). Empty = any kind.
2253    #[serde(default)]
2254    pub kinds: Vec<String>,
2255}
2256
2257impl PoolSelector {
2258    /// Does this selector match the given allocation routing key?
2259    /// Pure: no side effects.
2260    pub fn matches(&self, key: &MatchKey<'_>) -> bool {
2261        glob_any(&self.repos, key.repo)
2262            && glob_any(&self.branches, key.branch)
2263            && labels_subset(&self.pr_labels, key.pr_labels)
2264            && kind_any(&self.kinds, key.kind)
2265    }
2266
2267    /// Specificity score — higher = more specific. Used by the
2268    /// reconciler to break ties between selectors that all match.
2269    pub fn specificity(&self) -> u32 {
2270        let mut score = 0;
2271        if !self.repos.is_empty() {
2272            score += 8;
2273        }
2274        if !self.branches.is_empty() {
2275            score += 4;
2276        }
2277        score += (self.pr_labels.len() as u32) * 2;
2278        if !self.kinds.is_empty() {
2279            score += 1;
2280        }
2281        score
2282    }
2283}
2284
2285/// Allocation routing key — what the reconciler matches against pool selectors.
2286#[derive(Clone, Copy, Debug)]
2287pub struct MatchKey<'a> {
2288    pub repo: &'a str,
2289    pub branch: &'a str,
2290    pub pr_labels: &'a [String],
2291    pub kind: &'a str,
2292}
2293
2294impl<'a> MatchKey<'a> {
2295    /// Canonical 4-slot composer. Every routing key threads through
2296    /// this constructor so a future field addition — or a slot-order
2297    /// change — lands at ONE site instead of every fixture literal
2298    /// across `tatara-process::pool` tests + `tatara-pool-reconciler`
2299    /// (router + allocation_decide).
2300    #[must_use]
2301    pub const fn new(
2302        repo: &'a str,
2303        branch: &'a str,
2304        pr_labels: &'a [String],
2305        kind: &'a str,
2306    ) -> Self {
2307        Self {
2308            repo,
2309            branch,
2310            pr_labels,
2311            kind,
2312        }
2313    }
2314
2315    /// Label-agnostic peer of [`Self::new`] — routes through it with a
2316    /// static empty label slice. Every test that doesn't exercise the
2317    /// `pr_labels` axis names its intent (no labels) by calling this
2318    /// instead of hand-writing `pr_labels: &[]`.
2319    #[must_use]
2320    pub const fn unlabeled(repo: &'a str, branch: &'a str, kind: &'a str) -> Self {
2321        Self::new(repo, branch, &[], kind)
2322    }
2323}
2324
2325fn glob_any(patterns: &[String], value: &str) -> bool {
2326    if patterns.is_empty() {
2327        return true;
2328    }
2329    patterns.iter().any(|p| glob_match(p, value))
2330}
2331
2332fn kind_any(kinds: &[String], value: &str) -> bool {
2333    if kinds.is_empty() {
2334        return true;
2335    }
2336    kinds.iter().any(|k| k == value)
2337}
2338
2339fn labels_subset(required: &[String], present: &[String]) -> bool {
2340    required.iter().all(|r| present.iter().any(|p| p == r))
2341}
2342
2343/// Minimal glob: supports trailing `*` only (e.g., `"pleme-io/*"`,
2344/// `"release-*"`). Sufficient for repo/branch routing. Empty pattern
2345/// matches anything.
2346fn glob_match(pattern: &str, value: &str) -> bool {
2347    if pattern.is_empty() {
2348        return true;
2349    }
2350    if let Some(prefix) = pattern.strip_suffix('*') {
2351        value.starts_with(prefix)
2352    } else {
2353        pattern == value
2354    }
2355}
2356
2357#[cfg(test)]
2358mod tests {
2359    use super::*;
2360    // The closed-set tests below call `T::from_str(bad)` via the
2361    // derive-generated `FromStr` impls — bring the trait into scope at
2362    // the test module so the lib body doesn't carry an otherwise-unused
2363    // `use std::str::FromStr;` at the file head.
2364    use std::str::FromStr;
2365
2366    /// SLOT-BINDING CONTRACT: [`MatchKey::new`] threads every input
2367    /// verbatim into its named slot — a future field addition or
2368    /// slot-order swap in the struct lands here at ONE site instead
2369    /// of every fixture literal across the workspace.
2370    #[test]
2371    fn match_key_new_binds_all_four_slots_verbatim() {
2372        let labels: Vec<String> = vec!["needs-ephemeral".into()];
2373        let k = MatchKey::new("r", "b", &labels, "kind");
2374        assert_eq!(k.repo, "r");
2375        assert_eq!(k.branch, "b");
2376        assert_eq!(k.pr_labels, labels.as_slice());
2377        assert_eq!(k.kind, "kind");
2378    }
2379
2380    /// UNLABELED-PEER CONTRACT: [`MatchKey::unlabeled`] is
2381    /// definitionally [`MatchKey::new`] with a static empty label
2382    /// slice — the two composers must produce structurally identical
2383    /// keys on the label-empty axis.
2384    #[test]
2385    fn match_key_unlabeled_routes_through_new_with_empty_slice() {
2386        let empty: &[String] = &[];
2387        let via_unlabeled = MatchKey::unlabeled("r", "b", "kind");
2388        let via_new = MatchKey::new("r", "b", empty, "kind");
2389        assert_eq!(via_unlabeled.repo, via_new.repo);
2390        assert_eq!(via_unlabeled.branch, via_new.branch);
2391        assert_eq!(via_unlabeled.pr_labels, via_new.pr_labels);
2392        assert_eq!(via_unlabeled.kind, via_new.kind);
2393        assert!(via_unlabeled.pr_labels.is_empty());
2394    }
2395
2396    #[test]
2397    fn glob_trailing_star_matches_prefix() {
2398        assert!(glob_match("pleme-io/*", "pleme-io/demo-app"));
2399        assert!(!glob_match("pleme-io/*", "drzln/dotfiles"));
2400        assert!(glob_match("release-*", "release-2026-05"));
2401        assert!(!glob_match("release-*", "main"));
2402        assert!(glob_match("main", "main"));
2403        assert!(!glob_match("main", "develop"));
2404    }
2405
2406    #[test]
2407    fn empty_selector_matches_anything() {
2408        let s = PoolSelector::default();
2409        assert!(s.matches(&MatchKey::unlabeled("any/repo", "any-branch", "any")));
2410    }
2411
2412    #[test]
2413    fn repo_glob_filters_match_key() {
2414        let s = PoolSelector {
2415            repos: vec!["pleme-io/demo-*".into()],
2416            ..Default::default()
2417        };
2418        assert!(s.matches(&MatchKey::unlabeled("pleme-io/demo-app", "x", "y")));
2419        assert!(!s.matches(&MatchKey::unlabeled("pleme-io/other-repo", "x", "y")));
2420    }
2421
2422    #[test]
2423    fn pr_labels_require_all() {
2424        let s = PoolSelector {
2425            pr_labels: vec!["needs-ephemeral".into(), "integration".into()],
2426            ..Default::default()
2427        };
2428        // Both labels present → match.
2429        assert!(s.matches(&MatchKey::new(
2430            "x",
2431            "y",
2432            &[
2433                "needs-ephemeral".into(),
2434                "integration".into(),
2435                "extra".into()
2436            ],
2437            "z",
2438        )));
2439        // One label missing → no match.
2440        assert!(!s.matches(&MatchKey::new("x", "y", &["needs-ephemeral".into()], "z",)));
2441    }
2442
2443    #[test]
2444    fn specificity_ranks_more_constrained_higher() {
2445        let general = PoolSelector::default();
2446        let specific = PoolSelector {
2447            repos: vec!["pleme-io/*".into()],
2448            branches: vec!["main".into()],
2449            pr_labels: vec!["needs-ephemeral".into()],
2450            kinds: vec!["github-pr".into()],
2451        };
2452        assert!(specific.specificity() > general.specificity());
2453    }
2454
2455    #[test]
2456    fn return_policy_defaults_to_replace() {
2457        assert_eq!(ReturnPolicy::default(), ReturnPolicy::Replace);
2458    }
2459
2460    #[test]
2461    fn pool_phase_defaults_to_initializing() {
2462        assert_eq!(PoolPhase::default(), PoolPhase::Initializing);
2463    }
2464
2465    // ── closed-set algebra contracts for ReplacementPolicy
2466    //    (ALL × as_str × FromStr × predicate-pair) ────────────────────
2467
2468    /// Structural well-formedness of [`ReplacementPolicy`] as a
2469    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
2470    /// testkit lift that pins all three structural invariants (`ALL`
2471    /// is non-empty, every variant round-trips through
2472    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
2473    /// outside the closed set) at ONE call site. Replaces the hand-
2474    /// derived `replacement_policy_all_is_unique_and_complete` +
2475    /// `replacement_policy_roundtrip_via_as_str` + the empty-input arm
2476    /// of `unknown_replacement_policy_errors`. `FromStr` delegates to
2477    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
2478    /// exercises the same code path the pool reconciler hits when
2479    /// parsing a CRD `enum:`-validated value back to the typed policy.
2480    #[test]
2481    fn replacement_policy_is_well_formed_closed_set() {
2482        tatara_closed_set::assert_closed_set_well_formed::<ReplacementPolicy>();
2483    }
2484
2485    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2486    /// output verbatim for every variant. A future variant rename (or
2487    /// an `as_str` arm typo) lands here at one site, instead of
2488    /// drifting between the typed surface, the CRD enum, and the
2489    /// YAML wire format.
2490    #[test]
2491    fn replacement_policy_as_str_matches_serde() {
2492        crate::tagged_union::assert_label_matches_serde_serialization::<ReplacementPolicy>();
2493    }
2494
2495    /// The Display impl IS `as_str` — pinning this lets future callers
2496    /// reach for either projection without drift. The operator-facing
2497    /// "policy={policy}" diagnostic in `tatara-pool-reconciler::desired`
2498    /// composes through Display rather than through a hard-coded
2499    /// variant string.
2500    #[test]
2501    fn replacement_policy_display_matches_as_str() {
2502        crate::tagged_union::assert_display_matches_label::<ReplacementPolicy>();
2503    }
2504
2505    /// `FromStr` rejects strings that aren't in the canonical
2506    /// projection — lowercased / typo / cross-axis-leaked — and the
2507    /// error echoes the input verbatim so the operator-facing
2508    /// diagnostic carries the offending value, not a normalized form.
2509    /// The empty-input arm is pinned by
2510    /// [`replacement_policy_is_well_formed_closed_set`] via the
2511    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
2512    /// verbatim-echo contract on the [`UnknownReplacementPolicy`]
2513    /// newtype, which the trait's `make_unknown` can't see.
2514    #[test]
2515    fn unknown_replacement_policy_errors() {
2516        for bad in [
2517            "replaceimmediate",
2518            "PAUSEPOOL",
2519            "Replace-Immediate",
2520            "hold_failed",
2521            "Pause",
2522            "Reset",
2523        ] {
2524            let err = ReplacementPolicy::from_str(bad).unwrap_err();
2525            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2526        }
2527    }
2528
2529    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2530    /// documented per-variant on-failure behavior.
2531    #[test]
2532    fn replacement_policy_predicate_truth_tables() {
2533        assert!(ReplacementPolicy::ReplaceImmediate.replaces_failed());
2534        assert!(!ReplacementPolicy::ReplaceImmediate.pauses_on_failure());
2535
2536        assert!(!ReplacementPolicy::HoldFailed.replaces_failed());
2537        assert!(!ReplacementPolicy::HoldFailed.pauses_on_failure());
2538
2539        assert!(!ReplacementPolicy::PausePool.replaces_failed());
2540        assert!(ReplacementPolicy::PausePool.pauses_on_failure());
2541    }
2542
2543    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
2544    /// predicates simultaneously — the two on-failure actions
2545    /// (reap-each-failed vs pause-whole-pool) are mutually exclusive.
2546    /// A future `ReplacementPolicy::PauseAndReap` that returned true
2547    /// from both would FAIL here, forcing the author to either pick
2548    /// one bucket or extend the consumer dispatch site in
2549    /// `tatara-pool-reconciler::desired::PoolConvergence::decide`
2550    /// deliberately rather than silently double-firing both branches.
2551    #[test]
2552    fn replacement_policy_predicates_are_disjoint() {
2553        for policy in ReplacementPolicy::ALL {
2554            assert!(
2555                !(policy.replaces_failed() && policy.pauses_on_failure()),
2556                "{policy:?} returns true from both replaces_failed and pauses_on_failure",
2557            );
2558        }
2559    }
2560
2561    /// INJECTIVITY CONTRACT: the pair `(replaces_failed,
2562    /// pauses_on_failure)` is injective across `ALL`. Each variant
2563    /// projects to its own `(bool, bool)` bucket: `(true, false)` =
2564    /// reap; `(false, false)` = hold; `(false, true)` = pause. Pairing
2565    /// this with the disjointness contract above forces a future
2566    /// variant to land in a fresh `(replaces_failed,
2567    /// pauses_on_failure)` bucket — or the author extends the consumer
2568    /// dispatch in `tatara-pool-reconciler::desired::PoolConvergence`
2569    /// to recognize the new projection bucket.
2570    #[test]
2571    fn replacement_policy_predicate_pair_is_injective() {
2572        let projections: Vec<(bool, bool)> = ReplacementPolicy::ALL
2573            .into_iter()
2574            .map(|p| (p.replaces_failed(), p.pauses_on_failure()))
2575            .collect();
2576        let unique: std::collections::HashSet<_> = projections.iter().copied().collect();
2577        assert_eq!(
2578            projections.len(),
2579            unique.len(),
2580            "predicate pair projection is not injective: {projections:?}",
2581        );
2582    }
2583
2584    /// DEFAULT-AGREEMENT CONTRACT: `ReplacementPolicy::default()`
2585    /// returns the variant tagged `#[default]` in the enum, AND that
2586    /// variant reaps (the production-safe behavior). A future #[default]
2587    /// rename without flipping the predicates fails here.
2588    #[test]
2589    fn replacement_policy_default_replaces_failed() {
2590        let d = ReplacementPolicy::default();
2591        assert_eq!(d, ReplacementPolicy::ReplaceImmediate);
2592        assert!(d.replaces_failed());
2593        assert!(!d.pauses_on_failure());
2594    }
2595
2596    #[test]
2597    fn kinds_filter_to_known_set() {
2598        let s = PoolSelector {
2599            kinds: vec!["github-pr".into(), "manual".into()],
2600            ..Default::default()
2601        };
2602        assert!(s.matches(&MatchKey::unlabeled("x", "y", "github-pr")));
2603        assert!(!s.matches(&MatchKey::unlabeled("x", "y", "scheduled")));
2604    }
2605
2606    // ── closed-set algebra contracts for ReturnPolicy
2607    //    (ALL × as_str × FromStr × predicate-pair) ────────────────────
2608
2609    /// Structural well-formedness of [`ReturnPolicy`] as a
2610    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
2611    /// symmetric to [`replacement_policy_is_well_formed_closed_set`]
2612    /// above.
2613    #[test]
2614    fn return_policy_is_well_formed_closed_set() {
2615        tatara_closed_set::assert_closed_set_well_formed::<ReturnPolicy>();
2616    }
2617
2618    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2619    /// output verbatim for every variant. A future variant rename (or
2620    /// an `as_str` arm typo) lands here at one site, instead of
2621    /// drifting between the typed surface, the CRD enum, and the
2622    /// YAML wire format.
2623    #[test]
2624    fn return_policy_as_str_matches_serde() {
2625        crate::tagged_union::assert_label_matches_serde_serialization::<ReturnPolicy>();
2626    }
2627
2628    /// The Display impl IS `as_str` — pinning this lets future callers
2629    /// reach for either projection without drift, mirroring the
2630    /// `ReplacementPolicy` discipline.
2631    #[test]
2632    fn return_policy_display_matches_as_str() {
2633        crate::tagged_union::assert_display_matches_label::<ReturnPolicy>();
2634    }
2635
2636    /// `FromStr` rejects strings that aren't in the canonical
2637    /// projection — lowercased / typo / cross-axis-leaked — and the
2638    /// error echoes the input verbatim so the operator-facing
2639    /// diagnostic carries the offending value, not a normalized form.
2640    /// The empty-input arm is pinned by
2641    /// [`return_policy_is_well_formed_closed_set`] via the
2642    /// `tatara_lisp::ClosedSet` testkit.
2643    #[test]
2644    fn unknown_return_policy_errors() {
2645        for bad in [
2646            "replace",
2647            "RESET",
2648            "Re-place",
2649            "keep_for_inspection",
2650            "DeleteAndRespawn",
2651            "ReplaceImmediate",
2652        ] {
2653            let err = ReturnPolicy::from_str(bad).unwrap_err();
2654            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2655        }
2656    }
2657
2658    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2659    /// documented per-variant on-release behavior.
2660    #[test]
2661    fn return_policy_predicate_truth_tables() {
2662        assert!(!ReturnPolicy::Replace.keeps_process());
2663        assert!(!ReturnPolicy::Replace.runs_reset_job());
2664
2665        assert!(ReturnPolicy::Reset.keeps_process());
2666        assert!(ReturnPolicy::Reset.runs_reset_job());
2667
2668        assert!(ReturnPolicy::Keep.keeps_process());
2669        assert!(!ReturnPolicy::Keep.runs_reset_job());
2670    }
2671
2672    /// IMPLICATION CONTRACT: `runs_reset_job` implies `keeps_process`.
2673    /// You cannot run a typed `:reset` Job against a Process you've
2674    /// just deleted; the impossible bucket `(false, true)` must stay
2675    /// empty. A future variant returning true from `runs_reset_job`
2676    /// while returning false from `keeps_process` fails here, which
2677    /// forces the author to either flip `keeps_process` to true or
2678    /// extend the consumer dispatch site in
2679    /// `tatara-pool-reconciler::return_policy::plan_return`
2680    /// deliberately rather than letting an impossible state slip in.
2681    #[test]
2682    fn return_policy_reset_implies_keeps_process() {
2683        for policy in ReturnPolicy::ALL {
2684            if policy.runs_reset_job() {
2685                assert!(
2686                    policy.keeps_process(),
2687                    "{policy:?} runs a reset job but does not keep the process",
2688                );
2689            }
2690        }
2691    }
2692
2693    /// INJECTIVITY CONTRACT: the pair `(keeps_process, runs_reset_job)`
2694    /// is injective across `ALL`. Each variant projects to its own
2695    /// `(bool, bool)` bucket: `(false, false)` = delete + respawn;
2696    /// `(true, true)` = reset-in-place; `(true, false)` = keep for
2697    /// inspection. Pairing this with the implication contract above
2698    /// forces a future variant to land in a fresh
2699    /// `(keeps_process, runs_reset_job)` bucket — or the author
2700    /// extends the consumer dispatch in
2701    /// `tatara-pool-reconciler::return_policy::plan_return` to
2702    /// recognize the new projection bucket.
2703    #[test]
2704    fn return_policy_predicate_pair_is_injective() {
2705        let projections: Vec<(bool, bool)> = ReturnPolicy::ALL
2706            .into_iter()
2707            .map(|p| (p.keeps_process(), p.runs_reset_job()))
2708            .collect();
2709        let unique: std::collections::HashSet<_> = projections.iter().copied().collect();
2710        assert_eq!(
2711            projections.len(),
2712            unique.len(),
2713            "predicate pair projection is not injective: {projections:?}",
2714        );
2715    }
2716
2717    /// DEFAULT-AGREEMENT CONTRACT: `ReturnPolicy::default()` returns
2718    /// the variant tagged `#[default]` in the enum, AND that variant
2719    /// is the safe "tear down + respawn" behavior — neither keeps the
2720    /// process nor runs a reset Job. A future `#[default]` rename
2721    /// without flipping the predicates fails here.
2722    #[test]
2723    fn return_policy_default_is_replace_and_neither_predicate_fires() {
2724        let d = ReturnPolicy::default();
2725        assert_eq!(d, ReturnPolicy::Replace);
2726        assert!(!d.keeps_process());
2727        assert!(!d.runs_reset_job());
2728    }
2729
2730    // ── closed-set algebra contracts for MemberState
2731    //    (ALL × as_str × FromStr × predicate pair) ────────────────────
2732
2733    /// Structural well-formedness of [`MemberState`] as a
2734    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
2735    /// symmetric to [`replacement_policy_is_well_formed_closed_set`]
2736    /// and [`return_policy_is_well_formed_closed_set`] above.
2737    #[test]
2738    fn member_state_is_well_formed_closed_set() {
2739        tatara_closed_set::assert_closed_set_well_formed::<MemberState>();
2740    }
2741
2742    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2743    /// output verbatim for every variant. A future variant rename (or
2744    /// an `as_str` arm typo) lands here at one site, instead of
2745    /// drifting between the typed surface, the CRD enum, and the YAML
2746    /// wire format the pool reconciler stamps on
2747    /// `status.members[].state`.
2748    #[test]
2749    fn member_state_as_str_matches_serde() {
2750        crate::tagged_union::assert_label_matches_serde_serialization::<MemberState>();
2751    }
2752
2753    /// The Display impl IS `as_str` — pinning this lets future callers
2754    /// reach for either projection without drift. Any operator-facing
2755    /// "state={state}" diagnostic that composes through Display
2756    /// inherits the canonical wire-format string automatically.
2757    #[test]
2758    fn member_state_display_matches_as_str() {
2759        crate::tagged_union::assert_display_matches_label::<MemberState>();
2760    }
2761
2762    /// `FromStr` rejects strings that aren't in the canonical
2763    /// projection — lowercased / typo / cross-axis-leaked — and
2764    /// the error echoes the input verbatim so the operator-facing
2765    /// diagnostic carries the offending value, not a normalized form.
2766    /// The empty-input arm is pinned by
2767    /// [`member_state_is_well_formed_closed_set`] via the
2768    /// `tatara_lisp::ClosedSet` testkit. The cross-axis leak cases
2769    /// pin the closed-set REJECTION contract that the trait can't see:
2770    /// `"ReplaceImmediate"`, `"Reset"`, and `"Attested"` are valid
2771    /// labels for sibling enums (`ReplacementPolicy`, `ReturnPolicy`,
2772    /// `ProcessPhase`) but MUST reject here, because the codomains
2773    /// are disjoint.
2774    #[test]
2775    fn unknown_member_state_errors() {
2776        for bad in [
2777            "free",
2778            "SPAWNING",
2779            "Free-State",
2780            "allocated_now",
2781            "ReplaceImmediate", // ReplacementPolicy-axis leak
2782            "Reset",            // ReturnPolicy-axis leak
2783            "Attested",         // ProcessPhase-axis leak
2784        ] {
2785            let err = MemberState::from_str(bad).unwrap_err();
2786            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2787        }
2788    }
2789
2790    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2791    /// documented per-variant lifecycle role. The pool reconciler's
2792    /// `pool_phase_from_members` supply calc collapses
2793    /// `count_state(Free) + count_state(Spawning)` into one
2794    /// `counts_toward_supply` filter; this table pins the per-variant
2795    /// projection that consumer depends on.
2796    #[test]
2797    fn member_state_predicate_truth_tables() {
2798        assert!(!MemberState::Spawning.is_failed());
2799        assert!(MemberState::Spawning.counts_toward_supply());
2800
2801        assert!(!MemberState::Free.is_failed());
2802        assert!(MemberState::Free.counts_toward_supply());
2803
2804        assert!(!MemberState::Allocated.is_failed());
2805        assert!(!MemberState::Allocated.counts_toward_supply());
2806
2807        assert!(!MemberState::Returning.is_failed());
2808        assert!(!MemberState::Returning.counts_toward_supply());
2809
2810        assert!(MemberState::Failed.is_failed());
2811        assert!(!MemberState::Failed.counts_toward_supply());
2812    }
2813
2814    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
2815    /// `is_failed` and `counts_toward_supply` simultaneously — a
2816    /// failed member can never be counted as available capacity. A
2817    /// future variant that returned true from both would FAIL here,
2818    /// forcing the author to either drop it from supply, or extend
2819    /// the consumer's bucketing in
2820    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
2821    /// deliberately rather than silently inflating the pool's supply
2822    /// count with failed slots.
2823    #[test]
2824    fn member_state_failed_implies_no_supply() {
2825        for state in MemberState::ALL {
2826            assert!(
2827                !(state.is_failed() && state.counts_toward_supply()),
2828                "{state:?} returns true from both is_failed and counts_toward_supply — \
2829                 a failed member can never be counted as available pool capacity",
2830            );
2831        }
2832    }
2833
2834    /// COVERAGE CONTRACT: every variant lands somewhere — either
2835    /// in supply, or as a failed slot, or as an in-use bucket
2836    /// (`Allocated | Returning`). A future variant that returns
2837    /// `false` from `counts_toward_supply` AND `false` from
2838    /// `is_failed` is fine *iff* it represents an in-use slot; this
2839    /// test pins the existing variants in their declared buckets so
2840    /// the consumer-side dispatch in
2841    /// `tatara-pool-reconciler::pool_decide::decide_pool_reconcile`
2842    /// stays grounded.
2843    #[test]
2844    fn member_state_buckets_cover_every_variant() {
2845        let mut supply = 0u32;
2846        let mut failed = 0u32;
2847        let mut in_use = 0u32;
2848        for state in MemberState::ALL {
2849            match (state.is_failed(), state.counts_toward_supply()) {
2850                (true, false) => failed += 1,
2851                (false, true) => supply += 1,
2852                (false, false) => in_use += 1,
2853                (true, true) => panic!("disjointness already pins this empty for {state:?}"),
2854            }
2855        }
2856        assert_eq!(supply, 2, "supply bucket: Free + Spawning");
2857        assert_eq!(failed, 1, "failed bucket: Failed");
2858        assert_eq!(in_use, 2, "in-use bucket: Allocated + Returning");
2859        assert_eq!(supply + failed + in_use, MemberState::ALL.len() as u32);
2860    }
2861
2862    // ── closed-set algebra contracts for PoolPhase
2863    //    (ALL × as_str × FromStr × predicate pair) ────────────────────
2864
2865    /// Structural well-formedness of [`PoolPhase`] as a
2866    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
2867    /// symmetric to [`member_state_is_well_formed_closed_set`] above.
2868    #[test]
2869    fn pool_phase_is_well_formed_closed_set() {
2870        tatara_closed_set::assert_closed_set_well_formed::<PoolPhase>();
2871    }
2872
2873    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2874    /// output verbatim for every variant. A future variant rename (or
2875    /// an `as_str` arm typo) lands here at one site, instead of
2876    /// drifting between the typed surface, the CRD enum, and the YAML
2877    /// wire format the pool reconciler stamps on `status.phase`.
2878    #[test]
2879    fn pool_phase_as_str_matches_serde() {
2880        crate::tagged_union::assert_label_matches_serde_serialization::<PoolPhase>();
2881    }
2882
2883    /// The Display impl IS `as_str` — pinning this lets future callers
2884    /// reach for either projection without drift. Any operator-facing
2885    /// "phase={phase}" diagnostic that composes through Display
2886    /// inherits the canonical wire-format string automatically.
2887    #[test]
2888    fn pool_phase_display_matches_as_str() {
2889        crate::tagged_union::assert_display_matches_label::<PoolPhase>();
2890    }
2891
2892    /// `FromStr` rejects strings that aren't in the canonical
2893    /// projection — lowercased / typo / cross-axis-leaked — and
2894    /// the error echoes the input verbatim so the operator-facing
2895    /// diagnostic carries the offending value, not a normalized form.
2896    /// The empty-input arm is pinned by
2897    /// [`pool_phase_is_well_formed_closed_set`] via the
2898    /// `tatara_lisp::ClosedSet` testkit. The cross-axis leak cases
2899    /// (`"Free"`, `"Replace"`, `"Attested"`, `"HoldFailed"`) pin the
2900    /// closed-set REJECTION contract that the trait can't see — those
2901    /// are valid sibling-axis labels but MUST reject here.
2902    #[test]
2903    fn unknown_pool_phase_errors() {
2904        for bad in [
2905            "steady",
2906            "SCALINGUP",
2907            "Scaling-Up",
2908            "scaling_down",
2909            "Free",       // MemberState-axis leak
2910            "Replace",    // ReturnPolicy-axis leak
2911            "Attested",   // ProcessPhase-axis leak
2912            "HoldFailed", // ReplacementPolicy-axis leak
2913        ] {
2914            let err = PoolPhase::from_str(bad).unwrap_err();
2915            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2916        }
2917    }
2918
2919    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2920    /// documented per-variant lifecycle role. Pinning this table at
2921    /// one site means any future status-aggregator surface
2922    /// (`feira pool list --healthy`, the SSE filter, the desired-loop
2923    /// heartbeat short-circuit) reads the same projection that the
2924    /// reconciler writes.
2925    #[test]
2926    fn pool_phase_predicate_truth_tables() {
2927        assert!(!PoolPhase::Initializing.is_steady());
2928        assert!(!PoolPhase::Initializing.is_terminal());
2929
2930        assert!(PoolPhase::Steady.is_steady());
2931        assert!(!PoolPhase::Steady.is_terminal());
2932
2933        assert!(!PoolPhase::ScalingUp.is_steady());
2934        assert!(!PoolPhase::ScalingUp.is_terminal());
2935
2936        assert!(!PoolPhase::ScalingDown.is_steady());
2937        assert!(!PoolPhase::ScalingDown.is_terminal());
2938
2939        assert!(!PoolPhase::Degraded.is_steady());
2940        assert!(!PoolPhase::Degraded.is_terminal());
2941
2942        assert!(!PoolPhase::Draining.is_steady());
2943        assert!(PoolPhase::Draining.is_terminal());
2944    }
2945
2946    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
2947    /// `is_steady` and `is_terminal` simultaneously — a draining pool
2948    /// is by definition transitioning OUT, not the goal converged
2949    /// state. A future variant that returned true from both would
2950    /// FAIL here, forcing the author to either pick one bucket or
2951    /// extend the consumer dispatch sites (status aggregators,
2952    /// heartbeat short-circuit) deliberately rather than silently
2953    /// double-firing both branches.
2954    #[test]
2955    fn pool_phase_steady_excludes_terminal() {
2956        for phase in PoolPhase::ALL {
2957            assert!(
2958                !(phase.is_steady() && phase.is_terminal()),
2959                "{phase:?} returns true from both is_steady and is_terminal — \
2960                 a draining pool is by definition not the converged goal state",
2961            );
2962        }
2963    }
2964
2965    /// COVERAGE CONTRACT: every variant lands somewhere — either the
2966    /// converged goal (`Steady`), the absorbing exit (`Draining`),
2967    /// or the transient bucket (`Initializing | ScalingUp |
2968    /// ScalingDown | Degraded` — pool is in motion toward desired).
2969    /// A future variant that returns `false` from BOTH predicates is
2970    /// fine *iff* it represents an in-motion state; this test pins
2971    /// the existing variants in their declared buckets so the
2972    /// projection consumers stay grounded.
2973    #[test]
2974    fn pool_phase_buckets_cover_every_variant() {
2975        let mut converged = 0u32;
2976        let mut terminal = 0u32;
2977        let mut transient = 0u32;
2978        for phase in PoolPhase::ALL {
2979            match (phase.is_steady(), phase.is_terminal()) {
2980                (true, false) => converged += 1,
2981                (false, true) => terminal += 1,
2982                (false, false) => transient += 1,
2983                (true, true) => panic!("disjointness already pins this empty for {phase:?}"),
2984            }
2985        }
2986        assert_eq!(converged, 1, "converged bucket: Steady");
2987        assert_eq!(terminal, 1, "terminal bucket: Draining");
2988        assert_eq!(
2989            transient, 4,
2990            "transient bucket: Initializing + ScalingUp + ScalingDown + Degraded"
2991        );
2992        assert_eq!(
2993            converged + terminal + transient,
2994            PoolPhase::ALL.len() as u32
2995        );
2996    }
2997
2998    /// DEFAULT-AGREEMENT CONTRACT: `PoolPhase::default()` returns the
2999    /// variant a freshly-admitted pool should land in — `Initializing`
3000    /// — AND that variant is neither steady (no members yet) nor
3001    /// terminal (not deletion-stamped). A future `Default` rename
3002    /// without flipping the predicates fails here.
3003    #[test]
3004    fn pool_phase_default_is_initializing_in_transient_bucket() {
3005        let d = PoolPhase::default();
3006        assert_eq!(d, PoolPhase::Initializing);
3007        assert!(!d.is_steady());
3008        assert!(!d.is_terminal());
3009    }
3010
3011    // ─────────────────────────────────────────────────────────────────
3012    // `EphemeralPool::name_or_empty` — borrow-form metadata-projection
3013    // primitive on the `metadata.name` axis. Pins the missing-slot
3014    // corner, the populated-slot corner, the pre-lift chain-shape
3015    // parity, and the pure-projection discipline that the two
3016    // `tatara-pool-reconciler` consumers routed onto the primitive
3017    // depend on. See the primitive's doc-comment for the full
3018    // migration rationale.
3019    // ─────────────────────────────────────────────────────────────────
3020
3021    fn empty_template() -> EphemeralSpec {
3022        EphemeralSpec {
3023            aplicacao: crate::intent::AplicacaoIntent::chart_only("oci://x", "1"),
3024            ttl: "1h".into(),
3025            teardown: crate::lifetime::TeardownPolicy::Always,
3026            max_concurrent: 0,
3027            postconditions: vec![],
3028            preconditions: vec![],
3029            verify_timeout: None,
3030            classification: None,
3031            parent: None,
3032            exports: vec![],
3033            routing: None,
3034        }
3035    }
3036
3037    fn pool_spec() -> PoolSpec {
3038        // Every non-template slot rides the ONE substrate composer
3039        // [`PoolSpec::with_template`] at its wire-published default;
3040        // pre-lift this fixture spelled the full 11-slot struct-literal
3041        // verbatim as one of eight cross-crate hand-authored copies past
3042        // the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold. See the
3043        // primitive's doc-comment for the full migration rationale.
3044        PoolSpec {
3045            desired_size: 1,
3046            ..PoolSpec::with_template(empty_template())
3047        }
3048    }
3049
3050    fn pool_named(name: &str) -> EphemeralPool {
3051        EphemeralPool::new(name, pool_spec())
3052    }
3053
3054    fn pool_unnamed() -> EphemeralPool {
3055        let mut p = EphemeralPool::new("scratch", pool_spec());
3056        p.metadata.name = None;
3057        p
3058    }
3059
3060    #[test]
3061    fn name_or_empty_returns_empty_string_when_metadata_name_is_none() {
3062        let p = pool_unnamed();
3063        assert!(p.metadata.name.is_none(), "fixture invariant");
3064        assert_eq!(p.name_or_empty(), "");
3065    }
3066
3067    #[test]
3068    fn name_or_empty_returns_populated_slot_verbatim() {
3069        let p = pool_named("attest-pool");
3070        assert_eq!(p.name_or_empty(), "attest-pool");
3071    }
3072
3073    #[test]
3074    fn name_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
3075        // Corner between `None` (missing slot) and `Some(String::new())`
3076        // (populated slot containing the empty string): the primitive
3077        // MUST fold both to the same `""` byte-shape so a downstream
3078        // `HashMap<String,_>::get(name)` / `str::cmp` sees ONE
3079        // "unnamed pool" bucket regardless of which shape the K8s API
3080        // server materialized. This is byte-identical to what the
3081        // pre-lift `.as_deref().unwrap_or("")` chain produced.
3082        let mut p = pool_named("scratch");
3083        p.metadata.name = Some(String::new());
3084        assert_eq!(p.name_or_empty(), "");
3085    }
3086
3087    #[test]
3088    fn name_or_empty_is_a_pure_projection() {
3089        // Consecutive calls return byte-identical slices — no cached
3090        // state, no mutation on the `EphemeralPool` between calls.
3091        // Guards against a future refactor that plants a cache field
3092        // and drifts one caller from another silently.
3093        let p = pool_named("router-pool");
3094        assert_eq!(p.name_or_empty(), p.name_or_empty());
3095        assert_eq!(p.name_or_empty(), "router-pool");
3096        assert_eq!(p.name_or_empty(), "router-pool");
3097    }
3098
3099    #[test]
3100    fn name_or_empty_matches_pre_lift_chain_verbatim() {
3101        // Byte-identical parity with the two hand-authored
3102        // `.metadata.name.as_deref().unwrap_or("")` chains the
3103        // primitive replaces in `tatara-pool-reconciler::router` and
3104        // `tatara-pool-reconciler::controller_allocation`. Runs across
3105        // the FULL corner set of the metadata.name slot: absent,
3106        // present-with-value, present-with-empty-string.
3107        let cases: [(Option<String>, &str); 3] = [
3108            (None, ""),
3109            (Some("attest-pool".into()), "attest-pool"),
3110            (Some(String::new()), ""),
3111        ];
3112        for (slot, expected) in cases {
3113            let mut p = pool_named("scratch");
3114            p.metadata.name = slot.clone();
3115            let pre_lift = p.metadata.name.as_deref().unwrap_or("");
3116            assert_eq!(pre_lift, expected, "pre-lift chain sanity");
3117            assert_eq!(p.name_or_empty(), pre_lift);
3118            assert_eq!(p.name_or_empty(), expected);
3119        }
3120    }
3121
3122    #[test]
3123    fn name_or_empty_borrows_from_metadata_name_slot() {
3124        // The returned `&str` is tied to the `EphemeralPool`'s
3125        // lifetime — the caller can compare / hash / index without
3126        // allocating. This is the load-bearing property that lets
3127        // the `HashMap<String, _>::get(pool.name_or_empty())` closure
3128        // in `controller_allocation::reconcile_inner` skip cloning.
3129        let p = pool_named("attest-pool");
3130        let s: &str = p.name_or_empty();
3131        assert_eq!(s.as_ptr(), p.metadata.name.as_deref().unwrap().as_ptr());
3132    }
3133
3134    // ─── EphemeralPool::owned_name_or_empty substrate pins ────────────
3135    //
3136    // The owned-form peer of the borrow-form `name_or_empty` primitive
3137    // above. Sibling to the sister-CRD primitive
3138    // `crate::crd::Process::owned_name_or_empty` (owned + empty sentinel
3139    // on `Process::metadata.name`) — the four primitives now partition
3140    // the (borrow × owned) × (name × uid) corner of the metadata-slot
3141    // family on identical missing-slot semantics across BOTH tatara-
3142    // process CRDs (`Process::uid_or_empty` + `Process::owned_name_or_empty`
3143    // + `EphemeralPool::name_or_empty` + this method). Fail-before-pass-
3144    // after granularity: `owned_name_or_empty` did not exist on the pool
3145    // CRD pre-lift; the compiler cannot resolve the name until the impl
3146    // block above is in place, so a rollback of the primitive breaks
3147    // this whole module.
3148    #[test]
3149    fn owned_name_or_empty_returns_empty_string_when_metadata_name_is_none() {
3150        let p = pool_unnamed();
3151        assert!(p.metadata.name.is_none(), "fixture invariant");
3152        assert_eq!(p.owned_name_or_empty(), String::new());
3153    }
3154
3155    #[test]
3156    fn owned_name_or_empty_returns_owned_string_when_slot_is_populated() {
3157        let p = pool_named("attest-pool");
3158        assert_eq!(p.owned_name_or_empty(), "attest-pool");
3159    }
3160
3161    #[test]
3162    fn owned_name_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
3163        // Corner between `None` (missing slot) and `Some(String::new())`
3164        // (populated slot containing the empty string): the primitive
3165        // MUST fold both to the same `""` byte-shape so a downstream
3166        // `HashMap<String,_>::get(name)` sees ONE "unnamed pool" bucket
3167        // regardless of which shape the K8s API server materialized.
3168        // Byte-identical to what the pre-lift `.clone().unwrap_or_default()`
3169        // chain produced.
3170        let mut p = pool_named("scratch");
3171        p.metadata.name = Some(String::new());
3172        assert_eq!(p.owned_name_or_empty(), String::new());
3173        assert!(p.owned_name_or_empty().is_empty());
3174    }
3175
3176    #[test]
3177    fn owned_name_or_empty_is_a_pure_projection() {
3178        // Consecutive calls return byte-identical Strings — no cached
3179        // state, no mutation on the `EphemeralPool` between calls.
3180        // Guards against a future refactor that plants a cache field
3181        // and drifts one caller from another silently.
3182        let p = pool_named("router-pool");
3183        assert_eq!(p.owned_name_or_empty(), p.owned_name_or_empty());
3184        assert_eq!(p.owned_name_or_empty(), "router-pool");
3185        assert_eq!(p.owned_name_or_empty(), "router-pool");
3186    }
3187
3188    #[test]
3189    fn owned_name_or_empty_matches_pre_lift_chain_verbatim() {
3190        // Byte-identical parity with the two hand-authored
3191        // `.metadata.name.clone().unwrap_or_default()` chains the
3192        // primitive replaces in `tatara-pool-reconciler::
3193        // controller_allocation::reconcile_inner` (HashMap key seed)
3194        // and `tatara-pool-reconciler::allocation_decide::
3195        // AllocationConvergenceCtx::observe` (AllocationRef.name slot
3196        // seed). Runs across the FULL corner set of the metadata.name
3197        // slot: absent, present-with-value, present-with-empty-string.
3198        // A regression that inserted a normalization step at the
3199        // primitive the pre-lift chain does NOT apply — or vice versa —
3200        // surfaces here rather than as silent drift between the two
3201        // owned-form callsites and the ONE substrate owner they now
3202        // route through.
3203        let cases: [(Option<String>, &str); 3] = [
3204            (None, ""),
3205            (Some("attest-pool".into()), "attest-pool"),
3206            (Some(String::new()), ""),
3207        ];
3208        for (slot, expected) in cases {
3209            let mut p = pool_named("scratch");
3210            p.metadata.name = slot.clone();
3211            let pre_lift = p.metadata.name.clone().unwrap_or_default();
3212            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
3213            assert_eq!(p.owned_name_or_empty(), pre_lift);
3214            assert_eq!(p.owned_name_or_empty().as_str(), expected);
3215        }
3216    }
3217
3218    #[test]
3219    fn owned_name_or_empty_matches_borrow_form_peer_on_populated_slot() {
3220        // Cross-primitive coherence pin at the sibling corner: when the
3221        // slot is present, the borrow-form (`name_or_empty`) and owned-
3222        // form (`owned_name_or_empty`) primitives return the SAME byte
3223        // sequence and differ only in ownership. A regression that
3224        // skewed one form's fallback would surface here rather than as
3225        // silent drift between the router tie-break comparator and the
3226        // AllocationRef seed on the SAME pool.
3227        let p = pool_named("attest-pool");
3228        assert_eq!(p.name_or_empty(), p.owned_name_or_empty().as_str());
3229    }
3230
3231    #[test]
3232    fn owned_name_or_empty_matches_borrow_form_peer_on_missing_slot() {
3233        // Sibling corner of the coherence pin above: when the slot is
3234        // absent (or explicitly empty), BOTH primitives fold to the
3235        // same empty-string byte-shape. The load-bearing property is
3236        // that a caller who switches between the two return-forms
3237        // based on downstream ownership requirements never sees a
3238        // different missing-slot spelling as a side effect.
3239        let p = pool_unnamed();
3240        assert_eq!(p.name_or_empty(), p.owned_name_or_empty().as_str());
3241        assert_eq!(p.name_or_empty(), "");
3242        assert_eq!(p.owned_name_or_empty(), String::new());
3243    }
3244
3245    // ─── EphemeralPool::is_being_deleted substrate pins ───────────────
3246    //
3247    // Pins the copy-form metadata-projection primitive on the deletion-
3248    // tombstone axis of the pool CRD. Peer to the borrow-form + owned-
3249    // form metadata-fallback family (`name_or_empty`,
3250    // `owned_name_or_empty`); this one opens the presence-probe corner
3251    // for the tombstone slot. Sibling to the sister-CRD primitive
3252    // `crate::crd::Process::is_being_deleted` — the two primitives
3253    // now partition the tombstone-presence probe across BOTH tatara-
3254    // process CRDs on identical missing-slot semantics. Fail-before-
3255    // pass-after granularity: `is_being_deleted` did not exist on the
3256    // pool CRD pre-lift; the compiler cannot resolve the name until
3257    // the impl block above is in place, so a rollback of the primitive
3258    // breaks this whole module.
3259
3260    fn tombstoned_pool() -> EphemeralPool {
3261        let mut p = pool_named("attest-pool");
3262        p.metadata.namespace = Some("ephemeral-pools".into());
3263        // Routes through the ONE substrate composer
3264        // `tatara_process::time::tombstone_now` — see the peer
3265        // `tombstoned_process` doc-comment in `crd.rs` for the full
3266        // migration rationale.
3267        p.metadata.deletion_timestamp = crate::time::tombstone_now();
3268        p
3269    }
3270
3271    #[test]
3272    fn is_being_deleted_returns_false_when_deletion_timestamp_is_absent() {
3273        // Missing-tombstone corner pin: the primitive collapses the
3274        // no-tombstone case to `false` so the `→ Drain` short-circuit
3275        // at `decide_pool_reconcile` is NOT taken and the observed-
3276        // phase composer at `pool_phase_from_members` proceeds to its
3277        // normal (free / spawning / allocated) arithmetic branches
3278        // instead of short-circuiting to `PoolPhase::Draining`.
3279        // Matches the pre-lift `.is_some()` chain's `false` byte-
3280        // identically at every consumer's downstream gate.
3281        let mut p = pool_named("attest-pool");
3282        p.metadata.deletion_timestamp = None;
3283        assert!(!p.is_being_deleted());
3284    }
3285
3286    #[test]
3287    fn is_being_deleted_returns_true_when_deletion_timestamp_is_present() {
3288        // Present-tombstone corner pin: the primitive returns `true`
3289        // on any populated `metadata.deletionTimestamp` slot regardless
3290        // of the timestamp payload — the two consumers only read the
3291        // tombstone's PRESENCE, never its RFC-3339 timestamp value.
3292        // A regression that gated the `true` return on the timestamp
3293        // being non-epoch, or parsed the timestamp before returning,
3294        // would surface here rather than as silent skew at the
3295        // `→ Drain` decision or the `→ Draining` phase report on the
3296        // SAME `EphemeralPool`.
3297        let p = tombstoned_pool();
3298        assert!(p.is_being_deleted());
3299    }
3300
3301    #[test]
3302    fn is_being_deleted_is_a_pure_projection() {
3303        // Purity pin: two consecutive calls return byte-identical
3304        // `bool` values (no lazy materialization, no interior
3305        // mutation of `self`). Peer to the sibling
3306        // `name_or_empty_is_a_pure_projection` +
3307        // `owned_name_or_empty_is_a_pure_projection` pins in this
3308        // module and to `is_being_deleted_is_a_pure_projection` on
3309        // the sister-CRD `Process`; all four bind the pure-projection
3310        // discipline on the ONE substrate accessor per metadata slot.
3311        let p = tombstoned_pool();
3312        let a = p.is_being_deleted();
3313        let b = p.is_being_deleted();
3314        assert_eq!(a, b);
3315        assert!(a);
3316    }
3317
3318    #[test]
3319    fn is_being_deleted_matches_pre_lift_pool_reconciler_chain_shape() {
3320        // Parity pin: sweeps the two corners every pre-lift consumer
3321        // plausibly encountered (missing tombstone, present tombstone)
3322        // and compares the substrate call against a hand-authored pre-
3323        // lift chain byte-identically. A regression that reshaped
3324        // either corner would surface here rather than as silent
3325        // operator-facing skew between the pool-reconciler's `→ Drain`
3326        // decision and the observed-phase composer's `→ Draining`
3327        // report on the SAME `EphemeralPool` within one reconcile
3328        // pass.
3329        fn pre_lift(p: &EphemeralPool) -> bool {
3330            p.metadata.deletion_timestamp.is_some()
3331        }
3332        // Missing slot.
3333        let mut p = pool_named("attest-pool");
3334        p.metadata.deletion_timestamp = None;
3335        assert_eq!(p.is_being_deleted(), pre_lift(&p));
3336        // Populated slot.
3337        let p = tombstoned_pool();
3338        assert_eq!(p.is_being_deleted(), pre_lift(&p));
3339    }
3340
3341    #[test]
3342    fn is_being_deleted_composes_with_pool_phase_draining_at_reconcile_preempt() {
3343        // Call-site-shape pin: the `pool_phase_from_members`
3344        // deletion-preempt returns `PoolPhase::Draining` as soon as
3345        // `pool.is_being_deleted()` holds, regardless of the (free +
3346        // spawning) supply arithmetic that would otherwise pick
3347        // `Ready` / `Scaling` / `Degraded`. The `→ Drain` decision at
3348        // `decide_pool_reconcile` composes with the same probe on the
3349        // same tombstone-presence slot. A regression that broadened
3350        // the tombstone probe implicitly (returning `false` on a
3351        // present but zero-timestamp) or narrowed it (requiring an
3352        // additional `.finalizers.is_empty()` conjunct that the two
3353        // consumers never spelled) would surface here rather than as
3354        // silent operator-facing skew between the pool reconciler's
3355        // decision and the observed-phase composer on the SAME
3356        // `EphemeralPool` within one reconcile pass.
3357        let alive = pool_named("attest-pool");
3358        assert!(!alive.is_being_deleted());
3359        let dying = tombstoned_pool();
3360        assert!(dying.is_being_deleted());
3361    }
3362
3363    // ─── EphemeralPool::owned_namespace_or_empty substrate pins ───────
3364    //
3365    // The owned-form peer of the `owned_name_or_empty` primitive on the
3366    // sibling `metadata.namespace` axis — the paired half of the
3367    // `AllocationRef { name, namespace }` struct literal both
3368    // `AllocationConvergenceCtx::observe` and the composition pin
3369    // consume through the SAME `AllocationRef::new(name, namespace)`
3370    // constructor. Fail-before-pass-after granularity:
3371    // `owned_namespace_or_empty` did not exist on the pool CRD pre-
3372    // lift; the compiler cannot resolve the name until the impl block
3373    // above is in place, so a rollback of the primitive breaks this
3374    // whole module.
3375    #[test]
3376    fn owned_namespace_or_empty_returns_empty_string_when_metadata_namespace_is_none() {
3377        // Missing-slot corner pin: the primitive collapses the no-
3378        // namespace case to the load-bearing empty-string sentinel so
3379        // the downstream `AllocationRef.namespace` slot carries `""`
3380        // rather than a defaulted `"default"` string. See the doc-
3381        // comment's DELIBERATE-EMPTY-SENTINEL rationale for why the
3382        // fallback matches `.clone().unwrap_or_default()` byte-for-
3383        // byte rather than substituting `Process::DEFAULT_NAMESPACE`
3384        // at the primitive.
3385        let mut p = pool_named("attest-pool");
3386        p.metadata.namespace = None;
3387        assert!(p.metadata.namespace.is_none(), "fixture invariant");
3388        assert_eq!(p.owned_namespace_or_empty(), String::new());
3389    }
3390
3391    #[test]
3392    fn owned_namespace_or_empty_returns_owned_string_when_slot_is_populated() {
3393        let mut p = pool_named("attest-pool");
3394        p.metadata.namespace = Some("ephemeral-pools".into());
3395        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
3396    }
3397
3398    #[test]
3399    fn owned_namespace_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
3400        // Corner between `None` (missing slot) and `Some(String::new())`
3401        // (populated slot containing the empty string): the primitive
3402        // MUST fold both to the same `""` byte-shape so a downstream
3403        // `AllocationRef.namespace ==` comparator at
3404        // `resolve_pool` sees ONE "unset namespace" bucket regardless
3405        // of which shape the K8s API server materialized. Byte-
3406        // identical to what the pre-lift `.clone().unwrap_or_default()`
3407        // chain produced.
3408        let mut p = pool_named("attest-pool");
3409        p.metadata.namespace = Some(String::new());
3410        assert_eq!(p.owned_namespace_or_empty(), String::new());
3411        assert!(p.owned_namespace_or_empty().is_empty());
3412    }
3413
3414    #[test]
3415    fn owned_namespace_or_empty_is_a_pure_projection() {
3416        // Consecutive calls return byte-identical Strings — no cached
3417        // state, no mutation on the `EphemeralPool` between calls.
3418        // Peer to the sibling `owned_name_or_empty_is_a_pure_projection`
3419        // pin in this module and to `is_being_deleted_is_a_pure_projection`
3420        // on the same CRD; all three bind the pure-projection
3421        // discipline on the ONE substrate accessor per metadata slot.
3422        let mut p = pool_named("attest-pool");
3423        p.metadata.namespace = Some("ephemeral-pools".into());
3424        assert_eq!(p.owned_namespace_or_empty(), p.owned_namespace_or_empty());
3425        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
3426        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
3427    }
3428
3429    #[test]
3430    fn owned_namespace_or_empty_matches_pre_lift_chain_verbatim() {
3431        // Byte-identical parity with the two hand-authored
3432        // `.metadata.namespace.clone().unwrap_or_default()` chains
3433        // the primitive replaces in `tatara-pool-reconciler::
3434        // allocation_decide::AllocationConvergenceCtx::observe`
3435        // (matched-pool `AllocationRef.namespace` seed) and in the
3436        // sibling composition pin
3437        // `allocation_ref_new_composes_with_owned_name_or_empty_pool_projection`.
3438        // Runs across the FULL corner set of the metadata.namespace
3439        // slot: absent, present-with-value, present-with-empty-string.
3440        // A regression that inserted a normalization step at the
3441        // primitive the pre-lift chain does NOT apply — or vice versa —
3442        // surfaces here rather than as silent drift between the two
3443        // owned-form callsites and the ONE substrate owner they now
3444        // route through.
3445        let cases: [(Option<String>, &str); 3] = [
3446            (None, ""),
3447            (Some("ephemeral-pools".into()), "ephemeral-pools"),
3448            (Some(String::new()), ""),
3449        ];
3450        for (slot, expected) in cases {
3451            let mut p = pool_named("attest-pool");
3452            p.metadata.namespace = slot.clone();
3453            let pre_lift = p.metadata.namespace.clone().unwrap_or_default();
3454            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
3455            assert_eq!(p.owned_namespace_or_empty(), pre_lift);
3456            assert_eq!(p.owned_namespace_or_empty().as_str(), expected);
3457        }
3458    }
3459
3460    #[test]
3461    fn owned_namespace_or_empty_composes_with_owned_name_or_empty_on_paired_slot_axis() {
3462        // Paired-axis coherence pin: the two owned-form primitives on
3463        // the pool CRD's `metadata.name` + `metadata.namespace` slots
3464        // share the SAME empty-string sentinel on the missing corner,
3465        // so a caller that composes both halves into an
3466        // `AllocationRef` (as `AllocationConvergenceCtx::observe`
3467        // does) never sees a mixed-fallback pair (one `""`, the
3468        // other `"default"`) as a side effect of one slot being
3469        // absent. A regression that skewed either primitive's
3470        // fallback would surface here rather than as silent operator-
3471        // facing skew between the paired halves of the SAME
3472        // `AllocationRef` seed.
3473        let mut p = pool_named("attest-pool");
3474        p.metadata.namespace = None;
3475        p.metadata.name = None;
3476        assert_eq!(p.owned_name_or_empty(), p.owned_namespace_or_empty());
3477        assert_eq!(p.owned_name_or_empty(), String::new());
3478        assert_eq!(p.owned_namespace_or_empty(), String::new());
3479    }
3480
3481    #[test]
3482    fn owned_namespace_or_empty_does_not_default_to_process_default_namespace() {
3483        // Deliberate-empty-sentinel pin: the primitive's fallback is
3484        // `""`, NOT `crate::crd::Process::DEFAULT_NAMESPACE`. The
3485        // sole downstream consumer (`AllocationConvergenceCtx::observe`)
3486        // feeds the produced value into `AllocationRef.namespace`,
3487        // which is then matched byte-identically against
3488        // `spec.pool_ref.namespace` at `resolve_pool`. A silent
3489        // substitution of `"default"` at this primitive would alias
3490        // every namespace-absent pool to the `"default"` bucket at
3491        // the matcher, hiding the missing-slot corner from an
3492        // operator who explicitly authored an allocation against a
3493        // namespace-unset pool. Pinned so a future "helpful"
3494        // canonicalization step lands as a compiler-visible failure
3495        // here rather than as silent operator-facing skew at the
3496        // matched-pool seed.
3497        let mut p = pool_named("attest-pool");
3498        p.metadata.namespace = None;
3499        assert_ne!(
3500            p.owned_namespace_or_empty(),
3501            crate::crd::Process::DEFAULT_NAMESPACE
3502        );
3503        assert_eq!(p.owned_namespace_or_empty(), "");
3504    }
3505
3506    // ─── EphemeralPool::owned_uid_or_name_or_empty substrate pins ─────
3507    //
3508    // Pins the compound owned-form projection on the paired
3509    // `(metadata.uid, metadata.name)` axis of the pool CRD — the
3510    // ONE-liner collapse of the paired `.metadata.uid.clone()
3511    // .unwrap_or_else(|| name.<into>())` chain every pool-slot-name
3512    // consumer restated by hand pre-lift at TWO production sites in
3513    // `tatara-pool-reconciler::controller_pool` (spawn arm +
3514    // apply_convergence_actions arm), both feeding the SAME
3515    // `member_process_name(&pool_name, &pool_uid_or_name_fallback,
3516    // slot)` composer. Fail-before-pass-after granularity:
3517    // `owned_uid_or_name_or_empty` did not exist on the pool CRD pre-
3518    // lift; the compiler cannot resolve the name until the impl block
3519    // above is in place, so a rollback of the primitive breaks this
3520    // whole module.
3521    #[test]
3522    fn owned_uid_or_name_or_empty_returns_uid_when_uid_is_present() {
3523        // Preferred-slot pin: uid populated → uid wins, regardless of
3524        // whether the name-fallback slot is populated. Byte-identical
3525        // to what each pre-lift `.metadata.uid.clone().unwrap_or_else
3526        // (|| name.<into>())` chain returned in the reachable-state
3527        // corner where the K8s API server has stamped a uid (the
3528        // common case at both callsites, which are already gated by
3529        // `owned_coordinates_required()?`).
3530        let mut p = pool_named("attest-pool");
3531        p.metadata.uid = Some("uid-42".into());
3532        assert_eq!(p.owned_uid_or_name_or_empty(), "uid-42");
3533    }
3534
3535    #[test]
3536    fn owned_uid_or_name_or_empty_falls_back_to_name_when_uid_is_missing() {
3537        // Fallback-slot pin: uid absent → name wins. Byte-identical
3538        // to what each pre-lift chain returned in the corner where
3539        // the K8s API server has NOT yet stamped a uid (pre-admission
3540        // / unit-test in-memory pool). The pre-lift chain reached
3541        // the fallback via a locally-bound `name` string derived from
3542        // the same `.metadata.name` slot the primitive reaches via
3543        // `owned_name_or_empty()`.
3544        let mut p = pool_named("attest-pool");
3545        p.metadata.uid = None;
3546        assert_eq!(p.owned_uid_or_name_or_empty(), "attest-pool");
3547    }
3548
3549    #[test]
3550    fn owned_uid_or_name_or_empty_sinks_to_empty_when_both_slots_are_missing() {
3551        // Missing-both corner pin: uid absent AND name absent → the
3552        // load-bearing empty-string sentinel. Coherent with the
3553        // sibling primitives `owned_name_or_empty` +
3554        // `owned_namespace_or_empty` on the SAME empty-sentinel axis.
3555        // A regression that dropped either fallback surfaces here
3556        // rather than as a runtime panic on `.unwrap()` at a spawn
3557        // callsite that assumed both slots were populated.
3558        let mut p = pool_named("attest-pool");
3559        p.metadata.uid = None;
3560        p.metadata.name = None;
3561        assert_eq!(p.owned_uid_or_name_or_empty(), String::new());
3562        assert!(p.owned_uid_or_name_or_empty().is_empty());
3563    }
3564
3565    #[test]
3566    fn owned_uid_or_name_or_empty_prefers_uid_when_both_slots_are_present() {
3567        // Precedence pin: both slots populated → uid wins. The pre-
3568        // lift `.unwrap_or_else(|| name.<into>())` chain's short-
3569        // circuit on the `Some(u)` arm skipped the fallback entirely;
3570        // the primitive matches that byte-for-byte via `.clone()
3571        // .unwrap_or_else(|| self.owned_name_or_empty())`, so the
3572        // name-fallback slot is not read when uid is populated.
3573        let mut p = pool_named("attest-pool");
3574        p.metadata.uid = Some("uid-preferred".into());
3575        p.metadata.name = Some("attest-pool".into());
3576        assert_eq!(p.owned_uid_or_name_or_empty(), "uid-preferred");
3577        assert_ne!(p.owned_uid_or_name_or_empty(), "attest-pool");
3578    }
3579
3580    #[test]
3581    fn owned_uid_or_name_or_empty_returns_uid_even_when_uid_is_explicitly_empty_string() {
3582        // Corner between `None` (missing slot) and `Some(String::new())`
3583        // (populated slot containing the empty string): the primitive
3584        // MUST return the populated-empty-string uid rather than
3585        // falling back to the name half — byte-identical to what the
3586        // pre-lift `.metadata.uid.clone().unwrap_or_else(|| name...)`
3587        // chain produced, whose `unwrap_or_else` short-circuits on
3588        // `Some(_)` regardless of the wrapped value. Pinned so a
3589        // future "helpful" canonicalization that treats
3590        // `Some(String::new())` as `None` at the primitive lands as
3591        // a compiler-visible failure here rather than as silent
3592        // operator-facing skew between the two spawn-slot-slug seeds.
3593        let mut p = pool_named("attest-pool");
3594        p.metadata.uid = Some(String::new());
3595        p.metadata.name = Some("attest-pool".into());
3596        assert_eq!(p.owned_uid_or_name_or_empty(), String::new());
3597        assert_ne!(p.owned_uid_or_name_or_empty(), "attest-pool");
3598    }
3599
3600    #[test]
3601    fn owned_uid_or_name_or_empty_is_a_pure_projection() {
3602        // Consecutive calls return byte-identical Strings across the
3603        // FULL corner set (uid-present, uid-absent name-fallback,
3604        // both-absent empty-sentinel) — no cached state, no mutation
3605        // on the `EphemeralPool` between calls. Peer to the sibling
3606        // `owned_name_or_empty_is_a_pure_projection` +
3607        // `owned_namespace_or_empty_is_a_pure_projection` pins in
3608        // this module; all three bind the pure-projection discipline
3609        // on the ONE substrate accessor per metadata-derived slot.
3610        let mut p = pool_named("attest-pool");
3611        p.metadata.uid = Some("uid-42".into());
3612        assert_eq!(
3613            p.owned_uid_or_name_or_empty(),
3614            p.owned_uid_or_name_or_empty()
3615        );
3616        p.metadata.uid = None;
3617        assert_eq!(
3618            p.owned_uid_or_name_or_empty(),
3619            p.owned_uid_or_name_or_empty()
3620        );
3621        p.metadata.name = None;
3622        assert_eq!(
3623            p.owned_uid_or_name_or_empty(),
3624            p.owned_uid_or_name_or_empty()
3625        );
3626    }
3627
3628    #[test]
3629    fn owned_uid_or_name_or_empty_matches_pre_lift_chain_verbatim() {
3630        // Byte-identical parity with the two hand-authored
3631        // `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
3632        // chains the primitive replaces in
3633        // `tatara-pool-reconciler::controller_pool` (spawn arm +
3634        // apply_convergence_actions arm). Runs across the FULL
3635        // corner set of the paired (metadata.uid, metadata.name)
3636        // slots. A regression that inserted a normalization step at
3637        // the primitive the pre-lift chain does NOT apply — or vice
3638        // versa — surfaces here rather than as silent drift between
3639        // the two owned-form callsites and the ONE substrate owner
3640        // they now route through.
3641        let cases: [(Option<String>, Option<String>, &str); 6] = [
3642            (Some("uid-42".into()), Some("attest-pool".into()), "uid-42"),
3643            (Some("uid-42".into()), None, "uid-42"),
3644            (Some(String::new()), Some("attest-pool".into()), ""),
3645            (None, Some("attest-pool".into()), "attest-pool"),
3646            (None, Some(String::new()), ""),
3647            (None, None, ""),
3648        ];
3649        for (uid_slot, name_slot, expected) in cases {
3650            let mut p = pool_named("attest-pool");
3651            p.metadata.uid = uid_slot.clone();
3652            p.metadata.name = name_slot.clone();
3653            // Reproduce the pre-lift chain shape at the spawn arm
3654            // (fallback `|| name.clone()` on an extracted-earlier
3655            // `String` name) — semantically equivalent to
3656            // `.metadata.name.clone().unwrap_or_default()` at the
3657            // point of call because `owned_coordinates_required()?`
3658            // gate guarantees the caller's `name` binding matches
3659            // the pool's own `metadata.name` slot.
3660            let pre_lift = p
3661                .metadata
3662                .uid
3663                .clone()
3664                .unwrap_or_else(|| p.metadata.name.clone().unwrap_or_default());
3665            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
3666            assert_eq!(p.owned_uid_or_name_or_empty(), pre_lift);
3667            assert_eq!(p.owned_uid_or_name_or_empty().as_str(), expected);
3668        }
3669    }
3670
3671    #[test]
3672    fn owned_uid_or_name_or_empty_composes_with_member_process_name_seed_shape() {
3673        // Composition pin: the produced owned `String` feeds the
3674        // downstream `member_process_name(&pool_name, &pool_uid_or_
3675        // name_fallback, slot)` composer at both callsites, so the
3676        // seed's `String` shape must survive being borrowed as
3677        // `&str` for the composer without any owned/borrow-form
3678        // adaptation at the callsite. Binds the primitive's return
3679        // type + the borrow-form availability that the pre-lift
3680        // chain also produced (a locally-owned `String` from
3681        // `.clone().unwrap_or_else(|| name.<into>())`).
3682        let mut p = pool_named("attest-pool");
3683        p.metadata.uid = Some("uid-42".into());
3684        let seed: String = p.owned_uid_or_name_or_empty();
3685        let _borrowed: &str = &seed;
3686        assert_eq!(seed, "uid-42");
3687        p.metadata.uid = None;
3688        let seed_fallback: String = p.owned_uid_or_name_or_empty();
3689        let _borrowed_fallback: &str = &seed_fallback;
3690        assert_eq!(seed_fallback, "attest-pool");
3691    }
3692
3693    // ─── AllocationRef::new substrate pins ────────────────────────────
3694    //
3695    // Pins the substrate constructor for [`AllocationRef`] — the
3696    // ONE-liner composer that lifts the paired
3697    // `AllocationRef { name, namespace }` struct-literal every
3698    // downstream consumer restated by hand pre-lift at FOUR production
3699    // sites (2 × controller_allocation.rs assignedProcess seeds, 1 ×
3700    // allocation_decide.rs pool_ref seed, 1 × allocation_factory.rs
3701    // pool_ref seed) onto ONE substrate owner on `AllocationRef`.
3702    // Fail-before-pass-after granularity: `AllocationRef::new` did not
3703    // exist pre-lift; the compiler cannot resolve the name until the
3704    // impl block above is in place, so a rollback of the primitive
3705    // breaks this whole module.
3706
3707    #[test]
3708    fn allocation_ref_new_composes_owned_string_pair_verbatim() {
3709        // Happy-path pin: the constructor materializes an
3710        // `AllocationRef { name: <name>, namespace: <namespace> }`
3711        // byte-identical to the pre-lift struct literal every consumer
3712        // spelled. A regression that dropped either slot (e.g. an
3713        // erroneous `..Default::default()` on a shape that never had
3714        // a Default derive) surfaces here rather than as silent slot
3715        // loss downstream at the assignedProcess / bound_pool /
3716        // matched_pool / spec.pool_ref sinks.
3717        let r = AllocationRef::new(String::from("pr-42-demo"), String::from("ephemeral-pools"));
3718        assert_eq!(r.name, "pr-42-demo");
3719        assert_eq!(r.namespace, "ephemeral-pools");
3720    }
3721
3722    #[test]
3723    fn allocation_ref_new_matches_pre_lift_struct_literal_verbatim() {
3724        // Byte-identical parity pin: the substrate constructor and the
3725        // hand-authored struct literal produce equal `AllocationRef`
3726        // values on every provenance the FOUR pre-lift sites carried
3727        // (owned `String` from an owned-form projection; `&str`
3728        // promoted through `.to_string()`). A regression that inserted
3729        // a normalization step at the primitive the pre-lift literal
3730        // does NOT apply — or vice versa — surfaces here rather than
3731        // as silent drift between the four consumers and the ONE
3732        // substrate owner they now route through.
3733        let owned_name = String::from("pr-42-demo");
3734        let owned_ns = String::from("ephemeral-pools");
3735        let lifted = AllocationRef::new(owned_name.clone(), owned_ns.clone());
3736        let pre_lift = AllocationRef {
3737            name: owned_name,
3738            namespace: owned_ns,
3739        };
3740        assert_eq!(lifted, pre_lift);
3741    }
3742
3743    #[test]
3744    fn allocation_ref_new_accepts_str_provenance_via_into_string() {
3745        // `Into<String>` provenance-closure pin: the primitive accepts
3746        // every provenance the pre-lift sites carried. The
3747        // controller_allocation.rs assignedProcess seeds passed owned
3748        // `String` values (a moved `member_process_name` +
3749        // `ns.clone()`); the allocation_factory.rs pool_ref seed
3750        // passed `&str` (`n.to_string()` / `namespace.to_string()`).
3751        // Both provenances produce byte-identical output. A future
3752        // refactor of the constructor signature that demanded owned
3753        // `String` at author sites (dropping `impl Into<String>`)
3754        // would force `.to_string()` back at the FOUR call sites — the
3755        // pin fences that regression at ONE place.
3756        let from_str = AllocationRef::new("pr-42-demo", "ephemeral-pools");
3757        let from_string =
3758            AllocationRef::new(String::from("pr-42-demo"), String::from("ephemeral-pools"));
3759        assert_eq!(from_str, from_string);
3760        // Mixed provenance is also load-bearing: the allocation_decide.rs
3761        // matched_pool seed pairs an owned `String` (from
3762        // `EphemeralPool::owned_name_or_empty()`) with a hand-authored
3763        // `.clone().unwrap_or_default()` — also `String`. The
3764        // controller_allocation.rs paths pair a moved `String` name
3765        // with a `.clone()`-ed `ns: String`. Verify (owned, borrow)
3766        // and (borrow, owned) both compose to the same shape as
3767        // (owned, owned) / (borrow, borrow).
3768        let mixed_a = AllocationRef::new(String::from("pr-42-demo"), "ephemeral-pools");
3769        let mixed_b = AllocationRef::new("pr-42-demo", String::from("ephemeral-pools"));
3770        assert_eq!(from_str, mixed_a);
3771        assert_eq!(from_str, mixed_b);
3772    }
3773
3774    #[test]
3775    fn allocation_ref_new_positional_axis_order_pinned_name_first_namespace_second() {
3776        // Axis-order pin: name is the FIRST positional argument;
3777        // namespace is the SECOND. Reversing the pair at the
3778        // constructor is the exact regression this pin fences — the
3779        // FOUR pre-lift sites all spelled `name` before `namespace`
3780        // (matching the struct definition's field order in
3781        // `pub struct AllocationRef { pub name, pub namespace }`)
3782        // and the wire-format serde output `{ "name": "...",
3783        // "namespace": "..." }` reflects that order. A slot swap at
3784        // the primitive would surface here rather than as silent
3785        // `<namespace>/<name>` inversion at every downstream
3786        // qualified-ref composer that reads `{ref.name}/{ref.namespace}`
3787        // as an audit-log key.
3788        let r = AllocationRef::new("alpha-name", "beta-namespace");
3789        assert_eq!(r.name, "alpha-name");
3790        assert_eq!(r.namespace, "beta-namespace");
3791        assert_ne!(r.name, "beta-namespace");
3792        assert_ne!(r.namespace, "alpha-name");
3793    }
3794
3795    #[test]
3796    fn allocation_ref_new_preserves_empty_string_verbatim() {
3797        // Empty-string sentinel pin: the constructor is pure — it does
3798        // NOT canonicalize empty inputs (does NOT default an empty
3799        // namespace to `"default"`; does NOT reject an empty name).
3800        // Preserves the pre-lift shape the allocation_decide.rs
3801        // matched_pool seed relied on: when the pool's metadata.namespace
3802        // is absent, `.clone().unwrap_or_default()` yields the empty
3803        // string, and the AllocationRef's namespace slot carries that
3804        // empty string verbatim to the downstream `bound_pool` sink.
3805        // A future canonicalization pass (e.g. defaulting to
3806        // `Process::DEFAULT_NAMESPACE`) MUST land here, not at the
3807        // primitive body silently, so the pre-lift consumers' empty-
3808        // sentinel semantics are the visible contract of the new
3809        // constructor.
3810        let r = AllocationRef::new("", "");
3811        assert_eq!(r.name, "");
3812        assert_eq!(r.namespace, "");
3813        let mixed = AllocationRef::new("pr-42-demo", "");
3814        assert_eq!(mixed.name, "pr-42-demo");
3815        assert_eq!(mixed.namespace, "");
3816    }
3817
3818    #[test]
3819    fn allocation_ref_new_composes_with_owned_name_or_empty_pool_projection() {
3820        // Composition pin: the constructor composes with the paired
3821        // substrate primitives [`EphemeralPool::owned_name_or_empty`]
3822        // + [`EphemeralPool::owned_namespace_or_empty`] at the
3823        // allocation_decide.rs pool_ref seed — the same primitive
3824        // family the pool CRD opened for both halves of the
3825        // `AllocationRef { name, namespace }` struct literal. The
3826        // composed pair carries an owned `String` name half (from
3827        // `pool.owned_name_or_empty()`) and an owned `String`
3828        // namespace half (from `pool.owned_namespace_or_empty()`) —
3829        // no pre-lift chain remains. A regression that broke the
3830        // primitive family's `impl Into<String>` acceptance of an
3831        // owned `String` return type would surface here rather than
3832        // as silent build failure at the pool-reconciler matched_pool
3833        // seed.
3834        let pool = pool_named("attest-pool");
3835        let r = AllocationRef::new(pool.owned_name_or_empty(), pool.owned_namespace_or_empty());
3836        assert_eq!(r.name, "attest-pool");
3837        assert_eq!(r.namespace, pool.owned_namespace_or_empty());
3838    }
3839
3840    #[test]
3841    fn allocation_ref_new_returns_wire_format_serialization_verbatim() {
3842        // Wire-format pin: the constructor produces an
3843        // [`AllocationRef`] whose serde `rename_all = "camelCase"`
3844        // serialization is byte-identical to the pre-lift struct
3845        // literal's serialization. The `bound_pool` and
3846        // `assignedProcess` slots on `AllocationStatus` (and the
3847        // `poolRef` slot on `AllocationSpec`) all round-trip through
3848        // this shape — the pin fences a regression that added a
3849        // private field or a `#[serde(skip)]` accidentally.
3850        let r = AllocationRef::new("pr-42-demo", "ephemeral-pools");
3851        let yaml = serde_yaml::to_string(&r).expect("AllocationRef serializes to yaml");
3852        assert!(yaml.contains("name: pr-42-demo"), "{yaml}");
3853        assert!(yaml.contains("namespace: ephemeral-pools"), "{yaml}");
3854        let back: AllocationRef =
3855            serde_yaml::from_str(&yaml).expect("AllocationRef round-trips through yaml");
3856        assert_eq!(back, r);
3857    }
3858
3859    fn member(state: MemberState) -> PoolMember {
3860        // 4-slot unallocated seed rides through the ONE substrate
3861        // owner `PoolMember::unallocated` (peer of the four workspace-
3862        // wide restatements of the SAME `PoolMember { process_name,
3863        // state, entered_state_at, allocation_ref: None }` fixture
3864        // literal that pre-lift lived at the production `controller_
3865        // pool::reconcile_inner` walk + the two `pool_decide::tests::
3866        // member` / `allocation_decide::tests::member` helpers + the
3867        // sibling `named_member` helper in this file).
3868        PoolMember::unallocated("m", state, crate::time::at_epoch_second(0))
3869    }
3870
3871    #[test]
3872    fn state_count_fanout_returns_all_zeros_on_empty_slice() {
3873        // Zero-length pin: the empty-members corner produces a
3874        // 4-tuple of zero counters, matching the pre-lift
3875        // `count_state` fanout's four `.iter().filter(...).count()`
3876        // calls each returning 0 on an empty iterator.
3877        assert_eq!(PoolMember::state_count_fanout(&[]), (0, 0, 0, 0));
3878    }
3879
3880    #[test]
3881    fn state_count_fanout_partitions_variants_into_correct_slots() {
3882        // Positional-axis pin: the returned 4-tuple's slot order
3883        // matches the four `PoolStatus` counter slots in declaration
3884        // order — `(ready, allocated, spawning, returning)`. A
3885        // regression that swapped two slots (e.g., `ready` ↔
3886        // `spawning`) surfaces here rather than as an operator-facing
3887        // scale-out oscillation at the pool reconciler.
3888        let members = vec![
3889            member(MemberState::Free),
3890            member(MemberState::Free),
3891            member(MemberState::Allocated),
3892            member(MemberState::Spawning),
3893            member(MemberState::Spawning),
3894            member(MemberState::Spawning),
3895            member(MemberState::Returning),
3896        ];
3897        assert_eq!(PoolMember::state_count_fanout(&members), (2, 1, 3, 1));
3898    }
3899
3900    #[test]
3901    fn state_count_fanout_excludes_failed_from_every_counter() {
3902        // Closed-set pin: no `PoolStatus` slot counts `Failed` members
3903        // (they surface via `PoolPhase::Degraded` instead of a status
3904        // counter). This test fences a regression that let a `Failed`
3905        // member drift into one of the four counters and inflate the
3906        // operator-visible ready/allocated/spawning/returning fanout.
3907        let members = vec![
3908            member(MemberState::Failed),
3909            member(MemberState::Failed),
3910            member(MemberState::Failed),
3911        ];
3912        assert_eq!(PoolMember::state_count_fanout(&members), (0, 0, 0, 0));
3913
3914        // Mixed with a Free member: the Free member is counted, the
3915        // Failed members are not.
3916        let mixed = vec![
3917            member(MemberState::Free),
3918            member(MemberState::Failed),
3919            member(MemberState::Failed),
3920        ];
3921        assert_eq!(PoolMember::state_count_fanout(&mixed), (1, 0, 0, 0));
3922    }
3923
3924    #[test]
3925    fn state_count_fanout_matches_pre_lift_count_state_helper_verbatim() {
3926        // Parity pin: for every possible members list, the 4-tuple
3927        // returned by the substrate primitive matches the pre-lift
3928        // `count_state(&members, MemberState::<slot>)` fanout that
3929        // pool-reconciler restated at both status-patch sites. The
3930        // pre-lift helper was
3931        // ```rust,ignore
3932        // fn count_state(members: &[PoolMember], target: MemberState) -> u32 {
3933        //     members.iter().filter(|m| m.state == target).count() as u32
3934        // }
3935        // ```
3936        // — re-implemented inline here as an oracle.
3937        fn count_state(members: &[PoolMember], target: MemberState) -> u32 {
3938            members.iter().filter(|m| m.state == target).count() as u32
3939        }
3940        let members = vec![
3941            member(MemberState::Free),
3942            member(MemberState::Allocated),
3943            member(MemberState::Allocated),
3944            member(MemberState::Spawning),
3945            member(MemberState::Returning),
3946            member(MemberState::Returning),
3947            member(MemberState::Failed),
3948        ];
3949        let (ready, allocated, spawning, returning) = PoolMember::state_count_fanout(&members);
3950        assert_eq!(ready, count_state(&members, MemberState::Free));
3951        assert_eq!(allocated, count_state(&members, MemberState::Allocated));
3952        assert_eq!(spawning, count_state(&members, MemberState::Spawning));
3953        assert_eq!(returning, count_state(&members, MemberState::Returning));
3954    }
3955
3956    // ─── PoolMember::process_names_set substrate pins ─────────────────
3957    //
3958    // Pins the closed-set slice-owned collection primitive on the
3959    // `process_name` axis into a `HashSet<String>` — the O(1)-lookup
3960    // shape both spawn arms in
3961    // `tatara-pool-reconciler::controller_pool` build pre-collision-
3962    // check against a candidate `member_process_name(&pool_name,
3963    // &pool_uid, slot)`. Sibling to `state_count_fanout` on the
3964    // `(collection shape × slice-owned fold)` axis; the fanout owns
3965    // the state-counter tuple corner, this primitive owns the
3966    // process-name-lookup corner. Fail-before-pass-after granularity:
3967    // `process_names_set` did not exist pre-lift; the compiler cannot
3968    // resolve the name until the impl block above is in place, so a
3969    // rollback of the primitive breaks this whole test group.
3970
3971    fn named_member(process_name: &str, state: MemberState) -> PoolMember {
3972        // 4-slot unallocated seed rides through the ONE substrate
3973        // owner `PoolMember::unallocated` — sibling to the `member`
3974        // helper in this file on the same epoch-anchored axis.
3975        PoolMember::unallocated(process_name, state, crate::time::at_epoch_second(0))
3976    }
3977
3978    #[test]
3979    fn process_names_set_returns_empty_hashset_on_empty_slice() {
3980        // Zero-length pin: the empty-members corner produces an
3981        // empty `HashSet<String>`, matching the pre-lift
3982        // `.iter().map(...).collect()` chain's empty-iterator
3983        // behavior. A regression that started producing a sentinel
3984        // entry (a `""` placeholder, a static seed) on the empty-
3985        // slice corner would silently reject the first spawn slot
3986        // downstream — the pin closes that failure mode.
3987        let empty: Vec<PoolMember> = vec![];
3988        assert!(PoolMember::process_names_set(&empty).is_empty());
3989    }
3990
3991    #[test]
3992    fn process_names_set_collects_every_process_name_from_populated_slice() {
3993        // Positive pin: every `PoolMember`'s `process_name` slot
3994        // lands in the returned `HashSet<String>` verbatim. Cross-
3995        // state (Free / Allocated / Spawning / Returning / Failed)
3996        // to prove the primitive is state-agnostic — the spawn arms
3997        // check occupancy on the name axis, NOT the state axis, so a
3998        // future refactor that filtered by state would silently
3999        // leave a returned/failed slot open to a duplicate spawn.
4000        let members = vec![
4001            named_member("pool-a-0", MemberState::Free),
4002            named_member("pool-a-1", MemberState::Allocated),
4003            named_member("pool-a-2", MemberState::Spawning),
4004            named_member("pool-a-3", MemberState::Returning),
4005            named_member("pool-a-4", MemberState::Failed),
4006        ];
4007        let set = PoolMember::process_names_set(&members);
4008        assert_eq!(set.len(), 5);
4009        for slot in 0..5 {
4010            let want = format!("pool-a-{slot}");
4011            assert!(set.contains(&want), "missing {want}; set = {set:?}");
4012        }
4013    }
4014
4015    #[test]
4016    fn process_names_set_deduplicates_duplicate_process_names() {
4017        // Deduplication pin: two `PoolMember` entries with the same
4018        // `process_name` (a race between the two spawn arms, an
4019        // adopted foreign Process the reconciler picked up twice)
4020        // collapse to ONE entry in the `HashSet<String>`. Pins the
4021        // `HashSet` deduplication semantics the pre-lift `.iter()
4022        // .map(...).collect()` chain already inherited from the
4023        // `FromIterator` impl — a regression that swapped the
4024        // aggregate to a `Vec<String>` or `BTreeSet<String>` still
4025        // matches the shape but changes the operator-visible count
4026        // at the `.len()` probe here.
4027        let members = vec![
4028            named_member("pool-b-0", MemberState::Free),
4029            named_member("pool-b-0", MemberState::Spawning),
4030            named_member("pool-b-1", MemberState::Free),
4031        ];
4032        let set = PoolMember::process_names_set(&members);
4033        assert_eq!(set.len(), 2);
4034        assert!(set.contains("pool-b-0"));
4035        assert!(set.contains("pool-b-1"));
4036    }
4037
4038    #[test]
4039    fn process_names_set_membership_probe_matches_pre_lift_chain_verbatim() {
4040        // Byte-identical parity pin: the `.contains(&candidate)`
4041        // probe on the substrate's `HashSet<String>` return returns
4042        // the same `bool` as the pre-lift `members.iter().map(|m|
4043        // m.process_name.clone()).collect::<HashSet<_>>().contains
4044        // (&candidate)` chain across the FULL cross product of
4045        // (candidate ∈ {an existing name, a novel name, the empty
4046        // string}). A regression that inserted a normalization step
4047        // at the primitive the pre-lift chain does NOT apply — or
4048        // vice versa — surfaces here rather than as silent drift
4049        // between the two spawn arms the primitive owns.
4050        let members = vec![
4051            named_member("pool-c-0", MemberState::Free),
4052            named_member("pool-c-1", MemberState::Allocated),
4053        ];
4054        let candidates: [&str; 4] = ["pool-c-0", "pool-c-1", "pool-c-2", ""];
4055        let via_primitive = PoolMember::process_names_set(&members);
4056        for candidate in candidates {
4057            let pre_lift: std::collections::HashSet<String> =
4058                members.iter().map(|m| m.process_name.clone()).collect();
4059            assert_eq!(
4060                via_primitive.contains(candidate),
4061                pre_lift.contains(candidate),
4062                "candidate = {candidate:?}"
4063            );
4064        }
4065    }
4066
4067    #[test]
4068    fn process_names_set_is_a_pure_projection() {
4069        // Consecutive calls on the same slice return equal sets —
4070        // no cached state, no mutation on the input. Guards against
4071        // a future refactor that plants a cache field somewhere and
4072        // drifts one caller from another silently.
4073        let members = vec![
4074            named_member("pool-d-0", MemberState::Free),
4075            named_member("pool-d-1", MemberState::Spawning),
4076        ];
4077        let first = PoolMember::process_names_set(&members);
4078        let second = PoolMember::process_names_set(&members);
4079        assert_eq!(first, second);
4080    }
4081
4082    #[test]
4083    fn pool_status_observed_composes_pre_lift_status_seed_verbatim() {
4084        // Composition pin: the substrate constructor produces a
4085        // `PoolStatus` structurally equal to the pre-lift 11-line
4086        // struct literal both pool-reconciler status-patch sites
4087        // stamped by hand. Any drift in the defaults (`message`,
4088        // `conditions`) or in the counter fanout surfaces here.
4089        let now = crate::time::at_epoch_second(1_700_000_000);
4090        let members = vec![
4091            member(MemberState::Free),
4092            member(MemberState::Allocated),
4093            member(MemberState::Spawning),
4094            member(MemberState::Returning),
4095            member(MemberState::Failed),
4096        ];
4097        let member_count = members.len();
4098        let observed = PoolStatus::observed(PoolPhase::Steady, members, now);
4099        assert_eq!(observed.phase, PoolPhase::Steady);
4100        assert_eq!(observed.phase_since, Some(now));
4101        assert_eq!(observed.ready_count, 1);
4102        assert_eq!(observed.allocated_count, 1);
4103        assert_eq!(observed.spawning_count, 1);
4104        assert_eq!(observed.returning_count, 1);
4105        assert_eq!(observed.members.len(), member_count);
4106        assert!(observed.message.is_none());
4107        assert!(observed.conditions.is_empty());
4108    }
4109
4110    #[test]
4111    fn pool_status_observed_moves_members_by_value_without_extra_clone() {
4112        // Ownership pin: the constructor consumes the members Vec by
4113        // value rather than borrowing + cloning internally. Both pre-
4114        // lift sites called `.clone()` on their `members` binding for
4115        // the struct-literal `members:` slot; the substrate lift keeps
4116        // the same one-clone bound at the caller (or a straight move
4117        // if the caller no longer needs the local `members` binding
4118        // after the seed) rather than accidentally cloning twice.
4119        let members = vec![member(MemberState::Free), member(MemberState::Spawning)];
4120        let now = crate::time::at_epoch_second(0);
4121        let observed = PoolStatus::observed(PoolPhase::Steady, members, now);
4122        assert_eq!(observed.members.len(), 2);
4123    }
4124
4125    // ─── PoolStatus::observed_now substrate pins ─────────────────────
4126    //
4127    // Bind [`PoolStatus::observed_now`] at fail-before-pass-after
4128    // granularity so a regression that dropped the wall-clock read
4129    // (yielding a `phase_since` of `Some(DateTime::default())`),
4130    // reshaped the delegation target (a peer 4-arg composer that
4131    // stamped different defaults), or diverged the peer from the 3-arg
4132    // [`PoolStatus::observed`] on any observable slot surfaces HERE
4133    // rather than as silent operator-facing drift at the two
4134    // controller_pool status-patch sites.
4135    //
4136    // Each pin is fail-before-pass-after: the primitive did not exist
4137    // pre-lift, so any test that invokes it fails to compile pre-lift
4138    // and passes post-lift; the byte-identity pins below then bind the
4139    // specific shape choice.
4140
4141    #[test]
4142    fn pool_status_observed_now_composes_through_observed_with_wall_clock() {
4143        // Composition pin: `observed_now` MUST agree with the 3-arg
4144        // `observed(phase, members, Utc::now())` peer at every slot
4145        // other than `phase_since` (which reads the wall clock at
4146        // different instants and diverges by scheduler jitter). A
4147        // regression that specialized either composer (a stray
4148        // canonicalization at `observed_now`, a swapped default at the
4149        // 3-arg peer) would surface HERE rather than as silent skew at
4150        // the two controller_pool sites the primitive owns.
4151        let members = vec![
4152            member(MemberState::Free),
4153            member(MemberState::Allocated),
4154            member(MemberState::Spawning),
4155            member(MemberState::Returning),
4156        ];
4157        let via_now = PoolStatus::observed_now(PoolPhase::Steady, members.clone());
4158        let via_injected =
4159            PoolStatus::observed(PoolPhase::Steady, members.clone(), chrono::Utc::now());
4160        assert_eq!(via_now.phase, via_injected.phase);
4161        assert_eq!(via_now.ready_count, via_injected.ready_count);
4162        assert_eq!(via_now.allocated_count, via_injected.allocated_count);
4163        assert_eq!(via_now.spawning_count, via_injected.spawning_count);
4164        assert_eq!(via_now.returning_count, via_injected.returning_count);
4165        assert_eq!(via_now.members.len(), via_injected.members.len());
4166        assert_eq!(via_now.message, via_injected.message);
4167        assert_eq!(via_now.conditions.len(), via_injected.conditions.len());
4168    }
4169
4170    #[test]
4171    fn pool_status_observed_now_reads_wall_clock_into_phase_since() {
4172        // Wall-clock pin: `phase_since` MUST fall between `Utc::now()`
4173        // reads bracketed around the call. A regression that dropped
4174        // the wall-clock read to a module-load constant (`Utc::now()`
4175        // captured at `static` init), a `DateTime::default()` (epoch),
4176        // or a stale `None` would fail this bracket check.
4177        let before = chrono::Utc::now();
4178        let observed = PoolStatus::observed_now(PoolPhase::Steady, vec![]);
4179        let after = chrono::Utc::now();
4180        let phase_since = observed
4181            .phase_since
4182            .expect("observed_now must stamp phase_since with the wall clock");
4183        assert!(
4184            phase_since >= before && phase_since <= after,
4185            "phase_since {phase_since} must fall in [{before}, {after}]"
4186        );
4187    }
4188
4189    #[test]
4190    fn pool_status_observed_now_stamps_the_same_defaults_as_the_injected_peer() {
4191        // Defaults pin: `message: None` + `conditions: vec![]` MUST
4192        // agree with the 3-arg [`PoolStatus::observed`] peer verbatim.
4193        // A regression that stamped a per-caller message default at
4194        // `observed_now` (a "wall-clock-stamped observation" prefix,
4195        // say) or seeded a "just-observed" Condition row would surface
4196        // HERE rather than as silent operator-facing drift at either
4197        // status-patch site.
4198        let observed = PoolStatus::observed_now(PoolPhase::Steady, vec![]);
4199        assert!(observed.message.is_none());
4200        assert!(observed.conditions.is_empty());
4201    }
4202
4203    #[test]
4204    fn pool_status_observed_now_wall_clock_is_read_per_invocation_not_cached() {
4205        // Monotonic-read pin: two back-to-back `observed_now` calls
4206        // MUST read `Utc::now()` twice — the second `phase_since` MUST
4207        // be `>=` the first. A regression that cached a wall-clock read
4208        // into a `OnceLock` / lazy `static` would fire the SAME
4209        // `phase_since` for every caller on the reconciler's process
4210        // and every status-patch would carry the module-load instant
4211        // rather than the tick instant. Both instants may coincide on
4212        // a fast machine; use `>=` (not `>`) to keep the pin robust
4213        // against subsecond scheduler granularity while still catching
4214        // a cached-constant regression (where the second read would
4215        // be < the wall clock).
4216        let first = PoolStatus::observed_now(PoolPhase::Steady, vec![])
4217            .phase_since
4218            .expect("first observed_now stamps phase_since");
4219        let second = PoolStatus::observed_now(PoolPhase::Steady, vec![])
4220            .phase_since
4221            .expect("second observed_now stamps phase_since");
4222        assert!(
4223            second >= first,
4224            "second phase_since {second} must be >= first phase_since {first}"
4225        );
4226        // AND the second read MUST NOT precede the wall clock reads
4227        // bracketing the call — a cached-past constant would fail
4228        // this bound.
4229        let after = chrono::Utc::now();
4230        assert!(
4231            second <= after,
4232            "second phase_since {second} must be <= {after}"
4233        );
4234    }
4235
4236    #[test]
4237    fn pool_status_observed_now_matches_pre_lift_utc_now_composition_shape() {
4238        // Byte-identical parity with the pre-lift
4239        // `PoolStatus::observed(phase, members.clone(), Utc::now())`
4240        // block both hand-authored callsites restated at their status-
4241        // patch sites, swept across representative pool-phase variants.
4242        // Both blocks read the wall clock at DIFFERENT instants so the
4243        // two anchors CAN differ by the wall-clock delta between calls
4244        // — bound the divergence at 100ms scheduler jitter, matching
4245        // the peer `seconds_ago_matches_hand_authored_pre_lift_chain_shape`
4246        // pin's tolerance on the sibling `crate::time` module.
4247        let members = vec![member(MemberState::Free), member(MemberState::Spawning)];
4248        for phase in [PoolPhase::Steady, PoolPhase::ScalingUp, PoolPhase::Degraded] {
4249            let composed = PoolStatus::observed_now(phase, members.clone())
4250                .phase_since
4251                .expect("observed_now stamps phase_since");
4252            let hand_authored = PoolStatus::observed(phase, members.clone(), chrono::Utc::now())
4253                .phase_since
4254                .expect("observed stamps phase_since");
4255            let delta = (hand_authored - composed).abs();
4256            assert!(
4257                delta <= chrono::Duration::milliseconds(100),
4258                "composed {composed} and hand-authored {hand_authored} must agree within 100ms scheduler jitter for phase={phase:?}"
4259            );
4260        }
4261    }
4262
4263    // ─── EphemeralPool::observed_phase_from substrate pins ───────────
4264    //
4265    // Pins the pure typed projection at fail-before-pass-after
4266    // granularity: `observed_phase_from` did not exist on
4267    // `EphemeralPool` pre-lift — the gate ladder lived at
4268    // `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
4269    // as a repo-internal free function. Any test that invokes
4270    // `pool.observed_phase_from(&members)` fails to compile pre-lift
4271    // and passes post-lift; the truth-table pins below then bind the
4272    // ladder's five gate corners individually so a regression that
4273    // reordered any two gates, dropped an arm, or drifted a threshold
4274    // surfaces per-corner rather than as silent operator-facing skew
4275    // at the two `controller_pool` status-patch callsites the
4276    // downstream compound composer `PoolStatus::observed_from`
4277    // delegates through.
4278
4279    fn pool_with_desired_and_min(desired: u32, min: u32) -> EphemeralPool {
4280        let spec = PoolSpec {
4281            desired_size: desired,
4282            min_size: min,
4283            ..PoolSpec::with_template(empty_template())
4284        };
4285        EphemeralPool::new("attest-pool", spec)
4286    }
4287
4288    #[test]
4289    fn observed_phase_from_returns_draining_on_tombstoned_pool_regardless_of_supply() {
4290        // Tombstone-first gate pin: a tombstoned pool MUST return
4291        // `Draining` regardless of `spec.min_size`, `spec.desired_size`,
4292        // and the member supply. A regression that let the supply
4293        // arithmetic pre-empt the tombstone probe would silently
4294        // classify a draining pool as `Steady` / `ScalingUp` /
4295        // `Degraded` and hide the deletion-in-flight state from
4296        // operators reading `kubectl get ephemeralpools`.
4297        let p = tombstoned_pool();
4298        // Every supply corner must yield the same `Draining` answer.
4299        for members in [
4300            vec![],
4301            vec![member(MemberState::Free)],
4302            vec![
4303                member(MemberState::Free),
4304                member(MemberState::Spawning),
4305                member(MemberState::Allocated),
4306            ],
4307            vec![member(MemberState::Failed)],
4308        ] {
4309            assert_eq!(
4310                p.observed_phase_from(&members),
4311                PoolPhase::Draining,
4312                "tombstoned pool must project Draining regardless of members={members:?}",
4313            );
4314        }
4315    }
4316
4317    #[test]
4318    fn observed_phase_from_returns_initializing_on_empty_members() {
4319        // Empty-members gate pin: an untombstoned pool with zero
4320        // members MUST return `Initializing`, regardless of
4321        // `spec.desired_size` and `spec.min_size`. A regression that
4322        // let the min-floor gate fire on empty members (supply = 0 <
4323        // min_size) would misreport the fresh-pool state as
4324        // `Degraded` and trip any downstream health aggregator that
4325        // treats `Degraded` as an alertable state.
4326        for (desired, min) in [(0, 0), (1, 0), (3, 1), (5, 3)] {
4327            let p = pool_with_desired_and_min(desired, min);
4328            assert_eq!(
4329                p.observed_phase_from(&[]),
4330                PoolPhase::Initializing,
4331                "empty members must project Initializing for (desired={desired}, min={min})",
4332            );
4333        }
4334    }
4335
4336    #[test]
4337    fn observed_phase_from_returns_degraded_when_supply_below_min_size() {
4338        // Min-floor gate pin: `min_size > 0 && supply < min_size` MUST
4339        // fire `Degraded` before the supply-vs-desired gate gets a
4340        // chance to pick `ScalingUp` / `ScalingDown` / `Steady`. Note
4341        // the guard `min_size > 0` — a pool with `min_size = 0` never
4342        // trips this gate even at zero supply. Sweep the (min, supply)
4343        // corners that plausibly reach the reconciler at tick time.
4344        let p = pool_with_desired_and_min(5, 2);
4345        // supply = 0 → 1 Allocated (does not count) + 0 Free/Spawning
4346        let members = vec![member(MemberState::Allocated)];
4347        assert_eq!(p.observed_phase_from(&members), PoolPhase::Degraded);
4348        // supply = 1 (< min_size = 2) → still Degraded even though
4349        // supply < desired (5) would otherwise pick ScalingUp.
4350        let members = vec![
4351            member(MemberState::Free),
4352            member(MemberState::Allocated),
4353            member(MemberState::Allocated),
4354        ];
4355        assert_eq!(p.observed_phase_from(&members), PoolPhase::Degraded);
4356    }
4357
4358    #[test]
4359    fn observed_phase_from_returns_scaling_up_when_supply_below_desired() {
4360        // Supply-vs-desired gate pin (up arm): `supply < desired_size`
4361        // and no min-floor breach → `ScalingUp`. The reconciler's
4362        // convergence loop is expected to spawn additional members to
4363        // close the gap.
4364        let p = pool_with_desired_and_min(3, 0);
4365        let members = vec![
4366            member(MemberState::Free),
4367            member(MemberState::Spawning),
4368            member(MemberState::Allocated),
4369        ];
4370        assert_eq!(p.observed_phase_from(&members), PoolPhase::ScalingUp);
4371    }
4372
4373    #[test]
4374    fn observed_phase_from_returns_scaling_down_when_supply_above_desired() {
4375        // Supply-vs-desired gate pin (down arm): `supply > desired_size`
4376        // → `ScalingDown`. The reconciler's convergence loop is
4377        // expected to reap excess Free members.
4378        let p = pool_with_desired_and_min(1, 0);
4379        let members = vec![
4380            member(MemberState::Free),
4381            member(MemberState::Free),
4382            member(MemberState::Spawning),
4383        ];
4384        assert_eq!(p.observed_phase_from(&members), PoolPhase::ScalingDown);
4385    }
4386
4387    #[test]
4388    fn observed_phase_from_returns_steady_when_supply_equals_desired() {
4389        // Terminal-arm pin: `supply == desired_size` with no tombstone,
4390        // no floor breach → `Steady`. This is the goal state the
4391        // reconciler drives the pool toward.
4392        let p = pool_with_desired_and_min(2, 0);
4393        let members = vec![
4394            member(MemberState::Free),
4395            member(MemberState::Spawning),
4396            member(MemberState::Allocated),
4397        ];
4398        // Free + Spawning count toward supply (Allocated does not) → 2.
4399        assert_eq!(p.observed_phase_from(&members), PoolPhase::Steady);
4400    }
4401
4402    #[test]
4403    fn observed_phase_from_excludes_failed_members_from_supply() {
4404        // Closed-set contract pin: `Failed` members MUST NOT count
4405        // toward supply (peer of the
4406        // `member_state_failed_implies_no_supply` contract on
4407        // `MemberState`). A regression that let `Failed` inflate the
4408        // supply count would silently satisfy the `supply >= min_size`
4409        // gate on a pool that's actually below floor and misclassify
4410        // the state as `Steady` / `ScalingUp` instead of `Degraded`.
4411        let p = pool_with_desired_and_min(2, 1);
4412        let members = vec![
4413            member(MemberState::Failed),
4414            member(MemberState::Failed),
4415            member(MemberState::Failed),
4416        ];
4417        // supply = 0 (no Free/Spawning) < min_size = 1 → Degraded.
4418        assert_eq!(p.observed_phase_from(&members), PoolPhase::Degraded);
4419    }
4420
4421    #[test]
4422    fn observed_phase_from_matches_pre_lift_reconciler_chain() {
4423        // Byte-identical parity with the pre-lift
4424        // `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
4425        // free function that the primitive absorbs. Runs across the
4426        // FULL corner set of the gate ladder (tombstone, empty, floor
4427        // breach, scaling up, scaling down, steady) so a regression
4428        // that drifted the gate ladder at the substrate surfaces here
4429        // per-corner rather than as silent operator-facing skew at the
4430        // two `controller_pool` status-patch callsites.
4431        fn pre_lift(pool: &EphemeralPool, members: &[PoolMember]) -> PoolPhase {
4432            if pool.is_being_deleted() {
4433                return PoolPhase::Draining;
4434            }
4435            let supply = members
4436                .iter()
4437                .filter(|m| m.state.counts_toward_supply())
4438                .count() as u32;
4439            let want = pool.spec.desired_size;
4440            if members.is_empty() {
4441                return PoolPhase::Initializing;
4442            }
4443            if pool.spec.min_size > 0 && supply < pool.spec.min_size {
4444                return PoolPhase::Degraded;
4445            }
4446            if supply < want {
4447                return PoolPhase::ScalingUp;
4448            }
4449            if supply > want {
4450                return PoolPhase::ScalingDown;
4451            }
4452            PoolPhase::Steady
4453        }
4454        let cases: [(EphemeralPool, Vec<PoolMember>); 6] = [
4455            (tombstoned_pool(), vec![member(MemberState::Free)]),
4456            (pool_with_desired_and_min(3, 0), vec![]),
4457            (
4458                pool_with_desired_and_min(5, 2),
4459                vec![member(MemberState::Allocated)],
4460            ),
4461            (
4462                pool_with_desired_and_min(3, 0),
4463                vec![member(MemberState::Free), member(MemberState::Spawning)],
4464            ),
4465            (
4466                pool_with_desired_and_min(1, 0),
4467                vec![
4468                    member(MemberState::Free),
4469                    member(MemberState::Free),
4470                    member(MemberState::Spawning),
4471                ],
4472            ),
4473            (
4474                pool_with_desired_and_min(2, 0),
4475                vec![
4476                    member(MemberState::Free),
4477                    member(MemberState::Spawning),
4478                    member(MemberState::Allocated),
4479                ],
4480            ),
4481        ];
4482        for (pool, members) in &cases {
4483            assert_eq!(
4484                pool.observed_phase_from(members),
4485                pre_lift(pool, members),
4486                "substrate primitive must match pre-lift reconciler chain for pool={:?} members={members:?}",
4487                pool.metadata.name,
4488            );
4489        }
4490    }
4491
4492    #[test]
4493    fn observed_phase_from_is_a_pure_projection() {
4494        // Purity pin: two consecutive calls on the same `(pool,
4495        // members)` return the same `PoolPhase` — no hidden state,
4496        // no per-tick clock read. Guards against a future refactor
4497        // that reaches for `Utc::now()` at the tombstone probe or
4498        // caches per-instance state.
4499        let p = pool_with_desired_and_min(3, 0);
4500        let members = vec![
4501            member(MemberState::Free),
4502            member(MemberState::Spawning),
4503            member(MemberState::Allocated),
4504        ];
4505        let a = p.observed_phase_from(&members);
4506        let b = p.observed_phase_from(&members);
4507        assert_eq!(a, b);
4508        assert_eq!(a, PoolPhase::ScalingUp);
4509    }
4510
4511    // ─── PoolStatus::observed_from substrate pins ────────────────────
4512    //
4513    // Pins the compound composer at fail-before-pass-after granularity:
4514    // `observed_from` did not exist pre-lift; the compiler cannot
4515    // resolve the name until the impl block above is in place, so a
4516    // rollback of the primitive breaks this whole test group. The
4517    // composer owns the 2-link `let phase = pool_phase_from_members
4518    // (&pool, &members); PoolStatus::observed_now(phase,
4519    // members.clone())` chain that both `controller_pool` status-
4520    // patch sites walked pre-lift; a regression that specialized
4521    // either component (a stray canonicalization at `observed_from`,
4522    // a swapped default at either component, a members-`Vec` clone
4523    // slipped into the composer boundary) would surface HERE rather
4524    // than as silent skew at either callsite.
4525
4526    #[test]
4527    fn pool_status_observed_from_delegates_through_phase_projection_and_observed_now() {
4528        // Delegation-shape pin: `observed_from(pool, members)` MUST
4529        // produce the SAME `PoolStatus` as the explicit 2-link
4530        // `PoolStatus::observed_now(pool.observed_phase_from(&members),
4531        // members)` chain at every slot other than `phase_since`
4532        // (which reads the wall clock at different instants and
4533        // diverges by scheduler jitter). Sweep the gate corners so a
4534        // regression at any phase arm surfaces here rather than as
4535        // silent skew at either controller_pool callsite.
4536        let cases: [(EphemeralPool, Vec<PoolMember>, PoolPhase); 4] = [
4537            (
4538                pool_with_desired_and_min(3, 0),
4539                vec![
4540                    member(MemberState::Free),
4541                    member(MemberState::Spawning),
4542                    member(MemberState::Allocated),
4543                ],
4544                PoolPhase::ScalingUp,
4545            ),
4546            (
4547                pool_with_desired_and_min(2, 0),
4548                vec![
4549                    member(MemberState::Free),
4550                    member(MemberState::Spawning),
4551                    member(MemberState::Allocated),
4552                ],
4553                PoolPhase::Steady,
4554            ),
4555            (
4556                pool_with_desired_and_min(1, 0),
4557                vec![
4558                    member(MemberState::Free),
4559                    member(MemberState::Free),
4560                    member(MemberState::Spawning),
4561                ],
4562                PoolPhase::ScalingDown,
4563            ),
4564            (tombstoned_pool(), vec![], PoolPhase::Draining),
4565        ];
4566        for (pool, members, expected_phase) in cases {
4567            let via_compound = PoolStatus::observed_from(&pool, members.clone());
4568            let via_manual =
4569                PoolStatus::observed_now(pool.observed_phase_from(&members), members.clone());
4570            assert_eq!(via_compound.phase, expected_phase);
4571            assert_eq!(via_compound.phase, via_manual.phase);
4572            assert_eq!(via_compound.ready_count, via_manual.ready_count);
4573            assert_eq!(via_compound.allocated_count, via_manual.allocated_count);
4574            assert_eq!(via_compound.spawning_count, via_manual.spawning_count);
4575            assert_eq!(via_compound.returning_count, via_manual.returning_count);
4576            assert_eq!(via_compound.members.len(), via_manual.members.len());
4577            assert_eq!(via_compound.message, via_manual.message);
4578            assert_eq!(via_compound.conditions.len(), via_manual.conditions.len(),);
4579        }
4580    }
4581
4582    #[test]
4583    fn pool_status_observed_from_reads_wall_clock_into_phase_since() {
4584        // Wall-clock pin (inherited from `observed_now`): `phase_since`
4585        // MUST fall between `Utc::now()` reads bracketed around the
4586        // call. A regression that specialized the compound composer
4587        // with a stale timestamp constant would fail this bracket.
4588        let p = pool_with_desired_and_min(1, 0);
4589        let members = vec![member(MemberState::Free)];
4590        let before = chrono::Utc::now();
4591        let observed = PoolStatus::observed_from(&p, members);
4592        let after = chrono::Utc::now();
4593        let phase_since = observed
4594            .phase_since
4595            .expect("observed_from must stamp phase_since with the wall clock");
4596        assert!(
4597            phase_since >= before && phase_since <= after,
4598            "phase_since {phase_since} must fall in [{before}, {after}]"
4599        );
4600    }
4601
4602    #[test]
4603    fn pool_status_observed_from_derives_phase_from_pool_and_members() {
4604        // Phase-derivation pin: the compound composer MUST derive the
4605        // phase from the (pool, members) observations via
4606        // `EphemeralPool::observed_phase_from`, not via a caller-
4607        // supplied phase argument. A regression that reached for a
4608        // hard-coded default phase (`Steady` / `Initializing`) would
4609        // misreport the observed state at both controller_pool
4610        // callsites. Pinned across the five non-tombstone gate arms
4611        // so a per-arm regression surfaces per-corner.
4612        let cases: [(EphemeralPool, Vec<PoolMember>, PoolPhase); 5] = [
4613            (
4614                pool_with_desired_and_min(1, 0),
4615                vec![],
4616                PoolPhase::Initializing,
4617            ),
4618            (
4619                pool_with_desired_and_min(5, 2),
4620                vec![member(MemberState::Allocated)],
4621                PoolPhase::Degraded,
4622            ),
4623            (
4624                pool_with_desired_and_min(3, 0),
4625                vec![member(MemberState::Free), member(MemberState::Spawning)],
4626                PoolPhase::ScalingUp,
4627            ),
4628            (
4629                pool_with_desired_and_min(1, 0),
4630                vec![
4631                    member(MemberState::Free),
4632                    member(MemberState::Free),
4633                    member(MemberState::Spawning),
4634                ],
4635                PoolPhase::ScalingDown,
4636            ),
4637            (
4638                pool_with_desired_and_min(2, 0),
4639                vec![
4640                    member(MemberState::Free),
4641                    member(MemberState::Spawning),
4642                    member(MemberState::Allocated),
4643                ],
4644                PoolPhase::Steady,
4645            ),
4646        ];
4647        for (pool, members, expected) in cases {
4648            let observed = PoolStatus::observed_from(&pool, members);
4649            assert_eq!(
4650                observed.phase, expected,
4651                "observed_from must derive phase={expected:?} from pool + members",
4652            );
4653        }
4654    }
4655
4656    #[test]
4657    fn pool_status_observed_from_matches_pre_lift_two_link_chain_shape() {
4658        // Byte-identical parity with the pre-lift 2-link `let phase =
4659        // pool_phase_from_members(&pool, &members);
4660        // PoolStatus::observed_now(phase, members.clone())` chain both
4661        // hand-authored callsites walked. Both blocks read the wall
4662        // clock at DIFFERENT instants so the two `phase_since` stamps
4663        // CAN differ by scheduler jitter — bound the divergence at
4664        // 100ms scheduler jitter, matching the peer
4665        // `pool_status_observed_now_matches_pre_lift_utc_now_composition_shape`
4666        // tolerance on the sibling composer.
4667        let p = pool_with_desired_and_min(3, 0);
4668        let members = vec![
4669            member(MemberState::Free),
4670            member(MemberState::Spawning),
4671            member(MemberState::Allocated),
4672        ];
4673        let composed = PoolStatus::observed_from(&p, members.clone())
4674            .phase_since
4675            .expect("observed_from stamps phase_since");
4676        // Pre-lift block: compute phase separately, then hand it to
4677        // observed_now — exactly the shape the two callsites walked.
4678        let hand_authored = {
4679            let phase = p.observed_phase_from(&members);
4680            PoolStatus::observed_now(phase, members.clone())
4681        }
4682        .phase_since
4683        .expect("observed_now stamps phase_since");
4684        let delta = (hand_authored - composed).abs();
4685        assert!(
4686            delta <= chrono::Duration::milliseconds(100),
4687            "composed {composed} and hand-authored {hand_authored} must agree within 100ms scheduler jitter",
4688        );
4689    }
4690
4691    // ─── PoolMember::unallocated substrate pins ───────────────────────
4692    //
4693    // Pins the 4-slot `{ process_name, state, entered_state_at,
4694    // allocation_ref: None }` composer's fill at fail-before-pass-after
4695    // granularity: `unallocated` did not exist pre-lift; the compiler
4696    // cannot resolve the name until the impl block above is in place,
4697    // so a rollback of the primitive breaks this whole test group. The
4698    // primitive owns FIVE workspace-wide seed sites (one production
4699    // walk in `tatara-pool-reconciler::controller_pool::reconcile_inner`
4700    // and four test helpers across `pool.rs`, `pool_decide.rs`, and
4701    // `allocation_decide.rs`) so a regression that drifts any of the
4702    // four slots (a mistyped `allocation_ref: Some(<sentinel>)`, a
4703    // reversed positional order at the composer entry, an accidental
4704    // canonicalization of the `entered_state_at` anchor) surfaces here
4705    // rather than as silent operator-facing skew between the production
4706    // seed and the three test-suite helpers on the SAME `PoolMember`
4707    // shape.
4708
4709    #[test]
4710    fn pool_member_unallocated_fills_every_slot_verbatim() {
4711        // Positional-axis pin: the composer's four inputs land at the
4712        // four struct slots in declaration order. A regression that
4713        // swapped `process_name` and `entered_state_at` at the composer
4714        // entry (or that renamed the `allocation_ref` invariant slot to
4715        // a different `None`-preserving field) surfaces here.
4716        let anchor = crate::time::at_epoch_second(1_700_000_000);
4717        let m = PoolMember::unallocated("pool-x-0", MemberState::Free, anchor);
4718        assert_eq!(m.process_name, "pool-x-0");
4719        assert_eq!(m.state, MemberState::Free);
4720        assert_eq!(m.entered_state_at, anchor);
4721        assert!(m.allocation_ref.is_none());
4722    }
4723
4724    #[test]
4725    fn pool_member_unallocated_accepts_owned_string_and_str_at_the_same_signature() {
4726        // `impl Into<String>` axis pin: both the `&'static str` shape
4727        // (every test-helper site) and the `String` shape (produced by
4728        // `Process::owned_name_or_empty` at the production
4729        // `controller_pool::reconcile_inner` site) reach the same
4730        // composer entry without a per-caller conversion. A regression
4731        // that narrowed the signature to `&str` alone would break the
4732        // production site's `owned_name_or_empty` handoff; a regression
4733        // that narrowed to `String` alone would force every test helper
4734        // to `.into()` at the callsite. This pin fences both corners.
4735        let anchor = crate::time::at_epoch_second(0);
4736        let via_str = PoolMember::unallocated("pool-y-0", MemberState::Spawning, anchor);
4737        let owned: String = "pool-y-0".to_string();
4738        let via_string = PoolMember::unallocated(owned, MemberState::Spawning, anchor);
4739        assert_eq!(via_str.process_name, via_string.process_name);
4740        assert_eq!(via_str.state, via_string.state);
4741        assert_eq!(via_str.entered_state_at, via_string.entered_state_at);
4742        assert_eq!(via_str.allocation_ref, via_string.allocation_ref);
4743    }
4744
4745    #[test]
4746    fn pool_member_unallocated_matches_pre_lift_struct_literal_bytewise() {
4747        // Byte-shape parity pin: the composer output is structurally
4748        // equal to the pre-lift 4-slot struct literal every hand-
4749        // authored site stamped. Sweeps every `MemberState` variant so
4750        // a regression that special-cased one variant (e.g., pinned
4751        // `Allocated` to a bogus `Some(<placeholder>)` at the composer)
4752        // surfaces here rather than at the four downstream helpers'
4753        // callsites.
4754        let anchor = crate::time::at_epoch_second(1_700_000_000);
4755        for state in [
4756            MemberState::Free,
4757            MemberState::Allocated,
4758            MemberState::Spawning,
4759            MemberState::Returning,
4760            MemberState::Failed,
4761        ] {
4762            let via_primitive = PoolMember::unallocated("m", state, anchor);
4763            let hand_authored = PoolMember {
4764                process_name: "m".into(),
4765                state,
4766                entered_state_at: anchor,
4767                allocation_ref: None,
4768            };
4769            assert_eq!(via_primitive.process_name, hand_authored.process_name);
4770            assert_eq!(via_primitive.state, hand_authored.state);
4771            assert_eq!(
4772                via_primitive.entered_state_at,
4773                hand_authored.entered_state_at
4774            );
4775            assert_eq!(via_primitive.allocation_ref, hand_authored.allocation_ref);
4776        }
4777    }
4778
4779    #[test]
4780    fn pool_member_unallocated_preserves_caller_clock_anchor() {
4781        // Clock-injectability pin: the composer does NOT read wall
4782        // time on its own — every consumer supplies its own
4783        // `entered_state_at` anchor (the production site from
4784        // `Process::observed_phase_since`, the `pool_decide` helper
4785        // from `crate::time::seconds_ago`, the `allocation_decide` and
4786        // `pool.rs` helpers from `Utc::now` / the epoch anchor). A
4787        // regression that started stamping the composer's own
4788        // `Utc::now()` would silently reset every downstream anchor
4789        // and break the fanout tests' epoch-based expectations.
4790        let epoch = crate::time::at_epoch_second(0);
4791        let future = crate::time::at_epoch_second(2_000_000_000);
4792        let anchored_at_epoch = PoolMember::unallocated("a", MemberState::Free, epoch);
4793        let anchored_at_future = PoolMember::unallocated("b", MemberState::Free, future);
4794        assert_eq!(anchored_at_epoch.entered_state_at, epoch);
4795        assert_eq!(anchored_at_future.entered_state_at, future);
4796        assert_ne!(
4797            anchored_at_epoch.entered_state_at, anchored_at_future.entered_state_at,
4798            "composer must preserve the caller-supplied anchor verbatim",
4799        );
4800    }
4801
4802    // ─── EphemeralPool::has_name substrate pins ───────────────────────
4803    //
4804    // Pins the copy-form metadata-projection primitive on the
4805    // `metadata.name` axis's presence-and-equal corner — the
4806    // discriminant every `candidate_pools.iter().find(|p| ...)`
4807    // closure that resolves a pool from an owned-name handle
4808    // (`AllocationRef.name` / `AllocationDecision::Bind.pool.name`)
4809    // routes through. Sibling to the `_or_empty` family on the SAME
4810    // slot ([`EphemeralPool::name_or_empty`] +
4811    // [`EphemeralPool::owned_name_or_empty`]) — this primitive owns
4812    // the `None`-preserving corner the `_or_empty` family folds away.
4813    // Fail-before-pass-after granularity: `has_name` did not exist
4814    // pre-lift; the compiler cannot resolve the name until the impl
4815    // block above is in place, so a rollback of the primitive breaks
4816    // this whole module.
4817    #[test]
4818    fn has_name_returns_true_when_slot_is_populated_and_equal() {
4819        // Happy-path pin: the slot is set AND byte-identical to the
4820        // candidate. Both pre-lift `find` closures — `resolve_pool`'s
4821        // explicit-`pool_ref` half and `controller_allocation`'s TTL-
4822        // inheritance fallback — resolve their target pool exactly in
4823        // this corner, and the primitive returns `true` here to
4824        // authorize the resolution.
4825        let p = pool_named("attest-pool");
4826        assert!(p.has_name("attest-pool"));
4827    }
4828
4829    #[test]
4830    fn has_name_returns_false_when_slot_is_populated_and_different() {
4831        // Populated-slot inequality pin: the primitive returns `false`
4832        // for every candidate that is NOT byte-identical to the slot,
4833        // including strict subsequences (`"attest"` vs. `"attest-pool"`),
4834        // strict superstrings (`"attest-pool-2"` vs. `"attest-pool"`),
4835        // and case-differ variants. This is the load-bearing property
4836        // that lets `find(|p| p.has_name(&candidate))` reject
4837        // non-matching pools rather than aliasing them together.
4838        let p = pool_named("attest-pool");
4839        assert!(!p.has_name("other-pool"));
4840        assert!(!p.has_name("attest"));
4841        assert!(!p.has_name("attest-pool-2"));
4842        assert!(!p.has_name("ATTEST-POOL"));
4843    }
4844
4845    #[test]
4846    fn has_name_returns_false_when_slot_is_none_even_against_empty_candidate() {
4847        // The `None`-preserving discipline pin: an unset `metadata.name`
4848        // slot returns `false` even when the candidate is the empty
4849        // string. Distinguishes `has_name` from a naïve substitution
4850        // through the sibling `name_or_empty` primitive, which would
4851        // fold both `None` and `Some("")` to `""` and silently promote
4852        // an unnamed pool with an empty candidate into a spurious
4853        // match at the resolver's `find` closure. Byte-identical to
4854        // what the pre-lift `.as_deref() == Some(<candidate>)` chain
4855        // produced (`None == Some("")` is `false`), which is what
4856        // both consumer sites relied on.
4857        let p = pool_unnamed();
4858        assert!(p.metadata.name.is_none(), "fixture invariant");
4859        assert!(!p.has_name(""));
4860        assert!(!p.has_name("attest-pool"));
4861    }
4862
4863    #[test]
4864    fn has_name_returns_true_only_when_populated_slot_and_candidate_are_both_empty() {
4865        // Populated-empty-slot corner pin: `Some(String::new())` is a
4866        // populated slot with an empty payload. `has_name("")` returns
4867        // `true` here (byte-identical `""` on both sides), while
4868        // `has_name("<anything else>")` returns `false`. This is the
4869        // corner where `has_name` DIVERGES from `name_or_empty`
4870        // observably: the `_or_empty` family folds this corner into
4871        // the same bucket as `None`, but `has_name` keeps the
4872        // presence bit visible — `Some("") == Some("")` is `true`
4873        // while `None == Some("")` is `false`.
4874        let mut p = pool_named("scratch");
4875        p.metadata.name = Some(String::new());
4876        assert!(p.has_name(""));
4877        assert!(!p.has_name("attest-pool"));
4878    }
4879
4880    #[test]
4881    fn has_name_matches_pre_lift_chain_verbatim_across_full_corner_set() {
4882        // Byte-identical parity pin: the primitive returns the same
4883        // `bool` as the pre-lift `.metadata.name.as_deref() == Some
4884        // (candidate)` chain across the FULL cross product of
4885        // (slot ∈ {None, Some("attest-pool"), Some("")}) × (candidate
4886        // ∈ {"attest-pool", "", "other"}). A regression that inserted
4887        // a normalization step at the primitive the pre-lift chain
4888        // does NOT apply — or vice versa — surfaces here rather than
4889        // as silent drift between the two `find` closures the primitive
4890        // owns.
4891        let slots: [Option<String>; 3] =
4892            [None, Some(String::from("attest-pool")), Some(String::new())];
4893        let candidates: [&str; 3] = ["attest-pool", "", "other"];
4894        for slot in slots {
4895            let mut p = pool_named("scratch");
4896            p.metadata.name = slot.clone();
4897            for candidate in candidates {
4898                let pre_lift = p.metadata.name.as_deref() == Some(candidate);
4899                assert_eq!(
4900                    p.has_name(candidate),
4901                    pre_lift,
4902                    "slot = {slot:?}, candidate = {candidate:?}"
4903                );
4904            }
4905        }
4906    }
4907
4908    #[test]
4909    fn has_name_diverges_from_name_or_empty_on_the_missing_slot_corner() {
4910        // Cross-primitive discipline pin: `has_name("")` and
4911        // `name_or_empty() == ""` MUST disagree on the `None`-slot
4912        // corner. `name_or_empty` returns `""` (its load-bearing
4913        // sentinel), so a naïve `name_or_empty() == ""` probe would
4914        // return `true` here — aliasing every unnamed pool to the
4915        // empty-candidate bucket at the resolver. `has_name`
4916        // preserves `Option::as_deref() == Some(_)`'s `None ⇒ false`
4917        // semantics, so it returns `false` and rejects the spurious
4918        // match. This test fences the WHOLE reason `has_name` exists
4919        // as a distinct primitive from the `_or_empty` family: a
4920        // future refactor that collapsed `has_name` into
4921        // `name_or_empty() == candidate` would break this pin and
4922        // silently regress the resolver's byte-comparison honesty.
4923        let p = pool_unnamed();
4924        assert_eq!(p.name_or_empty(), "");
4925        assert!(!p.has_name(""));
4926    }
4927
4928    #[test]
4929    fn has_name_is_a_pure_projection() {
4930        // Consecutive calls with the same candidate return the same
4931        // `bool` — no cached state, no mutation on the `EphemeralPool`
4932        // between calls. Guards against a future refactor that plants
4933        // a cache field on `EphemeralPool` and drifts one caller from
4934        // another silently.
4935        let p = pool_named("router-pool");
4936        assert_eq!(p.has_name("router-pool"), p.has_name("router-pool"));
4937        assert_eq!(p.has_name("other"), p.has_name("other"));
4938        assert!(p.has_name("router-pool"));
4939        assert!(!p.has_name("other"));
4940    }
4941
4942    // ─── PoolSpec::free_ttl_duration substrate pins ─────────────────
4943    //
4944    // The `humantime::parse_duration(&<field>).ok()` shape rides
4945    // through TWO peer inherent methods on peer spec types post-lift:
4946    // [`crate::lifetime::EphemeralLifetime::ttl_duration`] on the
4947    // `spec.lifetime.ephemeral.ttl` axis + [`PoolSpec::free_ttl_
4948    // duration`] on the `pool.spec.free_ttl` axis. These pins bind the
4949    // pool-spec-side primitive at fail-before-pass-after granularity
4950    // so a regression that drifts either surface (a per-fleet minimum
4951    // floor added at only one primitive, a canonical unit-normalization
4952    // pass, a warn-log on unparseable strings) fails here rather than
4953    // as silent operator-facing skew between the pool stale-free
4954    // bucket loop in `tatara-pool-reconciler::pool_decide::decide_pool`
4955    // and the ephemeral TTL-expiry gate in
4956    // `tatara-process::lifetime_clock::evaluate`.
4957
4958    fn pool_spec_with_free_ttl(free_ttl: &str) -> PoolSpec {
4959        PoolSpec {
4960            free_ttl: free_ttl.into(),
4961            ..pool_spec()
4962        }
4963    }
4964
4965    #[test]
4966    fn pool_spec_free_ttl_duration_parseable_humantime_projects_to_some() {
4967        for (ttl, expected_secs) in [
4968            ("30s", 30u64),
4969            ("5m", 300),
4970            ("1h", 3600),
4971            ("24h", 86_400),
4972            ("1d", 86_400),
4973        ] {
4974            let spec = pool_spec_with_free_ttl(ttl);
4975            assert_eq!(
4976                spec.free_ttl_duration(),
4977                Some(std::time::Duration::from_secs(expected_secs)),
4978                "free_ttl_duration drift for {ttl:?}",
4979            );
4980        }
4981    }
4982
4983    #[test]
4984    fn pool_spec_free_ttl_duration_unparseable_returns_none() {
4985        // A typo (`"1our"`), an unsupported unit (`"1w"` — humantime
4986        // supports `w`, but `"forever"` doesn't), a non-humantime
4987        // literal that reached the field via API-server acceptance
4988        // ALL collapse to `None`. The `pool_decide::decide_pool`
4989        // caller collapses the corner via `.unwrap_or_default()`,
4990        // yielding `Duration::ZERO` — byte-identical to the pre-lift
4991        // hand-authored `humantime::parse_duration(&spec.free_ttl)
4992        // .unwrap_or_default()` semantics.
4993        for bad in ["", "1our", "forever", "not-a-duration", "1", "-1s"] {
4994            let spec = pool_spec_with_free_ttl(bad);
4995            assert_eq!(
4996                spec.free_ttl_duration(),
4997                None,
4998                "free_ttl_duration should be None for {bad:?}",
4999            );
5000        }
5001    }
5002
5003    #[test]
5004    fn pool_spec_free_ttl_duration_zero_seconds_returns_some_zero() {
5005        // `"0s"` is a parseable-but-zero humantime literal — the
5006        // primitive returns `Some(Duration::ZERO)`, distinguishable
5007        // from the parse-failure `None` corner. Downstream consumers
5008        // that gate on `!free_ttl.is_zero()` collapse this back
5009        // together with the `None`-via-`unwrap_or_default()` corner,
5010        // but the primitive itself keeps the two shapes distinct so
5011        // a future consumer needing that distinction can reach for
5012        // it without a re-parse.
5013        let spec = pool_spec_with_free_ttl("0s");
5014        assert_eq!(
5015            spec.free_ttl_duration(),
5016            Some(std::time::Duration::ZERO),
5017            "0s should project to Some(Duration::ZERO), not None",
5018        );
5019    }
5020
5021    #[test]
5022    fn pool_spec_free_ttl_duration_default_free_ttl_matches_24h() {
5023        // The default `free_ttl` is `"24h"` (via [`default_free_ttl`]).
5024        // The primitive on a `PoolSpec` carrying the default must
5025        // agree with a manually-parsed `"24h"` — a future
5026        // `default_free_ttl` change (a shorter recycling window, a
5027        // per-fleet override) reaches BOTH surfaces at once (this
5028        // pin + the `default_free_ttl` fn) without silent skew.
5029        let spec = pool_spec_with_free_ttl(&default_free_ttl());
5030        assert_eq!(
5031            spec.free_ttl_duration(),
5032            Some(std::time::Duration::from_secs(24 * 3600)),
5033        );
5034    }
5035
5036    #[test]
5037    fn pool_spec_free_ttl_duration_matches_pre_lift_hand_authored_chain_bytewise() {
5038        // Byte-shape parity with the pre-lift hand-authored chain the
5039        // `pool_decide::decide_pool` stale-free bucket loop restated
5040        // (`humantime::parse_duration(&spec.free_ttl).ok()` — the
5041        // `.ok()` tail and the caller's `.unwrap_or_default()` compose
5042        // to the same `Duration::ZERO`-on-failure semantics). Sweeps
5043        // every callsite corner the pool reconciler plausibly
5044        // encounters: the default `"24h"` free-recycling window, a
5045        // short-window test override (`"10s"`), a parse-failure typo,
5046        // an empty string.
5047        for ttl in ["24h", "10s", "1our", ""] {
5048            let spec = pool_spec_with_free_ttl(ttl);
5049            let via_primitive = spec.free_ttl_duration();
5050            let hand_authored = humantime::parse_duration(&spec.free_ttl).ok();
5051            assert_eq!(
5052                via_primitive, hand_authored,
5053                "free_ttl_duration must be byte-identical to `humantime::\
5054                 parse_duration(&spec.free_ttl).ok()` for {ttl:?}",
5055            );
5056        }
5057    }
5058
5059    #[test]
5060    fn pool_spec_free_ttl_duration_matches_peer_ephemeral_lifetime_ttl_duration_shape() {
5061        // Return-shape parity with the peer primitive
5062        // [`crate::lifetime::EphemeralLifetime::ttl_duration`]: given
5063        // the SAME humantime string on both peer fields (the pool
5064        // `free_ttl` slot AND the ephemeral `ttl` slot), the two
5065        // primitives return byte-identical `Option<Duration>` values.
5066        // A regression that inserted a per-primitive normalization
5067        // step at only one surface — a per-fleet minimum floor, a
5068        // canonical unit-normalization pass — surfaces here rather
5069        // than as silent operator-facing skew between the pool
5070        // stale-free bucket loop and the ephemeral TTL-expiry gate
5071        // on the SAME humantime literal.
5072        for ttl in ["30s", "1h", "24h", "1our", ""] {
5073            let pool_spec = pool_spec_with_free_ttl(ttl);
5074            let eph = crate::lifetime::EphemeralLifetime {
5075                ttl: ttl.into(),
5076                ..Default::default()
5077            };
5078            assert_eq!(
5079                pool_spec.free_ttl_duration(),
5080                eph.ttl_duration(),
5081                "peer-primitive shape drift for {ttl:?}",
5082            );
5083        }
5084    }
5085
5086    // ── PoolSpec::with_template substrate pins ──────────────────────
5087    //
5088    // The 11-slot `PoolSpec { desired_size: <N>, min_size: 0, max_size:
5089    // 0, return_policy: ReturnPolicy::Replace, selector: <PoolSelector
5090    // ::default() or override>, template: <EphemeralSpec>, free_ttl:
5091    // "24h".into(), max_allocation_ttl: "4h".into(), desired: 0,
5092    // replacement_policy: Default::default(), stable_name_claim: false
5093    // }` struct-literal was open-coded verbatim at EIGHT hand-authored
5094    // callsites across two crates before this primitive closed it.
5095    // These pins bind the composed shape at fail-before-pass-after
5096    // granularity so a regression that drifted the wire-published
5097    // default at only one slot — a shorter `default_free_ttl`, a
5098    // widened `ReturnPolicy` default, a promoted `stable_name_claim`
5099    // seed — surfaces HERE rather than as silent operator-visible drift
5100    // across every fixture that keys assertions on the shape.
5101    fn hand_authored_pre_lift_with_template() -> PoolSpec {
5102        PoolSpec {
5103            desired_size: 0,
5104            min_size: 0,
5105            max_size: 0,
5106            return_policy: ReturnPolicy::Replace,
5107            selector: PoolSelector::default(),
5108            template: empty_template(),
5109            free_ttl: "24h".into(),
5110            max_allocation_ttl: "4h".into(),
5111            desired: 0,
5112            replacement_policy: ReplacementPolicy::default(),
5113            stable_name_claim: false,
5114        }
5115    }
5116
5117    #[test]
5118    fn with_template_stamps_caller_supplied_template_verbatim() {
5119        // The caller-supplied slot is the ONE the substrate does not
5120        // default. A regression that reshaped the primitive's
5121        // pass-through — a hidden re-encode through
5122        // `serde_json::to_value` and back, a per-primitive
5123        // normalization that flipped a defaulted-inner slot — would
5124        // surface HERE rather than at every downstream seed whose
5125        // assertions key on the template shape.
5126        let t = empty_template();
5127        let s = PoolSpec::with_template(t.clone());
5128        assert_eq!(
5129            serde_json::to_value(&s.template).unwrap(),
5130            serde_json::to_value(&t).unwrap(),
5131        );
5132    }
5133
5134    #[test]
5135    fn with_template_defaulted_slots_ride_wire_schema_defaults() {
5136        // Pins the sibling-default correspondence the doc-comment
5137        // names — every non-template slot rides its own
5138        // `#[serde(default = "…")]` value from the `pub struct
5139        // PoolSpec` schema above. A regression that promoted any
5140        // defaulted slot to a non-default (a shorter
5141        // `default_free_ttl`, a widened `ReturnPolicy` default, a
5142        // `stable_name_claim: true` seed) would move the baseline
5143        // HERE rather than at every downstream fixture.
5144        let s = PoolSpec::with_template(empty_template());
5145        assert_eq!(s.desired_size, 0);
5146        assert_eq!(s.min_size, 0);
5147        assert_eq!(s.max_size, 0);
5148        assert_eq!(s.return_policy, ReturnPolicy::default());
5149        assert_eq!(
5150            serde_json::to_value(&s.selector).unwrap(),
5151            serde_json::to_value(PoolSelector::default()).unwrap(),
5152        );
5153        assert_eq!(s.free_ttl, default_free_ttl());
5154        assert_eq!(s.max_allocation_ttl, default_max_allocation_ttl());
5155        assert_eq!(s.desired, 0);
5156        assert_eq!(s.replacement_policy, ReplacementPolicy::default());
5157        assert!(!s.stable_name_claim);
5158    }
5159
5160    #[test]
5161    fn with_template_matches_hand_authored_pre_lift_bytewise() {
5162        // Byte-identical parity pin between the substrate primitive
5163        // and the pre-lift 11-slot struct-literal that recurred at
5164        // eight hand-authored sites (compared with `desired_size:
5165        // 0` to match the primitive's baseline — the five hand-
5166        // authored `desired_size: 1` sites compose the baseline via
5167        // struct-update and the pin below binds THAT axis
5168        // separately). Compares via `serde_json` value equality —
5169        // `PoolSpec` does not derive `PartialEq` (the typed fields
5170        // it composes over do not uniformly derive it), so a
5171        // serialize round-trip is the shape-equality currency the
5172        // pin family already uses.
5173        let composed = PoolSpec::with_template(empty_template());
5174        let hand = hand_authored_pre_lift_with_template();
5175        assert_eq!(
5176            serde_json::to_value(&composed).unwrap(),
5177            serde_json::to_value(&hand).unwrap(),
5178        );
5179    }
5180
5181    #[test]
5182    fn with_template_supports_struct_update_override_at_each_pre_lift_axis() {
5183        // Sweeps every override axis the eight pre-lift seeds
5184        // exercised via struct-update syntax:
5185        // * `desired_size: 1` — six sites (the majority of pre-lift
5186        //   fixtures use a single-slot pool).
5187        // * `selector: <custom>` — two sites (router.rs +
5188        //   allocation_decide.rs).
5189        // * `desired: N` + `replacement_policy: <policy>` — one
5190        //   site (desired.rs's desired-count-loop fixture).
5191        // * `desired_size: N, min_size: N, max_size: N` — one site
5192        //   (pool_decide.rs's pure-decision fixture).
5193        // A regression that broke the struct-update path (e.g. a
5194        // `#[non_exhaustive]` attribute added to `PoolSpec` that
5195        // would refuse struct-update syntax across crate boundaries)
5196        // surfaces at compile time HERE rather than as an eight-site
5197        // downstream break.
5198        let base = PoolSpec::with_template(empty_template());
5199        let size_1 = PoolSpec {
5200            desired_size: 1,
5201            ..PoolSpec::with_template(empty_template())
5202        };
5203        assert_eq!(base.desired_size, 0);
5204        assert_eq!(size_1.desired_size, 1);
5205        // Every other slot rides the base composition.
5206        assert_eq!(size_1.free_ttl, base.free_ttl);
5207        assert_eq!(size_1.max_allocation_ttl, base.max_allocation_ttl);
5208
5209        let custom_selector = PoolSelector::default();
5210        let with_selector = PoolSpec {
5211            desired_size: 1,
5212            selector: custom_selector,
5213            ..PoolSpec::with_template(empty_template())
5214        };
5215        assert_eq!(with_selector.desired_size, 1);
5216        assert_eq!(with_selector.free_ttl, base.free_ttl);
5217
5218        let with_desired = PoolSpec {
5219            desired: 5,
5220            replacement_policy: ReplacementPolicy::HoldFailed,
5221            ..PoolSpec::with_template(empty_template())
5222        };
5223        assert_eq!(with_desired.desired, 5);
5224        assert_eq!(
5225            with_desired.replacement_policy,
5226            ReplacementPolicy::HoldFailed
5227        );
5228        assert_eq!(with_desired.desired_size, 0);
5229
5230        let with_sizes = PoolSpec {
5231            desired_size: 3,
5232            min_size: 1,
5233            max_size: 5,
5234            ..PoolSpec::with_template(empty_template())
5235        };
5236        assert_eq!(with_sizes.desired_size, 3);
5237        assert_eq!(with_sizes.min_size, 1);
5238        assert_eq!(with_sizes.max_size, 5);
5239        assert_eq!(with_sizes.replacement_policy, base.replacement_policy);
5240    }
5241
5242    #[test]
5243    fn with_template_is_call_time_construction_not_a_shared_singleton() {
5244        // Two independent calls produce structurally-equal but
5245        // distinct values — pins that the primitive is a plain
5246        // constructor rather than a `lazy_static` clone whose in-
5247        // place mutation at one consumer would silently mutate the
5248        // shape at every other consumer. Mirrors the sibling
5249        // `gate_compute_defaults_is_call_time_construction_not_a_
5250        // shared_singleton` pin on `ProcessSpec::gate_compute_defaults`.
5251        let a = PoolSpec::with_template(empty_template());
5252        let b = PoolSpec::with_template(empty_template());
5253        assert_eq!(
5254            serde_json::to_value(&a).unwrap(),
5255            serde_json::to_value(&b).unwrap(),
5256        );
5257        assert!(!std::ptr::eq(&a, &b));
5258    }
5259
5260    #[test]
5261    fn with_template_free_ttl_composes_with_free_ttl_duration_at_default_window() {
5262        // The primitive's `free_ttl` slot rides `default_free_ttl()`;
5263        // the sibling `free_ttl_duration` primitive parses that
5264        // literal into the same 24h `Duration` every pre-lift
5265        // reconciler-side seed produced. Pins the round-trip so a
5266        // regression that shifted `default_free_ttl` without
5267        // updating this baseline (or vice versa) surfaces HERE
5268        // rather than as silent skew between the composer and the
5269        // ttl-parse gate that consumes it.
5270        let s = PoolSpec::with_template(empty_template());
5271        assert_eq!(
5272            s.free_ttl_duration(),
5273            Some(std::time::Duration::from_secs(24 * 3600)),
5274        );
5275    }
5276
5277    // ─── EphemeralPool::new_in substrate pins ─────────────────────────
5278    //
5279    // The pre-lift 2-line `let mut p = EphemeralPool::new(<name>,
5280    // <spec>); p.meta_mut().namespace = Some(<ns>.into());` chain
5281    // recurred at FOUR workspace-wide fixture sites in
5282    // `tatara-pool-reconciler` past the ★★ PRIME-DIRECTIVE ≥ 2
5283    // threshold. Post-lift the ONE substrate composer stamps a
5284    // namespaced `EphemeralPool` from `(name, ns, spec)` in one call.
5285    // Fail-before-pass-after granularity: `new_in` did not exist pre-
5286    // lift; the compiler cannot resolve the name until the impl block
5287    // above is in place, so a rollback of the primitive breaks this
5288    // whole pin block.
5289
5290    #[test]
5291    fn new_in_stamps_metadata_name_from_the_name_slot() {
5292        // `name` slot → `metadata.name` projection pin. Guards against
5293        // a regression that dropped the `name` slot into a `generate_
5294        // name` slot, an `annotations` seed, or any downstream slot the
5295        // kube-derived [`Self::new`] does not populate at
5296        // `metadata.name` verbatim.
5297        let s = pool_spec();
5298        let p = EphemeralPool::new_in("attest-pool", "pools", s);
5299        assert_eq!(p.metadata.name.as_deref(), Some("attest-pool"));
5300    }
5301
5302    #[test]
5303    fn new_in_stamps_metadata_namespace_from_the_ns_slot() {
5304        // `ns` slot → `metadata.namespace` projection pin. Guards
5305        // against a regression that dropped the `ns` slot into a
5306        // `labels` seed, an unrelated annotation, or that stamped
5307        // `namespace = None` even after a caller-supplied value.
5308        let s = pool_spec();
5309        let p = EphemeralPool::new_in("attest-pool", "pools", s);
5310        assert_eq!(p.metadata.namespace.as_deref(), Some("pools"));
5311    }
5312
5313    #[test]
5314    fn new_in_stamps_spec_from_the_spec_slot_verbatim() {
5315        // `spec` slot → `spec` projection pin. A regression that
5316        // silently normalized the caller-supplied spec inside the
5317        // composer (a defaulted-slot reset, a per-fleet override) would
5318        // diverge from the byte-identical pass-through the pre-lift
5319        // 2-line chain produced.
5320        let mut s = pool_spec();
5321        s.desired_size = 7;
5322        let p = EphemeralPool::new_in("attest-pool", "pools", s.clone());
5323        assert_eq!(p.spec.desired_size, s.desired_size);
5324        assert_eq!(p.spec.min_size, s.min_size);
5325        assert_eq!(p.spec.max_size, s.max_size);
5326    }
5327
5328    #[test]
5329    fn new_in_accepts_both_owned_and_borrowed_namespace_slot() {
5330        // The `impl Into<String>` ergonomic contract round-trips
5331        // through both `&'static str` (majority pre-lift caller shape)
5332        // AND owned `String` at the SAME signature. Guards against a
5333        // regression that narrowed the slot to `&str` only or that
5334        // silently double-`.into()`d an already-owned String.
5335        let s = pool_spec();
5336        let via_str = EphemeralPool::new_in("attest-pool", "pools", s.clone());
5337        let via_string = EphemeralPool::new_in("attest-pool", String::from("pools"), s.clone());
5338        assert_eq!(via_str.metadata.namespace, via_string.metadata.namespace);
5339    }
5340
5341    #[test]
5342    fn new_in_matches_pre_lift_construct_then_set_namespace_bytewise() {
5343        // Byte-shape parity witness against the pre-lift 2-line chain
5344        // across the two representative namespace shapes the collapsed
5345        // sites used (`"ephemeral-pools"` at `router::pool`, `"pools"`
5346        // at `pool_decide::pool` + `desired::pool` +
5347        // `allocation_decide::pool`). A regression that shifted the
5348        // composer's output would diverge from the pre-lift literal
5349        // HERE rather than at every downstream fixture's downstream
5350        // assertion.
5351        for ns in ["ephemeral-pools", "pools"] {
5352            let via_primitive = EphemeralPool::new_in("attest-pool", ns, pool_spec());
5353            let mut hand_authored = EphemeralPool::new("attest-pool", pool_spec());
5354            hand_authored.metadata.namespace = Some(ns.into());
5355            assert_eq!(via_primitive.metadata.name, hand_authored.metadata.name);
5356            assert_eq!(
5357                via_primitive.metadata.namespace,
5358                hand_authored.metadata.namespace,
5359            );
5360        }
5361    }
5362
5363    #[test]
5364    fn new_in_defaults_other_metadata_slots_at_kube_derived_new() {
5365        // The composer forwards to the kube-derived [`Self::new`] for
5366        // every non-namespace metadata slot. A regression that stamped
5367        // finalizers, owner_references, labels, or annotations inside
5368        // the composer's body — inheriting the pre-lift chain's
5369        // undocumented emptiness at those slots — would surface here.
5370        let p = EphemeralPool::new_in("attest-pool", "pools", pool_spec());
5371        assert!(p.metadata.finalizers.is_none());
5372        assert!(p.metadata.owner_references.is_none());
5373        assert!(p.metadata.labels.is_none());
5374        assert!(p.metadata.annotations.is_none());
5375    }
5376}