lean_ctx_protocol/
outcome_engine.rs1use crate::{AcceptanceState, ControlPlaneDecision, OutcomeSignalsV1, TaskId, TaskProfileV1};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct QualityDimensionsV2 {
10 pub correctness_milli: u16,
11 pub completeness_milli: u16,
12 pub maintainability_milli: u16,
13 pub efficiency_milli: u16,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct OutcomeSignalV2 {
20 pub task_id: TaskId,
21 pub accepted: AcceptanceState,
22 pub quality_score_milli: u16,
23 pub quality_dimensions: QualityDimensionsV2,
24 pub signals: OutcomeSignalsV1,
25 pub observed_at: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct LearningEvent {
32 pub task_id: TaskId,
33 pub decision_taken: ControlPlaneDecision,
34 pub outcome_observed: OutcomeSignalV2,
35 pub counterfactual_estimate: Option<f64>,
36}
37
38pub trait OutcomeEngineContract {
40 fn observe(&mut self, event: LearningEvent);
42
43 fn predict_outcome(&self, task_profile: &TaskProfileV1) -> f64;
45}
46
47#[derive(Debug, Default, Clone, PartialEq)]
49pub struct LocalOutcomeEngine {
50 events: Vec<LearningEvent>,
51}
52
53impl LocalOutcomeEngine {
54 pub fn events(&self) -> &[LearningEvent] {
56 &self.events
57 }
58}
59
60impl OutcomeEngineContract for LocalOutcomeEngine {
61 fn observe(&mut self, event: LearningEvent) {
62 self.events.push(event);
63 }
64
65 fn predict_outcome(&self, _task_profile: &TaskProfileV1) -> f64 {
66 0.0
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn id<T>(value: &str) -> T
75 where
76 T: TryFrom<String>,
77 <T as TryFrom<String>>::Error: std::fmt::Debug,
78 {
79 T::try_from(value.to_owned()).expect("identifier should be valid")
80 }
81
82 fn event() -> LearningEvent {
83 LearningEvent {
84 task_id: id("task-1"),
85 decision_taken: ControlPlaneDecision {
86 selected_model: "local/default".to_owned(),
87 selected_provider: "local".to_owned(),
88 reasoning_budget: 1,
89 context_adjustments: Vec::new(),
90 confidence: 900,
91 },
92 outcome_observed: OutcomeSignalV2 {
93 task_id: id("task-1"),
94 accepted: AcceptanceState::Accepted,
95 quality_score_milli: 900,
96 quality_dimensions: QualityDimensionsV2 {
97 correctness_milli: 900,
98 completeness_milli: 900,
99 maintainability_milli: 900,
100 efficiency_milli: 900,
101 },
102 signals: OutcomeSignalsV1 {
103 build: None,
104 tests: None,
105 lint: None,
106 typecheck: None,
107 completion: None,
108 pr: None,
109 correction: None,
110 rollback: None,
111 retry: None,
112 },
113 observed_at: "2026-08-12T00:00:00Z".to_owned(),
114 },
115 counterfactual_estimate: None,
116 }
117 }
118
119 #[test]
120 fn local_outcome_engine_stores_events_without_prediction() {
121 let mut engine = LocalOutcomeEngine::default();
122 engine.observe(event());
123 assert_eq!(engine.events().len(), 1);
124 assert_eq!(engine.predict_outcome(&TaskProfileV1::default()), 0.0);
125 }
126
127 #[test]
128 fn contract_is_object_safe() {
129 let mut engine: Box<dyn OutcomeEngineContract> = Box::new(LocalOutcomeEngine::default());
130 engine.observe(event());
131 assert_eq!(engine.predict_outcome(&TaskProfileV1::default()), 0.0);
132 }
133}