Skip to main content

tatara_process/
boundary.rs

1//! Boundary conditions — predicates that gate phase transitions.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::flux_resource::FluxResource;
7
8/// Boundary specification — preconditions gate Running,
9/// postconditions gate Running → Attested.
10#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "camelCase")]
12pub struct Boundary {
13    #[serde(default)]
14    pub preconditions: Vec<Condition>,
15    #[serde(default)]
16    pub postconditions: Vec<Condition>,
17    /// Max time before VERIFY fails — parsed as a `go`-style duration.
18    /// Empty = controller default (15m).
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub timeout: Option<String>,
21}
22
23impl Boundary {
24    /// True iff at least one [`Condition`] in
25    /// `preconditions ∪ postconditions` carries the given
26    /// [`ConditionKind`] — the ONE substrate primitive that owns the
27    /// (closed-set discriminator, boundary-condition presence) probe on
28    /// this typed surface.
29    ///
30    /// # Semantics
31    ///
32    /// The two condition vectors are unioned: a caller asking "does this
33    /// spec name a `ClosedLoopAuth` predicate anywhere" doesn't care
34    /// whether the operator authored it on the pre- or post-condition
35    /// side. A boundary with the given kind on ONLY preconditions returns
36    /// `true`; a boundary with the given kind on ONLY postconditions
37    /// returns `true`; a boundary with neither returns `false`.
38    ///
39    /// # Sibling to [`crate::intent::Intent::has`] + [`crate::lifetime::Lifetime::has`]
40    ///
41    /// Same shape, same axis, third instance in the workspace-wide
42    /// closed-set-driven presence-probe algebra. `Intent::has` +
43    /// `Lifetime::has` publish the same `(&self, K) -> bool` signature
44    /// where `K` is the discriminator's `Kind` (auto-derived through
45    /// `#[derive(DeriveClosedSet)]`). A future normalization at that
46    /// probe shape (a widened return carrying the matching Condition
47    /// ref, a debug-build assertion on pre/post drift, a fleet-wide
48    /// warn on redundant duplicates) lands at ONE site per surface
49    /// and every downstream `<xxx>-<kind>` require-tag family +
50    /// closed-set audit dispatcher picks it up mechanically.
51    ///
52    /// # Peer on the ephemeral surface — [`crate::ephemeral::EphemeralSpec::has_condition_kind`]
53    ///
54    /// Same signature `(ConditionKind) -> bool`, same union body
55    /// (`preconditions.has_kind(k) || postconditions.has_kind(k)`), on
56    /// the sugar-surface type [`crate::ephemeral::EphemeralSpec`] whose
57    /// pre/post condition vectors live directly on the struct rather
58    /// than inside a nested [`Boundary`] slot. Both methods compose
59    /// against the ONE slice-level substrate primitive
60    /// [`ConditionSliceExt::has_kind`] — a regression at the per-slice
61    /// walk fails at that primitive's tests rather than as silent drift
62    /// at either struct-level union caller. The ephemeral require-tag
63    /// classifier reaches its `condition-<kind>` prefix family through
64    /// the peer method byte-for-byte symmetrical with the point
65    /// surface's `condition-<kind>` family that composes through this
66    /// method.
67    ///
68    /// # Compounding
69    ///
70    /// The point-domain require-tag surface in
71    /// `tatara-reconciler::bin::tatara-check` composes this primitive
72    /// with the closed-set `FromStr` autoderived on [`ConditionKind`]
73    /// through the `strip_and_classify_prefixed_kind` substrate to
74    /// publish a `condition-<kind>` prefix family byte-for-byte
75    /// symmetrical with `intent-<kind>` + `lifetime-<kind>`. A future
76    /// [`ConditionKind`] variant added to `ALL` reaches every downstream
77    /// (require-tag classifier, coherence check, editor completion
78    /// provider) through the SAME closed-set walk with no per-caller
79    /// edit.
80    ///
81    /// Theory anchor: THEORY.md §II.1 invariant 5 (composition preserves
82    /// proofs — the presence-probe body lives at ONE substrate site so
83    /// every downstream `condition-<kind>` requires-tag surface,
84    /// closed-set audit dispatcher, and future variant addition binds
85    /// through the SAME shape). THEORY.md §VI.1 (generation over
86    /// composition — a ninth [`ConditionKind`] variant lands at ONE
87    /// `ALL` entry + ONE `as_str` arm and the presence probe picks it
88    /// up mechanically without further per-consumer edits).
89    #[must_use]
90    pub fn has_condition_kind(&self, kind: ConditionKind) -> bool {
91        self.preconditions.has_kind(kind) || self.postconditions.has_kind(kind)
92    }
93}
94
95/// Slice-level `(ConditionKind, presence)` probe on any `&[Condition]`
96/// — the ONE substrate primitive that owns the
97/// `.iter().any(|c| c.kind == K)` walk shape both current production
98/// sites hand-authored past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
99/// threshold. Callers compose the two-half union at their site
100/// ([`Boundary::has_condition_kind`] on `preconditions ∪
101/// postconditions`) or on ONE half only (the ephemeral require-tag
102/// classifier's `closed-loop-auth` arm on `spec.postconditions`) —
103/// the primitive owns ONLY the per-slice walk, so the composition
104/// choice stays typed at the caller.
105///
106/// # Why lift
107///
108/// Pre-lift the `.iter().any(|c| c.kind == K)` walk lived
109/// hand-authored at THREE production sites: twice inside
110/// [`Boundary::has_condition_kind`]'s union (pre + post), once at
111/// `evaluate_ephemeral_require_tag`'s `closed-loop-auth` arm in
112/// `tatara-reconciler::bin::tatara-check` (with `matches!` sugar
113/// instead of `==`, but the same predicate). The (`&[Condition]`,
114/// `ConditionKind`) → `bool` shape is the substrate primitive: a
115/// future consumer that walks a `Vec<Condition>` (a coherence check
116/// that verifies "every `ClosedLoopAuth` postcondition carries an
117/// `issuer` param key", an editor completion listing which
118/// [`ConditionKind`] arms appear on ONE side only, a hypothetical
119/// `postcondition-<kind>` require-tag prefix family that dispatches
120/// on `postconditions` alone — the peer of the existing
121/// `condition-<kind>` family that dispatches on the pre ∪ post union
122/// via [`Boundary::has_condition_kind`]) reaches this ONE primitive
123/// through `slice.has_kind(k)` instead of restating the `.iter().any`
124/// closure body.
125///
126/// # Sibling to [`Boundary::has_condition_kind`]
127///
128/// Same axis, one refinement lower: `Boundary::has_condition_kind` is
129/// the two-slice-union probe; `has_kind` here is the one-slice probe
130/// the union composes twice. A future normalization at the presence
131/// probe shape (widening the return to `Option<&Condition>` for
132/// deeper diagnostics, adding a debug-build assertion on redundant
133/// duplicates, switching to a linear scan that also counts matches)
134/// lands at ONE site here — both [`Boundary::has_condition_kind`] +
135/// every downstream `slice.has_kind(K)` callsite pick it up
136/// mechanically.
137///
138/// # Compounding
139///
140/// A future extension of the probe algebra to a
141/// `has_kind_matching(|&Condition| -> bool)` predicate variant (e.g.
142/// "does any `ClosedLoopAuth` postcondition have a non-empty
143/// `probeImage`?") lands as ONE new default method on this trait —
144/// the closed-set discriminator case above becomes `has_kind(k) ==
145/// self.has_kind_matching(|c| c.kind == k)` by construction, so a
146/// regression that drifted one from the other becomes structurally
147/// impossible past the trait boundary.
148///
149/// Theory anchor: THEORY.md §II.1 invariant 5 — composition preserves
150/// proofs; the per-slice walk lives at ONE substrate site so the
151/// two-half union in [`Boundary`] and the one-half probe on
152/// [`crate::ephemeral::EphemeralSpec::postconditions`] compose
153/// through the SAME primitive. THEORY.md §VI.1 — generation over
154/// composition; a future `Vec<Condition>` consumer reaches the
155/// primitive through `slice.has_kind(k)` with no per-caller
156/// restatement of the `.iter().any(|c| c.kind == K)` closure body.
157pub trait ConditionSliceExt {
158    /// True iff at least one [`Condition`] in this slice carries the
159    /// given [`ConditionKind`]. The single-slice presence probe both
160    /// [`Boundary::has_condition_kind`] (twice, in a union) and the
161    /// ephemeral `closed-loop-auth` require-tag arm (once, on
162    /// postconditions only) compose against.
163    fn has_kind(&self, kind: ConditionKind) -> bool;
164}
165
166impl ConditionSliceExt for [Condition] {
167    fn has_kind(&self, kind: ConditionKind) -> bool {
168        self.iter().any(|c| c.kind == kind)
169    }
170}
171
172/// A single boundary predicate.
173#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "camelCase")]
175pub struct Condition {
176    pub kind: ConditionKind,
177    /// Kind-specific payload (free-form JSON).
178    #[serde(default)]
179    #[schemars(schema_with = "crate::schema_helpers::preserve_unknown_object")]
180    pub params: serde_json::Value,
181}
182
183#[derive(
184    Clone,
185    Copy,
186    Debug,
187    PartialEq,
188    Eq,
189    Hash,
190    Serialize,
191    Deserialize,
192    JsonSchema,
193    tatara_closed_set::DeriveClosedSet,
194)]
195#[serde(rename_all = "PascalCase")]
196#[closed_set(via = "as_str", display, generate_unknown)]
197pub enum ConditionKind {
198    /// Another Process must be in a given phase.
199    /// `params`: `{ "processRef": "...", "namespace": "...", "phase": "Attested" }`
200    ProcessPhase,
201    /// FluxCD `Kustomization.status.conditions[type=Ready]` must be `True`.
202    /// `params`: `{ "name": "...", "namespace": "flux-system" }`
203    KustomizationHealthy,
204    /// FluxCD `HelmRelease.status.conditions[type=Ready]` must be `True`.
205    /// `params`: `{ "name": "...", "namespace": "..." }`
206    HelmReleaseReleased,
207    /// Prometheus query — truthy scalar required.
208    /// `params`: `{ "query": "..." }`
209    PromQL,
210    /// CEL expression over a scoped object set.
211    /// `params`: `{ "expression": "..." }`
212    Cel,
213    /// Nix evaluation equality check.
214    /// `params`: `{ "flakeRef": "...", "attribute": "...", "expect": "..." }`
215    NixEval,
216    /// A Kubernetes Job must complete successfully and its emitted BLAKE3
217    /// receipt must verify.
218    /// `params`: `{ "name": "...", "namespace": "...", "expectReceipt": true }`
219    JobAttested,
220    /// Closed-loop authentication probe — the canonical postcondition for
221    /// any system that can produce credentials for its own client under
222    /// test. The probe Job (rendered by the VERIFY handler) fetches a
223    /// fresh secret from `issuer` (a Service inside the same namespace),
224    /// presents it to `consumer` (another Service in the same namespace),
225    /// and verifies that `consumer` authenticated successfully against
226    /// `jwk_source` (the issuer's published JWK endpoint).
227    ///
228    /// The Job emits a three-pillar BLAKE3 receipt that the reconciler
229    /// chains into `status.attestation`. This turns "the gateway↔SaaS
230    /// loop holds" from an assertion into a theorem provable for every
231    /// ephemeral run.
232    ///
233    /// `params`:
234    /// ```json
235    /// {
236    ///   "issuer":   { "service": "demo-app-issuer",
237    ///                 "port": 8080,
238    ///                 "secretPath": "/v2/get-secret-value" },
239    ///   "consumer": { "service": "demo-app-gateway",
240    ///                 "port": 8000,
241    ///                 "authPath": "/api/v3/auth" },
242    ///   "jwkSource":{ "service": "demo-app-issuer",
243    ///                 "port": 8080,
244    ///                 "path": "/.well-known/jwks.json" },
245    ///   "probeImage": "ghcr.io/pleme-io/closed-loop-probe:0.1.0",
246    ///   "timeoutSeconds": 120
247    /// }
248    /// ```
249    ClosedLoopAuth,
250}
251
252impl ConditionKind {
253    /// The closed set of boundary-condition kinds the reconciler honors.
254    /// Single source of truth that drives the `as_str` / Display /
255    /// `FromStr` triad on this enum and the `stub_message` lift of the
256    /// "not yet implemented" arms the reconciler used to hand-roll three
257    /// times. Adding a 9th variant lands at one `ALL` entry + one `as_str`
258    /// arm + one `stub_message` arm — exhaustively checked by the
259    /// compiler (the array literal forces arity).
260    ///
261    /// Sibling closed-set lifts: [`crate::phase::ProcessPhase::ALL`],
262    /// [`crate::signal::ProcessSignal::ALL`], [`crate::intent::IntentKind::ALL`],
263    /// [`crate::lifetime::LifetimeKind::ALL`].
264    pub const ALL: [Self; 8] = [
265        Self::ProcessPhase,
266        Self::KustomizationHealthy,
267        Self::HelmReleaseReleased,
268        Self::PromQL,
269        Self::Cel,
270        Self::NixEval,
271        Self::JobAttested,
272        Self::ClosedLoopAuth,
273    ];
274
275    /// Canonical PascalCase wire-format projection — matches the serde
276    /// `rename_all = "PascalCase"` output verbatim. Used by Display
277    /// (single source of truth), by `FromStr` to identify the variant
278    /// from its annotation / status-field representation, and by
279    /// operator-facing diagnostics that need the kind name without
280    /// re-serializing the enum through serde_json. Pinned by
281    /// `condition_kind_as_str_matches_serde`.
282    pub const fn as_str(self) -> &'static str {
283        match self {
284            Self::ProcessPhase => "ProcessPhase",
285            Self::KustomizationHealthy => "KustomizationHealthy",
286            Self::HelmReleaseReleased => "HelmReleaseReleased",
287            Self::PromQL => "PromQL",
288            Self::Cel => "Cel",
289            Self::NixEval => "NixEval",
290            Self::JobAttested => "JobAttested",
291            Self::ClosedLoopAuth => "ClosedLoopAuth",
292        }
293    }
294
295    /// The operator-facing "evaluator not yet implemented" message for
296    /// stub kinds — `Some` iff this kind has no live evaluator wired in
297    /// `tatara-reconciler::boundary`. ONE site owns the per-kind stub
298    /// string; the reconciler's dispatch reaches for this projection
299    /// instead of hand-rolling three parallel `Unknown(...)` strings.
300    ///
301    /// A future variant added as a live evaluator returns `None`; a
302    /// future variant added as a stub returns `Some("<kind> evaluator
303    /// not yet implemented")` — both reachable through one match
304    /// instead of three identical-shape arms drifting in parallel.
305    pub const fn stub_message(self) -> Option<&'static str> {
306        match self {
307            Self::PromQL => Some("PromQL evaluator not yet implemented"),
308            Self::Cel => Some("CEL evaluator not yet implemented"),
309            Self::NixEval => Some("NixEval evaluator not yet implemented"),
310            Self::ProcessPhase
311            | Self::KustomizationHealthy
312            | Self::HelmReleaseReleased
313            | Self::JobAttested
314            | Self::ClosedLoopAuth => None,
315        }
316    }
317
318    /// True iff this kind has no live evaluator (its [`Self::stub_message`]
319    /// is `Some`). Pairs with the reconciler's `evaluate` dispatch — a
320    /// stub kind unconditionally yields `Satisfaction::Unknown`.
321    pub const fn is_stub(self) -> bool {
322        self.stub_message().is_some()
323    }
324
325    /// The [`FluxResource`] variant this condition kind fetches from
326    /// the K8s API server, or `None` for non-Flux-fetching kinds — the
327    /// typed projection owning the (ConditionKind → FluxResource)
328    /// association every reconciler `evaluate` dispatch arm and every
329    /// future coherence check binds through.
330    ///
331    /// Pre-lift the association was open-coded at TWO adjacent
332    /// `evaluate` arms in `tatara-reconciler::boundary::evaluate` past
333    /// the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold — each arm
334    /// hand-authored a `(FluxResource::X.api_version(),
335    /// FluxResource::X.kind())` pair as the two `&str` slots the
336    /// pre-lift `evaluate_flux_ready(api_version: &str, kind: &str)`
337    /// signature required. Post-lift the mapping lives at ONE typed
338    /// projection here, the callee accepts a typed
339    /// [`FluxResource`] slot (invalid `(apiVersion, kind)` pairings
340    /// like Kustomization's apiVersion paired with HelmRelease's kind
341    /// become unrepresentable), and the two `evaluate` arms collapse
342    /// onto ONE `KustomizationHealthy | HelmReleaseReleased` OR-arm
343    /// that reads the FluxResource variant from `.flux_resource()`.
344    ///
345    /// A future ConditionKind that fetches a fourth Flux resource
346    /// variant (a hypothetical `BucketSynced` kind against a Flux
347    /// `Bucket` source) lands as ONE new arm here + ONE new variant
348    /// on [`FluxResource`] + ONE OR-pattern extension at the
349    /// reconciler dispatch — no hand-authored `(apiVersion, kind)`
350    /// pair at the callsite, no widening of the callee's signature.
351    ///
352    /// The three current non-Flux-fetching arms return `None`:
353    /// - `ProcessPhase` fetches a tatara `Process` (through its own
354    ///   [`crate::api_version`] + [`crate::PROCESS_KIND`] pair, not
355    ///   a Flux `(apiVersion, kind)`).
356    /// - `JobAttested` / `ClosedLoopAuth` fetch a `batch/v1::Job` +
357    ///   an optional receipt `v1::ConfigMap`, both K8s built-ins
358    ///   (not Flux resources).
359    /// - `PromQL` / `Cel` / `NixEval` are stub evaluators
360    ///   ([`Self::is_stub`]) — no cluster fetch at all.
361    ///
362    /// Theory anchor: THEORY.md §II.1 invariant 5 (composition
363    /// preserves proofs — the (ConditionKind → FluxResource)
364    /// association lives at ONE typed algebra projection here, not
365    /// at every reconciler dispatch arm).
366    pub const fn flux_resource(self) -> Option<FluxResource> {
367        match self {
368            Self::KustomizationHealthy => Some(FluxResource::Kustomization),
369            Self::HelmReleaseReleased => Some(FluxResource::HelmRelease),
370            Self::ProcessPhase
371            | Self::PromQL
372            | Self::Cel
373            | Self::NixEval
374            | Self::JobAttested
375            | Self::ClosedLoopAuth => None,
376        }
377    }
378}
379
380// `impl fmt::Display for ConditionKind` + `impl FromStr for
381// ConditionKind` + `impl tatara_lisp::ClosedSet for ConditionKind` +
382// `pub struct UnknownConditionKind(pub String)` are generated by
383// `#[derive(tatara_closed_set::DeriveClosedSet)]` + `#[closed_set(via =
384// "as_str", display, generate_unknown)]` on the enum declaration above.
385// The auto-derived label `"condition kind"` matches the prior hand-
386// rolled `#[error("unknown condition kind: {0}")]` verbatim. The
387// inherent `as_str` projection stays load-bearing — the PascalCase
388// wire-format that matches the serde rename + the CRD `enum:` listing
389// verbatim (notably preserving `PromQL`'s consecutive caps that heck
390// would have lowercased) — while the trait method `label` gives
391// generic consumers a STABLE name across the 36+ workspace-wide
392// closed-set implementors.
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use serde_json::json;
398
399    #[test]
400    fn serde_process_phase_condition() {
401        let c = Condition {
402            kind: ConditionKind::ProcessPhase,
403            params: json!({ "processRef": "secret-injection", "phase": "Attested" }),
404        };
405        let yaml = serde_yaml::to_string(&c).unwrap();
406        assert!(yaml.contains("kind: ProcessPhase"));
407        assert!(yaml.contains("processRef: secret-injection"));
408    }
409
410    #[test]
411    fn serde_closed_loop_auth_condition() {
412        let c = Condition {
413            kind: ConditionKind::ClosedLoopAuth,
414            params: json!({
415                "issuer":   { "service": "demo-app-issuer", "port": 8080 },
416                "consumer": { "service": "demo-app-gateway", "port": 8000 },
417                "probeImage": "ghcr.io/pleme-io/closed-loop-probe:0.1.0",
418            }),
419        };
420        let yaml = serde_yaml::to_string(&c).unwrap();
421        assert!(yaml.contains("kind: ClosedLoopAuth"));
422        assert!(yaml.contains("probeImage: ghcr.io/pleme-io/closed-loop-probe:0.1.0"));
423        let back: Condition = serde_yaml::from_str(&yaml).unwrap();
424        assert_eq!(back.kind, ConditionKind::ClosedLoopAuth);
425    }
426
427    #[test]
428    fn serde_job_attested_condition() {
429        let c = Condition {
430            kind: ConditionKind::JobAttested,
431            params: json!({ "name": "seed-job", "namespace": "demo-test" }),
432        };
433        let yaml = serde_yaml::to_string(&c).unwrap();
434        assert!(yaml.contains("kind: JobAttested"));
435    }
436
437    // ── closed-set algebra contracts (ALL × as_str × FromStr × stub_message) ─
438
439    /// Structural well-formedness of [`ConditionKind`] as a
440    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
441    /// testkit lift that pins all three structural invariants (`ALL`
442    /// is non-empty, every variant round-trips through `label ↔
443    /// parse_label`, labels are pairwise distinct, `""` is outside the
444    /// closed set) at ONE call site. Replaces the hand-derived
445    /// `condition_kind_all_is_unique_and_complete` +
446    /// `condition_kind_roundtrip_via_as_str` + the empty-input arm of
447    /// `unknown_condition_kind_errors`. `FromStr` delegates to
448    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
449    /// exercises the same code path the reconciler hits when parsing a
450    /// CRD `enum:`-validated value back to the typed kind.
451    #[test]
452    fn condition_kind_is_well_formed_closed_set() {
453        tatara_closed_set::assert_closed_set_well_formed::<ConditionKind>();
454    }
455
456    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
457    /// output verbatim for every variant. A future variant rename
458    /// (or an `as_str` arm typo) lands here at one site. The probe
459    /// confirmed `PromQL` survives `rename_all = "PascalCase"` as
460    /// `"PromQL"` (heck preserves consecutive caps in the leading
461    /// word), so this contract is the operator-facing pin.
462    #[test]
463    fn condition_kind_as_str_matches_serde() {
464        crate::tagged_union::assert_label_matches_serde_serialization::<ConditionKind>();
465    }
466
467    /// The Display impl IS `as_str` — pinning this lets future
468    /// callers reach for either projection without drift. If a
469    /// reviewer accidentally re-introduces an inline match in
470    /// Display, this fails the moment a variant rename touches one
471    /// site but not the other.
472    #[test]
473    fn condition_kind_display_matches_as_str() {
474        crate::tagged_union::assert_display_matches_label::<ConditionKind>();
475    }
476
477    /// `FromStr` rejects strings that aren't in the canonical
478    /// projection — lowercased / typo / unrelated — and the error
479    /// echoes the input verbatim so the operator-facing diagnostic
480    /// carries the offending value, not a normalized form. The
481    /// empty-input arm is pinned by
482    /// [`condition_kind_is_well_formed_closed_set`] via the
483    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
484    /// verbatim-echo contract on the [`UnknownConditionKind`]
485    /// newtype, which the trait's `make_unknown` can't see.
486    #[test]
487    fn unknown_condition_kind_errors() {
488        use std::str::FromStr;
489        for bad in ["processPhase", "PROMQL", "Promql", "Bogus"] {
490            let err = ConditionKind::from_str(bad).unwrap_err();
491            assert_eq!(err.0, bad, "error payload should echo input verbatim");
492        }
493    }
494
495    /// STUB CONTRACT: the three placeholder evaluators
496    /// (PromQL / Cel / NixEval) are exactly the set whose
497    /// `stub_message` is `Some`. The five live evaluators return
498    /// `None`. A future variant promoted from stub → live must drop
499    /// its `stub_message` arm; a new stub must add one. Both
500    /// transitions land at this test by sweeping ALL.
501    #[test]
502    fn condition_kind_stub_set_matches_stubs() {
503        use ConditionKind::*;
504        for kind in ConditionKind::ALL {
505            let expected_is_stub = matches!(kind, PromQL | Cel | NixEval);
506            assert_eq!(
507                kind.is_stub(),
508                expected_is_stub,
509                "is_stub disagreed for {kind:?}",
510            );
511            assert_eq!(
512                kind.stub_message().is_some(),
513                expected_is_stub,
514                "stub_message disagreed for {kind:?}",
515            );
516        }
517    }
518
519    /// Pin the exact stub strings so a rename of the operator-facing
520    /// "not yet implemented" message lands at one site (here) instead
521    /// of three parallel inline strings in the reconciler.
522    #[test]
523    fn condition_kind_stub_messages_are_pinned() {
524        assert_eq!(
525            ConditionKind::PromQL.stub_message(),
526            Some("PromQL evaluator not yet implemented"),
527        );
528        assert_eq!(
529            ConditionKind::Cel.stub_message(),
530            Some("CEL evaluator not yet implemented"),
531        );
532        assert_eq!(
533            ConditionKind::NixEval.stub_message(),
534            Some("NixEval evaluator not yet implemented"),
535        );
536    }
537
538    // ── (ConditionKind → FluxResource) typed projection contracts ────
539
540    /// The two Flux-fetching kinds project to their canonical
541    /// [`FluxResource`] variants. A future ConditionKind rename or
542    /// FluxResource variant rename that skewed the projection at ONE
543    /// arm surfaces here.
544    #[test]
545    fn kustomization_healthy_projects_to_flux_resource_kustomization() {
546        assert_eq!(
547            ConditionKind::KustomizationHealthy.flux_resource(),
548            Some(FluxResource::Kustomization),
549        );
550    }
551
552    #[test]
553    fn helm_release_released_projects_to_flux_resource_helm_release() {
554        assert_eq!(
555            ConditionKind::HelmReleaseReleased.flux_resource(),
556            Some(FluxResource::HelmRelease),
557        );
558    }
559
560    /// The six non-Flux-fetching kinds project to `None`. Sweeps
561    /// `ConditionKind::ALL` filtering by `flux_resource().is_none()`
562    /// so a new variant added without a `flux_resource` arm surfaces
563    /// at rustc's non-exhaustive-match gate BEFORE this test even
564    /// runs; a new variant added with a hand-coded `Some(...)` arm
565    /// that shouldn't fetch Flux surfaces here.
566    #[test]
567    fn non_flux_fetching_kinds_project_to_none() {
568        use ConditionKind::*;
569        let non_flux: Vec<_> = ConditionKind::ALL
570            .iter()
571            .copied()
572            .filter(|k| k.flux_resource().is_none())
573            .collect();
574        assert_eq!(
575            non_flux,
576            vec![
577                ProcessPhase,
578                PromQL,
579                Cel,
580                NixEval,
581                JobAttested,
582                ClosedLoopAuth
583            ],
584        );
585    }
586
587    /// Every variant of [`ConditionKind`] whose `flux_resource()` is
588    /// `Some` uniquely names its FluxResource variant (no two
589    /// ConditionKind arms may fetch the SAME FluxResource — that
590    /// would signal a redundant closed-set entry). Peers the
591    /// `every_variants_api_version_and_kind_are_distinct_across_the_closed_set`
592    /// pin on the sibling [`FluxResource`] closed set.
593    #[test]
594    fn flux_resource_projection_is_injective_on_the_some_arms() {
595        let mut seen = std::collections::HashSet::new();
596        for k in ConditionKind::ALL {
597            if let Some(fr) = k.flux_resource() {
598                assert!(
599                    seen.insert(fr),
600                    "duplicate FluxResource projection at {k:?}: {fr:?}",
601                );
602            }
603        }
604    }
605
606    /// `flux_resource` is `const fn` — the projection is reachable
607    /// at compile time. A regression that dropped the `const`
608    /// qualifier would fail-loudly here rather than as a wrong-slot
609    /// runtime dispatch at every consumer callsite.
610    #[test]
611    fn flux_resource_projection_is_const_fn_reachable() {
612        const K: Option<FluxResource> = ConditionKind::KustomizationHealthy.flux_resource();
613        const H: Option<FluxResource> = ConditionKind::HelmReleaseReleased.flux_resource();
614        const P: Option<FluxResource> = ConditionKind::ProcessPhase.flux_resource();
615        assert_eq!(K, Some(FluxResource::Kustomization));
616        assert_eq!(H, Some(FluxResource::HelmRelease));
617        assert_eq!(P, None);
618    }
619
620    // ── Boundary::has_condition_kind substrate pins ──────────────────
621    //
622    // Fail-before-pass-after granularity: `Boundary::has_condition_kind`
623    // did not exist before this commit — the (preconditions +
624    // postconditions .iter().any(|c| c.kind == K)) union-probe shape
625    // lived hand-authored inline at the ephemeral require-tag surface
626    // (`spec.postconditions.iter().any(|c| matches!(c.kind, K))`, sans
627    // the pre-condition side). The lift places the closed-set-driven
628    // presence probe on ONE substrate site so the point-domain
629    // `condition-<kind>` prefix family in `tatara-check` composes it
630    // through `strip_and_classify_prefixed_kind` byte-for-byte
631    // symmetrical with `intent-<kind>` (via `Intent::has`) +
632    // `lifetime-<kind>` (via `Lifetime::has`) — third instance in the
633    // workspace closed-set-driven presence-probe algebra.
634
635    fn condition_with(kind: ConditionKind) -> Condition {
636        Condition {
637            kind,
638            params: json!({}),
639        }
640    }
641
642    /// EMPTY-BOUNDARY pin — a default [`Boundary`] (no preconditions,
643    /// no postconditions) returns `false` for EVERY [`ConditionKind`].
644    /// Sweep `ConditionKind::ALL` so a new variant added without a
645    /// matching arm in the presence probe surfaces at rustc's
646    /// exhaustiveness gate on the ALL literal (arity forced by
647    /// `[Self; 8]`) rather than as a silent false-positive at every
648    /// downstream `condition-<kind>` require-tag callsite.
649    #[test]
650    fn has_condition_kind_returns_false_on_empty_boundary_for_every_kind() {
651        let b = Boundary::default();
652        for kind in ConditionKind::ALL {
653            assert!(
654                !b.has_condition_kind(kind),
655                "default boundary must return false for {kind:?}",
656            );
657        }
658    }
659
660    /// POSTCONDITION-only pin — a boundary that carries the kind on
661    /// ONLY postconditions returns `true` for that kind, `false` for
662    /// every other variant. Sweep the ALL × ALL cross so a regression
663    /// that (a) hard-coded the arm to a single kind (silently
664    /// returning true for every populated boundary regardless of
665    /// which kind was queried), (b) skipped the postcondition side of
666    /// the union (silently returning false when the kind lived
667    /// post-only), or (c) matched on Condition::params instead of
668    /// Condition::kind fails HERE at the substrate primitive.
669    #[test]
670    fn has_condition_kind_reads_postconditions_per_kind() {
671        for populated in ConditionKind::ALL {
672            let mut b = Boundary::default();
673            b.postconditions.push(condition_with(populated));
674            for query in ConditionKind::ALL {
675                let expected = query == populated;
676                assert_eq!(
677                    b.has_condition_kind(query),
678                    expected,
679                    "postcondition populated={populated:?}: query {query:?} drifted",
680                );
681            }
682        }
683    }
684
685    /// PRECONDITION-only pin — mirrors the postcondition sweep on the
686    /// other half of the union. Locks the union semantics on both
687    /// halves separately so a regression that dropped the
688    /// pre-condition side of the OR fails here even though the
689    /// postcondition-side pin above passes.
690    #[test]
691    fn has_condition_kind_reads_preconditions_per_kind() {
692        for populated in ConditionKind::ALL {
693            let mut b = Boundary::default();
694            b.preconditions.push(condition_with(populated));
695            for query in ConditionKind::ALL {
696                let expected = query == populated;
697                assert_eq!(
698                    b.has_condition_kind(query),
699                    expected,
700                    "precondition populated={populated:?}: query {query:?} drifted",
701                );
702            }
703        }
704    }
705
706    /// UNION pin — a kind that appears on preconditions returns
707    /// `true` even when postconditions carries a DIFFERENT kind, and
708    /// vice versa. Pins the OR-composition of the two halves so a
709    /// regression that collapsed the union to an intersection (AND)
710    /// silently reclassifies pre-only or post-only kinds as absent.
711    #[test]
712    fn has_condition_kind_unions_pre_and_post_condition_arms() {
713        let mut b = Boundary::default();
714        b.preconditions
715            .push(condition_with(ConditionKind::KustomizationHealthy));
716        b.postconditions
717            .push(condition_with(ConditionKind::ClosedLoopAuth));
718        assert!(
719            b.has_condition_kind(ConditionKind::KustomizationHealthy),
720            "pre-only kind must resolve through the union",
721        );
722        assert!(
723            b.has_condition_kind(ConditionKind::ClosedLoopAuth),
724            "post-only kind must resolve through the union",
725        );
726        assert!(
727            !b.has_condition_kind(ConditionKind::PromQL),
728            "an absent kind must return false even with populated halves",
729        );
730    }
731
732    // ── ConditionSliceExt::has_kind substrate pins ────────────────────
733    //
734    // Fail-before-pass-after granularity: `ConditionSliceExt::has_kind`
735    // did not exist before this commit — the `(&[Condition],
736    // ConditionKind) -> bool` walk shape lived hand-authored inline at
737    // THREE production sites (twice inside `Boundary::has_condition_kind`
738    // on `preconditions` ∪ `postconditions`, once at the ephemeral
739    // require-tag classifier's `closed-loop-auth` arm on
740    // `spec.postconditions` in `tatara-reconciler::bin::tatara-check`,
741    // with `matches!` sugar instead of `==` but the same predicate).
742    // The lift places the per-slice presence probe on ONE substrate site
743    // so the two-half union at `Boundary` and the one-half probe at the
744    // ephemeral surface compose against the SAME primitive rather than
745    // restating the `.iter().any(|c| c.kind == K)` closure body.
746
747    /// EMPTY-SLICE pin — an empty `&[Condition]` returns `false` for
748    /// EVERY [`ConditionKind`]. Sweep `ConditionKind::ALL` so a new
749    /// variant added without a matching arm in the primitive surfaces
750    /// at rustc's exhaustiveness gate on the ALL literal (arity forced
751    /// by `[Self; 8]`) rather than as a silent false-positive at every
752    /// downstream callsite composing this primitive.
753    #[test]
754    fn condition_slice_has_kind_returns_false_on_empty_slice_for_every_kind() {
755        let empty: &[Condition] = &[];
756        for kind in ConditionKind::ALL {
757            assert!(
758                !empty.has_kind(kind),
759                "empty slice must return false for {kind:?}",
760            );
761        }
762    }
763
764    /// PER-VARIANT pin — a single-element slice returns `true` for
765    /// exactly the kind it carries, `false` for every other variant.
766    /// Sweep the ALL × ALL cross so a regression that (a) hard-coded
767    /// the arm to a single kind (silently returning true for every
768    /// populated slice regardless of query kind), or (b) matched on
769    /// [`Condition::params`] instead of [`Condition::kind`] fails HERE
770    /// at the substrate primitive.
771    #[test]
772    fn condition_slice_has_kind_reads_kind_field_per_variant() {
773        for populated in ConditionKind::ALL {
774            let slice = [condition_with(populated)];
775            for query in ConditionKind::ALL {
776                let expected = query == populated;
777                assert_eq!(
778                    slice.has_kind(query),
779                    expected,
780                    "populated={populated:?}: query {query:?} drifted",
781                );
782            }
783        }
784    }
785
786    /// MULTI-ENTRY pin — a slice with multiple entries returns `true`
787    /// for every kind that appears at any position (existential
788    /// quantifier over the slice), `false` for kinds that appear at
789    /// no position. Locks the `any` semantics so a regression that
790    /// collapsed to a `first`-only probe (`slice.first().map_or(false,
791    /// |c| c.kind == kind)`) fails here even though the single-element
792    /// per-variant pin above passes.
793    #[test]
794    fn condition_slice_has_kind_scans_beyond_the_first_position() {
795        let slice = [
796            condition_with(ConditionKind::KustomizationHealthy),
797            condition_with(ConditionKind::ClosedLoopAuth),
798            condition_with(ConditionKind::JobAttested),
799        ];
800        for present in [
801            ConditionKind::KustomizationHealthy,
802            ConditionKind::ClosedLoopAuth,
803            ConditionKind::JobAttested,
804        ] {
805            assert!(
806                slice.has_kind(present),
807                "kind at any position must resolve true: {present:?}",
808            );
809        }
810        for absent in [
811            ConditionKind::ProcessPhase,
812            ConditionKind::HelmReleaseReleased,
813            ConditionKind::PromQL,
814            ConditionKind::Cel,
815            ConditionKind::NixEval,
816        ] {
817            assert!(
818                !slice.has_kind(absent),
819                "kind absent from the slice must resolve false: {absent:?}",
820            );
821        }
822    }
823
824    /// COMPOSITION pin — [`Boundary::has_condition_kind`] equals the OR
825    /// of the two half-slice probes at EVERY (populated arrangement,
826    /// query) pair on `ConditionKind::ALL`. Locks the (union-probe =
827    /// pre.has_kind ∨ post.has_kind) composition contract at ONE test
828    /// so a regression that (a) dropped the `||` (silently narrowing
829    /// the union to an intersection, or to one side only), or
830    /// (b) hand-authored the union with a divergent walk shape (e.g.
831    /// summing counts, comparing lengths) surfaces HERE at the
832    /// composition boundary rather than as silent classifier drift at
833    /// every downstream `condition-<kind>` require-tag callsite.
834    #[test]
835    fn boundary_has_condition_kind_equals_or_of_half_slice_probes() {
836        for pre_kind in ConditionKind::ALL {
837            for post_kind in ConditionKind::ALL {
838                let mut b = Boundary::default();
839                b.preconditions.push(condition_with(pre_kind));
840                b.postconditions.push(condition_with(post_kind));
841                for query in ConditionKind::ALL {
842                    let expected =
843                        b.preconditions.has_kind(query) || b.postconditions.has_kind(query);
844                    assert_eq!(
845                        b.has_condition_kind(query),
846                        expected,
847                        "union drifted: pre={pre_kind:?} post={post_kind:?} query={query:?}",
848                    );
849                }
850            }
851        }
852    }
853}