Skip to main content

tatara_process/
pool.rs

1//! `EphemeralPool` CRD — a population of warm, pre-attested ephemeral
2//! Processes that get *allocated* to requestors (e.g., a GitHub PR
3//! flow) on demand and *returned* (per a typed policy) when the
4//! requestor releases them.
5//!
6//! Compounding move: the pool is a population manager **over the
7//! existing Process algebra**, not a parallel runtime. A pool member
8//! is just a `Process` with `Lifetime::Permanent` while in the free
9//! list; allocation is "the operator (the pool reconciler) flips
10//! that Process's lifetime slot to Ephemeral with the requestor's
11//! TTL." Zero new compute primitive.
12//!
13//! Topology:
14//!
15//! ```text
16//! EphemeralPool       (this CRD)
17//!   ├── PoolSpec      (desired_size, template (EphemeralSpec), return_policy, selector)
18//!   ├── PoolStatus    (phase, free / allocated / spawning / returning counts, members)
19//!   └── owns N Processes via ownerReferences (one per pool slot)
20//!
21//! EphemeralAllocation (see allocation.rs)
22//!   ├── AllocationSpec (pool_ref, requestor, requested_at, lifetime override)
23//!   └── AllocationStatus (phase, assigned_process_ref, allocated_at, expires_at)
24//! ```
25
26use chrono::{DateTime, Utc};
27use kube::CustomResource;
28use schemars::JsonSchema;
29use serde::{Deserialize, Serialize};
30
31use crate::ephemeral::EphemeralSpec;
32
33/// `EphemeralPool` CRD spec — typed pool of warm Processes.
34///
35/// ```yaml
36/// apiVersion: tatara.pleme.io/v1alpha1
37/// kind: EphemeralPool
38/// metadata:
39///   name: attest-pool
40///   namespace: ephemeral-pools
41/// spec:
42///   desiredSize: 3
43///   minSize: 1
44///   maxSize: 5
45///   returnPolicy: Reset
46///   selector:
47///     repos: ["pleme-io/demo-*"]
48///     branches: ["main", "release-*"]
49///     prLabels: ["needs-ephemeral"]
50///   template:
51///     aplicacao:
52///       chartRef: "oci://ghcr.io/pleme-io/charts/lareira-demo-app"
53///       version: "0.5.5"
54///       profile: "all-in-one"
55///       …
56///     ttl: "2h"
57///     teardown: OnAttested
58///     postconditions: [ … ]
59/// ```
60#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
61#[kube(
62    group = "tatara.pleme.io",
63    version = "v1alpha1",
64    kind = "EphemeralPool",
65    plural = "ephemeralpools",
66    shortname = "epool",
67    namespaced,
68    status = "PoolStatus",
69    printcolumn = r#"{"name":"Desired","type":"integer","jsonPath":".spec.desiredSize"}"#,
70    printcolumn = r#"{"name":"Ready","type":"integer","jsonPath":".status.readyCount"}"#,
71    printcolumn = r#"{"name":"Allocated","type":"integer","jsonPath":".status.allocatedCount"}"#,
72    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
73    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
74)]
75#[serde(rename_all = "camelCase")]
76pub struct PoolSpec {
77    /// Target number of warm Processes the pool maintains in `Free`
78    /// state (sum of Free + Spawning targets `desired_size`).
79    pub desired_size: u32,
80
81    /// Hard floor on the free count. The reconciler refuses to scale
82    /// below this even on cost-pressure signals. Default = 0.
83    #[serde(default)]
84    pub min_size: u32,
85
86    /// Hard ceiling on total pool members (free + allocated + spawning).
87    /// `0` = no cap. Default = 0.
88    #[serde(default)]
89    pub max_size: u32,
90
91    /// What to do when an allocation releases.
92    #[serde(default)]
93    pub return_policy: ReturnPolicy,
94
95    /// Routing selector — which allocation requests this pool serves.
96    /// The reconciler matches incoming `EphemeralAllocation` CRs
97    /// against this selector (most-specific wins across pools sharing
98    /// a namespace).
99    #[serde(default)]
100    pub selector: PoolSelector,
101
102    /// Template for each pool member — a typed `EphemeralSpec` that
103    /// the reconciler lowers to `ProcessSpec` and instantiates.
104    /// While in the free list each member's lifetime is overridden
105    /// to `Permanent`; allocation flips it back to `Ephemeral` with
106    /// the requestor's TTL.
107    pub template: EphemeralSpec,
108
109    /// How long a pool member may sit in `Free` before the reconciler
110    /// recycles it (humantime). Defends against drift / stale state.
111    /// Default `"24h"`.
112    #[serde(default = "default_free_ttl")]
113    pub free_ttl: String,
114
115    /// Max time the reconciler allows a single allocation to hold a
116    /// member before forcibly returning it (humantime). Hard cap
117    /// independent of the allocation's own TTL. Default `"4h"`.
118    #[serde(default = "default_max_allocation_ttl")]
119    pub max_allocation_ttl: String,
120
121    /// **R5 desired-count loop** — when set non-zero, the pool
122    /// reconciler maintains exactly this many *healthy* (Running or
123    /// Attested) Processes regardless of allocation pressure. Drives
124    /// the "always seeking stability" property: failed members are
125    /// replaced per `replacement_policy`. `0` keeps the legacy
126    /// allocation-driven sizing (desired = floor of free + allocated).
127    ///
128    /// Operator usage: `desired: 5` means "always have 5 of these
129    /// running"; failures auto-replace.
130    #[serde(default)]
131    pub desired: u32,
132
133    /// **R5** — what the pool reconciler does when a member reaches
134    /// `Failed` phase.
135    #[serde(default)]
136    pub replacement_policy: ReplacementPolicy,
137
138    /// **R5** — when true, exactly one healthy member of the pool
139    /// holds the unprefixed-form DNS hostnames declared in
140    /// `template.routing` at any moment. The claim arbiter (see
141    /// `tatara-reconciler::claim`) transfers atomically when the
142    /// holder fails.
143    #[serde(default)]
144    pub stable_name_claim: bool,
145}
146
147impl EphemeralPool {
148    /// Borrow-form metadata-projection primitive on the `metadata.name`
149    /// axis of `EphemeralPool`: returns the K8s object name slice with
150    /// the missing-name corner collapsed to the load-bearing empty-string
151    /// sentinel — the ONE-liner collapse of the paired
152    /// `self.metadata.name.as_deref().unwrap_or("")` incantation every
153    /// pool-side consumer restated by hand pre-lift.
154    ///
155    /// Pre-lift the `.metadata.name.as_deref().unwrap_or("")` chain
156    /// was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
157    /// duplication threshold in `tatara-pool-reconciler`, both keyed
158    /// by the pool's own name slot:
159    /// * `router::pool_name` — the tie-break comparator inside
160    ///   `best_match`; a deterministic lexicographic-min-name arbiter
161    ///   across two pool candidates whose specificity scores tie.
162    /// * `controller_allocation::reconcile_inner` — the `HashMap<
163    ///   pool-name, Vec<PoolMember>>` lookup closure fed into
164    ///   `decide_allocation_reconcile`; keys the "which pool members
165    ///   back this allocation candidate?" projection at every
166    ///   allocation-reconcile pass.
167    ///
168    /// Both sites walked the SAME `.as_deref().unwrap_or("")` chain
169    /// and both wanted the `&str` form the primitive returns — as a
170    /// borrow suitable for lexicographic `str::cmp` in the tie-break
171    /// AND for the `HashMap<String, _>::get(&str)` lookup. Post-lift
172    /// each caller reaches for `pool.name_or_empty()` and the produced
173    /// slice feeds the same downstream comparator / lookup unchanged.
174    ///
175    /// The empty-string fallback is the SAME sentinel the sibling
176    /// borrow-form primitive [`crate::crd::Process::uid_or_empty`]
177    /// returns AND the SAME sentinel the owned-form sibling
178    /// [`crate::crd::Process::owned_name_or_empty`] returns on the
179    /// `metadata.name` axis of the sister CRD — the three primitives
180    /// partition the (borrow-form × owned-form) × (uid × name) corner
181    /// of the metadata-slot family on identical fallback semantics
182    /// (empty string means "the slot is unset"), so a consumer that
183    /// switches between the CRD surfaces based on downstream keying
184    /// requirements never sees a different missing-slot spelling as
185    /// a side effect.
186    ///
187    /// Return-form axis: `&str` mirrors the borrow-first discipline
188    /// of the peer metadata primitives on `Process`
189    /// ([`crate::crd::Process::namespace_or_default`],
190    /// [`crate::crd::Process::name_or_placeholder`],
191    /// [`crate::crd::Process::uid_or_empty`]). The one missing-slot
192    /// corner the chain swallowed pre-lift (missing `metadata.name`)
193    /// collapses to the empty-string sentinel so `str::is_empty` /
194    /// `HashMap::get` on an unnamed pool behaves identically to what
195    /// the pre-lift `.as_deref().unwrap_or("")` chain produced.
196    ///
197    /// A future normalization step (a name-canonicalization pass, a
198    /// case-fold key builder, a per-cluster prefix stripper for
199    /// cross-cluster pool-name aliasing) lands at ONE substrate
200    /// method here and both downstream consumers pick up the upgrade
201    /// mechanically — no per-callsite hand-edit at `pool_name` /
202    /// `reconcile_inner`.
203    ///
204    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
205    /// the `.metadata.name.as_deref().unwrap_or("")` chain recurred
206    /// at two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
207    /// duplication trigger, and is lifted to ONE owner here).
208    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
209    /// the pins bind the missing-name corner + the empty-string
210    /// sentinel byte-shape + the borrow-form `&str` lifetime + the
211    /// byte-identical parity with the pre-lift chain + the fallback-
212    /// value coherence with `Process::uid_or_empty` /
213    /// `Process::owned_name_or_empty` on the metadata-slot × empty-
214    /// sentinel axis, so a regression that drifted any surface at
215    /// `tests::name_or_empty_*` here rather than as silent operator-
216    /// facing skew between the router tie-break and the allocation
217    /// member-lookup on the SAME pool candidate).
218    pub fn name_or_empty(&self) -> &str {
219        self.metadata.name.as_deref().unwrap_or("")
220    }
221
222    /// Owned-form metadata-projection primitive on the `metadata.name`
223    /// axis of `EphemeralPool`: returns an owned `String` copy of the K8s
224    /// object name with the missing-name corner collapsed to the load-
225    /// bearing empty-string sentinel — the ONE-liner collapse of the
226    /// paired `self.metadata.name.clone().unwrap_or_default()` incantation
227    /// every pool-side consumer restated by hand pre-lift.
228    ///
229    /// Pre-lift the `.metadata.name.clone().unwrap_or_default()` chain
230    /// was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
231    /// duplication threshold in `tatara-pool-reconciler`, both keyed by
232    /// the pool's own name slot in an `owned String` context:
233    /// * `controller_allocation::reconcile_inner` — the
234    ///   `HashMap<String, Vec<PoolMember>>` key seed inside a
235    ///   `pools.iter().map(|p| ...).collect()` fanout; the map key is
236    ///   the owned `String` form because the produced `HashMap<String, _>`
237    ///   outlives the pool-list borrow that generated it and the
238    ///   downstream `pool_members.get(pool.name_or_empty())` closure
239    ///   consumes it as `&str`.
240    /// * `allocation_decide::AllocationConvergenceCtx::observe` — the
241    ///   `AllocationRef::name` slot seed stamped on the matched-pool
242    ///   handle; the struct literal is `AllocationRef { name: String,
243    ///   namespace: String }` and the produced value is threaded through
244    ///   the `Decision::decide` transition rule downstream.
245    ///
246    /// Both sites walked the SAME `.clone().unwrap_or_default()` chain
247    /// and both wanted the `String` form the primitive returns — as the
248    /// owned key of a `HashMap<String, _>` and as the `String` slot of
249    /// an `AllocationRef` struct literal. Post-lift each callsite reads
250    /// `pool.owned_name_or_empty()` and the produced value feeds the
251    /// same downstream key / struct-literal slot unchanged.
252    ///
253    /// The empty-string fallback is the SAME sentinel the sibling
254    /// borrow-form primitive [`Self::name_or_empty`] returns AND the
255    /// SAME sentinel the sibling owned-form primitive
256    /// [`crate::crd::Process::owned_name_or_empty`] returns on the
257    /// `metadata.name` axis of the sister CRD — the three primitives
258    /// partition the (borrow-form × owned-form) corner of the metadata-
259    /// name family across BOTH tatara-process CRDs on identical missing-
260    /// slot semantics (empty string means "the slot is unset"), so a
261    /// consumer that switches between the CRD surfaces based on
262    /// downstream ownership requirements never sees a different
263    /// missing-slot spelling as a side effect.
264    ///
265    /// Peer to [`Self::name_or_empty`] on the (return-form × ownership)
266    /// axis pair — closes the corner the pool-side family previously
267    /// left open:
268    ///
269    /// * borrow + empty sentinel → [`Self::name_or_empty`] (router tie-
270    ///   break comparator, `HashMap<String, _>::get(&str)` lookup —
271    ///   consumers whose downstream keys by `&str` and allocates
272    ///   nothing);
273    /// * owned + empty sentinel → **this method** (HashMap-key seed in
274    ///   an outliving-borrow context, `AllocationRef::name` struct-
275    ///   literal slot — consumers whose downstream requires the owned
276    ///   `String` form because the produced value outlives the source-
277    ///   pool borrow).
278    ///
279    /// A future normalization step (a name-canonicalization pass, a
280    /// case-fold key builder, a per-cluster prefix stripper for cross-
281    /// cluster pool-name aliasing) lands at ONE substrate method here
282    /// and both downstream consumers pick up the upgrade mechanically —
283    /// no per-callsite hand-edit at `reconcile_inner` /
284    /// `AllocationConvergenceCtx::observe`.
285    ///
286    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
287    /// the `.metadata.name.clone().unwrap_or_default()` chain recurred
288    /// at two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
289    /// duplication trigger, and is lifted to ONE owner here).
290    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
291    /// the pins bind the missing-name corner + the empty-string
292    /// sentinel byte-shape + the owned-form `String` return type + the
293    /// byte-identical parity with the pre-lift chain + the fallback-
294    /// value coherence with [`Self::name_or_empty`] +
295    /// [`crate::crd::Process::owned_name_or_empty`] on the metadata-
296    /// slot × empty-sentinel axis, so a regression that drifted any
297    /// surface at `tests::owned_name_or_empty_*` here rather than as
298    /// silent operator-facing skew between the pool-members lookup key
299    /// and the AllocationRef seed on the SAME pool candidate).
300    pub fn owned_name_or_empty(&self) -> String {
301        self.metadata.name.clone().unwrap_or_default()
302    }
303
304    /// Copy-form metadata-projection primitive on the deletion-tombstone
305    /// axis of `EphemeralPool`: returns `true` iff the K8s API server
306    /// has stamped a `metadata.deletionTimestamp` on this pool (the
307    /// moment the object entered the "being deleted" corner of its
308    /// lifecycle, after which further mutating writes are refused and
309    /// finalizers are drained before the object is actually removed) —
310    /// the ONE-liner collapse of the paired
311    /// `self.metadata.deletion_timestamp.is_some()` incantation every
312    /// pool-side consumer restated by hand pre-lift.
313    ///
314    /// Pre-lift the `.metadata.deletion_timestamp.is_some()` chain was
315    /// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
316    /// duplication threshold in `tatara-pool-reconciler`, both
317    /// projecting the SAME tombstone-presence predicate on an
318    /// `EphemeralPool` value:
319    /// * `pool_decide::decide_pool_reconcile` — the pure decision
320    ///   function's deletion-preempt gate that forces
321    ///   [`PoolDecision::Drain`] as soon as the API server stamps
322    ///   the tombstone, before the (desired vs actual) supply-arithmetic
323    ///   branches get a chance to run. Wired at the very top of the
324    ///   decision so a draining pool never spawns / reaps / expires
325    ///   through the normal replenishment arithmetic while the
326    ///   deletion is in flight.
327    /// * `controller_pool::pool_phase_from_members` — the observed-
328    ///   phase composer's tombstone-first arm that returns
329    ///   [`PoolPhase::Draining`] regardless of the supply / demand
330    ///   arithmetic that would otherwise pick `Ready` / `Scaling` /
331    ///   `Degraded`. Keeps the reported phase honest during the
332    ///   finalizer drain so operators reading `kubectl get
333    ///   ephemeralpools` see the tombstone-present state as
334    ///   `Draining`, not as a stale `Ready`.
335    ///
336    /// Both sites walked the SAME `.metadata.deletion_timestamp
337    /// .is_some()` chain and both wanted the `bool` form the primitive
338    /// returns — the `decide_pool_reconcile` site to gate the
339    /// `→ Drain` short-circuit and the `pool_phase_from_members` site
340    /// to gate the `→ Draining` short-circuit. Post-lift each callsite
341    /// reads `pool.is_being_deleted()` and the produced `bool` feeds
342    /// the same downstream short-circuit unchanged.
343    ///
344    /// Sibling to [`crate::crd::Process::is_being_deleted`] on the
345    /// deletion-tombstone axis of the sister CRD — the two primitives
346    /// now partition the tombstone-presence probe across BOTH
347    /// tatara-process CRDs on identical missing-slot semantics
348    /// (present timestamp means "the API server has begun deletion"),
349    /// so an operator or reconciler that switches between the CRD
350    /// surfaces never sees a different tombstone-detection spelling
351    /// as a side effect.
352    ///
353    /// Return-form axis: `bool` matches the copy-form discipline of
354    /// the sibling [`crate::crd::Process::is_being_deleted`] and of
355    /// the pool-side [`crate::phase::ProcessPhase::is_alive`] +
356    /// [`Self::name_or_empty`]-family primitives — the underlying
357    /// slot is a wire-format `Option<Time>` that carries only
358    /// presence information at this axis (the RFC-3339 timestamp
359    /// payload itself is not what the two consumers read; both only
360    /// probe presence to detect the tombstone-stamped state).
361    /// Returning the raw `Option<&Time>` would push the `.is_some()`
362    /// probe back to every callsite, restating the pre-lift chain
363    /// one link shorter without collapsing the primitive.
364    ///
365    /// Peer to [`Self::name_or_empty`] and [`Self::owned_name_or_empty`]
366    /// on the metadata-projection axis for `EphemeralPool`; this method
367    /// opens the presence-probe corner for the tombstone slot. Future
368    /// metadata-presence projections on the pool CRD (an
369    /// `is_being_finalized` projection on
370    /// `metadata.finalizers.is_empty()`'s negation, a `has_owner`
371    /// projection on `metadata.owner_references.is_empty()`'s
372    /// negation) land as peer methods on this same axis.
373    ///
374    /// A future normalization step (a per-tombstone staleness gate
375    /// that returns `false` for a tombstone older than the reconciler's
376    /// grace-period budget, a canonicalization pass that treats a
377    /// tombstone from a paused controller as absent, a cross-cluster
378    /// tombstone-observation clock skew guard) lands at ONE substrate
379    /// method here and both downstream consumers pick up the upgrade
380    /// mechanically — no per-callsite hand-edit at
381    /// `decide_pool_reconcile` / `pool_phase_from_members`.
382    ///
383    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
384    /// the `.metadata.deletion_timestamp.is_some()` chain recurred at
385    /// two hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
386    /// duplication trigger, and is lifted to ONE owner here).
387    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
388    /// the pins bind the missing-tombstone corner + the present-
389    /// tombstone corner + the copy-form `bool` return + the byte-
390    /// identical parity with the pre-lift `.is_some()` chain + the
391    /// cross-CRD coherence with `crate::crd::Process::is_being_deleted`
392    /// on the tombstone axis, so a regression that drifted any surface
393    /// at `tests::is_being_deleted_*` rather than as silent operator-
394    /// facing skew between the pool-reconciler's `→ Drain` decision
395    /// and the observed-phase composer's `→ Draining` report on the
396    /// SAME `EphemeralPool` within one reconcile pass).
397    pub fn is_being_deleted(&self) -> bool {
398        self.metadata.deletion_timestamp.is_some()
399    }
400
401    /// Owned-form metadata-projection primitive on the `metadata.namespace`
402    /// axis of `EphemeralPool`: returns an owned `String` copy of the K8s
403    /// namespace with the missing-namespace corner collapsed to the load-
404    /// bearing empty-string sentinel — the ONE-liner collapse of the
405    /// paired `self.metadata.namespace.clone().unwrap_or_default()`
406    /// incantation every pool-side consumer restated by hand pre-lift.
407    ///
408    /// Pre-lift the `.metadata.namespace.clone().unwrap_or_default()`
409    /// chain was hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE
410    /// ≥ 2 duplication threshold, both stamping the `AllocationRef
411    /// { namespace: String, .. }` slot inside an owned-`String` context:
412    /// * `tatara-pool-reconciler::allocation_decide::AllocationConvergenceCtx
413    ///   ::observe` — the matched-pool seed's `AllocationRef.namespace`
414    ///   slot, right beside the peer [`Self::owned_name_or_empty`] call
415    ///   that owns the paired name half. This is the exact site the
416    ///   pre-existing peer-primitive doc-comment forecast (`"a future
417    ///   run may lift owned_namespace_or_empty as the sibling axis
418    ///   peer"`).
419    /// * `crate::pool::tests::allocation_ref_new_composes_with_owned_name_or_empty_pool_projection`
420    ///   — the composition pin that seeded an `AllocationRef` from the
421    ///   same paired-primitive-half construction the production consumer
422    ///   in `allocation_decide::observe` performs. Post-lift the pin
423    ///   composes two peer primitives (`owned_name_or_empty` +
424    ///   `owned_namespace_or_empty`) rather than one primitive plus the
425    ///   pre-lift chain, sharpening it from a mixed-form composition
426    ///   check into a paired-primitive-family composition check.
427    ///
428    /// Both sites walked the SAME `.clone().unwrap_or_default()` chain
429    /// and both wanted the `String` form the primitive returns — as the
430    /// `String` slot of an `AllocationRef` struct literal built through
431    /// [`crate::pool::AllocationRef::new`]. Post-lift each callsite reads
432    /// `pool.owned_namespace_or_empty()` and the produced value feeds
433    /// the same downstream `AllocationRef` slot unchanged.
434    ///
435    /// The empty-string fallback is the SAME sentinel the sibling owned-
436    /// form primitive [`Self::owned_name_or_empty`] returns on the
437    /// `metadata.name` axis of the same CRD — the two primitives now
438    /// partition the (owned `String` × `metadata.<slot>`) corner of the
439    /// pool CRD's metadata family across BOTH object-coordinate slots
440    /// on identical missing-slot semantics (empty string means "the
441    /// slot is unset"), so the [`crate::pool::AllocationRef::new`]
442    /// composer sees a coherent owned-empty pair regardless of which
443    /// slot is absent on the source pool. Coherent with the workspace-
444    /// wide owned-empty sentinel that the peer primitives
445    /// [`crate::crd::Process::uid_or_empty`],
446    /// [`crate::crd::Process::owned_name_or_empty`],
447    /// [`Self::name_or_empty`], and [`Self::owned_name_or_empty`]
448    /// already share on the metadata-slot × empty-sentinel axis.
449    ///
450    /// Peer to [`Self::owned_name_or_empty`] on the
451    /// (`metadata.name` × `metadata.namespace`) axis of the owned-form
452    /// projection family — closes the corner the pool-side family
453    /// previously left open:
454    ///
455    /// * owned + name + empty sentinel → [`Self::owned_name_or_empty`]
456    ///   (`AllocationRef.name` seed, `HashMap<String, _>` key seed);
457    /// * owned + namespace + empty sentinel → **this method**
458    ///   (`AllocationRef.namespace` seed — the paired half the same
459    ///   `AllocationRef::new(name, namespace)` constructor consumes);
460    /// * copy + deletion + tombstone probe → [`Self::is_being_deleted`]
461    ///   (the presence-probe corner of the same metadata axis, already
462    ///   opened).
463    ///
464    /// A future normalization step (a namespace-canonicalization pass,
465    /// a case-fold key builder, a per-cluster prefix stripper, or the
466    /// canonical-namespace default lift that would substitute
467    /// [`crate::crd::Process::DEFAULT_NAMESPACE`] on the missing-slot
468    /// corner rather than the empty-string sentinel) lands at ONE
469    /// substrate method here and both downstream consumers pick up the
470    /// upgrade mechanically — no per-callsite hand-edit at
471    /// `AllocationConvergenceCtx::observe` / the composition pin.
472    ///
473    /// The empty-string fallback (rather than
474    /// [`crate::crd::Process::DEFAULT_NAMESPACE`]) is DELIBERATELY
475    /// pinned: the sole downstream consumer
476    /// (`AllocationConvergenceCtx::observe`'s matched-pool seed) feeds
477    /// the produced value into `AllocationRef.namespace`, which is then
478    /// matched byte-identically against `spec.pool_ref.namespace` at
479    /// [`crate::pool::allocation_decide::resolve_pool`]-style comparators.
480    /// A silent substitution of `"default"` at this primitive would
481    /// alias every namespace-absent pool to the `"default"` bucket at
482    /// the matcher, hiding the missing-slot corner from an operator
483    /// who explicitly authored an allocation against a namespace-
484    /// unset pool. The load-bearing empty-string sentinel keeps the
485    /// pre-lift `.clone().unwrap_or_default()` shape verbatim so the
486    /// downstream matcher's byte-comparison stays honest.
487    ///
488    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
489    /// the `.metadata.namespace.clone().unwrap_or_default()` chain
490    /// recurred at two hand-authored sites past the ★★ PRIME-DIRECTIVE
491    /// ≥ 2 duplication trigger, and is lifted to ONE owner here).
492    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
493    /// the pins bind the missing-namespace corner + the empty-string
494    /// sentinel byte-shape + the owned-form `String` return type + the
495    /// byte-identical parity with the pre-lift chain + the fallback-
496    /// value coherence with [`Self::owned_name_or_empty`] on the
497    /// paired-slot axis, so a regression that drifted any surface at
498    /// `tests::owned_namespace_or_empty_*` rather than as silent
499    /// operator-facing skew between the paired name / namespace halves
500    /// of the SAME `AllocationRef` seed).
501    pub fn owned_namespace_or_empty(&self) -> String {
502        self.metadata.namespace.clone().unwrap_or_default()
503    }
504
505    /// Compound owned-form metadata-projection primitive on the paired
506    /// `(metadata.uid, metadata.name)` axis of `EphemeralPool`: returns
507    /// a stable owned `String` seed for slot-slug derivation, PREFERRING
508    /// the K8s-assigned uid, FALLING BACK to the pool's own name, then
509    /// SINKING to the load-bearing empty-string sentinel when both slots
510    /// are absent — the ONE-liner collapse of the paired
511    /// `pool.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
512    /// incantation every pool-slot-name-composing consumer restated by
513    /// hand pre-lift.
514    ///
515    /// Pre-lift the `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
516    /// chain was hand-authored at TWO production sites past the ★★
517    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
518    /// `tatara-pool-reconciler::controller_pool`, both feeding the SAME
519    /// `member_process_name(&pool_name, &pool_uid_or_name_fallback, slot)`
520    /// composer:
521    /// * `reconcile_inner` — the desired-count `PoolDecision::Spawn`
522    ///   arm's spawn-loop slot-slug seed (fallback bound as
523    ///   `|| name.clone()` from the extracted-earlier owned `name`
524    ///   half of `owned_coordinates_required()`).
525    /// * `apply_convergence_actions` — the legacy allocation-driven
526    ///   `ConvergenceAction::CreateMember` arm's slot-slug seed
527    ///   (fallback bound as `|| name.to_string()` from the borrowed
528    ///   `name: &str` parameter that the same
529    ///   `owned_coordinates_required()`-extracted `String` was passed
530    ///   through by reference).
531    ///
532    /// Both sites computed the SAME "prefer the k8s uid; fall back to
533    /// the pool's own name" projection on the SAME `EphemeralPool`
534    /// value, differing only in the surface syntax of the fallback
535    /// (`.clone()` vs `.to_string()`) — a per-callsite typing artefact
536    /// of the enclosing scope's `name` binding rather than a semantic
537    /// distinction. Post-lift each callsite reads
538    /// `pool.owned_uid_or_name_or_empty()` and the produced owned
539    /// `String` feeds the same `member_process_name(&name, &_, slot)`
540    /// composer verbatim; the caller no longer threads its own local
541    /// `name` handle through as the fallback, since the primitive
542    /// reaches through the same `self.metadata.name` slot the caller
543    /// extracted from earlier — coherent by construction with the
544    /// sibling primitive [`Self::owned_name_or_empty`] on the missing-
545    /// name corner.
546    ///
547    /// The compound (uid-preferred, name-fallback, empty-sentinel)
548    /// precedence is DELIBERATELY pinned: the K8s API server stamps
549    /// `metadata.uid` on every persisted object at admission time, so
550    /// the reachable state at both callsites (each already gated by
551    /// `owned_coordinates_required()?`) has `uid = Some(_)`. The name
552    /// fallback is a load-bearing safety net for the vanishingly rare
553    /// pre-admission-uid corner + the unit-test path that constructs
554    /// an `EphemeralPool` value in-memory without stamping a uid; the
555    /// empty-string sink is the sentinel-coherent complement of the
556    /// missing-both corner (both slots `None`) so a regression that
557    /// dropped either fallback surfaces as a compiler-visible test
558    /// failure rather than as an operator-facing skew between spawn
559    /// slots derived from mixed-fallback seeds within one reconcile
560    /// pass. Coherent with the workspace-wide owned-empty sentinel
561    /// that the peer primitives [`Self::owned_name_or_empty`],
562    /// [`Self::owned_namespace_or_empty`],
563    /// [`crate::crd::Process::owned_name_or_empty`], and
564    /// [`crate::crd::Process::uid_or_empty`] already share on the
565    /// metadata-slot × empty-sentinel axis.
566    ///
567    /// A future normalization step (a per-cluster uid-prefix stripper,
568    /// a case-fold key builder, canonicalization of a suspiciously-
569    /// empty uid to the name fallback, a namespace-scoped hashing pass
570    /// that mixes cluster identity into the seed) lands at ONE
571    /// substrate method here and both downstream `spawn` /
572    /// `apply_convergence_actions` consumers pick up the upgrade
573    /// mechanically — no per-callsite hand-edit at `controller_pool`.
574    ///
575    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
576    /// the `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
577    /// chain recurred at two hand-authored sites past the ★★ PRIME-
578    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
579    /// here). THEORY.md §II.1 invariant 5 (composition preserves
580    /// proofs — the pins bind the uid-present corner + the uid-absent
581    /// name-fallback corner + the both-absent empty-sentinel corner +
582    /// the owned-form `String` return type + the byte-identical parity
583    /// with each pre-lift callsite's fallback surface, so a regression
584    /// that drifted any surface at `tests::owned_uid_or_name_or_empty_*`
585    /// rather than as silent operator-facing skew between the two
586    /// slot-slug seeds within ONE reconcile pass).
587    pub fn owned_uid_or_name_or_empty(&self) -> String {
588        self.metadata
589            .uid
590            .clone()
591            .unwrap_or_else(|| self.owned_name_or_empty())
592    }
593
594    /// Copy-form metadata-projection primitive on the `metadata.name`
595    /// axis of `EphemeralPool` in its `presence-and-equal` corner:
596    /// returns `true` iff the K8s object name slot is BOTH `Some(_)`
597    /// AND byte-identical to the supplied candidate — the ONE-liner
598    /// collapse of the paired
599    /// `self.metadata.name.as_deref() == Some(candidate)` incantation
600    /// every pool-side lookup consumer restated by hand pre-lift.
601    ///
602    /// Pre-lift the `.metadata.name.as_deref() == Some(<candidate>)`
603    /// chain was hand-authored at TWO production sites past the ★★
604    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
605    /// `tatara-pool-reconciler`, both keyed by the `EphemeralPool`'s
606    /// own name slot inside a `candidate_pools.iter().find(|p| ...)`
607    /// closure that resolves a pool from an `AllocationRef.name` half:
608    /// * `allocation_decide::resolve_pool` — the explicit-`pool_ref`
609    ///   half of the pool-resolution ladder, one of two conjuncts in
610    ///   the `(name == X && namespace == Y)` byte-comparison against
611    ///   `AllocationSpec::pool_ref`. Pairs with the sibling namespace
612    ///   comparison (a future run may lift `has_namespace` as the
613    ///   paired-axis peer once a second namespace-probe site opens).
614    /// * `controller_allocation::reconcile_inner` — the TTL-inheritance
615    ///   fallback path's pool-lookup by `AllocationDecision::Bind::pool
616    ///   .name`, feeding the matched pool's `spec.template.ttl` into
617    ///   the just-bound member Process's lifetime overlay.
618    ///
619    /// Both sites walked the SAME `.as_deref() == Some(<x>.as_str())`
620    /// chain against a `&str` candidate held by an [`AllocationRef`]
621    /// or a similar owned-name handle, and both wanted the `bool`
622    /// form the primitive returns — the transition rule's discriminant
623    /// on either the `find(|p| p.has_name(&pool_ref.name))` closure
624    /// (which either matches ONE candidate pool or none) or the
625    /// TTL-inheritance closure's short-circuit through
626    /// `.map(...).unwrap_or_else(...)`. Post-lift each callsite reads
627    /// `p.has_name(&candidate)` and the produced `bool` feeds the same
628    /// downstream `find` / `map` closure unchanged.
629    ///
630    /// Distinct in semantics from the sibling primitive
631    /// [`Self::name_or_empty`] on the SAME `metadata.name` axis: the
632    /// `_or_empty` family folds the missing-slot corner to the load-
633    /// bearing empty-string sentinel (so `None` and `Some("")` both
634    /// project to `""`), whereas this primitive keeps `None` distinct
635    /// from `Some("")` at the `==` operator — a `None` slot returns
636    /// `false` even when the candidate is the empty string. That
637    /// discipline is load-bearing at both consumer sites: pre-lift
638    /// they compared `Option<&str>` against `Some(<candidate>)`, so a
639    /// substitution through `Self::name_or_empty` would silently
640    /// promote a namespace-absent pool with a `""` candidate into a
641    /// spurious match at the `find` closure, aliasing every unnamed
642    /// pool to the same lookup bucket at the resolver. Preserving the
643    /// `None ⇒ false` corner keeps the resolver's byte-comparison
644    /// honest.
645    ///
646    /// Peer to the sibling substrate primitives already opened on the
647    /// pool-side (`metadata.name` × return-form) axis:
648    /// * borrow-form + empty sentinel → [`Self::name_or_empty`] (`&str`
649    ///   projection with a `""` fallback for missing / explicitly-empty
650    ///   name slots; router tie-break comparator);
651    /// * owned-form + empty sentinel → [`Self::owned_name_or_empty`]
652    ///   (`String` projection with a `""` fallback; `AllocationRef.name`
653    ///   seed);
654    /// * **presence-and-equal probe → this method** (`bool` projection
655    ///   with `None`-preserving semantics; pool-lookup closure
656    ///   discriminant).
657    ///
658    /// A future normalization step (a name-canonicalization pass, a
659    /// case-fold key builder, a per-cluster prefix stripper for cross-
660    /// cluster pool-name aliasing, or a canonical-namespace default
661    /// lift) lands at ONE substrate method here and both downstream
662    /// consumers pick up the upgrade mechanically — no per-callsite
663    /// hand-edit at `resolve_pool` / `controller_allocation
664    /// ::reconcile_inner`.
665    ///
666    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
667    /// the `.metadata.name.as_deref() == Some(<candidate>)` chain
668    /// recurred at two hand-authored sites past the ★★ PRIME-
669    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
670    /// here). THEORY.md §II.1 invariant 5 (composition preserves
671    /// proofs — the pins bind the missing-slot corner (`None ⇒
672    /// false`, even against a `""` candidate) + the populated-slot
673    /// equal corner + the populated-slot unequal corner + the
674    /// byte-identical parity with the pre-lift `.as_deref() == Some
675    /// (<candidate>)` chain + the disjoint semantics vs. the
676    /// `_or_empty` sibling family, so a regression that drifted any
677    /// surface at `tests::has_name_*` here rather than as silent
678    /// operator-facing skew between the two `find` closures the
679    /// primitive owns).
680    #[must_use]
681    pub fn has_name(&self, candidate: &str) -> bool {
682        self.metadata.name.as_deref() == Some(candidate)
683    }
684}
685
686/// What the pool reconciler does when a member reaches `Failed`.
687///
688/// Sibling closed-set lifts on the same `tatara-process` axis:
689/// [`crate::compliance::VerificationPhase::ALL`],
690/// [`crate::signal::SighupStrategy::ALL`],
691/// [`crate::spec::MustReachPhase::ALL`],
692/// [`crate::intent::WorkloadKind::ALL`],
693/// [`crate::export::ReportFormat::ALL`],
694/// [`crate::encapsulates::EncapsulationMode::ALL`],
695/// [`crate::export::ExportTrigger::ALL`],
696/// [`crate::lifetime::TeardownPolicy::ALL`],
697/// [`crate::boundary::ConditionKind::ALL`],
698/// [`crate::lifetime::LifetimeKind::ALL`],
699/// [`crate::intent::IntentKind::ALL`],
700/// [`crate::phase::ProcessPhase::ALL`],
701/// [`crate::signal::ProcessSignal::ALL`].
702#[derive(
703    Clone,
704    Copy,
705    Debug,
706    Default,
707    Serialize,
708    Deserialize,
709    JsonSchema,
710    PartialEq,
711    Eq,
712    Hash,
713    tatara_closed_set::DeriveClosedSet,
714)]
715#[serde(rename_all = "PascalCase")]
716#[closed_set(via = "as_str", generate_unknown, display)]
717pub enum ReplacementPolicy {
718    /// **Default** — Failed member is reaped + replaced immediately
719    /// (pool stays at `desired` count). Most production-like.
720    #[default]
721    ReplaceImmediate,
722    /// Failed member stays for inspection; pool runs short until the
723    /// operator manually reaps it. Useful for debugging.
724    HoldFailed,
725    /// Failed member triggers pool-wide pause: `desired` is
726    /// effectively 0 until the operator manually resumes via a
727    /// pool-status patch. Used for "halt on any failure" workflows.
728    PausePool,
729}
730
731impl ReplacementPolicy {
732    /// The closed set of replacement policies — single source of truth
733    /// that drives the `as_str` / Display / `FromStr` triad and the
734    /// `replaces_failed` / `pauses_on_failure` predicate pair. Adding a
735    /// fourth variant lands at one `ALL` entry + one `as_str` arm + one
736    /// predicate arm per projection — exhaustively checked by the
737    /// compiler (the `[Self; 3]` array literal forces the arity) and by
738    /// the predicate-pair injectivity test below (a new variant must
739    /// land in its own (replaces_failed, pauses_on_failure) bucket or
740    /// the author has to extend the consumer dispatch in
741    /// `tatara-pool-reconciler::desired::PoolConvergence::decide`).
742    pub const ALL: [Self; 3] = [Self::ReplaceImmediate, Self::HoldFailed, Self::PausePool];
743
744    /// Canonical PascalCase wire-format projection — matches the serde
745    /// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
746    /// enumeration the pool reconciler stamps on the
747    /// `ephemeralpools.tatara.pleme.io` schema. Pinned by
748    /// `replacement_policy_as_str_matches_serde` so a variant rename
749    /// can't drift between the typed surface, the CRD enum, the YAML
750    /// wire format AND the operator-facing diagnostic (the
751    /// `desired.rs` Pause reason composes `policy={policy}` via
752    /// Display, not a hard-coded `"PausePool"` literal that would
753    /// silently rot).
754    pub const fn as_str(self) -> &'static str {
755        match self {
756            Self::ReplaceImmediate => "ReplaceImmediate",
757            Self::HoldFailed => "HoldFailed",
758            Self::PausePool => "PausePool",
759        }
760    }
761
762    /// Should the pool auto-spawn a replacement for a Failed member?
763    /// Closed-set match (not `matches!`) so a future variant triggers
764    /// the compiler's exhaustiveness check at this site rather than
765    /// silently defaulting to `false`. Paired with
766    /// `pauses_on_failure` they form the two-axis projection
767    /// consumers in `tatara-pool-reconciler::desired::PoolConvergence`
768    /// pattern-match against — `replaces_failed` true ⇒ emit
769    /// `ReapFailed` per failure; `pauses_on_failure` true with any
770    /// failure ⇒ emit `Pause` and short-circuit. The pair is
771    /// `(true, false) | (false, false) | (false, true)` — pinned
772    /// injective by `replacement_policy_predicate_pair_is_injective`.
773    pub const fn replaces_failed(self) -> bool {
774        match self {
775            Self::ReplaceImmediate => true,
776            Self::HoldFailed | Self::PausePool => false,
777        }
778    }
779
780    /// Should reaching Failed on any member pause the whole pool?
781    /// See `replaces_failed` for the closed-match rationale + the
782    /// predicate-pair contract.
783    pub const fn pauses_on_failure(self) -> bool {
784        match self {
785            Self::PausePool => true,
786            Self::ReplaceImmediate | Self::HoldFailed => false,
787        }
788    }
789}
790
791// `impl FromStr for ReplacementPolicy` + `impl tatara_lisp::ClosedSet for
792// ReplacementPolicy` + `impl fmt::Display for ReplacementPolicy` are
793// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
794// declaration above. `label` delegates to the inherent
795// `ReplacementPolicy::as_str` via `#[closed_set(via = "as_str")]` so the
796// PascalCase wire-format projection stays load-bearing (matches the
797// serde `rename_all = "PascalCase"` output AND the
798// `tatara-pool-reconciler::desired::PoolConvergence` Pause reason
799// emission verbatim) while generic `T: ClosedSet` consumers reach the
800// STABLE workspace-wide name (`label`); Display delegates to the same
801// inherent projection via `#[closed_set(display)]` so the
802// `Pause` reason emitter's `policy={policy}` composition stays
803// pinned on the closed-set algebra rather than on a hand-rolled
804// `fmt::Display` block per implementor.
805
806// `pub struct UnknownReplacementPolicy(pub String)` is generated by
807// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
808// on the enum declaration above. The auto-derived label
809// `"replacement policy"` matches the prior hand-rolled
810// `#[error("unknown replacement policy: {0}")]` verbatim. Symmetric to
811// [`UnknownMemberState`], [`UnknownPoolPhase`], [`UnknownReturnPolicy`],
812// [`crate::export::UnknownReportFormat`],
813// [`crate::export::UnknownChannelKind`],
814// [`crate::export::UnknownExportTrigger`],
815// [`crate::lifetime::UnknownTeardownPolicy`],
816// [`crate::boundary::UnknownConditionKind`], and
817// [`crate::phase::UnknownPhase`].
818
819fn default_free_ttl() -> String {
820    "24h".to_string()
821}
822fn default_max_allocation_ttl() -> String {
823    "4h".to_string()
824}
825
826/// `EphemeralPool.status` — observed pool population state.
827#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
828#[serde(rename_all = "camelCase")]
829pub struct PoolStatus {
830    /// Pool lifecycle phase.
831    #[serde(default)]
832    pub phase: PoolPhase,
833
834    /// When the pool entered the current phase.
835    #[serde(default, skip_serializing_if = "Option::is_none")]
836    pub phase_since: Option<DateTime<Utc>>,
837
838    /// Number of members currently in `Free` state (ready for allocation).
839    #[serde(default)]
840    pub ready_count: u32,
841
842    /// Number of members currently `Allocated`.
843    #[serde(default)]
844    pub allocated_count: u32,
845
846    /// Number of members currently `Spawning` (not yet Attested).
847    #[serde(default)]
848    pub spawning_count: u32,
849
850    /// Number of members currently `Returning` (reset or replace
851    /// in progress).
852    #[serde(default)]
853    pub returning_count: u32,
854
855    /// Member ledger — one entry per pool slot.
856    #[serde(default)]
857    pub members: Vec<PoolMember>,
858
859    /// Operator-visible message (e.g., "scaled down to floor").
860    #[serde(default, skip_serializing_if = "Option::is_none")]
861    pub message: Option<String>,
862
863    /// Standard Kubernetes Conditions.
864    #[serde(default)]
865    pub conditions: Vec<PoolCondition>,
866}
867
868/// One pool slot's state.
869#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
870#[serde(rename_all = "camelCase")]
871pub struct PoolMember {
872    /// `metadata.name` of the backing Process.
873    pub process_name: String,
874    /// Pool member's current slot state.
875    pub state: MemberState,
876    /// When the member entered the current state.
877    pub entered_state_at: DateTime<Utc>,
878    /// If allocated: the AllocationRef holding this slot.
879    #[serde(default, skip_serializing_if = "Option::is_none")]
880    pub allocation_ref: Option<AllocationRef>,
881}
882
883impl PoolStatus {
884    /// Substrate constructor for the observed [`PoolStatus`] seed:
885    /// composes the `(phase, phase_since, ready/allocated/spawning
886    /// /returning counts, members, message, conditions)` 9-slot record
887    /// every pool-reconciler status-patch site restated by hand pre-
888    /// lift. The four counters ride a SINGLE closed-set-driven fold
889    /// over the members list (one pass rather than four independent
890    /// filter-and-count passes); the `message` + `conditions` slots
891    /// stay at their invariant `None` / `vec![]` defaults every pre-
892    /// lift caller stamped verbatim, and `phase_since` is derived from
893    /// the caller-supplied `now` timestamp so the constructor stays
894    /// clock-injectable rather than implicitly reading wall time.
895    ///
896    /// Pre-lift the 11-line
897    /// ```rust,ignore
898    /// PoolStatus {
899    ///     phase,
900    ///     phase_since: Some(Utc::now()),
901    ///     ready_count: count_state(&members, MemberState::Free),
902    ///     allocated_count: count_state(&members, MemberState::Allocated),
903    ///     spawning_count: count_state(&members, MemberState::Spawning),
904    ///     returning_count: count_state(&members, MemberState::Returning),
905    ///     members: members.clone(),
906    ///     message: None,
907    ///     conditions: vec![],
908    /// }
909    /// ```
910    /// incantation was hand-authored at TWO sites past the ★★ PRIME-
911    /// DIRECTIVE ≥ 2 duplication threshold in
912    /// `tatara-pool-reconciler::controller_pool::reconcile_inner`,
913    /// both restating the same 4-slot count fanout + defaults:
914    /// * The `desired > 0` path — status patch after the
915    ///   convergence-action loop when the operator drives the pool
916    ///   through the R11 desired-count invariant.
917    /// * The legacy allocation-driven path (`desired == 0`) — status
918    ///   patch after the [`crate::pool::PoolDecision`] apply loop.
919    ///
920    /// Both sites walked the SAME 4-slot count fanout on the SAME
921    /// four `MemberState` variants (Free/Allocated/Spawning/Returning)
922    /// and stamped the SAME defaults (`message: None`, `conditions:
923    /// vec![]`), even though the four counters walked the members list
924    /// four independent times pre-lift when a single pass suffices.
925    /// Post-lift both callers write
926    /// `PoolStatus::observed(phase, members, Utc::now())` and share
927    /// ONE substrate owner; a future counter slot (e.g., a
928    /// `warming_count` for a `MemberState::Warming` variant between
929    /// Spawning and Free) plugs into the fold at ONE match arm and
930    /// both status-patch sites inherit the new slot mechanically.
931    ///
932    /// The `Failed` variant is deliberately absent from the fold — no
933    /// `PoolStatus` slot counts failed members (they surface via
934    /// `pool_phase_from_members`'s `PoolPhase::Degraded` transition
935    /// instead), and the closed-set match on
936    /// [`MemberState`] pins that a future variant which SHOULD count
937    /// toward one of the four buckets triggers the compiler's
938    /// exhaustiveness check at this fold rather than silently sinking
939    /// into `Failed`'s no-op arm.
940    ///
941    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
942    /// the 11-line status-seed incantation recurred at two hand-
943    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
944    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
945    /// invariant 5 (composition preserves proofs — the pins bind the
946    /// 4-slot count fanout + the closed-set exhaustiveness on
947    /// `MemberState` + the invariant defaults, so a regression that
948    /// dropped a counter slot or swapped a variant surfaces at
949    /// `tests::pool_status_observed_*` rather than as silent operator-
950    /// facing skew between the two status-patch sites on the SAME
951    /// pool).
952    #[must_use]
953    pub fn observed(phase: PoolPhase, members: Vec<PoolMember>, now: DateTime<Utc>) -> Self {
954        let (ready_count, allocated_count, spawning_count, returning_count) =
955            PoolMember::state_count_fanout(&members);
956        Self {
957            phase,
958            phase_since: Some(now),
959            ready_count,
960            allocated_count,
961            spawning_count,
962            returning_count,
963            members,
964            message: None,
965            conditions: vec![],
966        }
967    }
968}
969
970impl PoolMember {
971    /// Substrate primitive: single-pass closed-set fold over a
972    /// `[PoolMember]` slice producing the `(ready, allocated,
973    /// spawning, returning)` 4-tuple every `PoolStatus` seed stamps at
974    /// its four counter slots. The `Failed` arm is a no-op (no
975    /// `PoolStatus` counter tracks failed members — they surface via
976    /// [`PoolPhase::Degraded`] instead), pinned by the closed-set
977    /// match so a future variant that SHOULD count toward one of the
978    /// four buckets triggers the compiler's exhaustiveness check here
979    /// rather than silently falling through.
980    ///
981    /// Consumed by [`PoolStatus::observed`]. A caller that needs a
982    /// single per-variant count outside the status-seed fanout should
983    /// keep spelling `members.iter().filter(...).count()` rather than
984    /// walking this 4-tuple — the fanout is shaped for the
985    /// `PoolStatus` fill, not for arbitrary per-variant queries.
986    #[must_use]
987    pub fn state_count_fanout(members: &[Self]) -> (u32, u32, u32, u32) {
988        let mut ready = 0u32;
989        let mut allocated = 0u32;
990        let mut spawning = 0u32;
991        let mut returning = 0u32;
992        for m in members {
993            match m.state {
994                MemberState::Free => ready += 1,
995                MemberState::Allocated => allocated += 1,
996                MemberState::Spawning => spawning += 1,
997                MemberState::Returning => returning += 1,
998                MemberState::Failed => {}
999            }
1000        }
1001        (ready, allocated, spawning, returning)
1002    }
1003
1004    /// Substrate primitive: single-pass closed-set collection of the
1005    /// `process_name` axis over a `[PoolMember]` slice into an owned
1006    /// `HashSet<String>` — the O(1)-lookup shape every spawn-arm on
1007    /// the workspace builds pre-collision-check against a candidate
1008    /// [`crate::pool::PoolMember::process_name`] produced by
1009    /// [`tatara-pool-reconciler::naming::member_process_name`].
1010    ///
1011    /// Pre-lift the 2-line
1012    /// `members.iter().map(|m| m.process_name.clone()).collect()`
1013    /// chain was hand-authored at TWO sites past the ★★ PRIME-
1014    /// DIRECTIVE ≥ 2 duplication threshold in
1015    /// `tatara-pool-reconciler::controller_pool`, both restating the
1016    /// SAME `process_name` projection through the SAME
1017    /// `iter → map → collect` shape and both feeding a `.contains
1018    /// (&candidate)` probe:
1019    /// * `reconcile_inner`'s legacy allocation-driven
1020    ///   `PoolDecision::Spawn` arm (`desired == 0` path) —
1021    ///   collision-set for
1022    ///   `member_process_name(&pool_name, &pool_uid, slot)` per spawn
1023    ///   slot.
1024    /// * `apply_convergence_actions` — collision-set for the SAME
1025    ///   composer inside the R11 desired-count
1026    ///   `ConvergenceAction::CreateMember` loop.
1027    ///
1028    /// Post-lift both consumers share ONE substrate owner; the
1029    /// composed `HashSet<String>` still feeds the same
1030    /// `HashSet::<String>::contains(&candidate)` probe at each
1031    /// callsite unchanged. A future normalization step on the
1032    /// occupied-name axis (case-fold before insertion, a per-cluster
1033    /// prefix strip, deduplication against a sibling stale-name
1034    /// registry, exclusion of `Returning`/`Failed` members that no
1035    /// longer own their slot) lands at ONE substrate method rather
1036    /// than being restated at each callsite.
1037    ///
1038    /// Sibling to [`Self::state_count_fanout`] on the `(collection
1039    /// shape × slice-owned fold)` axis: both primitives fold a
1040    /// `[PoolMember]` slice into one caller-shaped aggregate in a
1041    /// single pass, both are `#[must_use]`, both take the slice by
1042    /// reference so no caller has to reshape its `Vec<PoolMember>` or
1043    /// `Vec<PoolMember>` slice upstream.
1044    ///
1045    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1046    /// the `HashSet<String>` collision-set shape recurred at TWO
1047    /// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
1048    /// duplication trigger, and is lifted to ONE owner here).
1049    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
1050    /// the pins bind the axis (`process_name`), the aggregate shape
1051    /// (`HashSet<String>`), the empty-slice corner, and the
1052    /// duplicate-name deduplication semantics `HashSet` provides
1053    /// implicitly, so a regression at any of those surfaces at
1054    /// `tests::process_names_set_*` rather than as silent occupied-
1055    /// slot skew at either spawn arm).
1056    #[must_use]
1057    pub fn process_names_set(members: &[Self]) -> std::collections::HashSet<String> {
1058        members.iter().map(|m| m.process_name.clone()).collect()
1059    }
1060}
1061
1062/// Light reference to an `EphemeralAllocation`.
1063#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
1064#[serde(rename_all = "camelCase")]
1065pub struct AllocationRef {
1066    pub name: String,
1067    pub namespace: String,
1068}
1069
1070impl AllocationRef {
1071    /// Substrate constructor for [`AllocationRef`]: composes the
1072    /// `(name, namespace)` pair through ONE `impl Into<String>`-gated
1073    /// entry point — the ONE-liner collapse of the paired
1074    /// `AllocationRef { name: n.into(), namespace: ns.into() }`
1075    /// struct-literal incantation every downstream consumer restated
1076    /// by hand pre-lift.
1077    ///
1078    /// Pre-lift the `AllocationRef { name, namespace }` struct-literal
1079    /// was hand-authored at FOUR production sites past the ★★ PRIME-
1080    /// DIRECTIVE ≥ 2 duplication threshold across the workspace, all
1081    /// composing an owned `(name: String, namespace: String)` pair
1082    /// under one of two roles:
1083    /// * `tatara-pool-reconciler::controller_allocation::reconcile_inner`
1084    ///   Bind path — the `assignedProcess` status slot's ref, pairing
1085    ///   the just-bound member Process name with the allocation's
1086    ///   containing namespace.
1087    /// * `tatara-pool-reconciler::controller_allocation::reconcile_inner`
1088    ///   Release path — the same `assignedProcess` slot shape, stamped
1089    ///   at the release-side status patch alongside the (unchanged)
1090    ///   `boundPool` ref.
1091    /// * `tatara-pool-reconciler::allocation_decide::AllocationConvergenceCtx::observe`
1092    ///   pool-matched handle — the `matched_pool` slot's ref, pairing
1093    ///   [`EphemeralPool::owned_name_or_empty`] with the pool's
1094    ///   containing namespace.
1095    /// * `tatara-github-watcher::allocation_factory::allocation_from_pr`
1096    ///   — the `pool_ref` slot on the `AllocationSpec` emitted from a
1097    ///   PullRequestEvent, pairing the operator-configured pool name
1098    ///   with the watcher's target namespace.
1099    ///
1100    /// All FOUR sites walked the SAME two-field struct-literal shape
1101    /// — an owned name half, an owned namespace half — differing only
1102    /// in provenance. Post-lift each callsite reads
1103    /// `AllocationRef::new(name, ns)` and the produced value feeds the
1104    /// same downstream slot (`assignedProcess` / `bound_pool` /
1105    /// `matched_pool` / `spec.pool_ref`) unchanged. The `impl Into<String>`
1106    /// signature accepts every provenance the pre-lift sites carried —
1107    /// owned `String` (the reconciler's owned-form projections), `&str`
1108    /// (the factory's `n.to_string()` / `namespace.to_string()`
1109    /// borrow-to-owned promotions), `Cow<str>`, and every other
1110    /// `Into<String>` implementor — so no callsite has to change its
1111    /// upstream provenance to route through the primitive.
1112    ///
1113    /// Return-form axis: owned [`AllocationRef`] — the wire-format
1114    /// shape [`crate::pool::AllocationRef`]'s serde `rename_all =
1115    /// "camelCase"` produces on both spec (`poolRef`) and status
1116    /// (`boundPool` / `assignedProcess`) slots. The primitive owns
1117    /// the axis-order `(name, namespace)` — the same order the four
1118    /// consumers spelled — so a slot swap surfaces at the
1119    /// `allocation_ref_new_positional_axis_order` pin below rather
1120    /// than as silent `<namespace>/<name>` inversion downstream.
1121    ///
1122    /// Peer to the sibling substrate primitives already opened on the
1123    /// pool-side (name, namespace) axis pair:
1124    /// [`EphemeralPool::name_or_empty`] (borrow-form name),
1125    /// [`EphemeralPool::owned_name_or_empty`] (owned-form name); this
1126    /// constructor is the composer that folds the owned-form projections
1127    /// into the wire-format ref shape.
1128    ///
1129    /// A future refactor of [`AllocationRef`]'s field set (a
1130    /// `resource_kind: String` field for cross-CRD refs, an
1131    /// `api_version: String` field for FQN references, a
1132    /// canonicalization pass over the namespace half, a non-empty-name
1133    /// gate) lands at ONE substrate constructor site here and every
1134    /// downstream consumer inherits the upgrade mechanically — no per-
1135    /// callsite hand-edit at the FOUR reconciler + factory sites.
1136    ///
1137    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1138    /// the `AllocationRef { name, namespace }` struct-literal shape
1139    /// recurred at four hand-authored sites past the ★★ PRIME-
1140    /// DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE owner
1141    /// here). THEORY.md §II.1 invariant 5 (composition preserves
1142    /// proofs — the pins bind the positional axis-order + the
1143    /// `Into<String>` provenance closure + byte-identical parity with
1144    /// the pre-lift struct-literal + `PartialEq` coherence with the
1145    /// hand-authored form, so a regression that reshaped any surface
1146    /// at `tests::allocation_ref_new_*` rather than as silent
1147    /// operator-facing skew between the assignedProcess / bound_pool
1148    /// / matched_pool / spec.pool_ref slots on the SAME allocation).
1149    #[must_use]
1150    pub fn new(name: impl Into<String>, namespace: impl Into<String>) -> Self {
1151        Self {
1152            name: name.into(),
1153            namespace: namespace.into(),
1154        }
1155    }
1156}
1157
1158/// Per-slot state in the pool's free list.
1159///
1160/// Sibling closed-sets on the `EphemeralPool` axis: [`ReplacementPolicy::ALL`]
1161/// (the on-failure policy that the pool reconciler dispatches against
1162/// the [`Self::is_failed`] projection), [`ReturnPolicy::ALL`] (the
1163/// release-time disposition that transitions an [`Self::Allocated`]
1164/// member into [`Self::Returning`] before it either re-enters
1165/// [`Self::Free`] or gets [`Self::Spawning`]'d as a fresh slot).
1166#[derive(
1167    Clone,
1168    Copy,
1169    Debug,
1170    PartialEq,
1171    Eq,
1172    Hash,
1173    Serialize,
1174    Deserialize,
1175    JsonSchema,
1176    tatara_closed_set::DeriveClosedSet,
1177)]
1178#[serde(rename_all = "PascalCase")]
1179#[closed_set(via = "as_str", generate_unknown, display)]
1180pub enum MemberState {
1181    /// Pool reconciler is creating/converging the backing Process.
1182    Spawning,
1183    /// Process is `Attested`; ready for allocation.
1184    Free,
1185    /// Held by an `EphemeralAllocation`.
1186    Allocated,
1187    /// Return policy is being applied (Reset → reset Job; Replace →
1188    /// Process is being torn down and recreated).
1189    Returning,
1190    /// Permanent failure — the member needs operator attention.
1191    Failed,
1192}
1193
1194impl MemberState {
1195    /// The closed set of member states — single source of truth that
1196    /// drives the `as_str` / Display / `FromStr` triad AND the
1197    /// `is_failed` / `counts_toward_supply` predicate pair. Adding a
1198    /// sixth variant lands at one `ALL` entry + one `as_str` arm + one
1199    /// arm per predicate — exhaustively checked by the compiler (the
1200    /// `[Self; 5]` array literal forces the arity) and by the
1201    /// per-variant truth-table contract test (a new variant must
1202    /// declare its own `(is_failed, counts_toward_supply)` projection
1203    /// or the consumer dispatch in
1204    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
1205    /// and `tatara-pool-reconciler::pool_decide::decide_pool_reconcile`
1206    /// will silently bucket it into the wrong lifecycle column).
1207    pub const ALL: [Self; 5] = [
1208        Self::Spawning,
1209        Self::Free,
1210        Self::Allocated,
1211        Self::Returning,
1212        Self::Failed,
1213    ];
1214
1215    /// Canonical PascalCase wire-format projection — matches the serde
1216    /// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
1217    /// enumeration that `ephemeralpools.tatara.pleme.io` stamps on
1218    /// `status.members[].state`. Pinned by
1219    /// `member_state_as_str_matches_serde` so a variant rename can't
1220    /// drift between the typed surface, the CRD enum, the YAML wire
1221    /// format AND any future operator-facing diagnostic that composes
1222    /// `state={state}` via Display rather than a hard-coded literal
1223    /// that would silently rot.
1224    pub const fn as_str(self) -> &'static str {
1225        match self {
1226            Self::Spawning => "Spawning",
1227            Self::Free => "Free",
1228            Self::Allocated => "Allocated",
1229            Self::Returning => "Returning",
1230            Self::Failed => "Failed",
1231        }
1232    }
1233
1234    /// Is this member in a permanent-failure state — needs operator
1235    /// attention? Closed-set match (not `matches!`) so a future variant
1236    /// triggers the compiler's exhaustiveness check at this site rather
1237    /// than silently defaulting to `false`. Consumed by
1238    /// `tatara-pool-reconciler::pool_decide::decide_pool_reconcile` to
1239    /// gate the highest-priority `ReplaceMembers` decision branch — a
1240    /// future variant that should also trigger replacement (e.g.
1241    /// `MemberState::Quarantined`) flips this predicate at one site
1242    /// and inherits the priority-1 dispatch without touching the
1243    /// consumer match arm.
1244    pub const fn is_failed(self) -> bool {
1245        match self {
1246            Self::Failed => true,
1247            Self::Spawning | Self::Free | Self::Allocated | Self::Returning => false,
1248        }
1249    }
1250
1251    /// Does this member contribute to the pool's *available supply*
1252    /// (current ready slots + slots coming online)? Closed-set match so
1253    /// a future variant triggers the compiler's exhaustiveness check.
1254    /// Consumed by
1255    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
1256    /// — the `(free + spawning)` supply calc collapses into one
1257    /// predicate-driven filter, so a future "warming-up" state
1258    /// (`MemberState::Warming` between Spawning and Free) plugs into
1259    /// the supply count at one site rather than three. Disjoint with
1260    /// `is_failed` — pinned by `member_state_failed_implies_no_supply`
1261    /// (a Failed member can never count toward supply; the pool
1262    /// reconciler would otherwise double-count failures as available
1263    /// capacity).
1264    pub const fn counts_toward_supply(self) -> bool {
1265        match self {
1266            Self::Free | Self::Spawning => true,
1267            Self::Allocated | Self::Returning | Self::Failed => false,
1268        }
1269    }
1270}
1271
1272// `impl FromStr for MemberState` + `impl tatara_lisp::ClosedSet for
1273// MemberState` + `impl fmt::Display for MemberState` are generated by
1274// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
1275// above. `label` delegates to the inherent `MemberState::as_str` via
1276// `#[closed_set(via = "as_str")]` so the
1277// `pool_phase_from_members` supply calc can keep keying on
1278// `counts_toward_supply` against the typed variant while a generic
1279// `T: ClosedSet` consumer reaches the STABLE workspace-wide name
1280// (`label`) without knowing this enum lives in `tatara-process::pool`;
1281// Display delegates to the same inherent projection via
1282// `#[closed_set(display)]` so the diagnostic emitter's
1283// `state={state}` composition stays pinned on the closed-set algebra.
1284
1285// `pub struct UnknownMemberState(pub String)` is generated by
1286// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1287// on the enum declaration above. The auto-derived label `"member state"`
1288// matches the prior hand-rolled `#[error("unknown member state: {0}")]`
1289// verbatim. Symmetric to [`UnknownReplacementPolicy`],
1290// [`UnknownPoolPhase`], [`UnknownReturnPolicy`],
1291// [`crate::lifetime::UnknownTeardownPolicy`],
1292// [`crate::boundary::UnknownConditionKind`], and
1293// [`crate::phase::UnknownPhase`].
1294
1295/// Pool lifecycle phase (observed across the whole pool population).
1296///
1297/// Sibling closed-set on the same `EphemeralPool` axis as
1298/// [`MemberState::ALL`] (the per-slot lifecycle this phase aggregates
1299/// over via [`MemberState::counts_toward_supply`]),
1300/// [`ReplacementPolicy::ALL`] (on-failure policy) and
1301/// [`ReturnPolicy::ALL`] (release-time disposition). Together with
1302/// `MemberState`, this closes the pool reconciler's
1303/// `(slot-state, pool-phase)` two-tier observation algebra on the
1304/// same closed-set discipline as the rest of `tatara-process`.
1305#[derive(
1306    Clone,
1307    Copy,
1308    Debug,
1309    PartialEq,
1310    Eq,
1311    Hash,
1312    Serialize,
1313    Deserialize,
1314    JsonSchema,
1315    tatara_closed_set::DeriveClosedSet,
1316)]
1317#[serde(rename_all = "PascalCase")]
1318#[closed_set(via = "as_str", generate_unknown, display)]
1319pub enum PoolPhase {
1320    /// Just admitted; no members yet.
1321    Initializing,
1322    /// `ready_count == desired_size`.
1323    Steady,
1324    /// `ready_count + spawning_count < desired_size` and reconciler
1325    /// is creating new members.
1326    ScalingUp,
1327    /// `ready_count > desired_size` and reconciler is reaping excess.
1328    ScalingDown,
1329    /// `min_size` constraint violated.
1330    Degraded,
1331    /// Pool is being deleted; reconciler is reaping all members.
1332    Draining,
1333}
1334
1335impl Default for PoolPhase {
1336    fn default() -> Self {
1337        Self::Initializing
1338    }
1339}
1340
1341impl PoolPhase {
1342    /// The closed set of pool phases — single source of truth that
1343    /// drives the `as_str` / Display / `FromStr` triad AND the
1344    /// `is_steady` / `is_terminal` predicate pair. Adding a seventh
1345    /// variant lands at one `ALL` entry + one `as_str` arm + one arm
1346    /// per predicate — exhaustively checked by the compiler (the
1347    /// `[Self; 6]` array literal forces the arity) AND by the
1348    /// per-variant truth-table contract test (a new variant must
1349    /// declare its own `(is_steady, is_terminal)` projection or any
1350    /// future status-aggregator surface — `feira pool list
1351    /// --healthy`, the operator-facing condition aggregator, the
1352    /// desired-loop heartbeat short-circuit — will silently bucket
1353    /// it into the wrong lifecycle column).
1354    pub const ALL: [Self; 6] = [
1355        Self::Initializing,
1356        Self::Steady,
1357        Self::ScalingUp,
1358        Self::ScalingDown,
1359        Self::Degraded,
1360        Self::Draining,
1361    ];
1362
1363    /// Canonical PascalCase wire-format projection — matches the
1364    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
1365    /// `enum:` enumeration that `ephemeralpools.tatara.pleme.io`
1366    /// stamps on `status.phase`. Pinned by
1367    /// `pool_phase_as_str_matches_serde` so a variant rename can't
1368    /// drift between the typed surface, the CRD enum, the YAML wire
1369    /// format AND any future operator-facing diagnostic that
1370    /// composes `phase={phase}` via Display rather than a hard-coded
1371    /// literal that would silently rot. Display + FromStr triad
1372    /// over `ALL` mirrors `MemberState` / `ReplacementPolicy` /
1373    /// `ReturnPolicy` / `AllocationPhase` / `TeardownPolicy` /
1374    /// `ConditionKind` / `ProcessPhase` / `ProcessSignal`.
1375    pub const fn as_str(self) -> &'static str {
1376        match self {
1377            Self::Initializing => "Initializing",
1378            Self::Steady => "Steady",
1379            Self::ScalingUp => "ScalingUp",
1380            Self::ScalingDown => "ScalingDown",
1381            Self::Degraded => "Degraded",
1382            Self::Draining => "Draining",
1383        }
1384    }
1385
1386    /// Is the pool fully converged — supply matches desired, no
1387    /// reconciler-driven population change pending? Closed-set match
1388    /// (not `matches!`) so a future variant triggers the compiler's
1389    /// exhaustiveness check at this site rather than silently
1390    /// defaulting to `false`. Paired with `is_terminal` they form
1391    /// the two-axis projection that future status aggregators
1392    /// (operator-facing fleet health, `feira pool list --healthy`,
1393    /// the SSE filter "show non-steady pools") dispatch against —
1394    /// `is_steady && !is_terminal` ⇒ converged (goal state);
1395    /// `!is_steady && is_terminal` ⇒ being deleted (no future
1396    /// spawn); `!is_steady && !is_terminal` ⇒ transient
1397    /// (Initializing | ScalingUp | ScalingDown | Degraded — pool
1398    /// is in motion toward desired). The impossible bucket
1399    /// `(true, true)` — a draining pool that's somehow also steady
1400    /// — is pinned empty by `pool_phase_steady_excludes_terminal`.
1401    pub const fn is_steady(self) -> bool {
1402        match self {
1403            Self::Steady => true,
1404            Self::Initializing
1405            | Self::ScalingUp
1406            | Self::ScalingDown
1407            | Self::Degraded
1408            | Self::Draining => false,
1409        }
1410    }
1411
1412    /// Is the pool in its absorbing exit state — deletion-stamped,
1413    /// reconciler is reaping every member, no spawn will ever
1414    /// happen again? Closed-set match so a future variant triggers
1415    /// the compiler's exhaustiveness check. See `is_steady` for the
1416    /// predicate-pair contract + bucket definitions.
1417    pub const fn is_terminal(self) -> bool {
1418        match self {
1419            Self::Draining => true,
1420            Self::Initializing
1421            | Self::Steady
1422            | Self::ScalingUp
1423            | Self::ScalingDown
1424            | Self::Degraded => false,
1425        }
1426    }
1427}
1428
1429// `impl FromStr for PoolPhase` + `impl tatara_lisp::ClosedSet for PoolPhase`
1430// + `impl fmt::Display for PoolPhase` are generated by
1431// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration above.
1432// `label` delegates to the inherent `PoolPhase::as_str` via
1433// `#[closed_set(via = "as_str")]` so the operator-facing
1434// `phase={phase}` Display composition keeps reading the same canonical
1435// PascalCase projection while a generic `T: ClosedSet` consumer (a
1436// status-aggregator filter, the `feira pool list --healthy` predicate, a
1437// future SSE event router) can walk every variant without knowing the
1438// closed set lives in `tatara-process::pool`; Display delegates to the
1439// same inherent projection via `#[closed_set(display)]` so the
1440// `phase={phase}` composition stays pinned on the closed-set algebra
1441// rather than a hand-rolled `fmt::Display` block.
1442
1443// `pub struct UnknownPoolPhase(pub String)` is generated by
1444// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1445// on the enum declaration above. The auto-derived label `"pool phase"`
1446// matches the prior hand-rolled `#[error("unknown pool phase: {0}")]`
1447// verbatim. Symmetric to [`UnknownMemberState`],
1448// [`UnknownReplacementPolicy`], [`UnknownReturnPolicy`],
1449// [`crate::lifetime::UnknownTeardownPolicy`],
1450// [`crate::boundary::UnknownConditionKind`], and
1451// [`crate::phase::UnknownPhase`].
1452
1453/// Standard K8s Condition shape (kept local so tatara-process doesn't
1454/// depend on k8s_openapi types in its public schema).
1455#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1456#[serde(rename_all = "camelCase")]
1457pub struct PoolCondition {
1458    pub type_: String,
1459    pub status: String,
1460    pub reason: String,
1461    pub message: String,
1462    pub last_transition_time: DateTime<Utc>,
1463}
1464
1465/// What the pool does when an allocation releases a member.
1466///
1467/// Sibling closed-set on the `EphemeralPool` axis:
1468/// [`ReplacementPolicy::ALL`]. Sibling closed-sets on the
1469/// `tatara-process` algebra: [`crate::lifetime::TeardownPolicy::ALL`]
1470/// (the *release*-time counterpart for non-pooled ephemeral envs),
1471/// [`crate::boundary::ConditionKind::ALL`],
1472/// [`crate::lifetime::LifetimeKind::ALL`],
1473/// [`crate::intent::IntentKind::ALL`],
1474/// [`crate::phase::ProcessPhase::ALL`],
1475/// [`crate::signal::ProcessSignal::ALL`].
1476#[derive(
1477    Clone,
1478    Copy,
1479    Debug,
1480    Hash,
1481    PartialEq,
1482    Eq,
1483    Serialize,
1484    Deserialize,
1485    JsonSchema,
1486    Default,
1487    tatara_closed_set::DeriveClosedSet,
1488)]
1489#[serde(rename_all = "PascalCase")]
1490#[closed_set(via = "as_str", generate_unknown, display)]
1491pub enum ReturnPolicy {
1492    /// Tear down the Process + create a fresh one. Safe but slow
1493    /// (1-2 min spin-up before the slot is Free again).
1494    #[default]
1495    Replace,
1496    /// Keep the Process running; run a typed `:reset` Job that wipes
1497    /// state (DB drop, secrets rotate). Fast (~5-10s) but depends on
1498    /// the reset Job being correct for the workload. API-authoritative
1499    /// systems are natural fits because the control API owns all state.
1500    Reset,
1501    /// Keep the Process indefinitely after release (debugging aid;
1502    /// operator must `feira pool reap NAME` to clean up). Useful for
1503    /// post-mortem of a flaky test.
1504    Keep,
1505}
1506
1507impl ReturnPolicy {
1508    /// The closed set of return policies — single source of truth that
1509    /// drives the `as_str` / Display / `FromStr` triad and the
1510    /// `keeps_process` / `runs_reset_job` predicate pair. Adding a
1511    /// fourth variant lands at one `ALL` entry + one `as_str` arm +
1512    /// one arm per predicate — exhaustively checked by the compiler
1513    /// (the `[Self; 3]` array literal forces the arity) and by the
1514    /// predicate-pair injectivity test (a new variant must land in
1515    /// its own (keeps_process, runs_reset_job) bucket or the author
1516    /// has to extend the consumer dispatch in
1517    /// `tatara-pool-reconciler::return_policy::plan_return`).
1518    pub const ALL: [Self; 3] = [Self::Replace, Self::Reset, Self::Keep];
1519
1520    /// Canonical PascalCase wire-format projection — matches the
1521    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
1522    /// `enum:` enumeration the pool reconciler stamps on the
1523    /// `ephemeralpools.tatara.pleme.io` schema. Pinned by
1524    /// `return_policy_as_str_matches_serde` so a variant rename can't
1525    /// drift between the typed surface, the CRD enum, the YAML wire
1526    /// format AND any future operator-facing diagnostic that composes
1527    /// `policy={policy}` via Display rather than a hard-coded literal.
1528    pub const fn as_str(self) -> &'static str {
1529        match self {
1530            Self::Replace => "Replace",
1531            Self::Reset => "Reset",
1532            Self::Keep => "Keep",
1533        }
1534    }
1535
1536    /// Does the pool keep the backing Process alive across release?
1537    /// Closed-set match (not `matches!`) so a future variant triggers
1538    /// the compiler's exhaustiveness check at this site rather than
1539    /// silently defaulting to `false`. Paired with `runs_reset_job`
1540    /// they form the two-axis projection that the consumer in
1541    /// `tatara-pool-reconciler::return_policy::plan_return` matches
1542    /// against — `keeps_process` false ⇒ `DeleteAndRespawn`;
1543    /// `keeps_process && runs_reset_job` ⇒ `ResetThenFree`;
1544    /// `keeps_process && !runs_reset_job` ⇒ `KeepForInspection`. The
1545    /// pair is `(false, false) | (true, true) | (true, false)` —
1546    /// pinned injective by
1547    /// `return_policy_predicate_pair_is_injective`.
1548    pub const fn keeps_process(self) -> bool {
1549        match self {
1550            Self::Replace => false,
1551            Self::Reset | Self::Keep => true,
1552        }
1553    }
1554
1555    /// Does the policy run a typed `:reset` Job to wipe state in
1556    /// place? See `keeps_process` for the closed-match rationale +
1557    /// the predicate-pair contract.
1558    pub const fn runs_reset_job(self) -> bool {
1559        match self {
1560            Self::Reset => true,
1561            Self::Replace | Self::Keep => false,
1562        }
1563    }
1564}
1565
1566// `impl FromStr for ReturnPolicy` + `impl tatara_lisp::ClosedSet for
1567// ReturnPolicy` + `impl fmt::Display for ReturnPolicy` are generated by
1568// `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum declaration
1569// above. `label` delegates to the inherent `ReturnPolicy::as_str` via
1570// `#[closed_set(via = "as_str")]` so the
1571// `tatara-pool-reconciler::return_policy::plan_return` dispatch keeps
1572// reading the canonical PascalCase projection that matches the CRD
1573// `enum:` literal verbatim, while a generic `T: ClosedSet` consumer
1574// plugs in without knowing the enum lives in `tatara-process::pool`;
1575// Display delegates to the same inherent projection via
1576// `#[closed_set(display)]` so the `policy={policy}` diagnostic
1577// composition stays pinned on the closed-set algebra.
1578
1579// `pub struct UnknownReturnPolicy(pub String)` is generated by
1580// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
1581// on the enum declaration above. The auto-derived label `"return policy"`
1582// matches the prior hand-rolled `#[error("unknown return policy: {0}")]`
1583// verbatim. Symmetric to [`UnknownReplacementPolicy`],
1584// [`UnknownMemberState`], [`UnknownPoolPhase`],
1585// [`crate::lifetime::UnknownTeardownPolicy`],
1586// [`crate::boundary::UnknownConditionKind`], and
1587// [`crate::phase::UnknownPhase`].
1588
1589/// Routing selector — matches an `EphemeralAllocation`'s requestor
1590/// against pool-eligibility predicates.
1591#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
1592#[serde(rename_all = "camelCase")]
1593pub struct PoolSelector {
1594    /// Glob-matched against `EphemeralAllocation.spec.requestor.repo`.
1595    /// Empty = match every repo.
1596    #[serde(default)]
1597    pub repos: Vec<String>,
1598
1599    /// Glob-matched against `EphemeralAllocation.spec.requestor.branch`.
1600    /// Empty = match every branch.
1601    #[serde(default)]
1602    pub branches: Vec<String>,
1603
1604    /// PR labels (all-must-match, AND semantics). Empty = no label
1605    /// requirement.
1606    #[serde(default)]
1607    pub pr_labels: Vec<String>,
1608
1609    /// Allocation `kind` strings this pool can serve (e.g., "github-pr",
1610    /// "manual", "ci-run"). Empty = any kind.
1611    #[serde(default)]
1612    pub kinds: Vec<String>,
1613}
1614
1615impl PoolSelector {
1616    /// Does this selector match the given allocation routing key?
1617    /// Pure: no side effects.
1618    pub fn matches(&self, key: &MatchKey<'_>) -> bool {
1619        glob_any(&self.repos, key.repo)
1620            && glob_any(&self.branches, key.branch)
1621            && labels_subset(&self.pr_labels, key.pr_labels)
1622            && kind_any(&self.kinds, key.kind)
1623    }
1624
1625    /// Specificity score — higher = more specific. Used by the
1626    /// reconciler to break ties between selectors that all match.
1627    pub fn specificity(&self) -> u32 {
1628        let mut score = 0;
1629        if !self.repos.is_empty() {
1630            score += 8;
1631        }
1632        if !self.branches.is_empty() {
1633            score += 4;
1634        }
1635        score += (self.pr_labels.len() as u32) * 2;
1636        if !self.kinds.is_empty() {
1637            score += 1;
1638        }
1639        score
1640    }
1641}
1642
1643/// Allocation routing key — what the reconciler matches against pool selectors.
1644#[derive(Clone, Copy, Debug)]
1645pub struct MatchKey<'a> {
1646    pub repo: &'a str,
1647    pub branch: &'a str,
1648    pub pr_labels: &'a [String],
1649    pub kind: &'a str,
1650}
1651
1652fn glob_any(patterns: &[String], value: &str) -> bool {
1653    if patterns.is_empty() {
1654        return true;
1655    }
1656    patterns.iter().any(|p| glob_match(p, value))
1657}
1658
1659fn kind_any(kinds: &[String], value: &str) -> bool {
1660    if kinds.is_empty() {
1661        return true;
1662    }
1663    kinds.iter().any(|k| k == value)
1664}
1665
1666fn labels_subset(required: &[String], present: &[String]) -> bool {
1667    required.iter().all(|r| present.iter().any(|p| p == r))
1668}
1669
1670/// Minimal glob: supports trailing `*` only (e.g., `"pleme-io/*"`,
1671/// `"release-*"`). Sufficient for repo/branch routing. Empty pattern
1672/// matches anything.
1673fn glob_match(pattern: &str, value: &str) -> bool {
1674    if pattern.is_empty() {
1675        return true;
1676    }
1677    if let Some(prefix) = pattern.strip_suffix('*') {
1678        value.starts_with(prefix)
1679    } else {
1680        pattern == value
1681    }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686    use super::*;
1687    // The closed-set tests below call `T::from_str(bad)` via the
1688    // derive-generated `FromStr` impls — bring the trait into scope at
1689    // the test module so the lib body doesn't carry an otherwise-unused
1690    // `use std::str::FromStr;` at the file head.
1691    use std::str::FromStr;
1692
1693    #[test]
1694    fn glob_trailing_star_matches_prefix() {
1695        assert!(glob_match("pleme-io/*", "pleme-io/demo-app"));
1696        assert!(!glob_match("pleme-io/*", "drzln/dotfiles"));
1697        assert!(glob_match("release-*", "release-2026-05"));
1698        assert!(!glob_match("release-*", "main"));
1699        assert!(glob_match("main", "main"));
1700        assert!(!glob_match("main", "develop"));
1701    }
1702
1703    #[test]
1704    fn empty_selector_matches_anything() {
1705        let s = PoolSelector::default();
1706        assert!(s.matches(&MatchKey {
1707            repo: "any/repo",
1708            branch: "any-branch",
1709            pr_labels: &[],
1710            kind: "any",
1711        }));
1712    }
1713
1714    #[test]
1715    fn repo_glob_filters_match_key() {
1716        let s = PoolSelector {
1717            repos: vec!["pleme-io/demo-*".into()],
1718            ..Default::default()
1719        };
1720        assert!(s.matches(&MatchKey {
1721            repo: "pleme-io/demo-app",
1722            branch: "x",
1723            pr_labels: &[],
1724            kind: "y",
1725        }));
1726        assert!(!s.matches(&MatchKey {
1727            repo: "pleme-io/other-repo",
1728            branch: "x",
1729            pr_labels: &[],
1730            kind: "y",
1731        }));
1732    }
1733
1734    #[test]
1735    fn pr_labels_require_all() {
1736        let s = PoolSelector {
1737            pr_labels: vec!["needs-ephemeral".into(), "integration".into()],
1738            ..Default::default()
1739        };
1740        // Both labels present → match.
1741        assert!(s.matches(&MatchKey {
1742            repo: "x",
1743            branch: "y",
1744            pr_labels: &[
1745                "needs-ephemeral".into(),
1746                "integration".into(),
1747                "extra".into()
1748            ],
1749            kind: "z",
1750        }));
1751        // One label missing → no match.
1752        assert!(!s.matches(&MatchKey {
1753            repo: "x",
1754            branch: "y",
1755            pr_labels: &["needs-ephemeral".into()],
1756            kind: "z",
1757        }));
1758    }
1759
1760    #[test]
1761    fn specificity_ranks_more_constrained_higher() {
1762        let general = PoolSelector::default();
1763        let specific = PoolSelector {
1764            repos: vec!["pleme-io/*".into()],
1765            branches: vec!["main".into()],
1766            pr_labels: vec!["needs-ephemeral".into()],
1767            kinds: vec!["github-pr".into()],
1768        };
1769        assert!(specific.specificity() > general.specificity());
1770    }
1771
1772    #[test]
1773    fn return_policy_defaults_to_replace() {
1774        assert_eq!(ReturnPolicy::default(), ReturnPolicy::Replace);
1775    }
1776
1777    #[test]
1778    fn pool_phase_defaults_to_initializing() {
1779        assert_eq!(PoolPhase::default(), PoolPhase::Initializing);
1780    }
1781
1782    // ── closed-set algebra contracts for ReplacementPolicy
1783    //    (ALL × as_str × FromStr × predicate-pair) ────────────────────
1784
1785    /// Structural well-formedness of [`ReplacementPolicy`] as a
1786    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1787    /// testkit lift that pins all three structural invariants (`ALL`
1788    /// is non-empty, every variant round-trips through
1789    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1790    /// outside the closed set) at ONE call site. Replaces the hand-
1791    /// derived `replacement_policy_all_is_unique_and_complete` +
1792    /// `replacement_policy_roundtrip_via_as_str` + the empty-input arm
1793    /// of `unknown_replacement_policy_errors`. `FromStr` delegates to
1794    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1795    /// exercises the same code path the pool reconciler hits when
1796    /// parsing a CRD `enum:`-validated value back to the typed policy.
1797    #[test]
1798    fn replacement_policy_is_well_formed_closed_set() {
1799        tatara_closed_set::assert_closed_set_well_formed::<ReplacementPolicy>();
1800    }
1801
1802    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1803    /// output verbatim for every variant. A future variant rename (or
1804    /// an `as_str` arm typo) lands here at one site, instead of
1805    /// drifting between the typed surface, the CRD enum, and the
1806    /// YAML wire format.
1807    #[test]
1808    fn replacement_policy_as_str_matches_serde() {
1809        crate::tagged_union::assert_label_matches_serde_serialization::<ReplacementPolicy>();
1810    }
1811
1812    /// The Display impl IS `as_str` — pinning this lets future callers
1813    /// reach for either projection without drift. The operator-facing
1814    /// "policy={policy}" diagnostic in `tatara-pool-reconciler::desired`
1815    /// composes through Display rather than through a hard-coded
1816    /// variant string.
1817    #[test]
1818    fn replacement_policy_display_matches_as_str() {
1819        crate::tagged_union::assert_display_matches_label::<ReplacementPolicy>();
1820    }
1821
1822    /// `FromStr` rejects strings that aren't in the canonical
1823    /// projection — lowercased / typo / cross-axis-leaked — and the
1824    /// error echoes the input verbatim so the operator-facing
1825    /// diagnostic carries the offending value, not a normalized form.
1826    /// The empty-input arm is pinned by
1827    /// [`replacement_policy_is_well_formed_closed_set`] via the
1828    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1829    /// verbatim-echo contract on the [`UnknownReplacementPolicy`]
1830    /// newtype, which the trait's `make_unknown` can't see.
1831    #[test]
1832    fn unknown_replacement_policy_errors() {
1833        for bad in [
1834            "replaceimmediate",
1835            "PAUSEPOOL",
1836            "Replace-Immediate",
1837            "hold_failed",
1838            "Pause",
1839            "Reset",
1840        ] {
1841            let err = ReplacementPolicy::from_str(bad).unwrap_err();
1842            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1843        }
1844    }
1845
1846    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
1847    /// documented per-variant on-failure behavior.
1848    #[test]
1849    fn replacement_policy_predicate_truth_tables() {
1850        assert!(ReplacementPolicy::ReplaceImmediate.replaces_failed());
1851        assert!(!ReplacementPolicy::ReplaceImmediate.pauses_on_failure());
1852
1853        assert!(!ReplacementPolicy::HoldFailed.replaces_failed());
1854        assert!(!ReplacementPolicy::HoldFailed.pauses_on_failure());
1855
1856        assert!(!ReplacementPolicy::PausePool.replaces_failed());
1857        assert!(ReplacementPolicy::PausePool.pauses_on_failure());
1858    }
1859
1860    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
1861    /// predicates simultaneously — the two on-failure actions
1862    /// (reap-each-failed vs pause-whole-pool) are mutually exclusive.
1863    /// A future `ReplacementPolicy::PauseAndReap` that returned true
1864    /// from both would FAIL here, forcing the author to either pick
1865    /// one bucket or extend the consumer dispatch site in
1866    /// `tatara-pool-reconciler::desired::PoolConvergence::decide`
1867    /// deliberately rather than silently double-firing both branches.
1868    #[test]
1869    fn replacement_policy_predicates_are_disjoint() {
1870        for policy in ReplacementPolicy::ALL {
1871            assert!(
1872                !(policy.replaces_failed() && policy.pauses_on_failure()),
1873                "{policy:?} returns true from both replaces_failed and pauses_on_failure",
1874            );
1875        }
1876    }
1877
1878    /// INJECTIVITY CONTRACT: the pair `(replaces_failed,
1879    /// pauses_on_failure)` is injective across `ALL`. Each variant
1880    /// projects to its own `(bool, bool)` bucket: `(true, false)` =
1881    /// reap; `(false, false)` = hold; `(false, true)` = pause. Pairing
1882    /// this with the disjointness contract above forces a future
1883    /// variant to land in a fresh `(replaces_failed,
1884    /// pauses_on_failure)` bucket — or the author extends the consumer
1885    /// dispatch in `tatara-pool-reconciler::desired::PoolConvergence`
1886    /// to recognize the new projection bucket.
1887    #[test]
1888    fn replacement_policy_predicate_pair_is_injective() {
1889        let projections: Vec<(bool, bool)> = ReplacementPolicy::ALL
1890            .into_iter()
1891            .map(|p| (p.replaces_failed(), p.pauses_on_failure()))
1892            .collect();
1893        let unique: std::collections::HashSet<_> = projections.iter().copied().collect();
1894        assert_eq!(
1895            projections.len(),
1896            unique.len(),
1897            "predicate pair projection is not injective: {projections:?}",
1898        );
1899    }
1900
1901    /// DEFAULT-AGREEMENT CONTRACT: `ReplacementPolicy::default()`
1902    /// returns the variant tagged `#[default]` in the enum, AND that
1903    /// variant reaps (the production-safe behavior). A future #[default]
1904    /// rename without flipping the predicates fails here.
1905    #[test]
1906    fn replacement_policy_default_replaces_failed() {
1907        let d = ReplacementPolicy::default();
1908        assert_eq!(d, ReplacementPolicy::ReplaceImmediate);
1909        assert!(d.replaces_failed());
1910        assert!(!d.pauses_on_failure());
1911    }
1912
1913    #[test]
1914    fn kinds_filter_to_known_set() {
1915        let s = PoolSelector {
1916            kinds: vec!["github-pr".into(), "manual".into()],
1917            ..Default::default()
1918        };
1919        assert!(s.matches(&MatchKey {
1920            repo: "x",
1921            branch: "y",
1922            pr_labels: &[],
1923            kind: "github-pr",
1924        }));
1925        assert!(!s.matches(&MatchKey {
1926            repo: "x",
1927            branch: "y",
1928            pr_labels: &[],
1929            kind: "scheduled",
1930        }));
1931    }
1932
1933    // ── closed-set algebra contracts for ReturnPolicy
1934    //    (ALL × as_str × FromStr × predicate-pair) ────────────────────
1935
1936    /// Structural well-formedness of [`ReturnPolicy`] as a
1937    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
1938    /// symmetric to [`replacement_policy_is_well_formed_closed_set`]
1939    /// above.
1940    #[test]
1941    fn return_policy_is_well_formed_closed_set() {
1942        tatara_closed_set::assert_closed_set_well_formed::<ReturnPolicy>();
1943    }
1944
1945    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1946    /// output verbatim for every variant. A future variant rename (or
1947    /// an `as_str` arm typo) lands here at one site, instead of
1948    /// drifting between the typed surface, the CRD enum, and the
1949    /// YAML wire format.
1950    #[test]
1951    fn return_policy_as_str_matches_serde() {
1952        crate::tagged_union::assert_label_matches_serde_serialization::<ReturnPolicy>();
1953    }
1954
1955    /// The Display impl IS `as_str` — pinning this lets future callers
1956    /// reach for either projection without drift, mirroring the
1957    /// `ReplacementPolicy` discipline.
1958    #[test]
1959    fn return_policy_display_matches_as_str() {
1960        crate::tagged_union::assert_display_matches_label::<ReturnPolicy>();
1961    }
1962
1963    /// `FromStr` rejects strings that aren't in the canonical
1964    /// projection — lowercased / typo / cross-axis-leaked — and the
1965    /// error echoes the input verbatim so the operator-facing
1966    /// diagnostic carries the offending value, not a normalized form.
1967    /// The empty-input arm is pinned by
1968    /// [`return_policy_is_well_formed_closed_set`] via the
1969    /// `tatara_lisp::ClosedSet` testkit.
1970    #[test]
1971    fn unknown_return_policy_errors() {
1972        for bad in [
1973            "replace",
1974            "RESET",
1975            "Re-place",
1976            "keep_for_inspection",
1977            "DeleteAndRespawn",
1978            "ReplaceImmediate",
1979        ] {
1980            let err = ReturnPolicy::from_str(bad).unwrap_err();
1981            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1982        }
1983    }
1984
1985    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
1986    /// documented per-variant on-release behavior.
1987    #[test]
1988    fn return_policy_predicate_truth_tables() {
1989        assert!(!ReturnPolicy::Replace.keeps_process());
1990        assert!(!ReturnPolicy::Replace.runs_reset_job());
1991
1992        assert!(ReturnPolicy::Reset.keeps_process());
1993        assert!(ReturnPolicy::Reset.runs_reset_job());
1994
1995        assert!(ReturnPolicy::Keep.keeps_process());
1996        assert!(!ReturnPolicy::Keep.runs_reset_job());
1997    }
1998
1999    /// IMPLICATION CONTRACT: `runs_reset_job` implies `keeps_process`.
2000    /// You cannot run a typed `:reset` Job against a Process you've
2001    /// just deleted; the impossible bucket `(false, true)` must stay
2002    /// empty. A future variant returning true from `runs_reset_job`
2003    /// while returning false from `keeps_process` fails here, which
2004    /// forces the author to either flip `keeps_process` to true or
2005    /// extend the consumer dispatch site in
2006    /// `tatara-pool-reconciler::return_policy::plan_return`
2007    /// deliberately rather than letting an impossible state slip in.
2008    #[test]
2009    fn return_policy_reset_implies_keeps_process() {
2010        for policy in ReturnPolicy::ALL {
2011            if policy.runs_reset_job() {
2012                assert!(
2013                    policy.keeps_process(),
2014                    "{policy:?} runs a reset job but does not keep the process",
2015                );
2016            }
2017        }
2018    }
2019
2020    /// INJECTIVITY CONTRACT: the pair `(keeps_process, runs_reset_job)`
2021    /// is injective across `ALL`. Each variant projects to its own
2022    /// `(bool, bool)` bucket: `(false, false)` = delete + respawn;
2023    /// `(true, true)` = reset-in-place; `(true, false)` = keep for
2024    /// inspection. Pairing this with the implication contract above
2025    /// forces a future variant to land in a fresh
2026    /// `(keeps_process, runs_reset_job)` bucket — or the author
2027    /// extends the consumer dispatch in
2028    /// `tatara-pool-reconciler::return_policy::plan_return` to
2029    /// recognize the new projection bucket.
2030    #[test]
2031    fn return_policy_predicate_pair_is_injective() {
2032        let projections: Vec<(bool, bool)> = ReturnPolicy::ALL
2033            .into_iter()
2034            .map(|p| (p.keeps_process(), p.runs_reset_job()))
2035            .collect();
2036        let unique: std::collections::HashSet<_> = projections.iter().copied().collect();
2037        assert_eq!(
2038            projections.len(),
2039            unique.len(),
2040            "predicate pair projection is not injective: {projections:?}",
2041        );
2042    }
2043
2044    /// DEFAULT-AGREEMENT CONTRACT: `ReturnPolicy::default()` returns
2045    /// the variant tagged `#[default]` in the enum, AND that variant
2046    /// is the safe "tear down + respawn" behavior — neither keeps the
2047    /// process nor runs a reset Job. A future `#[default]` rename
2048    /// without flipping the predicates fails here.
2049    #[test]
2050    fn return_policy_default_is_replace_and_neither_predicate_fires() {
2051        let d = ReturnPolicy::default();
2052        assert_eq!(d, ReturnPolicy::Replace);
2053        assert!(!d.keeps_process());
2054        assert!(!d.runs_reset_job());
2055    }
2056
2057    // ── closed-set algebra contracts for MemberState
2058    //    (ALL × as_str × FromStr × predicate pair) ────────────────────
2059
2060    /// Structural well-formedness of [`MemberState`] as a
2061    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
2062    /// symmetric to [`replacement_policy_is_well_formed_closed_set`]
2063    /// and [`return_policy_is_well_formed_closed_set`] above.
2064    #[test]
2065    fn member_state_is_well_formed_closed_set() {
2066        tatara_closed_set::assert_closed_set_well_formed::<MemberState>();
2067    }
2068
2069    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2070    /// output verbatim for every variant. A future variant rename (or
2071    /// an `as_str` arm typo) lands here at one site, instead of
2072    /// drifting between the typed surface, the CRD enum, and the YAML
2073    /// wire format the pool reconciler stamps on
2074    /// `status.members[].state`.
2075    #[test]
2076    fn member_state_as_str_matches_serde() {
2077        crate::tagged_union::assert_label_matches_serde_serialization::<MemberState>();
2078    }
2079
2080    /// The Display impl IS `as_str` — pinning this lets future callers
2081    /// reach for either projection without drift. Any operator-facing
2082    /// "state={state}" diagnostic that composes through Display
2083    /// inherits the canonical wire-format string automatically.
2084    #[test]
2085    fn member_state_display_matches_as_str() {
2086        crate::tagged_union::assert_display_matches_label::<MemberState>();
2087    }
2088
2089    /// `FromStr` rejects strings that aren't in the canonical
2090    /// projection — lowercased / typo / cross-axis-leaked — and
2091    /// the error echoes the input verbatim so the operator-facing
2092    /// diagnostic carries the offending value, not a normalized form.
2093    /// The empty-input arm is pinned by
2094    /// [`member_state_is_well_formed_closed_set`] via the
2095    /// `tatara_lisp::ClosedSet` testkit. The cross-axis leak cases
2096    /// pin the closed-set REJECTION contract that the trait can't see:
2097    /// `"ReplaceImmediate"`, `"Reset"`, and `"Attested"` are valid
2098    /// labels for sibling enums (`ReplacementPolicy`, `ReturnPolicy`,
2099    /// `ProcessPhase`) but MUST reject here, because the codomains
2100    /// are disjoint.
2101    #[test]
2102    fn unknown_member_state_errors() {
2103        for bad in [
2104            "free",
2105            "SPAWNING",
2106            "Free-State",
2107            "allocated_now",
2108            "ReplaceImmediate", // ReplacementPolicy-axis leak
2109            "Reset",            // ReturnPolicy-axis leak
2110            "Attested",         // ProcessPhase-axis leak
2111        ] {
2112            let err = MemberState::from_str(bad).unwrap_err();
2113            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2114        }
2115    }
2116
2117    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2118    /// documented per-variant lifecycle role. The pool reconciler's
2119    /// `pool_phase_from_members` supply calc collapses
2120    /// `count_state(Free) + count_state(Spawning)` into one
2121    /// `counts_toward_supply` filter; this table pins the per-variant
2122    /// projection that consumer depends on.
2123    #[test]
2124    fn member_state_predicate_truth_tables() {
2125        assert!(!MemberState::Spawning.is_failed());
2126        assert!(MemberState::Spawning.counts_toward_supply());
2127
2128        assert!(!MemberState::Free.is_failed());
2129        assert!(MemberState::Free.counts_toward_supply());
2130
2131        assert!(!MemberState::Allocated.is_failed());
2132        assert!(!MemberState::Allocated.counts_toward_supply());
2133
2134        assert!(!MemberState::Returning.is_failed());
2135        assert!(!MemberState::Returning.counts_toward_supply());
2136
2137        assert!(MemberState::Failed.is_failed());
2138        assert!(!MemberState::Failed.counts_toward_supply());
2139    }
2140
2141    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
2142    /// `is_failed` and `counts_toward_supply` simultaneously — a
2143    /// failed member can never be counted as available capacity. A
2144    /// future variant that returned true from both would FAIL here,
2145    /// forcing the author to either drop it from supply, or extend
2146    /// the consumer's bucketing in
2147    /// `tatara-pool-reconciler::controller_pool::pool_phase_from_members`
2148    /// deliberately rather than silently inflating the pool's supply
2149    /// count with failed slots.
2150    #[test]
2151    fn member_state_failed_implies_no_supply() {
2152        for state in MemberState::ALL {
2153            assert!(
2154                !(state.is_failed() && state.counts_toward_supply()),
2155                "{state:?} returns true from both is_failed and counts_toward_supply — \
2156                 a failed member can never be counted as available pool capacity",
2157            );
2158        }
2159    }
2160
2161    /// COVERAGE CONTRACT: every variant lands somewhere — either
2162    /// in supply, or as a failed slot, or as an in-use bucket
2163    /// (`Allocated | Returning`). A future variant that returns
2164    /// `false` from `counts_toward_supply` AND `false` from
2165    /// `is_failed` is fine *iff* it represents an in-use slot; this
2166    /// test pins the existing variants in their declared buckets so
2167    /// the consumer-side dispatch in
2168    /// `tatara-pool-reconciler::pool_decide::decide_pool_reconcile`
2169    /// stays grounded.
2170    #[test]
2171    fn member_state_buckets_cover_every_variant() {
2172        let mut supply = 0u32;
2173        let mut failed = 0u32;
2174        let mut in_use = 0u32;
2175        for state in MemberState::ALL {
2176            match (state.is_failed(), state.counts_toward_supply()) {
2177                (true, false) => failed += 1,
2178                (false, true) => supply += 1,
2179                (false, false) => in_use += 1,
2180                (true, true) => panic!("disjointness already pins this empty for {state:?}"),
2181            }
2182        }
2183        assert_eq!(supply, 2, "supply bucket: Free + Spawning");
2184        assert_eq!(failed, 1, "failed bucket: Failed");
2185        assert_eq!(in_use, 2, "in-use bucket: Allocated + Returning");
2186        assert_eq!(supply + failed + in_use, MemberState::ALL.len() as u32);
2187    }
2188
2189    // ── closed-set algebra contracts for PoolPhase
2190    //    (ALL × as_str × FromStr × predicate pair) ────────────────────
2191
2192    /// Structural well-formedness of [`PoolPhase`] as a
2193    /// [`tatara_lisp::ClosedSet`] implementor — testkit lift
2194    /// symmetric to [`member_state_is_well_formed_closed_set`] above.
2195    #[test]
2196    fn pool_phase_is_well_formed_closed_set() {
2197        tatara_closed_set::assert_closed_set_well_formed::<PoolPhase>();
2198    }
2199
2200    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2201    /// output verbatim for every variant. A future variant rename (or
2202    /// an `as_str` arm typo) lands here at one site, instead of
2203    /// drifting between the typed surface, the CRD enum, and the YAML
2204    /// wire format the pool reconciler stamps on `status.phase`.
2205    #[test]
2206    fn pool_phase_as_str_matches_serde() {
2207        crate::tagged_union::assert_label_matches_serde_serialization::<PoolPhase>();
2208    }
2209
2210    /// The Display impl IS `as_str` — pinning this lets future callers
2211    /// reach for either projection without drift. Any operator-facing
2212    /// "phase={phase}" diagnostic that composes through Display
2213    /// inherits the canonical wire-format string automatically.
2214    #[test]
2215    fn pool_phase_display_matches_as_str() {
2216        crate::tagged_union::assert_display_matches_label::<PoolPhase>();
2217    }
2218
2219    /// `FromStr` rejects strings that aren't in the canonical
2220    /// projection — lowercased / typo / cross-axis-leaked — and
2221    /// the error echoes the input verbatim so the operator-facing
2222    /// diagnostic carries the offending value, not a normalized form.
2223    /// The empty-input arm is pinned by
2224    /// [`pool_phase_is_well_formed_closed_set`] via the
2225    /// `tatara_lisp::ClosedSet` testkit. The cross-axis leak cases
2226    /// (`"Free"`, `"Replace"`, `"Attested"`, `"HoldFailed"`) pin the
2227    /// closed-set REJECTION contract that the trait can't see — those
2228    /// are valid sibling-axis labels but MUST reject here.
2229    #[test]
2230    fn unknown_pool_phase_errors() {
2231        for bad in [
2232            "steady",
2233            "SCALINGUP",
2234            "Scaling-Up",
2235            "scaling_down",
2236            "Free",       // MemberState-axis leak
2237            "Replace",    // ReturnPolicy-axis leak
2238            "Attested",   // ProcessPhase-axis leak
2239            "HoldFailed", // ReplacementPolicy-axis leak
2240        ] {
2241            let err = PoolPhase::from_str(bad).unwrap_err();
2242            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2243        }
2244    }
2245
2246    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
2247    /// documented per-variant lifecycle role. Pinning this table at
2248    /// one site means any future status-aggregator surface
2249    /// (`feira pool list --healthy`, the SSE filter, the desired-loop
2250    /// heartbeat short-circuit) reads the same projection that the
2251    /// reconciler writes.
2252    #[test]
2253    fn pool_phase_predicate_truth_tables() {
2254        assert!(!PoolPhase::Initializing.is_steady());
2255        assert!(!PoolPhase::Initializing.is_terminal());
2256
2257        assert!(PoolPhase::Steady.is_steady());
2258        assert!(!PoolPhase::Steady.is_terminal());
2259
2260        assert!(!PoolPhase::ScalingUp.is_steady());
2261        assert!(!PoolPhase::ScalingUp.is_terminal());
2262
2263        assert!(!PoolPhase::ScalingDown.is_steady());
2264        assert!(!PoolPhase::ScalingDown.is_terminal());
2265
2266        assert!(!PoolPhase::Degraded.is_steady());
2267        assert!(!PoolPhase::Degraded.is_terminal());
2268
2269        assert!(!PoolPhase::Draining.is_steady());
2270        assert!(PoolPhase::Draining.is_terminal());
2271    }
2272
2273    /// DISJOINTNESS CONTRACT: no variant returns true from BOTH
2274    /// `is_steady` and `is_terminal` simultaneously — a draining pool
2275    /// is by definition transitioning OUT, not the goal converged
2276    /// state. A future variant that returned true from both would
2277    /// FAIL here, forcing the author to either pick one bucket or
2278    /// extend the consumer dispatch sites (status aggregators,
2279    /// heartbeat short-circuit) deliberately rather than silently
2280    /// double-firing both branches.
2281    #[test]
2282    fn pool_phase_steady_excludes_terminal() {
2283        for phase in PoolPhase::ALL {
2284            assert!(
2285                !(phase.is_steady() && phase.is_terminal()),
2286                "{phase:?} returns true from both is_steady and is_terminal — \
2287                 a draining pool is by definition not the converged goal state",
2288            );
2289        }
2290    }
2291
2292    /// COVERAGE CONTRACT: every variant lands somewhere — either the
2293    /// converged goal (`Steady`), the absorbing exit (`Draining`),
2294    /// or the transient bucket (`Initializing | ScalingUp |
2295    /// ScalingDown | Degraded` — pool is in motion toward desired).
2296    /// A future variant that returns `false` from BOTH predicates is
2297    /// fine *iff* it represents an in-motion state; this test pins
2298    /// the existing variants in their declared buckets so the
2299    /// projection consumers stay grounded.
2300    #[test]
2301    fn pool_phase_buckets_cover_every_variant() {
2302        let mut converged = 0u32;
2303        let mut terminal = 0u32;
2304        let mut transient = 0u32;
2305        for phase in PoolPhase::ALL {
2306            match (phase.is_steady(), phase.is_terminal()) {
2307                (true, false) => converged += 1,
2308                (false, true) => terminal += 1,
2309                (false, false) => transient += 1,
2310                (true, true) => panic!("disjointness already pins this empty for {phase:?}"),
2311            }
2312        }
2313        assert_eq!(converged, 1, "converged bucket: Steady");
2314        assert_eq!(terminal, 1, "terminal bucket: Draining");
2315        assert_eq!(
2316            transient, 4,
2317            "transient bucket: Initializing + ScalingUp + ScalingDown + Degraded"
2318        );
2319        assert_eq!(
2320            converged + terminal + transient,
2321            PoolPhase::ALL.len() as u32
2322        );
2323    }
2324
2325    /// DEFAULT-AGREEMENT CONTRACT: `PoolPhase::default()` returns the
2326    /// variant a freshly-admitted pool should land in — `Initializing`
2327    /// — AND that variant is neither steady (no members yet) nor
2328    /// terminal (not deletion-stamped). A future `Default` rename
2329    /// without flipping the predicates fails here.
2330    #[test]
2331    fn pool_phase_default_is_initializing_in_transient_bucket() {
2332        let d = PoolPhase::default();
2333        assert_eq!(d, PoolPhase::Initializing);
2334        assert!(!d.is_steady());
2335        assert!(!d.is_terminal());
2336    }
2337
2338    // ─────────────────────────────────────────────────────────────────
2339    // `EphemeralPool::name_or_empty` — borrow-form metadata-projection
2340    // primitive on the `metadata.name` axis. Pins the missing-slot
2341    // corner, the populated-slot corner, the pre-lift chain-shape
2342    // parity, and the pure-projection discipline that the two
2343    // `tatara-pool-reconciler` consumers routed onto the primitive
2344    // depend on. See the primitive's doc-comment for the full
2345    // migration rationale.
2346    // ─────────────────────────────────────────────────────────────────
2347
2348    fn empty_template() -> EphemeralSpec {
2349        EphemeralSpec {
2350            aplicacao: crate::intent::AplicacaoIntent {
2351                chart_ref: "oci://x".into(),
2352                version: "1".into(),
2353                profile: String::new(),
2354                values_overlay: serde_json::Value::Null,
2355                release_name: None,
2356                target_namespace: None,
2357                install_timeout: None,
2358            },
2359            ttl: "1h".into(),
2360            teardown: crate::lifetime::TeardownPolicy::Always,
2361            max_concurrent: 0,
2362            postconditions: vec![],
2363            preconditions: vec![],
2364            verify_timeout: None,
2365            classification: None,
2366            parent: None,
2367            exports: vec![],
2368            routing: None,
2369        }
2370    }
2371
2372    fn pool_spec() -> PoolSpec {
2373        PoolSpec {
2374            desired_size: 1,
2375            min_size: 0,
2376            max_size: 0,
2377            return_policy: ReturnPolicy::Replace,
2378            selector: PoolSelector::default(),
2379            template: empty_template(),
2380            free_ttl: "24h".into(),
2381            max_allocation_ttl: "4h".into(),
2382            desired: 0,
2383            replacement_policy: ReplacementPolicy::default(),
2384            stable_name_claim: false,
2385        }
2386    }
2387
2388    fn pool_named(name: &str) -> EphemeralPool {
2389        EphemeralPool::new(name, pool_spec())
2390    }
2391
2392    fn pool_unnamed() -> EphemeralPool {
2393        let mut p = EphemeralPool::new("scratch", pool_spec());
2394        p.metadata.name = None;
2395        p
2396    }
2397
2398    #[test]
2399    fn name_or_empty_returns_empty_string_when_metadata_name_is_none() {
2400        let p = pool_unnamed();
2401        assert!(p.metadata.name.is_none(), "fixture invariant");
2402        assert_eq!(p.name_or_empty(), "");
2403    }
2404
2405    #[test]
2406    fn name_or_empty_returns_populated_slot_verbatim() {
2407        let p = pool_named("attest-pool");
2408        assert_eq!(p.name_or_empty(), "attest-pool");
2409    }
2410
2411    #[test]
2412    fn name_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
2413        // Corner between `None` (missing slot) and `Some(String::new())`
2414        // (populated slot containing the empty string): the primitive
2415        // MUST fold both to the same `""` byte-shape so a downstream
2416        // `HashMap<String,_>::get(name)` / `str::cmp` sees ONE
2417        // "unnamed pool" bucket regardless of which shape the K8s API
2418        // server materialized. This is byte-identical to what the
2419        // pre-lift `.as_deref().unwrap_or("")` chain produced.
2420        let mut p = pool_named("scratch");
2421        p.metadata.name = Some(String::new());
2422        assert_eq!(p.name_or_empty(), "");
2423    }
2424
2425    #[test]
2426    fn name_or_empty_is_a_pure_projection() {
2427        // Consecutive calls return byte-identical slices — no cached
2428        // state, no mutation on the `EphemeralPool` between calls.
2429        // Guards against a future refactor that plants a cache field
2430        // and drifts one caller from another silently.
2431        let p = pool_named("router-pool");
2432        assert_eq!(p.name_or_empty(), p.name_or_empty());
2433        assert_eq!(p.name_or_empty(), "router-pool");
2434        assert_eq!(p.name_or_empty(), "router-pool");
2435    }
2436
2437    #[test]
2438    fn name_or_empty_matches_pre_lift_chain_verbatim() {
2439        // Byte-identical parity with the two hand-authored
2440        // `.metadata.name.as_deref().unwrap_or("")` chains the
2441        // primitive replaces in `tatara-pool-reconciler::router` and
2442        // `tatara-pool-reconciler::controller_allocation`. Runs across
2443        // the FULL corner set of the metadata.name slot: absent,
2444        // present-with-value, present-with-empty-string.
2445        let cases: [(Option<String>, &str); 3] = [
2446            (None, ""),
2447            (Some("attest-pool".into()), "attest-pool"),
2448            (Some(String::new()), ""),
2449        ];
2450        for (slot, expected) in cases {
2451            let mut p = pool_named("scratch");
2452            p.metadata.name = slot.clone();
2453            let pre_lift = p.metadata.name.as_deref().unwrap_or("");
2454            assert_eq!(pre_lift, expected, "pre-lift chain sanity");
2455            assert_eq!(p.name_or_empty(), pre_lift);
2456            assert_eq!(p.name_or_empty(), expected);
2457        }
2458    }
2459
2460    #[test]
2461    fn name_or_empty_borrows_from_metadata_name_slot() {
2462        // The returned `&str` is tied to the `EphemeralPool`'s
2463        // lifetime — the caller can compare / hash / index without
2464        // allocating. This is the load-bearing property that lets
2465        // the `HashMap<String, _>::get(pool.name_or_empty())` closure
2466        // in `controller_allocation::reconcile_inner` skip cloning.
2467        let p = pool_named("attest-pool");
2468        let s: &str = p.name_or_empty();
2469        assert_eq!(s.as_ptr(), p.metadata.name.as_deref().unwrap().as_ptr());
2470    }
2471
2472    // ─── EphemeralPool::owned_name_or_empty substrate pins ────────────
2473    //
2474    // The owned-form peer of the borrow-form `name_or_empty` primitive
2475    // above. Sibling to the sister-CRD primitive
2476    // `crate::crd::Process::owned_name_or_empty` (owned + empty sentinel
2477    // on `Process::metadata.name`) — the four primitives now partition
2478    // the (borrow × owned) × (name × uid) corner of the metadata-slot
2479    // family on identical missing-slot semantics across BOTH tatara-
2480    // process CRDs (`Process::uid_or_empty` + `Process::owned_name_or_empty`
2481    // + `EphemeralPool::name_or_empty` + this method). Fail-before-pass-
2482    // after granularity: `owned_name_or_empty` did not exist on the pool
2483    // CRD pre-lift; the compiler cannot resolve the name until the impl
2484    // block above is in place, so a rollback of the primitive breaks
2485    // this whole module.
2486    #[test]
2487    fn owned_name_or_empty_returns_empty_string_when_metadata_name_is_none() {
2488        let p = pool_unnamed();
2489        assert!(p.metadata.name.is_none(), "fixture invariant");
2490        assert_eq!(p.owned_name_or_empty(), String::new());
2491    }
2492
2493    #[test]
2494    fn owned_name_or_empty_returns_owned_string_when_slot_is_populated() {
2495        let p = pool_named("attest-pool");
2496        assert_eq!(p.owned_name_or_empty(), "attest-pool");
2497    }
2498
2499    #[test]
2500    fn owned_name_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
2501        // Corner between `None` (missing slot) and `Some(String::new())`
2502        // (populated slot containing the empty string): the primitive
2503        // MUST fold both to the same `""` byte-shape so a downstream
2504        // `HashMap<String,_>::get(name)` sees ONE "unnamed pool" bucket
2505        // regardless of which shape the K8s API server materialized.
2506        // Byte-identical to what the pre-lift `.clone().unwrap_or_default()`
2507        // chain produced.
2508        let mut p = pool_named("scratch");
2509        p.metadata.name = Some(String::new());
2510        assert_eq!(p.owned_name_or_empty(), String::new());
2511        assert!(p.owned_name_or_empty().is_empty());
2512    }
2513
2514    #[test]
2515    fn owned_name_or_empty_is_a_pure_projection() {
2516        // Consecutive calls return byte-identical Strings — no cached
2517        // state, no mutation on the `EphemeralPool` between calls.
2518        // Guards against a future refactor that plants a cache field
2519        // and drifts one caller from another silently.
2520        let p = pool_named("router-pool");
2521        assert_eq!(p.owned_name_or_empty(), p.owned_name_or_empty());
2522        assert_eq!(p.owned_name_or_empty(), "router-pool");
2523        assert_eq!(p.owned_name_or_empty(), "router-pool");
2524    }
2525
2526    #[test]
2527    fn owned_name_or_empty_matches_pre_lift_chain_verbatim() {
2528        // Byte-identical parity with the two hand-authored
2529        // `.metadata.name.clone().unwrap_or_default()` chains the
2530        // primitive replaces in `tatara-pool-reconciler::
2531        // controller_allocation::reconcile_inner` (HashMap key seed)
2532        // and `tatara-pool-reconciler::allocation_decide::
2533        // AllocationConvergenceCtx::observe` (AllocationRef.name slot
2534        // seed). Runs across the FULL corner set of the metadata.name
2535        // slot: absent, present-with-value, present-with-empty-string.
2536        // A regression that inserted a normalization step at the
2537        // primitive the pre-lift chain does NOT apply — or vice versa —
2538        // surfaces here rather than as silent drift between the two
2539        // owned-form callsites and the ONE substrate owner they now
2540        // route through.
2541        let cases: [(Option<String>, &str); 3] = [
2542            (None, ""),
2543            (Some("attest-pool".into()), "attest-pool"),
2544            (Some(String::new()), ""),
2545        ];
2546        for (slot, expected) in cases {
2547            let mut p = pool_named("scratch");
2548            p.metadata.name = slot.clone();
2549            let pre_lift = p.metadata.name.clone().unwrap_or_default();
2550            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
2551            assert_eq!(p.owned_name_or_empty(), pre_lift);
2552            assert_eq!(p.owned_name_or_empty().as_str(), expected);
2553        }
2554    }
2555
2556    #[test]
2557    fn owned_name_or_empty_matches_borrow_form_peer_on_populated_slot() {
2558        // Cross-primitive coherence pin at the sibling corner: when the
2559        // slot is present, the borrow-form (`name_or_empty`) and owned-
2560        // form (`owned_name_or_empty`) primitives return the SAME byte
2561        // sequence and differ only in ownership. A regression that
2562        // skewed one form's fallback would surface here rather than as
2563        // silent drift between the router tie-break comparator and the
2564        // AllocationRef seed on the SAME pool.
2565        let p = pool_named("attest-pool");
2566        assert_eq!(p.name_or_empty(), p.owned_name_or_empty().as_str());
2567    }
2568
2569    #[test]
2570    fn owned_name_or_empty_matches_borrow_form_peer_on_missing_slot() {
2571        // Sibling corner of the coherence pin above: when the slot is
2572        // absent (or explicitly empty), BOTH primitives fold to the
2573        // same empty-string byte-shape. The load-bearing property is
2574        // that a caller who switches between the two return-forms
2575        // based on downstream ownership requirements never sees a
2576        // different missing-slot spelling as a side effect.
2577        let p = pool_unnamed();
2578        assert_eq!(p.name_or_empty(), p.owned_name_or_empty().as_str());
2579        assert_eq!(p.name_or_empty(), "");
2580        assert_eq!(p.owned_name_or_empty(), String::new());
2581    }
2582
2583    // ─── EphemeralPool::is_being_deleted substrate pins ───────────────
2584    //
2585    // Pins the copy-form metadata-projection primitive on the deletion-
2586    // tombstone axis of the pool CRD. Peer to the borrow-form + owned-
2587    // form metadata-fallback family (`name_or_empty`,
2588    // `owned_name_or_empty`); this one opens the presence-probe corner
2589    // for the tombstone slot. Sibling to the sister-CRD primitive
2590    // `crate::crd::Process::is_being_deleted` — the two primitives
2591    // now partition the tombstone-presence probe across BOTH tatara-
2592    // process CRDs on identical missing-slot semantics. Fail-before-
2593    // pass-after granularity: `is_being_deleted` did not exist on the
2594    // pool CRD pre-lift; the compiler cannot resolve the name until
2595    // the impl block above is in place, so a rollback of the primitive
2596    // breaks this whole module.
2597
2598    fn tombstoned_pool() -> EphemeralPool {
2599        let mut p = pool_named("attest-pool");
2600        p.metadata.namespace = Some("ephemeral-pools".into());
2601        p.metadata.deletion_timestamp = Some(k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(
2602            Utc::now(),
2603        ));
2604        p
2605    }
2606
2607    #[test]
2608    fn is_being_deleted_returns_false_when_deletion_timestamp_is_absent() {
2609        // Missing-tombstone corner pin: the primitive collapses the
2610        // no-tombstone case to `false` so the `→ Drain` short-circuit
2611        // at `decide_pool_reconcile` is NOT taken and the observed-
2612        // phase composer at `pool_phase_from_members` proceeds to its
2613        // normal (free / spawning / allocated) arithmetic branches
2614        // instead of short-circuiting to `PoolPhase::Draining`.
2615        // Matches the pre-lift `.is_some()` chain's `false` byte-
2616        // identically at every consumer's downstream gate.
2617        let mut p = pool_named("attest-pool");
2618        p.metadata.deletion_timestamp = None;
2619        assert!(!p.is_being_deleted());
2620    }
2621
2622    #[test]
2623    fn is_being_deleted_returns_true_when_deletion_timestamp_is_present() {
2624        // Present-tombstone corner pin: the primitive returns `true`
2625        // on any populated `metadata.deletionTimestamp` slot regardless
2626        // of the timestamp payload — the two consumers only read the
2627        // tombstone's PRESENCE, never its RFC-3339 timestamp value.
2628        // A regression that gated the `true` return on the timestamp
2629        // being non-epoch, or parsed the timestamp before returning,
2630        // would surface here rather than as silent skew at the
2631        // `→ Drain` decision or the `→ Draining` phase report on the
2632        // SAME `EphemeralPool`.
2633        let p = tombstoned_pool();
2634        assert!(p.is_being_deleted());
2635    }
2636
2637    #[test]
2638    fn is_being_deleted_is_a_pure_projection() {
2639        // Purity pin: two consecutive calls return byte-identical
2640        // `bool` values (no lazy materialization, no interior
2641        // mutation of `self`). Peer to the sibling
2642        // `name_or_empty_is_a_pure_projection` +
2643        // `owned_name_or_empty_is_a_pure_projection` pins in this
2644        // module and to `is_being_deleted_is_a_pure_projection` on
2645        // the sister-CRD `Process`; all four bind the pure-projection
2646        // discipline on the ONE substrate accessor per metadata slot.
2647        let p = tombstoned_pool();
2648        let a = p.is_being_deleted();
2649        let b = p.is_being_deleted();
2650        assert_eq!(a, b);
2651        assert!(a);
2652    }
2653
2654    #[test]
2655    fn is_being_deleted_matches_pre_lift_pool_reconciler_chain_shape() {
2656        // Parity pin: sweeps the two corners every pre-lift consumer
2657        // plausibly encountered (missing tombstone, present tombstone)
2658        // and compares the substrate call against a hand-authored pre-
2659        // lift chain byte-identically. A regression that reshaped
2660        // either corner would surface here rather than as silent
2661        // operator-facing skew between the pool-reconciler's `→ Drain`
2662        // decision and the observed-phase composer's `→ Draining`
2663        // report on the SAME `EphemeralPool` within one reconcile
2664        // pass.
2665        fn pre_lift(p: &EphemeralPool) -> bool {
2666            p.metadata.deletion_timestamp.is_some()
2667        }
2668        // Missing slot.
2669        let mut p = pool_named("attest-pool");
2670        p.metadata.deletion_timestamp = None;
2671        assert_eq!(p.is_being_deleted(), pre_lift(&p));
2672        // Populated slot.
2673        let p = tombstoned_pool();
2674        assert_eq!(p.is_being_deleted(), pre_lift(&p));
2675    }
2676
2677    #[test]
2678    fn is_being_deleted_composes_with_pool_phase_draining_at_reconcile_preempt() {
2679        // Call-site-shape pin: the `pool_phase_from_members`
2680        // deletion-preempt returns `PoolPhase::Draining` as soon as
2681        // `pool.is_being_deleted()` holds, regardless of the (free +
2682        // spawning) supply arithmetic that would otherwise pick
2683        // `Ready` / `Scaling` / `Degraded`. The `→ Drain` decision at
2684        // `decide_pool_reconcile` composes with the same probe on the
2685        // same tombstone-presence slot. A regression that broadened
2686        // the tombstone probe implicitly (returning `false` on a
2687        // present but zero-timestamp) or narrowed it (requiring an
2688        // additional `.finalizers.is_empty()` conjunct that the two
2689        // consumers never spelled) would surface here rather than as
2690        // silent operator-facing skew between the pool reconciler's
2691        // decision and the observed-phase composer on the SAME
2692        // `EphemeralPool` within one reconcile pass.
2693        let alive = pool_named("attest-pool");
2694        assert!(!alive.is_being_deleted());
2695        let dying = tombstoned_pool();
2696        assert!(dying.is_being_deleted());
2697    }
2698
2699    // ─── EphemeralPool::owned_namespace_or_empty substrate pins ───────
2700    //
2701    // The owned-form peer of the `owned_name_or_empty` primitive on the
2702    // sibling `metadata.namespace` axis — the paired half of the
2703    // `AllocationRef { name, namespace }` struct literal both
2704    // `AllocationConvergenceCtx::observe` and the composition pin
2705    // consume through the SAME `AllocationRef::new(name, namespace)`
2706    // constructor. Fail-before-pass-after granularity:
2707    // `owned_namespace_or_empty` did not exist on the pool CRD pre-
2708    // lift; the compiler cannot resolve the name until the impl block
2709    // above is in place, so a rollback of the primitive breaks this
2710    // whole module.
2711    #[test]
2712    fn owned_namespace_or_empty_returns_empty_string_when_metadata_namespace_is_none() {
2713        // Missing-slot corner pin: the primitive collapses the no-
2714        // namespace case to the load-bearing empty-string sentinel so
2715        // the downstream `AllocationRef.namespace` slot carries `""`
2716        // rather than a defaulted `"default"` string. See the doc-
2717        // comment's DELIBERATE-EMPTY-SENTINEL rationale for why the
2718        // fallback matches `.clone().unwrap_or_default()` byte-for-
2719        // byte rather than substituting `Process::DEFAULT_NAMESPACE`
2720        // at the primitive.
2721        let mut p = pool_named("attest-pool");
2722        p.metadata.namespace = None;
2723        assert!(p.metadata.namespace.is_none(), "fixture invariant");
2724        assert_eq!(p.owned_namespace_or_empty(), String::new());
2725    }
2726
2727    #[test]
2728    fn owned_namespace_or_empty_returns_owned_string_when_slot_is_populated() {
2729        let mut p = pool_named("attest-pool");
2730        p.metadata.namespace = Some("ephemeral-pools".into());
2731        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
2732    }
2733
2734    #[test]
2735    fn owned_namespace_or_empty_returns_empty_string_when_slot_is_explicitly_empty_string() {
2736        // Corner between `None` (missing slot) and `Some(String::new())`
2737        // (populated slot containing the empty string): the primitive
2738        // MUST fold both to the same `""` byte-shape so a downstream
2739        // `AllocationRef.namespace ==` comparator at
2740        // `resolve_pool` sees ONE "unset namespace" bucket regardless
2741        // of which shape the K8s API server materialized. Byte-
2742        // identical to what the pre-lift `.clone().unwrap_or_default()`
2743        // chain produced.
2744        let mut p = pool_named("attest-pool");
2745        p.metadata.namespace = Some(String::new());
2746        assert_eq!(p.owned_namespace_or_empty(), String::new());
2747        assert!(p.owned_namespace_or_empty().is_empty());
2748    }
2749
2750    #[test]
2751    fn owned_namespace_or_empty_is_a_pure_projection() {
2752        // Consecutive calls return byte-identical Strings — no cached
2753        // state, no mutation on the `EphemeralPool` between calls.
2754        // Peer to the sibling `owned_name_or_empty_is_a_pure_projection`
2755        // pin in this module and to `is_being_deleted_is_a_pure_projection`
2756        // on the same CRD; all three bind the pure-projection
2757        // discipline on the ONE substrate accessor per metadata slot.
2758        let mut p = pool_named("attest-pool");
2759        p.metadata.namespace = Some("ephemeral-pools".into());
2760        assert_eq!(p.owned_namespace_or_empty(), p.owned_namespace_or_empty());
2761        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
2762        assert_eq!(p.owned_namespace_or_empty(), "ephemeral-pools");
2763    }
2764
2765    #[test]
2766    fn owned_namespace_or_empty_matches_pre_lift_chain_verbatim() {
2767        // Byte-identical parity with the two hand-authored
2768        // `.metadata.namespace.clone().unwrap_or_default()` chains
2769        // the primitive replaces in `tatara-pool-reconciler::
2770        // allocation_decide::AllocationConvergenceCtx::observe`
2771        // (matched-pool `AllocationRef.namespace` seed) and in the
2772        // sibling composition pin
2773        // `allocation_ref_new_composes_with_owned_name_or_empty_pool_projection`.
2774        // Runs across the FULL corner set of the metadata.namespace
2775        // slot: absent, present-with-value, present-with-empty-string.
2776        // A regression that inserted a normalization step at the
2777        // primitive the pre-lift chain does NOT apply — or vice versa —
2778        // surfaces here rather than as silent drift between the two
2779        // owned-form callsites and the ONE substrate owner they now
2780        // route through.
2781        let cases: [(Option<String>, &str); 3] = [
2782            (None, ""),
2783            (Some("ephemeral-pools".into()), "ephemeral-pools"),
2784            (Some(String::new()), ""),
2785        ];
2786        for (slot, expected) in cases {
2787            let mut p = pool_named("attest-pool");
2788            p.metadata.namespace = slot.clone();
2789            let pre_lift = p.metadata.namespace.clone().unwrap_or_default();
2790            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
2791            assert_eq!(p.owned_namespace_or_empty(), pre_lift);
2792            assert_eq!(p.owned_namespace_or_empty().as_str(), expected);
2793        }
2794    }
2795
2796    #[test]
2797    fn owned_namespace_or_empty_composes_with_owned_name_or_empty_on_paired_slot_axis() {
2798        // Paired-axis coherence pin: the two owned-form primitives on
2799        // the pool CRD's `metadata.name` + `metadata.namespace` slots
2800        // share the SAME empty-string sentinel on the missing corner,
2801        // so a caller that composes both halves into an
2802        // `AllocationRef` (as `AllocationConvergenceCtx::observe`
2803        // does) never sees a mixed-fallback pair (one `""`, the
2804        // other `"default"`) as a side effect of one slot being
2805        // absent. A regression that skewed either primitive's
2806        // fallback would surface here rather than as silent operator-
2807        // facing skew between the paired halves of the SAME
2808        // `AllocationRef` seed.
2809        let mut p = pool_named("attest-pool");
2810        p.metadata.namespace = None;
2811        p.metadata.name = None;
2812        assert_eq!(p.owned_name_or_empty(), p.owned_namespace_or_empty());
2813        assert_eq!(p.owned_name_or_empty(), String::new());
2814        assert_eq!(p.owned_namespace_or_empty(), String::new());
2815    }
2816
2817    #[test]
2818    fn owned_namespace_or_empty_does_not_default_to_process_default_namespace() {
2819        // Deliberate-empty-sentinel pin: the primitive's fallback is
2820        // `""`, NOT `crate::crd::Process::DEFAULT_NAMESPACE`. The
2821        // sole downstream consumer (`AllocationConvergenceCtx::observe`)
2822        // feeds the produced value into `AllocationRef.namespace`,
2823        // which is then matched byte-identically against
2824        // `spec.pool_ref.namespace` at `resolve_pool`. A silent
2825        // substitution of `"default"` at this primitive would alias
2826        // every namespace-absent pool to the `"default"` bucket at
2827        // the matcher, hiding the missing-slot corner from an
2828        // operator who explicitly authored an allocation against a
2829        // namespace-unset pool. Pinned so a future "helpful"
2830        // canonicalization step lands as a compiler-visible failure
2831        // here rather than as silent operator-facing skew at the
2832        // matched-pool seed.
2833        let mut p = pool_named("attest-pool");
2834        p.metadata.namespace = None;
2835        assert_ne!(
2836            p.owned_namespace_or_empty(),
2837            crate::crd::Process::DEFAULT_NAMESPACE
2838        );
2839        assert_eq!(p.owned_namespace_or_empty(), "");
2840    }
2841
2842    // ─── EphemeralPool::owned_uid_or_name_or_empty substrate pins ─────
2843    //
2844    // Pins the compound owned-form projection on the paired
2845    // `(metadata.uid, metadata.name)` axis of the pool CRD — the
2846    // ONE-liner collapse of the paired `.metadata.uid.clone()
2847    // .unwrap_or_else(|| name.<into>())` chain every pool-slot-name
2848    // consumer restated by hand pre-lift at TWO production sites in
2849    // `tatara-pool-reconciler::controller_pool` (spawn arm +
2850    // apply_convergence_actions arm), both feeding the SAME
2851    // `member_process_name(&pool_name, &pool_uid_or_name_fallback,
2852    // slot)` composer. Fail-before-pass-after granularity:
2853    // `owned_uid_or_name_or_empty` did not exist on the pool CRD pre-
2854    // lift; the compiler cannot resolve the name until the impl block
2855    // above is in place, so a rollback of the primitive breaks this
2856    // whole module.
2857    #[test]
2858    fn owned_uid_or_name_or_empty_returns_uid_when_uid_is_present() {
2859        // Preferred-slot pin: uid populated → uid wins, regardless of
2860        // whether the name-fallback slot is populated. Byte-identical
2861        // to what each pre-lift `.metadata.uid.clone().unwrap_or_else
2862        // (|| name.<into>())` chain returned in the reachable-state
2863        // corner where the K8s API server has stamped a uid (the
2864        // common case at both callsites, which are already gated by
2865        // `owned_coordinates_required()?`).
2866        let mut p = pool_named("attest-pool");
2867        p.metadata.uid = Some("uid-42".into());
2868        assert_eq!(p.owned_uid_or_name_or_empty(), "uid-42");
2869    }
2870
2871    #[test]
2872    fn owned_uid_or_name_or_empty_falls_back_to_name_when_uid_is_missing() {
2873        // Fallback-slot pin: uid absent → name wins. Byte-identical
2874        // to what each pre-lift chain returned in the corner where
2875        // the K8s API server has NOT yet stamped a uid (pre-admission
2876        // / unit-test in-memory pool). The pre-lift chain reached
2877        // the fallback via a locally-bound `name` string derived from
2878        // the same `.metadata.name` slot the primitive reaches via
2879        // `owned_name_or_empty()`.
2880        let mut p = pool_named("attest-pool");
2881        p.metadata.uid = None;
2882        assert_eq!(p.owned_uid_or_name_or_empty(), "attest-pool");
2883    }
2884
2885    #[test]
2886    fn owned_uid_or_name_or_empty_sinks_to_empty_when_both_slots_are_missing() {
2887        // Missing-both corner pin: uid absent AND name absent → the
2888        // load-bearing empty-string sentinel. Coherent with the
2889        // sibling primitives `owned_name_or_empty` +
2890        // `owned_namespace_or_empty` on the SAME empty-sentinel axis.
2891        // A regression that dropped either fallback surfaces here
2892        // rather than as a runtime panic on `.unwrap()` at a spawn
2893        // callsite that assumed both slots were populated.
2894        let mut p = pool_named("attest-pool");
2895        p.metadata.uid = None;
2896        p.metadata.name = None;
2897        assert_eq!(p.owned_uid_or_name_or_empty(), String::new());
2898        assert!(p.owned_uid_or_name_or_empty().is_empty());
2899    }
2900
2901    #[test]
2902    fn owned_uid_or_name_or_empty_prefers_uid_when_both_slots_are_present() {
2903        // Precedence pin: both slots populated → uid wins. The pre-
2904        // lift `.unwrap_or_else(|| name.<into>())` chain's short-
2905        // circuit on the `Some(u)` arm skipped the fallback entirely;
2906        // the primitive matches that byte-for-byte via `.clone()
2907        // .unwrap_or_else(|| self.owned_name_or_empty())`, so the
2908        // name-fallback slot is not read when uid is populated.
2909        let mut p = pool_named("attest-pool");
2910        p.metadata.uid = Some("uid-preferred".into());
2911        p.metadata.name = Some("attest-pool".into());
2912        assert_eq!(p.owned_uid_or_name_or_empty(), "uid-preferred");
2913        assert_ne!(p.owned_uid_or_name_or_empty(), "attest-pool");
2914    }
2915
2916    #[test]
2917    fn owned_uid_or_name_or_empty_returns_uid_even_when_uid_is_explicitly_empty_string() {
2918        // Corner between `None` (missing slot) and `Some(String::new())`
2919        // (populated slot containing the empty string): the primitive
2920        // MUST return the populated-empty-string uid rather than
2921        // falling back to the name half — byte-identical to what the
2922        // pre-lift `.metadata.uid.clone().unwrap_or_else(|| name...)`
2923        // chain produced, whose `unwrap_or_else` short-circuits on
2924        // `Some(_)` regardless of the wrapped value. Pinned so a
2925        // future "helpful" canonicalization that treats
2926        // `Some(String::new())` as `None` at the primitive lands as
2927        // a compiler-visible failure here rather than as silent
2928        // operator-facing skew between the two spawn-slot-slug seeds.
2929        let mut p = pool_named("attest-pool");
2930        p.metadata.uid = Some(String::new());
2931        p.metadata.name = Some("attest-pool".into());
2932        assert_eq!(p.owned_uid_or_name_or_empty(), String::new());
2933        assert_ne!(p.owned_uid_or_name_or_empty(), "attest-pool");
2934    }
2935
2936    #[test]
2937    fn owned_uid_or_name_or_empty_is_a_pure_projection() {
2938        // Consecutive calls return byte-identical Strings across the
2939        // FULL corner set (uid-present, uid-absent name-fallback,
2940        // both-absent empty-sentinel) — no cached state, no mutation
2941        // on the `EphemeralPool` between calls. Peer to the sibling
2942        // `owned_name_or_empty_is_a_pure_projection` +
2943        // `owned_namespace_or_empty_is_a_pure_projection` pins in
2944        // this module; all three bind the pure-projection discipline
2945        // on the ONE substrate accessor per metadata-derived slot.
2946        let mut p = pool_named("attest-pool");
2947        p.metadata.uid = Some("uid-42".into());
2948        assert_eq!(
2949            p.owned_uid_or_name_or_empty(),
2950            p.owned_uid_or_name_or_empty()
2951        );
2952        p.metadata.uid = None;
2953        assert_eq!(
2954            p.owned_uid_or_name_or_empty(),
2955            p.owned_uid_or_name_or_empty()
2956        );
2957        p.metadata.name = None;
2958        assert_eq!(
2959            p.owned_uid_or_name_or_empty(),
2960            p.owned_uid_or_name_or_empty()
2961        );
2962    }
2963
2964    #[test]
2965    fn owned_uid_or_name_or_empty_matches_pre_lift_chain_verbatim() {
2966        // Byte-identical parity with the two hand-authored
2967        // `.metadata.uid.clone().unwrap_or_else(|| name.<into>())`
2968        // chains the primitive replaces in
2969        // `tatara-pool-reconciler::controller_pool` (spawn arm +
2970        // apply_convergence_actions arm). Runs across the FULL
2971        // corner set of the paired (metadata.uid, metadata.name)
2972        // slots. A regression that inserted a normalization step at
2973        // the primitive the pre-lift chain does NOT apply — or vice
2974        // versa — surfaces here rather than as silent drift between
2975        // the two owned-form callsites and the ONE substrate owner
2976        // they now route through.
2977        let cases: [(Option<String>, Option<String>, &str); 6] = [
2978            (Some("uid-42".into()), Some("attest-pool".into()), "uid-42"),
2979            (Some("uid-42".into()), None, "uid-42"),
2980            (Some(String::new()), Some("attest-pool".into()), ""),
2981            (None, Some("attest-pool".into()), "attest-pool"),
2982            (None, Some(String::new()), ""),
2983            (None, None, ""),
2984        ];
2985        for (uid_slot, name_slot, expected) in cases {
2986            let mut p = pool_named("attest-pool");
2987            p.metadata.uid = uid_slot.clone();
2988            p.metadata.name = name_slot.clone();
2989            // Reproduce the pre-lift chain shape at the spawn arm
2990            // (fallback `|| name.clone()` on an extracted-earlier
2991            // `String` name) — semantically equivalent to
2992            // `.metadata.name.clone().unwrap_or_default()` at the
2993            // point of call because `owned_coordinates_required()?`
2994            // gate guarantees the caller's `name` binding matches
2995            // the pool's own `metadata.name` slot.
2996            let pre_lift = p
2997                .metadata
2998                .uid
2999                .clone()
3000                .unwrap_or_else(|| p.metadata.name.clone().unwrap_or_default());
3001            assert_eq!(pre_lift.as_str(), expected, "pre-lift chain sanity");
3002            assert_eq!(p.owned_uid_or_name_or_empty(), pre_lift);
3003            assert_eq!(p.owned_uid_or_name_or_empty().as_str(), expected);
3004        }
3005    }
3006
3007    #[test]
3008    fn owned_uid_or_name_or_empty_composes_with_member_process_name_seed_shape() {
3009        // Composition pin: the produced owned `String` feeds the
3010        // downstream `member_process_name(&pool_name, &pool_uid_or_
3011        // name_fallback, slot)` composer at both callsites, so the
3012        // seed's `String` shape must survive being borrowed as
3013        // `&str` for the composer without any owned/borrow-form
3014        // adaptation at the callsite. Binds the primitive's return
3015        // type + the borrow-form availability that the pre-lift
3016        // chain also produced (a locally-owned `String` from
3017        // `.clone().unwrap_or_else(|| name.<into>())`).
3018        let mut p = pool_named("attest-pool");
3019        p.metadata.uid = Some("uid-42".into());
3020        let seed: String = p.owned_uid_or_name_or_empty();
3021        let _borrowed: &str = &seed;
3022        assert_eq!(seed, "uid-42");
3023        p.metadata.uid = None;
3024        let seed_fallback: String = p.owned_uid_or_name_or_empty();
3025        let _borrowed_fallback: &str = &seed_fallback;
3026        assert_eq!(seed_fallback, "attest-pool");
3027    }
3028
3029    // ─── AllocationRef::new substrate pins ────────────────────────────
3030    //
3031    // Pins the substrate constructor for [`AllocationRef`] — the
3032    // ONE-liner composer that lifts the paired
3033    // `AllocationRef { name, namespace }` struct-literal every
3034    // downstream consumer restated by hand pre-lift at FOUR production
3035    // sites (2 × controller_allocation.rs assignedProcess seeds, 1 ×
3036    // allocation_decide.rs pool_ref seed, 1 × allocation_factory.rs
3037    // pool_ref seed) onto ONE substrate owner on `AllocationRef`.
3038    // Fail-before-pass-after granularity: `AllocationRef::new` did not
3039    // exist pre-lift; the compiler cannot resolve the name until the
3040    // impl block above is in place, so a rollback of the primitive
3041    // breaks this whole module.
3042
3043    #[test]
3044    fn allocation_ref_new_composes_owned_string_pair_verbatim() {
3045        // Happy-path pin: the constructor materializes an
3046        // `AllocationRef { name: <name>, namespace: <namespace> }`
3047        // byte-identical to the pre-lift struct literal every consumer
3048        // spelled. A regression that dropped either slot (e.g. an
3049        // erroneous `..Default::default()` on a shape that never had
3050        // a Default derive) surfaces here rather than as silent slot
3051        // loss downstream at the assignedProcess / bound_pool /
3052        // matched_pool / spec.pool_ref sinks.
3053        let r = AllocationRef::new(String::from("pr-42-demo"), String::from("ephemeral-pools"));
3054        assert_eq!(r.name, "pr-42-demo");
3055        assert_eq!(r.namespace, "ephemeral-pools");
3056    }
3057
3058    #[test]
3059    fn allocation_ref_new_matches_pre_lift_struct_literal_verbatim() {
3060        // Byte-identical parity pin: the substrate constructor and the
3061        // hand-authored struct literal produce equal `AllocationRef`
3062        // values on every provenance the FOUR pre-lift sites carried
3063        // (owned `String` from an owned-form projection; `&str`
3064        // promoted through `.to_string()`). A regression that inserted
3065        // a normalization step at the primitive the pre-lift literal
3066        // does NOT apply — or vice versa — surfaces here rather than
3067        // as silent drift between the four consumers and the ONE
3068        // substrate owner they now route through.
3069        let owned_name = String::from("pr-42-demo");
3070        let owned_ns = String::from("ephemeral-pools");
3071        let lifted = AllocationRef::new(owned_name.clone(), owned_ns.clone());
3072        let pre_lift = AllocationRef {
3073            name: owned_name,
3074            namespace: owned_ns,
3075        };
3076        assert_eq!(lifted, pre_lift);
3077    }
3078
3079    #[test]
3080    fn allocation_ref_new_accepts_str_provenance_via_into_string() {
3081        // `Into<String>` provenance-closure pin: the primitive accepts
3082        // every provenance the pre-lift sites carried. The
3083        // controller_allocation.rs assignedProcess seeds passed owned
3084        // `String` values (a moved `member_process_name` +
3085        // `ns.clone()`); the allocation_factory.rs pool_ref seed
3086        // passed `&str` (`n.to_string()` / `namespace.to_string()`).
3087        // Both provenances produce byte-identical output. A future
3088        // refactor of the constructor signature that demanded owned
3089        // `String` at author sites (dropping `impl Into<String>`)
3090        // would force `.to_string()` back at the FOUR call sites — the
3091        // pin fences that regression at ONE place.
3092        let from_str = AllocationRef::new("pr-42-demo", "ephemeral-pools");
3093        let from_string =
3094            AllocationRef::new(String::from("pr-42-demo"), String::from("ephemeral-pools"));
3095        assert_eq!(from_str, from_string);
3096        // Mixed provenance is also load-bearing: the allocation_decide.rs
3097        // matched_pool seed pairs an owned `String` (from
3098        // `EphemeralPool::owned_name_or_empty()`) with a hand-authored
3099        // `.clone().unwrap_or_default()` — also `String`. The
3100        // controller_allocation.rs paths pair a moved `String` name
3101        // with a `.clone()`-ed `ns: String`. Verify (owned, borrow)
3102        // and (borrow, owned) both compose to the same shape as
3103        // (owned, owned) / (borrow, borrow).
3104        let mixed_a = AllocationRef::new(String::from("pr-42-demo"), "ephemeral-pools");
3105        let mixed_b = AllocationRef::new("pr-42-demo", String::from("ephemeral-pools"));
3106        assert_eq!(from_str, mixed_a);
3107        assert_eq!(from_str, mixed_b);
3108    }
3109
3110    #[test]
3111    fn allocation_ref_new_positional_axis_order_pinned_name_first_namespace_second() {
3112        // Axis-order pin: name is the FIRST positional argument;
3113        // namespace is the SECOND. Reversing the pair at the
3114        // constructor is the exact regression this pin fences — the
3115        // FOUR pre-lift sites all spelled `name` before `namespace`
3116        // (matching the struct definition's field order in
3117        // `pub struct AllocationRef { pub name, pub namespace }`)
3118        // and the wire-format serde output `{ "name": "...",
3119        // "namespace": "..." }` reflects that order. A slot swap at
3120        // the primitive would surface here rather than as silent
3121        // `<namespace>/<name>` inversion at every downstream
3122        // qualified-ref composer that reads `{ref.name}/{ref.namespace}`
3123        // as an audit-log key.
3124        let r = AllocationRef::new("alpha-name", "beta-namespace");
3125        assert_eq!(r.name, "alpha-name");
3126        assert_eq!(r.namespace, "beta-namespace");
3127        assert_ne!(r.name, "beta-namespace");
3128        assert_ne!(r.namespace, "alpha-name");
3129    }
3130
3131    #[test]
3132    fn allocation_ref_new_preserves_empty_string_verbatim() {
3133        // Empty-string sentinel pin: the constructor is pure — it does
3134        // NOT canonicalize empty inputs (does NOT default an empty
3135        // namespace to `"default"`; does NOT reject an empty name).
3136        // Preserves the pre-lift shape the allocation_decide.rs
3137        // matched_pool seed relied on: when the pool's metadata.namespace
3138        // is absent, `.clone().unwrap_or_default()` yields the empty
3139        // string, and the AllocationRef's namespace slot carries that
3140        // empty string verbatim to the downstream `bound_pool` sink.
3141        // A future canonicalization pass (e.g. defaulting to
3142        // `Process::DEFAULT_NAMESPACE`) MUST land here, not at the
3143        // primitive body silently, so the pre-lift consumers' empty-
3144        // sentinel semantics are the visible contract of the new
3145        // constructor.
3146        let r = AllocationRef::new("", "");
3147        assert_eq!(r.name, "");
3148        assert_eq!(r.namespace, "");
3149        let mixed = AllocationRef::new("pr-42-demo", "");
3150        assert_eq!(mixed.name, "pr-42-demo");
3151        assert_eq!(mixed.namespace, "");
3152    }
3153
3154    #[test]
3155    fn allocation_ref_new_composes_with_owned_name_or_empty_pool_projection() {
3156        // Composition pin: the constructor composes with the paired
3157        // substrate primitives [`EphemeralPool::owned_name_or_empty`]
3158        // + [`EphemeralPool::owned_namespace_or_empty`] at the
3159        // allocation_decide.rs pool_ref seed — the same primitive
3160        // family the pool CRD opened for both halves of the
3161        // `AllocationRef { name, namespace }` struct literal. The
3162        // composed pair carries an owned `String` name half (from
3163        // `pool.owned_name_or_empty()`) and an owned `String`
3164        // namespace half (from `pool.owned_namespace_or_empty()`) —
3165        // no pre-lift chain remains. A regression that broke the
3166        // primitive family's `impl Into<String>` acceptance of an
3167        // owned `String` return type would surface here rather than
3168        // as silent build failure at the pool-reconciler matched_pool
3169        // seed.
3170        let pool = pool_named("attest-pool");
3171        let r = AllocationRef::new(pool.owned_name_or_empty(), pool.owned_namespace_or_empty());
3172        assert_eq!(r.name, "attest-pool");
3173        assert_eq!(r.namespace, pool.owned_namespace_or_empty());
3174    }
3175
3176    #[test]
3177    fn allocation_ref_new_returns_wire_format_serialization_verbatim() {
3178        // Wire-format pin: the constructor produces an
3179        // [`AllocationRef`] whose serde `rename_all = "camelCase"`
3180        // serialization is byte-identical to the pre-lift struct
3181        // literal's serialization. The `bound_pool` and
3182        // `assignedProcess` slots on `AllocationStatus` (and the
3183        // `poolRef` slot on `AllocationSpec`) all round-trip through
3184        // this shape — the pin fences a regression that added a
3185        // private field or a `#[serde(skip)]` accidentally.
3186        let r = AllocationRef::new("pr-42-demo", "ephemeral-pools");
3187        let yaml = serde_yaml::to_string(&r).expect("AllocationRef serializes to yaml");
3188        assert!(yaml.contains("name: pr-42-demo"), "{yaml}");
3189        assert!(yaml.contains("namespace: ephemeral-pools"), "{yaml}");
3190        let back: AllocationRef =
3191            serde_yaml::from_str(&yaml).expect("AllocationRef round-trips through yaml");
3192        assert_eq!(back, r);
3193    }
3194
3195    fn member(state: MemberState) -> PoolMember {
3196        PoolMember {
3197            process_name: "m".into(),
3198            state,
3199            entered_state_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
3200            allocation_ref: None,
3201        }
3202    }
3203
3204    #[test]
3205    fn state_count_fanout_returns_all_zeros_on_empty_slice() {
3206        // Zero-length pin: the empty-members corner produces a
3207        // 4-tuple of zero counters, matching the pre-lift
3208        // `count_state` fanout's four `.iter().filter(...).count()`
3209        // calls each returning 0 on an empty iterator.
3210        assert_eq!(PoolMember::state_count_fanout(&[]), (0, 0, 0, 0));
3211    }
3212
3213    #[test]
3214    fn state_count_fanout_partitions_variants_into_correct_slots() {
3215        // Positional-axis pin: the returned 4-tuple's slot order
3216        // matches the four `PoolStatus` counter slots in declaration
3217        // order — `(ready, allocated, spawning, returning)`. A
3218        // regression that swapped two slots (e.g., `ready` ↔
3219        // `spawning`) surfaces here rather than as an operator-facing
3220        // scale-out oscillation at the pool reconciler.
3221        let members = vec![
3222            member(MemberState::Free),
3223            member(MemberState::Free),
3224            member(MemberState::Allocated),
3225            member(MemberState::Spawning),
3226            member(MemberState::Spawning),
3227            member(MemberState::Spawning),
3228            member(MemberState::Returning),
3229        ];
3230        assert_eq!(PoolMember::state_count_fanout(&members), (2, 1, 3, 1));
3231    }
3232
3233    #[test]
3234    fn state_count_fanout_excludes_failed_from_every_counter() {
3235        // Closed-set pin: no `PoolStatus` slot counts `Failed` members
3236        // (they surface via `PoolPhase::Degraded` instead of a status
3237        // counter). This test fences a regression that let a `Failed`
3238        // member drift into one of the four counters and inflate the
3239        // operator-visible ready/allocated/spawning/returning fanout.
3240        let members = vec![
3241            member(MemberState::Failed),
3242            member(MemberState::Failed),
3243            member(MemberState::Failed),
3244        ];
3245        assert_eq!(PoolMember::state_count_fanout(&members), (0, 0, 0, 0));
3246
3247        // Mixed with a Free member: the Free member is counted, the
3248        // Failed members are not.
3249        let mixed = vec![
3250            member(MemberState::Free),
3251            member(MemberState::Failed),
3252            member(MemberState::Failed),
3253        ];
3254        assert_eq!(PoolMember::state_count_fanout(&mixed), (1, 0, 0, 0));
3255    }
3256
3257    #[test]
3258    fn state_count_fanout_matches_pre_lift_count_state_helper_verbatim() {
3259        // Parity pin: for every possible members list, the 4-tuple
3260        // returned by the substrate primitive matches the pre-lift
3261        // `count_state(&members, MemberState::<slot>)` fanout that
3262        // pool-reconciler restated at both status-patch sites. The
3263        // pre-lift helper was
3264        // ```rust,ignore
3265        // fn count_state(members: &[PoolMember], target: MemberState) -> u32 {
3266        //     members.iter().filter(|m| m.state == target).count() as u32
3267        // }
3268        // ```
3269        // — re-implemented inline here as an oracle.
3270        fn count_state(members: &[PoolMember], target: MemberState) -> u32 {
3271            members.iter().filter(|m| m.state == target).count() as u32
3272        }
3273        let members = vec![
3274            member(MemberState::Free),
3275            member(MemberState::Allocated),
3276            member(MemberState::Allocated),
3277            member(MemberState::Spawning),
3278            member(MemberState::Returning),
3279            member(MemberState::Returning),
3280            member(MemberState::Failed),
3281        ];
3282        let (ready, allocated, spawning, returning) = PoolMember::state_count_fanout(&members);
3283        assert_eq!(ready, count_state(&members, MemberState::Free));
3284        assert_eq!(allocated, count_state(&members, MemberState::Allocated));
3285        assert_eq!(spawning, count_state(&members, MemberState::Spawning));
3286        assert_eq!(returning, count_state(&members, MemberState::Returning));
3287    }
3288
3289    // ─── PoolMember::process_names_set substrate pins ─────────────────
3290    //
3291    // Pins the closed-set slice-owned collection primitive on the
3292    // `process_name` axis into a `HashSet<String>` — the O(1)-lookup
3293    // shape both spawn arms in
3294    // `tatara-pool-reconciler::controller_pool` build pre-collision-
3295    // check against a candidate `member_process_name(&pool_name,
3296    // &pool_uid, slot)`. Sibling to `state_count_fanout` on the
3297    // `(collection shape × slice-owned fold)` axis; the fanout owns
3298    // the state-counter tuple corner, this primitive owns the
3299    // process-name-lookup corner. Fail-before-pass-after granularity:
3300    // `process_names_set` did not exist pre-lift; the compiler cannot
3301    // resolve the name until the impl block above is in place, so a
3302    // rollback of the primitive breaks this whole test group.
3303
3304    fn named_member(process_name: &str, state: MemberState) -> PoolMember {
3305        PoolMember {
3306            process_name: process_name.into(),
3307            state,
3308            entered_state_at: DateTime::<Utc>::from_timestamp(0, 0).unwrap(),
3309            allocation_ref: None,
3310        }
3311    }
3312
3313    #[test]
3314    fn process_names_set_returns_empty_hashset_on_empty_slice() {
3315        // Zero-length pin: the empty-members corner produces an
3316        // empty `HashSet<String>`, matching the pre-lift
3317        // `.iter().map(...).collect()` chain's empty-iterator
3318        // behavior. A regression that started producing a sentinel
3319        // entry (a `""` placeholder, a static seed) on the empty-
3320        // slice corner would silently reject the first spawn slot
3321        // downstream — the pin closes that failure mode.
3322        let empty: Vec<PoolMember> = vec![];
3323        assert!(PoolMember::process_names_set(&empty).is_empty());
3324    }
3325
3326    #[test]
3327    fn process_names_set_collects_every_process_name_from_populated_slice() {
3328        // Positive pin: every `PoolMember`'s `process_name` slot
3329        // lands in the returned `HashSet<String>` verbatim. Cross-
3330        // state (Free / Allocated / Spawning / Returning / Failed)
3331        // to prove the primitive is state-agnostic — the spawn arms
3332        // check occupancy on the name axis, NOT the state axis, so a
3333        // future refactor that filtered by state would silently
3334        // leave a returned/failed slot open to a duplicate spawn.
3335        let members = vec![
3336            named_member("pool-a-0", MemberState::Free),
3337            named_member("pool-a-1", MemberState::Allocated),
3338            named_member("pool-a-2", MemberState::Spawning),
3339            named_member("pool-a-3", MemberState::Returning),
3340            named_member("pool-a-4", MemberState::Failed),
3341        ];
3342        let set = PoolMember::process_names_set(&members);
3343        assert_eq!(set.len(), 5);
3344        for slot in 0..5 {
3345            let want = format!("pool-a-{slot}");
3346            assert!(set.contains(&want), "missing {want}; set = {set:?}");
3347        }
3348    }
3349
3350    #[test]
3351    fn process_names_set_deduplicates_duplicate_process_names() {
3352        // Deduplication pin: two `PoolMember` entries with the same
3353        // `process_name` (a race between the two spawn arms, an
3354        // adopted foreign Process the reconciler picked up twice)
3355        // collapse to ONE entry in the `HashSet<String>`. Pins the
3356        // `HashSet` deduplication semantics the pre-lift `.iter()
3357        // .map(...).collect()` chain already inherited from the
3358        // `FromIterator` impl — a regression that swapped the
3359        // aggregate to a `Vec<String>` or `BTreeSet<String>` still
3360        // matches the shape but changes the operator-visible count
3361        // at the `.len()` probe here.
3362        let members = vec![
3363            named_member("pool-b-0", MemberState::Free),
3364            named_member("pool-b-0", MemberState::Spawning),
3365            named_member("pool-b-1", MemberState::Free),
3366        ];
3367        let set = PoolMember::process_names_set(&members);
3368        assert_eq!(set.len(), 2);
3369        assert!(set.contains("pool-b-0"));
3370        assert!(set.contains("pool-b-1"));
3371    }
3372
3373    #[test]
3374    fn process_names_set_membership_probe_matches_pre_lift_chain_verbatim() {
3375        // Byte-identical parity pin: the `.contains(&candidate)`
3376        // probe on the substrate's `HashSet<String>` return returns
3377        // the same `bool` as the pre-lift `members.iter().map(|m|
3378        // m.process_name.clone()).collect::<HashSet<_>>().contains
3379        // (&candidate)` chain across the FULL cross product of
3380        // (candidate ∈ {an existing name, a novel name, the empty
3381        // string}). A regression that inserted a normalization step
3382        // at the primitive the pre-lift chain does NOT apply — or
3383        // vice versa — surfaces here rather than as silent drift
3384        // between the two spawn arms the primitive owns.
3385        let members = vec![
3386            named_member("pool-c-0", MemberState::Free),
3387            named_member("pool-c-1", MemberState::Allocated),
3388        ];
3389        let candidates: [&str; 4] = ["pool-c-0", "pool-c-1", "pool-c-2", ""];
3390        let via_primitive = PoolMember::process_names_set(&members);
3391        for candidate in candidates {
3392            let pre_lift: std::collections::HashSet<String> =
3393                members.iter().map(|m| m.process_name.clone()).collect();
3394            assert_eq!(
3395                via_primitive.contains(candidate),
3396                pre_lift.contains(candidate),
3397                "candidate = {candidate:?}"
3398            );
3399        }
3400    }
3401
3402    #[test]
3403    fn process_names_set_is_a_pure_projection() {
3404        // Consecutive calls on the same slice return equal sets —
3405        // no cached state, no mutation on the input. Guards against
3406        // a future refactor that plants a cache field somewhere and
3407        // drifts one caller from another silently.
3408        let members = vec![
3409            named_member("pool-d-0", MemberState::Free),
3410            named_member("pool-d-1", MemberState::Spawning),
3411        ];
3412        let first = PoolMember::process_names_set(&members);
3413        let second = PoolMember::process_names_set(&members);
3414        assert_eq!(first, second);
3415    }
3416
3417    #[test]
3418    fn pool_status_observed_composes_pre_lift_status_seed_verbatim() {
3419        // Composition pin: the substrate constructor produces a
3420        // `PoolStatus` structurally equal to the pre-lift 11-line
3421        // struct literal both pool-reconciler status-patch sites
3422        // stamped by hand. Any drift in the defaults (`message`,
3423        // `conditions`) or in the counter fanout surfaces here.
3424        let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap();
3425        let members = vec![
3426            member(MemberState::Free),
3427            member(MemberState::Allocated),
3428            member(MemberState::Spawning),
3429            member(MemberState::Returning),
3430            member(MemberState::Failed),
3431        ];
3432        let member_count = members.len();
3433        let observed = PoolStatus::observed(PoolPhase::Steady, members, now);
3434        assert_eq!(observed.phase, PoolPhase::Steady);
3435        assert_eq!(observed.phase_since, Some(now));
3436        assert_eq!(observed.ready_count, 1);
3437        assert_eq!(observed.allocated_count, 1);
3438        assert_eq!(observed.spawning_count, 1);
3439        assert_eq!(observed.returning_count, 1);
3440        assert_eq!(observed.members.len(), member_count);
3441        assert!(observed.message.is_none());
3442        assert!(observed.conditions.is_empty());
3443    }
3444
3445    #[test]
3446    fn pool_status_observed_moves_members_by_value_without_extra_clone() {
3447        // Ownership pin: the constructor consumes the members Vec by
3448        // value rather than borrowing + cloning internally. Both pre-
3449        // lift sites called `.clone()` on their `members` binding for
3450        // the struct-literal `members:` slot; the substrate lift keeps
3451        // the same one-clone bound at the caller (or a straight move
3452        // if the caller no longer needs the local `members` binding
3453        // after the seed) rather than accidentally cloning twice.
3454        let members = vec![member(MemberState::Free), member(MemberState::Spawning)];
3455        let now = DateTime::<Utc>::from_timestamp(0, 0).unwrap();
3456        let observed = PoolStatus::observed(PoolPhase::Steady, members, now);
3457        assert_eq!(observed.members.len(), 2);
3458    }
3459
3460    // ─── EphemeralPool::has_name substrate pins ───────────────────────
3461    //
3462    // Pins the copy-form metadata-projection primitive on the
3463    // `metadata.name` axis's presence-and-equal corner — the
3464    // discriminant every `candidate_pools.iter().find(|p| ...)`
3465    // closure that resolves a pool from an owned-name handle
3466    // (`AllocationRef.name` / `AllocationDecision::Bind.pool.name`)
3467    // routes through. Sibling to the `_or_empty` family on the SAME
3468    // slot ([`EphemeralPool::name_or_empty`] +
3469    // [`EphemeralPool::owned_name_or_empty`]) — this primitive owns
3470    // the `None`-preserving corner the `_or_empty` family folds away.
3471    // Fail-before-pass-after granularity: `has_name` did not exist
3472    // pre-lift; the compiler cannot resolve the name until the impl
3473    // block above is in place, so a rollback of the primitive breaks
3474    // this whole module.
3475    #[test]
3476    fn has_name_returns_true_when_slot_is_populated_and_equal() {
3477        // Happy-path pin: the slot is set AND byte-identical to the
3478        // candidate. Both pre-lift `find` closures — `resolve_pool`'s
3479        // explicit-`pool_ref` half and `controller_allocation`'s TTL-
3480        // inheritance fallback — resolve their target pool exactly in
3481        // this corner, and the primitive returns `true` here to
3482        // authorize the resolution.
3483        let p = pool_named("attest-pool");
3484        assert!(p.has_name("attest-pool"));
3485    }
3486
3487    #[test]
3488    fn has_name_returns_false_when_slot_is_populated_and_different() {
3489        // Populated-slot inequality pin: the primitive returns `false`
3490        // for every candidate that is NOT byte-identical to the slot,
3491        // including strict subsequences (`"attest"` vs. `"attest-pool"`),
3492        // strict superstrings (`"attest-pool-2"` vs. `"attest-pool"`),
3493        // and case-differ variants. This is the load-bearing property
3494        // that lets `find(|p| p.has_name(&candidate))` reject
3495        // non-matching pools rather than aliasing them together.
3496        let p = pool_named("attest-pool");
3497        assert!(!p.has_name("other-pool"));
3498        assert!(!p.has_name("attest"));
3499        assert!(!p.has_name("attest-pool-2"));
3500        assert!(!p.has_name("ATTEST-POOL"));
3501    }
3502
3503    #[test]
3504    fn has_name_returns_false_when_slot_is_none_even_against_empty_candidate() {
3505        // The `None`-preserving discipline pin: an unset `metadata.name`
3506        // slot returns `false` even when the candidate is the empty
3507        // string. Distinguishes `has_name` from a naïve substitution
3508        // through the sibling `name_or_empty` primitive, which would
3509        // fold both `None` and `Some("")` to `""` and silently promote
3510        // an unnamed pool with an empty candidate into a spurious
3511        // match at the resolver's `find` closure. Byte-identical to
3512        // what the pre-lift `.as_deref() == Some(<candidate>)` chain
3513        // produced (`None == Some("")` is `false`), which is what
3514        // both consumer sites relied on.
3515        let p = pool_unnamed();
3516        assert!(p.metadata.name.is_none(), "fixture invariant");
3517        assert!(!p.has_name(""));
3518        assert!(!p.has_name("attest-pool"));
3519    }
3520
3521    #[test]
3522    fn has_name_returns_true_only_when_populated_slot_and_candidate_are_both_empty() {
3523        // Populated-empty-slot corner pin: `Some(String::new())` is a
3524        // populated slot with an empty payload. `has_name("")` returns
3525        // `true` here (byte-identical `""` on both sides), while
3526        // `has_name("<anything else>")` returns `false`. This is the
3527        // corner where `has_name` DIVERGES from `name_or_empty`
3528        // observably: the `_or_empty` family folds this corner into
3529        // the same bucket as `None`, but `has_name` keeps the
3530        // presence bit visible — `Some("") == Some("")` is `true`
3531        // while `None == Some("")` is `false`.
3532        let mut p = pool_named("scratch");
3533        p.metadata.name = Some(String::new());
3534        assert!(p.has_name(""));
3535        assert!(!p.has_name("attest-pool"));
3536    }
3537
3538    #[test]
3539    fn has_name_matches_pre_lift_chain_verbatim_across_full_corner_set() {
3540        // Byte-identical parity pin: the primitive returns the same
3541        // `bool` as the pre-lift `.metadata.name.as_deref() == Some
3542        // (candidate)` chain across the FULL cross product of
3543        // (slot ∈ {None, Some("attest-pool"), Some("")}) × (candidate
3544        // ∈ {"attest-pool", "", "other"}). A regression that inserted
3545        // a normalization step at the primitive the pre-lift chain
3546        // does NOT apply — or vice versa — surfaces here rather than
3547        // as silent drift between the two `find` closures the primitive
3548        // owns.
3549        let slots: [Option<String>; 3] =
3550            [None, Some(String::from("attest-pool")), Some(String::new())];
3551        let candidates: [&str; 3] = ["attest-pool", "", "other"];
3552        for slot in slots {
3553            let mut p = pool_named("scratch");
3554            p.metadata.name = slot.clone();
3555            for candidate in candidates {
3556                let pre_lift = p.metadata.name.as_deref() == Some(candidate);
3557                assert_eq!(
3558                    p.has_name(candidate),
3559                    pre_lift,
3560                    "slot = {slot:?}, candidate = {candidate:?}"
3561                );
3562            }
3563        }
3564    }
3565
3566    #[test]
3567    fn has_name_diverges_from_name_or_empty_on_the_missing_slot_corner() {
3568        // Cross-primitive discipline pin: `has_name("")` and
3569        // `name_or_empty() == ""` MUST disagree on the `None`-slot
3570        // corner. `name_or_empty` returns `""` (its load-bearing
3571        // sentinel), so a naïve `name_or_empty() == ""` probe would
3572        // return `true` here — aliasing every unnamed pool to the
3573        // empty-candidate bucket at the resolver. `has_name`
3574        // preserves `Option::as_deref() == Some(_)`'s `None ⇒ false`
3575        // semantics, so it returns `false` and rejects the spurious
3576        // match. This test fences the WHOLE reason `has_name` exists
3577        // as a distinct primitive from the `_or_empty` family: a
3578        // future refactor that collapsed `has_name` into
3579        // `name_or_empty() == candidate` would break this pin and
3580        // silently regress the resolver's byte-comparison honesty.
3581        let p = pool_unnamed();
3582        assert_eq!(p.name_or_empty(), "");
3583        assert!(!p.has_name(""));
3584    }
3585
3586    #[test]
3587    fn has_name_is_a_pure_projection() {
3588        // Consecutive calls with the same candidate return the same
3589        // `bool` — no cached state, no mutation on the `EphemeralPool`
3590        // between calls. Guards against a future refactor that plants
3591        // a cache field on `EphemeralPool` and drifts one caller from
3592        // another silently.
3593        let p = pool_named("router-pool");
3594        assert_eq!(p.has_name("router-pool"), p.has_name("router-pool"));
3595        assert_eq!(p.has_name("other"), p.has_name("other"));
3596        assert!(p.has_name("router-pool"));
3597        assert!(!p.has_name("other"));
3598    }
3599}