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}