Skip to main content

tatara_process/
classification.rs

1//! The six classification dimensions — CRD-facing with `JsonSchema`,
2//! `From`/`Into` bridges to `tatara_core::domain::classification`.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use tatara_core::domain::classification as core;
8use tatara_core::domain::compliance_binding as core_compl;
9
10/// Lattice position of a Process — six orthogonal axes.
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "camelCase")]
13pub struct Classification {
14    pub point_type: ConvergencePointType,
15    pub substrate: SubstrateType,
16    #[serde(default)]
17    pub horizon: Horizon,
18    #[serde(default)]
19    pub calm: CalmClassification,
20    #[serde(default)]
21    pub data_classification: DataClassification,
22}
23
24impl Classification {
25    /// The workspace-baseline classification — a [`ConvergencePointType::Gate`]
26    /// point on the [`SubstrateType::Compute`] substrate with every other axis
27    /// at its [`Default`]. The `(Gate, Compute)` pair names an unremarkable
28    /// barrier point in the Compute plane: no domain-specific structural
29    /// claim (no fan-out / fan-in / broadcast / observation semantics beyond
30    /// the barrier gate) and no domain-specific substrate claim (no
31    /// `Financial` / `Network` / `Storage` / `Security` / `Identity` /
32    /// `Observability` / `Regulatory` plane bringing in its own compliance
33    /// baselines). The three defaulted axes ride at the intentional
34    /// workspace baseline the sibling closed-set primitives already own:
35    /// [`Horizon`] at [`HorizonKind::Bounded`] (terminates naturally, no
36    /// asymptotic metric axes required), [`CalmClassification::Monotone`]
37    /// (no coordination required per CALM), and
38    /// [`DataClassification::Internal`] (access-controlled but not
39    /// externally regulated).
40    ///
41    /// Pre-lift the six-line `Classification { point_type: Gate, substrate:
42    /// Compute, horizon: Default::default(), calm: Default::default(),
43    /// data_classification: Default::default() }` struct-literal recurred
44    /// at TEN sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold
45    /// — one production consumer plus nine test-fixture callsites spread
46    /// across four crates, each restating the SAME `(Gate, Compute)`
47    /// baseline verbatim:
48    /// * `crate::ephemeral::default_ephemeral_class` — the substitute
49    ///   [`EphemeralSpec::into::<crate::crd::ProcessSpec>`] fills into
50    ///   [`crate::crd::ProcessSpec::classification`] when the operator
51    ///   omits an explicit `:classification` slot on `(defephemeral …)`.
52    ///   The one PRODUCTION consumer of the shape — a regression that
53    ///   drifted its point-type or substrate axis silently retargets every
54    ///   unadorned ephemeral to a different plane.
55    /// * `crate::crd`'s + `crate::lib`'s + `crate::lifetime_clock`'s +
56    ///   `tatara_reconciler::{claim,render}`'s + `tatara_pool_reconciler::
57    ///   controller_pool`'s `empty_spec` / `empty_process_spec` /
58    ///   `ephemeral_process` / `permanent_process` test-fixture helpers +
59    ///   inline `ProcessSpec` literals — nine test-fixture callsites
60    ///   restating the SAME six-line struct-literal at the same shape.
61    ///
62    /// Post-lift every callsite reads `Classification::gate_compute()`;
63    /// a future workspace-wide baseline shift (a new [`Horizon`] default,
64    /// a promotion of `Compute` to a compound baseline that pre-fills a
65    /// canonical [`CalmClassification`], a per-baseline compliance overlay
66    /// stamping through the classification, or a rename of either axis
67    /// enum) lands at ONE substrate function here and every downstream
68    /// consumer inherits the upgrade mechanically. The current pin ties
69    /// the three defaulted axes to the sibling closed-set defaults
70    /// ([`HorizonKind::Bounded`], [`CalmClassification::Monotone`],
71    /// [`DataClassification::Internal`]) so a future change to any sibling
72    /// default surfaces at this primitive's tests rather than as silent
73    /// drift across ten independent callsites.
74    ///
75    /// Sibling to the `_or_default` / `_or_placeholder` primitive family on
76    /// [`crate::prelude::Process`] on the (return-form × axis) axis — those
77    /// primitives own the borrow-form projections off a live `Process`;
78    /// this one owns the construction shape for a fresh
79    /// [`crate::crd::ProcessSpec`] whose classification axis is
80    /// unremarkable. A future peer `Classification::observe_observability()`
81    /// or similar named variant lands as a sibling method here when a
82    /// second unremarkable-baseline shape opens.
83    ///
84    /// Theory anchor: THEORY.md §VI.1 (generation over composition — the
85    /// six-line struct-literal shape recurred at TEN hand-authored sites
86    /// past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and is lifted
87    /// onto ONE workspace-wide owner here). THEORY.md §II.1 invariant 5
88    /// (composition preserves proofs — a regression that drifted the
89    /// baseline axis choice at only one consumer, or that broke the
90    /// sibling-default correspondence, surfaces at this primitive's tests
91    /// rather than as silent operator-visible skew between the ephemeral
92    /// sugar substitute and the ten downstream test-fixtures whose
93    /// assertions depend on the shape).
94    #[must_use]
95    pub fn gate_compute() -> Self {
96        Self {
97            point_type: ConvergencePointType::Gate,
98            substrate: SubstrateType::Compute,
99            horizon: Horizon::default(),
100            calm: CalmClassification::default(),
101            data_classification: DataClassification::default(),
102        }
103    }
104}
105
106/// Structural type — how data flows through the point.
107///
108/// Closed-set sibling on the classification axis algebra; the `ALL` /
109/// `as_str` / Display / `FromStr` triad mirrors
110/// [`DataClassification::ALL`], [`crate::pool::PoolPhase::ALL`],
111/// [`crate::pool::MemberState::ALL`], [`crate::pool::ReplacementPolicy::ALL`],
112/// [`crate::pool::ReturnPolicy::ALL`],
113/// [`crate::boundary::ConditionKind::ALL`],
114/// [`crate::lifetime::TeardownPolicy::ALL`],
115/// [`crate::lifetime::LifetimeKind::ALL`],
116/// [`crate::intent::IntentKind::ALL`],
117/// [`crate::phase::ProcessPhase::ALL`],
118/// [`crate::signal::ProcessSignal::ALL`]. The
119/// `(input_arity, output_arity)` projection (via [`Arity`]) closes the
120/// graph-topology contract: each variant lands in exactly one of the
121/// three structural buckets — endomorphic (1→1), diffusive (1→N), or
122/// convergent (N→1) — so future DAG composition / edge-cardinality
123/// validators dispatch on a typed projection rather than re-deriving
124/// from variant names.
125#[derive(
126    Clone,
127    Copy,
128    Debug,
129    PartialEq,
130    Eq,
131    Hash,
132    Serialize,
133    Deserialize,
134    JsonSchema,
135    tatara_closed_set::DeriveClosedSet,
136)]
137#[serde(rename_all = "PascalCase")]
138#[closed_set(via = "as_str", generate_unknown, display)]
139pub enum ConvergencePointType {
140    /// 1 input → 1 output (linear conversion).
141    Transform,
142    /// 1 input → N outputs (fan-out, spawns downstream DAGs).
143    Fork,
144    /// N inputs → 1 output (fan-in, merges upstream results).
145    Join,
146    /// N inputs → 1 output (barrier, waits for all inputs).
147    Gate,
148    /// N inputs → 1 output (choice, picks best by policy).
149    Select,
150    /// 1 input → N outputs same type (replicate signal).
151    Broadcast,
152    /// N inputs → 1 output (fold/aggregate).
153    Reduce,
154    /// 1 input → 1 output + side-channel (tap for observation).
155    Observe,
156}
157
158impl ConvergencePointType {
159    /// The closed set of point types — single source of truth that
160    /// drives the `as_str` / Display / `FromStr` triad AND the
161    /// `(input_arity, output_arity)` typed pair (via [`Arity`]) AND the
162    /// `is_endomorphic` / `is_diffusive` / `is_convergent` predicate
163    /// triple. Adding a ninth variant lands at one `ALL` entry + one
164    /// `as_str` arm + one `input_arity` arm + one `output_arity` arm +
165    /// one arm per predicate — exhaustively checked by the compiler
166    /// (the `[Self; 8]` array literal forces the arity) AND by the
167    /// per-variant truth-table contract test (a new variant must
168    /// declare its own `(input, output)` arity pair or any future
169    /// DAG composition validator that dispatches on
170    /// `(input_arity, output_arity)` will silently mis-wire it).
171    /// Closes the load-bearing classification-axis enum that
172    /// `tatara_core::domain::compliance_binding::PointSelector::ByType`
173    /// already dispatches against and that every `Process`'s
174    /// `Classification.point_type` reads as the topological identity
175    /// of the convergence point.
176    pub const ALL: [Self; 8] = [
177        Self::Transform,
178        Self::Fork,
179        Self::Join,
180        Self::Gate,
181        Self::Select,
182        Self::Broadcast,
183        Self::Reduce,
184        Self::Observe,
185    ];
186
187    /// Canonical PascalCase wire-format projection — matches the
188    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
189    /// `enum:` enumeration that the Process schema stamps on
190    /// `spec.classification.pointType`. Pinned by
191    /// `convergence_point_type_as_str_matches_serde` so a variant
192    /// rename can't drift between the typed surface, the CRD enum,
193    /// the YAML wire format AND any future operator-facing
194    /// diagnostic that composes `pointType={kind}` via Display
195    /// rather than a hard-coded literal that would silently rot.
196    /// Display + FromStr triad over `ALL` mirrors `DataClassification`
197    /// / `PoolPhase` / `MemberState` / `ReplacementPolicy` /
198    /// `ReturnPolicy` / `TeardownPolicy` / `ConditionKind` /
199    /// `ProcessPhase` / `ProcessSignal`.
200    pub const fn as_str(self) -> &'static str {
201        match self {
202            Self::Transform => "Transform",
203            Self::Fork => "Fork",
204            Self::Join => "Join",
205            Self::Gate => "Gate",
206            Self::Select => "Select",
207            Self::Broadcast => "Broadcast",
208            Self::Reduce => "Reduce",
209            Self::Observe => "Observe",
210        }
211    }
212
213    /// Cardinality of the input edge into this point — `One` for
214    /// `Transform | Fork | Broadcast | Observe` (single-source
215    /// projections), `Many` for `Join | Gate | Select | Reduce`
216    /// (multi-source convergent reductions). Closed-set match (not
217    /// `matches!`) so a future variant triggers the compiler's
218    /// exhaustiveness check at this site rather than silently
219    /// defaulting to `One`. Paired with [`Self::output_arity`] they
220    /// form the typed `(input, output)` projection that future
221    /// DAG composition validators (edge-cardinality checks: "you
222    /// can't connect a Fork's output to a Transform's input
223    /// without a Join in between") dispatch against — a single
224    /// projection per variant means a future `Demux` / `Mux` /
225    /// `Pipeline` point lands in exactly one cell of the
226    /// `Arity × Arity` topology table rather than rotting against
227    /// open-coded `== ConvergencePointType::Fork` checks.
228    pub const fn input_arity(self) -> Arity {
229        match self {
230            Self::Transform | Self::Fork | Self::Broadcast | Self::Observe => Arity::One,
231            Self::Join | Self::Gate | Self::Select | Self::Reduce => Arity::Many,
232        }
233    }
234
235    /// Cardinality of the output edge from this point — `Many` for
236    /// `Fork | Broadcast` (fan-out), `One` for everything else.
237    /// Closed-set match so a future variant triggers the compiler's
238    /// exhaustiveness check. See [`Self::input_arity`] for the
239    /// arity-pair contract + bucket definitions.
240    pub const fn output_arity(self) -> Arity {
241        match self {
242            Self::Fork | Self::Broadcast => Arity::Many,
243            Self::Transform
244            | Self::Join
245            | Self::Gate
246            | Self::Select
247            | Self::Reduce
248            | Self::Observe => Arity::One,
249        }
250    }
251
252    /// Does this point preserve the single-input single-output
253    /// shape? `(input, output) == (One, One)` — `Transform`
254    /// (identity-shaped reshape) and `Observe` (passthrough +
255    /// side-channel tap). Closed-set match so a future variant
256    /// triggers the compiler's exhaustiveness check. Paired with
257    /// `is_diffusive` and `is_convergent` they form the three-way
258    /// disjoint bucket carving sealed by
259    /// `convergence_point_type_buckets_cover_every_variant` AND
260    /// `convergence_point_type_arity_pair_agrees_with_bucket` —
261    /// the bridge that lets the bucket predicates and the arity
262    /// pair name the same topology partition from two angles.
263    pub const fn is_endomorphic(self) -> bool {
264        match self {
265            Self::Transform | Self::Observe => true,
266            Self::Fork
267            | Self::Join
268            | Self::Gate
269            | Self::Select
270            | Self::Broadcast
271            | Self::Reduce => false,
272        }
273    }
274
275    /// Does this point fan out — single input replicated/split
276    /// across many outputs? `(input, output) == (One, Many)` —
277    /// `Fork` and `Broadcast`. Closed-set match so a future variant
278    /// triggers the compiler's exhaustiveness check. See
279    /// `is_endomorphic` for the bucket-carving contract.
280    pub const fn is_diffusive(self) -> bool {
281        match self {
282            Self::Fork | Self::Broadcast => true,
283            Self::Transform
284            | Self::Join
285            | Self::Gate
286            | Self::Select
287            | Self::Reduce
288            | Self::Observe => false,
289        }
290    }
291
292    /// Does this point reduce — many inputs collapsed to one
293    /// output? `(input, output) == (Many, One)` — `Join`, `Gate`,
294    /// `Select`, `Reduce`. Closed-set match so a future variant
295    /// triggers the compiler's exhaustiveness check. See
296    /// `is_endomorphic` for the bucket-carving contract. The
297    /// impossible `(Many, Many)` topology bucket is pinned empty
298    /// by `convergence_point_type_arity_pair_agrees_with_bucket`
299    /// — a `(Many, Many)` point would mean "many independent
300    /// inputs replicated across many independent outputs", which
301    /// has no convergence semantics: every DAG-composition
302    /// validator would have to special-case it. A future variant
303    /// that wants `(Many, Many)` must first extend the bucket
304    /// carving deliberately.
305    pub const fn is_convergent(self) -> bool {
306        match self {
307            Self::Join | Self::Gate | Self::Select | Self::Reduce => true,
308            Self::Transform | Self::Fork | Self::Broadcast | Self::Observe => false,
309        }
310    }
311}
312
313// `impl FromStr for ConvergencePointType` +
314// `impl tatara_lisp::ClosedSet for ConvergencePointType` +
315// `impl std::fmt::Display for ConvergencePointType` +
316// `pub struct UnknownConvergencePointType(pub String)` are all generated
317// by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
318// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
319// enum declaration above. `label` delegates to the inherent
320// `ConvergencePointType::as_str` — the inherent name (PascalCase
321// `as_str`) stays the load-bearing wire-vocabulary projection that
322// matches the serde `rename_all = "PascalCase"` output AND the CRD
323// `enum:` enumeration the Process schema stamps on
324// `spec.classification.pointType` verbatim, while generic
325// `T: ClosedSet` consumers reach the STABLE workspace-wide name
326// (`label`). The `display` flag emits the
327// `f.write_str(self.as_str())` delegation block at the same
328// proc-macro site rather than a hand-rolled `fmt::Display` block per
329// implementor. The auto-derived carrier label "convergence point
330// type" matches the prior hand-rolled `#[error("unknown convergence
331// point type: {0}")]` annotation byte-for-byte. Symmetric to the
332// other five classification-axis closed-sets in this file
333// (`SubstrateType` / `HorizonKind` / `OptimizationDirection` /
334// `CalmClassification` / `DataClassification`) AND every other
335// `#[derive(DeriveClosedSet)]` implementor across the workspace
336// (`crate::pool::{ReplacementPolicy,MemberState,PoolPhase,ReturnPolicy}`,
337// `crate::export::{ArtifactKind,ReportFormat,ChannelKind,ExportTrigger}`,
338// `crate::allocation::{RequestorKind,AllocationPhase}`).
339
340/// Edge cardinality of a [`ConvergencePointType`]'s input or output.
341///
342/// Typed projection used by [`ConvergencePointType::input_arity`] and
343/// [`ConvergencePointType::output_arity`] so DAG composition validators
344/// reach for a closed-set enum rather than re-deriving the in/out
345/// cardinality from variant names. `Many` is the "≥1, could be N"
346/// cardinality — it carries no upper bound because the convergence
347/// point's variant tag is already the structural identity; the
348/// number itself is a runtime property of the DAG, not the typescape.
349#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
350#[closed_set(via = "as_str", display, generate_unknown)]
351pub enum Arity {
352    /// Single edge — exactly one input or one output.
353    One,
354    /// Multiple edges — any number ≥ 1.
355    Many,
356}
357
358impl Arity {
359    /// The closed set of arities — single source of truth that
360    /// drives `as_str` / Display AND the `is_one` predicate. Adding
361    /// a third variant (e.g. `Arity::Zero` for sinks) lands at one
362    /// `ALL` entry + one `as_str` arm + one predicate arm —
363    /// exhaustively checked by the compiler.
364    pub const ALL: [Self; 2] = [Self::One, Self::Many];
365
366    /// Canonical projection — `"One" | "Many"`. Pinned by
367    /// `arity_display_matches_as_str` so a future Display impl
368    /// can't drift from the canonical string.
369    pub const fn as_str(self) -> &'static str {
370        match self {
371            Self::One => "One",
372            Self::Many => "Many",
373        }
374    }
375
376    /// Is this the single-edge cardinality? Closed-set match (not
377    /// `matches!`) so a future variant triggers the compiler's
378    /// exhaustiveness check.
379    pub const fn is_one(self) -> bool {
380        match self {
381            Self::One => true,
382            Self::Many => false,
383        }
384    }
385}
386
387// `impl fmt::Display for Arity` + `impl std::str::FromStr for Arity` +
388// `impl tatara_lisp::ClosedSet for Arity` + `pub struct UnknownArity(pub
389// String)` are all generated by
390// `#[derive(tatara_closed_set::DeriveClosedSet)]` +
391// `#[closed_set(via = "as_str", display, generate_unknown)]` on the enum
392// declaration above. The inherent `as_str` projection stays load-bearing
393// — the canonical `"One" | "Many"` string every DAG composition
394// validator reads; `via = "as_str"` binds `ClosedSet::label` to the same
395// projection so the substrate-wide `assert_display_matches_label` /
396// `assert_closed_set_well_formed` primitives dispatch through the same
397// byte-identical shape every other closed-set implementor across the
398// crate publishes. Aligns `Arity` with the substrate-wide
399// `#[derive(DeriveClosedSet)]` idiom that every other closed-set enum on
400// this classification axis (`ConvergencePointType`, `SubstrateType`,
401// `HorizonKind`, `OptimizationDirection`, `CalmClassification`,
402// `DataClassification`) already carries — the last hand-rolled
403// `impl fmt::Display` on the axis is closed at ONE substrate site.
404
405/// Operational substrate.
406///
407/// Closed-set sibling on the classification axis algebra; the `ALL` /
408/// `as_str` / Display / `FromStr` triad mirrors
409/// [`ConvergencePointType::ALL`], [`DataClassification::ALL`],
410/// [`crate::pool::PoolPhase::ALL`], [`crate::pool::MemberState::ALL`],
411/// [`crate::pool::ReplacementPolicy::ALL`],
412/// [`crate::pool::ReturnPolicy::ALL`],
413/// [`crate::boundary::ConditionKind::ALL`],
414/// [`crate::lifetime::TeardownPolicy::ALL`],
415/// [`crate::lifetime::LifetimeKind::ALL`],
416/// [`crate::intent::IntentKind::ALL`],
417/// [`crate::phase::ProcessPhase::ALL`],
418/// [`crate::signal::ProcessSignal::ALL`]. The
419/// `is_resource` / `is_policy` / `is_telemetry` predicate triple
420/// carves the eight variants into three structurally-disjoint
421/// substrate planes — resource (you allocate from it), policy (it
422/// gates access for other workloads), telemetry (it observes other
423/// workloads) — so future compliance-baseline selectors that
424/// dispatch on a substrate's plane (resource budgets only apply to
425/// resource substrates; policy substrates inherit baselines from
426/// what they govern; telemetry substrates inherit baselines from
427/// what they observe) read a typed projection rather than
428/// re-deriving from variant names.
429#[derive(
430    Clone,
431    Copy,
432    Debug,
433    PartialEq,
434    Eq,
435    Hash,
436    PartialOrd,
437    Ord,
438    Serialize,
439    Deserialize,
440    JsonSchema,
441    tatara_closed_set::DeriveClosedSet,
442)]
443#[serde(rename_all = "PascalCase")]
444#[closed_set(via = "as_str", generate_unknown, display)]
445pub enum SubstrateType {
446    Financial,
447    Compute,
448    Network,
449    Storage,
450    Security,
451    Identity,
452    Observability,
453    Regulatory,
454}
455
456impl SubstrateType {
457    /// The closed set of substrates — single source of truth that
458    /// drives the `as_str` / Display / `FromStr` triad AND the
459    /// `is_resource` / `is_policy` / `is_telemetry` predicate triple.
460    /// Adding a ninth variant lands at one `ALL` entry + one
461    /// `as_str` arm + one arm per predicate — exhaustively checked
462    /// by the compiler (the `[Self; 8]` array literal forces the
463    /// arity) AND by the per-variant plane-bucket contract test (a
464    /// new variant must declare its own plane or any future
465    /// compliance-baseline selector that dispatches on
466    /// `(is_resource, is_policy, is_telemetry)` will silently
467    /// mis-classify it). Closes the load-bearing classification-axis
468    /// enum that
469    /// `tatara_core::domain::compliance_binding::PointSelector::BySubstrate`
470    /// already dispatches against and that every `Process`'s
471    /// `Classification.substrate` reads as the operational
472    /// substrate the convergence point lives on.
473    pub const ALL: [Self; 8] = [
474        Self::Financial,
475        Self::Compute,
476        Self::Network,
477        Self::Storage,
478        Self::Security,
479        Self::Identity,
480        Self::Observability,
481        Self::Regulatory,
482    ];
483
484    /// Canonical PascalCase wire-format projection — matches the
485    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
486    /// `enum:` enumeration that the Process schema stamps on
487    /// `spec.classification.substrate`. Pinned by
488    /// `substrate_type_as_str_matches_serde` so a variant rename
489    /// can't drift between the typed surface, the CRD enum, the YAML
490    /// wire format AND any future operator-facing diagnostic that
491    /// composes `substrate={kind}` via Display rather than a
492    /// hard-coded literal that would silently rot. Display + FromStr
493    /// triad over `ALL` mirrors `ConvergencePointType` /
494    /// `DataClassification` / `PoolPhase` / `MemberState` /
495    /// `ReplacementPolicy` / `ReturnPolicy` / `TeardownPolicy` /
496    /// `ConditionKind` / `ProcessPhase` / `ProcessSignal`.
497    pub const fn as_str(self) -> &'static str {
498        match self {
499            Self::Financial => "Financial",
500            Self::Compute => "Compute",
501            Self::Network => "Network",
502            Self::Storage => "Storage",
503            Self::Security => "Security",
504            Self::Identity => "Identity",
505            Self::Observability => "Observability",
506            Self::Regulatory => "Regulatory",
507        }
508    }
509
510    /// Is this a resource substrate — one you allocate budgets from
511    /// to run workloads? `Financial | Compute | Network | Storage`.
512    /// Closed-set match (not `matches!`) so a future variant
513    /// triggers the compiler's exhaustiveness check at this site
514    /// rather than silently defaulting to `false`. Paired with
515    /// `is_policy` and `is_telemetry` they form the three-way
516    /// disjoint plane carving sealed by
517    /// `substrate_type_buckets_cover_every_variant` — the bridge
518    /// that lets future compliance-baseline selectors dispatch on
519    /// plane without re-deriving from variant names.
520    pub const fn is_resource(self) -> bool {
521        match self {
522            Self::Financial | Self::Compute | Self::Network | Self::Storage => true,
523            Self::Security | Self::Identity | Self::Observability | Self::Regulatory => false,
524        }
525    }
526
527    /// Is this a policy substrate — one that gates access or
528    /// compliance for other workloads rather than carrying their
529    /// payload? `Security | Identity | Regulatory`. Closed-set match
530    /// so a future variant triggers the compiler's exhaustiveness
531    /// check. See `is_resource` for the bucket-carving contract.
532    pub const fn is_policy(self) -> bool {
533        match self {
534            Self::Security | Self::Identity | Self::Regulatory => true,
535            Self::Financial
536            | Self::Compute
537            | Self::Network
538            | Self::Storage
539            | Self::Observability => false,
540        }
541    }
542
543    /// Is this a telemetry substrate — one that passively observes
544    /// other workloads (metrics, logs, traces) without carrying
545    /// their payload or gating their access? `Observability` only.
546    /// Closed-set match so a future variant triggers the compiler's
547    /// exhaustiveness check. See `is_resource` for the
548    /// bucket-carving contract. A telemetry substrate's compliance
549    /// baseline is inherited from what it observes — the singleton
550    /// bucket is intentional, not a placeholder.
551    pub const fn is_telemetry(self) -> bool {
552        match self {
553            Self::Observability => true,
554            Self::Financial
555            | Self::Compute
556            | Self::Network
557            | Self::Storage
558            | Self::Security
559            | Self::Identity
560            | Self::Regulatory => false,
561        }
562    }
563}
564
565// `impl FromStr for SubstrateType` +
566// `impl tatara_lisp::ClosedSet for SubstrateType` +
567// `impl std::fmt::Display for SubstrateType` +
568// `pub struct UnknownSubstrateType(pub String)` are all generated by
569// `#[derive(tatara_closed_set::DeriveClosedSet)]` +
570// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
571// enum declaration above. The auto-derived carrier label "substrate
572// type" matches the prior hand-rolled `#[error("unknown substrate
573// type: {0}")]` annotation byte-for-byte. See the retrofit comment
574// block on [`ConvergencePointType`] for the canonical narrative.
575
576/// How long the point runs. Flattened struct-of-optionals so the OpenAPI
577/// schema carries a single `kind` discriminator without per-variant merge.
578#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
579#[serde(rename_all = "camelCase")]
580pub struct Horizon {
581    #[serde(default)]
582    pub kind: HorizonKind,
583    /// Metric being optimized (Asymptotic only).
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub metric: Option<String>,
586    /// Whether to minimize or maximize the metric (Asymptotic only).
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub direction: Option<OptimizationDirection>,
589    /// Rate threshold considered healthy (Asymptotic only).
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub healthy_rate_threshold: Option<f64>,
592}
593
594/// The shape of a convergence horizon's lifetime — does the point
595/// run toward a fixed point and terminate, or run in perpetuity with
596/// a rate signal?
597///
598/// Closed-set sibling on the classification axis algebra; the `ALL` /
599/// `as_str` / Display / `FromStr` triad mirrors
600/// [`ConvergencePointType::ALL`], [`SubstrateType::ALL`],
601/// [`DataClassification::ALL`], [`CalmClassification::ALL`],
602/// [`OptimizationDirection::ALL`], [`crate::pool::PoolPhase::ALL`],
603/// [`crate::pool::MemberState::ALL`],
604/// [`crate::pool::ReplacementPolicy::ALL`],
605/// [`crate::pool::ReturnPolicy::ALL`],
606/// [`crate::boundary::ConditionKind::ALL`],
607/// [`crate::lifetime::TeardownPolicy::ALL`],
608/// [`crate::lifetime::LifetimeKind::ALL`],
609/// [`crate::intent::IntentKind::ALL`],
610/// [`crate::phase::ProcessPhase::ALL`],
611/// [`crate::signal::ProcessSignal::ALL`]. The [`Self::terminates`]
612/// predicate is the load-bearing horizon-shape primitive — schedulers
613/// asking "will this Process ever reach `Reaped` via natural
614/// termination?" read it as the typed image of the lattice ordering
615/// (`Bounded ≤ Asymptotic` because the bounded horizon strictly
616/// refines the asymptotic one by also terminating) rather than
617/// re-deriving from the variant name. The
618/// [`Self::requires_metric_axes`] predicate is the typed validity
619/// witness for the [`Horizon`] struct's three `Option<…>` fields
620/// (`metric`, `direction`, `healthy_rate_threshold`) — they're
621/// `Some(_)` iff the kind requires them, so the implicit invariant
622/// the optionality encodes becomes a checkable per-kind predicate
623/// instead of operator folklore.
624#[derive(
625    Clone,
626    Copy,
627    Debug,
628    PartialEq,
629    Eq,
630    Hash,
631    Serialize,
632    Deserialize,
633    JsonSchema,
634    Default,
635    tatara_closed_set::DeriveClosedSet,
636)]
637#[serde(rename_all = "PascalCase")]
638#[closed_set(via = "as_str", generate_unknown, display)]
639pub enum HorizonKind {
640    /// Has a fixed point — distance reaches 0 and terminates.
641    #[default]
642    Bounded,
643    /// Runs in perpetuity — rate is the health signal, not distance.
644    Asymptotic,
645}
646
647impl HorizonKind {
648    /// The closed set of horizon kinds — single source of truth that
649    /// drives the `as_str` / Display / `FromStr` triad AND the
650    /// `terminates` predicate AND the `requires_metric_axes` shape-
651    /// validity witness. Adding a third variant (e.g. a `Periodic`
652    /// sentinel for "terminates on each window boundary then
653    /// re-arms", which neither perpetually-running nor singularly-
654    /// terminating names) lands at one `ALL` entry + one `as_str`
655    /// arm + one `terminates` arm + one `requires_metric_axes` arm —
656    /// exhaustively checked by the compiler (the `[Self; 2]` array
657    /// literal forces the arity) AND by the per-variant truth-table
658    /// tests (a new variant must declare its own termination AND
659    /// metric-axes requirement, or every scheduler / horizon-shape
660    /// validator will silently bucket it). Closes the load-bearing
661    /// classification sub-axis that the `Horizon.kind` field threads
662    /// through every `Classification.horizon` field on every
663    /// Process — the last open sibling on the classification axis
664    /// algebra after `OptimizationDirection` (980a318),
665    /// `CalmClassification` (da3430c), `SubstrateType` (b9d7b3b),
666    /// `ConvergencePointType` (7941527), `Arity`, and
667    /// `DataClassification` (81bffa0).
668    pub const ALL: [Self; 2] = [Self::Bounded, Self::Asymptotic];
669
670    /// Canonical PascalCase wire-format projection — matches the
671    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
672    /// `enum:` enumeration the Process schema stamps on
673    /// `spec.classification.horizon.kind`. Pinned by
674    /// `horizon_kind_as_str_matches_serde` so a variant rename
675    /// can't drift between the typed surface, the CRD enum, the
676    /// YAML wire format AND any future operator-facing diagnostic
677    /// composing `horizon.kind={kind}` via Display rather than a
678    /// hard-coded literal. Display + FromStr triad over `ALL`
679    /// mirrors every sibling closed-set enum in this crate.
680    pub const fn as_str(self) -> &'static str {
681        match self {
682            Self::Bounded => "Bounded",
683            Self::Asymptotic => "Asymptotic",
684        }
685    }
686
687    /// LOAD-BEARING HORIZON-SHAPE PRIMITIVE: does this kind terminate
688    /// naturally — i.e. does it have a fixed point that
689    /// `ConvergenceDistance` can reach? Closed-set match (not
690    /// `matches!`) so a future variant triggers the compiler's
691    /// exhaustiveness check rather than silently defaulting to
692    /// `false` (which would silently mis-route a terminating
693    /// variant through the asymptotic rate-window evaluator) or
694    /// `true` (which would silently invent a fixed point for a
695    /// perpetual variant). `Bounded ⇒ true`, `Asymptotic ⇒ false`
696    /// is the typed image of the documented lattice ordering
697    /// `Bounded ≤ Asymptotic` — the bounded horizon strictly refines
698    /// the asymptotic one BY ALSO TERMINATING. Future schedulers
699    /// asking "will this Process reach `Reaped` via natural
700    /// termination?" read this predicate, and the tatara-lattice
701    /// `Lattice for Horizon` impl (which currently dispatches on
702    /// `self.kind == HorizonKind::Bounded` at three sites) can be
703    /// recast in a future run to read `self.kind.terminates()` so
704    /// the lattice basis is the typed primitive rather than a
705    /// variant-name comparison.
706    pub const fn terminates(self) -> bool {
707        match self {
708            Self::Bounded => true,
709            Self::Asymptotic => false,
710        }
711    }
712
713    /// LOAD-BEARING SHAPE-VALIDITY WITNESS: does this kind require
714    /// the three asymptotic-only [`Horizon`] axes (`metric`,
715    /// `direction`, `healthy_rate_threshold`) to be `Some(_)`?
716    /// Closed-set match (not `matches!`) so a future variant
717    /// triggers the compiler's exhaustiveness check rather than
718    /// silently defaulting to `false` (which would silently let an
719    /// asymptotic-shaped variant ship with missing metric axes and
720    /// trip the rate-window evaluator at runtime). `Bounded ⇒
721    /// false`, `Asymptotic ⇒ true` is the typed image of the
722    /// optionality the [`Horizon`] struct encodes via three
723    /// `Option<…>` fields — the implicit invariant ("Asymptotic
724    /// only" in the field docs) is now a checkable per-kind
725    /// predicate. Future horizon-shape validators (CRD admission,
726    /// `tatara-check` form linter, Lisp authoring-time predicate)
727    /// read this rather than re-deriving from variant names.
728    /// Pinned as the antisymmetric partner of [`Self::terminates`]
729    /// — exactly one of `(terminates, requires_metric_axes)` is
730    /// true per variant — by
731    /// `horizon_kind_terminate_xor_requires_metric_axes`.
732    pub const fn requires_metric_axes(self) -> bool {
733        match self {
734            Self::Bounded => false,
735            Self::Asymptotic => true,
736        }
737    }
738}
739
740// `impl FromStr for HorizonKind` +
741// `impl tatara_lisp::ClosedSet for HorizonKind` +
742// `impl std::fmt::Display for HorizonKind` +
743// `pub struct UnknownHorizonKind(pub String)` are all generated by
744// `#[derive(tatara_closed_set::DeriveClosedSet)]` +
745// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
746// enum declaration above. The auto-derived carrier label "horizon
747// kind" matches the prior hand-rolled `#[error("unknown horizon
748// kind: {0}")]` annotation byte-for-byte. See the retrofit comment
749// block on [`ConvergencePointType`] for the canonical narrative.
750
751impl Horizon {
752    pub fn bounded() -> Self {
753        Self::default()
754    }
755
756    pub fn asymptotic(
757        metric: impl Into<String>,
758        direction: OptimizationDirection,
759        threshold: f64,
760    ) -> Self {
761        Self {
762            kind: HorizonKind::Asymptotic,
763            metric: Some(metric.into()),
764            direction: Some(direction),
765            healthy_rate_threshold: Some(threshold),
766        }
767    }
768}
769
770/// Direction of asymptotic optimization — does the metric trend
771/// downward (cost / latency / error rate) or upward
772/// (throughput / coverage / revenue)?
773///
774/// Closed-set sibling on the classification axis algebra; the `ALL` /
775/// `as_str` / Display / `FromStr` triad mirrors
776/// [`ConvergencePointType::ALL`], [`SubstrateType::ALL`],
777/// [`DataClassification::ALL`], [`CalmClassification::ALL`],
778/// [`crate::pool::PoolPhase::ALL`], [`crate::pool::MemberState::ALL`],
779/// [`crate::pool::ReplacementPolicy::ALL`],
780/// [`crate::pool::ReturnPolicy::ALL`],
781/// [`crate::boundary::ConditionKind::ALL`],
782/// [`crate::lifetime::TeardownPolicy::ALL`],
783/// [`crate::lifetime::LifetimeKind::ALL`],
784/// [`crate::intent::IntentKind::ALL`],
785/// [`crate::phase::ProcessPhase::ALL`],
786/// [`crate::signal::ProcessSignal::ALL`]. The
787/// [`Self::is_improvement`] predicate is the load-bearing
788/// optimization primitive — `Asymptotic` horizons read it as the
789/// typed image of "did this metric sample improve over the last
790/// one?" rather than re-deriving `<` vs `>` from the variant name
791/// at every consumer site (rate-window evaluators, breathe-band
792/// regression detectors, asymptotic-health probes).
793#[derive(
794    Clone,
795    Copy,
796    Debug,
797    PartialEq,
798    Eq,
799    Hash,
800    Serialize,
801    Deserialize,
802    JsonSchema,
803    Default,
804    tatara_closed_set::DeriveClosedSet,
805)]
806#[serde(rename_all = "PascalCase")]
807#[closed_set(via = "as_str", generate_unknown, display)]
808pub enum OptimizationDirection {
809    /// Cost / latency / error rate — lower is better. The default for
810    /// an under-specified `Asymptotic` horizon so an unannotated
811    /// metric can't silently flip the rate-window evaluator's polarity
812    /// (a future `Maximize`-default-via-rename would silently invert
813    /// every existing alert that treats decreasing rate as healthy).
814    #[default]
815    Minimize,
816    /// Throughput / coverage / revenue — higher is better.
817    Maximize,
818}
819
820impl OptimizationDirection {
821    /// The closed set of optimization directions — single source of
822    /// truth that drives the `as_str` / Display / `FromStr` triad AND
823    /// the `prefers_lower` partition AND the `is_improvement`
824    /// load-bearing primitive AND both `From` bridge arms. Adding a
825    /// third variant (e.g. a `Stabilize` sentinel for "drive toward
826    /// a target value", which neither minimization nor maximization
827    /// names) lands at one `ALL` entry + one `as_str` arm + one
828    /// `prefers_lower` arm + one `is_improvement` arm + two bridge
829    /// arms — exhaustively checked by the compiler (the `[Self; 2]`
830    /// array literal forces the arity) AND by the per-variant
831    /// truth-table tests (a new variant must declare its own
832    /// improvement semantics, or every asymptotic-health probe will
833    /// silently bucket it). Closes the load-bearing classification
834    /// sub-axis that the `Horizon.direction` field threads through
835    /// every `Asymptotic` Process.
836    pub const ALL: [Self; 2] = [Self::Minimize, Self::Maximize];
837
838    /// Canonical PascalCase wire-format projection — matches the serde
839    /// `rename_all = "PascalCase"` output verbatim AND the CRD `enum:`
840    /// enumeration the Process schema stamps on
841    /// `spec.classification.horizon.direction`. Pinned by
842    /// `optimization_direction_as_str_matches_serde` so a variant
843    /// rename can't drift between the typed surface, the CRD enum, the
844    /// YAML wire format AND any future operator-facing diagnostic
845    /// composed as `direction={kind}` via Display rather than a
846    /// hard-coded literal. Display + `FromStr` triad over `ALL`
847    /// mirrors every sibling closed-set enum in this crate.
848    pub const fn as_str(self) -> &'static str {
849        match self {
850            Self::Minimize => "Minimize",
851            Self::Maximize => "Maximize",
852        }
853    }
854
855    /// Does this direction prefer numerically lower values?
856    /// Closed-set match (not `matches!`) so a future variant triggers
857    /// the compiler's exhaustiveness check at this site rather than
858    /// silently defaulting to `false` (which would mis-bucket a
859    /// `Stabilize`-style variant onto the maximization path). The
860    /// boolean partition is the algebraic shape of an optimization
861    /// direction: `Minimize ⇒ true`, `Maximize ⇒ false`. Mirrors
862    /// [`CalmClassification::requires_coordination`] — a two-variant
863    /// truth-table that any future dispatch on a per-direction policy
864    /// (rate-window evaluator polarity, breathe-band regression
865    /// detector sign, asymptotic-health threshold direction) reads
866    /// once rather than re-deriving from the variant name.
867    pub const fn prefers_lower(self) -> bool {
868        match self {
869            Self::Minimize => true,
870            Self::Maximize => false,
871        }
872    }
873
874    /// LOAD-BEARING OPTIMIZATION PRIMITIVE: under this direction, is
875    /// `after` strictly better than `before`? Closed-set match so a
876    /// future variant triggers the compiler's exhaustiveness check
877    /// rather than silently defaulting to `false` (which would
878    /// silently mark every sample as a regression). For `Minimize`,
879    /// improvement means `after < before`; for `Maximize`, `after >
880    /// before`. Strict inequality so a no-op sample (equal values) is
881    /// NOT counted as improvement — pinned by
882    /// `optimization_direction_no_op_is_not_improvement`, which
883    /// guarantees a flatlined rate-window evaluator doesn't silently
884    /// keep claiming "still improving" forever and skipping the
885    /// healthy-rate-threshold gate. NaN on either operand short-
886    /// circuits to `false` (no improvement claim from indeterminate
887    /// data) via the standard `PartialOrd` behavior — pinned by
888    /// `optimization_direction_nan_is_not_improvement`. The
889    /// asymmetry contract (`is_improvement(a, b)` xor
890    /// `is_improvement(b, a)` for distinct finite samples) is pinned
891    /// by `optimization_direction_is_improvement_is_antisymmetric`,
892    /// the algebraic shape that every asymptotic-health rate-window
893    /// evaluator depends on to avoid double-counting an improvement
894    /// as a regression on the reverse traversal.
895    pub fn is_improvement(self, before: f64, after: f64) -> bool {
896        match self {
897            Self::Minimize => after < before,
898            Self::Maximize => after > before,
899        }
900    }
901}
902
903// `impl FromStr for OptimizationDirection` +
904// `impl tatara_lisp::ClosedSet for OptimizationDirection` +
905// `impl std::fmt::Display for OptimizationDirection` +
906// `pub struct UnknownOptimizationDirection(pub String)` are all
907// generated by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
908// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
909// enum declaration above. The auto-derived carrier label
910// "optimization direction" matches the prior hand-rolled
911// `#[error("unknown optimization direction: {0}")]` annotation
912// byte-for-byte. See the retrofit comment block on
913// [`ConvergencePointType`] for the canonical narrative.
914
915/// CALM theorem classification — determines whether coordination is required.
916///
917/// Closed-set sibling on the classification axis algebra; the `ALL` /
918/// `as_str` / Display / `FromStr` triad mirrors
919/// [`ConvergencePointType::ALL`], [`SubstrateType::ALL`],
920/// [`DataClassification::ALL`], [`crate::pool::PoolPhase::ALL`],
921/// [`crate::pool::MemberState::ALL`], [`crate::pool::ReplacementPolicy::ALL`],
922/// [`crate::pool::ReturnPolicy::ALL`],
923/// [`crate::boundary::ConditionKind::ALL`],
924/// [`crate::lifetime::TeardownPolicy::ALL`],
925/// [`crate::lifetime::LifetimeKind::ALL`],
926/// [`crate::intent::IntentKind::ALL`],
927/// [`crate::phase::ProcessPhase::ALL`],
928/// [`crate::signal::ProcessSignal::ALL`]. The
929/// [`Self::requires_coordination`] predicate is the CALM theorem
930/// keystone — Hellerstein's "Consistency As Logical Monotonicity"
931/// states that a program can be distributed without coordination iff
932/// it computes a monotone function, so `Monotone ⇒ no coordination`
933/// and `NonMonotone ⇒ requires coordination` is a typed image of the
934/// theorem itself rather than a runtime convention. Future reconciler
935/// dispatch on `calm.requires_coordination()` (Raft for non-monotone
936/// writes; gossip for monotone ones) reads this projection rather
937/// than re-deriving from variant names.
938#[derive(
939    Clone,
940    Copy,
941    Debug,
942    PartialEq,
943    Eq,
944    Hash,
945    Serialize,
946    Deserialize,
947    JsonSchema,
948    Default,
949    tatara_closed_set::DeriveClosedSet,
950)]
951#[serde(rename_all = "PascalCase")]
952#[closed_set(via = "as_str", generate_unknown, display)]
953pub enum CalmClassification {
954    /// Can be distributed without coordination (CALM ⇒ the program
955    /// computes a monotone function).
956    #[default]
957    Monotone,
958    /// Requires coordination (CALM ⇒ the program is not monotone).
959    NonMonotone,
960}
961
962impl CalmClassification {
963    /// The closed set of CALM classifications — single source of truth
964    /// that drives the `as_str` / Display / `FromStr` triad AND the
965    /// `requires_coordination` predicate. Adding a third variant
966    /// (e.g. a `ConditionallyMonotone` sentinel for ops that are
967    /// monotone under a witness, like CRDT joins under a fixed
968    /// schema) lands at one `ALL` entry + one `as_str` arm + one
969    /// predicate arm + one bridge-pair arm — exhaustively checked by
970    /// the compiler (the `[Self; 2]` array literal forces the arity)
971    /// AND by the per-variant predicate truth-table test (a new
972    /// variant must declare its own coordination requirement or any
973    /// future reconciler-side dispatch will silently bucket it).
974    /// Closes the load-bearing classification-axis enum that the
975    /// `Classification.calm` field exposes to every Process and that
976    /// [`tatara_lattice`]'s boolean-lattice `Lattice for
977    /// CalmClassification` impl reads via [`Self::requires_coordination`]
978    /// as the lattice's `top()` predicate.
979    pub const ALL: [Self; 2] = [Self::Monotone, Self::NonMonotone];
980
981    /// Canonical PascalCase wire-format projection — matches the
982    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
983    /// `enum:` enumeration that the Process schema stamps on
984    /// `spec.classification.calm`. Pinned by
985    /// `calm_classification_as_str_matches_serde` so a variant rename
986    /// can't drift between the typed surface, the CRD enum, the YAML
987    /// wire format AND any future operator-facing diagnostic that
988    /// composes `calm={kind}` via Display rather than a hard-coded
989    /// literal that would silently rot. Display + FromStr triad over
990    /// `ALL` mirrors every sibling closed-set enum in this crate.
991    pub const fn as_str(self) -> &'static str {
992        match self {
993            Self::Monotone => "Monotone",
994            Self::NonMonotone => "NonMonotone",
995        }
996    }
997
998    /// CALM-THEOREM KEYSTONE: does this classification require
999    /// distributed coordination? Closed-set match (not `matches!`) so
1000    /// a future variant triggers the compiler's exhaustiveness check
1001    /// at this site rather than silently defaulting to `false` and
1002    /// shipping a non-monotone operation onto the no-coordination
1003    /// path. The theorem (Hellerstein 2010) states that a program can
1004    /// be distributed without coordination iff it computes a monotone
1005    /// function — `Monotone ⇒ false` and `NonMonotone ⇒ true` is the
1006    /// typed image of that biconditional. Consumers (future reconciler
1007    /// dispatch between Raft writes and gossip propagation; current
1008    /// `tatara_lattice` boolean-lattice ordering where `Monotone ≤
1009    /// NonMonotone`) read this predicate rather than re-deriving from
1010    /// variant names.
1011    pub const fn requires_coordination(self) -> bool {
1012        match self {
1013            Self::Monotone => false,
1014            Self::NonMonotone => true,
1015        }
1016    }
1017}
1018
1019// `impl FromStr for CalmClassification` +
1020// `impl tatara_lisp::ClosedSet for CalmClassification` +
1021// `impl std::fmt::Display for CalmClassification` +
1022// `pub struct UnknownCalmClassification(pub String)` are all generated
1023// by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
1024// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
1025// enum declaration above. The auto-derived carrier label
1026// "calm classification" matches the prior hand-rolled
1027// `#[error("unknown calm classification: {0}")]` annotation
1028// byte-for-byte. See the retrofit comment block on
1029// [`ConvergencePointType`] for the canonical narrative.
1030
1031/// Data sensitivity, drives compliance baseline selection.
1032///
1033/// Sibling closed-set on the classification axis algebra; the `ALL` /
1034/// `as_str` / Display / `FromStr` triad mirrors
1035/// [`crate::pool::PoolPhase::ALL`], [`crate::pool::MemberState::ALL`],
1036/// [`crate::pool::ReplacementPolicy::ALL`],
1037/// [`crate::pool::ReturnPolicy::ALL`],
1038/// [`crate::boundary::ConditionKind::ALL`],
1039/// [`crate::lifetime::TeardownPolicy::ALL`],
1040/// [`crate::lifetime::LifetimeKind::ALL`],
1041/// [`crate::intent::IntentKind::ALL`],
1042/// [`crate::phase::ProcessPhase::ALL`],
1043/// [`crate::signal::ProcessSignal::ALL`].
1044#[derive(
1045    Clone,
1046    Copy,
1047    Debug,
1048    PartialEq,
1049    Eq,
1050    PartialOrd,
1051    Ord,
1052    Hash,
1053    Serialize,
1054    Deserialize,
1055    JsonSchema,
1056    Default,
1057    tatara_closed_set::DeriveClosedSet,
1058)]
1059#[serde(rename_all = "PascalCase")]
1060#[closed_set(via = "as_str", generate_unknown, display)]
1061pub enum DataClassification {
1062    Public,
1063    #[default]
1064    Internal,
1065    Confidential,
1066    Pii,
1067    Phi,
1068    Pci,
1069}
1070
1071impl DataClassification {
1072    /// The closed set of data classifications — single source of truth
1073    /// that drives the `as_str` / Display / `FromStr` triad AND the
1074    /// `sensitivity_rank` total-order projection AND the
1075    /// `is_restricted` / `is_regulated` predicate pair. Adding a
1076    /// seventh variant lands at one `ALL` entry + one `as_str` arm +
1077    /// one `sensitivity_rank` arm + one arm per predicate —
1078    /// exhaustively checked by the compiler (the `[Self; 6]` array
1079    /// literal forces the arity) AND by the per-variant truth-table
1080    /// contract test (a new variant must declare its own
1081    /// `(is_restricted, is_regulated)` bucket or any future
1082    /// compliance-baseline auto-selector that dispatches on the pair
1083    /// will silently bucket it into the wrong sensitivity column).
1084    /// This closes the sixth classification-axis enum and the closure
1085    /// is consumed by [`tatara_lattice`]'s total-order `Lattice` impl
1086    /// via [`Self::sensitivity_rank`] so the lattice ordering no
1087    /// longer rides silently on declaration order.
1088    pub const ALL: [Self; 6] = [
1089        Self::Public,
1090        Self::Internal,
1091        Self::Confidential,
1092        Self::Pii,
1093        Self::Phi,
1094        Self::Pci,
1095    ];
1096
1097    /// Canonical PascalCase wire-format projection — matches the
1098    /// serde `rename_all = "PascalCase"` output verbatim AND the CRD
1099    /// `enum:` enumeration that the Process schema stamps on
1100    /// `spec.classification.dataClassification`. Pinned by
1101    /// `data_classification_as_str_matches_serde` so a variant rename
1102    /// can't drift between the typed surface, the CRD enum, the YAML
1103    /// wire format AND any future operator-facing diagnostic that
1104    /// composes `dataClassification={class}` via Display rather than
1105    /// a hard-coded literal that would silently rot. Display +
1106    /// FromStr triad over `ALL` mirrors `PoolPhase` / `MemberState` /
1107    /// `ReplacementPolicy` / `ReturnPolicy` / `TeardownPolicy` /
1108    /// `ConditionKind` / `ProcessPhase` / `ProcessSignal`.
1109    pub const fn as_str(self) -> &'static str {
1110        match self {
1111            Self::Public => "Public",
1112            Self::Internal => "Internal",
1113            Self::Confidential => "Confidential",
1114            Self::Pii => "Pii",
1115            Self::Phi => "Phi",
1116            Self::Pci => "Pci",
1117        }
1118    }
1119
1120    /// Explicit total-order rank, sealed at one site so the lattice
1121    /// ordering stops riding silently on declaration order. Pre-lift
1122    /// the tatara-lattice `Lattice for DataClassification` impl
1123    /// compared variants via `(*self as u8) <= (*other as u8)`, so a
1124    /// future variant inserted in the middle of the enum (say a
1125    /// `Restricted` between `Internal` and `Confidential`) would
1126    /// silently shift every subsequent variant's `as u8` value AND
1127    /// the lattice's `leq` relation — no compile error, no test
1128    /// failure, but every compliance-baseline comparison
1129    /// downstream would have moved by one slot. Post-lift the rank
1130    /// is declared explicitly per variant; an insertion forces the
1131    /// author to pick a rank deliberately (and
1132    /// `data_classification_rank_is_strictly_monotone_over_all`
1133    /// pins the existing six variants at 0..6 so the lattice's
1134    /// total order remains the documented
1135    /// `Public < Internal < Confidential < Pii < Phi < Pci`).
1136    pub const fn sensitivity_rank(self) -> u8 {
1137        match self {
1138            Self::Public => 0,
1139            Self::Internal => 1,
1140            Self::Confidential => 2,
1141            Self::Pii => 3,
1142            Self::Phi => 4,
1143            Self::Pci => 5,
1144        }
1145    }
1146
1147    /// Is this classification subject to external regulatory regime
1148    /// (HIPAA / PCI-DSS / GDPR-style data-subject controls)?
1149    /// Closed-set match (not `matches!`) so a future variant triggers
1150    /// the compiler's exhaustiveness check at this site rather than
1151    /// silently defaulting to `false`. Paired with `is_restricted`
1152    /// they form the two-axis projection that future
1153    /// compliance-baseline auto-selectors dispatch against —
1154    /// `(false, false)` ⇒ freely distributable (`Public`);
1155    /// `(false, true)` ⇒ access-controlled but not regulated
1156    /// (`Internal | Confidential`); `(true, true)` ⇒ regulated data
1157    /// that implies access control (`Pii | Phi | Pci`). The
1158    /// impossible bucket `(true, false)` — regulated data without
1159    /// access control — is pinned empty by
1160    /// `data_classification_regulated_implies_restricted`.
1161    pub const fn is_regulated(self) -> bool {
1162        match self {
1163            Self::Pii | Self::Phi | Self::Pci => true,
1164            Self::Public | Self::Internal | Self::Confidential => false,
1165        }
1166    }
1167
1168    /// Does this classification require access controls beyond
1169    /// freely-distributable? Closed-set match so a future variant
1170    /// triggers the compiler's exhaustiveness check. See
1171    /// `is_regulated` for the predicate-pair contract + bucket
1172    /// definitions.
1173    pub const fn is_restricted(self) -> bool {
1174        match self {
1175            Self::Public => false,
1176            Self::Internal | Self::Confidential | Self::Pii | Self::Phi | Self::Pci => true,
1177        }
1178    }
1179}
1180
1181// `impl FromStr for DataClassification` +
1182// `impl tatara_lisp::ClosedSet for DataClassification` +
1183// `impl std::fmt::Display for DataClassification` +
1184// `pub struct UnknownDataClassification(pub String)` are all generated
1185// by `#[derive(tatara_closed_set::DeriveClosedSet)]` +
1186// `#[closed_set(via = "as_str", generate_unknown, display)]` on the
1187// enum declaration above. The auto-derived carrier label
1188// "data classification" matches the prior hand-rolled
1189// `#[error("unknown data classification: {0}")]` annotation
1190// byte-for-byte. See the retrofit comment block on
1191// [`ConvergencePointType`] for the canonical narrative.
1192
1193// ───────────────────────────── bridges to tatara-core ─────────────────
1194
1195impl From<ConvergencePointType> for core::ConvergencePointType {
1196    fn from(v: ConvergencePointType) -> Self {
1197        use ConvergencePointType::*;
1198        match v {
1199            Transform => Self::Transform,
1200            Fork => Self::Fork,
1201            Join => Self::Join,
1202            Gate => Self::Gate,
1203            Select => Self::Select,
1204            Broadcast => Self::Broadcast,
1205            Reduce => Self::Reduce,
1206            Observe => Self::Observe,
1207        }
1208    }
1209}
1210
1211impl From<core::ConvergencePointType> for ConvergencePointType {
1212    fn from(v: core::ConvergencePointType) -> Self {
1213        use core::ConvergencePointType as C;
1214        match v {
1215            C::Transform => Self::Transform,
1216            C::Fork => Self::Fork,
1217            C::Join => Self::Join,
1218            C::Gate => Self::Gate,
1219            C::Select => Self::Select,
1220            C::Broadcast => Self::Broadcast,
1221            C::Reduce => Self::Reduce,
1222            C::Observe => Self::Observe,
1223        }
1224    }
1225}
1226
1227impl From<SubstrateType> for core::SubstrateType {
1228    fn from(v: SubstrateType) -> Self {
1229        use SubstrateType::*;
1230        match v {
1231            Financial => Self::Financial,
1232            Compute => Self::Compute,
1233            Network => Self::Network,
1234            Storage => Self::Storage,
1235            Security => Self::Security,
1236            Identity => Self::Identity,
1237            Observability => Self::Observability,
1238            Regulatory => Self::Regulatory,
1239        }
1240    }
1241}
1242
1243impl From<core::SubstrateType> for SubstrateType {
1244    fn from(v: core::SubstrateType) -> Self {
1245        use core::SubstrateType as C;
1246        match v {
1247            C::Financial => Self::Financial,
1248            C::Compute => Self::Compute,
1249            C::Network => Self::Network,
1250            C::Storage => Self::Storage,
1251            C::Security => Self::Security,
1252            C::Identity => Self::Identity,
1253            C::Observability => Self::Observability,
1254            C::Regulatory => Self::Regulatory,
1255        }
1256    }
1257}
1258
1259impl From<OptimizationDirection> for core::OptimizationDirection {
1260    fn from(v: OptimizationDirection) -> Self {
1261        match v {
1262            OptimizationDirection::Minimize => Self::Minimize,
1263            OptimizationDirection::Maximize => Self::Maximize,
1264        }
1265    }
1266}
1267
1268impl From<core::OptimizationDirection> for OptimizationDirection {
1269    fn from(v: core::OptimizationDirection) -> Self {
1270        use core::OptimizationDirection as C;
1271        match v {
1272            C::Minimize => Self::Minimize,
1273            C::Maximize => Self::Maximize,
1274        }
1275    }
1276}
1277
1278impl From<Horizon> for core::ConvergenceHorizon {
1279    fn from(v: Horizon) -> Self {
1280        match v.kind {
1281            HorizonKind::Bounded => Self::Bounded,
1282            HorizonKind::Asymptotic => Self::Asymptotic {
1283                metric: v.metric.unwrap_or_default(),
1284                direction: v.direction.unwrap_or_default().into(),
1285                healthy_rate_threshold: v.healthy_rate_threshold.unwrap_or_default(),
1286            },
1287        }
1288    }
1289}
1290
1291impl From<CalmClassification> for core::CalmClassification {
1292    fn from(v: CalmClassification) -> Self {
1293        match v {
1294            CalmClassification::Monotone => Self::Monotone,
1295            CalmClassification::NonMonotone => Self::NonMonotone,
1296        }
1297    }
1298}
1299
1300impl From<core::CalmClassification> for CalmClassification {
1301    fn from(v: core::CalmClassification) -> Self {
1302        use core::CalmClassification as C;
1303        match v {
1304            C::Monotone => Self::Monotone,
1305            C::NonMonotone => Self::NonMonotone,
1306        }
1307    }
1308}
1309
1310impl From<DataClassification> for core_compl::DataClassification {
1311    fn from(v: DataClassification) -> Self {
1312        use DataClassification::*;
1313        match v {
1314            Public => Self::Public,
1315            Internal => Self::Internal,
1316            Confidential => Self::Confidential,
1317            Pii => Self::Pii,
1318            Phi => Self::Phi,
1319            Pci => Self::Pci,
1320        }
1321    }
1322}
1323
1324impl From<core_compl::DataClassification> for DataClassification {
1325    fn from(v: core_compl::DataClassification) -> Self {
1326        use core_compl::DataClassification as C;
1327        match v {
1328            C::Public => Self::Public,
1329            C::Internal => Self::Internal,
1330            C::Confidential => Self::Confidential,
1331            C::Pii => Self::Pii,
1332            C::Phi => Self::Phi,
1333            C::Pci => Self::Pci,
1334        }
1335    }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341    // The closed-set tests below call `T::from_str(bad)` via the
1342    // derive-generated `FromStr` impls — bring the trait into scope at
1343    // the test module so the lib body doesn't carry an otherwise-unused
1344    // `use std::str::FromStr;` at the file head.
1345    use std::str::FromStr;
1346
1347    #[test]
1348    fn bridges_roundtrip() {
1349        let pt: core::ConvergencePointType = ConvergencePointType::Gate.into();
1350        let back: ConvergencePointType = pt.into();
1351        assert_eq!(back, ConvergencePointType::Gate);
1352
1353        let sub: core::SubstrateType = SubstrateType::Observability.into();
1354        let back: SubstrateType = sub.into();
1355        assert_eq!(back, SubstrateType::Observability);
1356    }
1357
1358    #[test]
1359    fn data_classification_ordering() {
1360        assert!(DataClassification::Public < DataClassification::Pii);
1361        assert!(DataClassification::Internal < DataClassification::Confidential);
1362    }
1363
1364    #[test]
1365    fn horizon_default_is_bounded() {
1366        assert_eq!(Horizon::default().kind, HorizonKind::Bounded);
1367    }
1368
1369    // ── Classification::gate_compute substrate pins ─────────────────────
1370    //
1371    // The six-line `Classification { point_type: Gate, substrate: Compute,
1372    // horizon: Default::default(), calm: Default::default(),
1373    // data_classification: Default::default() }` struct-literal was
1374    // open-coded verbatim at ten hand-authored callsites before the
1375    // primitive closed it. These pins bind the composed shape at
1376    // fail-before-pass-after granularity so a regression that flipped a
1377    // baseline axis, drifted a sibling default, or leaked a non-baseline
1378    // slot into the substrate composer surfaces HERE rather than as
1379    // silent operator-visible drift at every unadorned ephemeral env
1380    // (the one production consumer, `default_ephemeral_class`) AND every
1381    // downstream test fixture that keys assertions on the shape.
1382
1383    #[test]
1384    fn gate_compute_composes_the_five_baseline_axes() {
1385        // Primary shape: every axis parked at the workspace baseline.
1386        // A regression that flipped `point_type` off `Gate` or
1387        // `substrate` off `Compute` — the two axes with no `Default` —
1388        // surfaces here.
1389        let c = Classification::gate_compute();
1390        assert_eq!(c.point_type, ConvergencePointType::Gate);
1391        assert_eq!(c.substrate, SubstrateType::Compute);
1392        assert_eq!(c.horizon, Horizon::default());
1393        assert_eq!(c.calm, CalmClassification::default());
1394        assert_eq!(c.data_classification, DataClassification::default());
1395    }
1396
1397    #[test]
1398    fn gate_compute_defaulted_axes_ride_sibling_closed_set_defaults() {
1399        // Pins the sibling-default correspondence the doc comment
1400        // names — a regression that flipped a sibling default (a new
1401        // `HorizonKind` variant promoted to `#[default]`, a rename of
1402        // `CalmClassification::Monotone`, a promotion of `Pii` above
1403        // `Internal` in the `DataClassification` ordering) would move
1404        // the baseline HERE rather than at every downstream consumer.
1405        let c = Classification::gate_compute();
1406        assert_eq!(c.horizon.kind, HorizonKind::Bounded);
1407        assert_eq!(c.calm, CalmClassification::Monotone);
1408        assert_eq!(c.data_classification, DataClassification::Internal);
1409    }
1410
1411    #[test]
1412    fn gate_compute_matches_hand_authored_pre_lift_bytewise() {
1413        // Byte-identical parity with the pre-lift six-line struct-literal
1414        // that recurred at ten hand-authored sites. A regression that
1415        // reshaped the primitive would diverge from the pre-lift block
1416        // HERE rather than at every downstream fixture that keys on the
1417        // shape.
1418        let composed = Classification::gate_compute();
1419        let hand_authored = Classification {
1420            point_type: ConvergencePointType::Gate,
1421            substrate: SubstrateType::Compute,
1422            horizon: Horizon::default(),
1423            calm: CalmClassification::default(),
1424            data_classification: DataClassification::default(),
1425        };
1426        assert_eq!(composed, hand_authored);
1427    }
1428
1429    #[test]
1430    fn gate_compute_is_call_time_construction_not_a_shared_singleton() {
1431        // Two independent calls produce structurally-equal but distinct
1432        // values — pins that the primitive is a plain constructor
1433        // rather than a `lazy_static` clone (which would leak a shared
1434        // singleton whose in-place mutation at one consumer would
1435        // silently mutate the shape at every other consumer). The `!=`
1436        // check on `&mut _`-obtained pointer addresses is intentional:
1437        // a shared singleton would collide, and the pin catches the
1438        // regression at the primitive rather than at the operator-facing
1439        // shape-drift downstream.
1440        let a = Classification::gate_compute();
1441        let b = Classification::gate_compute();
1442        assert_eq!(a, b);
1443        assert!(!std::ptr::eq(&a, &b));
1444    }
1445
1446    // ── closed-set algebra contracts for DataClassification
1447    //    (ALL × as_str × FromStr × rank × predicate pair) ────────────
1448
1449    /// Structural well-formedness of [`DataClassification`] as a
1450    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1451    /// testkit lift that pins all three structural invariants (`ALL`
1452    /// is non-empty, every variant round-trips through
1453    /// `label ↔ parse_label`, labels are pairwise distinct, `""` is
1454    /// outside the closed set) at ONE call site. Replaces the hand-
1455    /// derived `data_classification_all_is_unique_and_complete` +
1456    /// `data_classification_roundtrip_via_as_str` + the empty-input arm
1457    /// of `unknown_data_classification_errors`. `FromStr` delegates to
1458    /// `<Self as tatara_closed_set::ClosedSet>::parse_label`, so this helper
1459    /// exercises the same code path the reconciler hits when parsing a
1460    /// CRD `enum:`-validated `dataClassification` value back to the
1461    /// typed classification.
1462    #[test]
1463    fn data_classification_is_well_formed_closed_set() {
1464        tatara_closed_set::assert_closed_set_well_formed::<DataClassification>();
1465    }
1466
1467    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1468    /// output verbatim for every variant. A future variant rename (or
1469    /// an `as_str` arm typo) lands here at one site, instead of
1470    /// drifting between the typed surface, the CRD enum, and the YAML
1471    /// wire format the reconciler stamps on
1472    /// `spec.classification.dataClassification`.
1473    #[test]
1474    fn data_classification_as_str_matches_serde() {
1475        crate::tagged_union::assert_label_matches_serde_serialization::<DataClassification>();
1476    }
1477
1478    /// The Display impl IS `as_str` — pinning this lets future callers
1479    /// reach for either projection without drift. Any operator-facing
1480    /// "dataClassification={class}" diagnostic that composes through
1481    /// Display inherits the canonical wire-format string automatically.
1482    #[test]
1483    fn data_classification_display_matches_as_str() {
1484        crate::tagged_union::assert_display_matches_label::<DataClassification>();
1485    }
1486
1487    /// `FromStr` rejects strings that aren't in the canonical
1488    /// projection — lowercased / typo / cross-axis-leaked — and the
1489    /// error echoes the input verbatim so the operator-facing
1490    /// diagnostic carries the offending value, not a normalized form.
1491    /// The empty-input arm is pinned by
1492    /// [`data_classification_is_well_formed_closed_set`] via the
1493    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
1494    /// verbatim-echo contract on the [`UnknownDataClassification`]
1495    /// newtype, which the trait's `make_unknown` can't see.
1496    #[test]
1497    fn unknown_data_classification_errors() {
1498        for bad in [
1499            "pii",          // lowercased
1500            "PII",          // uppercased
1501            "PersonalData", // typo
1502            "internal_data",
1503            "Steady",   // PoolPhase-axis leak
1504            "Replace",  // ReturnPolicy-axis leak
1505            "Attested", // ProcessPhase-axis leak
1506            "Compute",  // SubstrateType-axis leak
1507            "Gate",     // ConvergencePointType-axis leak
1508            "Monotone", // CalmClassification-axis leak
1509        ] {
1510            let err = DataClassification::from_str(bad).unwrap_err();
1511            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1512        }
1513    }
1514
1515    // `unknown_data_classification_message_matches_substrate_convention`
1516    // removed — clause (5) of
1517    // `tatara_closed_set::assert_closed_set_well_formed::<DataClassification>()`
1518    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1519    // shape generically (called from
1520    // `data_classification_is_well_formed_closed_set` above); the
1521    // `SET_LABEL` projection is pinned by
1522    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1523
1524    /// TRUTH-TABLE CONTRACT: the predicate pair agrees with the
1525    /// documented per-variant compliance role. Pinning this table at
1526    /// one site means any future compliance-baseline auto-selector
1527    /// reads the same projection that the reconciler writes.
1528    #[test]
1529    fn data_classification_predicate_truth_tables() {
1530        assert!(!DataClassification::Public.is_restricted());
1531        assert!(!DataClassification::Public.is_regulated());
1532
1533        assert!(DataClassification::Internal.is_restricted());
1534        assert!(!DataClassification::Internal.is_regulated());
1535
1536        assert!(DataClassification::Confidential.is_restricted());
1537        assert!(!DataClassification::Confidential.is_regulated());
1538
1539        assert!(DataClassification::Pii.is_restricted());
1540        assert!(DataClassification::Pii.is_regulated());
1541
1542        assert!(DataClassification::Phi.is_restricted());
1543        assert!(DataClassification::Phi.is_regulated());
1544
1545        assert!(DataClassification::Pci.is_restricted());
1546        assert!(DataClassification::Pci.is_regulated());
1547    }
1548
1549    /// IMPLICATION CONTRACT: every regulated classification is also
1550    /// restricted. The impossible bucket (regulated AND
1551    /// freely-distributable) is pinned empty so a future variant that
1552    /// returned `(true, false)` from the predicate pair would FAIL
1553    /// here, forcing the author to either flip `is_restricted` or
1554    /// extend the consumer dispatch sites (compliance-baseline
1555    /// auto-selector, audit-log mandatory-fields validator)
1556    /// deliberately rather than silently producing a regulated class
1557    /// the API server would accept as freely-distributable. Encoded as
1558    /// material implication `is_regulated → is_restricted` so the
1559    /// boolean reads as the documented contract, not its NAND form.
1560    #[test]
1561    fn data_classification_regulated_implies_restricted() {
1562        for class in DataClassification::ALL {
1563            assert!(
1564                !class.is_regulated() || class.is_restricted(),
1565                "{class:?} is regulated but not restricted — \
1566                 regulated data is by definition not freely distributable",
1567            );
1568        }
1569    }
1570
1571    /// COVERAGE CONTRACT: every variant lands in exactly one of three
1572    /// compliance buckets — freely distributable (`Public`),
1573    /// restricted-only (`Internal | Confidential`), or regulated
1574    /// (`Pii | Phi | Pci`). Pins the three buckets at their declared
1575    /// cardinalities (1, 2, 3 — sum to `ALL.len()`) so a future
1576    /// variant lands somewhere deliberately.
1577    #[test]
1578    fn data_classification_buckets_cover_every_variant() {
1579        let mut free = 0u32;
1580        let mut restricted_only = 0u32;
1581        let mut regulated = 0u32;
1582        for class in DataClassification::ALL {
1583            match (class.is_restricted(), class.is_regulated()) {
1584                (false, false) => free += 1,
1585                (true, false) => restricted_only += 1,
1586                (true, true) => regulated += 1,
1587                (false, true) => {
1588                    panic!("regulated_implies_restricted already pins this empty for {class:?}")
1589                }
1590            }
1591        }
1592        assert_eq!(free, 1, "free bucket: Public");
1593        assert_eq!(
1594            restricted_only, 2,
1595            "restricted-only bucket: Internal + Confidential"
1596        );
1597        assert_eq!(regulated, 3, "regulated bucket: Pii + Phi + Pci");
1598        assert_eq!(
1599            free + restricted_only + regulated,
1600            DataClassification::ALL.len() as u32
1601        );
1602    }
1603
1604    /// MONOTONE-RANK CONTRACT: `sensitivity_rank` is strictly
1605    /// monotone over `ALL`'s declared order, so the lattice ordering
1606    /// `Public < Internal < Confidential < Pii < Phi < Pci` is sealed
1607    /// at one site (this enum's projection) instead of riding on the
1608    /// silent `as u8` cast in [`tatara_lattice`]. A future variant
1609    /// inserted in the middle would either preserve strict monotonicity
1610    /// here (and the lattice keeps working) or FAIL here at compile or
1611    /// test time (and the author has to renumber deliberately). Also
1612    /// pins the rank codomain at `0..ALL.len()` so no variant can
1613    /// silently outrank the documented top.
1614    #[test]
1615    fn data_classification_rank_is_strictly_monotone_over_all() {
1616        let ranks: Vec<u8> = DataClassification::ALL
1617            .into_iter()
1618            .map(DataClassification::sensitivity_rank)
1619            .collect();
1620        for win in ranks.windows(2) {
1621            assert!(win[0] < win[1], "ranks not strictly monotone: {ranks:?}");
1622        }
1623        assert_eq!(*ranks.first().unwrap(), 0, "bottom rank must be 0");
1624        assert_eq!(
1625            *ranks.last().unwrap(),
1626            (DataClassification::ALL.len() as u8) - 1,
1627            "top rank must be ALL.len() - 1"
1628        );
1629    }
1630
1631    /// RANK-AGREES-WITH-ORD CONTRACT: the typed `sensitivity_rank`
1632    /// projection agrees with the derived `PartialOrd` / `Ord` for
1633    /// every pair in `ALL × ALL`. This is the bridge that lets
1634    /// [`tatara_lattice`]'s total-order `Lattice for DataClassification`
1635    /// impl call `sensitivity_rank` instead of `as u8` without changing
1636    /// any observable lattice behavior — and it lets a future
1637    /// reordering of the enum's variant declarations land at this test
1638    /// site (forcing the rank arms to be renumbered) rather than
1639    /// silently shifting the lattice's `leq` relation.
1640    #[test]
1641    fn data_classification_rank_agrees_with_partial_ord() {
1642        for a in DataClassification::ALL {
1643            for b in DataClassification::ALL {
1644                assert_eq!(
1645                    a.sensitivity_rank() <= b.sensitivity_rank(),
1646                    a <= b,
1647                    "rank vs. PartialOrd drift on ({a:?}, {b:?})"
1648                );
1649            }
1650        }
1651    }
1652
1653    /// DEFAULT-AGREEMENT CONTRACT: `DataClassification::default()`
1654    /// returns `Internal` (the variant tagged `#[default]`), AND that
1655    /// variant lands in the restricted-only bucket — neither freely
1656    /// distributable nor externally regulated. A future `#[default]`
1657    /// rename without flipping the predicates fails here.
1658    #[test]
1659    fn data_classification_default_is_internal_in_restricted_only_bucket() {
1660        let d = DataClassification::default();
1661        assert_eq!(d, DataClassification::Internal);
1662        assert!(d.is_restricted());
1663        assert!(!d.is_regulated());
1664        assert_eq!(d.sensitivity_rank(), 1);
1665    }
1666
1667    /// BRIDGE ROUND-TRIP CONTRACT: every variant survives the
1668    /// CRD-facing (`PascalCase`) ↔ tatara-core (`snake_case`)
1669    /// `From` hop. Today the bridge is two hand-written 6-arm matches
1670    /// in this file; pinning the round-trip over `ALL` means a future
1671    /// variant added without extending the bridge fails here at one
1672    /// site instead of drifting between the CRD wire format and the
1673    /// `core_compl::DataClassification` selector axis.
1674    #[test]
1675    fn data_classification_bridge_roundtrip_over_all() {
1676        for class in DataClassification::ALL {
1677            let core: core_compl::DataClassification = class.into();
1678            let back: DataClassification = core.into();
1679            assert_eq!(back, class, "bridge round-trip failed for {class:?}");
1680        }
1681    }
1682
1683    // ── closed-set algebra contracts for ConvergencePointType
1684    //    (ALL × as_str × FromStr × arity-pair × predicate triple) ────
1685
1686    /// Structural well-formedness of [`ConvergencePointType`] as a
1687    /// [`tatara_lisp::ClosedSet`] implementor — the workspace-wide
1688    /// testkit lift that pins all three structural invariants (`ALL`
1689    /// is non-empty, every variant round-trips through `label ↔
1690    /// parse_label`, labels are pairwise distinct, `""` is outside
1691    /// the closed set) at ONE call site. Replaces the hand-derived
1692    /// `convergence_point_type_all_is_unique_and_complete` +
1693    /// `convergence_point_type_roundtrip_via_as_str` + the empty-
1694    /// input arm of `unknown_convergence_point_type_errors`.
1695    /// `FromStr` delegates to `<Self as tatara_closed_set::ClosedSet>::parse_label`,
1696    /// so this helper exercises the same code path the reconciler
1697    /// hits when parsing a CRD `enum:`-validated value back to the
1698    /// typed point-type. The forced `[Self; 8]` array literal on
1699    /// `ConvergencePointType::ALL` still pins the cardinality at the
1700    /// declaration site.
1701    #[test]
1702    fn convergence_point_type_is_well_formed_closed_set() {
1703        tatara_closed_set::assert_closed_set_well_formed::<ConvergencePointType>();
1704    }
1705
1706    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1707    /// output verbatim for every variant. A future variant rename (or
1708    /// an `as_str` arm typo) lands here at one site, instead of
1709    /// drifting between the typed surface, the CRD enum, and the YAML
1710    /// wire format the reconciler reads from
1711    /// `spec.classification.pointType`.
1712    #[test]
1713    fn convergence_point_type_as_str_matches_serde() {
1714        crate::tagged_union::assert_label_matches_serde_serialization::<ConvergencePointType>();
1715    }
1716
1717    /// The Display impl IS `as_str` — pinning this lets future callers
1718    /// reach for either projection without drift.
1719    #[test]
1720    fn convergence_point_type_display_matches_as_str() {
1721        crate::tagged_union::assert_display_matches_label::<ConvergencePointType>();
1722    }
1723
1724    /// `FromStr` rejects strings outside the canonical projection —
1725    /// lowercased / typo / cross-axis-leaked — and the error echoes
1726    /// the input verbatim so the operator-facing diagnostic surfaces
1727    /// the bad value, not a normalized form. The empty-input arm is
1728    /// pinned by [`convergence_point_type_is_well_formed_closed_set`]
1729    /// via the `tatara_lisp::ClosedSet` testkit; the cases here pin
1730    /// the verbatim-echo contract on the
1731    /// [`UnknownConvergencePointType`] newtype, which the trait's
1732    /// `make_unknown` can't see.
1733    #[test]
1734    fn unknown_convergence_point_type_errors() {
1735        for bad in [
1736            "gate",       // lowercased
1737            "GATE",       // uppercased
1738            "Transformr", // typo
1739            "Filter",
1740            "Steady",   // PoolPhase-axis leak
1741            "Pii",      // DataClassification-axis leak
1742            "Attested", // ProcessPhase-axis leak
1743            "Compute",  // SubstrateType-axis leak
1744            "Monotone", // CalmClassification-axis leak
1745            "PromQL",   // ConditionKind-axis leak
1746        ] {
1747            let err = ConvergencePointType::from_str(bad).unwrap_err();
1748            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1749        }
1750    }
1751
1752    // `unknown_convergence_point_type_message_matches_substrate_convention`
1753    // removed — clause (5) of
1754    // `tatara_closed_set::assert_closed_set_well_formed::<ConvergencePointType>()`
1755    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
1756    // shape generically (called from
1757    // `convergence_point_type_is_well_formed_closed_set` above); the
1758    // `SET_LABEL` projection is pinned by
1759    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
1760
1761    /// TRUTH-TABLE CONTRACT: the predicate triple agrees with the
1762    /// documented per-variant topology role. Pinning this table at
1763    /// one site means any future DAG validator reads the same
1764    /// projection that compliance bindings dispatch against.
1765    #[test]
1766    fn convergence_point_type_predicate_truth_tables() {
1767        // Endomorphic: 1→1
1768        assert!(ConvergencePointType::Transform.is_endomorphic());
1769        assert!(!ConvergencePointType::Transform.is_diffusive());
1770        assert!(!ConvergencePointType::Transform.is_convergent());
1771
1772        assert!(ConvergencePointType::Observe.is_endomorphic());
1773        assert!(!ConvergencePointType::Observe.is_diffusive());
1774        assert!(!ConvergencePointType::Observe.is_convergent());
1775
1776        // Diffusive: 1→N
1777        assert!(!ConvergencePointType::Fork.is_endomorphic());
1778        assert!(ConvergencePointType::Fork.is_diffusive());
1779        assert!(!ConvergencePointType::Fork.is_convergent());
1780
1781        assert!(!ConvergencePointType::Broadcast.is_endomorphic());
1782        assert!(ConvergencePointType::Broadcast.is_diffusive());
1783        assert!(!ConvergencePointType::Broadcast.is_convergent());
1784
1785        // Convergent: N→1
1786        for t in [
1787            ConvergencePointType::Join,
1788            ConvergencePointType::Gate,
1789            ConvergencePointType::Select,
1790            ConvergencePointType::Reduce,
1791        ] {
1792            assert!(!t.is_endomorphic(), "{t:?} should not be endomorphic");
1793            assert!(!t.is_diffusive(), "{t:?} should not be diffusive");
1794            assert!(t.is_convergent(), "{t:?} should be convergent");
1795        }
1796    }
1797
1798    /// COVERAGE CONTRACT: every variant lands in *exactly one* of the
1799    /// three topology buckets — endomorphic, diffusive, or convergent.
1800    /// Pins the three buckets at their declared cardinalities (2, 2, 4
1801    /// — sum to `ALL.len()`) so a future variant lands somewhere
1802    /// deliberately. No variant returns true from more than one
1803    /// predicate; no variant returns false from all three.
1804    #[test]
1805    fn convergence_point_type_buckets_cover_every_variant() {
1806        let mut endomorphic = 0u32;
1807        let mut diffusive = 0u32;
1808        let mut convergent = 0u32;
1809        for t in ConvergencePointType::ALL {
1810            let buckets = [t.is_endomorphic(), t.is_diffusive(), t.is_convergent()];
1811            let hits: u32 = buckets.iter().map(|b| u32::from(*b)).sum();
1812            assert_eq!(
1813                hits, 1,
1814                "{t:?} landed in {hits} buckets: {buckets:?} (must be exactly one)"
1815            );
1816            if t.is_endomorphic() {
1817                endomorphic += 1;
1818            }
1819            if t.is_diffusive() {
1820                diffusive += 1;
1821            }
1822            if t.is_convergent() {
1823                convergent += 1;
1824            }
1825        }
1826        assert_eq!(endomorphic, 2, "endomorphic bucket: Transform + Observe");
1827        assert_eq!(diffusive, 2, "diffusive bucket: Fork + Broadcast");
1828        assert_eq!(
1829            convergent, 4,
1830            "convergent bucket: Join + Gate + Select + Reduce"
1831        );
1832        assert_eq!(
1833            endomorphic + diffusive + convergent,
1834            ConvergencePointType::ALL.len() as u32
1835        );
1836    }
1837
1838    /// ARITY-PAIR ⇔ BUCKET CONTRACT: the `(input_arity, output_arity)`
1839    /// projection names the same topology partition as the
1840    /// `is_endomorphic` / `is_diffusive` / `is_convergent` predicate
1841    /// triple. `(One, One) ⇒ endomorphic`; `(One, Many) ⇒ diffusive`;
1842    /// `(Many, One) ⇒ convergent`. The impossible `(Many, Many)`
1843    /// bucket is pinned empty here — a `(Many, Many)` point would
1844    /// have no convergence semantics (many independent inputs
1845    /// replicated across many independent outputs) and every future
1846    /// DAG-composition validator would have to special-case it. This
1847    /// seal is the bridge that lets a future graph validator dispatch
1848    /// on either projection (arity pair OR bucket predicates) without
1849    /// drift — and a future variant that wants `(Many, Many)` must
1850    /// extend the bucket carving deliberately rather than silently
1851    /// shipping a fourth topology class.
1852    #[test]
1853    fn convergence_point_type_arity_pair_agrees_with_bucket() {
1854        for t in ConvergencePointType::ALL {
1855            match (t.input_arity(), t.output_arity()) {
1856                (Arity::One, Arity::One) => assert!(
1857                    t.is_endomorphic(),
1858                    "{t:?} has (One, One) arity but is not endomorphic"
1859                ),
1860                (Arity::One, Arity::Many) => assert!(
1861                    t.is_diffusive(),
1862                    "{t:?} has (One, Many) arity but is not diffusive"
1863                ),
1864                (Arity::Many, Arity::One) => assert!(
1865                    t.is_convergent(),
1866                    "{t:?} has (Many, One) arity but is not convergent"
1867                ),
1868                (Arity::Many, Arity::Many) => panic!(
1869                    "{t:?} has (Many, Many) arity — pinned empty; \
1870                     extend the topology carving before adding a variant here"
1871                ),
1872            }
1873        }
1874    }
1875
1876    /// BRIDGE ROUND-TRIP CONTRACT: every variant survives the
1877    /// CRD-facing (`PascalCase`) ↔ tatara-core (`snake_case`)
1878    /// `From` hop. Today the bridge is two hand-written 8-arm
1879    /// matches in this file; pinning the round-trip over `ALL`
1880    /// means a future variant added without extending the bridge
1881    /// fails here at one site instead of drifting between the CRD
1882    /// wire format and the
1883    /// `core::ConvergencePointType` selector axis that
1884    /// `compliance_binding::PointSelector::ByType` already
1885    /// dispatches against.
1886    #[test]
1887    fn convergence_point_type_bridge_roundtrip_over_all() {
1888        for t in ConvergencePointType::ALL {
1889            let core_t: core::ConvergencePointType = t.into();
1890            let back: ConvergencePointType = core_t.into();
1891            assert_eq!(back, t, "bridge round-trip failed for {t:?}");
1892        }
1893    }
1894
1895    // ── closed-set algebra contracts for Arity ───────────────────
1896
1897    /// `ALL` is the source of truth — pin its closure so a variant
1898    /// added without an `ALL` entry fails here. The arity is asserted
1899    /// by the `[Self; 2]` array type itself.
1900    #[test]
1901    fn arity_all_is_unique_and_complete() {
1902        let mut seen = std::collections::HashSet::new();
1903        for a in Arity::ALL {
1904            assert!(seen.insert(a), "duplicate variant in ALL: {a:?}");
1905        }
1906        assert_eq!(seen.len(), Arity::ALL.len());
1907    }
1908
1909    /// The Display impl IS `as_str` — pinning this lets future
1910    /// callers reach for either projection without drift. No serde
1911    /// matching here because `Arity` is a typed projection, not a
1912    /// CRD-facing enum — it never crosses the wire. Routed through
1913    /// the substrate-wide [`crate::tagged_union::assert_display_matches_label`]
1914    /// primitive so the sweep body lives at ONE substrate site rather
1915    /// than restated per-implementor. Also exercised through the
1916    /// substrate-wide `every_production_display_impl_binds_through_the_testkit_primitive`
1917    /// sweep so a per-crate test-site drop cannot silently disable the
1918    /// check.
1919    #[test]
1920    fn arity_display_matches_as_str() {
1921        crate::tagged_union::assert_display_matches_label::<Arity>();
1922    }
1923
1924    /// PREDICATE CONTRACT: `is_one` is true exactly for `Arity::One`.
1925    /// The disjointness against `Many` is structural (only two
1926    /// variants) but pinning the codomain here means a future
1927    /// `Arity::Zero` variant must declare its own `is_one` arm
1928    /// deliberately rather than silently defaulting through a
1929    /// non-closed-set match.
1930    #[test]
1931    fn arity_is_one_predicate_truth_table() {
1932        assert!(Arity::One.is_one());
1933        assert!(!Arity::Many.is_one());
1934    }
1935
1936    // ── closed-set algebra contracts for SubstrateType
1937    //    (ALL × as_str × FromStr × predicate triple × bridge) ─────────
1938
1939    /// Structural well-formedness of [`SubstrateType`] as a
1940    /// [`tatara_lisp::ClosedSet`] implementor — see
1941    /// [`convergence_point_type_is_well_formed_closed_set`] for the
1942    /// canonical lift narrative. Replaces
1943    /// `substrate_type_all_is_unique_and_complete` +
1944    /// `substrate_type_roundtrip_via_as_str` + the empty-input arm
1945    /// of `unknown_substrate_type_errors`.
1946    #[test]
1947    fn substrate_type_is_well_formed_closed_set() {
1948        tatara_closed_set::assert_closed_set_well_formed::<SubstrateType>();
1949    }
1950
1951    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
1952    /// output verbatim for every variant. A future variant rename
1953    /// (or an `as_str` arm typo) lands here at one site, instead of
1954    /// drifting between the typed surface, the CRD enum, and the
1955    /// YAML wire format the reconciler reads from
1956    /// `spec.classification.substrate`.
1957    #[test]
1958    fn substrate_type_as_str_matches_serde() {
1959        crate::tagged_union::assert_label_matches_serde_serialization::<SubstrateType>();
1960    }
1961
1962    /// The Display impl IS `as_str` — pinning this lets future
1963    /// callers reach for either projection without drift. Any
1964    /// operator-facing `substrate={kind}` diagnostic that composes
1965    /// through Display inherits the canonical wire-format string
1966    /// automatically.
1967    #[test]
1968    fn substrate_type_display_matches_as_str() {
1969        crate::tagged_union::assert_display_matches_label::<SubstrateType>();
1970    }
1971
1972    /// `FromStr` rejects strings outside the canonical projection —
1973    /// lowercased / typo / cross-axis-leaked — and the error echoes
1974    /// the input verbatim so the operator-facing diagnostic surfaces
1975    /// the bad value, not a normalized form. The empty-input arm is
1976    /// pinned by [`substrate_type_is_well_formed_closed_set`] via
1977    /// the `tatara_lisp::ClosedSet` testkit; the cases here pin the
1978    /// verbatim-echo contract on the [`UnknownSubstrateType`]
1979    /// newtype, which the trait's `make_unknown` can't see.
1980    #[test]
1981    fn unknown_substrate_type_errors() {
1982        for bad in [
1983            "compute",  // lowercased
1984            "COMPUTE",  // uppercased
1985            "Computte", // typo
1986            "Database", "Steady",   // PoolPhase-axis leak
1987            "Pii",      // DataClassification-axis leak
1988            "Attested", // ProcessPhase-axis leak
1989            "Gate",     // ConvergencePointType-axis leak
1990            "Monotone", // CalmClassification-axis leak
1991            "PromQL",   // ConditionKind-axis leak
1992        ] {
1993            let err = SubstrateType::from_str(bad).unwrap_err();
1994            assert_eq!(err.0, bad, "error payload should echo input verbatim");
1995        }
1996    }
1997
1998    // `unknown_substrate_type_message_matches_substrate_convention`
1999    // removed — clause (5) of
2000    // `tatara_closed_set::assert_closed_set_well_formed::<SubstrateType>()`
2001    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
2002    // shape generically (called from
2003    // `substrate_type_is_well_formed_closed_set` above); the
2004    // `SET_LABEL` projection is pinned by
2005    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
2006
2007    /// TRUTH-TABLE CONTRACT: the predicate triple agrees with the
2008    /// documented per-variant plane role. Pinning this table at one
2009    /// site means any future compliance-baseline selector reads the
2010    /// same projection that the reconciler stamps on the CRD.
2011    #[test]
2012    fn substrate_type_predicate_truth_tables() {
2013        // Resource plane: you allocate budgets from it.
2014        for t in [
2015            SubstrateType::Financial,
2016            SubstrateType::Compute,
2017            SubstrateType::Network,
2018            SubstrateType::Storage,
2019        ] {
2020            assert!(t.is_resource(), "{t:?} should be a resource substrate");
2021            assert!(!t.is_policy(), "{t:?} should not be a policy substrate");
2022            assert!(
2023                !t.is_telemetry(),
2024                "{t:?} should not be a telemetry substrate"
2025            );
2026        }
2027
2028        // Policy plane: it gates access for other workloads.
2029        for t in [
2030            SubstrateType::Security,
2031            SubstrateType::Identity,
2032            SubstrateType::Regulatory,
2033        ] {
2034            assert!(!t.is_resource(), "{t:?} should not be a resource substrate");
2035            assert!(t.is_policy(), "{t:?} should be a policy substrate");
2036            assert!(
2037                !t.is_telemetry(),
2038                "{t:?} should not be a telemetry substrate"
2039            );
2040        }
2041
2042        // Telemetry plane: it observes other workloads.
2043        assert!(!SubstrateType::Observability.is_resource());
2044        assert!(!SubstrateType::Observability.is_policy());
2045        assert!(SubstrateType::Observability.is_telemetry());
2046    }
2047
2048    /// COVERAGE CONTRACT: every variant lands in *exactly one* of
2049    /// the three plane buckets — resource, policy, or telemetry.
2050    /// Pins the three buckets at their declared cardinalities (4,
2051    /// 3, 1 — sum to `ALL.len()`) so a future variant lands
2052    /// somewhere deliberately. No variant returns true from more
2053    /// than one predicate; no variant returns false from all three.
2054    #[test]
2055    fn substrate_type_buckets_cover_every_variant() {
2056        let mut resource = 0u32;
2057        let mut policy = 0u32;
2058        let mut telemetry = 0u32;
2059        for t in SubstrateType::ALL {
2060            let buckets = [t.is_resource(), t.is_policy(), t.is_telemetry()];
2061            let hits: u32 = buckets.iter().map(|b| u32::from(*b)).sum();
2062            assert_eq!(
2063                hits, 1,
2064                "{t:?} landed in {hits} buckets: {buckets:?} (must be exactly one)"
2065            );
2066            if t.is_resource() {
2067                resource += 1;
2068            }
2069            if t.is_policy() {
2070                policy += 1;
2071            }
2072            if t.is_telemetry() {
2073                telemetry += 1;
2074            }
2075        }
2076        assert_eq!(
2077            resource, 4,
2078            "resource bucket: Financial + Compute + Network + Storage"
2079        );
2080        assert_eq!(policy, 3, "policy bucket: Security + Identity + Regulatory");
2081        assert_eq!(telemetry, 1, "telemetry bucket: Observability");
2082        assert_eq!(
2083            resource + policy + telemetry,
2084            SubstrateType::ALL.len() as u32
2085        );
2086    }
2087
2088    /// BRIDGE ROUND-TRIP CONTRACT: every variant survives the
2089    /// CRD-facing (`PascalCase`) ↔ tatara-core (`snake_case`)
2090    /// `From` hop. Today the bridge is two hand-written 8-arm
2091    /// matches in this file; pinning the round-trip over `ALL`
2092    /// means a future variant added without extending the bridge
2093    /// fails here at one site instead of drifting between the CRD
2094    /// wire format and the `core::SubstrateType` selector axis
2095    /// that `compliance_binding::PointSelector::BySubstrate`
2096    /// already dispatches against.
2097    #[test]
2098    fn substrate_type_bridge_roundtrip_over_all() {
2099        for t in SubstrateType::ALL {
2100            let core_t: core::SubstrateType = t.into();
2101            let back: SubstrateType = core_t.into();
2102            assert_eq!(back, t, "bridge round-trip failed for {t:?}");
2103        }
2104    }
2105
2106    // ── closed-set algebra contracts for CalmClassification
2107    //    (ALL × as_str × FromStr × requires_coordination × bridge) ─────
2108
2109    /// Structural well-formedness of [`CalmClassification`] as a
2110    /// [`tatara_lisp::ClosedSet`] implementor — see
2111    /// [`convergence_point_type_is_well_formed_closed_set`] for the
2112    /// canonical lift narrative. Replaces
2113    /// `calm_classification_all_is_unique_and_complete` +
2114    /// `calm_classification_roundtrip_via_as_str` + the empty-input
2115    /// arm of `unknown_calm_classification_errors`.
2116    #[test]
2117    fn calm_classification_is_well_formed_closed_set() {
2118        tatara_closed_set::assert_closed_set_well_formed::<CalmClassification>();
2119    }
2120
2121    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2122    /// output verbatim for every variant. A future variant rename
2123    /// (or an `as_str` arm typo) lands here at one site, instead of
2124    /// drifting between the typed surface, the CRD enum, and the
2125    /// YAML wire format the reconciler reads from
2126    /// `spec.classification.calm`.
2127    #[test]
2128    fn calm_classification_as_str_matches_serde() {
2129        crate::tagged_union::assert_label_matches_serde_serialization::<CalmClassification>();
2130    }
2131
2132    /// The Display impl IS `as_str` — pinning this lets future
2133    /// callers reach for either projection without drift. Any
2134    /// operator-facing `calm={kind}` diagnostic that composes
2135    /// through Display inherits the canonical wire-format string
2136    /// automatically.
2137    #[test]
2138    fn calm_classification_display_matches_as_str() {
2139        crate::tagged_union::assert_display_matches_label::<CalmClassification>();
2140    }
2141
2142    /// `FromStr` rejects strings outside the canonical projection —
2143    /// lowercased / typo / cross-axis-leaked — and the error echoes
2144    /// the input verbatim so the operator-facing diagnostic surfaces
2145    /// the bad value, not a normalized form. The empty-input arm is
2146    /// pinned by [`calm_classification_is_well_formed_closed_set`]
2147    /// via the `tatara_lisp::ClosedSet` testkit; the cases here pin
2148    /// the verbatim-echo contract on the
2149    /// [`UnknownCalmClassification`] newtype, which the trait's
2150    /// `make_unknown` can't see.
2151    #[test]
2152    fn unknown_calm_classification_errors() {
2153        for bad in [
2154            "monotone",     // lowercased
2155            "MONOTONE",     // uppercased
2156            "Mono",         // typo
2157            "non_monotone", // core's snake_case form (must not cross axes)
2158            "non-monotone", // dashed
2159            "Monotonic",    // close-typo
2160            "Steady",       // PoolPhase-axis leak
2161            "Pii",          // DataClassification-axis leak
2162            "Attested",     // ProcessPhase-axis leak
2163            "Compute",      // SubstrateType-axis leak
2164            "Gate",         // ConvergencePointType-axis leak
2165            "PromQL",       // ConditionKind-axis leak
2166        ] {
2167            let err = CalmClassification::from_str(bad).unwrap_err();
2168            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2169        }
2170    }
2171
2172    // `unknown_calm_classification_message_matches_substrate_convention`
2173    // removed — clause (5) of
2174    // `tatara_closed_set::assert_closed_set_well_formed::<CalmClassification>()`
2175    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
2176    // shape generically (called from
2177    // `calm_classification_is_well_formed_closed_set` above); the
2178    // `SET_LABEL` projection is pinned by
2179    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
2180
2181    /// CALM-THEOREM TRUTH-TABLE CONTRACT: `requires_coordination`
2182    /// implements the biconditional half of Hellerstein's CALM
2183    /// theorem — `Monotone ⇒ false` and `NonMonotone ⇒ true`.
2184    /// Pinning this table at one site means any future reconciler
2185    /// dispatch that picks between Raft writes and gossip
2186    /// propagation reads the same projection the lattice ordering
2187    /// (`Monotone ≤ NonMonotone`) does. A future variant that
2188    /// flipped this mapping would have to renumber every consumer
2189    /// deliberately rather than silently shipping a non-monotone
2190    /// operation onto the no-coordination path.
2191    #[test]
2192    fn calm_classification_requires_coordination_truth_table() {
2193        assert!(!CalmClassification::Monotone.requires_coordination());
2194        assert!(CalmClassification::NonMonotone.requires_coordination());
2195    }
2196
2197    /// COVERAGE CONTRACT: every variant lands in exactly one of two
2198    /// coordination buckets — no-coordination (`Monotone`) or
2199    /// requires-coordination (`NonMonotone`). Pins the two buckets
2200    /// at their declared cardinalities (1, 1 — sum to `ALL.len()`)
2201    /// so a future variant lands somewhere deliberately. The
2202    /// biconditional structure of the CALM theorem makes this
2203    /// partition exhaustive by construction.
2204    #[test]
2205    fn calm_classification_buckets_cover_every_variant() {
2206        let mut no_coord = 0u32;
2207        let mut coord = 0u32;
2208        for c in CalmClassification::ALL {
2209            if c.requires_coordination() {
2210                coord += 1;
2211            } else {
2212                no_coord += 1;
2213            }
2214        }
2215        assert_eq!(no_coord, 1, "no-coordination bucket: Monotone");
2216        assert_eq!(coord, 1, "requires-coordination bucket: NonMonotone");
2217        assert_eq!(no_coord + coord, CalmClassification::ALL.len() as u32);
2218    }
2219
2220    /// DEFAULT-AGREEMENT CONTRACT: `CalmClassification::default()`
2221    /// returns `Monotone` (the variant tagged `#[default]`) AND that
2222    /// variant lands in the no-coordination bucket. A future
2223    /// `#[default]` rename without flipping the predicate fails
2224    /// here — the default for an under-specified Process must
2225    /// remain the no-coordination side so that an unannotated
2226    /// Process can't silently demand Raft writes the reconciler
2227    /// isn't configured to provide.
2228    #[test]
2229    fn calm_classification_default_is_monotone_no_coordination() {
2230        let c = CalmClassification::default();
2231        assert_eq!(c, CalmClassification::Monotone);
2232        assert!(!c.requires_coordination());
2233    }
2234
2235    /// BRIDGE ROUND-TRIP CONTRACT: every variant survives the
2236    /// CRD-facing (`PascalCase`) ↔ tatara-core (`snake_case`)
2237    /// `From` hop. Today the bridge is two hand-written 2-arm
2238    /// matches in this file; pinning the round-trip over `ALL`
2239    /// means a future variant added without extending the bridge
2240    /// fails here at one site instead of drifting between the CRD
2241    /// wire format and the `core::CalmClassification` selector
2242    /// axis. Closes the asymmetry that pre-lift had a
2243    /// `From<CalmClassification> for core::CalmClassification`
2244    /// forward bridge but no reverse — symmetric to every other
2245    /// classification-axis bridge in this file.
2246    #[test]
2247    fn calm_classification_bridge_roundtrip_over_all() {
2248        for c in CalmClassification::ALL {
2249            let core_c: core::CalmClassification = c.into();
2250            let back: CalmClassification = core_c.into();
2251            assert_eq!(back, c, "bridge round-trip failed for {c:?}");
2252        }
2253    }
2254
2255    // ── closed-set algebra contracts for OptimizationDirection
2256    //    (ALL × as_str × FromStr × prefers_lower × is_improvement) ───
2257
2258    /// Structural well-formedness of [`OptimizationDirection`] as a
2259    /// [`tatara_lisp::ClosedSet`] implementor — see
2260    /// [`convergence_point_type_is_well_formed_closed_set`] for the
2261    /// canonical lift narrative. Replaces
2262    /// `optimization_direction_all_is_unique_and_complete` +
2263    /// `optimization_direction_roundtrip_via_as_str` + the empty-
2264    /// input arm of `unknown_optimization_direction_errors`.
2265    #[test]
2266    fn optimization_direction_is_well_formed_closed_set() {
2267        tatara_closed_set::assert_closed_set_well_formed::<OptimizationDirection>();
2268    }
2269
2270    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2271    /// output verbatim for every variant. A future variant rename
2272    /// (or an `as_str` arm typo) lands here at one site, instead of
2273    /// drifting between the typed surface, the CRD enum, and the
2274    /// YAML wire format the reconciler reads from
2275    /// `spec.classification.horizon.direction`.
2276    #[test]
2277    fn optimization_direction_as_str_matches_serde() {
2278        crate::tagged_union::assert_label_matches_serde_serialization::<OptimizationDirection>();
2279    }
2280
2281    /// The Display impl IS `as_str` — pinning this lets future
2282    /// callers reach for either projection without drift. Any
2283    /// operator-facing `direction={kind}` diagnostic that composes
2284    /// through Display inherits the canonical wire-format string
2285    /// automatically.
2286    #[test]
2287    fn optimization_direction_display_matches_as_str() {
2288        crate::tagged_union::assert_display_matches_label::<OptimizationDirection>();
2289    }
2290
2291    /// `FromStr` rejects strings outside the canonical projection —
2292    /// lowercased / typo / cross-axis-leaked — and the error echoes
2293    /// the input verbatim so the operator-facing diagnostic surfaces
2294    /// the bad value, not a normalized form. The empty-input arm is
2295    /// pinned by [`optimization_direction_is_well_formed_closed_set`]
2296    /// via the `tatara_lisp::ClosedSet` testkit; the cases here pin
2297    /// the verbatim-echo contract on the
2298    /// [`UnknownOptimizationDirection`] newtype, which the trait's
2299    /// `make_unknown` can't see.
2300    #[test]
2301    fn unknown_optimization_direction_errors() {
2302        for bad in [
2303            "minimize", // lowercased
2304            "MINIMIZE", // uppercased
2305            "Minimze",  // typo
2306            "Lower",    // synonym, not canonical
2307            "Higher",   // synonym, not canonical
2308            "Asc",      // wire-leak from sort-order axis
2309            "Desc",     // wire-leak from sort-order axis
2310            "Bounded",  // HorizonKind-axis leak
2311            "Monotone", // CalmClassification-axis leak
2312            "Steady",   // PoolPhase-axis leak
2313            "Pii",      // DataClassification-axis leak
2314            "Attested", // ProcessPhase-axis leak
2315            "Compute",  // SubstrateType-axis leak
2316            "Gate",     // ConvergencePointType-axis leak
2317            "PromQL",   // ConditionKind-axis leak
2318        ] {
2319            let err = OptimizationDirection::from_str(bad).unwrap_err();
2320            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2321        }
2322    }
2323
2324    // `unknown_optimization_direction_message_matches_substrate_convention`
2325    // removed — clause (5) of
2326    // `tatara_closed_set::assert_closed_set_well_formed::<OptimizationDirection>()`
2327    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
2328    // shape generically (called from
2329    // `optimization_direction_is_well_formed_closed_set` above); the
2330    // `SET_LABEL` projection is pinned by
2331    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
2332
2333    /// TRUTH-TABLE CONTRACT: `prefers_lower` is the boolean
2334    /// partition `Minimize ⇒ true`, `Maximize ⇒ false`. Pinning this
2335    /// table at one site means any future dispatch on per-direction
2336    /// polarity (rate-window evaluator, breathe-band regression
2337    /// detector) reads the same projection rather than re-deriving
2338    /// from the variant name. Mirrors
2339    /// [`CalmClassification::requires_coordination`]'s truth-table
2340    /// shape.
2341    #[test]
2342    fn optimization_direction_prefers_lower_truth_table() {
2343        assert!(OptimizationDirection::Minimize.prefers_lower());
2344        assert!(!OptimizationDirection::Maximize.prefers_lower());
2345    }
2346
2347    /// COVERAGE CONTRACT: every variant lands in exactly one of two
2348    /// polarity buckets — prefers-lower (`Minimize`) or
2349    /// prefers-higher (`Maximize`). Pins the two buckets at their
2350    /// declared cardinalities (1, 1 — sum to `ALL.len()`) so a
2351    /// future variant lands somewhere deliberately.
2352    #[test]
2353    fn optimization_direction_buckets_cover_every_variant() {
2354        let mut lower = 0u32;
2355        let mut higher = 0u32;
2356        for d in OptimizationDirection::ALL {
2357            if d.prefers_lower() {
2358                lower += 1;
2359            } else {
2360                higher += 1;
2361            }
2362        }
2363        assert_eq!(lower, 1, "prefers-lower bucket: Minimize");
2364        assert_eq!(higher, 1, "prefers-higher bucket: Maximize");
2365        assert_eq!(lower + higher, OptimizationDirection::ALL.len() as u32);
2366    }
2367
2368    /// LOAD-BEARING TRUTH-TABLE: `is_improvement` answers "is `after`
2369    /// strictly better than `before` under this direction?" for the
2370    /// canonical samples. Pins the strict-improvement semantic at
2371    /// one site so a future rate-window evaluator or breathe-band
2372    /// regression detector reads the same projection that the
2373    /// asymptotic-health probe writes.
2374    #[test]
2375    fn optimization_direction_is_improvement_truth_table() {
2376        // Minimize: lower-is-better
2377        assert!(OptimizationDirection::Minimize.is_improvement(10.0, 5.0));
2378        assert!(!OptimizationDirection::Minimize.is_improvement(5.0, 10.0));
2379
2380        // Maximize: higher-is-better
2381        assert!(OptimizationDirection::Maximize.is_improvement(5.0, 10.0));
2382        assert!(!OptimizationDirection::Maximize.is_improvement(10.0, 5.0));
2383    }
2384
2385    /// NO-OP CONTRACT: a sample equal to the previous one is NOT an
2386    /// improvement under either direction. Pinning this guarantees
2387    /// a flatlined rate-window evaluator doesn't silently keep
2388    /// claiming "still improving" forever and skipping the
2389    /// healthy-rate-threshold gate.
2390    #[test]
2391    fn optimization_direction_no_op_is_not_improvement() {
2392        for d in OptimizationDirection::ALL {
2393            assert!(
2394                !d.is_improvement(7.0, 7.0),
2395                "{d:?}: equal samples must not count as improvement",
2396            );
2397            assert!(
2398                !d.is_improvement(0.0, 0.0),
2399                "{d:?}: zero/zero must not count as improvement",
2400            );
2401        }
2402    }
2403
2404    /// NaN CONTRACT: NaN on either operand short-circuits to `false`
2405    /// (no improvement claim from indeterminate data) via the
2406    /// standard `PartialOrd` behavior. Without this, a rate-window
2407    /// evaluator that sampled a NaN partway through (a transient
2408    /// metric-scrape failure) would either panic on an `Ord`
2409    /// comparison or — worse — silently claim improvement on the
2410    /// next valid sample by treating NaN as the worst case.
2411    #[test]
2412    fn optimization_direction_nan_is_not_improvement() {
2413        let nan = f64::NAN;
2414        for d in OptimizationDirection::ALL {
2415            assert!(
2416                !d.is_improvement(nan, 1.0),
2417                "{d:?}: NaN before must not count as improvement",
2418            );
2419            assert!(
2420                !d.is_improvement(1.0, nan),
2421                "{d:?}: NaN after must not count as improvement",
2422            );
2423            assert!(
2424                !d.is_improvement(nan, nan),
2425                "{d:?}: NaN/NaN must not count as improvement",
2426            );
2427        }
2428    }
2429
2430    /// ANTISYMMETRY CONTRACT: for distinct finite samples,
2431    /// `is_improvement(a, b)` xor `is_improvement(b, a)` —
2432    /// exactly one direction of the pair counts as improvement.
2433    /// This is the algebraic shape every asymptotic-health
2434    /// rate-window evaluator depends on to avoid double-counting
2435    /// an improvement as a regression on the reverse traversal.
2436    /// A future variant that returned `true` for both directions
2437    /// (or `false` for both, the equal-sample case) would FAIL
2438    /// here, forcing the author to extend the consumer dispatch
2439    /// deliberately.
2440    #[test]
2441    fn optimization_direction_is_improvement_is_antisymmetric() {
2442        let pairs = [(1.0_f64, 2.0_f64), (0.0, 100.0), (-3.5, 3.5), (1e9, 1e-9)];
2443        for d in OptimizationDirection::ALL {
2444            for (a, b) in pairs {
2445                assert!(a != b, "test fixture requires distinct samples");
2446                assert!(
2447                    d.is_improvement(a, b) ^ d.is_improvement(b, a),
2448                    "{d:?}: antisymmetry violated on ({a}, {b})",
2449                );
2450            }
2451        }
2452    }
2453
2454    /// DEFAULT-AGREEMENT CONTRACT:
2455    /// `OptimizationDirection::default()` returns `Minimize` (the
2456    /// variant tagged `#[default]`), AND that variant lands in the
2457    /// prefers-lower bucket. A future `#[default]` rename without
2458    /// flipping the predicate fails here — `Minimize` is the
2459    /// canonical default for distributed-systems asymptotic
2460    /// optimization (cost / latency / error rate), so an
2461    /// unannotated metric must not silently flip the rate-window
2462    /// evaluator's polarity. This is also the same value the
2463    /// `Horizon → ConvergenceHorizon` bridge falls back to when
2464    /// `direction` is unset, so pinning the default here pins the
2465    /// bridge's behavior at one site.
2466    #[test]
2467    fn optimization_direction_default_is_minimize_prefers_lower() {
2468        let d = OptimizationDirection::default();
2469        assert_eq!(d, OptimizationDirection::Minimize);
2470        assert!(d.prefers_lower());
2471    }
2472
2473    /// BRIDGE ROUND-TRIP CONTRACT: every variant survives the
2474    /// CRD-facing (`PascalCase`) ↔ tatara-core (`snake_case`)
2475    /// `From` hop. Pre-lift the bridge was a one-way
2476    /// `From<OptimizationDirection> for core::OptimizationDirection`
2477    /// with no reverse — asymmetric to every other classification-
2478    /// axis bridge in this file. Pinning the round-trip over `ALL`
2479    /// means a future variant added without extending the bridge
2480    /// fails here at one site instead of drifting between the CRD
2481    /// wire format and `core::OptimizationDirection`.
2482    #[test]
2483    fn optimization_direction_bridge_roundtrip_over_all() {
2484        for d in OptimizationDirection::ALL {
2485            let core_d: core::OptimizationDirection = d.into();
2486            let back: OptimizationDirection = core_d.into();
2487            assert_eq!(back, d, "bridge round-trip failed for {d:?}");
2488        }
2489    }
2490
2491    // ── closed-set algebra contracts for HorizonKind
2492    //    (ALL × as_str × FromStr × terminates × requires_metric_axes) ──
2493
2494    /// Structural well-formedness of [`HorizonKind`] as a
2495    /// [`tatara_lisp::ClosedSet`] implementor — see
2496    /// [`convergence_point_type_is_well_formed_closed_set`] for the
2497    /// canonical lift narrative. Replaces
2498    /// `horizon_kind_all_is_unique_and_complete` +
2499    /// `horizon_kind_roundtrip_via_as_str` + the empty-input arm of
2500    /// `unknown_horizon_kind_errors`.
2501    #[test]
2502    fn horizon_kind_is_well_formed_closed_set() {
2503        tatara_closed_set::assert_closed_set_well_formed::<HorizonKind>();
2504    }
2505
2506    /// CANONICAL-KEY CONTRACT: `as_str` matches serde's PascalCase
2507    /// output verbatim for every variant. A future variant rename
2508    /// (or an `as_str` arm typo) lands here at one site, instead of
2509    /// drifting between the typed surface, the CRD enum, and the
2510    /// YAML wire format the reconciler stamps on
2511    /// `spec.classification.horizon.kind`.
2512    #[test]
2513    fn horizon_kind_as_str_matches_serde() {
2514        crate::tagged_union::assert_label_matches_serde_serialization::<HorizonKind>();
2515    }
2516
2517    /// The Display impl IS `as_str` — pinning this lets future
2518    /// callers reach for either projection without drift. Any
2519    /// operator-facing `horizon.kind={kind}` diagnostic that
2520    /// composes through Display inherits the canonical wire-format
2521    /// string automatically.
2522    #[test]
2523    fn horizon_kind_display_matches_as_str() {
2524        crate::tagged_union::assert_display_matches_label::<HorizonKind>();
2525    }
2526
2527    /// `FromStr` rejects strings outside the canonical projection —
2528    /// lowercased / typo / cross-axis-leaked — and the error echoes
2529    /// the input verbatim so the operator-facing diagnostic surfaces
2530    /// the bad value, not a normalized form. The empty-input arm is
2531    /// pinned by [`horizon_kind_is_well_formed_closed_set`] via the
2532    /// `tatara_lisp::ClosedSet` testkit; the cases here pin the
2533    /// verbatim-echo contract on the [`UnknownHorizonKind`] newtype,
2534    /// which the trait's `make_unknown` can't see.
2535    #[test]
2536    fn unknown_horizon_kind_errors() {
2537        for bad in [
2538            "bounded",   // lowercased
2539            "BOUNDED",   // uppercased
2540            "Boundd",    // typo
2541            "Finite",    // synonym, not canonical
2542            "Perpetual", // synonym, not canonical
2543            "Infinite",  // synonym, not canonical
2544            "Minimize",  // OptimizationDirection-axis leak
2545            "Monotone",  // CalmClassification-axis leak
2546            "Pii",       // DataClassification-axis leak
2547            "Steady",    // PoolPhase-axis leak
2548            "Attested",  // ProcessPhase-axis leak
2549            "Compute",   // SubstrateType-axis leak
2550            "Gate",      // ConvergencePointType-axis leak
2551            "PromQL",    // ConditionKind-axis leak
2552        ] {
2553            let err = HorizonKind::from_str(bad).unwrap_err();
2554            assert_eq!(err.0, bad, "error payload should echo input verbatim");
2555        }
2556    }
2557
2558    // `unknown_horizon_kind_message_matches_substrate_convention`
2559    // removed — clause (5) of
2560    // `tatara_closed_set::assert_closed_set_well_formed::<HorizonKind>()`
2561    // verifies the substrate-wide `"unknown {SET_LABEL}: {input}"`
2562    // shape generically (called from
2563    // `horizon_kind_is_well_formed_closed_set` above); the
2564    // `SET_LABEL` projection is pinned by
2565    // `tatara_lisp_derive::pascal_to_spaced_lowercase_tests`.
2566
2567    /// LOAD-BEARING TRUTH-TABLE: `terminates` is the boolean
2568    /// partition `Bounded ⇒ true`, `Asymptotic ⇒ false`. Pinning
2569    /// this table at one site means any future scheduler asking
2570    /// "will this Process reach `Reaped` via natural termination?"
2571    /// reads the same projection that the lattice ordering encodes
2572    /// (Bounded ≤ Asymptotic BECAUSE the bounded horizon strictly
2573    /// refines the asymptotic one by also terminating).
2574    #[test]
2575    fn horizon_kind_terminates_truth_table() {
2576        assert!(HorizonKind::Bounded.terminates());
2577        assert!(!HorizonKind::Asymptotic.terminates());
2578    }
2579
2580    /// LOAD-BEARING TRUTH-TABLE: `requires_metric_axes` is the
2581    /// boolean partition `Bounded ⇒ false`, `Asymptotic ⇒ true` —
2582    /// the typed image of the optionality the [`Horizon`] struct
2583    /// encodes via its three `Option<…>` fields (`metric`,
2584    /// `direction`, `healthy_rate_threshold`). The implicit
2585    /// "Asymptotic only" invariant in the field docs is now a
2586    /// checkable per-kind predicate. Pinning this table at one site
2587    /// means any future horizon-shape validator (CRD admission,
2588    /// `tatara-check` form linter, Lisp authoring-time predicate)
2589    /// reads the same projection.
2590    #[test]
2591    fn horizon_kind_requires_metric_axes_truth_table() {
2592        assert!(!HorizonKind::Bounded.requires_metric_axes());
2593        assert!(HorizonKind::Asymptotic.requires_metric_axes());
2594    }
2595
2596    /// COVERAGE CONTRACT: every variant lands in exactly one of two
2597    /// termination buckets — terminating (`Bounded`) or perpetual
2598    /// (`Asymptotic`). Pins the two buckets at their declared
2599    /// cardinalities (1, 1 — sum to `ALL.len()`) so a future variant
2600    /// lands somewhere deliberately.
2601    #[test]
2602    fn horizon_kind_buckets_cover_every_variant() {
2603        let mut terminating = 0u32;
2604        let mut perpetual = 0u32;
2605        for k in HorizonKind::ALL {
2606            if k.terminates() {
2607                terminating += 1;
2608            } else {
2609                perpetual += 1;
2610            }
2611        }
2612        assert_eq!(terminating, 1, "terminating bucket: Bounded");
2613        assert_eq!(perpetual, 1, "perpetual bucket: Asymptotic");
2614        assert_eq!(terminating + perpetual, HorizonKind::ALL.len() as u32);
2615    }
2616
2617    /// ANTISYMMETRY CONTRACT: for every variant, exactly one of
2618    /// `(terminates, requires_metric_axes)` is true — the two
2619    /// predicates carve the variants into complementary buckets
2620    /// (terminating ↔ no metric axes; perpetual ↔ requires metric
2621    /// axes). A future variant that returned `true` for both (a
2622    /// terminating horizon that nonetheless tracks an asymptotic
2623    /// metric) or `false` for both (an inert horizon with no
2624    /// termination AND no metric signal — there'd be nothing to
2625    /// observe) would fail here, forcing the author to extend
2626    /// either the predicates or the [`Horizon`] struct's
2627    /// optionality contract deliberately.
2628    #[test]
2629    fn horizon_kind_terminate_xor_requires_metric_axes() {
2630        for k in HorizonKind::ALL {
2631            assert!(
2632                k.terminates() ^ k.requires_metric_axes(),
2633                "{k:?}: terminates() XOR requires_metric_axes() must hold",
2634            );
2635        }
2636    }
2637
2638    /// DEFAULT-AGREEMENT CONTRACT: `HorizonKind::default()` returns
2639    /// `Bounded` (the variant tagged `#[default]`), AND that
2640    /// variant lands in the terminating bucket. A future
2641    /// `#[default]` rename without flipping the predicate fails
2642    /// here — `Bounded` is the canonical default for a convergence
2643    /// horizon (a point with no asymptotic axes declared should
2644    /// terminate naturally, not silently flip into a perpetual
2645    /// rate-window evaluator with zero threshold). This is also
2646    /// the same value `Horizon::default()` carries, so pinning the
2647    /// default here pins the struct-default behavior at one site.
2648    #[test]
2649    fn horizon_kind_default_is_bounded_terminates() {
2650        let k = HorizonKind::default();
2651        assert_eq!(k, HorizonKind::Bounded);
2652        assert!(k.terminates());
2653        assert!(!k.requires_metric_axes());
2654    }
2655
2656    /// HORIZON ↔ KIND AGREEMENT: every variant in `HorizonKind::ALL`
2657    /// composes with the existing [`Horizon::bounded`] /
2658    /// [`Horizon::asymptotic`] constructors to produce a `Horizon`
2659    /// whose `kind` matches AND whose `Option<…>` fields agree
2660    /// with `requires_metric_axes`. Pins the implicit contract
2661    /// between the kind discriminator and the optionality at one
2662    /// site — a future kind added without extending either the
2663    /// constructors or `requires_metric_axes` fails here before
2664    /// drifting between the typed surface and the documented
2665    /// "Asymptotic only" field invariant.
2666    #[test]
2667    fn horizon_kind_agrees_with_struct_optionality() {
2668        let bounded = Horizon::bounded();
2669        assert_eq!(bounded.kind, HorizonKind::Bounded);
2670        assert!(!bounded.kind.requires_metric_axes());
2671        assert!(bounded.metric.is_none());
2672        assert!(bounded.direction.is_none());
2673        assert!(bounded.healthy_rate_threshold.is_none());
2674
2675        let asymp = Horizon::asymptotic("p99_latency", OptimizationDirection::Minimize, 0.1);
2676        assert_eq!(asymp.kind, HorizonKind::Asymptotic);
2677        assert!(asymp.kind.requires_metric_axes());
2678        assert!(asymp.metric.is_some());
2679        assert!(asymp.direction.is_some());
2680        assert!(asymp.healthy_rate_threshold.is_some());
2681    }
2682}