Skip to main content

lean_ctx_protocol/
outcome.rs

1//! Accepted-outcome contract.
2
3use crate::common::{
4    OutcomeId, TaskId, ValidationError, deserialize_optional_milliunit, deserialize_schema_version,
5    validate_milliunit, validate_schema_version,
6};
7use crate::evidence::EvidenceRefV1;
8use serde::{Deserialize, Serialize};
9
10/// Tri-state acceptance prevents an absent observation from being treated as a rejection.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum AcceptanceState {
14    Accepted,
15    Rejected,
16    Unknown,
17}
18
19/// State of an individual completion or quality signal.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SignalState {
23    Passed,
24    Failed,
25    Unknown,
26    NotRun,
27}
28
29/// Verification signals attached to an accepted-outcome observation.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct OutcomeSignalsV1 {
33    pub build: Option<SignalState>,
34    pub tests: Option<SignalState>,
35    pub lint: Option<SignalState>,
36    pub typecheck: Option<SignalState>,
37    pub completion: Option<SignalState>,
38    pub pr: Option<SignalState>,
39    pub correction: Option<SignalState>,
40    pub rollback: Option<SignalState>,
41    pub retry: Option<SignalState>,
42}
43
44/// Canonical outcome observation used for acceptance and efficiency accounting.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct AcceptedOutcomeV1 {
48    #[serde(deserialize_with = "deserialize_schema_version")]
49    pub schema_version: u32,
50    pub outcome_id: OutcomeId,
51    pub task_id: TaskId,
52    pub accepted: AcceptanceState,
53    #[serde(
54        default,
55        skip_serializing_if = "Option::is_none",
56        deserialize_with = "deserialize_optional_milliunit"
57    )]
58    pub quality_score_milli: Option<u16>,
59    pub signals: OutcomeSignalsV1,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub contract_ref: Option<String>,
62    pub evidence_refs: Vec<EvidenceRefV1>,
63    pub observed_at: String,
64}
65
66impl AcceptedOutcomeV1 {
67    /// Validate invariants that also apply to values constructed in Rust.
68    pub fn validate(&self) -> Result<(), ValidationError> {
69        validate_schema_version(self.schema_version)?;
70        if let Some(value) = self.quality_score_milli {
71            validate_milliunit(value, "quality_score_milli")?;
72        }
73        Ok(())
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn id<T>(value: &str) -> T
82    where
83        T: TryFrom<String>,
84        <T as TryFrom<String>>::Error: std::fmt::Debug,
85    {
86        T::try_from(value.to_owned()).expect("identifier should be valid")
87    }
88
89    #[test]
90    fn serialization_round_trip() {
91        let outcome = AcceptedOutcomeV1 {
92            schema_version: 1,
93            outcome_id: id("outcome-1"),
94            task_id: id("task-1"),
95            accepted: AcceptanceState::Accepted,
96            quality_score_milli: Some(950),
97            signals: OutcomeSignalsV1 {
98                build: Some(SignalState::Passed),
99                tests: Some(SignalState::Passed),
100                lint: Some(SignalState::Passed),
101                typecheck: Some(SignalState::Passed),
102                completion: Some(SignalState::Passed),
103                pr: Some(SignalState::NotRun),
104                correction: Some(SignalState::NotRun),
105                rollback: Some(SignalState::NotRun),
106                retry: Some(SignalState::Failed),
107            },
108            contract_ref: Some("contract:outcome".to_owned()),
109            evidence_refs: vec![],
110            observed_at: "2026-08-09T12:00:00Z".to_owned(),
111        };
112        let json = serde_json::to_string(&outcome).expect("outcome should serialize");
113        let decoded: AcceptedOutcomeV1 =
114            serde_json::from_str(&json).expect("outcome should deserialize");
115        assert_eq!(outcome, decoded);
116        outcome
117            .validate()
118            .expect("outcome should satisfy invariants");
119    }
120}