1pub mod baseline;
15
16use tatara_process::classification::{
17 CalmClassification, Classification, DataClassification, Horizon, HorizonKind,
18 OptimizationDirection, SubstrateType,
19};
20
21pub trait Lattice: Sized + Clone + PartialEq {
23 fn meet(&self, other: &Self) -> Self;
25 fn join(&self, other: &Self) -> Self;
27 fn leq(&self, other: &Self) -> bool {
29 self.meet(other) == *self
30 }
31 fn bottom() -> Self;
33 fn top() -> Self;
35}
36
37impl Lattice for DataClassification {
54 fn meet(&self, other: &Self) -> Self {
55 if self.leq(other) {
56 self.clone()
57 } else {
58 other.clone()
59 }
60 }
61 fn join(&self, other: &Self) -> Self {
62 if self.leq(other) {
63 other.clone()
64 } else {
65 self.clone()
66 }
67 }
68 fn leq(&self, other: &Self) -> bool {
69 self.sensitivity_rank() <= other.sensitivity_rank()
70 }
71 fn bottom() -> Self {
72 DataClassification::Public
73 }
74 fn top() -> Self {
75 DataClassification::Pci
76 }
77}
78
79impl Lattice for SubstrateType {
83 fn meet(&self, other: &Self) -> Self {
84 if self == other {
85 self.clone()
86 } else {
87 Self::top()
88 }
89 }
90 fn join(&self, other: &Self) -> Self {
91 if self == other {
92 self.clone()
93 } else {
94 Self::bottom()
95 }
96 }
97 fn leq(&self, other: &Self) -> bool {
98 self == other || *other == Self::top()
99 }
100 fn bottom() -> Self {
101 SubstrateType::Financial
102 }
103 fn top() -> Self {
105 SubstrateType::Regulatory
106 }
107}
108
109impl Lattice for CalmClassification {
111 fn meet(&self, other: &Self) -> Self {
112 match (self, other) {
113 (Self::Monotone, _) | (_, Self::Monotone) => Self::Monotone,
114 _ => Self::NonMonotone,
115 }
116 }
117 fn join(&self, other: &Self) -> Self {
118 match (self, other) {
119 (Self::NonMonotone, _) | (_, Self::NonMonotone) => Self::NonMonotone,
120 _ => Self::Monotone,
121 }
122 }
123 fn leq(&self, other: &Self) -> bool {
124 matches!(
125 (self, other),
126 (Self::Monotone, _) | (Self::NonMonotone, Self::NonMonotone)
127 )
128 }
129 fn bottom() -> Self {
130 Self::Monotone
131 }
132 fn top() -> Self {
133 Self::NonMonotone
134 }
135}
136
137impl Lattice for Horizon {
143 fn meet(&self, other: &Self) -> Self {
144 match (self.kind, other.kind) {
145 (HorizonKind::Bounded, _) | (_, HorizonKind::Bounded) => Self::bounded(),
146 _ => self.clone(),
147 }
148 }
149 fn join(&self, other: &Self) -> Self {
150 match (self.kind, other.kind) {
151 (HorizonKind::Asymptotic, _) => self.clone(),
152 (_, HorizonKind::Asymptotic) => other.clone(),
153 _ => Self::bounded(),
154 }
155 }
156 fn leq(&self, other: &Self) -> bool {
157 matches!(
158 (self.kind, other.kind),
159 (HorizonKind::Bounded, _) | (HorizonKind::Asymptotic, HorizonKind::Asymptotic)
160 )
161 }
162 fn bottom() -> Self {
163 Self::bounded()
164 }
165 fn top() -> Self {
166 Self::asymptotic("", OptimizationDirection::Minimize, f64::MIN)
167 }
168}
169
170impl Lattice for Classification {
175 fn meet(&self, other: &Self) -> Self {
176 Self {
177 point_type: self.point_type,
179 substrate: self.substrate.meet(&other.substrate),
180 horizon: self.horizon.meet(&other.horizon),
181 calm: self.calm.meet(&other.calm),
182 data_classification: self.data_classification.meet(&other.data_classification),
183 }
184 }
185 fn join(&self, other: &Self) -> Self {
186 Self {
187 point_type: self.point_type,
188 substrate: self.substrate.join(&other.substrate),
189 horizon: self.horizon.join(&other.horizon),
190 calm: self.calm.join(&other.calm),
191 data_classification: self.data_classification.join(&other.data_classification),
192 }
193 }
194 fn leq(&self, other: &Self) -> bool {
195 self.substrate.leq(&other.substrate)
196 && self.horizon.leq(&other.horizon)
197 && self.calm.leq(&other.calm)
198 && self.data_classification.leq(&other.data_classification)
199 }
200 fn bottom() -> Self {
201 Self {
202 point_type: tatara_process::classification::ConvergencePointType::Transform,
203 substrate: SubstrateType::bottom(),
204 horizon: Horizon::bottom(),
205 calm: CalmClassification::bottom(),
206 data_classification: DataClassification::bottom(),
207 }
208 }
209 fn top() -> Self {
210 Self {
211 point_type: tatara_process::classification::ConvergencePointType::Transform,
212 substrate: SubstrateType::top(),
213 horizon: Horizon::top(),
214 calm: CalmClassification::top(),
215 data_classification: DataClassification::top(),
216 }
217 }
218}
219
220pub fn satisfies(cluster: &Classification, requires: &Classification) -> bool {
224 cluster.leq(requires)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use tatara_process::classification::ConvergencePointType;
233
234 #[test]
235 fn data_classification_total_order() {
236 assert!(DataClassification::Public.leq(&DataClassification::Internal));
237 assert!(DataClassification::Internal.leq(&DataClassification::Confidential));
238 assert!(DataClassification::Confidential.leq(&DataClassification::Pii));
239 }
240
241 #[test]
242 fn idempotent_meet() {
243 let c = Classification {
244 point_type: ConvergencePointType::Gate,
245 substrate: SubstrateType::Observability,
246 horizon: Horizon::bounded(),
247 calm: CalmClassification::Monotone,
248 data_classification: DataClassification::Internal,
249 };
250 assert_eq!(c.meet(&c), c);
251 }
252
253 #[test]
254 fn absorption() {
255 let a = Classification {
256 point_type: ConvergencePointType::Gate,
257 substrate: SubstrateType::Observability,
258 horizon: Horizon::bounded(),
259 calm: CalmClassification::Monotone,
260 data_classification: DataClassification::Internal,
261 };
262 let b = Classification {
263 point_type: ConvergencePointType::Gate,
264 substrate: SubstrateType::Observability,
265 horizon: Horizon::bounded(),
266 calm: CalmClassification::NonMonotone,
267 data_classification: DataClassification::Pii,
268 };
269 assert_eq!(a.meet(&a.join(&b)), a);
270 }
271
272 #[test]
273 fn calm_monotone_is_refinement() {
274 assert!(CalmClassification::Monotone.leq(&CalmClassification::NonMonotone));
275 assert!(!CalmClassification::NonMonotone.leq(&CalmClassification::Monotone));
276 }
277
278 #[test]
279 fn substrate_flat_antichain() {
280 let s = SubstrateType::Compute;
281 let t = SubstrateType::Storage;
282 assert!(!s.leq(&t));
283 assert!(!t.leq(&s));
284 assert_eq!(s.meet(&t), SubstrateType::Regulatory);
286 }
287
288 fn bounded_classification(data: DataClassification) -> Classification {
291 Classification {
292 point_type: ConvergencePointType::Gate,
293 substrate: SubstrateType::Observability,
294 horizon: Horizon::bounded(),
295 calm: CalmClassification::Monotone,
296 data_classification: data,
297 }
298 }
299
300 #[test]
301 fn satisfies_is_true_when_cluster_is_as_refined_as_requirement() {
302 let cluster = bounded_classification(DataClassification::Public);
305 let requirement_public = bounded_classification(DataClassification::Public);
306 let requirement_internal = bounded_classification(DataClassification::Internal);
307 assert!(satisfies(&cluster, &requirement_public));
308 assert!(satisfies(&cluster, &requirement_internal));
309 }
310
311 #[test]
312 fn satisfies_is_false_when_cluster_is_less_refined_than_requirement() {
313 let cluster = bounded_classification(DataClassification::Confidential);
318 let requirement = bounded_classification(DataClassification::Public);
319 assert!(!satisfies(&cluster, &requirement));
320 }
321
322 #[test]
323 fn satisfies_equal_always_true() {
324 let c = bounded_classification(DataClassification::Pii);
327 assert!(satisfies(&c, &c));
328 }
329
330 use proptest::prelude::*;
333
334 #[test]
348 fn data_classification_leq_uses_typed_rank() {
349 for a in DataClassification::ALL {
350 for b in DataClassification::ALL {
351 assert_eq!(
352 a.leq(&b),
353 a.sensitivity_rank() <= b.sensitivity_rank(),
354 "Lattice::leq for ({a:?}, {b:?}) disagrees with sensitivity_rank — \
355 the lattice ordering has drifted away from the typed rank \
356 projection that seals it",
357 );
358 }
359 }
360 }
361
362 fn from_all<T: Copy + std::fmt::Debug + 'static>(
369 all: &'static [T],
370 ) -> impl Strategy<Value = T> {
371 (0..all.len()).prop_map(move |i| all[i])
372 }
373
374 fn any_data_class() -> impl Strategy<Value = DataClassification> {
375 from_all(&DataClassification::ALL)
376 }
377
378 fn any_calm() -> impl Strategy<Value = CalmClassification> {
379 prop_oneof![
380 Just(CalmClassification::Monotone),
381 Just(CalmClassification::NonMonotone),
382 ]
383 }
384
385 proptest! {
386 #[test]
399 fn data_class_idempotent(a in any_data_class()) {
400 prop_assert_eq!(a.meet(&a), a);
401 prop_assert_eq!(a.join(&a), a);
402 }
403
404 #[test]
405 fn data_class_commutative(a in any_data_class(), b in any_data_class()) {
406 prop_assert_eq!(a.meet(&b), b.meet(&a));
407 prop_assert_eq!(a.join(&b), b.join(&a));
408 }
409
410 #[test]
411 fn data_class_associative(
412 a in any_data_class(),
413 b in any_data_class(),
414 c in any_data_class(),
415 ) {
416 prop_assert_eq!(a.meet(&b).meet(&c), a.meet(&b.meet(&c)));
417 prop_assert_eq!(a.join(&b).join(&c), a.join(&b.join(&c)));
418 }
419
420 #[test]
421 fn data_class_absorption(a in any_data_class(), b in any_data_class()) {
422 prop_assert_eq!(a.meet(&a.join(&b)), a);
424 prop_assert_eq!(a.join(&a.meet(&b)), a);
426 }
427
428 #[test]
429 fn data_class_leq_agrees_with_meet(a in any_data_class(), b in any_data_class()) {
430 prop_assert_eq!(a.leq(&b), a.meet(&b) == a);
433 }
434
435 #[test]
436 fn data_class_leq_agrees_with_join(a in any_data_class(), b in any_data_class()) {
437 prop_assert_eq!(a.leq(&b), a.join(&b) == b);
439 }
440
441 #[test]
442 fn data_class_bottom_is_universal_min(a in any_data_class()) {
443 prop_assert!(DataClassification::bottom().leq(&a));
445 }
446
447 #[test]
448 fn data_class_top_is_universal_max(a in any_data_class()) {
449 prop_assert!(a.leq(&DataClassification::top()));
451 }
452
453 #[test]
456 fn calm_idempotent(a in any_calm()) {
457 prop_assert_eq!(a.meet(&a), a);
458 prop_assert_eq!(a.join(&a), a);
459 }
460
461 #[test]
462 fn calm_commutative(a in any_calm(), b in any_calm()) {
463 prop_assert_eq!(a.meet(&b), b.meet(&a));
464 prop_assert_eq!(a.join(&b), b.join(&a));
465 }
466
467 #[test]
468 fn calm_associative(a in any_calm(), b in any_calm(), c in any_calm()) {
469 prop_assert_eq!(a.meet(&b).meet(&c), a.meet(&b.meet(&c)));
470 prop_assert_eq!(a.join(&b).join(&c), a.join(&b.join(&c)));
471 }
472
473 #[test]
474 fn calm_absorption(a in any_calm(), b in any_calm()) {
475 prop_assert_eq!(a.meet(&a.join(&b)), a);
476 prop_assert_eq!(a.join(&a.meet(&b)), a);
477 }
478
479 #[test]
480 fn calm_leq_agrees_with_meet(a in any_calm(), b in any_calm()) {
481 prop_assert_eq!(a.leq(&b), a.meet(&b) == a);
482 }
483
484 #[test]
485 fn calm_leq_agrees_with_join(a in any_calm(), b in any_calm()) {
486 prop_assert_eq!(a.leq(&b), a.join(&b) == b);
487 }
488
489 #[test]
490 fn calm_bottom_is_monotone(a in any_calm()) {
491 prop_assert!(CalmClassification::bottom().leq(&a));
492 prop_assert_eq!(CalmClassification::bottom(), CalmClassification::Monotone);
493 }
494
495 #[test]
496 fn calm_top_is_nonmonotone(a in any_calm()) {
497 prop_assert!(a.leq(&CalmClassification::top()));
498 prop_assert_eq!(CalmClassification::top(), CalmClassification::NonMonotone);
499 }
500 }
501}