lean_ctx_workgraph/outcome/
contracts.rs1use serde::{Deserialize, Serialize};
6
7use super::signals::SignalType;
8
9pub const OUTCOME_CONTRACT_VERSION: &str = "1.0.0";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum TaskClass {
16 BugFix,
17 Refactor,
18 TestAddition,
19 Documentation,
20 Investigation,
21}
22
23impl TaskClass {
24 pub const fn as_str(self) -> &'static str {
26 match self {
27 Self::BugFix => "bug_fix",
28 Self::Refactor => "refactor",
29 Self::TestAddition => "test_addition",
30 Self::Documentation => "documentation",
31 Self::Investigation => "investigation",
32 }
33 }
34}
35
36impl std::fmt::Display for TaskClass {
37 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 formatter.write_str(self.as_str())
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct SignalRequirement {
45 pub signal_type: SignalType,
46 pub required: bool,
47 pub weight_milli: u32,
49}
50
51impl SignalRequirement {
52 pub const fn required(signal_type: SignalType, weight_milli: u32) -> Self {
54 Self {
55 signal_type,
56 required: true,
57 weight_milli,
58 }
59 }
60
61 pub const fn optional(signal_type: SignalType, weight_milli: u32) -> Self {
63 Self {
64 signal_type,
65 required: false,
66 weight_milli,
67 }
68 }
69
70 pub const fn new(signal_type: SignalType, required: bool, weight_milli: u32) -> Self {
72 Self {
73 signal_type,
74 required,
75 weight_milli,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct OutcomeContractV1 {
83 pub task_class: TaskClass,
84 pub contract_version: String,
85 pub required_signals: Vec<SignalRequirement>,
86 pub optional_signals: Vec<SignalRequirement>,
87 pub expiry_window_hours: u32,
88 pub supersession_allowed: bool,
89}
90
91impl OutcomeContractV1 {
92 pub fn for_task_class(task_class: TaskClass) -> Self {
94 match task_class {
95 TaskClass::BugFix => bug_fix_contract(),
96 TaskClass::Refactor => refactor_contract(),
97 TaskClass::TestAddition => test_addition_contract(),
98 TaskClass::Documentation => documentation_contract(),
99 TaskClass::Investigation => investigation_contract(),
100 }
101 }
102
103 pub fn reference(&self) -> String {
105 format!(
106 "outcome-contract/{}/v{}",
107 self.task_class.as_str(),
108 self.contract_version
109 )
110 }
111
112 pub fn validate(&self) -> Result<(), String> {
114 if self.contract_version.trim().is_empty() {
115 return Err("contract_version must not be empty".to_owned());
116 }
117
118 if self
119 .required_signals
120 .iter()
121 .chain(self.optional_signals.iter())
122 .any(|requirement| requirement.weight_milli > 1000)
123 {
124 return Err("signal weight_milli must be between 0 and 1000".to_owned());
125 }
126
127 if self
128 .optional_signals
129 .iter()
130 .any(|requirement| requirement.required)
131 {
132 return Err("optional signal requirements must set required=false".to_owned());
133 }
134
135 Ok(())
136 }
137}
138
139pub fn bug_fix_contract() -> OutcomeContractV1 {
141 OutcomeContractV1 {
142 task_class: TaskClass::BugFix,
143 contract_version: OUTCOME_CONTRACT_VERSION.to_owned(),
144 required_signals: vec![
145 SignalRequirement::required(SignalType::BuildSuccess, 500),
146 SignalRequirement::required(SignalType::TestsPassing, 500),
147 ],
148 optional_signals: vec![SignalRequirement::optional(SignalType::PrMerge, 100)],
149 expiry_window_hours: 168,
150 supersession_allowed: true,
151 }
152}
153
154pub fn bug_fix() -> OutcomeContractV1 {
156 bug_fix_contract()
157}
158
159pub fn refactor_contract() -> OutcomeContractV1 {
161 OutcomeContractV1 {
162 task_class: TaskClass::Refactor,
163 contract_version: OUTCOME_CONTRACT_VERSION.to_owned(),
164 required_signals: vec![
165 SignalRequirement::required(SignalType::BuildSuccess, 334),
166 SignalRequirement::required(SignalType::TestsPassing, 333),
167 SignalRequirement::required(SignalType::LintClean, 333),
168 ],
169 optional_signals: vec![SignalRequirement::optional(
170 SignalType::TypecheckPassing,
171 100,
172 )],
173 expiry_window_hours: 168,
174 supersession_allowed: true,
175 }
176}
177
178pub fn refactor() -> OutcomeContractV1 {
180 refactor_contract()
181}
182
183pub fn test_addition_contract() -> OutcomeContractV1 {
189 OutcomeContractV1 {
190 task_class: TaskClass::TestAddition,
191 contract_version: OUTCOME_CONTRACT_VERSION.to_owned(),
192 required_signals: vec![
193 SignalRequirement::required(SignalType::TestsPassing, 600),
194 SignalRequirement::required(SignalType::TestsPassing, 400),
195 ],
196 optional_signals: vec![SignalRequirement::optional(SignalType::CiPassing, 100)],
197 expiry_window_hours: 336,
198 supersession_allowed: true,
199 }
200}
201
202pub fn test_addition() -> OutcomeContractV1 {
204 test_addition_contract()
205}
206
207pub fn documentation_contract() -> OutcomeContractV1 {
209 OutcomeContractV1 {
210 task_class: TaskClass::Documentation,
211 contract_version: OUTCOME_CONTRACT_VERSION.to_owned(),
212 required_signals: vec![SignalRequirement::required(SignalType::BuildSuccess, 1000)],
213 optional_signals: Vec::new(),
214 expiry_window_hours: 720,
215 supersession_allowed: true,
216 }
217}
218
219pub fn documentation() -> OutcomeContractV1 {
221 documentation_contract()
222}
223
224pub fn investigation_contract() -> OutcomeContractV1 {
230 OutcomeContractV1 {
231 task_class: TaskClass::Investigation,
232 contract_version: OUTCOME_CONTRACT_VERSION.to_owned(),
233 required_signals: vec![SignalRequirement::required(
234 SignalType::HumanAcceptance,
235 1000,
236 )],
237 optional_signals: vec![SignalRequirement::optional(
238 SignalType::AgentCompletion,
239 1000,
240 )],
241 expiry_window_hours: 168,
242 supersession_allowed: true,
243 }
244}
245
246pub fn investigation() -> OutcomeContractV1 {
248 investigation_contract()
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn canonical_contracts_have_distinct_task_classes() {
257 let contracts = [
258 bug_fix_contract(),
259 refactor_contract(),
260 test_addition_contract(),
261 documentation_contract(),
262 investigation_contract(),
263 ];
264
265 assert_eq!(
266 contracts
267 .iter()
268 .map(|contract| contract.task_class)
269 .collect::<std::collections::HashSet<_>>()
270 .len(),
271 5
272 );
273 assert!(contracts.iter().all(|contract| contract.validate().is_ok()));
274 }
275}