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}