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}