lean_ctx_protocol/
outcome.rs1use 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#[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#[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#[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#[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 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}