Skip to main content

qualia_core_db/query/
temporal_scrub.rs

1//! Temporal scrub and frame-diff API for reconstructing the world state
2//! at a given historical coordinate, supporting Phase 3 replay-to-state
3//! and continuous spawn/decay validation.
4
5use crate::git_bridge::DagStore;
6use crate::NQuin;
7
8/// Replay mutations up to `as_of_ms` and return the materialized `NQuin` state.
9///
10/// Uses `resolver` to map from a DagNode's `quins_merkle` to the actual `NQuin` block.
11pub fn replay_to_state<'a, F>(dag: &'a DagStore, as_of_ms: u64, resolver: F) -> Vec<NQuin>
12where
13    F: Fn([u8; 32]) -> Option<Vec<NQuin>>,
14{
15    let mut state = Vec::new();
16
17    // Iterate nodes in topological order (insertion order) up to as_of_ms
18    for (node, _) in dag.nodes().iter().filter(|(n, _)| n.timestamp <= as_of_ms) {
19        if let Some(mutations) = resolver(node.quins_merkle) {
20            for quin in mutations {
21                state.push(quin);
22            }
23        }
24    }
25
26    state
27}
28
29/// Compute the frame-diff hints between two timestamps `t0` and `t1`.
30///
31/// Returns `(added_quins, removed_quins)` to tell the renderer exactly what changed.
32pub fn frame_diff_hints<'a, F>(
33    dag: &'a DagStore,
34    t0: u64,
35    t1: u64,
36    resolver: F,
37) -> (Vec<NQuin>, Vec<NQuin>)
38where
39    F: Fn([u8; 32]) -> Option<Vec<NQuin>>,
40{
41    let mut added = Vec::new();
42    let mut removed = Vec::new();
43
44    // To properly diff, we normally just take the mutations between t0 and t1.
45    // If the DAG nodes between t0 and t1 contain additions/removals, we extract them.
46    // Assuming monotonic additions for now since Q42 mutations are append-only.
47    // Invalidations are tombstoned logically rather than deleted physically.
48    let min_t = t0.min(t1);
49    let max_t = t0.max(t1);
50
51    for (node, _) in dag.nodes().iter() {
52        if node.timestamp > min_t && node.timestamp <= max_t {
53            if let Some(mutations) = resolver(node.quins_merkle) {
54                for quin in mutations {
55                    if t1 >= t0 {
56                        added.push(quin); // Rolling forward
57                    } else {
58                        removed.push(quin); // Rolling backward
59                    }
60                }
61            }
62        }
63    }
64
65    (added, removed)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_replay_to_state() {
74        let mut dag = DagStore::new();
75        let q1 = NQuin {
76            subject: 1,
77            predicate: 2,
78            object: 3,
79            context: 0,
80            metadata: 0,
81            parity: 0,
82        };
83        let q2 = NQuin {
84            subject: 4,
85            predicate: 5,
86            object: 6,
87            context: 0,
88            metadata: 0,
89            parity: 0,
90        };
91
92        dag.commit_node([0u8; 32], &[q1.clone()], 123, 1000, "commit1");
93        dag.commit_node([0u8; 32], &[q2.clone()], 123, 2000, "commit2");
94
95        let resolver = |merkle: [u8; 32]| -> Option<Vec<NQuin>> {
96            if merkle == crate::git_bridge::quins_merkle(&[q1.clone()]) {
97                Some(vec![q1.clone()])
98            } else if merkle == crate::git_bridge::quins_merkle(&[q2.clone()]) {
99                Some(vec![q2.clone()])
100            } else {
101                None
102            }
103        };
104
105        let state_at_1500 = replay_to_state(&dag, 1500, &resolver);
106        assert_eq!(state_at_1500.len(), 1);
107        assert_eq!(state_at_1500[0].subject, 1);
108
109        let state_at_2500 = replay_to_state(&dag, 2500, &resolver);
110        assert_eq!(state_at_2500.len(), 2);
111    }
112
113    #[test]
114    fn test_frame_diff_hints() {
115        let mut dag = DagStore::new();
116        let q1 = NQuin {
117            subject: 1,
118            predicate: 2,
119            object: 3,
120            context: 0,
121            metadata: 0,
122            parity: 0,
123        };
124        let q2 = NQuin {
125            subject: 4,
126            predicate: 5,
127            object: 6,
128            context: 0,
129            metadata: 0,
130            parity: 0,
131        };
132
133        dag.commit_node([0u8; 32], &[q1.clone()], 123, 1000, "commit1");
134        dag.commit_node([0u8; 32], &[q2.clone()], 123, 2000, "commit2");
135
136        let resolver = |merkle: [u8; 32]| -> Option<Vec<NQuin>> {
137            if merkle == crate::git_bridge::quins_merkle(&[q1.clone()]) {
138                Some(vec![q1.clone()])
139            } else if merkle == crate::git_bridge::quins_merkle(&[q2.clone()]) {
140                Some(vec![q2.clone()])
141            } else {
142                None
143            }
144        };
145
146        // Forward scrub
147        let (added, removed) = frame_diff_hints(&dag, 1500, 2500, &resolver);
148        assert_eq!(added.len(), 1);
149        assert_eq!(added[0].subject, 4);
150        assert_eq!(removed.len(), 0);
151
152        // Backward scrub
153        let (added, removed) = frame_diff_hints(&dag, 2500, 1500, &resolver);
154        assert_eq!(added.len(), 0);
155        assert_eq!(removed.len(), 1);
156        assert_eq!(removed[0].subject, 4);
157    }
158}