Skip to main content

agi4_core/
evidence.rs

1//! Evidence types representing upstream benchmark measurements.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7/// Stable identifier for an upstream source (e.g., "arc-agi-3", "metr-80pct-time-horizon").
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct SourceId(String);
10
11impl SourceId {
12    /// Create a new source identifier.
13    pub fn new(id: impl Into<String>) -> Self {
14        Self(id.into())
15    }
16
17    /// Get the source ID as a string.
18    pub fn as_str(&self) -> &str {
19        &self.0
20    }
21}
22
23/// Stable identifier for a measurement within a source.
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct MeasurementId(String);
26
27impl MeasurementId {
28    /// Create a new measurement identifier.
29    pub fn new(id: impl Into<String>) -> Self {
30        Self(id.into())
31    }
32
33    /// Get the measurement ID as a string.
34    pub fn as_str(&self) -> &str {
35        &self.0
36    }
37}
38
39/// Bounded fraction in [0.0, 1.0].
40#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
41pub struct BoundedFraction(f64);
42
43impl BoundedFraction {
44    /// Create a bounded fraction, validating that value is in [0.0, 1.0].
45    pub fn new(value: f64) -> Result<Self, String> {
46        if value.is_nan() {
47            return Err("BoundedFraction cannot be NaN".to_string());
48        }
49        if !(0.0..=1.0).contains(&value) {
50            return Err(format!(
51                "BoundedFraction must be in [0.0, 1.0], got {}",
52                value
53            ));
54        }
55        Ok(Self(value))
56    }
57
58    /// Get the underlying f64 value.
59    pub fn value(&self) -> f64 {
60        self.0
61    }
62}
63
64/// Non-negative hours with upper bound T_MAX.
65#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
66pub struct NonNegativeHours(f64);
67
68impl NonNegativeHours {
69    /// Create a non-negative hour value, validating that it's >= 0.0.
70    pub fn new(value: f64) -> Result<Self, String> {
71        if value.is_nan() {
72            return Err("NonNegativeHours cannot be NaN".to_string());
73        }
74        if value < 0.0 {
75            return Err(format!("NonNegativeHours must be >= 0.0, got {}", value));
76        }
77        Ok(Self(value))
78    }
79
80    /// Get the underlying f64 value.
81    pub fn value(&self) -> f64 {
82        self.0
83    }
84}
85
86/// The value of a measurement, bounded to meaningful ranges.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum SourceValue {
89    Fraction(BoundedFraction),
90    Hours(NonNegativeHours),
91}
92
93/// Provenance metadata for a measurement.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Provenance {
96    pub source_url: Url,
97    pub fetch_timestamp: DateTime<Utc>,
98    pub source_version: Option<String>,
99    pub raw_value: String,
100}
101
102/// Evidence ingested from one upstream source for one measurement.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Evidence {
105    pub source: SourceId,
106    pub measurement: MeasurementId,
107    pub value: SourceValue,
108    pub reliability_percentile: u8,
109    pub provenance: Provenance,
110}