Skip to main content

nexcore_cybercinetics/
lib.rs

1#![doc = "Cyber-Cinetics: typed feedback controller encoding ∂(→(ν, ς, ρ))"]
2#![doc = ""]
3#![doc = "Maps the primitive composition ∂(→(ν, ς, ρ)) to a Rust type system:"]
4#![doc = "- ν (frequency) — oscillation rate of the control loop"]
5#![doc = "- ς (state) — current system snapshot"]
6#![doc = "- ρ (recursion) — self-referential observation depth"]
7#![doc = "- → (causality) — cause-effect chain linking input to output"]
8#![doc = "- ∂ (boundary) — the controller envelope constraining all above"]
9#![doc = ""]
10#![doc = "Primary use: hook-binary feedback linking where hooks observe"]
11#![doc = "system state and binaries act on it, creating a closed loop."]
12#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
13#![forbid(unsafe_code)]
14
15use serde::{Deserialize, Serialize};
16use std::fmt;
17
18/// ν — Frequency of the control loop (Hz or iterations/sec).
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct Nu {
21    pub rate: f64,
22    pub floor: f64,
23}
24
25impl Nu {
26    pub fn new(rate: f64, floor: f64) -> Self {
27        Self { rate, floor }
28    }
29
30    pub fn is_decayed(&self) -> bool {
31        self.rate < self.floor
32    }
33
34    pub fn health_ratio(&self) -> f64 {
35        if self.floor <= 0.0 {
36            return f64::INFINITY;
37        }
38        self.rate / self.floor
39    }
40}
41
42/// ς — State snapshot of the controlled system.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub struct Sigma<S: Clone + PartialEq> {
45    pub value: S,
46    pub tick: u64,
47}
48
49impl<S: Clone + PartialEq> Sigma<S> {
50    pub fn new(value: S) -> Self {
51        Self { value, tick: 0 }
52    }
53
54    pub fn transition(&mut self, next: S) {
55        self.value = next;
56        self.tick += 1;
57    }
58
59    pub fn has_transitioned(&self) -> bool {
60        self.tick > 0
61    }
62}
63
64/// ρ — Recursion depth for self-referential observation.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Rho {
67    pub depth: u8,
68    pub ceiling: u8,
69}
70
71impl Rho {
72    pub fn new(ceiling: u8) -> Self {
73        Self { depth: 0, ceiling }
74    }
75
76    pub fn deepen(&mut self) -> bool {
77        if self.depth < self.ceiling {
78            self.depth += 1;
79            true
80        } else {
81            false
82        }
83    }
84
85    pub fn surface(&mut self) {
86        self.depth = 0;
87    }
88
89    pub fn is_saturated(&self) -> bool {
90        self.depth >= self.ceiling
91    }
92}
93
94/// A single cause-effect link in the causal chain.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CausalLink {
97    pub cause: String,
98    pub effect: String,
99    pub fidelity: f64,
100}
101
102/// → — Causal chain from input to output.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Arrow {
105    links: Vec<CausalLink>,
106}
107
108impl Arrow {
109    pub fn new() -> Self {
110        Self { links: Vec::new() }
111    }
112
113    pub fn push(&mut self, cause: impl Into<String>, effect: impl Into<String>, fidelity: f64) {
114        self.links.push(CausalLink {
115            cause: cause.into(),
116            effect: effect.into(),
117            fidelity: fidelity.clamp(0.0, 1.0),
118        });
119    }
120
121    /// F_total = Product(F_i) across all hops.
122    pub fn f_total(&self) -> f64 {
123        if self.links.is_empty() {
124            return 0.0;
125        }
126        self.links.iter().map(|l| l.fidelity).product()
127    }
128
129    pub fn len(&self) -> usize {
130        self.links.len()
131    }
132
133    pub fn is_empty(&self) -> bool {
134        self.links.is_empty()
135    }
136
137    pub fn weakest(&self) -> Option<&CausalLink> {
138        self.links.iter().min_by(|a, b| {
139            a.fidelity
140                .partial_cmp(&b.fidelity)
141                .unwrap_or(std::cmp::Ordering::Equal)
142        })
143    }
144}
145
146impl Default for Arrow {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152/// Controller verdict after one tick of the feedback loop.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub enum Verdict {
155    Stable,
156    FrequencyDecay,
157    FidelityDegraded,
158    RecursionSaturated,
159    Compound(Vec<Verdict>),
160}
161
162impl fmt::Display for Verdict {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            Verdict::Stable => write!(f, "STABLE"),
166            Verdict::FrequencyDecay => write!(f, "FREQ_DECAY"),
167            Verdict::FidelityDegraded => write!(f, "FIDELITY_DEGRADED"),
168            Verdict::RecursionSaturated => write!(f, "RECURSION_SATURATED"),
169            Verdict::Compound(vs) => {
170                let labels: Vec<String> = vs.iter().map(|v| v.to_string()).collect();
171                write!(f, "COMPOUND({})", labels.join("+"))
172            }
173        }
174    }
175}
176
177/// ∂(→(ν, ς, ρ)) — The feedback controller.
178///
179/// Parametric over the state type `S`. The boundary (∂) wraps
180/// the causal chain (→) which links frequency (ν), state (ς),
181/// and recursion (ρ).
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Controller<S: Clone + PartialEq> {
184    pub nu: Nu,
185    pub sigma: Sigma<S>,
186    pub rho: Rho,
187    pub arrow: Arrow,
188    pub f_min: f64,
189}
190
191impl<S: Clone + PartialEq + fmt::Debug> Controller<S> {
192    pub fn new(initial_state: S, nu_rate: f64, nu_floor: f64, rho_ceiling: u8, f_min: f64) -> Self {
193        Self {
194            nu: Nu::new(nu_rate, nu_floor),
195            sigma: Sigma::new(initial_state),
196            rho: Rho::new(rho_ceiling),
197            arrow: Arrow::new(),
198            f_min,
199        }
200    }
201
202    pub fn tick(&mut self) -> Verdict {
203        let mut issues = Vec::new();
204
205        if self.nu.is_decayed() {
206            issues.push(Verdict::FrequencyDecay);
207        }
208
209        if !self.arrow.is_empty() && self.arrow.f_total() < self.f_min {
210            issues.push(Verdict::FidelityDegraded);
211        }
212
213        if self.rho.is_saturated() {
214            issues.push(Verdict::RecursionSaturated);
215        }
216
217        match issues.len() {
218            0 => Verdict::Stable,
219            1 => issues.into_iter().next().unwrap_or(Verdict::Stable),
220            _ => Verdict::Compound(issues),
221        }
222    }
223
224    pub fn act(
225        &mut self,
226        next_state: S,
227        cause: impl Into<String>,
228        effect: impl Into<String>,
229        fidelity: f64,
230    ) {
231        self.sigma.transition(next_state);
232        self.arrow.push(cause, effect, fidelity);
233    }
234
235    pub fn observe(&mut self) -> bool {
236        self.rho.deepen()
237    }
238
239    pub fn surface(&mut self) {
240        self.rho.surface();
241    }
242
243    pub fn measure_frequency(&mut self, rate: f64) {
244        self.nu.rate = rate;
245    }
246}
247
248/// A hook-binary pair in the feedback loop.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct HookBinding {
251    pub hook: String,
252    pub binary: String,
253    pub event: String,
254    pub fidelity: f64,
255}
256
257impl HookBinding {
258    pub fn new(
259        hook: impl Into<String>,
260        binary: impl Into<String>,
261        event: impl Into<String>,
262    ) -> Self {
263        Self {
264            hook: hook.into(),
265            binary: binary.into(),
266            event: event.into(),
267            fidelity: 1.0,
268        }
269    }
270
271    pub fn degrade(&mut self, factor: f64) {
272        self.fidelity = (self.fidelity * factor).clamp(0.0, 1.0);
273    }
274
275    pub fn restore(&mut self) {
276        self.fidelity = 1.0;
277    }
278}
279
280/// Registry of hook-binary bindings governed by a controller.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct BindingRegistry<S: Clone + PartialEq> {
283    pub controller: Controller<S>,
284    pub bindings: Vec<HookBinding>,
285}
286
287impl<S: Clone + PartialEq + fmt::Debug> BindingRegistry<S> {
288    pub fn new(controller: Controller<S>) -> Self {
289        Self {
290            controller,
291            bindings: Vec::new(),
292        }
293    }
294
295    pub fn register(&mut self, binding: HookBinding) {
296        self.bindings.push(binding);
297    }
298
299    pub fn aggregate_fidelity(&self) -> f64 {
300        if self.bindings.is_empty() {
301            return 0.0;
302        }
303        self.bindings.iter().map(|b| b.fidelity).product()
304    }
305
306    pub fn degraded_bindings(&self, threshold: f64) -> Vec<&HookBinding> {
307        self.bindings
308            .iter()
309            .filter(|b| b.fidelity < threshold)
310            .collect()
311    }
312
313    /// Batch degrade all bindings that missed their last execution window.
314    ///
315    /// `factor` is the degradation multiplier (0.0–1.0) applied to each
316    /// binding whose fidelity is above `floor`. Bindings already at or
317    /// below `floor` are left unchanged.
318    ///
319    /// Returns the count of bindings that were degraded.
320    pub fn decay_all(&mut self, factor: f64, floor: f64) -> usize {
321        let factor = factor.clamp(0.0, 1.0);
322        let floor = floor.clamp(0.0, 1.0);
323        let mut count = 0;
324        for binding in &mut self.bindings {
325            if binding.fidelity > floor {
326                binding.fidelity = (binding.fidelity * factor).max(floor);
327                count += 1;
328            }
329        }
330        count
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn nu_health_ratio() {
340        let nu = Nu::new(10.0, 5.0);
341        assert!(!nu.is_decayed());
342        assert!((nu.health_ratio() - 2.0).abs() < f64::EPSILON);
343
344        let decayed = Nu::new(3.0, 5.0);
345        assert!(decayed.is_decayed());
346    }
347
348    #[test]
349    fn sigma_transitions() {
350        let mut sigma = Sigma::new("idle");
351        assert!(!sigma.has_transitioned());
352        sigma.transition("active");
353        assert!(sigma.has_transitioned());
354        assert_eq!(sigma.tick, 1);
355        assert_eq!(sigma.value, "active");
356    }
357
358    #[test]
359    fn rho_depth_ceiling() {
360        let mut rho = Rho::new(2);
361        assert!(!rho.is_saturated());
362        assert!(rho.deepen());
363        assert!(rho.deepen());
364        assert!(rho.is_saturated());
365        assert!(!rho.deepen());
366        rho.surface();
367        assert_eq!(rho.depth, 0);
368    }
369
370    #[test]
371    fn arrow_fidelity_composition() {
372        let mut arrow = Arrow::new();
373        arrow.push("hook", "binary", 0.95);
374        arrow.push("binary", "state", 0.90);
375        assert!((arrow.f_total() - 0.855).abs() < 1e-9);
376        assert_eq!(arrow.len(), 2);
377    }
378
379    #[test]
380    fn arrow_weakest_link() {
381        let mut arrow = Arrow::new();
382        arrow.push("a", "b", 0.95);
383        arrow.push("b", "c", 0.70);
384        arrow.push("c", "d", 0.85);
385        let weakest = arrow.weakest();
386        assert!(weakest.is_some());
387        assert!((weakest.map(|w| w.fidelity).unwrap_or(0.0) - 0.70).abs() < f64::EPSILON);
388    }
389
390    #[test]
391    fn controller_stable_verdict() {
392        let mut ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
393        assert_eq!(ctrl.tick(), Verdict::Stable);
394    }
395
396    #[test]
397    fn controller_frequency_decay() {
398        let mut ctrl: Controller<&str> = Controller::new("idle", 3.0, 5.0, 3, 0.80);
399        assert_eq!(ctrl.tick(), Verdict::FrequencyDecay);
400    }
401
402    #[test]
403    fn controller_fidelity_degraded() {
404        let mut ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
405        ctrl.arrow.push("a", "b", 0.5);
406        ctrl.arrow.push("b", "c", 0.5);
407        assert_eq!(ctrl.tick(), Verdict::FidelityDegraded);
408    }
409
410    #[test]
411    fn controller_compound_verdict() {
412        let mut ctrl: Controller<&str> = Controller::new("idle", 3.0, 5.0, 1, 0.80);
413        ctrl.arrow.push("a", "b", 0.3);
414        assert!(ctrl.observe());
415        match ctrl.tick() {
416            Verdict::Compound(vs) => assert_eq!(vs.len(), 3),
417            other => {
418                let _ = other;
419                assert!(false, "expected Compound");
420            }
421        }
422    }
423
424    #[test]
425    fn controller_act_transitions() {
426        let mut ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
427        ctrl.act("active", "user_input", "state_change", 0.95);
428        assert_eq!(ctrl.sigma.value, "active");
429        assert_eq!(ctrl.sigma.tick, 1);
430        assert_eq!(ctrl.arrow.len(), 1);
431    }
432
433    #[test]
434    fn hook_binding_degrade_restore() {
435        let mut binding = HookBinding::new("exhale.sh", "brain-cli", "Stop");
436        assert!((binding.fidelity - 1.0).abs() < f64::EPSILON);
437        binding.degrade(0.8);
438        assert!((binding.fidelity - 0.8).abs() < f64::EPSILON);
439        binding.restore();
440        assert!((binding.fidelity - 1.0).abs() < f64::EPSILON);
441    }
442
443    #[test]
444    fn registry_aggregate_fidelity() {
445        let ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
446        let mut reg = BindingRegistry::new(ctrl);
447        let mut b1 = HookBinding::new("a.sh", "bin-a", "Start");
448        let b2 = HookBinding::new("b.sh", "bin-b", "Stop");
449        b1.degrade(0.9);
450        reg.register(b1);
451        reg.register(b2);
452        assert!((reg.aggregate_fidelity() - 0.9).abs() < f64::EPSILON);
453    }
454
455    #[test]
456    fn registry_degraded_bindings() {
457        let ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
458        let mut reg = BindingRegistry::new(ctrl);
459        let mut b1 = HookBinding::new("a.sh", "bin-a", "Start");
460        b1.degrade(0.5);
461        reg.register(b1);
462        reg.register(HookBinding::new("b.sh", "bin-b", "Stop"));
463        let degraded = reg.degraded_bindings(0.80);
464        assert_eq!(degraded.len(), 1);
465        assert_eq!(degraded[0].hook, "a.sh");
466    }
467
468    #[test]
469    fn verdict_display() {
470        assert_eq!(Verdict::Stable.to_string(), "STABLE");
471        assert_eq!(Verdict::FrequencyDecay.to_string(), "FREQ_DECAY");
472        let compound = Verdict::Compound(vec![Verdict::FrequencyDecay, Verdict::FidelityDegraded]);
473        assert_eq!(
474            compound.to_string(),
475            "COMPOUND(FREQ_DECAY+FIDELITY_DEGRADED)"
476        );
477    }
478
479    #[test]
480    fn empty_arrow_f_total_is_zero() {
481        let arrow = Arrow::new();
482        assert!((arrow.f_total() - 0.0).abs() < f64::EPSILON);
483    }
484
485    #[test]
486    fn nu_zero_floor() {
487        let nu = Nu::new(5.0, 0.0);
488        assert!(!nu.is_decayed());
489        assert!(nu.health_ratio().is_infinite());
490    }
491
492    #[test]
493    fn decay_all_degrades_above_floor() {
494        let ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
495        let mut reg = BindingRegistry::new(ctrl);
496        reg.register(HookBinding::new("a.sh", "bin-a", "Start"));
497        reg.register(HookBinding::new("b.sh", "bin-b", "Stop"));
498        let count = reg.decay_all(0.9, 0.5);
499        assert_eq!(count, 2);
500        assert!((reg.bindings[0].fidelity - 0.9).abs() < f64::EPSILON);
501        assert!((reg.bindings[1].fidelity - 0.9).abs() < f64::EPSILON);
502    }
503
504    #[test]
505    fn decay_all_respects_floor() {
506        let ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
507        let mut reg = BindingRegistry::new(ctrl);
508        let mut b1 = HookBinding::new("a.sh", "bin-a", "Start");
509        b1.degrade(0.3); // already below floor of 0.5
510        reg.register(b1);
511        reg.register(HookBinding::new("b.sh", "bin-b", "Stop"));
512        let count = reg.decay_all(0.8, 0.5);
513        assert_eq!(count, 1); // only b2 was above floor
514        assert!((reg.bindings[0].fidelity - 0.3).abs() < f64::EPSILON); // unchanged
515        assert!((reg.bindings[1].fidelity - 0.8).abs() < f64::EPSILON); // degraded
516    }
517
518    #[test]
519    fn decay_all_empty_registry() {
520        let ctrl: Controller<&str> = Controller::new("idle", 10.0, 5.0, 3, 0.80);
521        let mut reg = BindingRegistry::new(ctrl);
522        assert_eq!(reg.decay_all(0.9, 0.1), 0);
523    }
524}