Skip to main content

clawft_kernel/
lewm_invariant.rs

1//! LeWM ↔ ECC decoupling invariants (WEFT-519 / ADR-090).
2//!
3//! The latent world model (LeWM) is an **additive consumer** of sensor and
4//! ECC state. It must never become authoritative for causal reasoning, nor
5//! short-circuit causal edges. This module codifies the five formal rules
6//! as testable predicates and provides debug-time runtime checks for a
7//! thin world-model facade.
8//!
9//! Full LeWM crates land later; until then these checks guard API seams
10//! that accept world-model contributions into the kernel (impulses,
11//! observations). See `docs/adr/adr-090-lewm-ecc-decoupling-invariant.md`.
12
13use serde::{Deserialize, Serialize};
14
15/// The five formal decoupling rules (ADR-090).
16#[repr(u8)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum DecouplingRule {
20    /// R1 — ECC is the sole authoritative causal reasoner per node.
21    EccAuthoritative = 1,
22    /// R2 — World model is optional; sensor+ECC paths run without it.
23    WorldModelOptional = 2,
24    /// R3 — WM may publish impulses / observations only; never rewrite edges.
25    ImpulseOnlyWritePath = 3,
26    /// R4 — WM must not short-circuit a causal edge (no bypass of link/unlink).
27    NoCausalShortCircuit = 4,
28    /// R5 — Absent cluster/WM: local-ECC-only behaviour is unchanged.
29    GracefulDegradation = 5,
30}
31
32impl DecouplingRule {
33    pub const ALL: &'static [DecouplingRule] = &[
34        Self::EccAuthoritative,
35        Self::WorldModelOptional,
36        Self::ImpulseOnlyWritePath,
37        Self::NoCausalShortCircuit,
38        Self::GracefulDegradation,
39    ];
40
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Self::EccAuthoritative => "ecc_authoritative",
44            Self::WorldModelOptional => "world_model_optional",
45            Self::ImpulseOnlyWritePath => "impulse_only_write_path",
46            Self::NoCausalShortCircuit => "no_causal_short_circuit",
47            Self::GracefulDegradation => "graceful_degradation",
48        }
49    }
50}
51
52/// Classification of a proposed world-model write into the kernel.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum WmWriteKind {
56    /// Publish an impulse / observation (allowed).
57    Impulse,
58    /// Inject a sensor observation packet (allowed).
59    Observation,
60    /// Attempt to insert/rewrite a causal edge directly (forbidden).
61    CausalEdgeMutate,
62    /// Attempt to replace ECC reasoning result (forbidden).
63    ReasoningOverride,
64    /// Attempt to skip / short-circuit an existing edge evaluation (forbidden).
65    ShortCircuit,
66}
67
68/// Result of a decoupling check.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct InvariantCheck {
71    pub rule: DecouplingRule,
72    pub ok: bool,
73    pub detail: String,
74}
75
76/// Evaluate whether a world-model write is allowed under the five rules.
77pub fn check_wm_write(kind: WmWriteKind) -> Result<(), InvariantCheck> {
78    match kind {
79        WmWriteKind::Impulse | WmWriteKind::Observation => Ok(()),
80        WmWriteKind::CausalEdgeMutate => Err(InvariantCheck {
81            rule: DecouplingRule::ImpulseOnlyWritePath,
82            ok: false,
83            detail: "world model must not mutate causal edges directly".into(),
84        }),
85        WmWriteKind::ReasoningOverride => Err(InvariantCheck {
86            rule: DecouplingRule::EccAuthoritative,
87            ok: false,
88            detail: "world model must not override ECC reasoning".into(),
89        }),
90        WmWriteKind::ShortCircuit => Err(InvariantCheck {
91            rule: DecouplingRule::NoCausalShortCircuit,
92            ok: false,
93            detail: "world model must not short-circuit causal edges".into(),
94        }),
95    }
96}
97
98/// Predicate R2/R5: kernel without WM remains valid (always true for stub).
99pub fn local_ecc_sufficient_without_wm() -> bool {
100    true
101}
102
103/// Thin facade for future world-model service hooks (runtime assertions).
104#[derive(Debug, Default)]
105pub struct WorldModelFacade {
106    /// When true, `debug_assert` / explicit checks reject forbidden writes.
107    pub enforce: bool,
108    /// Count of rejected forbidden writes (for tests / metrics).
109    pub rejected: u64,
110    /// Count of accepted impulse/observation writes.
111    pub accepted: u64,
112}
113
114impl WorldModelFacade {
115    pub fn new(enforce: bool) -> Self {
116        Self {
117            enforce,
118            rejected: 0,
119            accepted: 0,
120        }
121    }
122
123    /// Submit a write; returns `Err` when the decoupling invariant rejects it.
124    pub fn submit(&mut self, kind: WmWriteKind) -> Result<(), InvariantCheck> {
125        match check_wm_write(kind) {
126            Ok(()) => {
127                self.accepted += 1;
128                Ok(())
129            }
130            Err(e) => {
131                self.rejected += 1;
132                // Debug builds outside of unit tests: hard-fail so invariant
133                // violations surface immediately during local development.
134                // Under `cfg(test)` we only count + return Err so property
135                // tests can exercise the reject path without panicking.
136                #[cfg(all(debug_assertions, not(test)))]
137                if self.enforce {
138                    panic!(
139                        "LeWM decoupling invariant violated: {:?} — {}",
140                        e.rule, e.detail
141                    );
142                }
143                let _ = self.enforce;
144                Err(e)
145            }
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn five_rules_enumerated() {
156        assert_eq!(DecouplingRule::ALL.len(), 5);
157        assert_eq!(DecouplingRule::EccAuthoritative as u8, 1);
158        assert_eq!(DecouplingRule::GracefulDegradation as u8, 5);
159    }
160
161    #[test]
162    fn allows_impulse_and_observation() {
163        assert!(check_wm_write(WmWriteKind::Impulse).is_ok());
164        assert!(check_wm_write(WmWriteKind::Observation).is_ok());
165    }
166
167    #[test]
168    fn rejects_short_circuit() {
169        let err = check_wm_write(WmWriteKind::ShortCircuit).unwrap_err();
170        assert_eq!(err.rule, DecouplingRule::NoCausalShortCircuit);
171        assert!(!err.ok);
172    }
173
174    #[test]
175    fn rejects_edge_mutate_and_override() {
176        assert_eq!(
177            check_wm_write(WmWriteKind::CausalEdgeMutate)
178                .unwrap_err()
179                .rule,
180            DecouplingRule::ImpulseOnlyWritePath
181        );
182        assert_eq!(
183            check_wm_write(WmWriteKind::ReasoningOverride)
184                .unwrap_err()
185                .rule,
186            DecouplingRule::EccAuthoritative
187        );
188    }
189
190    #[test]
191    fn facade_counts_rejections() {
192        let mut f = WorldModelFacade::new(true);
193        assert!(f.submit(WmWriteKind::Impulse).is_ok());
194        assert!(f.submit(WmWriteKind::ShortCircuit).is_err());
195        assert_eq!(f.accepted, 1);
196        assert_eq!(f.rejected, 1);
197    }
198
199    #[test]
200    fn property_forbidden_short_circuit_never_accepted() {
201        // Property-style: for any facade, short-circuit is always rejected.
202        for enforce in [false, true] {
203            let mut f = WorldModelFacade::new(enforce);
204            assert!(f.submit(WmWriteKind::ShortCircuit).is_err());
205        }
206    }
207
208    #[test]
209    fn graceful_degradation_predicate() {
210        assert!(local_ecc_sufficient_without_wm());
211    }
212}