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 K8s wire-form literal for this variant — exact-case ASCII,
165    /// safe to write directly into a `metav1.Condition.type` slot
166    /// without further normalization. Byte-identical to the pre-lift
167    /// hand-authored `"Ready"` / `"Attested"` literals every writer
168    /// and reader restated inline.
169    #[must_use = "a ProcessCondition type wire-form projection that isn't bound swallows the composition"]
170    pub const fn as_wire_str(self) -> &'static str {
171        match self {
172            Self::Ready => "Ready",
173            Self::Attested => "Attested",
174        }
175    }
176
177    /// Parse a K8s wire-form ProcessCondition type literal into its
178    /// typed variant. Returns `None` for any input outside the
179    /// closed set — case-drift (`"ready"`), whitespace-wrapped
180    /// variants (`" Ready"`), unrelated literals (`""`, `"True"`,
181    /// `"KustomizationHealthy"`), all reject silently and the caller
182    /// falls through to its own `_ => ...` arm.
183    ///
184    /// Invertible with [`Self::as_wire_str`]: a round-trip through
185    /// `from_wire_str(v.as_wire_str())` yields `Some(v)` for every
186    /// variant. Pinned at
187    /// [`tests::wire_form_round_trip_holds_for_every_variant`].
188    #[must_use = "a ProcessCondition type parse result that isn't bound swallows the classification"]
189    pub fn from_wire_str(s: &str) -> Option<Self> {
190        match s {
191            "Ready" => Some(Self::Ready),
192            "Attested" => Some(Self::Attested),
193            _ => None,
194        }
195    }
196}
197
198impl fmt::Display for ProcessConditionType {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.write_str(self.as_wire_str())
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::ProcessConditionType;
207
208    /// Fail-before-pass-after: the substrate's `as_wire_str`
209    /// projection produces byte-identical output to the pre-lift
210    /// hand-authored `"Ready"` / `"Attested"` literals every writer
211    /// and reader restated inline. A regression that lower-cased
212    /// one variant, added a whitespace prefix, or a future
213    /// `#[derive(Serialize)]` `serde(rename)` drift on this type
214    /// would surface HERE, not as silent operator-facing wire-form
215    /// skew across every ProcessCondition writer and reader in the
216    /// workspace.
217    #[test]
218    fn as_wire_str_matches_pre_lift_literals_bytewise() {
219        assert_eq!(ProcessConditionType::Ready.as_wire_str(), "Ready");
220        assert_eq!(ProcessConditionType::Attested.as_wire_str(), "Attested");
221    }
222
223    /// Round-trip through `from_wire_str(v.as_wire_str())` yields
224    /// `Some(v)` for every variant. Pins the invariant that the
225    /// writer + reader compose invertibly at the closed-set boundary
226    /// — a writer's `as_wire_str` output is always accepted by the
227    /// reader's `from_wire_str` on the SAME variant.
228    #[test]
229    fn wire_form_round_trip_holds_for_every_variant() {
230        for v in [ProcessConditionType::Ready, ProcessConditionType::Attested] {
231            assert_eq!(
232                ProcessConditionType::from_wire_str(v.as_wire_str()),
233                Some(v),
234            );
235        }
236    }
237
238    /// Inputs outside the closed set — case-drift, whitespace-
239    /// wrapped variants, empty string, unrelated K8s wire-form
240    /// literals (K8s ConditionStatus values, K8s built-in condition
241    /// types, ProcessCondition reason slot values) — all reject with
242    /// `None`. Pins the closed-set nature: `from_wire_str` is a total
243    /// function over the ProcessCondition type alphabet, not a
244    /// permissive parser that accepts synonyms.
245    #[test]
246    fn from_wire_str_rejects_case_drift_and_unknown() {
247        for bad in [
248            "",
249            "ready",
250            "READY",
251            "attested",
252            "ATTESTED",
253            " Ready",
254            "Ready ",
255            "Attested\n",
256            "True",
257            "False",
258            "Unknown",
259            "KustomizationHealthy",
260            "HelmReleaseReleased",
261            "ObservedRunning",
262            "AttestationWritten",
263            "1",
264        ] {
265            assert_eq!(
266                ProcessConditionType::from_wire_str(bad),
267                None,
268                "expected `{bad:?}` outside the ProcessCondition type closed set, but from_wire_str accepted it",
269            );
270        }
271    }
272
273    /// `Display` composes through `as_wire_str` — the two
274    /// projections are byte-identical so `format!("{v}")` and
275    /// `v.as_wire_str()` are interchangeable at every consumer.
276    /// Pins the invariant that a caller who reaches for the stdlib
277    /// `Display` conversion path (via `.to_string()`,
278    /// `format!("{v}")`, a `write!` macro) gets the same wire-form
279    /// bytes as a direct `.as_wire_str()` call.
280    #[test]
281    fn display_composes_through_as_wire_str_bytewise() {
282        for v in [ProcessConditionType::Ready, ProcessConditionType::Attested] {
283            assert_eq!(v.to_string(), v.as_wire_str());
284            assert_eq!(format!("{v}"), v.as_wire_str());
285        }
286    }
287
288    /// Closed-set completeness: the two variants exhaust the
289    /// ProcessCondition type alphabet as of this crate's revision.
290    /// A future ProcessCondition constructor that added a third
291    /// condition type (e.g. `"Reconverging"` for the SIGHUP re-
292    /// convergence path, `"Terminated"` for the Zombie/Reaped gate)
293    /// would land as one new variant here, and every consumer that
294    /// pattern-matched exhaustively against the closed set gets a
295    /// compile-time error until it handles the new arm — the fifth
296    /// invariant of the Rust+Lisp pattern. Compiler-verified below
297    /// with an exhaustive match; a regression that added a
298    /// `#[non_exhaustive]` attribute or a private constructor arm
299    /// would break the exhaustiveness proof at this test.
300    #[test]
301    fn closed_set_exhausts_the_process_condition_type_alphabet() {
302        fn describe(v: ProcessConditionType) -> &'static str {
303            match v {
304                ProcessConditionType::Ready => "Ready",
305                ProcessConditionType::Attested => "Attested",
306            }
307        }
308        assert_eq!(describe(ProcessConditionType::Ready), "Ready");
309        assert_eq!(describe(ProcessConditionType::Attested), "Attested");
310    }
311
312    /// `Copy` + `Clone` + `Eq` + `Hash` — pins the trait derives at
313    /// compile time so a regression that dropped one (a future
314    /// `#[derive(Serialize, Deserialize)]` addition that reshaped
315    /// the enum, a manual `impl Clone` that dropped `Copy`) surfaces
316    /// here rather than at a downstream consumer that stored the
317    /// value in a `HashMap` key or copied it across an `if` arm.
318    #[test]
319    fn value_semantics_hold_at_compile_time() {
320        fn assert_copy<T: Copy>() {}
321        fn assert_hash<T: std::hash::Hash>() {}
322        fn assert_eq<T: Eq>() {}
323        assert_copy::<ProcessConditionType>();
324        assert_hash::<ProcessConditionType>();
325        assert_eq::<ProcessConditionType>();
326    }
327
328    /// Sibling-axis coherence with [`crate::k8s_condition::
329    /// K8sConditionStatus`] on the K8s-Condition wire-form axis-
330    /// family — the two primitives partition the `metav1.Condition`
331    /// wire alphabet by SLOT (`type` here, `status` at the sibling),
332    /// so a caller that accidentally passed a ProcessConditionType
333    /// wire-form to `K8sConditionStatus::from_wire_str` (or vice
334    /// versa) MUST reject. Pins the invariant that the two closed
335    /// sets are DISJOINT — a case-flip or a copy-paste that swapped
336    /// the two `from_wire_str` calls surfaces HERE as a `None` at
337    /// the wrong reader, not as silent classification skew.
338    #[test]
339    fn from_wire_str_rejects_sibling_axis_wire_forms() {
340        use crate::k8s_condition::K8sConditionStatus;
341        for status_wire in [
342            K8sConditionStatus::True.as_wire_str(),
343            K8sConditionStatus::False.as_wire_str(),
344            K8sConditionStatus::Unknown.as_wire_str(),
345        ] {
346            assert_eq!(
347                ProcessConditionType::from_wire_str(status_wire),
348                None,
349                "ProcessConditionType::from_wire_str accepted the sibling-axis \
350                 K8sConditionStatus wire-form `{status_wire:?}` — the two closed \
351                 sets MUST stay disjoint",
352            );
353        }
354        for type_wire in [
355            ProcessConditionType::Ready.as_wire_str(),
356            ProcessConditionType::Attested.as_wire_str(),
357        ] {
358            assert_eq!(
359                K8sConditionStatus::from_wire_str(type_wire),
360                None,
361                "K8sConditionStatus::from_wire_str accepted the sibling-axis \
362                 ProcessConditionType wire-form `{type_wire:?}` — the two closed \
363                 sets MUST stay disjoint",
364            );
365        }
366    }
367}