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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CausalLink {
97 pub cause: String,
98 pub effect: String,
99 pub fidelity: f64,
100}
101
102#[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 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#[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#[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#[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#[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 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); 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); assert!((reg.bindings[0].fidelity - 0.3).abs() < f64::EPSILON); assert!((reg.bindings[1].fidelity - 0.8).abs() < f64::EPSILON); }
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}