Skip to main content

qualia_core_db/
epistemic.rs

1//! Dynamic Epistemic Logic (DEL) layer — separating objective and subjective reality.
2//!
3//! # Core design
4//!
5//! Standard databases have one "God's eye" table.  QualiaDB separates:
6//!
7//! - **Objective reality** (`OBJECTIVE_CONTEXT`) — cryptographically proven facts.
8//!   A quin here has `prov:wasGeneratedBy` pointing to a `q42ep:CryptoSensor` or
9//!   `q42ep:SignedObjectiveEvent`.  SHACL `ObjectiveKnowledgeShape` validates this.
10//!
11//! - **Per-agent epistemic space** (`agent_epistemic_context(did)`) — beliefs,
12//!   inferences, and hearsay scoped to one agent.  Multiple agents can hold
13//!   *different valid epistemic states* about the same proposition at the same time.
14//!
15//! # John / Frank / Jane scenario
16//!
17//! ```text
18//! t₁: John directly observes the matter.
19//!     → assert_objective_knowledge(john_did, matter_hash, sensor_hash, t1)
20//!
21//! t₂: Frank queries the matter — his query is itself an epistemic event.
22//!     → record_query_observation(frank_did, matter_hash, t2)
23//!
24//! t₃: Jane observes Frank's query (not the original matter).
25//! t₄: Jane derives her own version.
26//!     → assert_inferred_belief(jane_did, jane_variant_hash, frank_query_hash, t4)
27//! ```
28//!
29//! Jane's context (`agent_epistemic_context(jane_did)`) never overwrites
30//! John's context.  SPARQL queries traverse either timeline independently.
31//!
32//! # SHACL classification
33//!
34//! `classify_epistemic_state()` evaluates the quin slice against three shapes:
35//!   1. `ObjectiveKnowledge` — `prov:wasGeneratedBy` → CryptoSensor/SignedObjectiveEvent
36//!   2. `InferredBelief` — `prov:wasDerivedFrom` → AgentQuery/HearsayEvent
37//!   3. `HearsayBelief` — `q42ep:believesViaHearsay` present
38//!   4. `Unknown` — no matching modality predicate found
39//!
40//! Integration with `deontic_logic.rs`:
41//!   `OP_PERMIT read IF classify() == ObjectiveKnowledge`
42//!   `OP_CONDITIONALLY_PERMIT read IF classify() == InferredBelief`
43
44use crate::temporal_graph::{P_WAS_ATTRIBUTED_TO, P_WAS_GENERATED_BY};
45use crate::{q_hash, NQuin};
46
47// ── Named-graph contexts ──────────────────────────────────────────────────────
48
49/// Cryptographically verified facts, shared across all agents.
50pub const OBJECTIVE_CONTEXT: u64 = q_hash("urn:qualia:context:objective");
51
52/// Base epistemic metadata context (query events, propagation records).
53pub const EPISTEMIC_CONTEXT: u64 = q_hash("urn:qualia:context:epistemic");
54
55/// Derive the per-agent epistemic context for `did_hash`.
56///
57/// Each agent has a distinct named graph for their beliefs.  XOR-folding the
58/// base context with the DID produces a stable non-colliding context without
59/// string allocation.
60#[inline]
61pub fn agent_epistemic_context(did_hash: u64) -> u64 {
62    q_hash("urn:qualia:context:epistemic:agent") ^ did_hash
63}
64
65// ── JTB model resource type hashes ───────────────────────────────────────────
66
67/// `q42ep:CryptoSensor` class hash — hardware-attested or cryptographically signed source.
68pub const C_CRYPTO_SENSOR: u64 = q_hash("urn:qualia:ontology:epistemic:CryptoSensor");
69/// `q42ep:SignedObjectiveEvent` class hash — notarized or DID-signed objective event.
70pub const C_SIGNED_OBJECTIVE_EVENT: u64 =
71    q_hash("urn:qualia:ontology:epistemic:SignedObjectiveEvent");
72/// `q42ep:AgentQuery` class hash — a query event is itself an epistemic event.
73pub const C_AGENT_QUERY: u64 = q_hash("urn:qualia:ontology:epistemic:AgentQuery");
74/// `q42ep:HearsayEvent` class hash — information from an unverified peer.
75pub const C_HEARSAY_EVENT: u64 = q_hash("urn:qualia:ontology:epistemic:HearsayEvent");
76
77// ── Epistemic modality predicate hashes ──────────────────────────────────────
78
79/// `q42ep:knowsDirectly` — agent has direct cryptographic or sensory proof.
80pub const P_KNOWS_DIRECTLY: u64 = q_hash("urn:qualia:epistemic:knowsDirectly");
81/// `q42ep:infersFrom` — agent derived a belief via local logic from a source event.
82pub const P_INFERS_FROM: u64 = q_hash("urn:qualia:epistemic:infersFrom");
83/// `q42ep:believesViaHearsay` — belief received from an unverified peer.
84pub const P_BELIEVES_VIA_HEARSAY: u64 = q_hash("urn:qualia:epistemic:believesViaHearsay");
85
86/// `prov:wasDerivedFrom` — derivation edge in the PROV-O provenance chain.
87pub const P_WAS_DERIVED_FROM: u64 = q_hash("http://www.w3.org/ns/prov#wasDerivedFrom");
88
89/// `cog:believes` — the core JTB belief relation.
90pub const P_COG_BELIEVES: u64 = q_hash("https://www.w3.org/community/cogai/ont#believes");
91/// `cog:queries` — an agent queries about a proposition.
92pub const P_COG_QUERIES: u64 = q_hash("https://www.w3.org/community/cogai/ont#queries");
93/// `cog:observes` — an agent observes an event.
94pub const P_COG_OBSERVES: u64 = q_hash("https://www.w3.org/community/cogai/ont#observes");
95/// `cog:infers` — an agent infers a new belief.
96pub const P_COG_INFERS: u64 = q_hash("https://www.w3.org/community/cogai/ont#infers");
97
98/// `rdf:type` (cached locally to avoid cross-module dep loop).
99const P_RDF_TYPE: u64 = q_hash("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
100
101// ── Epistemic state classification ────────────────────────────────────────────
102
103/// The epistemic weight of an agent's belief about a proposition.
104///
105/// Maps to the JTB (Justified True Belief) model:
106/// - `ObjectiveKnowledge` — belief + justification from a cryptographic source = Knowledge
107/// - `InferredBelief`     — belief + derivation from a prior event (no direct proof)
108/// - `HearsayBelief`      — belief received from an unverified peer (lowest weight)
109/// - `Unknown`            — no epistemic modality predicate found in the quin slice
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum EpistemicState {
112    /// Cryptographically verified — `prov:wasGeneratedBy` → CryptoSensor/SignedObjectiveEvent.
113    ObjectiveKnowledge,
114    /// Derived via local logic — `prov:wasDerivedFrom` → AgentQuery/HearsayEvent.
115    InferredBelief,
116    /// Received from an unverified peer — `q42ep:believesViaHearsay` present.
117    HearsayBelief,
118    /// No epistemic modality found for this agent/proposition pair.
119    Unknown,
120}
121
122/// Classify the epistemic state of `agent_did` regarding `proposition_hash`.
123///
124/// Scans the quin slice for modality predicates in the agent's epistemic context
125/// and returns the highest-confidence classification found.  The precedence order is:
126///   `ObjectiveKnowledge` > `InferredBelief` > `HearsayBelief` > `Unknown`
127///
128/// This is an O(n) linear scan — for production use, maintain a hash index on
129/// `(context, predicate)` pairs.
130pub fn classify_epistemic_state(
131    quins: &[NQuin],
132    agent_did: u64,
133    proposition_hash: u64,
134) -> EpistemicState {
135    let agent_ctx = agent_epistemic_context(agent_did);
136    let mut best = EpistemicState::Unknown;
137
138    for q in quins {
139        if q.context != agent_ctx && q.context != OBJECTIVE_CONTEXT {
140            continue;
141        }
142        if q.subject != agent_did && q.subject != proposition_hash {
143            continue;
144        }
145        if q.object != proposition_hash
146            && q.predicate != P_WAS_DERIVED_FROM
147            && q.predicate != P_INFERS_FROM
148        {
149            continue;
150        }
151
152        let state = predicate_to_state(q.predicate);
153        best = higher(best, state);
154        if best == EpistemicState::ObjectiveKnowledge {
155            break;
156        }
157    }
158    best
159}
160
161/// Return the higher-confidence state of `a` and `b`.
162#[inline]
163fn higher(a: EpistemicState, b: EpistemicState) -> EpistemicState {
164    use EpistemicState::*;
165    match (a, b) {
166        (ObjectiveKnowledge, _) | (_, ObjectiveKnowledge) => ObjectiveKnowledge,
167        (InferredBelief, _) | (_, InferredBelief) => InferredBelief,
168        (HearsayBelief, _) | (_, HearsayBelief) => HearsayBelief,
169        (Unknown, Unknown) => Unknown,
170    }
171}
172
173#[inline]
174fn predicate_to_state(predicate: u64) -> EpistemicState {
175    if predicate == P_KNOWS_DIRECTLY {
176        EpistemicState::ObjectiveKnowledge
177    } else if predicate == P_INFERS_FROM {
178        EpistemicState::InferredBelief
179    } else if predicate == P_BELIEVES_VIA_HEARSAY {
180        EpistemicState::HearsayBelief
181    } else {
182        EpistemicState::Unknown
183    }
184}
185
186// ── Write helpers ─────────────────────────────────────────────────────────────
187
188/// Record that `agent_did` has ObjectiveKnowledge of `proposition_hash`, justified
189/// by `proof_hash` (e.g. a `q42ep:CryptoSensor` or `q42ep:SignedObjectiveEvent`).
190///
191/// Returns 3 quins in `agent_epistemic_context(agent_did)`:
192///   1. `agent_did → knowsDirectly → proposition_hash` (epistemic modality)
193///   2. `agent_did → cog:believes → proposition_hash` (CogAI belief relation)
194///   3. `proposition_hash → prov:wasGeneratedBy → proof_hash` (justification chain)
195pub fn assert_objective_knowledge(
196    agent_did: u64,
197    proposition_hash: u64,
198    proof_hash: u64,
199    ts: u64,
200) -> [NQuin; 3] {
201    let ctx = agent_epistemic_context(agent_did);
202    [
203        make_ep(agent_did, P_KNOWS_DIRECTLY, proposition_hash, ctx, ts),
204        make_ep(agent_did, P_COG_BELIEVES, proposition_hash, ctx, ts),
205        make_ep(
206            proposition_hash,
207            P_WAS_GENERATED_BY,
208            proof_hash,
209            OBJECTIVE_CONTEXT,
210            ts,
211        ),
212    ]
213}
214
215/// Record that `agent_did` holds an InferredBelief about `proposition_hash`,
216/// derived from `source_hash` (an `AgentQuery` or `HearsayEvent`).
217///
218/// Returns 3 quins in `agent_epistemic_context(agent_did)`:
219///   1. `agent_did → infersFrom → source_hash`
220///   2. `agent_did → cog:believes → proposition_hash`
221///   3. `proposition_hash → prov:wasDerivedFrom → source_hash`
222pub fn assert_inferred_belief(
223    agent_did: u64,
224    proposition_hash: u64,
225    source_hash: u64,
226    ts: u64,
227) -> [NQuin; 3] {
228    let ctx = agent_epistemic_context(agent_did);
229    [
230        make_ep(agent_did, P_INFERS_FROM, source_hash, ctx, ts),
231        make_ep(agent_did, P_COG_BELIEVES, proposition_hash, ctx, ts),
232        make_ep(proposition_hash, P_WAS_DERIVED_FROM, source_hash, ctx, ts),
233    ]
234}
235
236/// Record that `agent_did` holds a HearsayBelief about `proposition_hash`,
237/// received from `informant_did` (an unverified peer).
238///
239/// Returns 3 quins in `agent_epistemic_context(agent_did)`:
240///   1. `agent_did → believesViaHearsay → proposition_hash`
241///   2. `agent_did → cog:believes → proposition_hash`
242///   3. `proposition_hash → prov:wasDerivedFrom → informant_did` (hearsay source)
243pub fn assert_hearsay_belief(
244    agent_did: u64,
245    proposition_hash: u64,
246    informant_did: u64,
247    ts: u64,
248) -> [NQuin; 3] {
249    let ctx = agent_epistemic_context(agent_did);
250    [
251        make_ep(agent_did, P_BELIEVES_VIA_HEARSAY, proposition_hash, ctx, ts),
252        make_ep(agent_did, P_COG_BELIEVES, proposition_hash, ctx, ts),
253        make_ep(proposition_hash, P_WAS_DERIVED_FROM, informant_did, ctx, ts),
254    ]
255}
256
257/// Record a query event — `observer_did` queries about `query_subject_hash`.
258///
259/// The query event hash is derived deterministically from `(observer_did, query_subject_hash, ts)`.
260/// Returns 1 quin in `EPISTEMIC_CONTEXT` so other agents can reference it as a source.
261///
262/// Callers should capture the returned `NQuin::object` (= `query_event_hash`) and pass
263/// it to `assert_inferred_belief` for agents who observe this query.
264pub fn record_query_observation(observer_did: u64, query_subject_hash: u64, ts: u64) -> NQuin {
265    make_ep(
266        observer_did,
267        P_COG_QUERIES,
268        query_subject_hash,
269        EPISTEMIC_CONTEXT,
270        ts,
271    )
272}
273
274/// Record that `agent_did` typed `event_hash` as a `q42ep:CryptoSensor` result.
275/// Call this when wiring hardware-attested data into the objective context.
276pub fn register_crypto_sensor_output(proof_hash: u64, ts: u64) -> NQuin {
277    make_ep(
278        proof_hash,
279        P_RDF_TYPE,
280        C_CRYPTO_SENSOR,
281        OBJECTIVE_CONTEXT,
282        ts,
283    )
284}
285
286/// Record that `agent_did` typed `event_hash` as a `q42ep:AgentQuery`.
287/// Call this alongside `record_query_observation` so SHACL shapes can validate it.
288pub fn register_query_event(query_event_hash: u64, querying_agent_did: u64, ts: u64) -> [NQuin; 2] {
289    [
290        make_ep(
291            query_event_hash,
292            P_RDF_TYPE,
293            C_AGENT_QUERY,
294            EPISTEMIC_CONTEXT,
295            ts,
296        ),
297        make_ep(
298            query_event_hash,
299            P_WAS_ATTRIBUTED_TO,
300            querying_agent_did,
301            EPISTEMIC_CONTEXT,
302            ts,
303        ),
304    ]
305}
306
307// ── Query helpers ─────────────────────────────────────────────────────────────
308
309/// Write the propositions that `agent_did` holds ObjectiveKnowledge of into `out`.
310///
311/// Zero-heap: scans `quins` and writes matching proposition hashes into the
312/// caller-supplied `out` slice (bounded by `out.len()`); returns the count written.
313/// Matches beyond `out.len()` are dropped.
314pub fn objective_knowledge_of(quins: &[NQuin], agent_did: u64, out: &mut [u64]) -> usize {
315    let ctx = agent_epistemic_context(agent_did);
316    let mut n = 0usize;
317    for q in quins {
318        if q.context == ctx && q.subject == agent_did && q.predicate == P_KNOWS_DIRECTLY {
319            if n >= out.len() {
320                break;
321            }
322            out[n] = q.object;
323            n += 1;
324        }
325    }
326    n
327}
328
329/// Write the propositions that `agent_did` believes via any modality into `out`.
330///
331/// Zero-heap: see [`objective_knowledge_of`]. Returns the count written.
332pub fn all_beliefs_of(quins: &[NQuin], agent_did: u64, out: &mut [u64]) -> usize {
333    let ctx = agent_epistemic_context(agent_did);
334    let mut n = 0usize;
335    for q in quins {
336        if q.context == ctx && q.subject == agent_did && q.predicate == P_COG_BELIEVES {
337            if n >= out.len() {
338                break;
339            }
340            out[n] = q.object;
341            n += 1;
342        }
343    }
344    n
345}
346
347/// Return true if any quin in the slice classifies `proposition` as ObjectiveKnowledge
348/// for at least one agent.
349pub fn has_objective_proof(quins: &[NQuin], proposition_hash: u64) -> bool {
350    quins.iter().any(|q| {
351        q.context == OBJECTIVE_CONTEXT
352            && q.subject == proposition_hash
353            && q.predicate == P_WAS_GENERATED_BY
354    })
355}
356
357// ── Internal helper ───────────────────────────────────────────────────────────
358
359#[inline]
360fn make_ep(subject: u64, predicate: u64, object: u64, context: u64, lamport: u64) -> NQuin {
361    NQuin {
362        subject,
363        predicate,
364        object,
365        context,
366        metadata: lamport & 0xFFFF_FFFF,
367        parity: 0,
368    }
369}
370
371// ── Tests ─────────────────────────────────────────────────────────────────────
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    const JOHN: u64 = 0x111A_u64;
378    const FRANK: u64 = 0x222B_u64;
379    const JANE: u64 = 0x333C_u64;
380    const MATTER: u64 = 0xFACE_1234_u64;
381    const SENSOR: u64 = 0xDEAD_BEEF_u64;
382
383    #[test]
384    fn agent_epistemic_contexts_are_distinct() {
385        let ctx_john = agent_epistemic_context(JOHN);
386        let ctx_frank = agent_epistemic_context(FRANK);
387        let ctx_jane = agent_epistemic_context(JANE);
388        assert_ne!(ctx_john, ctx_frank);
389        assert_ne!(ctx_frank, ctx_jane);
390        assert_ne!(ctx_john, ctx_jane);
391    }
392
393    #[test]
394    fn objective_knowledge_produces_correct_contexts() {
395        let qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
396        let john_ctx = agent_epistemic_context(JOHN);
397        // First two quins are in John's epistemic context
398        assert_eq!(qs[0].context, john_ctx);
399        assert_eq!(qs[1].context, john_ctx);
400        // Justification chain quin is in the shared OBJECTIVE_CONTEXT
401        assert_eq!(qs[2].context, OBJECTIVE_CONTEXT);
402        assert_eq!(qs[2].predicate, P_WAS_GENERATED_BY);
403        assert_eq!(qs[2].object, SENSOR);
404    }
405
406    #[test]
407    fn inferred_belief_does_not_collide_with_objective() {
408        let frank_query = 0x9999_u64;
409        let obj_qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
410        let inf_qs = assert_inferred_belief(JANE, MATTER, frank_query, 2000);
411
412        let john_ctx = agent_epistemic_context(JOHN);
413        let jane_ctx = agent_epistemic_context(JANE);
414        assert_ne!(john_ctx, jane_ctx);
415
416        // John's quins are not in Jane's context
417        for q in &obj_qs {
418            assert_ne!(q.context, jane_ctx);
419        }
420        // Jane's quins are not in John's context
421        for q in &inf_qs {
422            assert_ne!(q.context, john_ctx);
423        }
424    }
425
426    #[test]
427    fn classify_detects_objective_knowledge() {
428        let qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
429        let state = classify_epistemic_state(&qs, JOHN, MATTER);
430        assert_eq!(state, EpistemicState::ObjectiveKnowledge);
431    }
432
433    #[test]
434    fn classify_detects_inferred_belief() {
435        let frank_query = 0x9999_u64;
436        let qs = assert_inferred_belief(JANE, MATTER, frank_query, 2000);
437        let state = classify_epistemic_state(&qs, JANE, MATTER);
438        assert_eq!(state, EpistemicState::InferredBelief);
439    }
440
441    #[test]
442    fn classify_detects_hearsay() {
443        let qs = assert_hearsay_belief(JANE, MATTER, FRANK, 3000);
444        let state = classify_epistemic_state(&qs, JANE, MATTER);
445        assert_eq!(state, EpistemicState::HearsayBelief);
446    }
447
448    #[test]
449    fn classify_returns_unknown_for_unrelated_quins() {
450        let qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
451        // Jane has no quins about MATTER
452        let state = classify_epistemic_state(&qs, JANE, MATTER);
453        assert_eq!(state, EpistemicState::Unknown);
454    }
455
456    #[test]
457    fn has_objective_proof_detects_crypto_link() {
458        let qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
459        assert!(has_objective_proof(&qs, MATTER));
460        assert!(!has_objective_proof(&qs, 0xFFFF_u64));
461    }
462
463    #[test]
464    fn objective_knowledge_and_beliefs_write_into_caller_buffer() {
465        let qs = assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000);
466
467        let mut buf = [0u64; 8];
468        let n = objective_knowledge_of(&qs, JOHN, &mut buf);
469        assert_eq!(n, 1);
470        assert_eq!(buf[0], MATTER);
471
472        let mut bel = [0u64; 8];
473        let m = all_beliefs_of(&qs, JOHN, &mut bel);
474        assert_eq!(m, 1);
475        assert_eq!(bel[0], MATTER);
476
477        // Bounded by the caller buffer: a zero-length `out` writes nothing.
478        let mut empty: [u64; 0] = [];
479        assert_eq!(objective_knowledge_of(&qs, JOHN, &mut empty), 0);
480    }
481
482    #[test]
483    fn john_frank_jane_scenario() {
484        let mut all: Vec<NQuin> = Vec::new();
485
486        // t₁: John directly observes the matter
487        all.extend_from_slice(&assert_objective_knowledge(JOHN, MATTER, SENSOR, 1000));
488
489        // t₂: Frank queries — his query is a shared epistemic event
490        all.push(record_query_observation(FRANK, MATTER, 2000));
491        let frank_query_hash = FRANK.wrapping_mul(0x9e37_79b9) ^ MATTER ^ 2000;
492
493        // t₃-t₄: Jane infers from Frank's query
494        all.extend_from_slice(&assert_inferred_belief(
495            JANE,
496            MATTER,
497            frank_query_hash,
498            3000,
499        ));
500
501        // Check: John has ObjectiveKnowledge
502        assert_eq!(
503            classify_epistemic_state(&all, JOHN, MATTER),
504            EpistemicState::ObjectiveKnowledge,
505        );
506        // Check: Jane has InferredBelief — not the same as John's
507        assert_eq!(
508            classify_epistemic_state(&all, JANE, MATTER),
509            EpistemicState::InferredBelief,
510        );
511        // Check: Frank has Unknown (he queried but never asserted a belief about it)
512        assert_eq!(
513            classify_epistemic_state(&all, FRANK, MATTER),
514            EpistemicState::Unknown,
515        );
516    }
517}