Skip to main content

aidens_kernel_kit/
regional_decoder.rs

1//! P11 — Regional Decoder Kernel types.
2//!
3//! Bounded execution region definitions, residual envelopes, syndrome
4//! envelopes, and convergence reports. These types enable iterative runtime
5//! convergence with stop rules and oscillation/degradation detection.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Bounded execution region definition. Defines a subgraph that can be
11/// iterated independently and checked for convergence.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RegionContractV1 {
14    /// Unique region identifier.
15    pub region_id: String,
16    /// Content digest of the region's compiled subgraph.
17    pub region_digest_id: String,
18    /// Node IDs within this region.
19    pub node_ids: Vec<String>,
20    /// Hyperedge IDs within this region.
21    pub hyperedge_ids: Vec<String>,
22    /// Constraint IDs applicable to this region.
23    pub constraint_ids: Vec<String>,
24    /// Whether this region is a bounded default unit of work.
25    pub bounded_default_unit_of_work: bool,
26    /// Maximum iterations before forced stop.
27    pub max_iterations: u32,
28    /// Convergence tolerance (change below this = converged).
29    pub convergence_tolerance: f64,
30}
31
32/// Expected-vs-observed mismatch envelope. Captures the residual
33/// between what the kernel expected and what it observed.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ResidualEnvelopeV1 {
36    /// Region this residual applies to.
37    pub region_id: String,
38    /// Expected values (node_id -> expected output).
39    pub expected: HashMap<String, f64>,
40    /// Observed values (node_id -> actual output).
41    pub observed: HashMap<String, f64>,
42    /// Computed residuals (node_id -> |expected - observed|).
43    pub residuals: HashMap<String, f64>,
44    /// Max residual across all nodes.
45    pub max_residual: f64,
46    /// Whether the residual is within tolerance.
47    pub within_tolerance: bool,
48}
49
50/// Violated constraint bundle. Captures which constraints
51/// were violated during execution and why.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SyndromeEnvelopeV1 {
54    /// Region this syndrome was detected in.
55    pub region_id: String,
56    /// Constraint IDs that were violated.
57    pub violated_constraint_ids: Vec<String>,
58    /// Human-readable descriptions of each violation.
59    pub violation_descriptions: Vec<String>,
60    /// Whether the syndrome is recoverable (retryable) or terminal.
61    pub recoverable: bool,
62}
63
64/// Iterative runtime convergence evidence. Reports whether
65/// a region's iterative execution converged, oscillated, or degraded.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RegionConvergenceReportV1 {
68    /// Region that was iterated.
69    pub region_id: String,
70    /// Number of iterations executed.
71    pub iterations: u32,
72    /// Final convergence state.
73    pub convergence_state: ConvergenceState,
74    /// Residual history (iteration -> max_residual).
75    pub residual_history: Vec<f64>,
76    /// Stop reason.
77    pub stop_reason: ConvergenceStopReason,
78    /// Time elapsed in milliseconds.
79    pub elapsed_ms: u64,
80}
81
82/// Convergence state after iterative execution.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum ConvergenceState {
85    /// Residuals decreased below tolerance.
86    Converged,
87    /// Residuals oscillated without converging.
88    Oscillating,
89    /// Residuals increased (diverged).
90    Diverging,
91    /// Hit max iterations before converging.
92    MaxIterationsReached,
93    /// Detected a constraint violation (syndrome).
94    SyndromeDetected,
95}
96
97/// Why the iteration stopped.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99pub enum ConvergenceStopReason {
100    /// Converged within tolerance.
101    Converged,
102    /// Max iterations reached.
103    MaxIterations,
104    /// Detected oscillation.
105    OscillationDetected,
106    /// Detected divergence.
107    DivergenceDetected,
108    /// Syndrome (constraint violation) detected.
109    Syndrome,
110    /// Budget exhausted.
111    BudgetExhausted,
112}
113
114impl RegionConvergenceReportV1 {
115    /// Whether the region converged successfully.
116    pub fn is_converged(&self) -> bool {
117        matches!(self.convergence_state, ConvergenceState::Converged)
118    }
119
120    /// Whether the region exhibited a problematic state.
121    pub fn is_problematic(&self) -> bool {
122        matches!(
123            self.convergence_state,
124            ConvergenceState::Oscillating
125                | ConvergenceState::Diverging
126                | ConvergenceState::SyndromeDetected
127        )
128    }
129}
130
131/// Detect convergence from a residual history.
132pub fn classify_convergence(
133    residual_history: &[f64],
134    tolerance: f64,
135    max_iterations: u32,
136) -> (ConvergenceState, ConvergenceStopReason) {
137    if residual_history.is_empty() {
138        return (
139            ConvergenceState::MaxIterationsReached,
140            ConvergenceStopReason::MaxIterations,
141        );
142    }
143
144    let last = *residual_history.last().unwrap();
145
146    if last < tolerance {
147        return (
148            ConvergenceState::Converged,
149            ConvergenceStopReason::Converged,
150        );
151    }
152
153    if residual_history.len() >= max_iterations as usize {
154        // Check for oscillation (last 3 values alternate)
155        if residual_history.len() >= 4 {
156            let n = residual_history.len();
157            let a = residual_history[n - 4];
158            let b = residual_history[n - 3];
159            let c = residual_history[n - 2];
160            let d = residual_history[n - 1];
161            if (a > b && b < c && c > d) || (a < b && b > c && c < d) {
162                return (
163                    ConvergenceState::Oscillating,
164                    ConvergenceStopReason::OscillationDetected,
165                );
166            }
167        }
168
169        // Check for divergence (residuals increasing)
170        if residual_history.len() >= 3 {
171            let n = residual_history.len();
172            if residual_history[n - 3] < residual_history[n - 2]
173                && residual_history[n - 2] < residual_history[n - 1]
174            {
175                return (
176                    ConvergenceState::Diverging,
177                    ConvergenceStopReason::DivergenceDetected,
178                );
179            }
180        }
181
182        return (
183            ConvergenceState::MaxIterationsReached,
184            ConvergenceStopReason::MaxIterations,
185        );
186    }
187
188    (
189        ConvergenceState::MaxIterationsReached,
190        ConvergenceStopReason::MaxIterations,
191    )
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn converged_state_is_converged() {
200        let report = RegionConvergenceReportV1 {
201            region_id: "r1".into(),
202            iterations: 5,
203            convergence_state: ConvergenceState::Converged,
204            residual_history: vec![1.0, 0.5, 0.1, 0.01, 0.001],
205            stop_reason: ConvergenceStopReason::Converged,
206            elapsed_ms: 42,
207        };
208        assert!(report.is_converged());
209        assert!(!report.is_problematic());
210    }
211
212    #[test]
213    fn oscillating_state_is_problematic() {
214        let report = RegionConvergenceReportV1 {
215            region_id: "r1".into(),
216            iterations: 10,
217            convergence_state: ConvergenceState::Oscillating,
218            residual_history: vec![0.5, 0.1, 0.5, 0.1, 0.5],
219            stop_reason: ConvergenceStopReason::OscillationDetected,
220            elapsed_ms: 100,
221        };
222        assert!(!report.is_converged());
223        assert!(report.is_problematic());
224    }
225
226    #[test]
227    fn classify_convergence_detects_converged() {
228        let history = vec![1.0, 0.5, 0.1, 0.001];
229        let (state, reason) = classify_convergence(&history, 0.01, 100);
230        assert_eq!(state, ConvergenceState::Converged);
231        assert_eq!(reason, ConvergenceStopReason::Converged);
232    }
233
234    #[test]
235    fn classify_convergence_detects_oscillation() {
236        let history = vec![0.5, 0.1, 0.5, 0.1, 0.5];
237        let (state, reason) = classify_convergence(&history, 0.01, 4);
238        assert_eq!(state, ConvergenceState::Oscillating);
239        assert_eq!(reason, ConvergenceStopReason::OscillationDetected);
240    }
241
242    #[test]
243    fn classify_convergence_detects_divergence() {
244        let history = vec![0.1, 0.2, 0.5, 0.8];
245        let (state, reason) = classify_convergence(&history, 0.01, 3);
246        assert_eq!(state, ConvergenceState::Diverging);
247        assert_eq!(reason, ConvergenceStopReason::DivergenceDetected);
248    }
249
250    #[test]
251    fn classify_convergence_detects_max_iterations() {
252        let history = vec![0.5, 0.4, 0.3, 0.25, 0.2];
253        let (state, reason) = classify_convergence(&history, 0.01, 4);
254        assert_eq!(state, ConvergenceState::MaxIterationsReached);
255        assert_eq!(reason, ConvergenceStopReason::MaxIterations);
256    }
257
258    #[test]
259    fn region_contract_serializes() {
260        let contract = RegionContractV1 {
261            region_id: "region-01".into(),
262            region_digest_id: "digest-01".into(),
263            node_ids: vec!["node-a".into(), "node-b".into()],
264            hyperedge_ids: vec!["edge-01".into()],
265            constraint_ids: vec!["constraint:edge-01".into()],
266            bounded_default_unit_of_work: true,
267            max_iterations: 100,
268            convergence_tolerance: 0.01,
269        };
270        let json = serde_json::to_string(&contract).unwrap();
271        let deserialized: RegionContractV1 = serde_json::from_str(&json).unwrap();
272        assert_eq!(contract.region_id, deserialized.region_id);
273        assert_eq!(contract.max_iterations, deserialized.max_iterations);
274    }
275
276    #[test]
277    fn residual_envelope_computes_within_tolerance() {
278        let mut expected = HashMap::new();
279        expected.insert("a".into(), 1.0);
280        expected.insert("b".into(), 2.0);
281        let mut observed = HashMap::new();
282        observed.insert("a".into(), 1.01);
283        observed.insert("b".into(), 2.02);
284
285        let mut residuals = HashMap::new();
286        residuals.insert("a".into(), 0.01);
287        residuals.insert("b".into(), 0.02);
288
289        let envelope = ResidualEnvelopeV1 {
290            region_id: "r1".into(),
291            expected,
292            observed,
293            residuals,
294            max_residual: 0.02,
295            within_tolerance: true,
296        };
297        assert!(envelope.within_tolerance);
298    }
299
300    #[test]
301    fn syndrome_envelope_marks_recoverable() {
302        let syndrome = SyndromeEnvelopeV1 {
303            region_id: "r1".into(),
304            violated_constraint_ids: vec!["c1".into()],
305            violation_descriptions: vec!["constraint c1 violated".into()],
306            recoverable: true,
307        };
308        assert!(syndrome.recoverable);
309    }
310}