Skip to main content

tatara_process/
allocation.rs

1//! `EphemeralAllocation` CRD — a typed request for a pool member.
2//!
3//! Pairs with `EphemeralPool`: an Allocation is the request side;
4//! the pool reconciler answers it by matching one of its free
5//! Process members and stamping the requestor's identity on the
6//! Allocation's status.
7//!
8//! Topology:
9//! - The requestor (GitHub PR webhook, CI runner, operator running
10//!   `feira allocation request …`) creates an `EphemeralAllocation`.
11//! - The pool reconciler watches Allocations; matches `spec.poolRef`
12//!   (or routes via PoolSelector if `poolRef` is omitted) to a pool;
13//!   picks one Free member; transitions the member to Allocated and
14//!   the Allocation to Bound.
15//! - When the requestor is done, it deletes the Allocation. The pool
16//!   reconciler honors the pool's `returnPolicy` (Reset / Replace /
17//!   Keep).
18
19use chrono::{DateTime, Utc};
20use kube::CustomResource;
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23
24use crate::pool::AllocationRef;
25
26/// `EphemeralAllocation` CRD spec — a typed request for a pool member.
27///
28/// ```yaml
29/// apiVersion: tatara.pleme.io/v1alpha1
30/// kind: EphemeralAllocation
31/// metadata:
32///   name: pr-123-demo-app
33///   namespace: ephemeral-pools
34/// spec:
35///   poolRef:
36///     name: attest-pool
37///     namespace: ephemeral-pools
38///   requestor:
39///     kind: github-pr
40///     repo: "pleme-io/demo-app"
41///     branch: "fix-something"
42///     prNumber: 123
43///     prLabels: ["needs-ephemeral"]
44///   ttl: "1h"
45/// ```
46#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
47#[kube(
48    group = "tatara.pleme.io",
49    version = "v1alpha1",
50    kind = "EphemeralAllocation",
51    plural = "ephemeralallocations",
52    shortname = "ealloc",
53    namespaced,
54    status = "AllocationStatus",
55    printcolumn = r#"{"name":"Pool","type":"string","jsonPath":".spec.poolRef.name"}"#,
56    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
57    printcolumn = r#"{"name":"Process","type":"string","jsonPath":".status.assignedProcess.name"}"#,
58    printcolumn = r#"{"name":"Requestor","type":"string","jsonPath":".spec.requestor.kind"}"#,
59    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
60)]
61#[serde(rename_all = "camelCase")]
62pub struct AllocationSpec {
63    /// Direct pool reference. When set, skip selector-based routing.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub pool_ref: Option<AllocationRef>,
66
67    /// Who is asking for the env.
68    pub requestor: Requestor,
69
70    /// How long the requestor needs the env (`humantime`). The pool
71    /// reconciler clamps this to `pool.spec.maxAllocationTtl`.
72    /// When unset, falls back to the pool's `template.ttl`.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub ttl: Option<String>,
75
76    /// Operator-supplied notes — surfaced in `feira allocation list`
77    /// for audit / debugging context.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub note: Option<String>,
80}
81
82impl AllocationSpec {
83    /// The canonical minimal [`AllocationSpec`] composer — binds only
84    /// the single caller-varying [`Self::requestor`] slot and leaves
85    /// the three-slot default tail (`pool_ref = None`, `ttl = None`,
86    /// `note = None`) at ONE substrate owner. The lift of the 5-line
87    /// `AllocationSpec { pool_ref: None, requestor: <r>, ttl: None,
88    /// note: None }` incantation past the ★★ PRIME-DIRECTIVE ≥ 2
89    /// duplication threshold — pre-lift the SAME requestor-only
90    /// fixture shape recurred at NINE workspace-wide fixture sites
91    /// (five inside [`crate::allocation`]'s own test module — the
92    /// `alloc_with_phase` / `alloc_without_status` observers on the
93    /// phase axis, the `alloc_with_bound_pool` observer on the
94    /// routing axis, the `alloc_with_expires_at` observer on the TTL
95    /// axis, and the `allocation_spec_omits_optional_fields` wire-
96    /// shape pin; three inside [`crate::lib`]'s pin fixtures — the
97    /// `alloc_fixture` + two `empty_alloc_spec` helpers on the
98    /// coordinate / annotation axes; one inside `tatara-pool-
99    /// reconciler::allocation_decide` — the `alloc` fixture seeding
100    /// the pool convergence-decision test battery). All nine sites
101    /// walked the SAME three-slot default tail — differing only in
102    /// the caller-varying [`Requestor`] value.
103    ///
104    /// The three-slot default tail is the SAFE minimal shape: `pool_ref
105    /// = None` triggers selector-based routing (rather than pinning a
106    /// specific pool), `ttl = None` falls back to the pool template's
107    /// TTL, `note = None` leaves the audit slot empty. Every
108    /// [`crate::pool::PoolSelector`] filter sees "no direct pool
109    /// binding" and matches every candidate pool for the requestor's
110    /// kind; post-lift a callsite that legitimately overrides a slot
111    /// lands the override at its own site via struct-update on top
112    /// of `requestor_only`.
113    ///
114    /// A future addition to [`AllocationSpec`] — a new optional slot
115    /// (e.g. a `priority` for admission-control kinds, a `budget`
116    /// for cost-accounting, a `labels` set for per-allocation
117    /// tagging) — lands at ONE primitive body and every fixture /
118    /// default-shape callsite inherits the upgrade mechanically.
119    /// Pre-lift a new field would have broken all NINE callsites
120    /// (each holds an exhaustive struct literal); post-lift only
121    /// sites that legitimately override the new slot need to name it.
122    ///
123    /// Sibling substrate primitives on the same "bind-the-required-
124    /// slot-only" axis: [`Requestor::kind_only`] (Requestor kind-
125    /// only composer; the six-slot Requestor counterpart, one of the
126    /// values this composer stamps into its own `requestor` slot),
127    /// [`crate::intent::AplicacaoIntent::chart_only`] (Aplicacao
128    /// chart-pointer-only composer; the 7-slot Aplicacao counterpart),
129    /// [`crate::pool::PoolSpec::with_template`] (Pool template-only
130    /// composer; the 11-slot Pool counterpart), and
131    /// [`crate::spec::ProcessSpec::gate_compute_defaults`] (Process
132    /// zero-arg-fixture composer; the 12-slot Process counterpart).
133    ///
134    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
135    /// the 3-slot default tail recurred at NINE hand-authored sites
136    /// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, spanning
137    /// two workspace crates, and is lifted onto the ONE workspace-
138    /// wide substrate owner here). THEORY.md §II.1 invariant 5
139    /// (composition preserves proofs — the pin block below binds the
140    /// primitive at fail-before-pass-after granularity so a
141    /// regression that drifted any of the three default-tail slots
142    /// surfaces at THESE pins rather than as silent fixture skew
143    /// across the nine downstream consumers).
144    #[must_use]
145    pub fn requestor_only(requestor: Requestor) -> Self {
146        Self {
147            pool_ref: None,
148            requestor,
149            ttl: None,
150            note: None,
151        }
152    }
153}
154
155/// Identity + routing context for a request.
156#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
157#[serde(rename_all = "camelCase")]
158pub struct Requestor {
159    /// Discriminator: `"github-pr"`, `"manual"`, `"ci-run"`,
160    /// `"scheduled"`, … The wire shape is open by design — operators
161    /// may register their own kinds and the [`crate::pool::PoolSelector`]
162    /// matches on raw string equality. The substrate's own emitters
163    /// stamp one of the four canonical kebab-case kinds enumerated by
164    /// [`RequestorKind::ALL`]; [`Requestor::known_kind`] projects the
165    /// open wire field through that closed-set view at ONE site so
166    /// future kind-keyed consumers (pool dashboards, completion lists,
167    /// audit-trail classifiers) sweep the typed variants without
168    /// re-implementing `match self.kind.as_str()` arm-by-arm. Sibling
169    /// shape to [`crate::receipt::ReceiptEnvelope::known_kind`].
170    pub kind: String,
171
172    /// Optional repo identifier (e.g., `"pleme-io/demo-app"`).
173    /// Matched against `PoolSelector.repos`.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub repo: Option<String>,
176
177    /// Optional branch name. Matched against `PoolSelector.branches`.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub branch: Option<String>,
180
181    /// Optional PR number (for `kind: github-pr`). Surfaces in
182    /// printcolumns + audit.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub pr_number: Option<u64>,
185
186    /// Optional commit SHA (for `kind: github-pr` or `ci-run`).
187    /// Stamped onto the allocated Process for traceability.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub sha: Option<String>,
190
191    /// PR / commit labels — matched as a subset against
192    /// `PoolSelector.prLabels`.
193    #[serde(default)]
194    pub pr_labels: Vec<String>,
195
196    /// Free-form actor — username, CI runner ID, etc.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub actor: Option<String>,
199}
200
201impl Requestor {
202    /// Decode [`Self::kind`] into the typed [`RequestorKind`] variant
203    /// when the wire string matches one of the four substrate-emitted
204    /// canonical kebab-case kinds; `None` when the kind is an
205    /// operator-registered open string (the schema is open by design —
206    /// every allocation remains a valid allocation, but only typed
207    /// kinds participate in closed-set dispatch). The (open `String`,
208    /// closed-typed view) split lets future kind-keyed consumers
209    /// (pool-selector classifiers, dashboard completion, audit-trail
210    /// classifiers) sweep the typed variants without touching the
211    /// open-by-design wire shape. Lifted as the canonical decode site
212    /// so no consumer re-implements the `match self.kind.as_str()` arm-
213    /// by-arm — the closed-set sweep happens through
214    /// [`RequestorKind::from_str`] at ONE site. Sibling shape to
215    /// [`crate::receipt::ReceiptEnvelope::known_kind`].
216    #[must_use]
217    pub fn known_kind(&self) -> Option<RequestorKind> {
218        self.kind.parse().ok()
219    }
220
221    /// The canonical minimal [`Requestor`] composer — binds only the
222    /// single caller-varying [`Self::kind`] slot and leaves the
223    /// six-slot default tail (`repo = None`, `branch = None`,
224    /// `pr_number = None`, `sha = None`, `pr_labels = vec![]`,
225    /// `actor = None`) at ONE substrate owner. The lift of the 9-line
226    /// `Requestor { kind: <lit>.into(), repo: None, branch: None,
227    /// pr_number: None, sha: None, pr_labels: vec![], actor: None }`
228    /// incantation past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
229    /// threshold — pre-lift the SAME kind-only fixture shape recurred
230    /// at TEN workspace-wide fixture sites (six inside
231    /// [`crate::allocation`]'s own test module — the
232    /// `known_kind_decodes_built_requestors` sweep, the
233    /// `known_kind_returns_none_for_open_kinds` open-kind pin, the
234    /// `alloc_with_phase` / `alloc_without_status` observers on the
235    /// phase axis, the `bound_pool` observer on the routing axis, the
236    /// `expires_at` observer on the TTL axis, and the
237    /// `allocation_spec_omits_optional_fields` wire-shape pin; three
238    /// inside [`crate::lib`]'s pin fixtures (the `alloc_fixture` +
239    /// two `empty_alloc_spec` helpers on the coordinate / annotation
240    /// axes)).
241    ///
242    /// `impl Into<String>` on the argument accepts every pre-lift
243    /// caller shape verbatim without an argument recast:
244    /// * `Requestor::kind_only("manual")` — the operator-authored
245    ///   default fixture, matching the pre-lift `kind: "manual".into()`.
246    /// * `Requestor::kind_only("github-pr")` — the GitHub-webhook
247    ///   fixture, matching the pre-lift `kind: "github-pr".into()`.
248    /// * `Requestor::kind_only(RequestorKind::GithubPr)` — the
249    ///   typed-round-trip callsite, matching the pre-lift `kind: k
250    ///   .into()` where `k: RequestorKind` composes through the
251    ///   `From<RequestorKind> for String` bridge.
252    /// * `Requestor::kind_only("operator-custom-kind")` — the
253    ///   open-kind pin, matching the pre-lift `kind:
254    ///   "operator-custom-kind".into()`.
255    ///
256    /// The six-slot default tail is the SAFE minimal shape: every
257    /// [`crate::pool::PoolSelector`] filter sees "no repo constraint,
258    /// no branch constraint, no PR labels" and matches every pool
259    /// (post-lift a callsite that legitimately overrides a slot lands
260    /// the override at its own site via struct-update on top of
261    /// `kind_only`). A future addition to [`Requestor`] — a new
262    /// optional slot (e.g. a `run_id` for CI kinds, an `email` for
263    /// scheduled kinds, a `cluster` scoping override) — lands at ONE
264    /// primitive body and every fixture / default-shape callsite
265    /// inherits the upgrade mechanically. Pre-lift a new field would
266    /// have broken all TEN callsites (each holds an exhaustive struct
267    /// literal); post-lift only sites that legitimately override the
268    /// new slot need to name it.
269    ///
270    /// Sibling substrate primitives on the same "bind-the-required-
271    /// slots-only" axis: [`crate::intent::AplicacaoIntent::chart_only`]
272    /// (Aplicacao chart-pointer-only composer; the 7-slot Aplicacao
273    /// counterpart), [`crate::pool::PoolSpec::with_template`] (Pool
274    /// template-only composer; the 11-slot Pool counterpart), and
275    /// [`crate::spec::ProcessSpec::gate_compute_defaults`] (Process
276    /// zero-arg-fixture composer; the 12-slot Process counterpart).
277    #[must_use]
278    pub fn kind_only(kind: impl Into<String>) -> Self {
279        Self {
280            kind: kind.into(),
281            repo: None,
282            branch: None,
283            pr_number: None,
284            sha: None,
285            pr_labels: vec![],
286            actor: None,
287        }
288    }
289}
290
291/// Closed-set view over the substrate-emitted canonical
292/// [`Requestor::kind`] wire strings — the four kebab-case
293/// discriminators every pleme-io requestor stamps onto an
294/// [`EphemeralAllocation`]: `github-pr` (the [`tatara_github_watcher`-
295/// authored](../../tatara-github-watcher/src/allocation_factory.rs)
296/// PR-driven path), `manual` (operator-authored via `feira allocation
297/// request …`), `ci-run` (non-PR CI driver), and `scheduled` (a
298/// cron-style emitter). The wire field stays `pub kind: String` on
299/// [`Requestor`] so operators can register their own kinds without a
300/// schema bump; this enum is the typed view future kind-keyed
301/// consumers (pool dashboards, LSP completion, audit-trail
302/// classifiers) sweep against.
303///
304/// Pre-lift the four canonical kinds existed only as `&'static str`
305/// literals at four scattered sites — the documentation header on
306/// [`Requestor::kind`], the [`crate::pool::PoolSelector::kinds`]
307/// docstring, the `tatara-github-watcher` allocation factory, and the
308/// per-test `kind: "github-pr".into()` fixtures. A rename of one
309/// canonical kind (e.g. `"github-pr"` → `"github-pull-request"`) had
310/// no compile-time link to the others, so the documentation drifted
311/// independently of the emitter, and the [`PoolSelector::matches`]
312/// kind-filter silently kept matching the old spelling forever. Post-
313/// lift the (canonical-name, typed-variant) pairing binds at ONE site
314/// ([`Self::as_str`]); the `From<RequestorKind> for String` bridge
315/// lets emitters compose `Requestor { kind: RequestorKind::GithubPr.into(), … }`
316/// so the four canonical strings stop appearing as bare `&'static str`
317/// literals at author sites.
318///
319/// Adding a fifth kind (e.g. `Slack` → `"slack"`, `Webhook` →
320/// `"webhook"`) lands at one [`Self::ALL`] entry + one [`Self::as_str`]
321/// arm — exhaustively checked by the compiler (the `[Self; 4]` array
322/// literal forces the arity) AND by the per-variant truth-table tests
323/// below.
324///
325/// Sibling closed-set `ALL`-keyed lifts across the crate:
326/// [`crate::receipt::ReceiptKind::ALL`] (the four substrate-emitted
327/// receipt kinds — direct shape peer, same open-wire + closed-view
328/// split), [`AllocationPhase::ALL`], [`crate::phase::ProcessPhase::ALL`],
329/// [`crate::signal::ProcessSignal::ALL`],
330/// [`crate::boundary::ConditionKind::ALL`],
331/// [`crate::lifetime::TeardownPolicy::ALL`],
332/// [`crate::lifetime::LifetimeKind::ALL`],
333/// [`crate::intent::IntentKind::ALL`],
334/// [`crate::lifetime_clock::TerminateReasonKind::ALL`].
335///
336/// Theory anchor: THEORY.md §III — the typescape; the substrate's own
337/// requestor kinds become a TYPE rather than four `&'static str`
338/// literals at every author + docstring + fixture site. THEORY.md
339/// §V.1 — knowable platform; the closed-set view turns "which kinds
340/// does the substrate actually emit" from a grep job into a method
341/// the compiler enforces exhaustively at every dispatch site.
342#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
343#[closed_set(via = "as_str", generate_unknown, display)]
344pub enum RequestorKind {
345    /// GitHub pull-request webhook — `tatara-github-watcher` stamps
346    /// this on every allocation built from a `PullRequestEvent`.
347    GithubPr,
348    /// Operator-authored allocation — `feira allocation request …`
349    /// and any hand-crafted CR.
350    Manual,
351    /// Non-PR CI driver — a pipeline run that wants an ephemeral env
352    /// without an associated pull request.
353    CiRun,
354    /// Cron-style scheduled emitter — periodic allocation creation
355    /// (e.g. nightly drift detection).
356    Scheduled,
357}
358
359impl RequestorKind {
360    /// The closed set of substrate-emitted requestor kinds — single
361    /// source of truth that drives the [`Self::from_str`] decode sweep
362    /// AND any future enumeration consumer (pool-selector classifiers,
363    /// dashboard completion, `tatara-check` kind enumeration). Adding
364    /// a fifth variant (e.g. `Slack` → `"slack"`) lands at one `ALL`
365    /// entry + one `as_str` arm — exhaustively checked by the compiler
366    /// (the `[Self; 4]` array literal forces the arity) AND by the
367    /// per-variant truth-table tests below.
368    pub const ALL: [Self; 4] = [Self::GithubPr, Self::Manual, Self::CiRun, Self::Scheduled];
369
370    /// Canonical kebab-case wire-format kind — the literal that lands
371    /// in [`Requestor::kind`] when this variant authors the request.
372    /// Pinned to four byte-exact strings the substrate has already
373    /// published (the `tatara-github-watcher` factory, the operator
374    /// fixtures in this file, the `PoolSelector.kinds` filter, the
375    /// CRD printcolumns) — renaming any one is a wire-format change,
376    /// not a typed-internal refactor, and the
377    /// `requestor_kind_canonical_names_pinned` truth-table test fails
378    /// first to keep the substrate honest. Used by [`std::fmt::Display`]
379    /// (single source of truth) and as the `String` projection that
380    /// `From<RequestorKind> for String` ([`Self::into`]) composes so
381    /// emitters can spell `Requestor { kind: RequestorKind::GithubPr.into(), … }`
382    /// without re-typing the canonical literal at every author site.
383    #[must_use]
384    pub const fn as_str(self) -> &'static str {
385        match self {
386            Self::GithubPr => "github-pr",
387            Self::Manual => "manual",
388            Self::CiRun => "ci-run",
389            Self::Scheduled => "scheduled",
390        }
391    }
392}
393
394// `impl FromStr for RequestorKind` + `impl tatara_lisp::ClosedSet for
395// RequestorKind` + `impl std::fmt::Display for RequestorKind` are
396// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
397// declaration above. `label` delegates to the inherent
398// `RequestorKind::as_str` via `#[closed_set(via = "as_str")]` so the
399// kebab-case wire-format projection stays load-bearing (matches the
400// `tatara-github-watcher` factory + the CRD printcolumns + the
401// `PoolSelector.kinds` filter verbatim) while generic `T: ClosedSet`
402// consumers reach the STABLE workspace-wide name (`label`). The
403// `display` flag emits the `f.write_str(self.as_str())` delegation
404// block — the substrate-wide closed-set-enum idiom's third piece —
405// at the same proc-macro site rather than a hand-rolled
406// `fmt::Display` block per implementor.
407
408// `pub struct UnknownRequestorKind(pub String)` is generated by
409// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
410// on the enum declaration above. The auto-derived label `"requestor kind"`
411// matches the prior hand-rolled `#[error("unknown requestor kind: {0}")]`
412// verbatim — pinned generically by clause (5) of
413// `tatara_closed_set::assert_closed_set_well_formed::<RequestorKind>()` (called
414// from `requestor_kind_is_well_formed_closed_set` in the test module).
415// Symmetric to every sibling `Unknown*` error in this crate (e.g.
416// [`UnknownAllocationPhase`], [`crate::receipt::UnknownReceiptKind`],
417// [`crate::phase::UnknownPhase`], [`crate::lifetime::UnknownTeardownPolicy`]).
418
419impl From<RequestorKind> for String {
420    /// Composes [`RequestorKind::as_str`] into an owned `String` so
421    /// every `impl Into<String>` API surface (the `kind:` field
422    /// initializer on [`Requestor`] most notably) accepts the typed
423    /// variant transparently — the call site stays
424    /// `kind: RequestorKind::GithubPr.into()` and the typed → wire
425    /// bridge runs through ONE place. Sibling shape to
426    /// [`crate::receipt::ReceiptKind`]'s `From for String`.
427    fn from(k: RequestorKind) -> Self {
428        k.as_str().to_owned()
429    }
430}
431
432impl From<RequestorKind> for &'static str {
433    fn from(k: RequestorKind) -> Self {
434        k.as_str()
435    }
436}
437
438/// `EphemeralAllocation.status` — observed allocation state.
439#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
440#[serde(rename_all = "camelCase")]
441pub struct AllocationStatus {
442    /// Current lifecycle phase.
443    #[serde(default)]
444    pub phase: AllocationPhase,
445
446    /// When the phase last changed.
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub phase_since: Option<DateTime<Utc>>,
449
450    /// Pool that owns the matched member. Set as soon as routing
451    /// resolves; not cleared on release (audit trail).
452    #[serde(default, skip_serializing_if = "Option::is_none")]
453    pub bound_pool: Option<AllocationRef>,
454
455    /// The Process backing this allocation, if Bound.
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub assigned_process: Option<AllocationRef>,
458
459    /// When the allocation was matched to a Process.
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub allocated_at: Option<DateTime<Utc>>,
462
463    /// Wall-clock expiry derived from `spec.ttl` + `allocated_at`.
464    /// The pool reconciler force-returns the member at this point.
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub expires_at: Option<DateTime<Utc>>,
467
468    /// Operator-visible message.
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub message: Option<String>,
471
472    /// Standard Conditions.
473    ///
474    /// The empty case is skipped at serialization so a merge-patch
475    /// body built from a caller-supplied [`AllocationStatus`] whose
476    /// `conditions` slot has not been touched does NOT emit
477    /// `"conditions": []` on the wire — under RFC-7396 JSON Merge
478    /// Patch (the shape `Patch::Merge` sends) an empty array
479    /// REPLACES the persisted list rather than merges into it, so a
480    /// controller round-trip that reused a scratch `AllocationStatus`
481    /// as a patch body would silently clobber whatever conditions the
482    /// prior status carried. Peer to `phase_since` /
483    /// `bound_pool` / `assigned_process` / `allocated_at` /
484    /// `expires_at` above, each already skip-serialized on its
485    /// [`Default`]-equivalent variant.
486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
487    pub conditions: Vec<AllocationCondition>,
488}
489
490impl AllocationStatus {
491    /// Substrate composer for a phase-transition [`AllocationStatus`]
492    /// seed: stamps the THREE always-present slots (`phase` +
493    /// `phase_since = Some(now)` + `message = Some(<supplied>)`) and
494    /// defaults every other slot (`bound_pool` / `assigned_process` /
495    /// `allocated_at` / `expires_at` = `None`, `conditions = vec![]`).
496    /// Caller-branches attach the extra slots via struct-update
497    /// syntax onto the seed.
498    ///
499    /// Pre-lift the 4-slot phase-transition seed
500    /// ```rust,ignore
501    /// json!({
502    ///     "status": {
503    ///         "phase": <AllocationPhase-variant>,
504    ///         "phaseSince": Utc::now(),
505    ///         "message": "<transition-reason>",
506    ///         …optional caller-attached slots…
507    ///     }
508    /// })
509    /// ```
510    /// was hand-authored at FOUR sites past the ★★ PRIME-DIRECTIVE
511    /// ≥ 2 duplication threshold in
512    /// `tatara-pool-reconciler::controller_allocation::reconcile_inner`,
513    /// each restating the SAME `phase + phase_since + message` invariant
514    /// triplet on a different [`AllocationPhase`] variant:
515    /// * `AllocationDecision::NoMatchingPool` — the "no Pool selector
516    ///   matched this Requestor" fallthrough
517    ///   ([`AllocationPhase::NoMatchingPool`]).
518    /// * `AllocationDecision::Wait` — the "pool matched; no Free member
519    ///   available" queued path
520    ///   ([`AllocationPhase::Queued`]) with a `bound_pool` addition.
521    /// * `AllocationDecision::Bind` — the "bound to pool member"
522    ///   allocation path ([`AllocationPhase::Bound`]) with
523    ///   `bound_pool` + `assigned_process` + `allocated_at` +
524    ///   `expires_at` additions.
525    /// * `AllocationDecision::Release` — the "released; pool reconciler
526    ///   will return the member" release path
527    ///   ([`AllocationPhase::Released`]) with `bound_pool` +
528    ///   `assigned_process` additions.
529    ///
530    /// All four hand-authored the SAME `phaseSince: Utc::now()` stamp
531    /// alongside the phase transition, and all four spelled the
532    /// invariant triplet as bare JSON keys inside a `json!({...})`
533    /// literal — a fragile shape where any drift in the underlying
534    /// [`AllocationStatus`] field naming (a rename from `phaseSince`
535    /// to `phase_since` at the serde surface, a promotion of `message`
536    /// to a structured envelope) silently stops the JSON keys from
537    /// mapping to the typed struct's fields and the K8s API server
538    /// merges an ill-shaped patch. Post-lift the four callers build a
539    /// typed [`AllocationStatus`] via `AllocationStatus::transition`,
540    /// attach any branch-specific slots via struct-update syntax, and
541    /// wrap the result in `json!({ "status": s })` — the serde
542    /// `rename_all = "camelCase"` derive on [`AllocationStatus`] owns
543    /// the wire-shape composition, so a field rename lands at ONE
544    /// site (the derive) and every emit site inherits the upgrade
545    /// mechanically.
546    ///
547    /// Cross-CRD peer to [`crate::pool::PoolStatus::observed`] on the
548    /// same `<CRD>Status` substrate-composer axis — both primitives
549    /// stamp `phase_since = Some(now)` from a caller-supplied `now`
550    /// timestamp so the composer stays clock-injectable rather than
551    /// implicitly reading wall time, and both close every optional slot
552    /// with its [`Default`]-equivalent variant so a future slot
553    /// addition on either status shape plugs into the composer at ONE
554    /// site and every downstream emit site inherits the new slot
555    /// mechanically.
556    ///
557    /// Cross-CRD peer to the `tatara-reconciler::patch::phase_status_msg`
558    /// primitive on the (CRD × phase-transition-with-message) axis —
559    /// both primitives own the three-slot `phase + phase_since +
560    /// message` invariant on their respective CRDs' status subresource,
561    /// and both accept `impl Into<String>` for the message so the
562    /// callsite carries `&'static str` literal reasons and
563    /// `format!(...)`-owned strings without widening the signature.
564    ///
565    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
566    /// the 4-slot phase-transition status-seed incantation recurred at
567    /// four hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
568    /// duplication trigger, and is lifted to ONE owner here).
569    /// THEORY.md §II.1 invariant 5 (composition preserves proofs —
570    /// the pins bind the three always-present slots + the
571    /// [`Default`]-defaulted rest + byte-identical parity with the
572    /// pre-lift `json!({...})` triplet through serde round-trip, so a
573    /// regression that drifted any surface at
574    /// `tests::allocation_status_transition_*` rather than as silent
575    /// operator-visible skew between the four allocation-decision
576    /// patch sites).
577    #[must_use]
578    pub fn transition(
579        phase: AllocationPhase,
580        message: impl Into<String>,
581        now: DateTime<Utc>,
582    ) -> Self {
583        Self {
584            phase,
585            phase_since: Some(now),
586            message: Some(message.into()),
587            ..Default::default()
588        }
589    }
590
591    /// Substrate composer for a phase-transition [`AllocationStatus`]
592    /// seed whose `bound_pool` + `assigned_process` axis-pair is
593    /// stamped alongside the base [`Self::transition`] triplet
594    /// (`phase` + `phase_since = Some(now)` + `message =
595    /// Some(<supplied>)`). Every other slot lands at its
596    /// [`Default`]-equivalent variant so a caller-branch that attaches
597    /// an optional slot via struct-update syntax (a `Bind` arm's
598    /// `allocated_at` / `expires_at` addenda, say) does not silently
599    /// inherit a pre-populated non-`None` value.
600    ///
601    /// Pre-lift the `bound_pool: Some(pool)` + `assigned_process:
602    /// Some(AllocationRef::new(name, ns))` pair rode struct-update
603    /// syntax onto [`Self::transition`] at TWO sites past the ★★
604    /// PRIME-DIRECTIVE ≥ 2 duplication threshold in
605    /// `tatara-pool-reconciler::controller_allocation::reconcile_inner`
606    /// — the `AllocationDecision::Bind` arm ([`AllocationPhase::Bound`]
607    /// with two extra `allocated_at` / `expires_at` addenda) and the
608    /// `AllocationDecision::Release` arm ([`AllocationPhase::Released`]
609    /// with no addenda). Both restated the SAME pair-of-`Some`-slot
610    /// invariant against the SAME struct-update seed and funneled the
611    /// resulting body through the SAME `patch_status` call on
612    /// `Api<EphemeralAllocation>`. Post-lift both callers reach the
613    /// pair through ONE substrate composer; a future normalization on
614    /// the bound-set axis (a symmetry gate that the assigned_process's
615    /// namespace matches the bound_pool's namespace, a canonicalization
616    /// that closes the pair against a stale audit record, a
617    /// backwards-compatibility rename of either slot at the serde
618    /// surface) lands at ONE substrate site rather than at each
619    /// callsite in the two-arm allocation reconciler.
620    ///
621    /// Composes atop [`Self::transition`] so any future evolution to
622    /// the base three-slot invariant triplet (a `phase_since` rename,
623    /// a `message` promotion to a structured envelope, a fourth
624    /// always-stamped diagnostic slot) reaches this composer through
625    /// ONE substrate site and both consumers inherit the upgrade
626    /// mechanically. Sibling composition discipline to
627    /// [`crate::pool::PoolStatus::observed`]'s `state_count_fanout` +
628    /// `Utc::now()` fold — the compound composer names its axis + calls
629    /// the substrate primitive on the invariant it wraps rather than
630    /// restating the wrapped shape inline.
631    ///
632    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
633    /// the `bound_pool + assigned_process` pair recurred at two hand-
634    /// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
635    /// trigger, and is lifted to ONE owner here). THEORY.md §II.1
636    /// invariant 5 (composition preserves proofs — the pins bind the
637    /// pair + the composed base triplet + byte-identical parity with
638    /// the pre-lift struct-update shape through serde round-trip, so a
639    /// regression that drifted any surface at
640    /// `tests::allocation_status_bound_transition_*` rather than as
641    /// silent operator-visible skew between the two Bind / Release
642    /// patch sites).
643    #[must_use]
644    pub fn bound_transition(
645        phase: AllocationPhase,
646        message: impl Into<String>,
647        now: DateTime<Utc>,
648        bound_pool: AllocationRef,
649        assigned_process: AllocationRef,
650    ) -> Self {
651        Self {
652            bound_pool: Some(bound_pool),
653            assigned_process: Some(assigned_process),
654            ..Self::transition(phase, message, now)
655        }
656    }
657}
658
659/// Allocation lifecycle phase.
660///
661/// Sibling closed-set lifts on the same `EphemeralAllocation` /
662/// `EphemeralPool` axis: [`crate::pool::ReplacementPolicy::ALL`],
663/// [`crate::pool::ReturnPolicy::ALL`]. Sibling closed-sets on the
664/// `tatara-process` algebra: [`crate::lifetime::TeardownPolicy::ALL`],
665/// [`crate::lifetime::LifetimeKind::ALL`],
666/// [`crate::boundary::ConditionKind::ALL`],
667/// [`crate::intent::IntentKind::ALL`],
668/// [`crate::phase::ProcessPhase::ALL`],
669/// [`crate::signal::ProcessSignal::ALL`].
670#[derive(
671    Clone,
672    Copy,
673    Debug,
674    PartialEq,
675    Eq,
676    Hash,
677    Serialize,
678    Deserialize,
679    JsonSchema,
680    tatara_closed_set::DeriveClosedSet,
681)]
682#[serde(rename_all = "PascalCase")]
683#[closed_set(via = "as_str", generate_unknown, display)]
684pub enum AllocationPhase {
685    /// Admitted; pool selector matching not yet attempted.
686    Pending,
687    /// Routed to a pool but no `Free` member is available — queued.
688    Queued,
689    /// A pool member has been assigned + transitioned to Allocated.
690    Bound,
691    /// `expires_at` reached or requestor deleted; member is returning.
692    Releasing,
693    /// Released; the allocation is a permanent audit record.
694    Released,
695    /// No pool selector matched. The reconciler will retry on each
696    /// pool spec update; surfaced in status so operators see why.
697    NoMatchingPool,
698    /// Pool refused (e.g., `max_size` reached and no member can be
699    /// freed) — operator intervention needed.
700    Failed,
701}
702
703impl Default for AllocationPhase {
704    fn default() -> Self {
705        Self::Pending
706    }
707}
708
709impl AllocationPhase {
710    /// The closed set of allocation phases — single source of truth
711    /// that drives the `as_str` / Display / `FromStr` triad AND the
712    /// `is_terminal` / `needs_pool_routing` predicate pair the
713    /// allocation reconciler's observe/decide split dispatches on.
714    /// Adding an eighth variant lands at one `ALL` entry + one
715    /// `as_str` arm + one arm per predicate — exhaustively checked by
716    /// the compiler (the `[Self; 7]` array literal forces the arity)
717    /// and by the implication test
718    /// (`allocation_phase_terminal_excludes_routing`) so a new
719    /// variant can't claim to be both terminal AND routing-eligible.
720    pub const ALL: [Self; 7] = [
721        Self::Pending,
722        Self::Queued,
723        Self::Bound,
724        Self::Releasing,
725        Self::Released,
726        Self::NoMatchingPool,
727        Self::Failed,
728    ];
729
730    /// Canonical PascalCase wire-format projection — matches the
731    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
732    /// `enum:` enumeration the allocation reconciler stamps on the
733    /// `ephemeralallocations.tatara.pleme.io` schema. Pinned by
734    /// `allocation_phase_as_str_matches_serde` so a variant rename
735    /// can't drift between the typed surface, the CRD enum, the YAML
736    /// wire format AND any operator-facing diagnostic composed via
737    /// Display rather than a hard-coded literal that would silently
738    /// rot.
739    pub const fn as_str(self) -> &'static str {
740        match self {
741            Self::Pending => "Pending",
742            Self::Queued => "Queued",
743            Self::Bound => "Bound",
744            Self::Releasing => "Releasing",
745            Self::Released => "Released",
746            Self::NoMatchingPool => "NoMatchingPool",
747            Self::Failed => "Failed",
748        }
749    }
750
751    /// True iff the allocation has reached an absorbing state —
752    /// `Released` (clean audit record) or `Failed` (pool refused;
753    /// operator intervention needed). The allocation reconciler
754    /// short-circuits both phases to `NoOp` rather than re-running
755    /// the routing / heartbeat ladder against a settled record.
756    ///
757    /// Closed-set match (not `matches!`) so a future variant
758    /// triggers the compiler's exhaustiveness check at this site
759    /// rather than silently defaulting to `false` and letting a new
760    /// terminal phase fall through into pool rebinding. Paired with
761    /// `needs_pool_routing` they form the two-axis projection
762    /// `allocation_decide::AllocationConvergence::decide` matches
763    /// against — the impossible bucket `(true, true)` is pinned
764    /// empty by `allocation_phase_terminal_excludes_routing`.
765    pub const fn is_terminal(self) -> bool {
766        match self {
767            Self::Released | Self::Failed => true,
768            Self::Pending | Self::Queued | Self::Bound | Self::Releasing | Self::NoMatchingPool => {
769                false
770            }
771        }
772    }
773
774    /// True iff the allocation is on the routing path — the
775    /// reconciler still needs to resolve a target pool + look up a
776    /// free member. `Pending` (just admitted), `Queued` (matched
777    /// pool was full last tick), and `NoMatchingPool` (no selector
778    /// matched yet; retry on pool spec updates) all live here. The
779    /// settled non-terminal phases `Bound` (already matched) and
780    /// `Releasing` (being torn down) don't — they short-circuit to
781    /// the heartbeat / release ladder without re-resolving the pool.
782    ///
783    /// Closed-set match (not `matches!`) — same exhaustiveness
784    /// discipline as [`Self::is_terminal`]. Lifts the open-coded
785    /// `phase != Released && phase != Bound` gate that
786    /// `allocation_decide::AllocationConvergenceCtx::observe` used
787    /// to predicate pool resolution on, AND closes the latent gap
788    /// where `Failed` / `Releasing` (neither `Released` nor `Bound`)
789    /// would slip through to the routing branch — a `Failed`
790    /// allocation without a deletion timestamp could be silently
791    /// rebound to a fresh pool member, which is the opposite of
792    /// "operator intervention needed."
793    pub const fn needs_pool_routing(self) -> bool {
794        match self {
795            Self::Pending | Self::Queued | Self::NoMatchingPool => true,
796            Self::Bound | Self::Releasing | Self::Released | Self::Failed => false,
797        }
798    }
799}
800
801// `impl FromStr for AllocationPhase` + `impl tatara_lisp::ClosedSet for
802// AllocationPhase` + `impl std::fmt::Display for AllocationPhase` are
803// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` on the enum
804// declaration above. `label` delegates to the inherent
805// `AllocationPhase::as_str` via `#[closed_set(via = "as_str")]` so the
806// PascalCase wire-format projection stays load-bearing (matches the serde
807// rename + the CRD `enum:` enumeration the allocation reconciler stamps
808// on the `ephemeralallocations.tatara.pleme.io` schema verbatim) while
809// generic `T: ClosedSet` consumers reach the STABLE workspace-wide name
810// (`label`). The `display` flag emits the `f.write_str(self.as_str())`
811// delegation block at the same proc-macro site rather than a
812// hand-rolled `fmt::Display` block per implementor.
813
814// `pub struct UnknownAllocationPhase(pub String)` is generated by
815// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(generate_unknown)]`
816// on the enum declaration above. The auto-derived label `"allocation phase"`
817// matches the prior hand-rolled `#[error("unknown allocation phase: {0}")]`
818// verbatim — pinned generically by clause (5) of
819// `tatara_closed_set::assert_closed_set_well_formed::<AllocationPhase>()` (called
820// from `allocation_phase_is_well_formed_closed_set` in the test module).
821// Symmetric to [`crate::pool::UnknownReplacementPolicy`],
822// [`crate::pool::UnknownReturnPolicy`],
823// [`crate::lifetime::UnknownTeardownPolicy`],
824// [`crate::boundary::UnknownConditionKind`], and
825// [`crate::phase::UnknownPhase`].
826
827/// Allocation Condition (same shape as PoolCondition for downstream
828/// uniformity).
829#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
830#[serde(rename_all = "camelCase")]
831pub struct AllocationCondition {
832    pub type_: String,
833    pub status: String,
834    pub reason: String,
835    pub message: String,
836    pub last_transition_time: DateTime<Utc>,
837}
838
839impl EphemeralAllocation {
840    /// The copy-form status-projection primitive on the phase axis:
841    /// returns the [`AllocationPhase`] the pool reconciler currently
842    /// persists at `status.phase`, wrapped in an `Option` so the
843    /// missing-`status` corner collapses to `None` — the ONE-liner
844    /// collapse of the paired `self.status.as_ref().map(|s| s.phase)`
845    /// incantation the pool reconciler's `AllocationConvergenceCtx::
846    /// observe` restated by hand pre-lift.
847    ///
848    /// Cross-CRD peer to [`crate::prelude::Process::observed_phase`]
849    /// on the (CRD × phase-slot × observed-status) axis pair — both
850    /// primitives walk the identical `.status.as_ref().map(|s| s.
851    /// phase)` shape, differing only in the `Phase` type projected
852    /// ([`AllocationPhase`] vs [`crate::phase::ProcessPhase`]). The
853    /// substrate now owns the borrow-form `.status.as_ref().map(|s|
854    /// s.phase)` chain axis-uniformly across the two `Phase`-having
855    /// CRDs so a future normalization (a generation-filter that
856    /// returns `None` for a phase stamped with a stale
857    /// `metadata.generation`, a staleness gate that drops a phase
858    /// whose observing `phase_since` predates a reconcile deadline,
859    /// a canonicalization pass that maps a phase outside the CRD's
860    /// closed set to `None`) lands at ONE substrate method per CRD
861    /// rather than being restated at every observer.
862    #[must_use]
863    pub fn observed_phase(&self) -> Option<AllocationPhase> {
864        self.status.as_ref().map(|s| s.phase)
865    }
866
867    /// The copy-form status-projection primitive on the phase axis
868    /// with the [`AllocationPhase::Pending`] sink applied — the
869    /// ONE-liner collapse of the paired `self.observed_phase().
870    /// unwrap_or(AllocationPhase::Pending)` incantation the pool
871    /// reconciler's `AllocationConvergenceCtx::observe` restated by
872    /// hand pre-lift as a 5-line `.status.as_ref().map(|s| s.phase).
873    /// unwrap_or(AllocationPhase::Pending)` chain.
874    ///
875    /// Pre-lift the chain sat at [`tatara-pool-reconciler::
876    /// allocation_decide::AllocationConvergenceCtx::observe`]'s
877    /// `phase` seed. Cross-CRD peer to [`crate::prelude::Process::
878    /// observed_phase_or_pending`] on the (CRD × phase-slot × sink)
879    /// axis pair — both primitives close the missing-`status`
880    /// corner with each CRD's respective [`Default`]-equivalent
881    /// `Pending` variant, and both compose on top of their peer
882    /// [`Self::observed_phase`] / [`crate::prelude::Process::
883    /// observed_phase`] borrow-form projections so a future
884    /// normalization at the underlying `observed_phase` primitive
885    /// reaches both the raw-`Option` accessor and the `Pending`-
886    /// sinked composer through the SAME upstream body.
887    ///
888    /// The [`AllocationPhase::Pending`] sink is load-bearing as the
889    /// "not yet observed" default — the pool reconciler's typed
890    /// `AllocationPhase::needs_pool_routing` predicate returns
891    /// `true` for `Pending`, so a freshly-admitted Allocation whose
892    /// pool reconciler has not yet stamped a `.status` slot reads
893    /// as `Pending` and immediately enters the routing ladder,
894    /// matching the pre-lift `AllocationPhase::Pending` fallback
895    /// semantics verbatim.
896    ///
897    /// Theory anchor: THEORY.md §VI.1 (generation over composition
898    /// — the two-link `.status.as_ref().map(|s| s.phase).unwrap_or
899    /// (AllocationPhase::Pending)` chain recurred at both the
900    /// [`crate::prelude::Process`] site (already lifted onto
901    /// [`crate::prelude::Process::observed_phase_or_pending`]) AND
902    /// the [`EphemeralAllocation`] site by hand, i.e. the SHAPE
903    /// itself recurs past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
904    /// trigger, and is lifted to ONE owner per CRD here). THEORY.md
905    /// §II.1 invariant 5 (composition preserves proofs — the pins
906    /// bind the missing-`status` sink to `Pending` + populated-
907    /// status pass-through + every [`AllocationPhase`] variant
908    /// round-trip + byte-identical parity with the pre-lift
909    /// two-link chain + cross-CRD peer coherence with
910    /// [`crate::prelude::Process::observed_phase_or_pending`], so
911    /// a regression that drifted any surface at
912    /// `tests::observed_phase_*` rather than as silent operator-
913    /// facing skew between the allocation observer's routing seed
914    /// and the Process observer's dispatch seed).
915    #[must_use]
916    pub fn observed_phase_or_pending(&self) -> AllocationPhase {
917        self.observed_phase().unwrap_or(AllocationPhase::Pending)
918    }
919
920    /// The borrow-form status-projection primitive on the bound-pool
921    /// axis: returns the [`AllocationRef`] the pool reconciler
922    /// currently persists at `status.bound_pool` (name + namespace of
923    /// the pool that owns the matched member), with the
924    /// missing-`status` corner AND the empty-slot corner BOTH
925    /// collapsed to `None` — the ONE-liner collapse of the paired
926    /// `self.status.as_ref().and_then(|s| s.bound_pool.<clone|as_ref>())`
927    /// incantation the pool reconciler's `AllocationConvergenceCtx::
928    /// observe` restated by hand pre-lift.
929    ///
930    /// Cross-CRD peer to [`crate::prelude::Process::observed_identity`]
931    /// on the (CRD × structured-record-slot × borrow-form) axis pair
932    /// — both primitives walk the identical `.status.as_ref()
933    /// .and_then(|s| s.<slot>.as_ref())` shape, differing only in the
934    /// record projected ([`AllocationRef`] here, [`crate::identity::
935    /// Identity`] on `Process`). The substrate now owns the
936    /// borrow-form `.status.as_ref().and_then(|s| s.<slot>.as_ref())`
937    /// chain on the second `structured-record` slot across the two
938    /// `status`-having CRDs, so a future normalization step (a
939    /// generation-filter that returns `None` for a bound-pool
940    /// reference stamped with a stale `metadata.generation`, a
941    /// canonicalization pass that rejects a malformed
942    /// `(name, namespace)` pair, a cross-cluster reference-rewrite
943    /// gate) lands at ONE substrate method per CRD rather than being
944    /// restated at every observer.
945    ///
946    /// Return-form axis: `Option<&AllocationRef>` mirrors the
947    /// borrow-first discipline of [`crate::prelude::Process::
948    /// observed_identity`]. The lone pre-lift consumer
949    /// ([`tatara-pool-reconciler::allocation_decide::
950    /// AllocationConvergenceCtx::observe`]'s `bound_pool` seed) spelled
951    /// the projection as `.and_then(|s| s.bound_pool.clone())` — an
952    /// eager clone allocated inside every reconcile pass even when the
953    /// downstream branch (the Release-composition arm) needed only the
954    /// borrow for the `.as_ref()` re-projection two lines later.
955    /// Post-lift the consumer reaches the primitive borrow-first
956    /// (`alloc.observed_bound_pool().cloned()`) and the empty-borrow
957    /// corner clones nothing (`Option::cloned` on `None` is `None`);
958    /// the composition point where the owned `AllocationRef` fallback
959    /// is required (the `AllocationConvergenceCtx` snapshot slot,
960    /// still `Option<AllocationRef>`-typed for serde stability) is the
961    /// ONLY site that materializes an owned copy.
962    ///
963    /// The missing-`status` corner AND the populated-status-with-
964    /// `bound_pool=None` corner BOTH collapse to `None` so
965    /// `.is_some()` / `if let Some(_)` / `.cloned()` behave
966    /// identically on an `EphemeralAllocation` whose status field is
967    /// `None` and on one whose status carries an unpopulated
968    /// `bound_pool` slot — matching what the pre-lift `.and_then(...)`
969    /// chain produced. Consumers that need to tell those corners
970    /// apart reach for [`Self::status`] directly, exactly as the
971    /// existing peer accessors [`Self::observed_phase`] +
972    /// [`Self::observed_phase_or_pending`] admit.
973    ///
974    /// Theory anchor: THEORY.md §VI.1 (generation over composition
975    /// — the `.status.as_ref().and_then(|s| s.<structured-record>
976    /// .<clone|as_ref>())` shape recurred as ONE hand-authored
977    /// `.and_then(|s| s.bound_pool.clone())` chain in
978    /// [`tatara-pool-reconciler::allocation_decide::
979    /// AllocationConvergenceCtx::observe`] AND as the peer
980    /// [`crate::prelude::Process::observed_identity`] primitive
981    /// already owned on the `Process` CRD's `status.identity` slot,
982    /// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger at
983    /// substrate-shape level. THEORY.md §II.1 invariant 5
984    /// (composition preserves proofs — the pins bind the missing-
985    /// `status` corner + the empty-`bound_pool`-slot corner + the
986    /// borrow-form `&AllocationRef` lifetime + the zero-copy
987    /// projection contract + byte-identical parity with the pre-lift
988    /// `.and_then(|s| s.bound_pool.clone())` chain across the full
989    /// corner set + cross-CRD peer coherence with
990    /// [`crate::prelude::Process::observed_identity`], so a
991    /// regression that drifted any surface at
992    /// `tests::observed_bound_pool_*` rather than as silent operator-
993    /// facing skew between the allocation observer's Release-
994    /// composition seed and the Process observer's FORK-time
995    /// identity seed on the SAME reconcile tick).
996    #[must_use]
997    pub fn observed_bound_pool(&self) -> Option<&AllocationRef> {
998        self.status.as_ref().and_then(|s| s.bound_pool.as_ref())
999    }
1000
1001    /// The copy-form status-projection primitive on the TTL-expiry axis:
1002    /// returns the wall-clock deadline the pool reconciler currently
1003    /// persists at `status.expires_at` (derived from `spec.ttl` +
1004    /// `allocated_at` at Bind time), wrapped in an `Option` so both the
1005    /// missing-`status` corner AND the populated-status-with-`expires_at
1006    /// =None` corner collapse to `None` — the ONE-liner collapse of the
1007    /// paired `self.status.as_ref().and_then(|s| s.expires_at)`
1008    /// incantation the pool reconciler's `AllocationConvergenceCtx::
1009    /// observe` restated by hand pre-lift.
1010    ///
1011    /// Same-CRD peer to [`Self::observed_phase`] on the (CRD × copy-form
1012    /// × status-slot) axis pair — both primitives walk the identical
1013    /// `.status.as_ref().<map|and_then>(|s| s.<Copy-field>)` shape,
1014    /// differing only in the record projected ([`DateTime<Utc>`] here,
1015    /// [`AllocationPhase`] on the phase axis) and in the outer combinator
1016    /// (`and_then` here because the persisted field is itself an
1017    /// `Option<DateTime<Utc>>`, `map` there because the persisted phase
1018    /// is bare). The substrate now owns the copy-form
1019    /// `.status.as_ref().<map|and_then>(|s| s.<Copy-field>)` chain
1020    /// axis-uniformly across every `Copy`-valued slot on
1021    /// `AllocationStatus`, so a future normalization (a clock-skew
1022    /// guard that drops an `expires_at` stamped before its owning
1023    /// allocation's observed `allocated_at`, a canonicalization pass
1024    /// that clamps a deadline to a monotonic upper bound, a stale-
1025    /// timestamp gate that returns `None` on an `expires_at` older than
1026    /// a controller-configured horizon) lands at ONE substrate method
1027    /// rather than being restated at every observer.
1028    ///
1029    /// Return-form axis: `Option<DateTime<Utc>>` mirrors the copy-first
1030    /// discipline of [`Self::observed_phase`]. The lone pre-lift consumer
1031    /// ([`tatara-pool-reconciler::allocation_decide::
1032    /// AllocationConvergenceCtx::observe`]'s `expires_at` seed) spelled
1033    /// the projection as `.status.as_ref().and_then(|s| s.expires_at)` —
1034    /// a 3-link hand-authored chain the observer walked on every
1035    /// reconcile pass. Post-lift the consumer reaches the primitive
1036    /// once and the whole missing-status + empty-slot corner cross
1037    /// collapses at the substrate rather than at the callsite.
1038    ///
1039    /// The missing-`status` corner AND the populated-status-with-
1040    /// `expires_at=None` corner BOTH collapse to `None` so
1041    /// `.is_some()` / `if let Some(_)` / any `>=` deadline comparison
1042    /// behave identically on an `EphemeralAllocation` whose status
1043    /// field is `None` and on one whose status carries an unpopulated
1044    /// `expires_at` slot — matching what the pre-lift `.and_then(...)`
1045    /// chain produced. Consumers that need to tell those corners apart
1046    /// reach for [`Self::status`] directly, exactly as the existing peer
1047    /// accessors [`Self::observed_phase`] +
1048    /// [`Self::observed_phase_or_pending`] admit.
1049    ///
1050    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1051    /// the `.status.as_ref().and_then(|s| s.<Copy-field>)` shape
1052    /// recurred as ONE hand-authored chain in
1053    /// [`tatara-pool-reconciler::allocation_decide::
1054    /// AllocationConvergenceCtx::observe`] AND as the copy-form peer
1055    /// [`Self::observed_phase`] primitive already owned on the same
1056    /// CRD's `status.phase` slot, past the substrate-shape recurrence
1057    /// trigger; the substrate now owns the third status-projection
1058    /// primitive on `EphemeralAllocation`, closing the copy-form family
1059    /// alongside the borrow-form [`Self::observed_bound_pool`]).
1060    /// THEORY.md §II.1 invariant 5 (composition preserves proofs — the
1061    /// pins bind the missing-`status` corner + the empty-`expires_at`-
1062    /// slot corner + the copy-form `DateTime<Utc>` return + byte-
1063    /// identical parity with the pre-lift `.and_then(|s| s.expires_at)`
1064    /// chain across the full corner set, so a regression that drifted
1065    /// any surface surfaces at `tests::observed_expires_at_*` rather
1066    /// than as silent operator-facing skew between the allocation
1067    /// observer's Release-composition TTL gate and any future consumer
1068    /// that reaches for the same slot).
1069    #[must_use]
1070    pub fn observed_expires_at(&self) -> Option<DateTime<Utc>> {
1071        self.status.as_ref().and_then(|s| s.expires_at)
1072    }
1073
1074    /// The namespaced-CRD constructor composer on the
1075    /// `EphemeralAllocation` axis: forwards `(name, spec)` to the
1076    /// kube-derived [`Self::new`] constructor + stamps
1077    /// `metadata.namespace` with the caller-supplied slot in ONE
1078    /// step. The ONE-liner collapse of the paired `let mut a =
1079    /// EphemeralAllocation::new(<name>, <spec>); a.meta_mut().
1080    /// namespace = Some(<ns>.into());` incantation every allocation-
1081    /// side emitter restated by hand pre-lift.
1082    ///
1083    /// Pre-lift the 2-line construct-then-set-namespace chain was
1084    /// hand-authored at TWO sites past the ★★ PRIME-DIRECTIVE ≥ 2
1085    /// duplication threshold across TWO workspace crates, composing
1086    /// a namespaced `EphemeralAllocation` fixture / event from a
1087    /// `name` slot and an `AllocationSpec`:
1088    /// * `tatara-github-watcher::allocation_factory::build_allocation`
1089    ///   — the production PR-webhook → `EphemeralAllocation` emitter
1090    ///   at `alloc.meta_mut().namespace = Some(namespace.to_string
1091    ///   ())`; stamps the ephemeral-pools namespace as part of the
1092    ///   canonical opened / reopened / synchronize allocation shape.
1093    /// * `tatara-pool-reconciler::allocation_decide::tests::alloc` —
1094    ///   the allocation-decision test fixture pinned to `"pools"`,
1095    ///   sibling to the peer `pool` fixture in the same module that
1096    ///   composes an `EphemeralPool` via [`crate::pool::EphemeralPool::
1097    ///   new_in`] on the same `ns` slot value.
1098    ///
1099    /// Both sites walked the SAME 2-line chain and both wanted the
1100    /// `EphemeralAllocation` back with `metadata.namespace` stamped as
1101    /// `Some(<ns>.into())`. Post-lift each callsite reads
1102    /// `EphemeralAllocation::new_in(<name>, <ns>, <spec>)` and the
1103    /// produced value feeds the same downstream `Api::create(&pp,
1104    /// &alloc)` chain / test-battery input unchanged.
1105    ///
1106    /// The `impl Into<String>` at the `namespace` slot matches the
1107    /// sibling `impl Into<String>`-widening discipline the workspace's
1108    /// other namespaced-CRD-adjacent composers walk
1109    /// ([`crate::pool::PoolMember::unallocated`] on the
1110    /// `process_name` slot, [`crate::pool::AllocationRef::new`] on the
1111    /// `(name, namespace)` slot pair, [`Requestor::kind_only`] on the
1112    /// `kind` slot) and accepts BOTH `&'static str` (the majority
1113    /// pre-lift caller shape) AND owned `String` at the SAME
1114    /// signature.
1115    ///
1116    /// Peer to [`crate::pool::EphemeralPool::new_in`] on the sister
1117    /// `EphemeralPool` CRD — the two primitives partition the
1118    /// namespaced-CRD-constructor family axis for the two pool-
1119    /// adjacent CRDs the workspace stamps at reconciler fixture /
1120    /// GitHub-webhook-emitter time. A future normalization (a per-
1121    /// fleet virtual-cluster prefix rewrite on the `namespace` slot,
1122    /// a per-cluster canonical case-fold pass, a `generateName`
1123    /// fallback on the `name` slot, an operator-scoped default
1124    /// namespace for cluster-local test rigs, an audit-tag stamped
1125    /// on every fixture-emitted CRD for post-hoc grep discipline)
1126    /// lands at ONE primitive body per CRD and every downstream
1127    /// consumer inherits the upgrade mechanically.
1128    ///
1129    /// `#[must_use]` on the return keeps a caller from composing the
1130    /// namespaced value and dropping it un-passed to a `kube::Api`
1131    /// create call or a `Vec<EphemeralAllocation>` reconciler-input
1132    /// slot.
1133    ///
1134    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
1135    /// the 2-line construct-then-set-namespace chain recurred at
1136    /// TWO hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
1137    /// duplication trigger, spanning two workspace crates, and is
1138    /// lifted to ONE substrate owner here). THEORY.md §II.1
1139    /// invariant 5 (composition preserves proofs — the pins below
1140    /// bind the (name-slot → metadata.name, ns-slot → metadata.
1141    /// namespace, spec-slot → spec) slot-projection triple + the
1142    /// byte-identical parity with the pre-lift 2-line chain across
1143    /// the two representative `impl Into<String>` value shapes
1144    /// (`&'static str` and owned `String`) + the sibling-composer
1145    /// coherence with [`Self::new`]).
1146    #[must_use]
1147    pub fn new_in(name: &str, namespace: impl Into<String>, spec: AllocationSpec) -> Self {
1148        let mut a = Self::new(name, spec);
1149        a.metadata.namespace = Some(namespace.into());
1150        a
1151    }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    // `FromStr` lives in scope at the test surface only — the derive
1157    // emits `impl ::core::str::FromStr` via the full path so the lib
1158    // body no longer reaches `FromStr` directly, but the cross-axis
1159    // sweeps + the verbatim-echo contract tests call
1160    // `AllocationPhase::from_str(bad)` / `bad.parse::<RequestorKind>()`.
1161    use std::str::FromStr;
1162
1163    use super::*;
1164
1165    #[test]
1166    fn requestor_minimum_shape_round_trips() {
1167        let r = Requestor {
1168            kind: "github-pr".into(),
1169            repo: Some("pleme-io/demo-app".into()),
1170            branch: Some("fix-something".into()),
1171            pr_number: Some(123),
1172            sha: Some("abc123def".into()),
1173            pr_labels: vec!["needs-ephemeral".into()],
1174            actor: Some("drzln".into()),
1175        };
1176        let yaml = serde_yaml::to_string(&r).unwrap();
1177        assert!(yaml.contains("kind: github-pr"));
1178        assert!(yaml.contains("prNumber: 123"));
1179        let back: Requestor = serde_yaml::from_str(&yaml).unwrap();
1180        assert_eq!(back.kind, "github-pr");
1181        assert_eq!(back.pr_number, Some(123));
1182    }
1183
1184    #[test]
1185    fn allocation_status_defaults_pending() {
1186        let s = AllocationStatus::default();
1187        assert_eq!(s.phase, AllocationPhase::Pending);
1188        assert!(s.bound_pool.is_none());
1189        assert!(s.assigned_process.is_none());
1190    }
1191
1192    #[test]
1193    fn allocation_phase_round_trips_via_serde() {
1194        for p in [
1195            AllocationPhase::Pending,
1196            AllocationPhase::Queued,
1197            AllocationPhase::Bound,
1198            AllocationPhase::Releasing,
1199            AllocationPhase::Released,
1200            AllocationPhase::NoMatchingPool,
1201            AllocationPhase::Failed,
1202        ] {
1203            let s = serde_yaml::to_string(&p).unwrap();
1204            let back: AllocationPhase = serde_yaml::from_str(&s).unwrap();
1205            assert_eq!(back, p);
1206        }
1207    }
1208
1209    // ── closed-set algebra contracts for AllocationPhase
1210    //    (ALL × as_str × FromStr × predicate-pair) ────────────────────
1211
1212    /// `ALL` is the source of truth — pin its closure so a variant
1213    /// added without an `ALL` entry fails here via the uniqueness
1214    /// check before drifting `FromStr` or the sweep tests below. The
1215    /// arity is asserted by the `[Self; 7]` array type itself.
1216    ///
1217    /// Structural well-formedness of [`AllocationPhase`] as a
1218    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1219    /// testkit lift that pins all three structural invariants
1220    /// (`ALL` is non-empty, every variant round-trips through
1221    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1222    /// outside the closed set) at ONE call site. Replaces the hand-
1223    /// derived `allocation_phase_all_is_unique_and_complete` +
1224    /// `allocation_phase_roundtrip_via_as_str` + the empty-input arm
1225    /// of `unknown_allocation_phase_errors`. `FromStr` delegates to
1226    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this
1227    /// helper exercises the same code path the allocation reconciler
1228    /// hits when parsing a CRD `enum:`-validated value back to the
1229    /// typed phase.
1230    #[test]
1231    fn allocation_phase_is_well_formed_closed_set() {
1232        tatara_closed_set::assert_closed_set_well_formed::<AllocationPhase>();
1233    }
1234
1235    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1236    /// output verbatim for every variant. A future variant rename
1237    /// (or an `as_str` arm typo) lands here at one site, instead of
1238    /// drifting between the typed surface, the CRD enum, the YAML
1239    /// wire format, and the operator-facing reason strings the
1240    /// reconciler stamps via Display.
1241    #[test]
1242    fn allocation_phase_as_str_matches_serde() {
1243        crate::tagged_union::assert_label_matches_serde_serialization::<AllocationPhase>();
1244    }
1245
1246    /// The Display impl IS `as_str` — pinning this lets future
1247    /// callers reach for either projection without drift.
1248    #[test]
1249    fn allocation_phase_display_matches_as_str() {
1250        crate::tagged_union::assert_display_matches_label::<AllocationPhase>();
1251    }
1252
1253    /// `FromStr` rejects strings that aren't in the canonical
1254    /// projection — lowercased / typo / unrelated — and the error
1255    /// echoes the input verbatim so the operator-facing diagnostic
1256    /// carries the offending value, not a normalized form. The
1257    /// empty-input arm is pinned by
1258    /// [`allocation_phase_is_well_formed_closed_set`] via the
1259    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1260    /// verbatim-echo contract on the [`UnknownAllocationPhase`]
1261    /// newtype, which the trait's `make_unknown` can't see.
1262    #[test]
1263    fn unknown_allocation_phase_errors() {
1264        for bad in [
1265            "pending",
1266            "BOUND",
1267            "no-matching-pool",
1268            "release",
1269            "failed_state",
1270            "Reaped",
1271        ] {
1272            let err = AllocationPhase::from_str(bad).unwrap_err();
1273            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1274        }
1275    }
1276
1277    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
1278    /// documented per-variant disposition. `Released` + `Failed` are
1279    /// terminal (absorbing); `Pending` / `Queued` / `NoMatchingPool`
1280    /// need pool routing; `Bound` / `Releasing` are settled-but-not-
1281    /// terminal (heartbeat / release ladder).
1282    #[test]
1283    fn allocation_phase_predicate_truth_tables() {
1284        assert!(!AllocationPhase::Pending.is_terminal());
1285        assert!(AllocationPhase::Pending.needs_pool_routing());
1286
1287        assert!(!AllocationPhase::Queued.is_terminal());
1288        assert!(AllocationPhase::Queued.needs_pool_routing());
1289
1290        assert!(!AllocationPhase::Bound.is_terminal());
1291        assert!(!AllocationPhase::Bound.needs_pool_routing());
1292
1293        assert!(!AllocationPhase::Releasing.is_terminal());
1294        assert!(!AllocationPhase::Releasing.needs_pool_routing());
1295
1296        assert!(AllocationPhase::Released.is_terminal());
1297        assert!(!AllocationPhase::Released.needs_pool_routing());
1298
1299        assert!(!AllocationPhase::NoMatchingPool.is_terminal());
1300        assert!(AllocationPhase::NoMatchingPool.needs_pool_routing());
1301
1302        assert!(AllocationPhase::Failed.is_terminal());
1303        assert!(!AllocationPhase::Failed.needs_pool_routing());
1304    }
1305
1306    /// IMPLICATION CONTRACT: `is_terminal → !needs_pool_routing`. A
1307    /// terminal allocation cannot also be routing-eligible — that's
1308    /// the bug the typed projection closes (a `Failed` allocation
1309    /// that's neither `Released` nor `Bound` would otherwise slip
1310    /// through the open-coded gate in `observe` and try to rebind to
1311    /// a pool member). A future variant that flipped both predicates
1312    /// true would fail here, forcing the author to flip one or
1313    /// extend the consumer dispatch site in
1314    /// `tatara-pool-reconciler::allocation_decide` deliberately
1315    /// rather than letting an impossible state slip in.
1316    #[test]
1317    fn allocation_phase_terminal_excludes_routing() {
1318        for phase in AllocationPhase::ALL {
1319            assert!(
1320                !(phase.is_terminal() && phase.needs_pool_routing()),
1321                "{phase:?} is both terminal and routing-eligible",
1322            );
1323        }
1324    }
1325
1326    /// DEFAULT-AGREEMENT CONTRACT: `AllocationPhase::default()` is
1327    /// `Pending` — the entry state, neither terminal nor settled —
1328    /// and it lives on the routing path. A future default-variant
1329    /// rename without flipping the predicates fails here.
1330    #[test]
1331    fn allocation_phase_default_is_pending_and_routes() {
1332        let d = AllocationPhase::default();
1333        assert_eq!(d, AllocationPhase::Pending);
1334        assert!(!d.is_terminal());
1335        assert!(d.needs_pool_routing());
1336    }
1337
1338    // ── RequestorKind closed-set truth-table ─────────────────────────
1339
1340    /// Structural well-formedness of [`RequestorKind`] as a
1341    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1342    /// testkit lift that pins all three structural invariants
1343    /// (`ALL` is non-empty, every variant round-trips through
1344    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1345    /// outside the closed set) at ONE call site. Replaces the hand-
1346    /// derived `requestor_kind_all_enumerates_each_variant_exactly_once`
1347    /// + `requestor_kind_from_str_round_trips_canonical_names` + the
1348    /// empty-input arm of `requestor_kind_from_str_rejects_open_kinds`.
1349    /// `FromStr` delegates to
1350    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1351    /// exercises the same code path
1352    /// [`Requestor::known_kind`]'s `Option<RequestorKind>` collapse
1353    /// rides on when classifying inbound `Requestor.kind` strings. The
1354    /// arity is asserted by the `[Self; 4]` array type itself.
1355    #[test]
1356    fn requestor_kind_is_well_formed_closed_set() {
1357        tatara_closed_set::assert_closed_set_well_formed::<RequestorKind>();
1358    }
1359
1360    /// Byte-exact wire-format pin — renaming any of these is a wire-
1361    /// format change (the `tatara-github-watcher` emitter, the CRD
1362    /// printcolumns, the `PoolSelector.kinds` filter strings, the
1363    /// per-test `kind: "…".into()` fixtures all depend on these
1364    /// literals), not a typed-internal refactor.
1365    #[test]
1366    fn requestor_kind_canonical_names_pinned() {
1367        assert_eq!(RequestorKind::GithubPr.as_str(), "github-pr");
1368        assert_eq!(RequestorKind::Manual.as_str(), "manual");
1369        assert_eq!(RequestorKind::CiRun.as_str(), "ci-run");
1370        assert_eq!(RequestorKind::Scheduled.as_str(), "scheduled");
1371    }
1372
1373    /// `FromStr` rejects strings that aren't in the canonical
1374    /// projection — lowercased-mismatch / typo / unrelated — and the
1375    /// error echoes the input verbatim so the operator-facing
1376    /// diagnostic carries the offending value, not a normalized form.
1377    /// The schema is open at the wire layer (operators MAY register
1378    /// new kinds and `Requestor::known_kind` collapses them to
1379    /// `None`), but the closed-set view is byte-exact. The empty-input
1380    /// arm is pinned by [`requestor_kind_is_well_formed_closed_set`]
1381    /// via the `tatara_lisp::ClosedSet` testkit; the cases here pin
1382    /// the verbatim-echo contract on the [`UnknownRequestorKind`]
1383    /// newtype, which the trait's `make_unknown` can't see.
1384    #[test]
1385    fn requestor_kind_from_str_rejects_open_kinds() {
1386        for bad in [
1387            "github_pr",
1388            "GithubPr",
1389            "operator-custom-kind",
1390            "ci_run",
1391            "Scheduled",
1392        ] {
1393            let err = bad.parse::<RequestorKind>().unwrap_err();
1394            assert_eq!(err, UnknownRequestorKind(bad.to_string()));
1395        }
1396    }
1397
1398    /// The Display impl IS `as_str` — pinning this lets future
1399    /// callers reach for either projection without drift (Display is
1400    /// what operator-facing diagnostics compose against).
1401    #[test]
1402    fn requestor_kind_display_delegates_to_as_str() {
1403        for k in RequestorKind::ALL {
1404            assert_eq!(format!("{k}"), k.as_str());
1405        }
1406    }
1407
1408    /// The `String` projection that `From<RequestorKind> for String`
1409    /// ([`RequestorKind::into`]) composes is byte-equal to `as_str`.
1410    /// This is the typed → wire bridge — emitters spell
1411    /// `kind: RequestorKind::GithubPr.into()` and the canonical
1412    /// literal is materialized at ONE place.
1413    #[test]
1414    fn requestor_kind_into_string_matches_as_str() {
1415        for k in RequestorKind::ALL {
1416            let s: String = k.into();
1417            assert_eq!(s, k.as_str());
1418        }
1419    }
1420
1421    /// The typed → wire → typed round-trip: composing a `Requestor`
1422    /// with `kind: RequestorKind::X.into()` produces an object whose
1423    /// `known_kind()` decodes back to `X`. Pins the bridge invariant
1424    /// at the `Requestor` boundary, not just at `RequestorKind`.
1425    #[test]
1426    fn known_kind_decodes_built_requestors() {
1427        for k in RequestorKind::ALL {
1428            // Routes through the ONE substrate composer
1429            // `Requestor::kind_only` — one of TEN pre-lift exact-match
1430            // sites past the ★★ PRIME-DIRECTIVE ≥ 2 threshold.
1431            let r = Requestor::kind_only(k);
1432            assert_eq!(r.known_kind(), Some(k), "round-trip failed for {k:?}");
1433        }
1434    }
1435
1436    /// Open-by-design: a custom operator-registered kind still
1437    /// stamps a valid `Requestor` (no schema rejection), it just
1438    /// doesn't project through the closed-set typed view. Mirrors
1439    /// `ReceiptEnvelope::known_kind`'s open-kind posture.
1440    #[test]
1441    fn known_kind_returns_none_for_open_kinds() {
1442        // Routes through the ONE substrate composer
1443        // `Requestor::kind_only` — one of TEN pre-lift exact-match
1444        // sites past the ★★ PRIME-DIRECTIVE ≥ 2 threshold.
1445        let r = Requestor::kind_only("operator-custom-kind");
1446        assert_eq!(r.known_kind(), None);
1447    }
1448
1449    /// The four canonical literals match every previously-published
1450    /// fixture / doc anchor in this crate — pinning the bridge to
1451    /// existing call sites so any drift fails here before the next
1452    /// release ships.
1453    #[test]
1454    fn requestor_kind_matches_existing_fixture_literals() {
1455        // The `requestor_minimum_shape_round_trips` fixture above
1456        // composes `kind: "github-pr".into()` verbatim.
1457        assert_eq!(RequestorKind::GithubPr.as_str(), "github-pr");
1458        // The `allocation_spec_omits_optional_fields` fixture below
1459        // composes `kind: "manual".into()` verbatim.
1460        assert_eq!(RequestorKind::Manual.as_str(), "manual");
1461    }
1462
1463    // ─── Requestor::kind_only substrate pins ────────────────────────
1464    //
1465    // Fail-before-pass-after granularity: `Requestor::kind_only` did
1466    // not exist before this commit. The composer's job is to bind ONE
1467    // caller-varying slot (`kind`) and freeze the six-slot default
1468    // tail so a future addition to `Requestor` lands at ONE primitive
1469    // body rather than at every fixture / default-shape callsite.
1470    // Sibling to the `AplicacaoIntent::chart_only` pin family (the
1471    // 7-slot chart-pointer-only composer) and the `PoolSpec::with_template`
1472    // pin family (the 11-slot pool full-spec composer).
1473
1474    #[test]
1475    fn kind_only_binds_kind_slot_and_defaults_the_other_six() {
1476        // Positional-binding pin: the sole caller slot lands at
1477        // `kind`; every other slot lands at its safe empty default
1478        // (`None` / `vec![]`).
1479        let r = Requestor::kind_only("manual");
1480        assert_eq!(r.kind, "manual");
1481        assert!(r.repo.is_none());
1482        assert!(r.branch.is_none());
1483        assert!(r.pr_number.is_none());
1484        assert!(r.sha.is_none());
1485        assert!(r.pr_labels.is_empty());
1486        assert!(r.actor.is_none());
1487    }
1488
1489    #[test]
1490    fn kind_only_matches_hand_authored_pre_lift_struct_literal_shape() {
1491        // Byte-identity pin: every pre-lift `Requestor { kind: <lit>
1492        // .into(), repo: None, branch: None, pr_number: None, sha:
1493        // None, pr_labels: vec![], actor: None }` shape must
1494        // deserialize back to the same fixture composed via
1495        // `kind_only`. Sweeps the two families every callsite used
1496        // (`"manual"` — the operator-authored fixture at seven
1497        // sites; `"github-pr"` — the github-webhook fixture at three
1498        // sites) plus one open-kind sample (`"operator-custom-kind"` —
1499        // the `known_kind_returns_none_for_open_kinds` open-kind pin)
1500        // so any drift between the primitive and the pre-lift shape
1501        // surfaces at ONE pin rather than as silent fixture skew at
1502        // ten downstream consumers.
1503        for kind in ["manual", "github-pr", "operator-custom-kind"] {
1504            let via_primitive = Requestor::kind_only(kind);
1505            let hand_authored = Requestor {
1506                kind: kind.into(),
1507                repo: None,
1508                branch: None,
1509                pr_number: None,
1510                sha: None,
1511                pr_labels: vec![],
1512                actor: None,
1513            };
1514            let via_yaml = serde_yaml::to_string(&via_primitive).unwrap();
1515            let hand_yaml = serde_yaml::to_string(&hand_authored).unwrap();
1516            assert_eq!(
1517                via_yaml, hand_yaml,
1518                "kind_only({kind:?}) must be YAML-identical to the pre-lift struct literal"
1519            );
1520        }
1521    }
1522
1523    #[test]
1524    fn kind_only_accepts_string_and_str_and_requestor_kind_uniformly() {
1525        // `impl Into<String>` symmetry across the three caller shapes
1526        // pre-lift authors used verbatim: `&'static str` (`"manual"`),
1527        // owned `String` (from a formatted context), and
1528        // [`RequestorKind`] (via the `From<RequestorKind> for String`
1529        // bridge exercised at `known_kind_decodes_built_requestors`).
1530        let from_str_literal = Requestor::kind_only("manual");
1531        let from_owned_string = Requestor::kind_only(String::from("manual"));
1532        let from_typed_variant = Requestor::kind_only(RequestorKind::Manual);
1533        assert_eq!(from_str_literal.kind, "manual");
1534        assert_eq!(from_owned_string.kind, "manual");
1535        assert_eq!(from_typed_variant.kind, "manual");
1536    }
1537
1538    #[test]
1539    fn kind_only_composes_downstream_through_known_kind_projection() {
1540        // Cross-primitive coherence pin: every substrate-emitted
1541        // `RequestorKind` variant round-trips through `kind_only` +
1542        // `known_kind` back to the same typed variant. Byte-identical
1543        // to the `known_kind_decodes_built_requestors` sweep the
1544        // primitive replaced — pins the primitive as the composer the
1545        // typed decoder sees the SAME wire shape from.
1546        for k in RequestorKind::ALL {
1547            let r = Requestor::kind_only(k);
1548            assert_eq!(
1549                r.known_kind(),
1550                Some(k),
1551                "kind_only({k:?}).known_kind() must round-trip to Some({k:?})"
1552            );
1553        }
1554    }
1555
1556    // Per-implementor `unknown_X_message_matches_substrate_convention`
1557    // tests removed — clause (5) of
1558    // `tatara_closed_set::assert_closed_set_well_formed::<T>()` now verifies
1559    // the substrate-wide `"unknown {SET_LABEL}: {input}"` carrier shape
1560    // generically (called above on `RequestorKind` /
1561    // `AllocationPhase` through their `*_is_well_formed_closed_set`
1562    // sites). The `SET_LABEL` projection is pinned independently by
1563    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests` —
1564    // together the two contracts guarantee the operator-facing
1565    // diagnostic without needing per-enum literal pins.
1566
1567    // ─── EphemeralAllocation::observed_phase* substrate pins ────────
1568    //
1569    // Fail-before-pass-after granularity: neither `observed_phase` nor
1570    // `observed_phase_or_pending` existed before this commit, so each
1571    // pin fails to compile until the corresponding inherent method
1572    // lands. Post-lift the pins bind the missing-`status` corner + the
1573    // populated-status pass-through + byte-identical parity with the
1574    // pre-lift 5-line `.status.as_ref().map(|s| s.phase).unwrap_or
1575    // (AllocationPhase::Pending)` chain the pool reconciler's
1576    // `AllocationConvergenceCtx::observe` walked. Cross-CRD peer
1577    // coherence with `Process::observed_phase_or_pending` is pinned
1578    // by the `_matches_process_peer_shape` sweep at the tail.
1579
1580    fn alloc_with_phase(phase: AllocationPhase) -> EphemeralAllocation {
1581        // AllocationSpec rides through the ONE substrate composer
1582        // `AllocationSpec::requestor_only`; the inner Requestor rides
1583        // through the peer composer `Requestor::kind_only`. Nine pre-
1584        // lift exact-match `AllocationSpec { pool_ref: None,
1585        // requestor: <r>, ttl: None, note: None }` fixture sites past
1586        // the ★★ PRIME-DIRECTIVE ≥ 2 threshold collapse onto this
1587        // ONE substrate owner.
1588        let spec = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
1589        let mut a = EphemeralAllocation::new("obs-alloc", spec);
1590        a.status = Some(AllocationStatus {
1591            phase,
1592            ..AllocationStatus::default()
1593        });
1594        a
1595    }
1596
1597    fn alloc_without_status() -> EphemeralAllocation {
1598        // AllocationSpec rides through `AllocationSpec::requestor_only`
1599        // — sibling to `alloc_with_phase`.
1600        let spec = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
1601        let mut a = EphemeralAllocation::new("no-status-alloc", spec);
1602        a.status = None;
1603        a
1604    }
1605
1606    #[test]
1607    fn observed_phase_returns_none_when_status_is_none() {
1608        let a = alloc_without_status();
1609        assert!(a.observed_phase().is_none());
1610    }
1611
1612    #[test]
1613    fn observed_phase_returns_populated_variant_verbatim() {
1614        for p in AllocationPhase::ALL {
1615            let a = alloc_with_phase(p);
1616            assert_eq!(
1617                a.observed_phase(),
1618                Some(p),
1619                "observed_phase must project the persisted variant verbatim for {p:?}"
1620            );
1621        }
1622    }
1623
1624    #[test]
1625    fn observed_phase_matches_pre_lift_chain_bytewise() {
1626        // Sweep every corner: (status: None) plus every populated
1627        // (status: Some(phase)) variant. The pre-lift chain was
1628        // `alloc.status.as_ref().map(|s| s.phase)` — a 3-link chain
1629        // hand-authored inline at the observer. The primitive must
1630        // return the same `Option<AllocationPhase>` on every corner.
1631        let none_alloc = alloc_without_status();
1632        assert_eq!(
1633            none_alloc.observed_phase(),
1634            none_alloc.status.as_ref().map(|s| s.phase),
1635        );
1636        for p in AllocationPhase::ALL {
1637            let a = alloc_with_phase(p);
1638            assert_eq!(
1639                a.observed_phase(),
1640                a.status.as_ref().map(|s| s.phase),
1641                "primitive must be byte-identical to the pre-lift chain for {p:?}",
1642            );
1643        }
1644    }
1645
1646    #[test]
1647    fn observed_phase_or_pending_defaults_to_pending_when_status_absent() {
1648        let a = alloc_without_status();
1649        assert_eq!(a.observed_phase_or_pending(), AllocationPhase::Pending);
1650    }
1651
1652    #[test]
1653    fn observed_phase_or_pending_returns_populated_phase_verbatim() {
1654        for p in AllocationPhase::ALL {
1655            let a = alloc_with_phase(p);
1656            assert_eq!(
1657                a.observed_phase_or_pending(),
1658                p,
1659                "populated status must pass through verbatim for {p:?}"
1660            );
1661        }
1662    }
1663
1664    #[test]
1665    fn observed_phase_or_pending_defaults_agree_with_allocation_phase_default() {
1666        // The `Pending` sink is load-bearing as the "not yet observed"
1667        // default. `AllocationPhase::default()` returns `Pending`; the
1668        // primitive must return the same variant on the missing-status
1669        // corner. A future default-variant rename that flipped
1670        // `AllocationPhase::default` without flipping the primitive
1671        // (or vice versa) surfaces here as a divergent seed for the
1672        // routing ladder.
1673        let a = alloc_without_status();
1674        assert_eq!(a.observed_phase_or_pending(), AllocationPhase::default());
1675    }
1676
1677    #[test]
1678    fn observed_phase_or_pending_matches_pre_lift_chain_bytewise() {
1679        // The exact pre-lift 5-line chain in
1680        // `tatara-pool-reconciler::allocation_decide::
1681        // AllocationConvergenceCtx::observe` was:
1682        //     let phase = alloc
1683        //         .status
1684        //         .as_ref()
1685        //         .map(|s| s.phase)
1686        //         .unwrap_or(AllocationPhase::Pending);
1687        // Sweep every corner: (status: None) plus every populated
1688        // status variant. The primitive must be byte-identical for
1689        // every corner so the observer's routing decision matches
1690        // bytewise post-lift.
1691        let none_alloc = alloc_without_status();
1692        assert_eq!(
1693            none_alloc.observed_phase_or_pending(),
1694            none_alloc
1695                .status
1696                .as_ref()
1697                .map(|s| s.phase)
1698                .unwrap_or(AllocationPhase::Pending),
1699        );
1700        for p in AllocationPhase::ALL {
1701            let a = alloc_with_phase(p);
1702            assert_eq!(
1703                a.observed_phase_or_pending(),
1704                a.status
1705                    .as_ref()
1706                    .map(|s| s.phase)
1707                    .unwrap_or(AllocationPhase::Pending),
1708                "primitive must be byte-identical to the pre-lift 5-line chain for {p:?}",
1709            );
1710        }
1711    }
1712
1713    #[test]
1714    fn observed_phase_or_pending_composes_from_observed_phase() {
1715        // The composer sits on top of the borrow-form projection —
1716        // `observed_phase_or_pending() == observed_phase().unwrap_or
1717        // (Pending)`. Pinning the composition means a future
1718        // normalization step layered onto `observed_phase` (a
1719        // generation-filter, a staleness gate, a canonicalization
1720        // pass) reaches BOTH the raw-`Option` accessor and the
1721        // `Pending`-sinked composer through the SAME upstream body,
1722        // without needing a per-corner rewrite of the composer.
1723        let none_alloc = alloc_without_status();
1724        assert_eq!(
1725            none_alloc.observed_phase_or_pending(),
1726            none_alloc
1727                .observed_phase()
1728                .unwrap_or(AllocationPhase::Pending),
1729        );
1730        for p in AllocationPhase::ALL {
1731            let a = alloc_with_phase(p);
1732            assert_eq!(
1733                a.observed_phase_or_pending(),
1734                a.observed_phase().unwrap_or(AllocationPhase::Pending),
1735                "composer must ride on top of the borrow-form projection for {p:?}",
1736            );
1737        }
1738    }
1739
1740    #[test]
1741    fn observed_phase_is_a_pure_projection() {
1742        // Reading the phase twice must not mutate the allocation or
1743        // its status slot — pure projection semantics. Also witnesses
1744        // that the accessor doesn't clone / drop the inner `phase`
1745        // (the `Copy` scalar comes out identical on both reads).
1746        let a = alloc_with_phase(AllocationPhase::Bound);
1747        let one = a.observed_phase();
1748        let two = a.observed_phase();
1749        assert_eq!(one, two);
1750        assert!(a.status.is_some(), "projection must not consume the status");
1751    }
1752
1753    #[test]
1754    fn observed_phase_pending_missing_status_and_populated_pending_collapse_to_same_composer_output(
1755    ) {
1756        // A subtle correctness pin: the missing-`status` corner and
1757        // a populated-with-Pending status BOTH read as `Pending`
1758        // through the composer — the observer cannot distinguish the
1759        // two through this accessor. This matches the pre-lift 5-line
1760        // chain's semantics exactly (an operator patching
1761        // `status.phase: Pending` is indistinguishable from a
1762        // freshly-admitted allocation with no status stamped yet).
1763        // The borrow-form `observed_phase` accessor DOES distinguish
1764        // the two, so a caller that needs to tell them apart reaches
1765        // for the raw `Option`.
1766        let none_alloc = alloc_without_status();
1767        let pending_alloc = alloc_with_phase(AllocationPhase::Pending);
1768
1769        assert_eq!(
1770            none_alloc.observed_phase_or_pending(),
1771            pending_alloc.observed_phase_or_pending(),
1772        );
1773        assert_ne!(
1774            none_alloc.observed_phase(),
1775            pending_alloc.observed_phase(),
1776            "borrow-form accessor MUST distinguish missing-status from populated-Pending",
1777        );
1778    }
1779
1780    #[test]
1781    fn observed_phase_or_pending_missing_status_sink_agrees_with_process_peer_shape() {
1782        // Cross-CRD peer-axis coherence with
1783        // `Process::observed_phase_or_pending`. Both primitives walk
1784        // the identical `.status.as_ref().map(|s| s.phase).unwrap_or
1785        // (<Phase>::Pending)` chain differing ONLY in the `Phase`
1786        // type projected. On a missing-status observation, each
1787        // primitive must return its CRD's `Default`-equivalent
1788        // `Pending` variant — for `EphemeralAllocation` that's
1789        // `AllocationPhase::Pending`; for `Process` that's
1790        // `crate::phase::ProcessPhase::Pending`. This pin binds the
1791        // sink-parity structurally so a future rename of either
1792        // default variant surfaces here as a divergent seed for the
1793        // observer's routing / dispatch decision rather than as
1794        // silent drift between the two reconcilers.
1795        let no_status_alloc = alloc_without_status();
1796        assert_eq!(
1797            no_status_alloc.observed_phase_or_pending(),
1798            AllocationPhase::default(),
1799        );
1800        // Peer-axis invariant on the `Process` side — the primitive
1801        // that owns the same shape reads `ProcessPhase::Pending` on
1802        // the missing-status corner via its own inherent method. The
1803        // parity is coordinated at the `Default` seat: both CRDs'
1804        // phase types default to `Pending`, so a rename that broke
1805        // one without the other would fail one of these two
1806        // conjoined assertions.
1807        assert_eq!(AllocationPhase::default(), AllocationPhase::Pending,);
1808        assert_eq!(
1809            crate::phase::ProcessPhase::default(),
1810            crate::phase::ProcessPhase::Pending,
1811        );
1812    }
1813
1814    // ─── EphemeralAllocation::observed_bound_pool substrate pins ────
1815    //
1816    // The borrow-form status-projection primitive on the bound-pool
1817    // axis. Collapses the pre-lift hand-authored `.status.as_ref()
1818    // .and_then(|s| s.bound_pool.clone())` chain in
1819    // `tatara-pool-reconciler::allocation_decide::
1820    // AllocationConvergenceCtx::observe`'s `bound_pool` seed onto the
1821    // ONE substrate primitive. Cross-CRD peer to
1822    // `Process::observed_identity` on the (CRD × structured-record-
1823    // slot × borrow-form) axis pair — both primitives walk the
1824    // identical `.status.as_ref().and_then(|s| s.<slot>.as_ref())`
1825    // shape. Each pin is fail-before-pass-after: `observed_bound_pool`
1826    // did not exist pre-lift, so any test invoking it fails to compile
1827    // pre-lift and passes post-lift.
1828
1829    fn sample_pool_ref(name: &str, ns: &str) -> AllocationRef {
1830        // Fixture ref rides through the ONE substrate composer
1831        // `AllocationRef::new` — the `impl Into<String>` signature
1832        // accepts the borrow-form `&str` slot pair verbatim without
1833        // a per-fixture `.to_string()` promotion.
1834        AllocationRef::new(name, ns)
1835    }
1836
1837    fn alloc_with_bound_pool(bound: Option<AllocationRef>) -> EphemeralAllocation {
1838        // AllocationSpec rides through `AllocationSpec::requestor_only`
1839        // — sibling to `alloc_with_phase`.
1840        let spec = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
1841        let mut a = EphemeralAllocation::new("bp-alloc", spec);
1842        a.status = Some(AllocationStatus {
1843            phase: AllocationPhase::Bound,
1844            bound_pool: bound,
1845            ..AllocationStatus::default()
1846        });
1847        a
1848    }
1849
1850    #[test]
1851    fn observed_bound_pool_returns_none_when_status_is_none() {
1852        // Missing-`status` corner pin: the primitive collapses the
1853        // no-status case to `None` so downstream `.is_some()` /
1854        // `if let Some(_)` / `.cloned().unwrap_or_else(...)` behave
1855        // identically on an `EphemeralAllocation` whose status field
1856        // is `None` and on one whose status carries an unpopulated
1857        // `bound_pool` slot. Matches the pre-lift `.and_then(...)`
1858        // chain's `None` byte-identically at the pool reconciler's
1859        // Release-composition seed.
1860        let a = alloc_without_status();
1861        assert!(a.observed_bound_pool().is_none());
1862    }
1863
1864    #[test]
1865    fn observed_bound_pool_returns_none_when_slot_is_none() {
1866        // Empty-slot-under-populated-status corner pin: the primitive
1867        // returns `None`, matching the missing-`status` corner byte-
1868        // identically. A regression that treated the two corners
1869        // differently would silently promote an internal representation
1870        // detail (whether the pool reconciler has ever written a
1871        // status subresource) into observable behavior at the
1872        // Release-composition branch of the allocation reconciler's
1873        // `decide` transition rule.
1874        let a = alloc_with_bound_pool(None);
1875        assert!(a.observed_bound_pool().is_none());
1876    }
1877
1878    #[test]
1879    fn observed_bound_pool_returns_borrow_when_slot_is_populated() {
1880        // Happy-path pin: with a populated `status.bound_pool` slot,
1881        // the primitive returns a borrowed `&AllocationRef` whose
1882        // (name, namespace) fields match the persisted record. A
1883        // regression that filtered / reshaped / canonicalized the
1884        // record would surface here rather than as silent skew at the
1885        // Release-composition seed's `.cloned()` materialization.
1886        let expected = sample_pool_ref("demo-pool", "pools");
1887        let a = alloc_with_bound_pool(Some(expected.clone()));
1888        let observed = a.observed_bound_pool().expect("populated slot");
1889        assert_eq!(observed, &expected);
1890        assert_eq!(observed.name, "demo-pool");
1891        assert_eq!(observed.namespace, "pools");
1892    }
1893
1894    #[test]
1895    fn observed_bound_pool_is_a_zero_copy_borrow_projection() {
1896        // Borrow-discipline pin: the returned reference points at the
1897        // persisted `AllocationRef` in place — NOT a fresh allocation
1898        // or a clone. A regression that switched the projection to an
1899        // owned `AllocationRef` (via `.clone()`) would defeat the
1900        // zero-copy contract the lift's primary strict-widening
1901        // delivers (the observer's Release-composition arm clones
1902        // once at the composition point where the
1903        // `AllocationConvergenceCtx` snapshot slot requires the owned
1904        // value). Peer to the sibling
1905        // `Process::observed_identity_is_a_zero_copy_borrow_projection`
1906        // pin on the `Process` CRD's `status.identity` slot.
1907        let a = alloc_with_bound_pool(Some(sample_pool_ref("demo-pool", "pools")));
1908        let observed = a.observed_bound_pool().expect("populated slot") as *const _;
1909        let persisted = a.status.as_ref().unwrap().bound_pool.as_ref().unwrap() as *const _;
1910        assert!(std::ptr::eq(observed, persisted));
1911    }
1912
1913    #[test]
1914    fn observed_bound_pool_is_a_pure_projection() {
1915        // Purity pin: calling the projection twice on the same
1916        // `EphemeralAllocation` returns byte-identical borrows (same
1917        // pointer). A regression that introduced state — a lazy-
1918        // cached reference, a normalization step that ran once and
1919        // cached — would surface here rather than as silent drift
1920        // between two dispatches within one reconcile pass.
1921        let a = alloc_with_bound_pool(Some(sample_pool_ref("demo-pool", "pools")));
1922        let one = a.observed_bound_pool().expect("populated slot") as *const _;
1923        let two = a.observed_bound_pool().expect("populated slot") as *const _;
1924        assert!(std::ptr::eq(one, two));
1925    }
1926
1927    #[test]
1928    fn observed_bound_pool_matches_pre_lift_chain_bytewise() {
1929        // Byte-identical parity pin between the borrow-form primitive
1930        // here and the pre-lift `tatara-pool-reconciler`
1931        // `.status.as_ref().and_then(|s| s.bound_pool.clone())` chain.
1932        // Sweeps every corner every callsite plausibly encounters
1933        // (missing status, empty `bound_pool` slot, populated
1934        // `bound_pool` slot). A regression that inserted a
1935        // normalization step at the primitive the pre-lift chain does
1936        // NOT apply — or vice versa — surfaces here rather than as
1937        // silent drift between the pre-lift consumer site and the ONE
1938        // substrate owner it now routes through.
1939        fn pre_lift(a: &EphemeralAllocation) -> Option<AllocationRef> {
1940            a.status.as_ref().and_then(|s| s.bound_pool.clone())
1941        }
1942        // Missing status.
1943        let a = alloc_without_status();
1944        assert_eq!(a.observed_bound_pool().cloned(), pre_lift(&a));
1945        // Populated status, empty `bound_pool` slot.
1946        let a = alloc_with_bound_pool(None);
1947        assert_eq!(a.observed_bound_pool().cloned(), pre_lift(&a));
1948        // Populated status, populated `bound_pool` slot.
1949        let a = alloc_with_bound_pool(Some(sample_pool_ref("demo-pool", "pools")));
1950        assert_eq!(a.observed_bound_pool().cloned(), pre_lift(&a));
1951    }
1952
1953    #[test]
1954    fn observed_bound_pool_missing_status_and_empty_slot_collapse_to_the_same_option_shape() {
1955        // Cross-corner coherence pin: the missing-`status` corner and
1956        // the populated-empty-slot corner return `Option`s whose
1957        // `.is_none()` / `.is_some()` observations are IDENTICAL. A
1958        // regression that promoted the missing-`status` corner to a
1959        // typed error (via a signature change to `Result<_, _>`) — or
1960        // that widened the empty-slot corner to a synthetic
1961        // `Some(AllocationRef::default())` — would surface here rather
1962        // than as silent operator-facing divergence between a never-
1963        // status-written allocation and a bound-pool-cleared
1964        // allocation on the Release-composition branch.
1965        let a_no_status = alloc_without_status();
1966        let a_empty_slot = alloc_with_bound_pool(None);
1967        assert_eq!(
1968            a_no_status.observed_bound_pool().is_none(),
1969            a_empty_slot.observed_bound_pool().is_none(),
1970        );
1971        assert_eq!(
1972            a_no_status.observed_bound_pool().is_some(),
1973            a_empty_slot.observed_bound_pool().is_some(),
1974        );
1975    }
1976
1977    #[test]
1978    fn observed_bound_pool_shape_agrees_with_process_observed_identity_peer_axis() {
1979        // Cross-CRD peer-axis coherence pin binding the SAME
1980        // `.status.as_ref().and_then(|s| s.<slot>.as_ref())` shape
1981        // that both `EphemeralAllocation::observed_bound_pool` (this
1982        // primitive) and `Process::observed_identity` walk, differing
1983        // ONLY in the record projected. Structural test — both
1984        // signatures must resolve as `&Self -> Option<&Record>` fn
1985        // pointers, so a future rename or a signature drift that
1986        // (say) widened one side to `Option<Record>` or narrowed one
1987        // side to `Option<&str>` fails to compile here rather than
1988        // silently drifting the two reconcilers apart at their
1989        // respective observer seeds. The runtime side of the pin
1990        // sweeps the missing-status + empty-slot corners on the
1991        // `EphemeralAllocation` half; the `Process` half is exercised
1992        // by its own `crd.rs::tests::observed_identity_*` pin
1993        // family — this test binds only the peer-axis shape.
1994        let a_no_status = alloc_without_status();
1995        let a_empty_slot = alloc_with_bound_pool(None);
1996        assert!(a_no_status.observed_bound_pool().is_none());
1997        assert!(a_empty_slot.observed_bound_pool().is_none());
1998        // Structural peer-axis coherence: bind both signatures as fn
1999        // pointers at their peer resolution type so the compiler
2000        // refuses to build if either side's shape drifts. The `_`
2001        // let-bindings assert the target type inference.
2002        let _bound_pool_shape: fn(&EphemeralAllocation) -> Option<&AllocationRef> =
2003            EphemeralAllocation::observed_bound_pool;
2004        let _identity_shape: fn(&crate::prelude::Process) -> Option<&crate::identity::Identity> =
2005            crate::prelude::Process::observed_identity;
2006    }
2007
2008    // ─── EphemeralAllocation::observed_expires_at substrate pins ────
2009    //
2010    // The copy-form status-projection primitive on the TTL-expiry axis.
2011    // Collapses the pre-lift hand-authored `.status.as_ref().and_then(
2012    // |s| s.expires_at)` chain in `tatara-pool-reconciler::
2013    // allocation_decide::AllocationConvergenceCtx::observe`'s
2014    // `expires_at` seed onto the ONE substrate primitive. Same-CRD peer
2015    // to `observed_phase` on the (copy-form × status-slot) axis — both
2016    // primitives walk the identical `.status.as_ref().<map|and_then>(
2017    // |s| s.<Copy-field>)` shape. Each pin is fail-before-pass-after:
2018    // `observed_expires_at` did not exist pre-lift, so any test invoking
2019    // it fails to compile pre-lift and passes post-lift.
2020
2021    fn alloc_with_expires_at(expires_at: Option<DateTime<Utc>>) -> EphemeralAllocation {
2022        // AllocationSpec rides through `AllocationSpec::requestor_only`
2023        // — sibling to `alloc_with_phase`.
2024        let spec = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
2025        let mut a = EphemeralAllocation::new("exp-alloc", spec);
2026        a.status = Some(AllocationStatus {
2027            phase: AllocationPhase::Bound,
2028            expires_at,
2029            ..AllocationStatus::default()
2030        });
2031        a
2032    }
2033
2034    #[test]
2035    fn observed_expires_at_returns_none_when_status_is_none() {
2036        // Missing-`status` corner pin: the primitive collapses the
2037        // no-status case to `None` so downstream `.is_some()` / any
2038        // deadline comparison behaves identically on an
2039        // `EphemeralAllocation` whose status field is `None` and on
2040        // one whose status carries an unpopulated `expires_at` slot.
2041        // Matches the pre-lift `.and_then(...)` chain's `None` byte-
2042        // identically at the pool reconciler's Release-composition
2043        // TTL gate.
2044        let a = alloc_without_status();
2045        assert!(a.observed_expires_at().is_none());
2046    }
2047
2048    #[test]
2049    fn observed_expires_at_returns_none_when_slot_is_none() {
2050        // Empty-slot-under-populated-status corner pin: the primitive
2051        // returns `None`, matching the missing-`status` corner byte-
2052        // identically. A regression that treated the two corners
2053        // differently would silently promote an internal representation
2054        // detail (whether the pool reconciler has ever written a
2055        // `status.expires_at` field for a not-yet-Bound allocation)
2056        // into observable behavior at the Release-composition branch
2057        // of the allocation reconciler's `decide` transition rule.
2058        let a = alloc_with_expires_at(None);
2059        assert!(a.observed_expires_at().is_none());
2060    }
2061
2062    #[test]
2063    fn observed_expires_at_returns_populated_timestamp_verbatim() {
2064        // Happy-path pin: with a populated `status.expires_at` slot,
2065        // the primitive returns the persisted `DateTime<Utc>` verbatim.
2066        // A regression that filtered / clamped / canonicalized the
2067        // timestamp would surface here rather than as silent skew at
2068        // the Release-composition TTL gate's `>=` deadline comparison.
2069        let expected = Utc::now();
2070        let a = alloc_with_expires_at(Some(expected));
2071        assert_eq!(a.observed_expires_at(), Some(expected));
2072    }
2073
2074    #[test]
2075    fn observed_expires_at_is_a_pure_projection() {
2076        // Purity pin: calling the projection twice on the same
2077        // `EphemeralAllocation` returns byte-identical `Option`s. A
2078        // regression that introduced state — a lazy-cached value, a
2079        // normalization step that ran once and cached — would surface
2080        // here rather than as silent drift between two dispatches
2081        // within one reconcile pass.
2082        let expected = Utc::now();
2083        let a = alloc_with_expires_at(Some(expected));
2084        assert_eq!(a.observed_expires_at(), a.observed_expires_at());
2085    }
2086
2087    #[test]
2088    fn observed_expires_at_matches_pre_lift_chain_bytewise() {
2089        // Byte-identical parity pin between the copy-form primitive
2090        // here and the pre-lift `tatara-pool-reconciler`
2091        // `.status.as_ref().and_then(|s| s.expires_at)` chain. Sweeps
2092        // every corner every callsite plausibly encounters (missing
2093        // status, empty `expires_at` slot, populated `expires_at`
2094        // slot). A regression that inserted a normalization step at
2095        // the primitive the pre-lift chain does NOT apply — or vice
2096        // versa — surfaces here rather than as silent drift between
2097        // the pre-lift consumer site and the ONE substrate owner it
2098        // now routes through.
2099        fn pre_lift(a: &EphemeralAllocation) -> Option<DateTime<Utc>> {
2100            a.status.as_ref().and_then(|s| s.expires_at)
2101        }
2102        // Missing status.
2103        let a = alloc_without_status();
2104        assert_eq!(a.observed_expires_at(), pre_lift(&a));
2105        // Populated status, empty `expires_at` slot.
2106        let a = alloc_with_expires_at(None);
2107        assert_eq!(a.observed_expires_at(), pre_lift(&a));
2108        // Populated status, populated `expires_at` slot.
2109        let a = alloc_with_expires_at(Some(Utc::now()));
2110        assert_eq!(a.observed_expires_at(), pre_lift(&a));
2111    }
2112
2113    #[test]
2114    fn observed_expires_at_missing_status_and_empty_slot_collapse_to_the_same_option_shape() {
2115        // Cross-corner coherence pin: the missing-`status` corner and
2116        // the populated-empty-slot corner return `Option`s whose
2117        // `.is_none()` / `.is_some()` observations are IDENTICAL. A
2118        // regression that promoted the missing-`status` corner to a
2119        // typed error (via a signature change to `Result<_, _>`) — or
2120        // that widened the empty-slot corner to a synthetic
2121        // `Some(Utc::now())` — would surface here rather than as
2122        // silent operator-facing divergence between a never-status-
2123        // written allocation and a Bind-time-without-TTL allocation on
2124        // the Release-composition branch.
2125        let a_no_status = alloc_without_status();
2126        let a_empty_slot = alloc_with_expires_at(None);
2127        assert_eq!(
2128            a_no_status.observed_expires_at().is_none(),
2129            a_empty_slot.observed_expires_at().is_none(),
2130        );
2131        assert_eq!(
2132            a_no_status.observed_expires_at().is_some(),
2133            a_empty_slot.observed_expires_at().is_some(),
2134        );
2135    }
2136
2137    #[test]
2138    fn observed_expires_at_shape_agrees_with_observed_phase_peer_axis() {
2139        // Same-CRD peer-axis coherence pin binding the SAME
2140        // `.status.as_ref().<map|and_then>(|s| s.<Copy-field>)` shape
2141        // that both `EphemeralAllocation::observed_expires_at` (this
2142        // primitive) and `EphemeralAllocation::observed_phase` walk,
2143        // differing only in the outer combinator (`and_then` here
2144        // because the persisted field is itself `Option<T>`, `map`
2145        // there because the persisted phase is bare) and in the
2146        // projected `Copy` type. Structural test — both signatures
2147        // must resolve as `&Self -> Option<T>` fn pointers with `T`
2148        // `Copy`, so a future rename or a signature drift that (say)
2149        // widened one side to `Option<&T>` or narrowed one side to
2150        // `T` fails to compile here rather than silently drifting
2151        // the family apart. The runtime side of the pin sweeps the
2152        // missing-status + empty-slot corners on the `expires_at`
2153        // half; the `phase` half is exercised by its own
2154        // `tests::observed_phase_*` pin family — this test binds
2155        // only the peer-axis shape.
2156        let a_no_status = alloc_without_status();
2157        let a_empty_slot = alloc_with_expires_at(None);
2158        assert!(a_no_status.observed_expires_at().is_none());
2159        assert!(a_empty_slot.observed_expires_at().is_none());
2160        // Structural peer-axis coherence: bind both signatures as fn
2161        // pointers at their peer resolution type so the compiler
2162        // refuses to build if either side's shape drifts.
2163        let _expires_at_shape: fn(&EphemeralAllocation) -> Option<DateTime<Utc>> =
2164            EphemeralAllocation::observed_expires_at;
2165        let _phase_shape: fn(&EphemeralAllocation) -> Option<AllocationPhase> =
2166            EphemeralAllocation::observed_phase;
2167    }
2168
2169    #[test]
2170    fn allocation_spec_omits_optional_fields() {
2171        // AllocationSpec rides through `AllocationSpec::requestor_only`
2172        // + the inner Requestor through `Requestor::kind_only`; the
2173        // wire-shape pin still holds because BOTH composers produce
2174        // the byte-identical minimal shape whose `skip_serializing_if
2175        // = "Option::is_none"` + default-vec serde attributes elide
2176        // every optional slot from the YAML output.
2177        let s = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
2178        let yaml = serde_yaml::to_string(&s).unwrap();
2179        assert!(!yaml.contains("poolRef"));
2180        assert!(!yaml.contains("ttl"));
2181        assert!(!yaml.contains("note"));
2182    }
2183
2184    // ─── AllocationSpec::requestor_only substrate pins ──────────────
2185    //
2186    // The pre-lift `AllocationSpec { pool_ref: None, requestor: <r>,
2187    // ttl: None, note: None }` incantation recurred at NINE workspace-
2188    // wide fixture sites past the ★★ PRIME-DIRECTIVE ≥ 2 threshold
2189    // (five inside this file's own test module, three inside the
2190    // crate's `tests_owned_coordinates` / `tests_annotated` /
2191    // `tests_deletion_tombstoned` pin modules on `lib.rs`, and one in
2192    // `tatara-pool-reconciler::allocation_decide::alloc`). Every corner
2193    // of the three-slot default tail is pinned here so a future
2194    // normalization at the primitive lands with a fail-before-pass-
2195    // after regression at THIS composer's pins rather than as silent
2196    // fixture skew across the nine callsite arms.
2197
2198    #[test]
2199    fn requestor_only_leaves_the_three_slot_default_tail_at_the_substrate_owner() {
2200        // Every default-tail slot must land at the values the substrate
2201        // owner stamps: `pool_ref = None` (selector-based routing),
2202        // `ttl = None` (fall back to pool template TTL), `note = None`
2203        // (empty audit slot). A regression that drifted ANY of the three
2204        // defaults would silently reshape every downstream fixture
2205        // simultaneously; this pin catches it.
2206        let s = AllocationSpec::requestor_only(Requestor::kind_only("manual"));
2207        assert!(s.pool_ref.is_none(), "pool_ref must default to None");
2208        assert!(s.ttl.is_none(), "ttl must default to None");
2209        assert!(s.note.is_none(), "note must default to None");
2210    }
2211
2212    #[test]
2213    fn requestor_only_stamps_the_caller_requestor_verbatim() {
2214        // The single caller-varying slot MUST pass through untouched —
2215        // a regression that copied only a subset of the Requestor's
2216        // seven slots (e.g. re-authoring `Requestor { kind: r.kind, ..
2217        // Default::default() }` inside the composer) would drop the
2218        // caller's `repo` / `branch` / `pr_number` / `sha` / `pr_labels`
2219        // / `actor` at every downstream fixture. Passes a fully-
2220        // populated `Requestor` through and asserts every slot lands.
2221        let r = Requestor {
2222            kind: "github-pr".into(),
2223            repo: Some("pleme-io/demo".into()),
2224            branch: Some("main".into()),
2225            pr_number: Some(42),
2226            sha: Some("deadbeef".into()),
2227            pr_labels: vec!["needs-review".into()],
2228            actor: Some("dozer".into()),
2229        };
2230        let s = AllocationSpec::requestor_only(r.clone());
2231        assert_eq!(s.requestor.kind, r.kind);
2232        assert_eq!(s.requestor.repo, r.repo);
2233        assert_eq!(s.requestor.branch, r.branch);
2234        assert_eq!(s.requestor.pr_number, r.pr_number);
2235        assert_eq!(s.requestor.sha, r.sha);
2236        assert_eq!(s.requestor.pr_labels, r.pr_labels);
2237        assert_eq!(s.requestor.actor, r.actor);
2238    }
2239
2240    #[test]
2241    fn requestor_only_matches_hand_authored_pre_lift_bytewise() {
2242        // Byte-identical parity with the pre-lift 5-line struct-
2243        // literal every downstream fixture restated verbatim. Swept
2244        // across the two representative requestor shapes: the
2245        // kind-only `"manual"` fixture (the majority of the collapsed
2246        // callsites) and the fully-populated github-pr requestor (the
2247        // `tatara-pool-reconciler::allocation_decide::alloc` shape).
2248        // A regression that reshaped the composer's output would
2249        // diverge from the pre-lift literal HERE rather than at every
2250        // downstream fixture's downstream assertion.
2251        let sample_requestors = [
2252            Requestor::kind_only("manual"),
2253            Requestor::kind_only("github-pr"),
2254            Requestor {
2255                kind: "github-pr".into(),
2256                repo: Some("pleme-io/demo".into()),
2257                branch: Some("main".into()),
2258                pr_number: None,
2259                sha: None,
2260                pr_labels: vec![],
2261                actor: None,
2262            },
2263        ];
2264        for r in sample_requestors {
2265            let via_primitive = AllocationSpec::requestor_only(r.clone());
2266            let hand_authored = AllocationSpec {
2267                pool_ref: None,
2268                requestor: r.clone(),
2269                ttl: None,
2270                note: None,
2271            };
2272            // Sweep every slot rather than round-tripping through
2273            // serde, so a slot rename that keeps the same serde name
2274            // still surfaces as a defect at the primitive's slot-
2275            // level parity.
2276            assert!(via_primitive.pool_ref.is_none() && hand_authored.pool_ref.is_none());
2277            assert_eq!(via_primitive.requestor.kind, hand_authored.requestor.kind);
2278            assert_eq!(via_primitive.ttl, hand_authored.ttl);
2279            assert_eq!(via_primitive.note, hand_authored.note);
2280        }
2281    }
2282
2283    // ─── AllocationStatus::transition substrate pins ────────────────────
2284    //
2285    // Pin the substrate composer at fail-before-pass-after granularity:
2286    // the composer did not exist pre-lift, so any regression against
2287    // the four hand-authored sites in
2288    // `tatara-pool-reconciler::controller_allocation::reconcile_inner`
2289    // surfaces at these pins rather than as silent operator-visible
2290    // status-patch skew.
2291
2292    fn anchor_time() -> DateTime<Utc> {
2293        // A deterministic non-`Utc::now()` anchor so pins that read
2294        // back `phase_since` do not race the wall clock.
2295        DateTime::parse_from_rfc3339("2026-05-01T00:00:00Z")
2296            .unwrap()
2297            .with_timezone(&Utc)
2298    }
2299
2300    #[test]
2301    fn allocation_status_transition_stamps_supplied_phase_verbatim() {
2302        for phase in AllocationPhase::ALL {
2303            let s = AllocationStatus::transition(phase, "irrelevant", anchor_time());
2304            assert_eq!(s.phase, phase, "phase drifted for {phase:?}");
2305        }
2306    }
2307
2308    #[test]
2309    fn allocation_status_transition_stamps_supplied_message_verbatim() {
2310        let s = AllocationStatus::transition(
2311            AllocationPhase::Queued,
2312            "pool matched; no Free member available",
2313            anchor_time(),
2314        );
2315        assert_eq!(
2316            s.message.as_deref(),
2317            Some("pool matched; no Free member available"),
2318        );
2319    }
2320
2321    #[test]
2322    fn allocation_status_transition_sets_phase_since_to_supplied_now() {
2323        let anchor = anchor_time();
2324        let s = AllocationStatus::transition(AllocationPhase::Bound, "bound", anchor);
2325        assert_eq!(
2326            s.phase_since,
2327            Some(anchor),
2328            "phase_since must be the supplied `now`, not a fresh Utc::now()",
2329        );
2330    }
2331
2332    #[test]
2333    fn allocation_status_transition_defaults_every_optional_slot() {
2334        // The composer stamps only the three always-present slots
2335        // (`phase + phase_since + message`); every other slot on
2336        // `AllocationStatus` must land at its `Default`-equivalent
2337        // variant so a caller-branch that attaches an optional slot
2338        // via struct-update syntax does not silently inherit a
2339        // pre-populated non-`None`/non-empty value.
2340        let s = AllocationStatus::transition(AllocationPhase::Released, "released", anchor_time());
2341        assert!(s.bound_pool.is_none(), "bound_pool must default to None");
2342        assert!(
2343            s.assigned_process.is_none(),
2344            "assigned_process must default to None"
2345        );
2346        assert!(
2347            s.allocated_at.is_none(),
2348            "allocated_at must default to None"
2349        );
2350        assert!(s.expires_at.is_none(), "expires_at must default to None");
2351        assert!(
2352            s.conditions.is_empty(),
2353            "conditions must default to an empty Vec"
2354        );
2355    }
2356
2357    #[test]
2358    fn allocation_status_transition_accepts_owned_string_and_static_str() {
2359        // `impl Into<String>` matches every current callsite:
2360        // three of the four hand-authored sites pass `&'static str`
2361        // literal reasons; the fourth ("bound to pool member") also
2362        // passes a `&'static str`. Sibling to
2363        // `tatara-reconciler::patch::phase_status_msg`'s identical
2364        // `impl Into<String>` signature.
2365        let via_static = AllocationStatus::transition(
2366            AllocationPhase::NoMatchingPool,
2367            "no Pool selector matched this Requestor",
2368            anchor_time(),
2369        );
2370        let via_owned = AllocationStatus::transition(
2371            AllocationPhase::NoMatchingPool,
2372            String::from("no Pool selector matched this Requestor"),
2373            anchor_time(),
2374        );
2375        assert_eq!(via_static.message, via_owned.message);
2376    }
2377
2378    #[test]
2379    fn allocation_status_transition_serializes_to_pre_lift_json_shape() {
2380        // Byte-shape pin against the exact `json!({ "status": {
2381        // "phase": <variant>, "phaseSince": <now>, "message": "<msg>"
2382        // } })` incantation every pre-lift callsite restated. A
2383        // regression that reordered a slot, dropped the `phaseSince`
2384        // stamp, or drifted the camelCase key naming here surfaces at
2385        // THIS pin rather than as a subtle patch_status body the K8s
2386        // API server accepts but the pool reconciler's next observe
2387        // pass fails to read back.
2388        let anchor = anchor_time();
2389        let via_composer =
2390            AllocationStatus::transition(AllocationPhase::NoMatchingPool, "no match", anchor);
2391        let composed = serde_json::json!({ "status": via_composer });
2392        let hand_authored = serde_json::json!({
2393            "status": {
2394                "phase": AllocationPhase::NoMatchingPool,
2395                "phaseSince": anchor,
2396                "message": "no match",
2397            }
2398        });
2399        assert_eq!(composed, hand_authored);
2400    }
2401
2402    #[test]
2403    fn allocation_status_transition_composes_with_struct_update_for_bind_seed() {
2404        // Pin the compound shape the `AllocationDecision::Bind`
2405        // callsite composes: the substrate seed carries `phase +
2406        // phase_since + message`, and the branch attaches
2407        // `bound_pool` + `assigned_process` + `allocated_at` +
2408        // `expires_at` via struct-update syntax. Post-lift the four
2409        // extra slots survive the compose intact and the base three
2410        // slots inherit the composer's stamps verbatim.
2411        let anchor = anchor_time();
2412        let ttl = anchor + chrono::Duration::hours(1);
2413        let pool = AllocationRef::new("demo-pool", "pools");
2414        let assigned = AllocationRef::new("demo-abcd", "pools");
2415        let bind_status = AllocationStatus {
2416            bound_pool: Some(pool.clone()),
2417            assigned_process: Some(assigned.clone()),
2418            allocated_at: Some(anchor),
2419            expires_at: Some(ttl),
2420            ..AllocationStatus::transition(AllocationPhase::Bound, "bound to pool member", anchor)
2421        };
2422        // Base-three slots stamped by the composer.
2423        assert_eq!(bind_status.phase, AllocationPhase::Bound);
2424        assert_eq!(bind_status.phase_since, Some(anchor));
2425        assert_eq!(bind_status.message.as_deref(), Some("bound to pool member"));
2426        // Struct-update-attached branch slots.
2427        assert_eq!(
2428            bind_status.bound_pool.as_ref().map(|r| &r.name),
2429            Some(&pool.name)
2430        );
2431        assert_eq!(
2432            bind_status.assigned_process.as_ref().map(|r| &r.name),
2433            Some(&assigned.name)
2434        );
2435        assert_eq!(bind_status.allocated_at, Some(anchor));
2436        assert_eq!(bind_status.expires_at, Some(ttl));
2437    }
2438
2439    // ─── AllocationStatus::bound_transition substrate pins ─────────────
2440    //
2441    // Pin the compound composer at fail-before-pass-after granularity:
2442    // the composer wraps [`AllocationStatus::transition`] with the
2443    // `bound_pool + assigned_process` pair the Bind / Release arms
2444    // both stamped inline pre-lift.
2445
2446    #[test]
2447    fn allocation_status_bound_transition_stamps_supplied_pool_and_process_verbatim() {
2448        let anchor = anchor_time();
2449        let pool = AllocationRef::new("demo-pool", "pools");
2450        let assigned = AllocationRef::new("demo-abcd", "pools");
2451        let s = AllocationStatus::bound_transition(
2452            AllocationPhase::Released,
2453            "released; pool reconciler will return the member",
2454            anchor,
2455            pool.clone(),
2456            assigned.clone(),
2457        );
2458        assert_eq!(s.bound_pool.as_ref(), Some(&pool));
2459        assert_eq!(s.assigned_process.as_ref(), Some(&assigned));
2460    }
2461
2462    #[test]
2463    fn allocation_status_bound_transition_inherits_transition_triplet_verbatim() {
2464        // The compound composer must not stamp its own `phase +
2465        // phase_since + message` triplet — it MUST compose the pair
2466        // atop the substrate `Self::transition` seed so any future
2467        // evolution to the base triplet lands at ONE site and this
2468        // composer inherits the upgrade mechanically. Pin the triplet
2469        // through the same axis-uniform reads the transition tests use.
2470        let anchor = anchor_time();
2471        let via_compound = AllocationStatus::bound_transition(
2472            AllocationPhase::Bound,
2473            "bound to pool member",
2474            anchor,
2475            AllocationRef::new("p", "ns"),
2476            AllocationRef::new("q", "ns"),
2477        );
2478        let via_base =
2479            AllocationStatus::transition(AllocationPhase::Bound, "bound to pool member", anchor);
2480        assert_eq!(via_compound.phase, via_base.phase);
2481        assert_eq!(via_compound.phase_since, via_base.phase_since);
2482        assert_eq!(via_compound.message, via_base.message);
2483    }
2484
2485    #[test]
2486    fn allocation_status_bound_transition_defaults_every_optional_slot_beyond_the_pair() {
2487        // The compound composer stamps only the base triplet + the
2488        // `bound_pool + assigned_process` pair; every other optional
2489        // slot (`allocated_at` / `expires_at` / `conditions`) must
2490        // land at its `Default`-equivalent variant so a caller-branch
2491        // that attaches an addendum via struct-update syntax (a Bind
2492        // arm's `allocated_at` + `expires_at` stamp) does not
2493        // silently inherit a pre-populated non-`None`/non-empty value.
2494        let s = AllocationStatus::bound_transition(
2495            AllocationPhase::Released,
2496            "released",
2497            anchor_time(),
2498            AllocationRef::new("p", "ns"),
2499            AllocationRef::new("q", "ns"),
2500        );
2501        assert!(
2502            s.allocated_at.is_none(),
2503            "allocated_at must default to None"
2504        );
2505        assert!(s.expires_at.is_none(), "expires_at must default to None");
2506        assert!(
2507            s.conditions.is_empty(),
2508            "conditions must default to an empty Vec"
2509        );
2510    }
2511
2512    #[test]
2513    fn allocation_status_bound_transition_composes_with_struct_update_for_bind_seed() {
2514        // Pin the compound shape the `AllocationDecision::Bind`
2515        // callsite post-lift composes: the compound composer seeds
2516        // `phase + phase_since + message + bound_pool +
2517        // assigned_process`, and the Bind branch attaches
2518        // `allocated_at` + `expires_at` via struct-update syntax.
2519        // Post-lift the two extra slots survive the compose intact
2520        // and the base five slots inherit the composer's stamps
2521        // verbatim.
2522        let anchor = anchor_time();
2523        let ttl = anchor + chrono::Duration::hours(1);
2524        let pool = AllocationRef::new("demo-pool", "pools");
2525        let assigned = AllocationRef::new("demo-abcd", "pools");
2526        let bind_status = AllocationStatus {
2527            allocated_at: Some(anchor),
2528            expires_at: Some(ttl),
2529            ..AllocationStatus::bound_transition(
2530                AllocationPhase::Bound,
2531                "bound to pool member",
2532                anchor,
2533                pool.clone(),
2534                assigned.clone(),
2535            )
2536        };
2537        assert_eq!(bind_status.phase, AllocationPhase::Bound);
2538        assert_eq!(bind_status.phase_since, Some(anchor));
2539        assert_eq!(bind_status.message.as_deref(), Some("bound to pool member"));
2540        assert_eq!(bind_status.bound_pool.as_ref(), Some(&pool));
2541        assert_eq!(bind_status.assigned_process.as_ref(), Some(&assigned));
2542        assert_eq!(bind_status.allocated_at, Some(anchor));
2543        assert_eq!(bind_status.expires_at, Some(ttl));
2544    }
2545
2546    #[test]
2547    fn allocation_status_bound_transition_matches_pre_lift_release_arm_verbatim() {
2548        // Byte-shape pin against the exact pre-lift `AllocationStatus
2549        // { bound_pool: Some(pool), assigned_process:
2550        // Some(AllocationRef::new(..)), ..AllocationStatus::transition
2551        // (Released, "…", now) }` composition the
2552        // `AllocationDecision::Release` arm restated inline pre-lift.
2553        // A regression that reordered the pair, dropped a `Some`, or
2554        // drifted the composed base triplet here surfaces at THIS pin
2555        // rather than as a subtle patch_status body the K8s API
2556        // server accepts but the audit record disagrees on.
2557        let anchor = anchor_time();
2558        let pool = AllocationRef::new("demo-pool", "pools");
2559        let assigned = AllocationRef::new("demo-abcd", "pools");
2560        let via_composer = AllocationStatus::bound_transition(
2561            AllocationPhase::Released,
2562            "released; pool reconciler will return the member",
2563            anchor,
2564            pool.clone(),
2565            assigned.clone(),
2566        );
2567        let via_hand_authored = AllocationStatus {
2568            bound_pool: Some(pool),
2569            assigned_process: Some(assigned),
2570            ..AllocationStatus::transition(
2571                AllocationPhase::Released,
2572                "released; pool reconciler will return the member",
2573                anchor,
2574            )
2575        };
2576        assert_eq!(
2577            serde_json::to_value(&via_composer).unwrap(),
2578            serde_json::to_value(&via_hand_authored).unwrap(),
2579        );
2580    }
2581
2582    #[test]
2583    fn allocation_status_transition_shape_agrees_with_pool_status_observed_peer() {
2584        // Cross-CRD peer-axis coherence: both substrate composers
2585        // (`PoolStatus::observed`, `AllocationStatus::transition`)
2586        // accept a caller-supplied `now: DateTime<Utc>` at the SAME
2587        // signature slot, stamp it into `phase_since` uniformly, and
2588        // leave every other slot at its `Default`-equivalent variant.
2589        // Structural pin: if either side's `now` signature drifts to
2590        // `impl Into<DateTime<Utc>>` or a reference form, this bind
2591        // fails to compile here rather than silently drifting the
2592        // family apart.
2593        let _allocation_shape: fn(
2594            AllocationPhase,
2595            &'static str,
2596            DateTime<Utc>,
2597        ) -> AllocationStatus = AllocationStatus::transition;
2598        // (`PoolStatus::observed`'s pinned coherence lives at its own
2599        // peer pin family in `crate::pool`; this pin binds the
2600        // `AllocationStatus::transition` side of the peer pair.)
2601    }
2602
2603    // ─── EphemeralAllocation::new_in substrate pins ────────────────────
2604    //
2605    // The pre-lift 2-line `let mut a = EphemeralAllocation::new(<name>,
2606    // <spec>); a.meta_mut().namespace = Some(<ns>.into());` chain
2607    // recurred at TWO workspace-wide sites past the ★★ PRIME-DIRECTIVE
2608    // ≥ 2 threshold across TWO crates (production PR-webhook emitter
2609    // at `tatara-github-watcher::allocation_factory::build_allocation`
2610    // + allocation-decision test fixture at `tatara-pool-reconciler::
2611    // allocation_decide::tests::alloc`). Post-lift the ONE substrate
2612    // composer stamps a namespaced `EphemeralAllocation` from
2613    // `(name, ns, spec)` in one call. Fail-before-pass-after
2614    // granularity: `new_in` did not exist pre-lift; the compiler
2615    // cannot resolve the name until the impl block above is in place,
2616    // so a rollback of the primitive breaks this whole pin block.
2617
2618    fn sample_spec() -> AllocationSpec {
2619        AllocationSpec::requestor_only(Requestor::kind_only("manual"))
2620    }
2621
2622    #[test]
2623    fn allocation_new_in_stamps_metadata_name_from_the_name_slot() {
2624        // `name` slot → `metadata.name` projection pin. Guards against
2625        // a regression that dropped the `name` slot into a `generate_
2626        // name` slot, an `annotations` seed, or any downstream slot the
2627        // kube-derived [`EphemeralAllocation::new`] does not populate
2628        // at `metadata.name` verbatim.
2629        let a = EphemeralAllocation::new_in("pr-42-demo", "pools", sample_spec());
2630        assert_eq!(a.metadata.name.as_deref(), Some("pr-42-demo"));
2631    }
2632
2633    #[test]
2634    fn allocation_new_in_stamps_metadata_namespace_from_the_ns_slot() {
2635        // `ns` slot → `metadata.namespace` projection pin. Guards
2636        // against a regression that dropped the `ns` slot into a
2637        // `labels` seed, an unrelated annotation, or that stamped
2638        // `namespace = None` even after a caller-supplied value.
2639        let a = EphemeralAllocation::new_in("pr-42-demo", "pools", sample_spec());
2640        assert_eq!(a.metadata.namespace.as_deref(), Some("pools"));
2641    }
2642
2643    #[test]
2644    fn allocation_new_in_stamps_spec_from_the_spec_slot_verbatim() {
2645        // `spec` slot → `spec` projection pin. A regression that
2646        // silently normalized the caller-supplied spec inside the
2647        // composer would diverge from the byte-identical pass-through
2648        // the pre-lift 2-line chain produced.
2649        let spec = AllocationSpec::requestor_only(Requestor::kind_only("github-pr"));
2650        let a = EphemeralAllocation::new_in("pr-42-demo", "pools", spec.clone());
2651        assert_eq!(a.spec.requestor.kind, spec.requestor.kind);
2652        assert!(a.spec.pool_ref.is_none());
2653        assert!(a.spec.ttl.is_none());
2654        assert!(a.spec.note.is_none());
2655    }
2656
2657    #[test]
2658    fn allocation_new_in_accepts_both_owned_and_borrowed_namespace_slot() {
2659        // The `impl Into<String>` ergonomic contract round-trips
2660        // through both `&'static str` (the pre-lift test-fixture caller
2661        // shape) AND owned `String` (the pre-lift production-emitter
2662        // caller shape at `build_allocation`, where `namespace:
2663        // &str` was pushed through a `.to_string()`) at the SAME
2664        // signature. Guards against a regression that narrowed the
2665        // slot to `&str` only or that silently double-`.into()`d an
2666        // already-owned String.
2667        let via_str = EphemeralAllocation::new_in("pr-42-demo", "pools", sample_spec());
2668        let via_string =
2669            EphemeralAllocation::new_in("pr-42-demo", String::from("pools"), sample_spec());
2670        assert_eq!(via_str.metadata.namespace, via_string.metadata.namespace);
2671    }
2672
2673    #[test]
2674    fn allocation_new_in_matches_pre_lift_construct_then_set_namespace_bytewise() {
2675        // Byte-shape parity witness against the pre-lift 2-line chain
2676        // across the two representative namespace shapes the collapsed
2677        // sites used (`"pools"` at `allocation_decide::alloc` +
2678        // production-emitter `"ephemeral-pools"` at `build_allocation`
2679        // + the free-form namespace `build_allocation` accepts). A
2680        // regression that shifted the composer's output would diverge
2681        // from the pre-lift literal HERE rather than at every
2682        // downstream consumer's assertion.
2683        for ns in ["pools", "ephemeral-pools", "custom-ns"] {
2684            let via_primitive = EphemeralAllocation::new_in("pr-42-demo", ns, sample_spec());
2685            let mut hand_authored = EphemeralAllocation::new("pr-42-demo", sample_spec());
2686            hand_authored.metadata.namespace = Some(ns.into());
2687            assert_eq!(via_primitive.metadata.name, hand_authored.metadata.name);
2688            assert_eq!(
2689                via_primitive.metadata.namespace,
2690                hand_authored.metadata.namespace,
2691            );
2692        }
2693    }
2694
2695    #[test]
2696    fn allocation_new_in_defaults_other_metadata_slots_at_kube_derived_new() {
2697        // The composer forwards to the kube-derived
2698        // [`EphemeralAllocation::new`] for every non-namespace metadata
2699        // slot. A regression that stamped finalizers, owner_references,
2700        // labels, or annotations inside the composer's body —
2701        // inheriting the pre-lift chain's undocumented emptiness at
2702        // those slots — would surface here.
2703        let a = EphemeralAllocation::new_in("pr-42-demo", "pools", sample_spec());
2704        assert!(a.metadata.finalizers.is_none());
2705        assert!(a.metadata.owner_references.is_none());
2706        assert!(a.metadata.labels.is_none());
2707        assert!(a.metadata.annotations.is_none());
2708    }
2709
2710    #[test]
2711    fn allocation_new_in_shape_agrees_with_pool_new_in_peer() {
2712        // Cross-CRD peer-axis structural coherence: both substrate
2713        // composers (`EphemeralPool::new_in`, `EphemeralAllocation::
2714        // new_in`) accept `(name: &str, namespace: impl Into<String>,
2715        // spec: <CRD>Spec)` at the SAME positional slot order and
2716        // return the CRD with `metadata.name` + `metadata.namespace`
2717        // stamped uniformly. Structural pin: if either side's slot
2718        // order drifts (e.g. `(ns, name, spec)`), this bind fails to
2719        // compile here rather than silently drifting the family apart.
2720        let _allocation_shape: fn(&str, &'static str, AllocationSpec) -> EphemeralAllocation =
2721            EphemeralAllocation::new_in;
2722        // (`EphemeralPool::new_in`'s pinned coherence lives at its own
2723        // peer pin family in `crate::pool`; this pin binds the
2724        // `EphemeralAllocation::new_in` side of the peer pair.)
2725    }
2726}