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