Skip to main content

tatara_process/
condition_type.rs

1//! Substrate primitive over the K8s `metav1.Condition.type` wire-
2//! form axis every writer + reader in this workspace hand-authored
3//! as a bare `&'static str` literal on the `ProcessStatus.conditions[]`
4//! wire. Sibling on the same wire-form axis-family to
5//! [`crate::k8s_condition::K8sConditionStatus`] — that primitive
6//! owns the closed set of the `status` slot (`"True"` / `"False"`
7//! / `"Unknown"`); THIS primitive owns the closed set of the
8//! `type` slot (`"Ready"` / `"Attested"`) every
9//! [`crate::status::ProcessCondition`] carries.
10//!
11//! ## Why the substrate lives here
12//!
13//! Pre-lift the two `type` wire-form literals were hand-authored at
14//! FOUR production sites across two crates past the ★★ PRIME-
15//! DIRECTIVE ≥ 2 duplication threshold — each writer + reader pair
16//! silently coupled by exact-case ASCII agreement on the `type` slot:
17//!
18//! * [`crate::status::ProcessCondition::ready`] — writer, `type_:
19//!   "Ready".into()` on the `Ready` row emitted whenever the
20//!   reconciler observes a Process running.
21//! * [`crate::status::ProcessCondition::not_ready`] — writer, `type_:
22//!   "Ready".into()` on the same row emitted with `status: "False"`
23//!   whenever the reconciler observes a Process failing.
24//! * [`crate::status::ProcessCondition::attested`] — writer, `type_:
25//!   "Attested".into()` on the `Attested` row emitted whenever a
26//!   three-pillar attestation lands.
27//! * `tatara-reconciler::ssapply::ready_condition_value` — reader,
28//!   `if typ != "Ready" { continue; }` filter that isolates the
29//!   `Ready`-typed condition out of every FluxCD / Deployment /
30//!   HelmRelease / Kustomization / StatefulSet resource's
31//!   `status.conditions[]` list before classifying its `status` slot
32//!   through [`crate::k8s_condition::K8sConditionStatus::from_wire_str`].
33//!
34//! Every site restated the SAME `&'static str` byte-literal
35//! (`"Ready"` × 3 or `"Attested"` × 1). A copy-paste that lower-cased
36//! one letter (`"ready"` — silently invalid; the K8s API server does
37//! NOT case-normalize condition types, and every observer that
38//! selects by exact case would silently miss the drifted row),
39//! swapped the semantic slot (a writer that emitted `"Attested"` on
40//! the `Ready` row would silently drift the reconciler's
41//! `ready_condition_value` observer to always-Unknown on that
42//! resource), or introduced an alternate spelling drifts the wire-
43//! form at ONE end and leaves the other end unable to classify the
44//! condition — the reader falls through to `ReadyState::Unknown` and
45//! every Flux / Deployment readiness gate silently reports "not
46//! observed" for the remainder of the resource's life. Post-lift
47//! each writer composes with `ProcessConditionType::<V>.as_wire_str()`
48//! and the reader binds through `ProcessConditionType::from_wire_str
49//! (...)`; the wire-form literal lives at ONE substrate owner and a
50//! drift at either end becomes unrepresentable at the closed-set
51//! level.
52//!
53//! ## Closed-set completeness
54//!
55//! `ProcessCondition` is the crate-owned condition type — the K8s
56//! API does NOT dictate the `type` slot's closed set (unlike the
57//! `status` slot's three-value set at
58//! [`crate::k8s_condition::K8sConditionStatus`]); each CRD owns its
59//! own type alphabet. Every `ProcessCondition` constructor in
60//! [`crate::status`] pre-lift wrote exactly one of the two literals
61//! `"Ready"` or `"Attested"`; the enum below IS that closed set.
62//! A future ProcessCondition constructor that adds a third condition
63//! type (e.g. `"Reconverging"` for the SIGHUP re-convergence path,
64//! `"Terminated"` for the Zombie/Reaped gate) would land as ONE new
65//! variant at this ONE substrate owner AND ONE new
66//! `ProcessCondition::<constructor>` at [`crate::status`] AND (if
67//! the reader wants to classify it) ONE new arm at
68//! `ssapply::ready_condition_value` — exhaustively checked by the
69//! compiler at every downstream consumer that pattern-matches on
70//! the closed set.
71//!
72//! ## Byte-shape parity
73//!
74//! `as_wire_str` returns the EXACT-CASE ASCII byte-shape every pre-
75//! lift site restated inline — pinned bytewise at
76//! [`tests::as_wire_str_matches_pre_lift_literals_bytewise`] against
77//! a hand-authored fixture table of the pre-lift strings. A
78//! regression that lower-cased a variant, added a whitespace prefix,
79//! or reshaped the byte-form under a future `#[derive(Serialize)]`
80//! `serde(rename = "…")` drift surfaces at the pin rather than as
81//! silent operator-facing wire-form skew across every
82//! `ProcessCondition` writer + `ready_condition_value` reader in the
83//! workspace.
84//!
85//! `from_wire_str` is the invertible partner — a round-trip through
86//! `from_wire_str(v.as_wire_str())` yields `Some(v)` for every
87//! variant, pinned at
88//! [`tests::wire_form_round_trip_holds_for_every_variant`]. Any
89//! input outside the closed set (case-drift, whitespace, empty
90//! string, unrelated K8s condition-type literals) returns `None`;
91//! the closed-set nature is pinned at
92//! [`tests::from_wire_str_rejects_case_drift_and_unknown`].
93//!
94//! ## Naming — `as_wire_str`, not `as_str`
95//!
96//! Same discipline as the [`crate::k8s_condition::K8sConditionStatus`]
97//! sibling — the method signals that the returned `&'static str` is
98//! the K8s WIRE FORM (the exact byte-shape the API server accepts on
99//! the `type` slot of a `metav1.Condition`), not a debug-print or
100//! `Display` projection. A caller that reads `.as_wire_str()`
101//! immediately understands the return value is safe to write into a
102//! JSON payload without any further normalization; a call spelled
103//! `.as_str()` reads as a generic string projection and invites
104//! callers to reach for `.to_lowercase()` / `.trim()` normalizations
105//! that would break the wire form.
106//!
107//! ## `#[must_use]` on `as_wire_str`
108//!
109//! Every consumer feeds the returned `&'static str` into either a
110//! `String::from(...)` composition (writer side, going into
111//! `ProcessCondition.type_`) or a pattern-match / equality arm
112//! (reader side, filtering `status.conditions[]`). Dropping the
113//! return means the wire-form projection was computed for no
114//! observable reason — the attribute surfaces that as a warning at
115//! every consumer site.
116//!
117//! Theory anchor: THEORY.md §II.1 invariant 5 (composition preserves
118//! proofs — the wire-form literal at ONE substrate owner means the
119//! writer + reader sides of the K8s `status.conditions[].type` wire
120//! agree bytewise by construction; a drift at either end becomes
121//! unrepresentable at the closed-set level, not "detected at
122//! runtime by a mismatched observer log line"). THEORY.md §III
123//! (typescape — the ProcessCondition `type` slot's closed set is a
124//! first-class Rust enum, not a stringly-typed wire-form).
125//! THEORY.md §VI.1 (generation over composition — the two-literal
126//! closed set recurred at FOUR hand-authored sites past the ★★
127//! PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE
128//! substrate owner here on the K8s-Condition `type` wire-form axis).
129
130use std::fmt;
131
132/// The closed set of `metav1.Condition.type` values every
133/// [`crate::status::ProcessCondition`] constructor emits and every
134/// downstream `status.conditions[]` classifier filters against.
135/// Wire-form is the exact-case ASCII literal (`"Ready"`,
136/// `"Attested"`); a case-drifted variant would silently miss the
137/// reader's byte-exact filter.
138///
139/// Substrate primitive over the `type`-slot wire-form literal every
140/// writer + reader hand-authors on opposite sides of
141/// `status.conditions[]`. Sibling on the same K8s-Condition wire-form
142/// axis-family to [`crate::k8s_condition::K8sConditionStatus`] —
143/// that primitive owns the `status`-slot closed set, this primitive
144/// owns the `type`-slot closed set. See the module docs for the
145/// pre-lift lift audit + closed-set completeness argument.
146#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
147pub enum ProcessConditionType {
148    /// The `Ready`-typed condition — the reconciler's per-observation
149    /// readiness row emitted by [`crate::status::ProcessCondition::
150    /// ready`] (status = `True`) + [`crate::status::ProcessCondition::
151    /// not_ready`] (status = `False`), and filtered by
152    /// `tatara-reconciler::ssapply::ready_condition_value` on the
153    /// reader side.
154    Ready,
155    /// The `Attested`-typed condition — the reconciler's post-
156    /// three-pillar-attestation row emitted by
157    /// [`crate::status::ProcessCondition::attested`] (status = `True`)
158    /// once a `ProcessAttestation`'s `composed_root` is written into
159    /// the `message` slot.
160    Attested,
161}
162
163impl ProcessConditionType {
164    /// The closed set of `metav1.Condition.type` values every
165    /// [`crate::status::ProcessCondition`] constructor emits and every
166    /// downstream reader classifies — single source of truth that drives
167    /// every variant-sweep consumer (round-trip tests, Display parity
168    /// tests, sibling-axis disjointness tests, and any future dashboard /
169    /// tatara-check enumerator / typed-completion consumer that needs
170    /// to iterate the closed set exhaustively).
171    ///
172    /// Adding a third variant (e.g. `Reconverging` for the SIGHUP
173    /// re-convergence path, `Terminated` for the Zombie/Reaped gate)
174    /// lands at ONE `ALL` entry + ONE `as_wire_str` arm + ONE
175    /// `from_wire_str` arm — exhaustively checked by the compiler
176    /// (the `[Self; 2]` array literal forces the arity, and the
177    /// exhaustive-match on `as_wire_str` + `from_wire_str` covers the
178    /// rest).
179    ///
180    /// Pre-lift the two-variant array literal was hand-authored at
181    /// THREE test sites in this module past the ★★ PRIME-DIRECTIVE
182    /// ≥ 2 duplication threshold — the round-trip test, the Display
183    /// parity test, and the sibling-axis disjointness test. Post-lift
184    /// each iterates `Self::ALL` and the closed-set enumeration lives
185    /// at ONE substrate owner here.
186    ///
187    /// Sibling closed-set `ALL` slices across the crate's typescape:
188    /// [`crate::k8s_condition::K8sConditionStatus::ALL`] (the sibling on
189    /// the K8s-Condition wire-form axis-family — status-slot closed set,
190    /// this owns the type-slot closed set); [`crate::boundary::ConditionKind::ALL`],
191    /// [`crate::phase::ProcessPhase::ALL`], [`crate::signal::ProcessSignal::ALL`],
192    /// [`crate::intent::IntentKind::ALL`], [`crate::lifetime::LifetimeKind::ALL`],
193    /// [`crate::receipt::ReceiptKind::ALL`].
194    pub const ALL: [Self; 2] = [Self::Ready, Self::Attested];
195
196    /// The K8s wire-form literal for this variant — exact-case ASCII,
197    /// safe to write directly into a `metav1.Condition.type` slot
198    /// without further normalization. Byte-identical to the pre-lift
199    /// hand-authored `"Ready"` / `"Attested"` literals every writer
200    /// and reader restated inline.
201    #[must_use = "a ProcessCondition type wire-form projection that isn't bound swallows the composition"]
202    pub const fn as_wire_str(self) -> &'static str {
203        match self {
204            Self::Ready => "Ready",
205            Self::Attested => "Attested",
206        }
207    }
208
209    /// Parse a K8s wire-form ProcessCondition type literal into its
210    /// typed variant. Returns `None` for any input outside the
211    /// closed set — case-drift (`"ready"`), whitespace-wrapped
212    /// variants (`" Ready"`), unrelated literals (`""`, `"True"`,
213    /// `"KustomizationHealthy"`), all reject silently and the caller
214    /// falls through to its own `_ => ...` arm.
215    ///
216    /// Invertible with [`Self::as_wire_str`]: a round-trip through
217    /// `from_wire_str(v.as_wire_str())` yields `Some(v)` for every
218    /// variant. Pinned at
219    /// [`tests::wire_form_round_trip_holds_for_every_variant`].
220    #[must_use = "a ProcessCondition type parse result that isn't bound swallows the classification"]
221    pub fn from_wire_str(s: &str) -> Option<Self> {
222        match s {
223            "Ready" => Some(Self::Ready),
224            "Attested" => Some(Self::Attested),
225            _ => None,
226        }
227    }
228}
229
230impl fmt::Display for ProcessConditionType {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        f.write_str(self.as_wire_str())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::ProcessConditionType;
239
240    /// Fail-before-pass-after: the substrate's `as_wire_str`
241    /// projection produces byte-identical output to the pre-lift
242    /// hand-authored `"Ready"` / `"Attested"` literals every writer
243    /// and reader restated inline. A regression that lower-cased
244    /// one variant, added a whitespace prefix, or a future
245    /// `#[derive(Serialize)]` `serde(rename)` drift on this type
246    /// would surface HERE, not as silent operator-facing wire-form
247    /// skew across every ProcessCondition writer and reader in the
248    /// workspace.
249    #[test]
250    fn as_wire_str_matches_pre_lift_literals_bytewise() {
251        assert_eq!(ProcessConditionType::Ready.as_wire_str(), "Ready");
252        assert_eq!(ProcessConditionType::Attested.as_wire_str(), "Attested");
253    }
254
255    /// Round-trip through `from_wire_str(v.as_wire_str())` yields
256    /// `Some(v)` for every variant. Pins the invariant that the
257    /// writer + reader compose invertibly at the closed-set boundary
258    /// — a writer's `as_wire_str` output is always accepted by the
259    /// reader's `from_wire_str` on the SAME variant.
260    #[test]
261    fn wire_form_round_trip_holds_for_every_variant() {
262        for v in ProcessConditionType::ALL {
263            assert_eq!(
264                ProcessConditionType::from_wire_str(v.as_wire_str()),
265                Some(v),
266            );
267        }
268    }
269
270    /// Inputs outside the closed set — case-drift, whitespace-
271    /// wrapped variants, empty string, unrelated K8s wire-form
272    /// literals (K8s ConditionStatus values, K8s built-in condition
273    /// types, ProcessCondition reason slot values) — all reject with
274    /// `None`. Pins the closed-set nature: `from_wire_str` is a total
275    /// function over the ProcessCondition type alphabet, not a
276    /// permissive parser that accepts synonyms.
277    #[test]
278    fn from_wire_str_rejects_case_drift_and_unknown() {
279        for bad in [
280            "",
281            "ready",
282            "READY",
283            "attested",
284            "ATTESTED",
285            " Ready",
286            "Ready ",
287            "Attested\n",
288            "True",
289            "False",
290            "Unknown",
291            "KustomizationHealthy",
292            "HelmReleaseReleased",
293            "ObservedRunning",
294            "AttestationWritten",
295            "1",
296        ] {
297            assert_eq!(
298                ProcessConditionType::from_wire_str(bad),
299                None,
300                "expected `{bad:?}` outside the ProcessCondition type closed set, but from_wire_str accepted it",
301            );
302        }
303    }
304
305    /// `Display` composes through `as_wire_str` — the two
306    /// projections are byte-identical so `format!("{v}")` and
307    /// `v.as_wire_str()` are interchangeable at every consumer.
308    /// Pins the invariant that a caller who reaches for the stdlib
309    /// `Display` conversion path (via `.to_string()`,
310    /// `format!("{v}")`, a `write!` macro) gets the same wire-form
311    /// bytes as a direct `.as_wire_str()` call.
312    #[test]
313    fn display_composes_through_as_wire_str_bytewise() {
314        for v in ProcessConditionType::ALL {
315            assert_eq!(v.to_string(), v.as_wire_str());
316            assert_eq!(format!("{v}"), v.as_wire_str());
317        }
318    }
319
320    /// Closed-set completeness: the two variants exhaust the
321    /// ProcessCondition type alphabet as of this crate's revision.
322    /// A future ProcessCondition constructor that added a third
323    /// condition type (e.g. `"Reconverging"` for the SIGHUP re-
324    /// convergence path, `"Terminated"` for the Zombie/Reaped gate)
325    /// would land as one new variant here, and every consumer that
326    /// pattern-matched exhaustively against the closed set gets a
327    /// compile-time error until it handles the new arm — the fifth
328    /// invariant of the Rust+Lisp pattern. Compiler-verified below
329    /// with an exhaustive match; a regression that added a
330    /// `#[non_exhaustive]` attribute or a private constructor arm
331    /// would break the exhaustiveness proof at this test.
332    #[test]
333    fn closed_set_exhausts_the_process_condition_type_alphabet() {
334        fn describe(v: ProcessConditionType) -> &'static str {
335            match v {
336                ProcessConditionType::Ready => "Ready",
337                ProcessConditionType::Attested => "Attested",
338            }
339        }
340        assert_eq!(describe(ProcessConditionType::Ready), "Ready");
341        assert_eq!(describe(ProcessConditionType::Attested), "Attested");
342    }
343
344    /// `Copy` + `Clone` + `Eq` + `Hash` — pins the trait derives at
345    /// compile time so a regression that dropped one (a future
346    /// `#[derive(Serialize, Deserialize)]` addition that reshaped
347    /// the enum, a manual `impl Clone` that dropped `Copy`) surfaces
348    /// here rather than at a downstream consumer that stored the
349    /// value in a `HashMap` key or copied it across an `if` arm.
350    #[test]
351    fn value_semantics_hold_at_compile_time() {
352        fn assert_copy<T: Copy>() {}
353        fn assert_hash<T: std::hash::Hash>() {}
354        fn assert_eq<T: Eq>() {}
355        assert_copy::<ProcessConditionType>();
356        assert_hash::<ProcessConditionType>();
357        assert_eq::<ProcessConditionType>();
358    }
359
360    /// Sibling-axis coherence with [`crate::k8s_condition::
361    /// K8sConditionStatus`] on the K8s-Condition wire-form axis-
362    /// family — the two primitives partition the `metav1.Condition`
363    /// wire alphabet by SLOT (`type` here, `status` at the sibling),
364    /// so a caller that accidentally passed a ProcessConditionType
365    /// wire-form to `K8sConditionStatus::from_wire_str` (or vice
366    /// versa) MUST reject. Pins the invariant that the two closed
367    /// sets are DISJOINT — a case-flip or a copy-paste that swapped
368    /// the two `from_wire_str` calls surfaces HERE as a `None` at
369    /// the wrong reader, not as silent classification skew.
370    #[test]
371    fn from_wire_str_rejects_sibling_axis_wire_forms() {
372        use crate::k8s_condition::K8sConditionStatus;
373        for s in K8sConditionStatus::ALL {
374            let status_wire = s.as_wire_str();
375            assert_eq!(
376                ProcessConditionType::from_wire_str(status_wire),
377                None,
378                "ProcessConditionType::from_wire_str accepted the sibling-axis \
379                 K8sConditionStatus wire-form `{status_wire:?}` — the two closed \
380                 sets MUST stay disjoint",
381            );
382        }
383        for t in ProcessConditionType::ALL {
384            let type_wire = t.as_wire_str();
385            assert_eq!(
386                K8sConditionStatus::from_wire_str(type_wire),
387                None,
388                "K8sConditionStatus::from_wire_str accepted the sibling-axis \
389                 ProcessConditionType wire-form `{type_wire:?}` — the two closed \
390                 sets MUST stay disjoint",
391            );
392        }
393    }
394
395    /// Fail-before-pass-after: the `ALL` sweep MUST enumerate every
396    /// variant of the closed set exactly once, with no duplicates and
397    /// no omissions. Pinned three ways so any drift surfaces at ONE
398    /// substrate pin rather than as silent skew at every consumer that
399    /// iterates the sweep:
400    ///
401    /// 1. **Arity** — the array's length equals the variant count.
402    ///    The `[Self; 2]` type-level arity already forces this at the
403    ///    substrate; the test restates it as a runtime witness so a
404    ///    regression that widened the type to `&[Self]` (a slice
405    ///    literal) or a `Vec<Self>` builder would surface at the
406    ///    substrate pin rather than as silent shape drift.
407    /// 2. **No duplicates** — round-trip each entry through
408    ///    `from_wire_str(as_wire_str)` and confirm the round-trip
409    ///    yields distinct variants. A regression that stamped
410    ///    `[Self::Ready, Self::Ready]` (a copy-paste at the sweep) or
411    ///    `[Self::Ready, Self::Attested, Self::Ready]` (a paste that
412    ///    also drifted the arity) surfaces at the collected-set
413    ///    cardinality check.
414    /// 3. **Cover** — an exhaustive match on each iterated variant
415    ///    proves the compiler sees every arm at least once through the
416    ///    sweep, so a future variant addition that landed at
417    ///    `as_wire_str` + `from_wire_str` but was forgotten at `ALL`
418    ///    surfaces at the sweep's compile-time exhaustive-match check.
419    #[test]
420    fn all_covers_the_process_condition_type_closed_set_exhaustively() {
421        assert_eq!(
422            ProcessConditionType::ALL.len(),
423            2,
424            "ALL must enumerate every variant of the closed set — a regression that \
425             added a variant at `as_wire_str` but forgot to extend `ALL` surfaces here",
426        );
427
428        let seen: std::collections::HashSet<ProcessConditionType> =
429            ProcessConditionType::ALL.iter().copied().collect();
430        assert_eq!(
431            seen.len(),
432            ProcessConditionType::ALL.len(),
433            "ALL must not stamp any variant twice — a copy-paste at the sweep surfaces here",
434        );
435
436        for v in ProcessConditionType::ALL {
437            let _cover: &'static str = match v {
438                ProcessConditionType::Ready => "Ready",
439                ProcessConditionType::Attested => "Attested",
440            };
441        }
442    }
443}