use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::ids::{FactEventId, FactId, SourceId, TimestampMillis};
use crate::model::{
Authority, AuthorityLevel, FactEvent, FactEventKind, FactValue, Predicate, Subject, Ttl,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum FactLifecycle {
Active,
Contested,
Superseded,
Expired,
Retracted,
}
impl FactLifecycle {
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Superseded | Self::Expired | Self::Retracted)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FactState {
pub fact_id: FactId,
pub subject: Subject,
pub predicate: Predicate,
pub value: FactValue,
pub authority: Authority,
pub ttl: Ttl,
pub freshness_anchor: TimestampMillis,
#[serde(default)]
pub valid_from: Option<TimestampMillis>,
#[serde(default)]
pub valid_to: Option<TimestampMillis>,
pub lifecycle: FactLifecycle,
pub created_at: TimestampMillis,
pub updated_at: TimestampMillis,
pub last_event_id: FactEventId,
pub superseded_by: Option<FactId>,
pub contradicted_by: Vec<FactId>,
pub evidence_count: usize,
pub corroborating_sources: BTreeMap<SourceId, AuthorityLevel>,
#[serde(default)]
pub survived_challenges: BTreeMap<SourceId, AuthorityLevel>,
}
impl FactState {
#[must_use]
pub fn corroboration(&self) -> usize {
self.corroborating_sources.len()
}
#[must_use]
pub fn corroboration_at_or_above(&self, min: AuthorityLevel) -> usize {
self.corroborating_sources
.values()
.filter(|&&level| level >= min)
.count()
}
#[must_use]
pub fn survived_challenge_count(&self) -> usize {
self.survived_challenges.len()
}
#[must_use]
pub fn survived_challenges_at_or_above(&self, min: AuthorityLevel) -> usize {
self.survived_challenges
.values()
.filter(|&&level| level >= min)
.count()
}
#[must_use]
pub fn earned_entrenchment_at_or_above(&self, min: AuthorityLevel) -> usize {
self.corroboration_at_or_above(min) + self.survived_challenges_at_or_above(min)
}
#[must_use]
pub fn expires_at(&self) -> Option<TimestampMillis> {
match (self.ttl.expires_at(self.freshness_anchor), self.valid_to) {
(Some(ttl), Some(valid_to)) => Some(ttl.min(valid_to)),
(ttl, valid_to) => ttl.or(valid_to),
}
}
#[must_use]
pub fn is_not_yet_valid_at(&self, now: TimestampMillis) -> bool {
self.valid_from.is_some_and(|from| now < from)
}
#[must_use]
pub fn is_fresh_at(&self, now: TimestampMillis) -> bool {
!self.is_not_yet_valid_at(now) && !self.is_expired_at(now)
}
#[must_use]
pub fn is_expired_at(&self, now: TimestampMillis) -> bool {
self.expires_at()
.is_some_and(|expires_at| expires_at <= now)
}
}
pub fn apply_event(
current: Option<FactState>,
event: &FactEvent,
) -> Result<FactState, TransitionError> {
event.validate().map_err(TransitionError::InvalidEvent)?;
match current {
None => apply_initial_event(event),
Some(state) => apply_next_event(state, event),
}
}
fn apply_initial_event(event: &FactEvent) -> Result<FactState, TransitionError> {
if !matches!(event.kind, FactEventKind::Asserted) {
return Err(TransitionError::MissingInitialAssertion);
}
let value = event
.value
.clone()
.ok_or(TransitionError::MissingInitialAssertion)?;
let freshness_anchor = event
.valid_from
.or(event.observed_at)
.unwrap_or(event.provenance.recorded_at);
Ok(FactState {
fact_id: event.fact_id.clone(),
subject: event.subject.clone(),
predicate: event.predicate.clone(),
value,
authority: event.authority.clone(),
ttl: event.ttl.clone(),
freshness_anchor,
valid_from: event.valid_from,
valid_to: event.valid_to,
lifecycle: FactLifecycle::Active,
created_at: event.provenance.recorded_at,
updated_at: event.provenance.recorded_at,
last_event_id: event.event_id.clone(),
superseded_by: None,
contradicted_by: Vec::new(),
evidence_count: event.evidence.len(),
corroborating_sources: BTreeMap::from([(
event.provenance.source.clone(),
event.authority.level,
)]),
survived_challenges: BTreeMap::new(),
})
}
fn apply_next_event(mut state: FactState, event: &FactEvent) -> Result<FactState, TransitionError> {
if state.fact_id != event.fact_id {
return Err(TransitionError::FactIdMismatch);
}
if state.subject != event.subject || state.predicate != event.predicate {
return Err(TransitionError::FactShapeMismatch);
}
if state.lifecycle.is_terminal()
&& !matches!(
event.kind,
FactEventKind::Retrieved { .. } | FactEventKind::UsedInDecision { .. }
)
{
return Err(TransitionError::TerminalStateMutation(state.lifecycle));
}
match &event.kind {
FactEventKind::Asserted => return Err(TransitionError::DuplicateAssertion),
FactEventKind::Reinforced { .. } => {
if let Some(value) = &event.value
&& value != &state.value
{
return Err(TransitionError::ReinforcementValueMismatch);
}
state.evidence_count += event.evidence.len();
state
.corroborating_sources
.entry(event.provenance.source.clone())
.and_modify(|level| *level = (*level).max(event.authority.level))
.or_insert(event.authority.level);
}
FactEventKind::Contradicted { by, .. } => {
if state.authority.level == AuthorityLevel::Canonical {
return Err(TransitionError::CanonicalContradiction);
}
state.lifecycle = FactLifecycle::Contested;
if !state.contradicted_by.contains(by) {
state.contradicted_by.push(by.clone());
}
}
FactEventKind::Superseded { by, .. } => {
if event.authority.level < state.authority.level {
return Err(TransitionError::InsufficientAuthority {
incumbent: state.authority.level,
challenger: event.authority.level,
});
}
state.lifecycle = FactLifecycle::Superseded;
state.superseded_by = Some(by.clone());
}
FactEventKind::Expired { .. } => {
if event.authority.level < state.authority.level {
return Err(TransitionError::InsufficientAuthority {
incumbent: state.authority.level,
challenger: event.authority.level,
});
}
state.lifecycle = FactLifecycle::Expired;
}
FactEventKind::Retracted { .. } => {
if event.authority.level < state.authority.level {
return Err(TransitionError::InsufficientAuthority {
incumbent: state.authority.level,
challenger: event.authority.level,
});
}
state.lifecycle = FactLifecycle::Retracted;
}
FactEventKind::Retrieved { .. } | FactEventKind::UsedInDecision { .. } => {}
FactEventKind::ChallengeRejected { .. } => {
state
.survived_challenges
.entry(event.provenance.source.clone())
.and_modify(|level| *level = (*level).max(event.authority.level))
.or_insert(event.authority.level);
}
}
state.updated_at = event.provenance.recorded_at;
state.last_event_id = event.event_id.clone();
Ok(state)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TransitionError {
InvalidEvent(crate::model::ValidationError),
MissingInitialAssertion,
DuplicateAssertion,
FactIdMismatch,
FactShapeMismatch,
ReinforcementValueMismatch,
TerminalStateMutation(FactLifecycle),
InsufficientAuthority {
incumbent: AuthorityLevel,
challenger: AuthorityLevel,
},
CanonicalContradiction,
}
impl fmt::Display for TransitionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEvent(error) => write!(f, "invalid event: {error}"),
Self::MissingInitialAssertion => {
f.write_str("fact stream must start with fact.asserted")
}
Self::DuplicateAssertion => {
f.write_str("fact stream cannot assert the same fact twice")
}
Self::FactIdMismatch => f.write_str("event fact_id does not match current state"),
Self::FactShapeMismatch => {
f.write_str("event subject or predicate does not match current state")
}
Self::ReinforcementValueMismatch => {
f.write_str("fact.reinforced cannot change the fact value")
}
Self::TerminalStateMutation(state) => {
write!(f, "cannot mutate terminal fact state {state:?}")
}
Self::InsufficientAuthority {
incumbent,
challenger,
} => write!(
f,
"insufficient authority: {challenger} may not override or remove an incumbent of {incumbent}"
),
Self::CanonicalContradiction => {
f.write_str("a canonical fact cannot be contradicted; supersede it with equal authority instead")
}
}
}
}
impl std::error::Error for TransitionError {}
#[cfg(test)]
mod tests {
use crate::ids::{ActorId, EvidenceId, FactEventId, FactId, SourceId, TimestampMillis};
use crate::model::{
Authority, AuthorityLevel, Confidence, ContradictionBasis, Evidence, EvidenceKind,
ExpirationReason, FactEvent, FactEventKind, FactValue, Predicate, Provenance,
RetractionReason, Subject, SupersessionReason, Ttl,
};
use super::{FactLifecycle, TransitionError, apply_event};
const ALL_LEVELS: [AuthorityLevel; 5] = [
AuthorityLevel::Unknown,
AuthorityLevel::Low,
AuthorityLevel::Medium,
AuthorityLevel::High,
AuthorityLevel::Canonical,
];
fn with_authority(mut event: FactEvent, level: AuthorityLevel) -> FactEvent {
event.authority = Authority {
level,
issuer: None,
scope: None,
};
event
}
fn asserted(level: AuthorityLevel) -> FactEvent {
with_authority(
base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
),
level,
)
}
fn supersede(event_id: &str, level: AuthorityLevel) -> FactEvent {
with_authority(
base_event(
FactEventKind::Superseded {
by: fact_id("fact:2"),
reason: SupersessionReason::NewerObservation,
},
event_id,
None,
),
level,
)
}
fn retract(event_id: &str, level: AuthorityLevel) -> FactEvent {
with_authority(
base_event(
FactEventKind::Retracted {
reason: RetractionReason::UserDeleted,
},
event_id,
None,
),
level,
)
}
fn expire(event_id: &str, level: AuthorityLevel) -> FactEvent {
with_authority(
base_event(
FactEventKind::Expired {
reason: ExpirationReason::PolicyRetention,
},
event_id,
None,
),
level,
)
}
fn fact_id(value: &str) -> FactId {
FactId::new(value).expect("valid fact id")
}
fn base_event(kind: FactEventKind, event_id: &str, value: Option<FactValue>) -> FactEvent {
FactEvent {
event_id: FactEventId::new(event_id).expect("valid event id"),
fact_id: fact_id("fact:1"),
kind,
subject: Subject::new("repo", "dent8").expect("valid subject"),
predicate: Predicate::new("uses_database").expect("valid predicate"),
value,
confidence: Confidence::from_millis(900).expect("valid confidence"),
authority: Authority::unknown(),
ttl: Ttl::Never,
provenance: Provenance {
source: SourceId::new("source:test").expect("valid source"),
actor: ActorId::new("actor:test").expect("valid actor"),
tool: Some("unit-test".to_string()),
run_id: None,
input_digest: None,
recorded_at: TimestampMillis::from_unix_millis(1),
attestation: None,
},
evidence: vec![Evidence {
id: EvidenceId::new("evidence:1").expect("valid evidence id"),
kind: EvidenceKind::UserStatement,
locator: "test".to_string(),
digest: None,
summary: Some("test evidence".to_string()),
}],
observed_at: None,
valid_from: None,
valid_to: None,
}
}
fn challenge_rejected(event_id: &str, source: &str, level: AuthorityLevel) -> FactEvent {
let mut event = with_authority(
base_event(
FactEventKind::ChallengeRejected {
challenge: crate::model::ChallengeKind::Supersession,
by: Some(fact_id("fact:2")),
rejection: crate::model::ChallengeRejection::InsufficientAuthority,
},
event_id,
None,
),
level,
);
event.provenance.source = SourceId::new(source).expect("valid source");
event
}
#[test]
fn surviving_challenges_accumulates_challengers_at_their_strongest() {
let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("asserted");
let state = apply_event(
Some(state),
&challenge_rejected("event:2", "source:attacker", AuthorityLevel::Low),
)
.expect("recorded");
let state = apply_event(
Some(state),
&challenge_rejected("event:3", "source:attacker", AuthorityLevel::Medium),
)
.expect("recorded");
let state = apply_event(
Some(state),
&challenge_rejected("event:4", "source:other", AuthorityLevel::Low),
)
.expect("recorded");
assert_eq!(state.lifecycle, FactLifecycle::Active);
assert_eq!(state.authority.level, AuthorityLevel::High);
assert_eq!(state.survived_challenge_count(), 2);
assert_eq!(
state.survived_challenges_at_or_above(AuthorityLevel::Medium),
1,
"a Sybil flood of low-authority challenges cannot simulate surviving strong ones"
);
assert_eq!(
state.survived_challenges_at_or_above(AuthorityLevel::Low),
2
);
assert_eq!(
state.earned_entrenchment_at_or_above(AuthorityLevel::Low),
state.corroboration_at_or_above(AuthorityLevel::Low)
+ state.survived_challenges_at_or_above(AuthorityLevel::Low),
);
assert_eq!(
state.earned_entrenchment_at_or_above(AuthorityLevel::Low),
3, );
assert_eq!(
state.earned_entrenchment_at_or_above(AuthorityLevel::Medium),
2, );
assert_eq!(
state.earned_entrenchment_at_or_above(AuthorityLevel::Canonical),
0, );
}
#[test]
fn a_challenge_record_is_not_an_initial_event_and_not_a_terminal_mutation() {
assert!(matches!(
apply_event(
None,
&challenge_rejected("event:1", "source:attacker", AuthorityLevel::Low)
),
Err(TransitionError::MissingInitialAssertion)
));
let state = apply_event(None, &asserted(AuthorityLevel::Low)).expect("asserted");
let state = apply_event(Some(state), &supersede("event:2", AuthorityLevel::Low))
.expect("superseded");
assert!(matches!(
apply_event(
Some(state),
&challenge_rejected("event:3", "source:attacker", AuthorityLevel::Low)
),
Err(TransitionError::TerminalStateMutation(
FactLifecycle::Superseded
))
));
}
#[test]
fn fact_state_without_survived_challenges_field_still_deserializes() {
let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("asserted");
let mut json = serde_json::to_value(&state).expect("serialize");
json.as_object_mut()
.expect("object")
.remove("survived_challenges")
.expect("field present in new serialization");
let old: super::FactState = serde_json::from_value(json).expect("old shape deserializes");
assert_eq!(old.survived_challenge_count(), 0);
}
#[test]
fn valid_to_bounds_freshness_like_an_elapsed_ttl() {
let mut event = asserted(AuthorityLevel::High);
event.valid_from = Some(TimestampMillis::from_unix_millis(1_000));
event.valid_to = Some(TimestampMillis::from_unix_millis(2_000));
let state = apply_event(None, &event).expect("asserted");
assert_eq!(
state.expires_at(),
Some(TimestampMillis::from_unix_millis(2_000))
);
assert!(!state.is_expired_at(TimestampMillis::from_unix_millis(1_999)));
assert!(state.is_expired_at(TimestampMillis::from_unix_millis(2_000)));
let mut event = asserted(AuthorityLevel::High);
event.valid_from = Some(TimestampMillis::from_unix_millis(1_000));
event.valid_to = Some(TimestampMillis::from_unix_millis(2_000));
event.ttl = crate::model::Ttl::DurationMillis(500);
let state = apply_event(None, &event).expect("asserted");
assert_eq!(
state.expires_at(),
Some(TimestampMillis::from_unix_millis(1_500)),
"a shorter TTL beats a later valid_to"
);
let mut event = asserted(AuthorityLevel::High);
event.valid_from = Some(TimestampMillis::from_unix_millis(1_000));
event.valid_to = Some(TimestampMillis::from_unix_millis(1_200));
event.ttl = crate::model::Ttl::DurationMillis(500);
let state = apply_event(None, &event).expect("asserted");
assert_eq!(
state.expires_at(),
Some(TimestampMillis::from_unix_millis(1_200)),
"an earlier valid_to beats a longer TTL"
);
}
#[test]
fn a_future_valid_from_reads_not_yet_valid_not_fresh() {
let mut event = asserted(AuthorityLevel::High);
event.valid_from = Some(TimestampMillis::from_unix_millis(2_000));
let state = apply_event(None, &event).expect("asserted");
assert!(state.is_not_yet_valid_at(TimestampMillis::from_unix_millis(1_000)));
assert!(!state.is_fresh_at(TimestampMillis::from_unix_millis(1_000)));
assert!(!state.is_expired_at(TimestampMillis::from_unix_millis(1_000)));
assert!(!state.is_not_yet_valid_at(TimestampMillis::from_unix_millis(2_000)));
assert!(state.is_fresh_at(TimestampMillis::from_unix_millis(2_000)));
let plain = apply_event(None, &asserted(AuthorityLevel::High)).expect("asserted");
assert!(!plain.is_not_yet_valid_at(TimestampMillis::from_unix_millis(0)));
assert!(plain.is_fresh_at(TimestampMillis::from_unix_millis(0)));
}
#[test]
fn an_empty_or_inverted_validity_interval_is_rejected() {
let mut event = asserted(AuthorityLevel::High);
event.valid_from = Some(TimestampMillis::from_unix_millis(2_000));
event.valid_to = Some(TimestampMillis::from_unix_millis(2_000));
assert!(matches!(
apply_event(None, &event),
Err(TransitionError::InvalidEvent(
crate::model::ValidationError::InvalidValidityInterval
))
));
}
#[test]
fn assertion_creates_active_state() {
let event = base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
);
let state = apply_event(None, &event).expect("assertion applies");
assert_eq!(state.lifecycle, FactLifecycle::Active);
assert_eq!(state.evidence_count, 1);
}
#[test]
fn duplicate_assertion_is_rejected() {
let event = base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
);
let state = apply_event(None, &event).expect("assertion applies");
let error = apply_event(Some(state), &event).expect_err("duplicate rejected");
assert_eq!(error, TransitionError::DuplicateAssertion);
}
#[test]
fn contradiction_marks_fact_contested() {
let asserted = base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
);
let state = apply_event(None, &asserted).expect("assertion applies");
let contradicted = base_event(
FactEventKind::Contradicted {
by: fact_id("fact:2"),
basis: ContradictionBasis::SamePredicateDifferentValue,
},
"event:2",
None,
);
let state = apply_event(Some(state), &contradicted).expect("contradiction applies");
assert_eq!(state.lifecycle, FactLifecycle::Contested);
assert_eq!(state.contradicted_by, vec![fact_id("fact:2")]);
}
#[test]
fn terminal_state_rejects_lifecycle_mutation() {
let asserted = base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
);
let state = apply_event(None, &asserted).expect("assertion applies");
let superseded = base_event(
FactEventKind::Superseded {
by: fact_id("fact:2"),
reason: SupersessionReason::NewerObservation,
},
"event:2",
None,
);
let state = apply_event(Some(state), &superseded).expect("supersession applies");
let reinforced = base_event(
FactEventKind::Reinforced {
by: fact_id("fact:3"),
},
"event:3",
Some(FactValue::Text("postgres".to_string())),
);
let error = apply_event(Some(state), &reinforced).expect_err("terminal mutation rejected");
assert_eq!(
error,
TransitionError::TerminalStateMutation(FactLifecycle::Superseded)
);
}
#[test]
fn retrieval_does_not_change_lifecycle() {
let asserted = base_event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
);
let state = apply_event(None, &asserted).expect("assertion applies");
let retrieved = base_event(
FactEventKind::Retrieved {
purpose: "context".to_string(),
},
"event:2",
None,
);
let state = apply_event(Some(state), &retrieved).expect("retrieval applies");
assert_eq!(state.lifecycle, FactLifecycle::Active);
}
#[test]
fn lower_authority_supersession_is_rejected() {
let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("assertion applies");
let error = apply_event(Some(state), &supersede("event:2", AuthorityLevel::Low))
.expect_err("low-authority supersession rejected");
assert_eq!(
error,
TransitionError::InsufficientAuthority {
incumbent: AuthorityLevel::High,
challenger: AuthorityLevel::Low,
}
);
}
#[test]
fn equal_authority_supersession_succeeds() {
let state =
apply_event(None, &asserted(AuthorityLevel::Medium)).expect("assertion applies");
let state = apply_event(Some(state), &supersede("event:2", AuthorityLevel::Medium))
.expect("equal-authority supersession applies");
assert_eq!(state.lifecycle, FactLifecycle::Superseded);
}
#[test]
fn higher_authority_supersession_succeeds() {
let state = apply_event(None, &asserted(AuthorityLevel::Low)).expect("assertion applies");
let state = apply_event(
Some(state),
&supersede("event:2", AuthorityLevel::Canonical),
)
.expect("higher-authority supersession applies");
assert_eq!(state.lifecycle, FactLifecycle::Superseded);
}
#[test]
fn contradicting_a_canonical_fact_hard_alarms() {
let state =
apply_event(None, &asserted(AuthorityLevel::Canonical)).expect("assertion applies");
let contradicted = base_event(
FactEventKind::Contradicted {
by: fact_id("fact:2"),
basis: ContradictionBasis::SamePredicateDifferentValue,
},
"event:2",
None,
);
let error = apply_event(Some(state), &contradicted)
.expect_err("canonical contradiction hard-alarms");
assert_eq!(error, TransitionError::CanonicalContradiction);
}
#[test]
fn contradicting_a_non_canonical_fact_still_contests() {
let state = apply_event(None, &asserted(AuthorityLevel::High)).expect("assertion applies");
let contradicted = base_event(
FactEventKind::Contradicted {
by: fact_id("fact:2"),
basis: ContradictionBasis::SamePredicateDifferentValue,
},
"event:2",
None,
);
let state =
apply_event(Some(state), &contradicted).expect("non-canonical contradiction applies");
assert_eq!(state.lifecycle, FactLifecycle::Contested);
}
#[test]
fn authority_monotone_supersession_and_non_resurrection() {
for incumbent in ALL_LEVELS {
for challenger in ALL_LEVELS {
let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
let result = apply_event(Some(state), &supersede("event:2", challenger));
if challenger < incumbent {
assert_eq!(
result,
Err(TransitionError::InsufficientAuthority {
incumbent,
challenger,
}),
"challenger {challenger:?} should not supersede incumbent {incumbent:?}",
);
continue;
}
let superseded = result.expect("non-under-ranking supersession applies");
assert!(
superseded.lifecycle.is_terminal(),
"supersession should make the fact terminal",
);
let resurrect = supersede("event:3", AuthorityLevel::Canonical);
let error = apply_event(Some(superseded), &resurrect)
.expect_err("terminal fact cannot be resurrected");
assert_eq!(
error,
TransitionError::TerminalStateMutation(FactLifecycle::Superseded)
);
}
}
}
#[test]
fn authority_monotone_retraction_and_non_resurrection() {
for incumbent in ALL_LEVELS {
for challenger in ALL_LEVELS {
let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
let result = apply_event(Some(state), &retract("event:2", challenger));
if challenger < incumbent {
assert_eq!(
result,
Err(TransitionError::InsufficientAuthority {
incumbent,
challenger,
}),
"retractor {challenger:?} should not retract incumbent {incumbent:?}",
);
continue;
}
let retracted = result.expect("non-under-ranking retraction applies");
assert_eq!(retracted.lifecycle, FactLifecycle::Retracted);
assert!(retracted.lifecycle.is_terminal());
let resurrect = supersede("event:3", AuthorityLevel::Canonical);
let error = apply_event(Some(retracted), &resurrect)
.expect_err("terminal fact cannot be resurrected");
assert_eq!(
error,
TransitionError::TerminalStateMutation(FactLifecycle::Retracted)
);
}
}
}
#[test]
fn authority_monotone_expiration_and_non_resurrection() {
for incumbent in ALL_LEVELS {
for challenger in ALL_LEVELS {
let state = apply_event(None, &asserted(incumbent)).expect("assertion applies");
let result = apply_event(Some(state), &expire("event:2", challenger));
if challenger < incumbent {
assert_eq!(
result,
Err(TransitionError::InsufficientAuthority {
incumbent,
challenger,
}),
"expirer {challenger:?} should not expire incumbent {incumbent:?}",
);
continue;
}
let expired = result.expect("non-under-ranking expiration applies");
assert_eq!(expired.lifecycle, FactLifecycle::Expired);
assert!(expired.lifecycle.is_terminal());
let resurrect = supersede("event:3", AuthorityLevel::Canonical);
let error = apply_event(Some(expired), &resurrect)
.expect_err("terminal fact cannot be resurrected");
assert_eq!(
error,
TransitionError::TerminalStateMutation(FactLifecycle::Expired)
);
}
}
}
}
#[cfg(kani)]
mod proofs {
use crate::ids::{ActorId, EvidenceId, FactEventId, FactId, SourceId, TimestampMillis};
use crate::model::{
Authority, AuthorityLevel, Confidence, Evidence, EvidenceKind, ExpirationReason, FactEvent,
FactEventKind, FactValue, Predicate, Provenance, Subject, SupersessionReason, Ttl,
};
use super::apply_event;
fn level_from(n: u8) -> AuthorityLevel {
match n {
0 => AuthorityLevel::Unknown,
1 => AuthorityLevel::Low,
2 => AuthorityLevel::Medium,
3 => AuthorityLevel::High,
_ => AuthorityLevel::Canonical,
}
}
fn event(
kind: FactEventKind,
event_id: &str,
value: Option<FactValue>,
level: AuthorityLevel,
) -> FactEvent {
FactEvent {
event_id: FactEventId::new(event_id).unwrap(),
fact_id: FactId::new("fact:1").unwrap(),
kind,
subject: Subject::new("repo", "dent8").unwrap(),
predicate: Predicate::new("uses_database").unwrap(),
value,
confidence: Confidence::from_millis(900).unwrap(),
authority: Authority {
level,
issuer: None,
scope: None,
},
ttl: Ttl::Never,
provenance: Provenance {
source: SourceId::new("source:test").unwrap(),
actor: ActorId::new("actor:test").unwrap(),
tool: None,
run_id: None,
input_digest: None,
recorded_at: TimestampMillis::from_unix_millis(1),
attestation: None,
},
evidence: vec![Evidence {
id: EvidenceId::new("evidence:1").unwrap(),
kind: EvidenceKind::UserStatement,
locator: "x".to_string(),
digest: None,
summary: None,
}],
observed_at: None,
valid_from: None,
valid_to: None,
}
}
#[kani::proof]
fn supersession_is_authority_monotone_and_non_resurrecting() {
let a: u8 = kani::any();
let b: u8 = kani::any();
kani::assume(a < 5);
kani::assume(b < 5);
let incumbent = level_from(a);
let challenger = level_from(b);
let asserted = event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
incumbent,
);
let state = apply_event(None, &asserted).unwrap();
let superseded = event(
FactEventKind::Superseded {
by: FactId::new("fact:2").unwrap(),
reason: SupersessionReason::NewerObservation,
},
"event:2",
None,
challenger,
);
match apply_event(Some(state), &superseded) {
Ok(next) => {
assert!(challenger >= incumbent);
assert!(next.lifecycle.is_terminal());
let resurrect = event(
FactEventKind::Superseded {
by: FactId::new("fact:3").unwrap(),
reason: SupersessionReason::NewerObservation,
},
"event:3",
None,
AuthorityLevel::Canonical,
);
assert!(apply_event(Some(next), &resurrect).is_err());
}
Err(_) => {
assert!(challenger < incumbent);
}
}
}
#[kani::proof]
fn retraction_is_authority_monotone_and_non_resurrecting() {
use crate::model::RetractionReason;
let a: u8 = kani::any();
let b: u8 = kani::any();
kani::assume(a < 5);
kani::assume(b < 5);
let incumbent = level_from(a);
let challenger = level_from(b);
let asserted = event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
incumbent,
);
let state = apply_event(None, &asserted).unwrap();
let retracted = event(
FactEventKind::Retracted {
reason: RetractionReason::UserDeleted,
},
"event:2",
None,
challenger,
);
match apply_event(Some(state), &retracted) {
Ok(next) => {
assert!(challenger >= incumbent);
assert!(next.lifecycle.is_terminal());
let resurrect = event(
FactEventKind::Superseded {
by: FactId::new("fact:3").unwrap(),
reason: SupersessionReason::NewerObservation,
},
"event:3",
None,
AuthorityLevel::Canonical,
);
assert!(apply_event(Some(next), &resurrect).is_err());
}
Err(_) => {
assert!(challenger < incumbent);
}
}
}
#[kani::proof]
fn expiration_is_authority_monotone_and_non_resurrecting() {
let a: u8 = kani::any();
let b: u8 = kani::any();
kani::assume(a < 5);
kani::assume(b < 5);
let incumbent = level_from(a);
let challenger = level_from(b);
let asserted = event(
FactEventKind::Asserted,
"event:1",
Some(FactValue::Text("postgres".to_string())),
incumbent,
);
let state = apply_event(None, &asserted).unwrap();
let expired = event(
FactEventKind::Expired {
reason: ExpirationReason::PolicyRetention,
},
"event:2",
None,
challenger,
);
match apply_event(Some(state), &expired) {
Ok(next) => {
assert!(challenger >= incumbent);
assert!(next.lifecycle.is_terminal());
let resurrect = event(
FactEventKind::Superseded {
by: FactId::new("fact:3").unwrap(),
reason: SupersessionReason::NewerObservation,
},
"event:3",
None,
AuthorityLevel::Canonical,
);
assert!(apply_event(Some(next), &resurrect).is_err());
}
Err(_) => {
assert!(challenger < incumbent);
}
}
}
}