Skip to main content

qualia_core_db/modalities/
epistemic.rs

1use crate::NQuin;
2
3pub const OP_KNOWS: u8 = 0x20;
4pub const OP_BELIEVES: u8 = 0x21;
5pub const OP_COMMON_KNOWLEDGE: u8 = 0x22;
6pub const OP_INTENT_LOCK: u8 = 0x23;
7pub const OP_IS_LOCKED: u8 = 0x24;
8pub const OP_NAMESPACE_LOCK: u8 = 0x25;
9pub const OP_NAMESPACE_IS_LOCKED: u8 = 0x26;
10
11pub const CERTAINTY_BIT_SHIFT: u32 = 8;
12pub const NESTING_BIT_SHIFT: u32 = 16;
13
14// Named epistemic-strength bands for the `certainty` byte — the assertive/doxastic axis
15// (see core-ontologies/modal-junctures.n3). A juncture verb maps to a band; the eval is
16// Active at >= 128, so Knows/Affirms/Believes/Recognizes/Considers are Active and
17// Supposes/Suspects/Speculates/Doubts are Uncertain ("speculates" = a low-certainty
18// belief). The ILLOCUTIONARY speech acts (proclaims/declares/recommends/undertakes) are a
19// SEPARATE axis, not certainty — they route to deontic / soft-deontic / performative.
20pub const CERTAINTY_KNOWS: u8 = 255;
21pub const CERTAINTY_AFFIRMS: u8 = 230;
22pub const CERTAINTY_BELIEVES: u8 = 200;
23pub const CERTAINTY_RECOGNIZES: u8 = 200;
24pub const CERTAINTY_CONSIDERS: u8 = 128;
25pub const CERTAINTY_SUPPOSES: u8 = 100;
26pub const CERTAINTY_SUSPECTS: u8 = 80;
27pub const CERTAINTY_SPECULATES: u8 = 50;
28pub const CERTAINTY_DOUBTS: u8 = 20;
29
30#[derive(Debug, PartialEq, Clone, Copy)]
31pub enum EpistemicStatus {
32    Active,
33    Uncertain,
34    Skipped,
35}
36
37#[derive(Debug)]
38pub enum EpistemicError {
39    BufferOverflow,
40    NodeLocked(u64),      // Hash of the locked node
41    NamespaceLocked(u64), // Hash of the locked namespace context
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct EpistemicVerdict {
46    pub claim: NQuin,
47    pub status: EpistemicStatus,
48    pub certainty: u8,
49}
50
51/// Evaluates a slice of Quins for epistemic/doxastic claims.
52pub fn evaluate_epistemic_frame(
53    quins: &[NQuin],
54    agent_did_hash: u64, // 0 = accept all agents
55    world_hash: u64,     // 0 = accept all worlds
56    out: &mut [EpistemicVerdict],
57) -> Result<usize, EpistemicError> {
58    let mut count = 0;
59
60    for q in quins {
61        if world_hash != 0 && q.context != world_hash {
62            continue;
63        }
64        if agent_did_hash != 0 && q.subject != agent_did_hash {
65            continue;
66        }
67
68        let opcode = (q.predicate & 0xFF) as u8;
69        if opcode != OP_KNOWS && opcode != OP_BELIEVES && opcode != OP_COMMON_KNOWLEDGE {
70            continue;
71        }
72
73        let certainty = ((q.predicate >> CERTAINTY_BIT_SHIFT) & 0xFF) as u8;
74
75        let status = if certainty >= 128 || opcode == OP_KNOWS || opcode == OP_COMMON_KNOWLEDGE {
76            EpistemicStatus::Active
77        } else {
78            EpistemicStatus::Uncertain
79        };
80
81        if count >= out.len() {
82            return Err(EpistemicError::BufferOverflow);
83        }
84
85        out[count] = EpistemicVerdict {
86            claim: *q,
87            status,
88            certainty,
89        };
90        count += 1;
91    }
92
93    Ok(count)
94}
95
96/// Checks if any nodes requested by an intent quin are locked by another agent.
97pub fn check_node_locks(
98    intent_quins: &[NQuin],
99    current_graph: &[NQuin],
100    agent_did_hash: u64,
101) -> Result<(), EpistemicError> {
102    for i_quin in intent_quins {
103        let opcode = (i_quin.predicate & 0xFF) as u8;
104
105        // 1. Check Namespace Locks First
106        if opcode == OP_NAMESPACE_LOCK {
107            let target_namespace = i_quin.object;
108            // Ensure no other agent holds a namespace lock on this target
109            for c_quin in current_graph {
110                let c_opcode = (c_quin.predicate & 0xFF) as u8;
111                if c_opcode == OP_NAMESPACE_IS_LOCKED && c_quin.object == target_namespace {
112                    if c_quin.subject != agent_did_hash {
113                        return Err(EpistemicError::NamespaceLocked(target_namespace));
114                    }
115                }
116            }
117        }
118
119        // 2. Check Standard Node Locks
120        if opcode == OP_INTENT_LOCK {
121            let target_node = i_quin.object;
122            let target_namespace = i_quin.context; // The namespace context of the intent
123
124            for c_quin in current_graph {
125                let c_opcode = (c_quin.predicate & 0xFF) as u8;
126
127                // If the entire namespace is locked by another agent, reject the node lock
128                if c_opcode == OP_NAMESPACE_IS_LOCKED && c_quin.object == target_namespace {
129                    if c_quin.subject != agent_did_hash {
130                        return Err(EpistemicError::NamespaceLocked(target_namespace));
131                    }
132                }
133
134                // If the specific node is locked by another agent, reject the node lock
135                if c_opcode == OP_IS_LOCKED && c_quin.object == target_node {
136                    if c_quin.subject != agent_did_hash {
137                        return Err(EpistemicError::NodeLocked(target_node));
138                    }
139                }
140            }
141        }
142    }
143    Ok(())
144}
145
146// ─── Multi-agent epistemic operators (E, C, D), introspection, AGM revision ───────
147
148// AGM belief revision (expand / contract / revise over a signed-literal belief base) lives in
149// `modal.rs` and is re-exported here — belief revision IS an epistemic operation.
150pub use crate::modalities::modal::{
151    contract as agm_contract, expand as agm_expand, is_consistent as belief_set_consistent,
152    revise as agm_revise, Belief,
153};
154
155/// **Everyone-knows** `E φ`: every agent in the group knows φ. `agent_knows[i]` = does agent i know φ?
156pub fn everyone_knows(agent_knows: &[bool]) -> bool {
157    !agent_knows.is_empty() && agent_knows.iter().all(|&k| k)
158}
159
160/// **Distributed knowledge** `D φ`: the group COLLECTIVELY knows φ by pooling — φ is entailed by
161/// the union of what agents individually know. Modelled over fact-fragments: φ is distributed-
162/// known iff every fragment in `required` appears in `known` (the union of all agents' fragments).
163pub fn distributed_knowledge(required: &[u64], known: &[u64]) -> bool {
164    !required.is_empty() && required.iter().all(|r| known.contains(r))
165}
166
167/// **Common knowledge** `C φ`: practically established by a PUBLIC ANNOUNCEMENT to the whole group
168/// (everyone knows φ, everyone knows that everyone knows, ad infinitum). Holds iff the announcement
169/// was perceived by everyone.
170#[inline]
171pub fn common_knowledge_via_announcement(everyone_perceived: bool) -> bool {
172    everyone_perceived
173}
174
175/// **Positive introspection** (axiom 4): `Kφ → KKφ` — knowing implies knowing that one knows.
176#[inline]
177pub fn positive_introspection(knows_p: bool) -> bool {
178    knows_p
179}
180
181/// **Negative introspection** (axiom 5, S5): `¬Kφ → K¬Kφ` — not-knowing implies knowing one doesn't.
182#[inline]
183pub fn negative_introspection(knows_p: bool) -> bool {
184    !knows_p
185}
186
187/// The **Muddy Children** deduction: after the public announcement "at least one is muddy", each
188/// silent round eliminates a hypothesis; a muddy child deduces it is muddy exactly at
189/// `round == num_muddy` (1-indexed). Shows how common knowledge + iterated "I don't know" produces
190/// knowledge. Returns whether a muddy child KNOWS its own state at `round`.
191pub fn muddy_child_knows(num_muddy: u32, round: u32) -> bool {
192    num_muddy > 0 && round >= num_muddy
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::q_hash;
199
200    #[test]
201    fn test_epistemic_evaluation() {
202        let mut out = Vec::with_capacity(10);
203        for _ in 0..10 {
204            out.push(EpistemicVerdict {
205                claim: NQuin::default(),
206                status: EpistemicStatus::Skipped,
207                certainty: 0,
208            });
209        }
210
211        let agent_a = q_hash("agent_a");
212        let agent_b = q_hash("agent_b");
213        let world_w = q_hash("world_w");
214
215        let mut q_knows = NQuin::default();
216        q_knows.subject = agent_a;
217        q_knows.predicate = (200u64 << CERTAINTY_BIT_SHIFT) | (OP_KNOWS as u64);
218        q_knows.context = world_w;
219
220        let mut q_believes_low = NQuin::default();
221        q_believes_low.subject = agent_a;
222        q_believes_low.predicate = (50u64 << CERTAINTY_BIT_SHIFT) | (OP_BELIEVES as u64);
223        q_believes_low.context = world_w;
224
225        let mut q_wrong_world = NQuin::default();
226        q_wrong_world.subject = agent_a;
227        q_wrong_world.predicate = (200u64 << CERTAINTY_BIT_SHIFT) | (OP_KNOWS as u64);
228        q_wrong_world.context = q_hash("other_world");
229
230        let quins = [q_knows, q_believes_low, q_wrong_world];
231
232        // 1. Single-agent K_a(p)
233        let count = evaluate_epistemic_frame(&quins, agent_a, world_w, &mut out).unwrap();
234        assert_eq!(count, 2);
235        assert_eq!(out[0].status, EpistemicStatus::Active); // Knows
236        assert_eq!(out[1].status, EpistemicStatus::Uncertain); // Believes low certainty
237
238        // 2. World filter
239        let mut out2 = Vec::with_capacity(10);
240        for _ in 0..10 {
241            out2.push(EpistemicVerdict {
242                claim: NQuin::default(),
243                status: EpistemicStatus::Skipped,
244                certainty: 0,
245            });
246        }
247        let count2 = evaluate_epistemic_frame(&quins, 0, q_hash("other_world"), &mut out2).unwrap();
248        assert_eq!(count2, 1);
249
250        // 3. Agent filter
251        let count3 = evaluate_epistemic_frame(&quins, agent_b, 0, &mut out2).unwrap();
252        assert_eq!(count3, 0);
253
254        // 4. Empty slice
255        let count4 = evaluate_epistemic_frame(&[], 0, 0, &mut out2).unwrap();
256        assert_eq!(count4, 0);
257    }
258
259    #[test]
260    fn epistemic_strength_bands_map_to_active_or_uncertain() {
261        // The named certainty bands (modal-junctures.n3) route through the eval: confident
262        // attitudes (knows/believes/considers) are Active; tentative ones (supposes/
263        // speculates/doubts) are Uncertain — "speculates" as a low-certainty belief.
264        let agent = q_hash("agent");
265        let world = q_hash("world");
266        let mk = |band: u8| {
267            let mut q = NQuin::default();
268            q.subject = agent;
269            q.predicate = ((band as u64) << CERTAINTY_BIT_SHIFT) | (OP_BELIEVES as u64);
270            q.context = world;
271            q
272        };
273        let mut out = vec![
274            EpistemicVerdict {
275                claim: NQuin::default(),
276                status: EpistemicStatus::Skipped,
277                certainty: 0,
278            };
279            4
280        ];
281
282        for b in [CERTAINTY_KNOWS, CERTAINTY_BELIEVES, CERTAINTY_CONSIDERS] {
283            evaluate_epistemic_frame(&[mk(b)], agent, world, &mut out).unwrap();
284            assert_eq!(
285                out[0].status,
286                EpistemicStatus::Active,
287                "band {b} should be Active"
288            );
289        }
290        for b in [CERTAINTY_SUPPOSES, CERTAINTY_SPECULATES, CERTAINTY_DOUBTS] {
291            evaluate_epistemic_frame(&[mk(b)], agent, world, &mut out).unwrap();
292            assert_eq!(
293                out[0].status,
294                EpistemicStatus::Uncertain,
295                "band {b} should be Uncertain"
296            );
297        }
298    }
299
300    #[test]
301    fn test_namespace_lock_blocks_node_lock() {
302        let agent_a = q_hash("agent_a");
303        let agent_b = q_hash("agent_b");
304        let target_namespace = q_hash("specialized_libs/");
305        let target_node = q_hash("specialized_libs/file.rs");
306
307        // Graph: Agent A holds a namespace lock on specialized_libs/
308        let mut namespace_locked = NQuin::default();
309        namespace_locked.subject = agent_a;
310        namespace_locked.predicate = OP_NAMESPACE_IS_LOCKED as u64;
311        namespace_locked.object = target_namespace;
312
313        let current_graph = vec![namespace_locked];
314
315        // Intent: Agent B tries to lock a specific node within that namespace
316        let mut intent_node_lock = NQuin::default();
317        intent_node_lock.subject = agent_b;
318        intent_node_lock.predicate = OP_INTENT_LOCK as u64;
319        intent_node_lock.object = target_node;
320        intent_node_lock.context = target_namespace;
321
322        let result = check_node_locks(&[intent_node_lock], &current_graph, agent_b);
323        assert!(
324            matches!(result, Err(EpistemicError::NamespaceLocked(ns)) if ns == target_namespace)
325        );
326
327        // Intent: Agent A tries to lock a node within their own namespace lock -> Should Succeed
328        let mut intent_node_lock_a = NQuin::default();
329        intent_node_lock_a.subject = agent_a;
330        intent_node_lock_a.predicate = OP_INTENT_LOCK as u64;
331        intent_node_lock_a.object = target_node;
332        intent_node_lock_a.context = target_namespace;
333
334        let result_a = check_node_locks(&[intent_node_lock_a], &current_graph, agent_a);
335        assert!(result_a.is_ok());
336    }
337
338    #[test]
339    fn multi_agent_operators_and_introspection() {
340        // Everyone-knows: all agents must know it.
341        assert!(everyone_knows(&[true, true, true]));
342        assert!(!everyone_knows(&[true, false, true]));
343        assert!(!everyone_knows(&[]));
344        // Distributed knowledge: pooled fragments cover the requirement.
345        let (a, b, c) = (q_hash("f:a"), q_hash("f:b"), q_hash("f:c"));
346        assert!(distributed_knowledge(&[a, b], &[a, b, c]));
347        assert!(
348            !distributed_knowledge(&[a, b], &[a, c]),
349            "missing fragment b"
350        );
351        // Common knowledge via public announcement.
352        assert!(common_knowledge_via_announcement(true));
353        assert!(!common_knowledge_via_announcement(false));
354        // Introspection axioms (S5).
355        assert!(positive_introspection(true) && !positive_introspection(false));
356        assert!(negative_introspection(false) && !negative_introspection(true));
357    }
358
359    #[test]
360    fn muddy_children_deduction() {
361        // 2 muddy children: nobody knows in round 1; each deduces at round 2.
362        assert!(!muddy_child_knows(2, 1));
363        assert!(muddy_child_knows(2, 2));
364        // 1 muddy child knows immediately (round 1, from the announcement).
365        assert!(muddy_child_knows(1, 1));
366    }
367
368    #[test]
369    fn common_knowledge_propagation_across_two_agents() {
370        // Two agents both know φ (OP_KNOWS, certainty 255) in the same world.
371        // When everyone knows φ and a public announcement is made, φ becomes
372        // common knowledge. This tests the propagation path:
373        //   individual knowledge → everyone-knows → common knowledge
374        let agent_a = q_hash("agent_a");
375        let agent_b = q_hash("agent_b");
376        let world_w = q_hash("world_w");
377
378        let mk_knows = |agent: u64| {
379            let mut q = NQuin::default();
380            q.subject = agent;
381            q.predicate = (255u64 << CERTAINTY_BIT_SHIFT) | (OP_KNOWS as u64);
382            q.context = world_w;
383            q
384        };
385
386        let quins = [mk_knows(agent_a), mk_knows(agent_b)];
387
388        // Evaluate: both agents' knowledge claims should be Active
389        let mut out = [EpistemicVerdict {
390            claim: NQuin::default(),
391            status: EpistemicStatus::Skipped,
392            certainty: 0,
393        }; 4];
394        let count = evaluate_epistemic_frame(&quins, 0, world_w, &mut out).unwrap();
395        assert_eq!(count, 2, "both agents' claims must be evaluated");
396        assert_eq!(out[0].status, EpistemicStatus::Active);
397        assert_eq!(out[1].status, EpistemicStatus::Active);
398
399        // Both agents know → everyone_knows is true
400        let agent_knows = [
401            out[0].status == EpistemicStatus::Active,
402            out[1].status == EpistemicStatus::Active,
403        ];
404        assert!(everyone_knows(&agent_knows), "everyone knows φ");
405
406        // Public announcement → common knowledge
407        assert!(
408            common_knowledge_via_announcement(everyone_knows(&agent_knows)),
409            "φ becomes common knowledge when everyone knows and it is publicly announced"
410        );
411    }
412
413    #[test]
414    fn agm_belief_revision_is_available_in_the_epistemic_namespace() {
415        // The AGM operators (from modal.rs) are re-exported here; revise is consistent.
416        let p = Belief {
417            atom: 1,
418            positive: true,
419        };
420        let not_p = Belief {
421            atom: 1,
422            positive: false,
423        };
424        let mut out = [Belief {
425            atom: 0,
426            positive: true,
427        }; 4];
428        let n = agm_revise(&[not_p], p, &mut out);
429        assert!(out[..n].contains(&p) && !out[..n].contains(&not_p));
430        assert!(belief_set_consistent(&out[..n]));
431        let _ = agm_expand;
432        let _ = agm_contract;
433    }
434}