Skip to content
This repository was archived by the owner on Jul 19, 2026. It is now read-only.

Commit 5e79710

Browse files
RMANOVclaude
andcommitted
feat: phase-17 — Epistemic Evidence Graph: boolean-gated Bayesian inference
The swarm can now see its own contradictions. Seven subsystems — Byzantine, Trust, Quarantine, Gossip, GBP, Pheromone, OrderParams — were isolated signal processors, each screaming into the void. Phase 17 weaves them into a fixed-topology evidence graph where boolean gates aren't logic circuits but epistemic mode selectors that tell the Bayesian layer what kind of evidence it's looking at: XOR → conflicting sources: widen uncertainty, investigate XNOR → corroborating sources: boost confidence (scaled by independence) NOR → silent sources: preserve prior, flag information vacuum NAND → forbidden state combinations: hard block, escalate Six feedback loops now close the inference circuit: Byzantine.Reject →XOR→ TrustTracker (kinematic -0.05) Byzantine.Accept →XNOR→ TrustTracker (integrity +0.02) NOR(silent_peer) →Vacuum→ gossip fanout increase + GBP prior reset Quarantine.fear → strike threshold relaxation in combat Pheromone(Target∧Doubt) →XOR→ deposit cap modulation OrderParams(high entropy + low alignment) → escalation Structural memory tracks which edges are reliable via EMA, with periodic pruning of uninformative connections. XOR-triggered forgetting (α=0.3) resets GBP priors for peers with sustained high conflict rates — preventing confirmation bias from crystallizing into false certainty. Disabled by default. Zero modification to safety-critical paths (CBF/GCBF+, ROE, particle filter). Trust deltas clamped to [-0.1, +0.05] per tick. NaN-safe throughout. New: bool_gates.rs (264 LOC), evidence_graph.rs (810 LOC) Modified: 12 files, +268 LOC net across gossip, quarantine, stigmergy, trust, order_params, tick, XAI trace/narrator/replay, param_space, lib.rs 82 tests pass. 0 clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 561d309 commit 5e79710

14 files changed

