Skip to main content

qualia_core_db/query/
temporal_graph.rs

1//! Temporal overlay graph helpers — PROV-O + Dublin Core bi-temporal quins.
2//!
3//! All temporal quins are written to `T_CONTEXT`. Predicates are FNV-1a hashes
4//! of the standard W3C URIs, computed at compile time via `q_hash()`.
5//!
6//! Named graph context:
7//!   `T_CONTEXT = q_hash("urn:qualia:context:temporal")`
8//!
9//! Bi-temporal model:
10//! - **Assertion time** (`t_assert`) — when the system recorded the fact.
11//!   Stored as `prov:generatedAtTime`.
12//! - **Valid time** (`t_valid_start` / `t_valid_end`) — when the fact was true in reality.
13//!   Stored as `prov:startedAtTime` / `prov:endedAtTime`.
14//!
15//! All timestamps are milliseconds since Unix epoch (u64).
16
17use crate::{q_hash, NQuin};
18
19// ── Named-graph context ───────────────────────────────────────────────────────
20pub const T_CONTEXT: u64 = q_hash("urn:qualia:context:temporal");
21pub const AGENT_CONTEXT: u64 = q_hash("urn:qualia:context:agent");
22
23// ── PROV-O predicate hashes ───────────────────────────────────────────────────
24pub const P_GENERATED_AT: u64 = q_hash("http://www.w3.org/ns/prov#generatedAtTime");
25pub const P_STARTED_AT: u64 = q_hash("http://www.w3.org/ns/prov#startedAtTime");
26pub const P_ENDED_AT: u64 = q_hash("http://www.w3.org/ns/prov#endedAtTime");
27pub const P_WAS_ATTRIBUTED_TO: u64 = q_hash("http://www.w3.org/ns/prov#wasAttributedTo");
28pub const P_WAS_GENERATED_BY: u64 = q_hash("http://www.w3.org/ns/prov#wasGeneratedBy");
29pub const P_WAS_INVALIDATED_BY: u64 = q_hash("http://www.w3.org/ns/prov#wasInvalidatedBy");
30pub const P_INVALIDATED_AT: u64 = q_hash("http://www.w3.org/ns/prov#invalidatedAtTime");
31pub const P_HAD_PRIMARY_SOURCE: u64 = q_hash("http://www.w3.org/ns/prov#hadPrimarySource");
32
33// ── Dublin Core predicate hashes ─────────────────────────────────────────────
34pub const P_DC_VALID: u64 = q_hash("http://purl.org/dc/terms/valid");
35pub const P_DC_CREATOR: u64 = q_hash("http://purl.org/dc/terms/creator");
36pub const P_DC_DATE: u64 = q_hash("http://purl.org/dc/terms/date");
37
38// ── RDF type ─────────────────────────────────────────────────────────────────
39pub const P_RDF_TYPE: u64 = q_hash("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
40
41// ── CogAI class hashes ────────────────────────────────────────────────────────
42pub const C_COG_AGENT: u64 = q_hash("https://www.w3.org/community/cogai/ont#Agent");
43pub const C_COG_GOAL: u64 = q_hash("https://www.w3.org/community/cogai/ont#Goal");
44pub const C_COG_BELIEF: u64 = q_hash("https://www.w3.org/community/cogai/ont#Belief");
45pub const C_COG_PLAN: u64 = q_hash("https://www.w3.org/community/cogai/ont#Plan");
46pub const P_COG_HOLDS_BELIEF: u64 = q_hash("https://www.w3.org/community/cogai/ont#holdsBelief");
47pub const P_COG_HAS_GOAL: u64 = q_hash("https://www.w3.org/community/cogai/ont#hasGoal");
48pub const P_COG_HAS_PLAN: u64 = q_hash("https://www.w3.org/community/cogai/ont#hasPlan");
49
50// ── Q42 extension predicates ──────────────────────────────────────────────────
51pub const P_Q42_INFERENCE_AUTHORIZED_BY: u64 =
52    q_hash("urn:qualia:ontology:rights:inferenceAuthorizedBy");
53pub const P_Q42_PROVENANCE_CITATIONS: u64 =
54    q_hash("urn:qualia:ontology:rights:provenanceCitations");
55
56// ── assert_temporal ───────────────────────────────────────────────────────────
57
58/// Write PROV-O bi-temporal quins for `entity` to `T_CONTEXT`.
59///
60/// Returns up to 4 quins:
61///  1. `prov:generatedAtTime` = `t_assert` (always)
62///  2. `prov:startedAtTime` = `t_valid_start` (always)
63///  3. `prov:endedAtTime` = `t_valid_end` (if `Some`)
64///  4. `prov:wasAttributedTo` = `author_did` (if `Some`)
65///
66/// All timestamps are ms since Unix epoch.
67pub fn assert_temporal(
68    entity: u64,
69    t_valid_start: u64,
70    t_valid_end: Option<u64>,
71    t_assert: u64,
72    author_did: Option<u64>,
73) -> Vec<NQuin> {
74    let mut quins = Vec::with_capacity(4);
75    quins.push(make_temporal(entity, P_GENERATED_AT, t_assert));
76    quins.push(make_temporal(entity, P_STARTED_AT, t_valid_start));
77    if let Some(end) = t_valid_end {
78        quins.push(make_temporal(entity, P_ENDED_AT, end));
79    }
80    if let Some(did) = author_did {
81        quins.push(make_temporal(entity, P_WAS_ATTRIBUTED_TO, did));
82    }
83    quins
84}
85
86/// Record that an entity was invalidated (tombstoned) at `t_invalidate` by `agent_did`.
87pub fn invalidate_entity(entity: u64, agent_did: u64, t_invalidate: u64) -> [NQuin; 2] {
88    [
89        make_temporal(entity, P_WAS_INVALIDATED_BY, agent_did),
90        make_temporal(entity, P_INVALIDATED_AT, t_invalidate),
91    ]
92}
93
94// ── CogAI agent context quins ─────────────────────────────────────────────────
95
96/// Write `cog:Agent` type quin for a DID hash to `AGENT_CONTEXT`.
97pub fn register_agent(did_hash: u64, ts: u64) -> NQuin {
98    NQuin {
99        subject: did_hash,
100        predicate: P_RDF_TYPE,
101        object: C_COG_AGENT,
102        context: AGENT_CONTEXT,
103        metadata: ts & 0xFFFF_FFFF, // Lamport clock = lower 32 bits of ts
104        parity: 0,
105    }
106}
107
108/// Write `cog:Goal` for `(agent_did, goal_hash)` pair to `AGENT_CONTEXT`.
109pub fn write_goal(agent_did: u64, goal_hash: u64, ts: u64) -> [NQuin; 2] {
110    [
111        NQuin {
112            subject: goal_hash,
113            predicate: P_RDF_TYPE,
114            object: C_COG_GOAL,
115            context: AGENT_CONTEXT,
116            metadata: ts & 0xFFFF_FFFF,
117            parity: 0,
118        },
119        NQuin {
120            subject: agent_did,
121            predicate: P_COG_HAS_GOAL,
122            object: goal_hash,
123            context: AGENT_CONTEXT,
124            metadata: ts & 0xFFFF_FFFF,
125            parity: 0,
126        },
127    ]
128}
129
130/// Write `cog:Belief` with PROV-O provenance for `(agent_did, belief_hash)` pair.
131///
132/// Returns quins in `T_CONTEXT` (temporal) and `AGENT_CONTEXT` (belief link).
133pub fn write_belief(agent_did: u64, belief_hash: u64, t_assert: u64, t_valid: u64) -> [NQuin; 4] {
134    [
135        NQuin {
136            subject: belief_hash,
137            predicate: P_RDF_TYPE,
138            object: C_COG_BELIEF,
139            context: AGENT_CONTEXT,
140            metadata: t_assert & 0xFFFF_FFFF,
141            parity: 0,
142        },
143        NQuin {
144            subject: agent_did,
145            predicate: P_COG_HOLDS_BELIEF,
146            object: belief_hash,
147            context: AGENT_CONTEXT,
148            metadata: t_assert & 0xFFFF_FFFF,
149            parity: 0,
150        },
151        NQuin {
152            subject: belief_hash,
153            predicate: P_GENERATED_AT,
154            object: t_assert,
155            context: T_CONTEXT,
156            metadata: 0,
157            parity: 0,
158        },
159        NQuin {
160            subject: belief_hash,
161            predicate: P_STARTED_AT,
162            object: t_valid,
163            context: T_CONTEXT,
164            metadata: 0,
165            parity: 0,
166        },
167    ]
168}
169
170// ── Internal helpers ──────────────────────────────────────────────────────────
171
172#[inline]
173fn make_temporal(subject: u64, predicate: u64, object: u64) -> NQuin {
174    NQuin {
175        subject,
176        predicate,
177        object,
178        context: T_CONTEXT,
179        metadata: 0,
180        parity: 0,
181    }
182}
183
184// ── Fallback: no smallvec dependency ─────────────────────────────────────────
185// If smallvec is not in the dependency tree, fall back to Vec.
186// The module re-exports a `TempQuins` type alias so callers don't need to know.
187
188/// Variable-length return type for `assert_temporal`.
189pub type TempQuins = Vec<NQuin>;
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn assert_temporal_all_fields() {
197        let qs = assert_temporal(0x1111, 1_000, Some(2_000), 3_000, Some(0xDEAD));
198        assert_eq!(qs.len(), 4);
199        assert_eq!(qs[0].predicate, P_GENERATED_AT);
200        assert_eq!(qs[0].object, 3_000);
201        assert_eq!(qs[1].predicate, P_STARTED_AT);
202        assert_eq!(qs[1].object, 1_000);
203        assert_eq!(qs[2].predicate, P_ENDED_AT);
204        assert_eq!(qs[2].object, 2_000);
205        assert_eq!(qs[3].predicate, P_WAS_ATTRIBUTED_TO);
206        assert_eq!(qs[3].object, 0xDEAD);
207        for q in &qs {
208            assert_eq!(q.context, T_CONTEXT);
209        }
210    }
211
212    #[test]
213    fn assert_temporal_no_end_no_author() {
214        let qs = assert_temporal(0x1234, 500, None, 600, None);
215        assert_eq!(qs.len(), 2);
216    }
217
218    #[test]
219    fn cogai_belief_has_temporal_and_agent_context() {
220        let qs = write_belief(0xAAAA, 0xBBBB, 9_000, 8_000);
221        let t_ctx: Vec<_> = qs.iter().filter(|q| q.context == T_CONTEXT).collect();
222        let a_ctx: Vec<_> = qs.iter().filter(|q| q.context == AGENT_CONTEXT).collect();
223        assert_eq!(t_ctx.len(), 2);
224        assert_eq!(a_ctx.len(), 2);
225    }
226
227    #[test]
228    fn all_predicate_hashes_distinct() {
229        let predicates = [
230            P_GENERATED_AT,
231            P_STARTED_AT,
232            P_ENDED_AT,
233            P_WAS_ATTRIBUTED_TO,
234            P_WAS_GENERATED_BY,
235            P_WAS_INVALIDATED_BY,
236            P_INVALIDATED_AT,
237            P_DC_VALID,
238            P_DC_CREATOR,
239        ];
240        let mut seen = std::collections::HashSet::new();
241        for p in &predicates {
242            assert!(seen.insert(p), "duplicate predicate hash: {p}");
243        }
244    }
245}