Skip to main content

lean_ctx_workgraph/outcome/
signals.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Local signal values and adapters for command/test/CI observations.
4
5use serde::{Deserialize, Serialize};
6
7/// Signal kinds accepted by local outcome contracts.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SignalType {
11    BuildSuccess,
12    TestsPassing,
13    LintClean,
14    TypecheckPassing,
15    AgentCompletion,
16    RetryCount,
17    HumanAcceptance,
18    PrMerge,
19    CiPassing,
20    Correction,
21    Rollback,
22}
23
24impl SignalType {
25    /// Stable snake-case name used in explanations and serialized values.
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::BuildSuccess => "build_success",
29            Self::TestsPassing => "tests_passing",
30            Self::LintClean => "lint_clean",
31            Self::TypecheckPassing => "typecheck_passing",
32            Self::AgentCompletion => "agent_completion",
33            Self::RetryCount => "retry_count",
34            Self::HumanAcceptance => "human_acceptance",
35            Self::PrMerge => "pr_merge",
36            Self::CiPassing => "ci_passing",
37            Self::Correction => "correction",
38            Self::Rollback => "rollback",
39        }
40    }
41}
42
43impl std::fmt::Display for SignalType {
44    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        formatter.write_str(self.as_str())
46    }
47}
48
49/// A local observation and optional evidence metadata.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct OutcomeSignal {
52    pub signal_type: SignalType,
53    pub value: SignalValue,
54    pub evidence_ref: Option<String>,
55    pub observed_at: Option<String>,
56}
57
58impl OutcomeSignal {
59    /// Create an observation without optional metadata.
60    pub fn new(signal_type: SignalType, value: SignalValue) -> Self {
61        Self {
62            signal_type,
63            value,
64            evidence_ref: None,
65            observed_at: None,
66        }
67    }
68
69    /// Create an observation with evidence and observation-time metadata.
70    pub fn with_metadata(
71        signal_type: SignalType,
72        value: SignalValue,
73        evidence_ref: Option<String>,
74        observed_at: Option<String>,
75    ) -> Self {
76        Self {
77            signal_type,
78            value,
79            evidence_ref,
80            observed_at,
81        }
82    }
83
84    /// Attach an evidence reference.
85    pub fn with_evidence_ref(mut self, evidence_ref: impl Into<String>) -> Self {
86        self.evidence_ref = Some(evidence_ref.into());
87        self
88    }
89
90    /// Attach an observation timestamp.
91    pub fn with_observed_at(mut self, observed_at: impl Into<String>) -> Self {
92        self.observed_at = Some(observed_at.into());
93        self
94    }
95
96    /// Create an explicitly unobserved signal.
97    pub fn unknown(signal_type: SignalType) -> Self {
98        Self::new(signal_type, SignalValue::Unknown)
99    }
100}
101
102/// Value carried by a local signal adapter.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum SignalValue {
106    Boolean(bool),
107    Count(u32),
108    Unknown,
109}
110
111/// Namespace for adapters from local command and lifecycle observations.
112pub struct LocalSignalAdapters;
113
114impl LocalSignalAdapters {
115    /// Adapt a boolean result for any signal type.
116    pub fn boolean(signal_type: SignalType, value: bool) -> OutcomeSignal {
117        OutcomeSignal::new(signal_type, SignalValue::Boolean(value))
118    }
119
120    /// Adapt a count result for any signal type.
121    pub fn count(signal_type: SignalType, value: u32) -> OutcomeSignal {
122        OutcomeSignal::new(signal_type, SignalValue::Count(value))
123    }
124
125    /// Adapt an unobserved signal.
126    pub fn unknown(signal_type: SignalType) -> OutcomeSignal {
127        OutcomeSignal::unknown(signal_type)
128    }
129
130    pub fn build_success(value: bool) -> OutcomeSignal {
131        Self::boolean(SignalType::BuildSuccess, value)
132    }
133
134    pub fn tests_passing(value: bool) -> OutcomeSignal {
135        Self::boolean(SignalType::TestsPassing, value)
136    }
137
138    /// Adapt the positive delta in test count for a test-addition contract.
139    pub fn test_count_increased(delta: u32) -> OutcomeSignal {
140        Self::count(SignalType::TestsPassing, delta)
141    }
142
143    pub fn lint_clean(value: bool) -> OutcomeSignal {
144        Self::boolean(SignalType::LintClean, value)
145    }
146
147    pub fn typecheck_passing(value: bool) -> OutcomeSignal {
148        Self::boolean(SignalType::TypecheckPassing, value)
149    }
150
151    pub fn agent_completion(value: bool) -> OutcomeSignal {
152        Self::boolean(SignalType::AgentCompletion, value)
153    }
154
155    pub fn retry_count(value: u32) -> OutcomeSignal {
156        Self::count(SignalType::RetryCount, value)
157    }
158
159    pub fn human_acceptance(value: bool) -> OutcomeSignal {
160        Self::boolean(SignalType::HumanAcceptance, value)
161    }
162
163    pub fn pr_merge(value: bool) -> OutcomeSignal {
164        Self::boolean(SignalType::PrMerge, value)
165    }
166
167    pub fn ci_passing(value: bool) -> OutcomeSignal {
168        Self::boolean(SignalType::CiPassing, value)
169    }
170
171    pub fn correction(value: bool) -> OutcomeSignal {
172        Self::boolean(SignalType::Correction, value)
173    }
174
175    pub fn rollback(value: bool) -> OutcomeSignal {
176        Self::boolean(SignalType::Rollback, value)
177    }
178}
179
180pub fn adapt_boolean(signal_type: SignalType, value: bool) -> OutcomeSignal {
181    LocalSignalAdapters::boolean(signal_type, value)
182}
183
184pub fn adapt_count(signal_type: SignalType, value: u32) -> OutcomeSignal {
185    LocalSignalAdapters::count(signal_type, value)
186}
187
188pub fn adapt_build_success(value: bool) -> OutcomeSignal {
189    LocalSignalAdapters::build_success(value)
190}
191
192pub fn adapt_tests_passing(value: bool) -> OutcomeSignal {
193    LocalSignalAdapters::tests_passing(value)
194}
195
196pub fn adapt_test_count_increased(delta: u32) -> OutcomeSignal {
197    LocalSignalAdapters::test_count_increased(delta)
198}
199
200pub fn adapt_lint_clean(value: bool) -> OutcomeSignal {
201    LocalSignalAdapters::lint_clean(value)
202}
203
204pub fn adapt_typecheck_passing(value: bool) -> OutcomeSignal {
205    LocalSignalAdapters::typecheck_passing(value)
206}
207
208pub fn adapt_agent_completion(value: bool) -> OutcomeSignal {
209    LocalSignalAdapters::agent_completion(value)
210}
211
212pub fn adapt_retry_count(value: u32) -> OutcomeSignal {
213    LocalSignalAdapters::retry_count(value)
214}
215
216pub fn adapt_human_acceptance(value: bool) -> OutcomeSignal {
217    LocalSignalAdapters::human_acceptance(value)
218}
219
220pub fn adapt_pr_merge(value: bool) -> OutcomeSignal {
221    LocalSignalAdapters::pr_merge(value)
222}
223
224pub fn adapt_ci_passing(value: bool) -> OutcomeSignal {
225    LocalSignalAdapters::ci_passing(value)
226}
227
228pub fn adapt_correction(value: bool) -> OutcomeSignal {
229    LocalSignalAdapters::correction(value)
230}
231
232pub fn adapt_rollback(value: bool) -> OutcomeSignal {
233    LocalSignalAdapters::rollback(value)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn adapters_preserve_signal_kind_and_value() {
242        assert_eq!(
243            LocalSignalAdapters::build_success(true),
244            OutcomeSignal::new(SignalType::BuildSuccess, SignalValue::Boolean(true))
245        );
246        assert_eq!(
247            LocalSignalAdapters::retry_count(2).value,
248            SignalValue::Count(2)
249        );
250        assert_eq!(
251            LocalSignalAdapters::test_count_increased(1).signal_type,
252            SignalType::TestsPassing
253        );
254    }
255}