Lines changed: 1377 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
//! Boolean gate types for epistemic evidence processing.
2+
//!
3+
//! Provides the signal vocabulary used by the evidence graph to
4+
//! detect conflicts (XOR), corroboration (XNOR), information vacuums
5+
//! (NOR), forbidden states (NAND), and broken implications in the
6+
//! swarm's inference pipeline.
7+
//!
8+
//! These are **not** computational gates — they are epistemic mode
9+
//! selectors that tell the Bayesian layer how to handle evidence:
10+
//! - XOR → widen uncertainty, investigate
11+
//! - XNOR → boost confidence (scaled by source independence)
12+
//! - NOR → preserve prior, flag information gap
13+
//! - NAND → hard block, escalate
14+
15+
use serde::{Deserialize, Serialize};
16+
17+
use crate::trust::TrustDimension;
18+
use crate::NodeId;
19+
20+
// ---------------------------------------------------------------------------
21+
// Signal source — identifies where evidence comes from
22+
// ---------------------------------------------------------------------------
23+
24+
/// Source of an evidence signal in the STRIX subsystem graph.
25+
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
26+
pub enum SignalSource {
27+
/// From Byzantine validation of a peer's update.
28+
Byzantine(NodeId),
29+
/// From trust tracker observation.
30+
Trust(NodeId),
31+
/// From quarantine state change.
32+
Quarantine(NodeId),
33+
/// From gossip merge result.
34+
Gossip(NodeId),
35+
/// From GBP fusion uncertainty.
36+
Gbp(NodeId),
37+
/// From pheromone field (zone identifier).
38+
Pheromone(u64),
39+
/// From macro order parameters (swarm-level).
40+
OrderParams,
41+
}
42+
43+
// ---------------------------------------------------------------------------
44+
// Gate signals — emitted by subsystems
45+
// ---------------------------------------------------------------------------
46+
47+
/// Signal emitted when a subsystem detects an epistemic condition.
48+
#[derive(Debug, Clone, Serialize, Deserialize)]
49+
pub enum GateSignal {
50+
/// XOR: two sources disagree on the same hypothesis.
51+
Conflict {
52+
source_a: SignalSource,
53+
source_b: SignalSource,
54+
/// Severity of the conflict ∈ [0, 1].
55+
severity: f64,
56+
timestamp: f64,
57+
},
58+
/// XNOR: two or more sources agree.
59+
Corroboration {
60+
sources: Vec<SignalSource>,
61+
/// Independence between sources ∈ [0, 1]. Low independence
62+
/// (correlated sources) discounts the consensus value.
63+
independence: f64,
64+
/// How much to strengthen belief.
65+
confidence_boost: f64,
66+
timestamp: f64,
67+
},
68+
/// NOR: no signal received from an expected source.
69+
Vacuum {
70+
expected_source: SignalSource,
71+
/// Timestamp when the source was last active.
72+
last_seen: f64,
73+
/// How long the vacuum has persisted (seconds).
74+
duration: f64,
75+
},
76+
/// NAND: a forbidden combination of conditions is active.
77+
Violation {
78+
condition_a: String,
79+
condition_b: String,
80+
severity: f64,
81+
},
82+
/// Implication A → B is broken (A is true but B is false).
83+
ImplicationBreak {
84+
antecedent: String,
85+
consequent: String,
86+
context: String,
87+
},
88+
}
89+
90+
// ---------------------------------------------------------------------------
91+
// Feedback actions — produced by evidence graph processing
92+
// ---------------------------------------------------------------------------
93+
94+
/// Action the evidence graph instructs the orchestrator to perform.
95+
#[derive(Debug, Clone, Serialize, Deserialize)]
96+
pub enum FeedbackAction {
97+
/// Update a specific trust dimension for a peer.
98+
UpdateTrust {
99+
peer: NodeId,
100+
dimension: TrustDimension,
101+
/// Positive = trust more, negative = trust less.
102+
/// Clamped to [-0.1, +0.05] per tick.
103+
delta: f64,
104+
},
105+
/// Modulate a threshold (quarantine strike weight, etc.).
106+
AdjustThreshold { system: String, multiplier: f64 },
107+
/// Reset prior toward uninformative (XOR-triggered forgetting).
108+
ResetPrior {
109+
/// The peer whose GBP belief should be reset.
110+
peer: NodeId,
111+
/// 0 = no reset, 1 = full reset to uninformative.
112+
alpha: f64,
113+
},
114+
/// Mark an information vacuum for a region or peer.
115+
MarkVacuum { source: SignalSource, severity: f64 },
116+
/// Escalate — the evidence graph cannot resolve the situation locally.
117+
Escalate { reason: String, severity: f64 },
118+
}
119+
120+
// ---------------------------------------------------------------------------
121+
// Utility functions
122+
// ---------------------------------------------------------------------------
123+
124+
/// Quorum evaluation: passes if the weighted sum of positive votes
125+
/// exceeds `threshold`.
126+
///
127+
/// Each vote is `(voter_id, vote_bool, weight)`.
128+
pub fn quorum_passes(votes: &[(NodeId, bool, f64)], threshold: f64) -> bool {
129+
if votes.is_empty() {
130+
return false;
131+
}
132+
let weighted_yes: f64 = votes
133+
.iter()
134+
.filter(|(_, v, _)| *v)
135+
.map(|(_, _, w)| *w)
136+
.sum();
137+
let total_weight: f64 = votes.iter().map(|(_, _, w)| *w).sum();
138+
if total_weight < 1e-12 {
139+
return false;
140+
}
141+
weighted_yes / total_weight >= threshold
142+
}
143+
144+
/// XOR disagreement rate between two binary signal histories.
145+
///
146+
/// Returns the fraction of positions where the two histories differ.
147+
/// Empty histories return 0.0.
148+
pub fn xor_rate(history_a: &[bool], history_b: &[bool]) -> f64 {
149+
let len = history_a.len().min(history_b.len());
150+
if len == 0 {
151+
return 0.0;
152+
}
153+
let conflicts: usize = history_a
154+
.iter()
155+
.zip(history_b.iter())
156+
.filter(|(a, b)| a != b)
157+
.count();
158+
conflicts as f64 / len as f64
159+
}
160+
161+
/// XNOR consensus score with independence correction.
162+
///
163+
/// `agreements` = number of agreeing pairs out of `total_pairs`.
164+
/// `independence` ∈ [0, 1] corrects for correlated sources:
165+
/// effective_consensus = raw_consensus × independence.
166+
pub fn xnor_consensus(agreements: f64, total_pairs: f64, independence: f64) -> f64 {
167+
if total_pairs < 1e-12 {
168+
return 0.0;
169+
}
170+
let raw = (agreements / total_pairs).clamp(0.0, 1.0);
171+
raw * independence.clamp(0.0, 1.0)
172+
}
173+
174+
/// NAND check: returns `true` if both conditions are active
175+
/// (a forbidden combination).
176+
pub fn nand_violated(a: bool, b: bool) -> bool {
177+
a && b
178+
}
179+
180+
#[cfg(test)]
181+
mod tests {
182+
use super::*;
183+
184+
#[test]
185+
fn signal_construction_roundtrip() {
186+
let sig = GateSignal::Conflict {
187+
source_a: SignalSource::Byzantine(NodeId(1)),
188+
source_b: SignalSource::Gossip(NodeId(2)),
189+
severity: 0.8,
190+
timestamp: 10.0,
191+
};
192+
let json = serde_json::to_string(&sig).unwrap();
193+
let back: GateSignal = serde_json::from_str(&json).unwrap();
194+
match back {
195+
GateSignal::Conflict { severity, .. } => {
196+
assert!((severity - 0.8).abs() < 1e-6);
197+
}
198+
_ => panic!("wrong variant"),
199+
}
200+
}
201+
202+
#[test]
203+
fn quorum_passes_weighted() {
204+
let votes = vec![
205+
(NodeId(1), true, 0.8),
206+
(NodeId(2), true, 0.6),
207+
(NodeId(3), false, 0.9),
208+
];
209+
// weighted_yes = 1.4, total = 2.3, ratio ≈ 0.609
210+
assert!(quorum_passes(&votes, 0.5));
211+
assert!(!quorum_passes(&votes, 0.7));
212+
}
213+
214+
#[test]
215+
fn quorum_empty_fails() {
216+
assert!(!quorum_passes(&[], 0.5));
217+
}
218+
219+
#[test]
220+
fn xor_rate_calculation() {
221+
let a = vec![true, true, false, false, true];
222+
let b = vec![true, false, false, true, true];
223+
// differences at positions 1 and 3 → rate = 2/5 = 0.4
224+
assert!((xor_rate(&a, &b) - 0.4).abs() < 1e-6);
225+
}
226+
227+
#[test]
228+
fn xor_rate_empty_is_zero() {
229+
assert!((xor_rate(&[], &[])).abs() < 1e-6);
230+
}
231+
232+
#[test]
233+
fn xnor_consensus_with_independence() {
234+
// 8 agreements out of 10 pairs, full independence
235+
assert!((xnor_consensus(8.0, 10.0, 1.0) - 0.8).abs() < 1e-6);
236+
// same but independence = 0.5 → effective = 0.4
237+
assert!((xnor_consensus(8.0, 10.0, 0.5) - 0.4).abs() < 1e-6);
238+
// zero independence → 0.0
239+
assert!((xnor_consensus(8.0, 10.0, 0.0)).abs() < 1e-6);
240+
}
241+
242+
#[test]
243+
fn nand_violation_detection() {
244+
assert!(nand_violated(true, true));
245+
assert!(!nand_violated(true, false));
246+
assert!(!nand_violated(false, true));
247+
assert!(!nand_violated(false, false));
248+
}
249+
250+
#[test]
251+
fn feedback_action_trust_clamp_semantics() {
252+
// Verify the delta semantics are clear in types
253+
let action = FeedbackAction::UpdateTrust {
254+
peer: NodeId(5),
255+
dimension: TrustDimension::Kinematic,
256+
delta: -0.05,
257+
};
258+
match action {
259+
FeedbackAction::UpdateTrust { delta, .. } => {
260+
assert!(delta >= -0.1 && delta <= 0.05);
261+
}
262+
_ => panic!("wrong variant"),
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)