Skip to main content

tatara_process/
classification.rs

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