1use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40
41use crate::{T3, TrustDimension, V3, ValueDimension};
42
43#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
45#[serde(rename_all = "snake_case")]
46pub enum Criticality {
47 Soft,
49 #[default]
51 Firm,
52 Hard,
54}
55
56impl Criticality {
57 fn scale(self) -> f64 {
58 match self {
59 Criticality::Soft => 0.25,
60 Criticality::Firm => 1.0,
61 Criticality::Hard => 2.0,
62 }
63 }
64}
65
66#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
70pub struct Deadline {
71 pub due_at: DateTime<Utc>,
73 #[serde(default)]
75 pub criticality: Criticality,
76}
77
78impl Deadline {
79 pub fn new(due_at: DateTime<Utc>) -> Self {
80 Self { due_at, criticality: Criticality::Firm }
81 }
82
83 pub fn with_criticality(mut self, c: Criticality) -> Self {
84 self.criticality = c;
85 self
86 }
87
88 pub fn evaluate(
92 &self,
93 timing: &Timing,
94 witness: WitnessAvailability,
95 reachable: bool,
96 now: DateTime<Utc>,
97 ) -> DeadlineOutcome {
98 if witness == WitnessAvailability::Unavailable {
101 return DeadlineOutcome::Suspended;
102 }
103 match timing.completed_at {
104 Some(done) if done <= self.due_at => DeadlineOutcome::Met,
105 Some(done) => DeadlineOutcome::Late {
106 overdue_secs: (done - self.due_at).num_seconds().max(0),
107 },
108 None if now <= self.due_at => DeadlineOutcome::Met, None if !reachable => DeadlineOutcome::Unreachable, None => DeadlineOutcome::Missed {
112 overdue_secs: (now - self.due_at).num_seconds().max(0),
113 },
114 }
115 }
116
117 pub fn reputation_impact(&self, outcome: &DeadlineOutcome) -> TemporalImpact {
123 let scale = self.criticality.scale();
124 match outcome {
125 DeadlineOutcome::Met => TemporalImpact {
126 temperament: 0.01 * scale,
127 veracity: 0.005 * scale,
128 },
129 DeadlineOutcome::Late { overdue_secs } => {
130 let m = magnitude(*overdue_secs);
131 TemporalImpact { temperament: -0.02 * scale * m, veracity: -0.01 * scale * m }
132 }
133 DeadlineOutcome::Missed { overdue_secs } => {
134 let m = magnitude(*overdue_secs);
135 TemporalImpact { temperament: -0.05 * scale * m, veracity: -0.03 * scale * m }
136 }
137 DeadlineOutcome::Suspended | DeadlineOutcome::Unreachable => {
138 TemporalImpact { temperament: 0.0, veracity: 0.0 }
139 }
140 }
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
146pub struct Timing {
147 pub started_at: DateTime<Utc>,
148 #[serde(default)]
150 pub completed_at: Option<DateTime<Utc>>,
151}
152
153impl Timing {
154 pub fn started(at: DateTime<Utc>) -> Self {
155 Self { started_at: at, completed_at: None }
156 }
157 pub fn complete(mut self, at: DateTime<Utc>) -> Self {
158 self.completed_at = Some(at);
159 self
160 }
161}
162
163#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
166#[serde(rename_all = "snake_case")]
167pub enum WitnessAvailability {
168 #[default]
169 Available,
170 Unavailable,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "snake_case", tag = "outcome")]
179pub enum DeadlineOutcome {
180 Met,
182 Late { overdue_secs: i64 },
184 Missed { overdue_secs: i64 },
186 Suspended,
189 Unreachable,
192}
193
194impl DeadlineOutcome {
195 pub fn is_fault(&self) -> bool {
197 matches!(self, DeadlineOutcome::Late { .. } | DeadlineOutcome::Missed { .. })
198 }
199}
200
201#[derive(Clone, Copy, Debug, PartialEq)]
203pub struct TemporalImpact {
204 pub temperament: f64,
206 pub veracity: f64,
208}
209
210impl TemporalImpact {
211 pub fn apply(&self, t3: &mut T3, v3: &mut V3) {
214 t3.apply_delta(TrustDimension::Temperament, self.temperament);
215 v3.apply_delta(ValueDimension::Veracity, self.veracity);
216 }
217}
218
219fn magnitude(overdue_secs: i64) -> f64 {
222 let mins = overdue_secs.max(0) as f64 / 60.0;
223 ((1.0 + mins).ln() / 6.0).clamp(0.1, 1.0)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use chrono::Duration;
230
231 fn base() -> (DateTime<Utc>, Deadline) {
232 let due = DateTime::parse_from_rfc3339("2026-06-20T12:00:00Z").unwrap().with_timezone(&Utc);
233 (due, Deadline::new(due))
234 }
235
236 #[test]
237 fn met_when_completed_before_due() {
238 let (due, d) = base();
239 let t = Timing::started(due - Duration::minutes(10)).complete(due - Duration::minutes(1));
240 let o = d.evaluate(&t, WitnessAvailability::Available, true, due);
241 assert_eq!(o, DeadlineOutcome::Met);
242 let imp = d.reputation_impact(&o);
243 assert!(imp.temperament > 0.0, "meeting a deadline builds Temperament");
244 }
245
246 #[test]
247 fn late_when_completed_after_due_debits_trust() {
248 let (due, d) = base();
249 let t = Timing::started(due - Duration::minutes(10)).complete(due + Duration::minutes(30));
250 let o = d.evaluate(&t, WitnessAvailability::Available, true, due + Duration::hours(1));
251 assert!(matches!(o, DeadlineOutcome::Late { .. }));
252 assert!(o.is_fault());
253 let imp = d.reputation_impact(&o);
254 assert!(imp.temperament < 0.0 && imp.veracity < 0.0);
255 }
256
257 #[test]
258 fn missed_when_past_due_reachable_and_witnessed() {
259 let (due, d) = base();
260 let t = Timing::started(due - Duration::minutes(10)); let o = d.evaluate(&t, WitnessAvailability::Available, true, due + Duration::minutes(45));
262 assert!(matches!(o, DeadlineOutcome::Missed { .. }));
263 assert!(d.reputation_impact(&o).temperament < 0.0);
264 }
265
266 #[test]
267 fn witness_unavailable_suspends_not_misses() {
268 let (due, d) = base();
270 let t = Timing::started(due - Duration::minutes(10));
271 let o = d.evaluate(&t, WitnessAvailability::Unavailable, true, due + Duration::hours(5));
272 assert_eq!(o, DeadlineOutcome::Suspended);
273 assert!(!o.is_fault());
274 let imp = d.reputation_impact(&o);
275 assert_eq!((imp.temperament, imp.veracity), (0.0, 0.0));
276 }
277
278 #[test]
279 fn unreachable_is_not_a_temperament_fault() {
280 let (due, d) = base();
282 let t = Timing::started(due - Duration::minutes(10));
283 let o = d.evaluate(&t, WitnessAvailability::Available, false, due + Duration::minutes(45));
284 assert_eq!(o, DeadlineOutcome::Unreachable);
285 assert!(!o.is_fault());
286 assert_eq!(d.reputation_impact(&o).temperament, 0.0);
287 }
288
289 #[test]
290 fn criticality_scales_the_debit() {
291 let (due, _) = base();
292 let t = Timing::started(due).complete(due + Duration::minutes(30));
293 let soft = Deadline::new(due).with_criticality(Criticality::Soft);
294 let hard = Deadline::new(due).with_criticality(Criticality::Hard);
295 let now = due + Duration::hours(1);
296 let soft_imp = soft.reputation_impact(&soft.evaluate(&t, WitnessAvailability::Available, true, now));
297 let hard_imp = hard.reputation_impact(&hard.evaluate(&t, WitnessAvailability::Available, true, now));
298 assert!(hard_imp.temperament < soft_imp.temperament, "hard miss costs more");
299 }
300
301 #[test]
302 fn impact_folds_into_tensors() {
303 let (due, d) = base();
304 let t = Timing::started(due).complete(due + Duration::hours(2));
305 let o = d.evaluate(&t, WitnessAvailability::Available, true, due + Duration::hours(3));
306 let mut t3 = T3::new();
307 let mut v3 = V3::new();
308 let before = t3.score(TrustDimension::Temperament);
309 d.reputation_impact(&o).apply(&mut t3, &mut v3);
310 assert!(t3.score(TrustDimension::Temperament) < before, "late miss lowers Temperament");
311 }
312}