1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use crate::ids::{ClaimEventId, ClaimId, SourceId, TimestampMillis};
7use crate::model::{
8 Authority, AuthorityLevel, ClaimEvent, ClaimEventKind, ClaimValue, EntityRef, Predicate, Ttl,
9};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
12pub enum ClaimLifecycle {
13 Active,
14 Contested,
15 Superseded,
16 Expired,
17 Retracted,
18}
19
20impl ClaimLifecycle {
21 #[must_use]
22 pub const fn is_terminal(self) -> bool {
23 matches!(self, Self::Superseded | Self::Expired | Self::Retracted)
24 }
25}
26
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
31pub struct ClaimState {
32 pub claim_id: ClaimId,
33 pub subject: EntityRef,
34 pub predicate: Predicate,
35 pub value: ClaimValue,
36 pub authority: Authority,
39 pub ttl: Ttl,
42 pub freshness_anchor: TimestampMillis,
45 pub lifecycle: ClaimLifecycle,
46 pub created_at: TimestampMillis,
47 pub updated_at: TimestampMillis,
48 pub last_event_id: ClaimEventId,
49 pub superseded_by: Option<ClaimId>,
50 pub contradicted_by: Vec<ClaimId>,
51 pub evidence_count: usize,
52 pub corroborating_sources: BTreeMap<SourceId, AuthorityLevel>,
59}
60
61impl ClaimState {
62 #[must_use]
67 pub fn corroboration(&self) -> usize {
68 self.corroborating_sources.len()
69 }
70
71 #[must_use]
75 pub fn corroboration_at_or_above(&self, min: AuthorityLevel) -> usize {
76 self.corroborating_sources
77 .values()
78 .filter(|&&level| level >= min)
79 .count()
80 }
81
82 #[must_use]
84 pub fn expires_at(&self) -> Option<TimestampMillis> {
85 self.ttl.expires_at(self.freshness_anchor)
86 }
87
88 #[must_use]
92 pub fn is_expired_at(&self, now: TimestampMillis) -> bool {
93 self.ttl.is_expired_at(self.freshness_anchor, now)
94 }
95}
96
97pub fn apply_event(
98 current: Option<ClaimState>,
99 event: &ClaimEvent,
100) -> Result<ClaimState, TransitionError> {
101 event.validate().map_err(TransitionError::InvalidEvent)?;
102
103 match current {
104 None => apply_initial_event(event),
105 Some(state) => apply_next_event(state, event),
106 }
107}
108
109fn apply_initial_event(event: &ClaimEvent) -> Result<ClaimState, TransitionError> {
110 if !matches!(event.kind, ClaimEventKind::Asserted) {
111 return Err(TransitionError::MissingInitialAssertion);
112 }
113
114 let value = event
115 .value
116 .clone()
117 .ok_or(TransitionError::MissingInitialAssertion)?;
118
119 let freshness_anchor = event
120 .valid_from
121 .or(event.observed_at)
122 .unwrap_or(event.provenance.recorded_at);
123
124 Ok(ClaimState {
125 claim_id: event.claim_id.clone(),
126 subject: event.subject.clone(),
127 predicate: event.predicate.clone(),
128 value,
129 authority: event.authority.clone(),
130 ttl: event.ttl.clone(),
131 freshness_anchor,
132 lifecycle: ClaimLifecycle::Active,
133 created_at: event.provenance.recorded_at,
134 updated_at: event.provenance.recorded_at,
135 last_event_id: event.event_id.clone(),
136 superseded_by: None,
137 contradicted_by: Vec::new(),
138 evidence_count: event.evidence.len(),
139 corroborating_sources: BTreeMap::from([(
140 event.provenance.source.clone(),
141 event.authority.level,
142 )]),
143 })
144}
145
146fn apply_next_event(
147 mut state: ClaimState,
148 event: &ClaimEvent,
149) -> Result<ClaimState, TransitionError> {
150 if state.claim_id != event.claim_id {
151 return Err(TransitionError::ClaimIdMismatch);
152 }
153
154 if state.subject != event.subject || state.predicate != event.predicate {
155 return Err(TransitionError::ClaimShapeMismatch);
156 }
157
158 if state.lifecycle.is_terminal()
159 && !matches!(
160 event.kind,
161 ClaimEventKind::Retrieved { .. } | ClaimEventKind::UsedInDecision { .. }
162 )
163 {
164 return Err(TransitionError::TerminalStateMutation(state.lifecycle));
165 }
166
167 match &event.kind {
168 ClaimEventKind::Asserted => return Err(TransitionError::DuplicateAssertion),
169 ClaimEventKind::Reinforced { .. } => {
170 if let Some(value) = &event.value
171 && value != &state.value
172 {
173 return Err(TransitionError::ReinforcementValueMismatch);
174 }
175 state.evidence_count += event.evidence.len();
176 state
179 .corroborating_sources
180 .entry(event.provenance.source.clone())
181 .and_modify(|level| *level = (*level).max(event.authority.level))
182 .or_insert(event.authority.level);
183 }
184 ClaimEventKind::Contradicted { by, .. } => {
185 if state.authority.level == AuthorityLevel::Canonical {
190 return Err(TransitionError::CanonicalContradiction);
191 }
192 state.lifecycle = ClaimLifecycle::Contested;
193 if !state.contradicted_by.contains(by) {
194 state.contradicted_by.push(by.clone());
195 }
196 }
197 ClaimEventKind::Superseded { by, .. } => {
198 if event.authority.level < state.authority.level {
203 return Err(TransitionError::InsufficientAuthority {
204 incumbent: state.authority.level,
205 challenger: event.authority.level,
206 });
207 }
208 state.lifecycle = ClaimLifecycle::Superseded;
209 state.superseded_by = Some(by.clone());
210 }
211 ClaimEventKind::Expired { .. } => {
212 if event.authority.level < state.authority.level {
216 return Err(TransitionError::InsufficientAuthority {
217 incumbent: state.authority.level,
218 challenger: event.authority.level,
219 });
220 }
221 state.lifecycle = ClaimLifecycle::Expired;
222 }
223 ClaimEventKind::Retracted { .. } => {
224 if event.authority.level < state.authority.level {
232 return Err(TransitionError::InsufficientAuthority {
233 incumbent: state.authority.level,
234 challenger: event.authority.level,
235 });
236 }
237 state.lifecycle = ClaimLifecycle::Retracted;
238 }
239 ClaimEventKind::Retrieved { .. } | ClaimEventKind::UsedInDecision { .. } => {}
240 }
241
242 state.updated_at = event.provenance.recorded_at;
243 state.last_event_id = event.event_id.clone();
244 Ok(state)
245}
246
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub enum TransitionError {
249 InvalidEvent(crate::model::ValidationError),
250 MissingInitialAssertion,
251 DuplicateAssertion,
252 ClaimIdMismatch,
253 ClaimShapeMismatch,
254 ReinforcementValueMismatch,
255 TerminalStateMutation(ClaimLifecycle),
256 InsufficientAuthority {
263 incumbent: AuthorityLevel,
264 challenger: AuthorityLevel,
265 },
266 CanonicalContradiction,
270}
271
272impl fmt::Display for TransitionError {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match self {
275 Self::InvalidEvent(error) => write!(f, "invalid event: {error}"),
276 Self::MissingInitialAssertion => {
277 f.write_str("claim stream must start with claim.asserted")
278 }
279 Self::DuplicateAssertion => {
280 f.write_str("claim stream cannot assert the same claim twice")
281 }
282 Self::ClaimIdMismatch => f.write_str("event claim_id does not match current state"),
283 Self::ClaimShapeMismatch => {
284 f.write_str("event subject or predicate does not match current state")
285 }
286 Self::ReinforcementValueMismatch => {
287 f.write_str("claim.reinforced cannot change the claim value")
288 }
289 Self::TerminalStateMutation(state) => {
290 write!(f, "cannot mutate terminal claim state {state:?}")
291 }
292 Self::InsufficientAuthority {
293 incumbent,
294 challenger,
295 } => write!(
296 f,
297 "insufficient authority: {challenger:?} may not override or remove an incumbent of {incumbent:?}"
298 ),
299 Self::CanonicalContradiction => {
300 f.write_str("a canonical claim cannot be contradicted; supersede it with equal authority instead")
301 }
302 }
303 }
304}
305
306impl std::error::Error for TransitionError {}
307
308#[cfg(test)]
309mod tests {
310 use crate::ids::{ActorId, ClaimEventId, ClaimId, EvidenceId, SourceId, TimestampMillis};
311 use crate::model::{
312 Authority, AuthorityLevel, ClaimEvent, ClaimEventKind, ClaimValue, Confidence,
313 ContradictionBasis, EntityRef, Evidence, EvidenceKind, ExpirationReason, Predicate,
314 Provenance, RetractionReason, SupersessionReason, Ttl,
315 };
316
317 use super::{ClaimLifecycle, TransitionError, apply_event};
318
319 const ALL_LEVELS: [AuthorityLevel; 5] = [
320 AuthorityLevel::Unknown,
321 AuthorityLevel::Low,
322 AuthorityLevel::Medium,
323 AuthorityLevel::High,
324 AuthorityLevel::Canonical,
325 ];
326
327 fn with_authority(mut event: ClaimEvent, level: AuthorityLevel) -> ClaimEvent {
328 event.authority = Authority {
329 level,
330 issuer: None,
331 scope: None,
332 };
333 event
334 }
335
336 fn asserted(level: AuthorityLevel) -> ClaimEvent {
337 with_authority(
338 base_event(
339 ClaimEventKind::Asserted,
340 "event:1",
341 Some(ClaimValue::Text("postgres".to_string())),
342 ),
343 level,
344 )
345 }
346
347 fn supersede(event_id: &str, level: AuthorityLevel) -> ClaimEvent {
348 with_authority(
349 base_event(
350 ClaimEventKind::Superseded {
351 by: claim_id("claim:2"),
352 reason: SupersessionReason::NewerObservation,
353 },
354 event_id,
355 None,
356 ),
357 level,
358 )
359 }
360
361 fn retract(event_id: &str, level: AuthorityLevel) -> ClaimEvent {
362 with_authority(
363 base_event(
364 ClaimEventKind::Retracted {
365 reason: RetractionReason::UserDeleted,
366 },
367 event_id,
368 None,
369 ),
370 level,
371 )
372 }
373
374 fn expire(event_id: &str, level: AuthorityLevel) -> ClaimEvent {
375 with_authority(
376 base_event(
377 ClaimEventKind::Expired {
378 reason: ExpirationReason::PolicyRetention,
379 },
380 event_id,
381 None,
382 ),
383 level,
384 )
385 }
386
387 fn claim_id(value: &str) -> ClaimId {
388 ClaimId::new(value).expect("valid claim id")
389 }
390
391 fn base_event(kind: ClaimEventKind, event_id: &str, value: Option<ClaimValue>) -> ClaimEvent {
392 ClaimEvent {
393 event_id: ClaimEventId::new(event_id).expect("valid event id"),
394 claim_id: claim_id("claim:1"),
395 kind,
396 subject: EntityRef::new("repo", "dent8").expect("valid entity"),
397 predicate: Predicate::new("uses_database").expect("valid predicate"),
398 value,
399 confidence: Confidence::from_millis(900).expect("valid confidence"),
400 authority: Authority::unknown(),
401 ttl: Ttl::Never,
402 provenance: Provenance {
403 source: SourceId::new("source:test").expect("valid source"),
404 actor: ActorId::new("actor:test").expect("valid actor"),
405 tool: Some("unit-test".to_string()),
406 run_id: None,
407 input_digest: None,
408 recorded_at: TimestampMillis::from_unix_millis(1),
409 attestation: None,
410 },
411 evidence: vec![Evidence {
412 id: EvidenceId::new("evidence:1").expect("valid evidence id"),
413 kind: EvidenceKind::UserStatement,
414 locator: "test".to_string(),
415 digest: None,
416 summary: Some("test evidence".to_string()),
417 }],
418 observed_at: None,
419 valid_from: None,
420 }
421 }
422
423 #[test]
424 fn assertion_creates_active_state() {
425 let event = base_event(
426 ClaimEventKind::Asserted,
427 "event:1",
428 Some(ClaimValue::Text("postgres".to_string())),
429 );
430
431 let state = apply_event(None, &event).expect("assertion applies");
432
433 assert_eq!(state.lifecycle, ClaimLifecycle::Active);
434 assert_eq!(state.evidence_count, 1);
435 }
436
437 #[test]
438 fn duplicate_assertion_is_rejected() {
439 let event = base_event(
440 ClaimEventKind::Asserted,
441 "event:1",
442 Some(ClaimValue::Text("postgres".to_string())),
443 );
444 let state = apply_event(None, &event).expect("assertion applies");
445
446 let error = apply_event(Some(state), &event).expect_err("duplicate rejected");
447
448 assert_eq!(error, TransitionError::DuplicateAssertion);
449 }
450
451 #[test]
452 fn contradiction_marks_claim_contested() {
453 let asserted = base_event(
454 ClaimEventKind::Asserted,
455 "event:1",
456 Some(ClaimValue::Text("postgres".to_string())),
457 );
458 let state = apply_event(None, &asserted).expect("assertion applies");
459 let contradicted = base_event(
460 ClaimEventKind::Contradicted {
461 by: claim_id("claim:2"),
462 basis: ContradictionBasis::SamePredicateDifferentValue,
463 },
464 "event:2",
465 None,
466 );
467
468 let state = apply_event(Some(state), &contradicted).expect("contradiction applies");
469
470 assert_eq!(state.lifecycle, ClaimLifecycle::Contested);
471 assert_eq!(state.contradicted_by, vec![claim_id("claim:2")]);
472 }
473
474 #[test]
475 fn terminal_state_rejects_lifecycle_mutation() {
476 let asserted = base_event(
477 ClaimEventKind::Asserted,
478 "event:1",
479 Some(ClaimValue::Text("postgres".to_string())),
480 );
481 let state = apply_event(None, &asserted).expect("assertion applies");
482 let superseded = base_event(
483 ClaimEventKind::Superseded {
484 by: claim_id("claim:2"),
485 reason: SupersessionReason::NewerObservation,
486 },
487 "event:2",
488 None,
489 );
490 let state = apply_event(Some(state), &superseded).expect("supersession applies");
491 let reinforced = base_event(
492 ClaimEventKind::Reinforced {
493 by: claim_id("claim:3"),
494 },
495 "event:3",
496 Some(ClaimValue::Text("postgres".to_string())),
497 );
498
499 let error = apply_event(Some(state), &reinforced).expect_err("terminal mutation rejected");
500
501 assert_eq!(
502 error,
503 TransitionError::TerminalStateMutation(ClaimLifecycle::Superseded)
504 );
505 }
506
507 #[test]
508 fn retrieval_does_not_change_lifecycle() {
509 let asserted = base_event(
510 ClaimEventKind::Asserted,
511 "event:1",
512 Some(ClaimValue::Text("postgres".to_string())),
513 );
514 let state = apply_event(None, &asserted).expect("assertion applies");
515 let retrieved = base_event(
516 ClaimEventKind::Retrieved {
517 purpose: "context".to_string(),
518 },
519 "event:2",
520 None,
521 );
522
523 let state = apply_event(Some(state), &retrieved).expect("retrieval applies");
524
525 assert_eq!(state.lifecycle, ClaimLifecycle::Active);
526 }
527
528 #[test]
529 fn lower_authority_supersession_is_rejected() {
530 let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("assertion applies");
531
532 let error = apply_event(Some(state), &supersede("event:2", AuthorityLevel::Low))
533 .expect_err("low-authority supersession rejected");
534
535 assert_eq!(
536 error,
537 TransitionError::InsufficientAuthority {
538 incumbent: AuthorityLevel::High,
539 challenger: AuthorityLevel::Low,
540 }
541 );
542 }
543
544 #[test]
545 fn equal_authority_supersession_succeeds() {
546 let state =
547 apply_event(None, &asserted(AuthorityLevel::Medium)).expect("assertion applies");
548
549 let state = apply_event(Some(state), &supersede("event:2", AuthorityLevel::Medium))
550 .expect("equal-authority supersession applies");
551
552 assert_eq!(state.lifecycle, ClaimLifecycle::Superseded);
553 }
554
555 #[test]
556 fn higher_authority_supersession_succeeds() {
557 let state = apply_event(None, &asserted(AuthorityLevel::Low)).expect("assertion applies");
558
559 let state = apply_event(
560 Some(state),
561 &supersede("event:2", AuthorityLevel::Canonical),
562 )
563 .expect("higher-authority supersession applies");
564
565 assert_eq!(state.lifecycle, ClaimLifecycle::Superseded);
566 }
567
568 #[test]
569 fn contradicting_a_canonical_claim_hard_alarms() {
570 let state =
571 apply_event(None, &asserted(AuthorityLevel::Canonical)).expect("assertion applies");
572 let contradicted = base_event(
573 ClaimEventKind::Contradicted {
574 by: claim_id("claim:2"),
575 basis: ContradictionBasis::SamePredicateDifferentValue,
576 },
577 "event:2",
578 None,
579 );
580
581 let error = apply_event(Some(state), &contradicted)
582 .expect_err("canonical contradiction hard-alarms");
583
584 assert_eq!(error, TransitionError::CanonicalContradiction);
585 }
586
587 #[test]
588 fn contradicting_a_non_canonical_claim_still_contests() {
589 let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("assertion applies");
590 let contradicted = base_event(
591 ClaimEventKind::Contradicted {
592 by: claim_id("claim:2"),
593 basis: ContradictionBasis::SamePredicateDifferentValue,
594 },
595 "event:2",
596 None,
597 );
598
599 let state =
600 apply_event(Some(state), &contradicted).expect("non-canonical contradiction applies");
601
602 assert_eq!(state.lifecycle, ClaimLifecycle::Contested);
603 }
604
605 #[test]
612 fn authority_monotone_supersession_and_non_resurrection() {
613 for incumbent in ALL_LEVELS {
614 for challenger in ALL_LEVELS {
615 let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
616 let result = apply_event(Some(state), &supersede("event:2", challenger));
617
618 if challenger < incumbent {
619 assert_eq!(
620 result,
621 Err(TransitionError::InsufficientAuthority {
622 incumbent,
623 challenger,
624 }),
625 "challenger {challenger:?} should not supersede incumbent {incumbent:?}",
626 );
627 continue;
628 }
629
630 let superseded = result.expect("non-under-ranking supersession applies");
631 assert!(
632 superseded.lifecycle.is_terminal(),
633 "supersession should make the claim terminal",
634 );
635
636 let resurrect = supersede("event:3", AuthorityLevel::Canonical);
639 let error = apply_event(Some(superseded), &resurrect)
640 .expect_err("terminal claim cannot be resurrected");
641 assert_eq!(
642 error,
643 TransitionError::TerminalStateMutation(ClaimLifecycle::Superseded)
644 );
645 }
646 }
647 }
648
649 #[test]
655 fn authority_monotone_retraction_and_non_resurrection() {
656 for incumbent in ALL_LEVELS {
657 for challenger in ALL_LEVELS {
658 let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
659 let result = apply_event(Some(state), &retract("event:2", challenger));
660
661 if challenger < incumbent {
662 assert_eq!(
663 result,
664 Err(TransitionError::InsufficientAuthority {
665 incumbent,
666 challenger,
667 }),
668 "retractor {challenger:?} should not retract incumbent {incumbent:?}",
669 );
670 continue;
671 }
672
673 let retracted = result.expect("non-under-ranking retraction applies");
674 assert_eq!(retracted.lifecycle, ClaimLifecycle::Retracted);
675 assert!(retracted.lifecycle.is_terminal());
676
677 let resurrect = supersede("event:3", AuthorityLevel::Canonical);
678 let error = apply_event(Some(retracted), &resurrect)
679 .expect_err("terminal claim cannot be resurrected");
680 assert_eq!(
681 error,
682 TransitionError::TerminalStateMutation(ClaimLifecycle::Retracted)
683 );
684 }
685 }
686 }
687
688 #[test]
692 fn authority_monotone_expiration_and_non_resurrection() {
693 for incumbent in ALL_LEVELS {
694 for challenger in ALL_LEVELS {
695 let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
696 let result = apply_event(Some(state), &expire("event:2", challenger));
697
698 if challenger < incumbent {
699 assert_eq!(
700 result,
701 Err(TransitionError::InsufficientAuthority {
702 incumbent,
703 challenger,
704 }),
705 "expirer {challenger:?} should not expire incumbent {incumbent:?}",
706 );
707 continue;
708 }
709
710 let expired = result.expect("non-under-ranking expiration applies");
711 assert_eq!(expired.lifecycle, ClaimLifecycle::Expired);
712 assert!(expired.lifecycle.is_terminal());
713
714 let resurrect = supersede("event:3", AuthorityLevel::Canonical);
715 let error = apply_event(Some(expired), &resurrect)
716 .expect_err("terminal claim cannot be resurrected");
717 assert_eq!(
718 error,
719 TransitionError::TerminalStateMutation(ClaimLifecycle::Expired)
720 );
721 }
722 }
723 }
724}
725
726#[cfg(kani)]
732mod proofs {
733 use crate::ids::{ActorId, ClaimEventId, ClaimId, EvidenceId, SourceId, TimestampMillis};
734 use crate::model::{
735 Authority, AuthorityLevel, ClaimEvent, ClaimEventKind, ClaimValue, Confidence, EntityRef,
736 Evidence, EvidenceKind, ExpirationReason, Predicate, Provenance, SupersessionReason, Ttl,
737 };
738
739 use super::apply_event;
740
741 fn level_from(n: u8) -> AuthorityLevel {
742 match n {
743 0 => AuthorityLevel::Unknown,
744 1 => AuthorityLevel::Low,
745 2 => AuthorityLevel::Medium,
746 3 => AuthorityLevel::High,
747 _ => AuthorityLevel::Canonical,
748 }
749 }
750
751 fn event(
752 kind: ClaimEventKind,
753 event_id: &str,
754 value: Option<ClaimValue>,
755 level: AuthorityLevel,
756 ) -> ClaimEvent {
757 ClaimEvent {
758 event_id: ClaimEventId::new(event_id).unwrap(),
759 claim_id: ClaimId::new("claim:1").unwrap(),
760 kind,
761 subject: EntityRef::new("repo", "dent8").unwrap(),
762 predicate: Predicate::new("uses_database").unwrap(),
763 value,
764 confidence: Confidence::from_millis(900).unwrap(),
765 authority: Authority {
766 level,
767 issuer: None,
768 scope: None,
769 },
770 ttl: Ttl::Never,
771 provenance: Provenance {
772 source: SourceId::new("source:test").unwrap(),
773 actor: ActorId::new("actor:test").unwrap(),
774 tool: None,
775 run_id: None,
776 input_digest: None,
777 recorded_at: TimestampMillis::from_unix_millis(1),
778 attestation: None,
779 },
780 evidence: vec![Evidence {
781 id: EvidenceId::new("evidence:1").unwrap(),
782 kind: EvidenceKind::UserStatement,
783 locator: "x".to_string(),
784 digest: None,
785 summary: None,
786 }],
787 observed_at: None,
788 valid_from: None,
789 }
790 }
791
792 #[kani::proof]
793 fn supersession_is_authority_monotone_and_non_resurrecting() {
794 let a: u8 = kani::any();
795 let b: u8 = kani::any();
796 kani::assume(a < 5);
797 kani::assume(b < 5);
798 let incumbent = level_from(a);
799 let challenger = level_from(b);
800
801 let asserted = event(
802 ClaimEventKind::Asserted,
803 "event:1",
804 Some(ClaimValue::Text("postgres".to_string())),
805 incumbent,
806 );
807 let state = apply_event(None, &asserted).unwrap();
808
809 let superseded = event(
810 ClaimEventKind::Superseded {
811 by: ClaimId::new("claim:2").unwrap(),
812 reason: SupersessionReason::NewerObservation,
813 },
814 "event:2",
815 None,
816 challenger,
817 );
818
819 match apply_event(Some(state), &superseded) {
820 Ok(next) => {
821 assert!(challenger >= incumbent);
824 assert!(next.lifecycle.is_terminal());
825
826 let resurrect = event(
828 ClaimEventKind::Superseded {
829 by: ClaimId::new("claim:3").unwrap(),
830 reason: SupersessionReason::NewerObservation,
831 },
832 "event:3",
833 None,
834 AuthorityLevel::Canonical,
835 );
836 assert!(apply_event(Some(next), &resurrect).is_err());
837 }
838 Err(_) => {
839 assert!(challenger < incumbent);
841 }
842 }
843 }
844
845 #[kani::proof]
846 fn retraction_is_authority_monotone_and_non_resurrecting() {
847 use crate::model::RetractionReason;
848
849 let a: u8 = kani::any();
850 let b: u8 = kani::any();
851 kani::assume(a < 5);
852 kani::assume(b < 5);
853 let incumbent = level_from(a);
854 let challenger = level_from(b);
855
856 let asserted = event(
857 ClaimEventKind::Asserted,
858 "event:1",
859 Some(ClaimValue::Text("postgres".to_string())),
860 incumbent,
861 );
862 let state = apply_event(None, &asserted).unwrap();
863
864 let retracted = event(
865 ClaimEventKind::Retracted {
866 reason: RetractionReason::UserDeleted,
867 },
868 "event:2",
869 None,
870 challenger,
871 );
872
873 match apply_event(Some(state), &retracted) {
874 Ok(next) => {
875 assert!(challenger >= incumbent);
876 assert!(next.lifecycle.is_terminal());
877
878 let resurrect = event(
879 ClaimEventKind::Superseded {
880 by: ClaimId::new("claim:3").unwrap(),
881 reason: SupersessionReason::NewerObservation,
882 },
883 "event:3",
884 None,
885 AuthorityLevel::Canonical,
886 );
887 assert!(apply_event(Some(next), &resurrect).is_err());
888 }
889 Err(_) => {
890 assert!(challenger < incumbent);
891 }
892 }
893 }
894
895 #[kani::proof]
896 fn expiration_is_authority_monotone_and_non_resurrecting() {
897 let a: u8 = kani::any();
898 let b: u8 = kani::any();
899 kani::assume(a < 5);
900 kani::assume(b < 5);
901 let incumbent = level_from(a);
902 let challenger = level_from(b);
903
904 let asserted = event(
905 ClaimEventKind::Asserted,
906 "event:1",
907 Some(ClaimValue::Text("postgres".to_string())),
908 incumbent,
909 );
910 let state = apply_event(None, &asserted).unwrap();
911
912 let expired = event(
913 ClaimEventKind::Expired {
914 reason: ExpirationReason::PolicyRetention,
915 },
916 "event:2",
917 None,
918 challenger,
919 );
920
921 match apply_event(Some(state), &expired) {
922 Ok(next) => {
923 assert!(challenger >= incumbent);
924 assert!(next.lifecycle.is_terminal());
925
926 let resurrect = event(
927 ClaimEventKind::Superseded {
928 by: ClaimId::new("claim:3").unwrap(),
929 reason: SupersessionReason::NewerObservation,
930 },
931 "event:3",
932 None,
933 AuthorityLevel::Canonical,
934 );
935 assert!(apply_event(Some(next), &resurrect).is_err());
936 }
937 Err(_) => {
938 assert!(challenger < incumbent);
939 }
940 }
941 }
942}