Skip to main content

qualia_core_db/modalities/
paraconsistent.rs

1use crate::{q_hash, NQuin};
2
3pub const OP_ISOLATE: u8 = 0x30;
4pub const OP_CONTRADICTION_SCORE: u8 = 0x31;
5pub const OP_PARACONSISTENT_MERGE: u8 = 0x32;
6
7pub const ISOLATED_CONTEXT_PREFIX: u64 = q_hash("q42:isolated");
8
9#[derive(Debug)]
10pub enum ParaconsistentError {
11    BufferOverflow,
12}
13
14pub enum ContradictionStatus {
15    Consistent,
16    Isolated {
17        severity: u8,
18        isolation_context: u64,
19    },
20}
21
22/// Routes Quins into consistent and isolated (contradictory) sub-contexts
23pub fn route_paraconsistent(
24    quins: &[NQuin],
25    out_consistent: &mut [NQuin],
26    out_isolated: &mut [NQuin],
27) -> Result<(usize, usize), ParaconsistentError> {
28    let mut consistent_count = 0;
29    let mut isolated_count = 0;
30
31    for q in quins {
32        // If it's already isolated, pass it through to consistent to avoid recursive isolation
33        // (Wait, the requirements say: "Already-isolated Quin passes through without re-isolation")
34        if q.context == ISOLATED_CONTEXT_PREFIX {
35            if consistent_count >= out_consistent.len() {
36                return Err(ParaconsistentError::BufferOverflow);
37            }
38            out_consistent[consistent_count] = *q;
39            consistent_count += 1;
40            continue;
41        }
42
43        let mut is_contradiction = false;
44
45        // Contradiction rule: same subject + predicate, different object
46        for i in 0..consistent_count {
47            let prev = &out_consistent[i];
48            if prev.context == q.context
49                && prev.subject == q.subject
50                && prev.predicate == q.predicate
51                && prev.object != q.object
52            {
53                is_contradiction = true;
54                break;
55            }
56        }
57
58        if is_contradiction {
59            if isolated_count >= out_isolated.len() {
60                return Err(ParaconsistentError::BufferOverflow);
61            }
62            let mut isolated_q = *q;
63            isolated_q.context = ISOLATED_CONTEXT_PREFIX ^ q.context;
64            isolated_q.parity =
65                isolated_q.subject ^ isolated_q.predicate ^ isolated_q.object ^ isolated_q.context;
66            out_isolated[isolated_count] = isolated_q;
67            isolated_count += 1;
68        } else {
69            if consistent_count >= out_consistent.len() {
70                return Err(ParaconsistentError::BufferOverflow);
71            }
72            out_consistent[consistent_count] = *q;
73            consistent_count += 1;
74        }
75    }
76
77    Ok((consistent_count, isolated_count))
78}
79
80// ─── Belnap's four-valued logic (FOUR) ──────────────────────────────────────────────
81//
82// The basis of inconsistency-tolerant reasoning: a proposition is tracked by two
83// INDEPENDENT evidence bits — "told true" and "told false" — yielding four values:
84//   Neither (no info) · True · False · Both (a contained contradiction — no explosion).
85// Conjunction/disjunction are the meet/join in the truth order
86//   False ≤t Neither ≤t True  and  False ≤t Both ≤t True,
87// computed componentwise on the evidence bits (the standard Belnap tables).
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Belnap {
91    /// No information either way.
92    Neither,
93    /// Told true only.
94    True,
95    /// Told false only.
96    False,
97    /// Told both true and false — a contradiction, contained rather than exploding.
98    Both,
99}
100
101impl Belnap {
102    /// Assign a Belnap value from two independent evidence flags.
103    #[inline]
104    pub fn from_evidence(told_true: bool, told_false: bool) -> Belnap {
105        match (told_true, told_false) {
106            (false, false) => Belnap::Neither,
107            (true, false) => Belnap::True,
108            (false, true) => Belnap::False,
109            (true, true) => Belnap::Both,
110        }
111    }
112
113    /// Decompose into `(told_true, told_false)`.
114    #[inline]
115    pub fn evidence(self) -> (bool, bool) {
116        match self {
117            Belnap::Neither => (false, false),
118            Belnap::True => (true, false),
119            Belnap::False => (false, true),
120            Belnap::Both => (true, true),
121        }
122    }
123
124    /// Is this value a contained contradiction?
125    #[inline]
126    pub fn is_contradiction(self) -> bool {
127        matches!(self, Belnap::Both)
128    }
129
130    /// Belnap negation: swap the true/false evidence (Both and Neither are fixed points).
131    #[inline]
132    pub fn negate(self) -> Belnap {
133        let (t, f) = self.evidence();
134        Belnap::from_evidence(f, t)
135    }
136
137    /// Conjunction (∧) — truth-order meet: told_true ∧, told_false ∨.
138    #[inline]
139    pub fn and(self, other: Belnap) -> Belnap {
140        let (at, af) = self.evidence();
141        let (bt, bf) = other.evidence();
142        Belnap::from_evidence(at && bt, af || bf)
143    }
144
145    /// Disjunction (∨) — truth-order join: told_true ∨, told_false ∧.
146    #[inline]
147    pub fn or(self, other: Belnap) -> Belnap {
148        let (at, af) = self.evidence();
149        let (bt, bf) = other.evidence();
150        Belnap::from_evidence(at || bt, af && bf)
151    }
152}
153
154// ─── Inconsistency saturation metrics (local vs global) ─────────────────────────────
155//
156// "How contradictory is the graph?" — distinct measures so a localised contradiction (one bad
157// context) is not mistaken for a graph-wide collapse of consistency.
158
159/// **Global** inconsistency saturation: the fraction of routed quins that were isolated as
160/// contradictory (from [`route_paraconsistent`]'s counts). `total == 0 → 0.0`.
161pub fn global_saturation(consistent: usize, isolated: usize) -> f32 {
162    let total = consistent + isolated;
163    if total == 0 {
164        0.0
165    } else {
166        isolated as f32 / total as f32
167    }
168}
169
170/// **Local** (per-context) inconsistency saturation: among the quins in `context`, the fraction
171/// that contradict an earlier same-context quin (same subject+predicate, different object — the
172/// same contradiction rule [`route_paraconsistent`] uses). `0.0` if the context is empty.
173/// Zero-heap (nested linear scans, no allocation).
174pub fn local_saturation(quins: &[NQuin], context: u64) -> f32 {
175    let mut in_ctx = 0usize;
176    let mut contradictory = 0usize;
177    for (i, q) in quins.iter().enumerate() {
178        if q.context != context {
179            continue;
180        }
181        in_ctx += 1;
182        let conflicts = quins[..i].iter().any(|p| {
183            p.context == context
184                && p.subject == q.subject
185                && p.predicate == q.predicate
186                && p.object != q.object
187        });
188        if conflicts {
189            contradictory += 1;
190        }
191    }
192    if in_ctx == 0 {
193        0.0
194    } else {
195        contradictory as f32 / in_ctx as f32
196    }
197}
198
199/// Is inconsistency **saturated** at or above `threshold`? Lets a caller draw the line between a
200/// tolerable localised contradiction and a context whose consistency has broken down.
201#[inline]
202pub fn is_saturated(saturation: f32, threshold: f32) -> bool {
203    saturation >= threshold
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn belnap_four_valued_tables() {
212        // Negation: True↔False; Both and Neither are fixed.
213        assert_eq!(Belnap::True.negate(), Belnap::False);
214        assert_eq!(Belnap::False.negate(), Belnap::True);
215        assert_eq!(Belnap::Both.negate(), Belnap::Both);
216        assert_eq!(Belnap::Neither.negate(), Belnap::Neither);
217
218        // Conjunction (meet): True∧False = False; Both∧True = Both; Neither∧True = Neither.
219        assert_eq!(Belnap::True.and(Belnap::False), Belnap::False);
220        assert_eq!(Belnap::Both.and(Belnap::True), Belnap::Both);
221        assert_eq!(Belnap::Neither.and(Belnap::True), Belnap::Neither);
222        assert_eq!(Belnap::Both.and(Belnap::False), Belnap::False);
223
224        // Disjunction (join): True∨False = True; Both∨False = Both; Neither∨False = Neither.
225        assert_eq!(Belnap::True.or(Belnap::False), Belnap::True);
226        assert_eq!(Belnap::Both.or(Belnap::False), Belnap::Both);
227        assert_eq!(Belnap::Neither.or(Belnap::False), Belnap::Neither);
228
229        // A contradiction is contained, not exploded: Both ∧ ¬Both stays in FOUR.
230        assert!(Belnap::Both.is_contradiction());
231        assert_eq!(Belnap::Both.and(Belnap::Both.negate()), Belnap::Both);
232        assert!(!Belnap::True.is_contradiction());
233    }
234
235    #[test]
236    fn test_paraconsistent_routing() {
237        let mut out_c = [NQuin::default(); 10];
238        let mut out_i = [NQuin::default(); 10];
239
240        // 1. No contradictions -> all in out_consistent
241        let q1 = NQuin {
242            subject: 1,
243            predicate: 2,
244            object: 3,
245            context: 42,
246            ..Default::default()
247        };
248        let q2 = NQuin {
249            subject: 1,
250            predicate: 3,
251            object: 3,
252            context: 42,
253            ..Default::default()
254        };
255        let (c, i) = route_paraconsistent(&[q1, q2], &mut out_c, &mut out_i).unwrap();
256        assert_eq!(c, 2);
257        assert_eq!(i, 0);
258
259        // 2. Two Quins, same sub+pred, diff obj -> second isolated
260        let q3 = NQuin {
261            subject: 1,
262            predicate: 2,
263            object: 99,
264            context: 42,
265            ..Default::default()
266        };
267        let (c, i) = route_paraconsistent(&[q1, q3], &mut out_c, &mut out_i).unwrap();
268        assert_eq!(c, 1);
269        assert_eq!(i, 1);
270        assert_eq!(out_i[0].context, ISOLATED_CONTEXT_PREFIX ^ 42);
271
272        // 3. Three Quins: 1 normal, 2 contradicts 1, 3 normal
273        let q4 = NQuin {
274            subject: 10,
275            predicate: 20,
276            object: 30,
277            context: 42,
278            ..Default::default()
279        };
280        let (c, i) = route_paraconsistent(&[q1, q3, q4], &mut out_c, &mut out_i).unwrap();
281        assert_eq!(c, 2);
282        assert_eq!(i, 1);
283
284        // 4. Already isolated Quin
285        let mut q_iso = q3;
286        q_iso.context = ISOLATED_CONTEXT_PREFIX; // Simplify for test
287        let (c, i) = route_paraconsistent(&[q_iso], &mut out_c, &mut out_i).unwrap();
288        assert_eq!(c, 1);
289        assert_eq!(i, 0);
290    }
291
292    #[test]
293    fn saturation_metrics_local_and_global() {
294        // Global: 1 isolated out of 4 routed → 0.25.
295        assert!((global_saturation(3, 1) - 0.25).abs() < 1e-6);
296        assert_eq!(global_saturation(0, 0), 0.0);
297        assert_eq!(global_saturation(0, 5), 1.0);
298
299        // Local: context 42 has 3 quins, the 2nd contradicts the 1st → 1/3 contradictory.
300        let ctx = 42;
301        let q1 = NQuin {
302            subject: 1,
303            predicate: 2,
304            object: 3,
305            context: ctx,
306            ..Default::default()
307        };
308        let q2 = NQuin {
309            subject: 1,
310            predicate: 2,
311            object: 99,
312            context: ctx,
313            ..Default::default()
314        }; // contradicts q1
315        let q3 = NQuin {
316            subject: 5,
317            predicate: 6,
318            object: 7,
319            context: ctx,
320            ..Default::default()
321        };
322        let other = NQuin {
323            subject: 1,
324            predicate: 2,
325            object: 8,
326            context: 7,
327            ..Default::default()
328        }; // diff ctx
329        let s = local_saturation(&[q1, q2, q3, other], ctx);
330        assert!(
331            (s - (1.0 / 3.0)).abs() < 1e-6,
332            "1 of 3 in-context quins is contradictory"
333        );
334
335        // A clean context saturates at 0; threshold classification works.
336        assert_eq!(local_saturation(&[q1, q3], ctx), 0.0);
337        assert!(is_saturated(0.6, 0.5));
338        assert!(!is_saturated(0.4, 0.5));
339        // An empty context is not "saturated".
340        assert_eq!(local_saturation(&[], ctx), 0.0);
341    }
342}