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