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