use chrono::{DateTime, Duration, Utc};
#[derive(Debug, Clone, PartialEq)]
pub enum Risk {
Stable,
Warning,
Critical,
}
#[derive(Debug, Clone)]
pub struct TileTtl {
keel_date: DateTime<Utc>,
ttl: Duration,
data: String,
}
impl TileTtl {
pub fn new(data: impl Into<String>, ttl: Duration) -> Self {
Self { keel_date: Utc::now(), ttl, data: data.into() }
}
pub fn is_alive(&self) -> bool {
Utc::now() < self.keel_date + self.ttl
}
pub fn freshness(&self) -> f64 {
let elapsed = Utc::now() - self.keel_date;
if elapsed >= self.ttl { return 0.0 }
let remaining = self.ttl - elapsed;
remaining.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64
}
pub fn data(&self) -> Option<&str> {
if self.is_alive() { Some(&self.data) } else { None }
}
pub fn filter_active(tiles: &[Self]) -> Vec<&Self> {
tiles.iter().filter(|t| t.is_alive()).collect()
}
pub fn partition(tiles: Vec<Self>) -> (Vec<Self>, Vec<Self>) {
tiles.into_iter().partition(|t| t.is_alive())
}
}
#[derive(Debug, Clone)]
pub struct TaskTtl {
created: DateTime<Utc>,
ttl: Duration,
steps: Vec<String>,
completed: usize,
}
impl TaskTtl {
pub fn new(steps: Vec<String>, ttl: Duration) -> Self {
Self { created: Utc::now(), ttl, steps, completed: 0 }
}
pub fn is_stale(&self) -> bool {
Utc::now() >= self.created + self.ttl
}
pub fn execute_until_stale(&mut self) -> usize {
while self.completed < self.steps.len() {
if self.is_stale() {
break; }
self.completed += 1;
}
self.completed
}
pub fn progress(&self) -> f64 {
if self.steps.is_empty() { return 1.0 }
self.completed as f64 / self.steps.len() as f64
}
pub fn filter_fresh(tasks: &[Self]) -> Vec<&Self> {
tasks.iter().filter(|t| !t.is_stale()).collect()
}
}
#[derive(Debug, Clone)]
pub struct AgentTtl {
keel_date: DateTime<Utc>,
ttl: Duration,
last_output: DateTime<Utc>,
heading: String,
}
impl AgentTtl {
pub fn new(heading: impl Into<String>, ttl: Duration) -> Self {
Self {
keel_date: Utc::now(),
ttl,
last_output: Utc::now(),
heading: heading.into(),
}
}
pub fn is_present(&self) -> bool {
let now = Utc::now();
now < self.keel_date + self.ttl
&& now - self.last_output < self.ttl / 4
}
pub fn heartbeat(&mut self) {
self.last_output = Utc::now();
}
pub fn missed_beats(&self) -> i64 {
(Utc::now() - self.last_output).num_seconds() / (self.ttl.num_seconds() / 4).max(1)
}
pub fn heading(&self) -> &str { &self.heading }
pub fn change_heading(&mut self, heading: impl Into<String>) {
self.heading = heading.into();
}
pub fn filter_present(agents: &[Self]) -> Vec<&Self> {
agents.iter().filter(|a| a.is_present()).collect()
}
}
#[derive(Debug, Clone)]
pub struct BearingTtl {
target: String,
angle: f64, rate: f64, observed: DateTime<Utc>,
ttl: Duration,
}
impl BearingTtl {
pub fn new(target: impl Into<String>, angle: f64, rate: f64, ttl: Duration) -> Self {
Self { target: target.into(), angle, rate, observed: Utc::now(), ttl }
}
pub fn collision_risk(&self) -> Risk {
let now = Utc::now();
if now > self.observed + self.ttl {
return Risk::Critical; }
if self.rate.abs() < 0.001 && self.angle.abs() < 1.0 {
return Risk::Warning; }
Risk::Stable
}
pub fn is_current(&self) -> bool {
Utc::now() <= self.observed + self.ttl
}
}
#[derive(Debug, Clone)]
pub struct TrustTtl {
assertion: String,
confidence: f64,
provenance_depth: u8,
proven: DateTime<Utc>,
ttl: Duration,
}
impl TrustTtl {
pub fn new(assertion: impl Into<String>, confidence: f64, depth: u8, ttl: Duration) -> Self {
Self {
assertion: assertion.into(),
confidence: confidence.clamp(0.0, 1.0),
provenance_depth: depth,
proven: Utc::now(),
ttl,
}
}
pub fn effective_confidence(&self) -> f64 {
let age = Utc::now() - self.proven;
let age_frac = (age.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64).min(1.0);
let time_decay = 1.0 - age_frac * 0.5; let hop_decay = 0.5_f64.powi(self.provenance_depth as i32); self.confidence * time_decay * hop_decay
}
pub fn is_trusted(&self) -> bool {
self.effective_confidence() >= 0.7
}
pub fn needs_verification(&self) -> bool {
let c = self.effective_confidence();
c >= 0.3 && c < 0.7
}
pub fn needs_renewal(&self) -> bool {
self.effective_confidence() < 0.3
}
pub fn renew(&self, new_confidence: f64) -> Self {
Self::new(
self.assertion.clone(),
new_confidence,
self.provenance_depth,
self.ttl,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use chrono::Duration;
#[test]
fn tile_ttl_is_alive_after_creation() {
let tile = TileTtl::new("test", Duration::hours(1));
assert!(tile.is_alive());
}
#[test]
fn tile_ttl_dies_after_ttl() {
let tile = TileTtl::new("test", Duration::milliseconds(1));
thread::sleep(std::time::Duration::from_millis(5));
assert!(!tile.is_alive());
}
#[test]
fn tile_ttl_data_returns_none_when_dead() {
let tile = TileTtl::new("test", Duration::milliseconds(1));
thread::sleep(std::time::Duration::from_millis(5));
assert!(tile.data().is_none());
}
#[test]
fn tile_ttl_filter_active() {
let tiles = vec![
TileTtl::new("fresh", Duration::hours(1)),
TileTtl::new("stale", Duration::milliseconds(1)),
];
thread::sleep(std::time::Duration::from_millis(5));
let alive = TileTtl::filter_active(&tiles);
assert_eq!(alive.len(), 1);
assert_eq!(alive[0].data(), Some("fresh"));
}
#[test]
fn tile_ttl_freshness_decays() {
let tile = TileTtl::new("test", Duration::hours(1));
let f = tile.freshness();
assert!(f > 0.99 && f <= 1.0);
}
#[test]
fn task_ttl_stale_after_ttl() {
let mut task = TaskTtl::new(
vec!["step1".into(), "step2".into()],
Duration::milliseconds(1),
);
thread::sleep(std::time::Duration::from_millis(5));
assert!(task.is_stale());
}
#[test]
fn task_ttl_execute_until_stale() {
let mut task = TaskTtl::new(
vec!["step1".into(), "step2".into(), "step3".into()],
Duration::hours(1),
);
let done = task.execute_until_stale();
assert_eq!(done, 3);
}
#[test]
fn agent_ttl_present_after_creation() {
let agent = AgentTtl::new("research", Duration::hours(1));
assert!(agent.is_present());
}
#[test]
fn agent_ttl_fades_without_output() {
let agent = AgentTtl::new("research", Duration::milliseconds(10));
thread::sleep(std::time::Duration::from_millis(15));
assert!(!agent.is_present());
}
#[test]
fn agent_ttl_heartbeat_resets_fade() {
let mut agent = AgentTtl::new("research", Duration::milliseconds(50));
thread::sleep(std::time::Duration::from_millis(10));
agent.heartbeat();
thread::sleep(std::time::Duration::from_millis(10));
agent.heartbeat();
assert!(agent.is_present());
}
#[test]
fn bearing_ttl_stable_when_changing() {
let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::hours(1));
assert_eq!(bearing.collision_risk(), Risk::Stable);
}
#[test]
fn bearing_ttl_warning_when_constant() {
let bearing = BearingTtl::new("target", 0.1, 0.0001, Duration::hours(1));
assert_eq!(bearing.collision_risk(), Risk::Warning);
}
#[test]
fn bearing_ttl_critical_when_expired() {
let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::milliseconds(1));
thread::sleep(std::time::Duration::from_millis(5));
assert_eq!(bearing.collision_risk(), Risk::Critical);
}
#[test]
fn trust_ttl_confidence_decays() {
let trust = TrustTtl::new("verified proof", 0.95, 0, Duration::hours(1));
let c = trust.effective_confidence();
assert!(c > 0.9 && c <= 1.0);
}
#[test]
fn trust_ttl_provenance_halves_weight() {
let direct = TrustTtl::new("seen myself", 1.0, 0, Duration::hours(1));
let hop1 = TrustTtl::new("heard from bob", 1.0, 1, Duration::hours(1));
let hop2 = TrustTtl::new("bob heard from alice", 1.0, 2, Duration::hours(1));
assert!(direct.effective_confidence() > hop1.effective_confidence());
assert!(hop1.effective_confidence() > hop2.effective_confidence());
}
#[test]
fn trust_ttl_gray_zones() {
let trusted = TrustTtl::new("high trust", 0.9, 0, Duration::hours(1));
assert!(trusted.is_trusted());
let borderline = TrustTtl::new("medium trust", 0.7, 1, Duration::hours(1));
assert!(borderline.is_trusted() || borderline.needs_verification());
let expired = TrustTtl::new("old trust", 0.5, 0, Duration::milliseconds(1));
thread::sleep(std::time::Duration::from_millis(5));
assert!(expired.needs_renewal() || expired.needs_verification());
}
}