1use chrono::{DateTime, Duration, Utc};
23
24#[derive(Debug, Clone, PartialEq)]
31pub enum Risk {
32 Stable,
34 Warning,
36 Critical,
39}
40
41#[derive(Debug, Clone)]
56pub struct TileTtl {
57 keel_date: DateTime<Utc>,
58 ttl: Duration,
59 data: String,
60}
61
62impl TileTtl {
63 pub fn new(data: impl Into<String>, ttl: Duration) -> Self {
66 Self { keel_date: Utc::now(), ttl, data: data.into() }
67 }
68
69 pub fn is_alive(&self) -> bool {
72 Utc::now() < self.keel_date + self.ttl
73 }
74
75 pub fn freshness(&self) -> f64 {
77 let elapsed = Utc::now() - self.keel_date;
78 if elapsed >= self.ttl { return 0.0 }
79 let remaining = self.ttl - elapsed;
80 remaining.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64
81 }
82
83 pub fn data(&self) -> Option<&str> {
85 if self.is_alive() { Some(&self.data) } else { None }
86 }
87
88 pub fn filter_active(tiles: &[Self]) -> Vec<&Self> {
91 tiles.iter().filter(|t| t.is_alive()).collect()
92 }
93
94 pub fn partition(tiles: Vec<Self>) -> (Vec<Self>, Vec<Self>) {
96 tiles.into_iter().partition(|t| t.is_alive())
97 }
98}
99
100#[derive(Debug, Clone)]
107pub struct TaskTtl {
108 created: DateTime<Utc>,
109 ttl: Duration,
110 steps: Vec<String>,
111 completed: usize,
112}
113
114impl TaskTtl {
115 pub fn new(steps: Vec<String>, ttl: Duration) -> Self {
116 Self { created: Utc::now(), ttl, steps, completed: 0 }
117 }
118
119 pub fn is_stale(&self) -> bool {
121 Utc::now() >= self.created + self.ttl
122 }
123
124 pub fn execute_until_stale(&mut self) -> usize {
127 while self.completed < self.steps.len() {
128 if self.is_stale() {
129 break; }
131 self.completed += 1;
133 }
134 self.completed
135 }
136
137 pub fn progress(&self) -> f64 {
139 if self.steps.is_empty() { return 1.0 }
140 self.completed as f64 / self.steps.len() as f64
141 }
142
143 pub fn filter_fresh(tasks: &[Self]) -> Vec<&Self> {
145 tasks.iter().filter(|t| !t.is_stale()).collect()
146 }
147}
148
149#[derive(Debug, Clone)]
157pub struct AgentTtl {
158 keel_date: DateTime<Utc>,
159 ttl: Duration,
160 last_output: DateTime<Utc>,
161 heading: String,
162}
163
164impl AgentTtl {
165 pub fn new(heading: impl Into<String>, ttl: Duration) -> Self {
166 Self {
167 keel_date: Utc::now(),
168 ttl,
169 last_output: Utc::now(),
170 heading: heading.into(),
171 }
172 }
173
174 pub fn is_present(&self) -> bool {
177 let now = Utc::now();
178 now < self.keel_date + self.ttl
179 && now - self.last_output < self.ttl / 4
180 }
181
182 pub fn heartbeat(&mut self) {
184 self.last_output = Utc::now();
185 }
186
187 pub fn missed_beats(&self) -> i64 {
189 (Utc::now() - self.last_output).num_seconds() / (self.ttl.num_seconds() / 4).max(1)
190 }
191
192 pub fn heading(&self) -> &str { &self.heading }
194
195 pub fn change_heading(&mut self, heading: impl Into<String>) {
197 self.heading = heading.into();
198 }
199
200 pub fn filter_present(agents: &[Self]) -> Vec<&Self> {
202 agents.iter().filter(|a| a.is_present()).collect()
203 }
204}
205
206#[derive(Debug, Clone)]
214pub struct BearingTtl {
215 target: String,
216 angle: f64, rate: f64, observed: DateTime<Utc>,
219 ttl: Duration,
220}
221
222impl BearingTtl {
223 pub fn new(target: impl Into<String>, angle: f64, rate: f64, ttl: Duration) -> Self {
224 Self { target: target.into(), angle, rate, observed: Utc::now(), ttl }
225 }
226
227 pub fn collision_risk(&self) -> Risk {
231 let now = Utc::now();
232 if now > self.observed + self.ttl {
233 return Risk::Critical; }
235 if self.rate.abs() < 0.001 && self.angle.abs() < 1.0 {
236 return Risk::Warning; }
238 Risk::Stable
239 }
240
241 pub fn is_current(&self) -> bool {
243 Utc::now() <= self.observed + self.ttl
244 }
245}
246
247#[derive(Debug, Clone)]
255pub struct TrustTtl {
256 assertion: String,
257 confidence: f64,
258 provenance_depth: u8,
259 proven: DateTime<Utc>,
260 ttl: Duration,
261}
262
263impl TrustTtl {
264 pub fn new(assertion: impl Into<String>, confidence: f64, depth: u8, ttl: Duration) -> Self {
265 Self {
266 assertion: assertion.into(),
267 confidence: confidence.clamp(0.0, 1.0),
268 provenance_depth: depth,
269 proven: Utc::now(),
270 ttl,
271 }
272 }
273
274 pub fn effective_confidence(&self) -> f64 {
278 let age = Utc::now() - self.proven;
279 let age_frac = (age.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64).min(1.0);
280 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
283 }
284
285 pub fn is_trusted(&self) -> bool {
287 self.effective_confidence() >= 0.7
288 }
289
290 pub fn needs_verification(&self) -> bool {
292 let c = self.effective_confidence();
293 c >= 0.3 && c < 0.7
294 }
295
296 pub fn needs_renewal(&self) -> bool {
298 self.effective_confidence() < 0.3
299 }
300
301 pub fn renew(&self, new_confidence: f64) -> Self {
303 Self::new(
304 self.assertion.clone(),
305 new_confidence,
306 self.provenance_depth,
307 self.ttl,
308 )
309 }
310}
311
312#[cfg(test)]
315mod tests {
316 use super::*;
317 use std::thread;
318 use chrono::Duration;
319
320 #[test]
321 fn tile_ttl_is_alive_after_creation() {
322 let tile = TileTtl::new("test", Duration::hours(1));
323 assert!(tile.is_alive());
324 }
325
326 #[test]
327 fn tile_ttl_dies_after_ttl() {
328 let tile = TileTtl::new("test", Duration::milliseconds(1));
329 thread::sleep(std::time::Duration::from_millis(5));
330 assert!(!tile.is_alive());
331 }
332
333 #[test]
334 fn tile_ttl_data_returns_none_when_dead() {
335 let tile = TileTtl::new("test", Duration::milliseconds(1));
336 thread::sleep(std::time::Duration::from_millis(5));
337 assert!(tile.data().is_none());
338 }
339
340 #[test]
341 fn tile_ttl_filter_active() {
342 let tiles = vec![
343 TileTtl::new("fresh", Duration::hours(1)),
344 TileTtl::new("stale", Duration::milliseconds(1)),
345 ];
346 thread::sleep(std::time::Duration::from_millis(5));
347 let alive = TileTtl::filter_active(&tiles);
348 assert_eq!(alive.len(), 1);
349 assert_eq!(alive[0].data(), Some("fresh"));
350 }
351
352 #[test]
353 fn tile_ttl_freshness_decays() {
354 let tile = TileTtl::new("test", Duration::hours(1));
355 let f = tile.freshness();
356 assert!(f > 0.99 && f <= 1.0);
357 }
358
359 #[test]
360 fn task_ttl_stale_after_ttl() {
361 let mut task = TaskTtl::new(
362 vec!["step1".into(), "step2".into()],
363 Duration::milliseconds(1),
364 );
365 thread::sleep(std::time::Duration::from_millis(5));
366 assert!(task.is_stale());
367 }
368
369 #[test]
370 fn task_ttl_execute_until_stale() {
371 let mut task = TaskTtl::new(
372 vec!["step1".into(), "step2".into(), "step3".into()],
373 Duration::hours(1),
374 );
375 let done = task.execute_until_stale();
376 assert_eq!(done, 3);
377 }
378
379 #[test]
380 fn agent_ttl_present_after_creation() {
381 let agent = AgentTtl::new("research", Duration::hours(1));
382 assert!(agent.is_present());
383 }
384
385 #[test]
386 fn agent_ttl_fades_without_output() {
387 let agent = AgentTtl::new("research", Duration::milliseconds(10));
388 thread::sleep(std::time::Duration::from_millis(15));
389 assert!(!agent.is_present());
390 }
391
392 #[test]
393 fn agent_ttl_heartbeat_resets_fade() {
394 let mut agent = AgentTtl::new("research", Duration::milliseconds(50));
395 thread::sleep(std::time::Duration::from_millis(10));
396 agent.heartbeat();
397 thread::sleep(std::time::Duration::from_millis(10));
398 agent.heartbeat();
399 assert!(agent.is_present());
400 }
401
402 #[test]
403 fn bearing_ttl_stable_when_changing() {
404 let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::hours(1));
405 assert_eq!(bearing.collision_risk(), Risk::Stable);
406 }
407
408 #[test]
409 fn bearing_ttl_warning_when_constant() {
410 let bearing = BearingTtl::new("target", 0.1, 0.0001, Duration::hours(1));
411 assert_eq!(bearing.collision_risk(), Risk::Warning);
412 }
413
414 #[test]
415 fn bearing_ttl_critical_when_expired() {
416 let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::milliseconds(1));
417 thread::sleep(std::time::Duration::from_millis(5));
418 assert_eq!(bearing.collision_risk(), Risk::Critical);
419 }
420
421 #[test]
422 fn trust_ttl_confidence_decays() {
423 let trust = TrustTtl::new("verified proof", 0.95, 0, Duration::hours(1));
424 let c = trust.effective_confidence();
425 assert!(c > 0.9 && c <= 1.0);
426 }
427
428 #[test]
429 fn trust_ttl_provenance_halves_weight() {
430 let direct = TrustTtl::new("seen myself", 1.0, 0, Duration::hours(1));
431 let hop1 = TrustTtl::new("heard from bob", 1.0, 1, Duration::hours(1));
432 let hop2 = TrustTtl::new("bob heard from alice", 1.0, 2, Duration::hours(1));
433 assert!(direct.effective_confidence() > hop1.effective_confidence());
434 assert!(hop1.effective_confidence() > hop2.effective_confidence());
435 }
436
437 #[test]
438 fn trust_ttl_gray_zones() {
439 let trusted = TrustTtl::new("high trust", 0.9, 0, Duration::hours(1));
440 assert!(trusted.is_trusted());
441
442 let borderline = TrustTtl::new("medium trust", 0.7, 1, Duration::hours(1));
443 assert!(borderline.is_trusted() || borderline.needs_verification());
445
446 let expired = TrustTtl::new("old trust", 0.5, 0, Duration::milliseconds(1));
447 thread::sleep(std::time::Duration::from_millis(5));
448 assert!(expired.needs_renewal() || expired.needs_verification());
449 }
450}