Skip to main content

web4_core/
time.rs

1// Copyright (c) 2026 MetaLINXX Inc.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! # Time as a first-class web4 axis (the temporal twin of ATP)
5//!
6//! `Web4 = MCP + RDF + LCT + T3/V3*MRH + ATP/ADP` has **space** (MRH),
7//! **energy** (ATP/ADP), **trust** (T3/V3), and **identity** (LCT) — but no
8//! **time + events as an accountable axis**. This module adds it (Thor's RTOS
9//! proposal, 2026-06-20):
10//!
11//! 1. **A deadline is a Resource** — the temporal twin of ATP. ATP makes
12//!    *spending energy* accountable; this makes *spending time* accountable.
13//! 2. **R6/R7 is a wait-on-event-with-timeout** (see [`crate::event`]); a
14//!    deadline bounds the wait.
15//! 3. **A deadline-miss is a trust event** — it folds into T3 (Temperament) and
16//!    V3 (Veracity), propagating up the MRH like any other reputation delta.
17//!
18//! ## The accountability gate (HUB's review caveats, baked in)
19//!
20//! A non-response is *not* automatically a temperament fault. Only a genuine
21//! "could have responded in time and didn't" debits trust:
22//!
23//! - **Witness-unavailable ⇒ [`DeadlineOutcome::Suspended`], not missed.** If
24//!   the deadline-witness (e.g. the hub) was down/locked at the due time, every
25//!   in-flight deadline must *pause*, not expire — otherwise a hub outage would
26//!   fire a fleet-wide false-lateness cascade. *Hub downtime is not fleet
27//!   lateness.* Checked first; it overrides everything.
28//! - **Unreachable ≠ refused.** A responder partitioned/crashed is
29//!   [`DeadlineOutcome::Unreachable`] — a clock/network fault, **not** priced as
30//!   Temperament (don't poison the trust graph with the network).
31//! - Only [`DeadlineOutcome::Late`]/[`DeadlineOutcome::Missed`] with a reachable
32//!   responder and an available witness carry a trust debit.
33//!
34//! Idempotency (a requester that timed out, retried, then got the slow success)
35//! is handled at the event layer ([`crate::event::EventLog`]) so a slow success
36//! isn't double-counted as both a miss and a result.
37
38use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40
41use crate::{T3, TrustDimension, V3, ValueDimension};
42
43/// How critical lateness is — scales the trust debit, like RTOS task priority.
44#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
45#[serde(rename_all = "snake_case")]
46pub enum Criticality {
47    /// Best-effort; lateness is noted, barely penalized.
48    Soft,
49    /// The default — lateness is a real but bounded trust signal.
50    #[default]
51    Firm,
52    /// Hard real-time; a miss is a significant temperament fault.
53    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/// A deadline carried on an R6 [`Request`](crate::r6::Request) — **time as a
67/// Resource**, the twin of `atp_stake`. The requester commits to a `due_at`;
68/// the responder is accountable for meeting it.
69#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
70pub struct Deadline {
71    /// When the Result is due.
72    pub due_at: DateTime<Utc>,
73    /// How hard the deadline is.
74    #[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    /// Decide the outcome from observed timing + witnessed liveness, applying
89    /// the accountability gate. `now` is the evaluation time (for an action
90    /// that hasn't completed). See the module docs for the caveats.
91    pub fn evaluate(
92        &self,
93        timing: &Timing,
94        witness: WitnessAvailability,
95        reachable: bool,
96        now: DateTime<Utc>,
97    ) -> DeadlineOutcome {
98        // A clock fault overrides everything: if the deadline-witness was down,
99        // the deadline is paused, never auto-expired.
100        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            // Still in flight:
109            None if now <= self.due_at => DeadlineOutcome::Met, // not yet due
110            None if !reachable => DeadlineOutcome::Unreachable, // network fault
111            None => DeadlineOutcome::Missed {
112                overdue_secs: (now - self.due_at).num_seconds().max(0),
113            },
114        }
115    }
116
117    /// The T3/V3 reputation impact of an outcome — "lateness is a trust event",
118    /// gated so only genuine faults debit trust. Met → a small positive
119    /// (responsiveness builds Temperament); Late/Missed → negative scaled by
120    /// criticality × overdue magnitude; Suspended/Unreachable → **zero** (clock
121    /// and network faults are not temperament faults).
122    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/// Observed timing of an action's execution.
145#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
146pub struct Timing {
147    pub started_at: DateTime<Utc>,
148    /// `None` ⇒ still in-flight / never completed.
149    #[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/// Whether the deadline-witness (the observer timing the deadline) was reachable
164/// at the due time. The first thing the gate checks.
165#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
166#[serde(rename_all = "snake_case")]
167pub enum WitnessAvailability {
168    #[default]
169    Available,
170    /// The deadline-witness (e.g. the hub) was down/locked at the due time.
171    Unavailable,
172}
173
174/// The temporal-accountability verdict. Note `Late ≠ Missed ≠ Suspended ≠
175/// Unreachable` — only the first two are faults (HUB's "late ≠ never ≠ refused
176/// ≠ unreachable").
177#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "snake_case", tag = "outcome")]
179pub enum DeadlineOutcome {
180    /// Completed at or before the deadline (or not yet due).
181    Met,
182    /// Completed, but after the deadline.
183    Late { overdue_secs: i64 },
184    /// Past due, responder reachable, witness available — a genuine fault.
185    Missed { overdue_secs: i64 },
186    /// Deadline-witness was unavailable at the due time → paused, not missed.
187    /// Carries **no** trust debit.
188    Suspended,
189    /// Responder unreachable (partition/crash) — a network fault, not a
190    /// temperament fault. **No** trust debit.
191    Unreachable,
192}
193
194impl DeadlineOutcome {
195    /// Whether this outcome is a genuine accountability fault (debits trust).
196    pub fn is_fault(&self) -> bool {
197        matches!(self, DeadlineOutcome::Late { .. } | DeadlineOutcome::Missed { .. })
198    }
199}
200
201/// The T3/V3 trust signal a deadline outcome produces.
202#[derive(Clone, Copy, Debug, PartialEq)]
203pub struct TemporalImpact {
204    /// Delta to T3 Temperament (consistency / responsiveness).
205    pub temperament: f64,
206    /// Delta to V3 Veracity (did it do what it said, when it said).
207    pub veracity: f64,
208}
209
210impl TemporalImpact {
211    /// Fold this temporal trust signal into a T3 + V3 (Temperament / Veracity).
212    /// This is the "deadline-miss propagates to T3/V3" step.
213    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
219/// Map overdue seconds to a saturating penalty magnitude in `[0.1, 1.0]`:
220/// a small overshoot is minor; very late saturates (no unbounded punishment).
221fn 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)); // never completed
261        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        // HUB's caveat: hub down/locked at due time ⇒ paused, no debit.
269        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        // HUB's caveat: a partition/crash ≠ declining. No trust debit.
281        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}