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}