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