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