knowledge_runtime/temporal/
claims.rs1use crate::ids::EntityId;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TemporalClaim {
14 pub entity_id: EntityId,
16 pub claim: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub valid_from: Option<DateTime<Utc>>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub valid_until: Option<DateTime<Utc>>,
24 pub confidence: f32,
26 pub source_ids: Vec<String>,
28}
29
30impl TemporalClaim {
31 pub fn active_at(&self, when: &DateTime<Utc>) -> bool {
33 let after_start = match self.valid_from.as_ref() {
34 Some(from) => when >= from,
35 None => true,
36 };
37 let before_end = match self.valid_until.as_ref() {
38 Some(until) => when < until,
39 None => true,
40 };
41 after_start && before_end
42 }
43
44 pub fn overlaps(&self, other: &TemporalClaim) -> bool {
46 let a_before_d = match other.valid_until.as_ref() {
49 Some(d) => match self.valid_from.as_ref() {
50 Some(a) => a < d,
51 None => true,
52 },
53 None => true,
54 };
55 let c_before_b = match self.valid_until.as_ref() {
56 Some(b) => match other.valid_from.as_ref() {
57 Some(c) => c < b,
58 None => true,
59 },
60 None => true,
61 };
62 a_before_d && c_before_b
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum TemporalContradictionStatus {
70 None,
72 PossibleContradiction {
75 description: String,
77 },
78}
79
80pub fn check_contradiction(a: &TemporalClaim, b: &TemporalClaim) -> TemporalContradictionStatus {
86 if a.entity_id != b.entity_id {
87 return TemporalContradictionStatus::None;
88 }
89 if !a.overlaps(b) {
90 return TemporalContradictionStatus::None;
91 }
92 TemporalContradictionStatus::PossibleContradiction {
93 description: format!(
94 "overlapping claims on entity {}: {:?} vs {:?}",
95 a.entity_id, a.claim, b.claim
96 ),
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn make_claim(
105 entity: &str,
106 claim: &str,
107 from: Option<&str>,
108 until: Option<&str>,
109 ) -> TemporalClaim {
110 TemporalClaim {
111 entity_id: EntityId::new(entity),
112 claim: claim.to_string(),
113 valid_from: from.map(|s| s.parse::<DateTime<Utc>>().unwrap()),
114 valid_until: until.map(|s| s.parse::<DateTime<Utc>>().unwrap()),
115 confidence: 0.9,
116 source_ids: vec![],
117 }
118 }
119
120 #[test]
121 fn active_at_unbounded() {
122 let claim = make_claim("e1", "always true", None, None);
123 let now = Utc::now();
124 assert!(claim.active_at(&now));
125 }
126
127 #[test]
128 fn active_at_bounded() {
129 let claim = make_claim(
130 "e1",
131 "in range",
132 Some("2025-01-01T00:00:00Z"),
133 Some("2025-12-31T23:59:59Z"),
134 );
135 let mid: DateTime<Utc> = "2025-06-15T00:00:00Z".parse().unwrap();
136 let before: DateTime<Utc> = "2024-06-15T00:00:00Z".parse().unwrap();
137 assert!(claim.active_at(&mid));
138 assert!(!claim.active_at(&before));
139 }
140
141 #[test]
142 fn overlapping_same_entity_is_contradiction() {
143 let a = make_claim(
144 "e1",
145 "X is true",
146 Some("2025-01-01T00:00:00Z"),
147 Some("2025-06-01T00:00:00Z"),
148 );
149 let b = make_claim(
150 "e1",
151 "X is false",
152 Some("2025-03-01T00:00:00Z"),
153 Some("2025-09-01T00:00:00Z"),
154 );
155 assert!(matches!(
156 check_contradiction(&a, &b),
157 TemporalContradictionStatus::PossibleContradiction { .. }
158 ));
159 }
160
161 #[test]
162 fn non_overlapping_same_entity_no_contradiction() {
163 let a = make_claim(
164 "e1",
165 "X is true",
166 Some("2025-01-01T00:00:00Z"),
167 Some("2025-03-01T00:00:00Z"),
168 );
169 let b = make_claim(
170 "e1",
171 "X is false",
172 Some("2025-06-01T00:00:00Z"),
173 Some("2025-09-01T00:00:00Z"),
174 );
175 assert_eq!(
176 check_contradiction(&a, &b),
177 TemporalContradictionStatus::None
178 );
179 }
180
181 #[test]
182 fn different_entity_no_contradiction() {
183 let a = make_claim(
184 "e1",
185 "X",
186 Some("2025-01-01T00:00:00Z"),
187 Some("2025-12-01T00:00:00Z"),
188 );
189 let b = make_claim(
190 "e2",
191 "Y",
192 Some("2025-01-01T00:00:00Z"),
193 Some("2025-12-01T00:00:00Z"),
194 );
195 assert_eq!(
196 check_contradiction(&a, &b),
197 TemporalContradictionStatus::None
198 );
199 }
200}