Skip to main content

qualia_core_db/query/
shacl_compiler.rs

1use crate::modalities::epistemic::{
2    self, EpistemicStatus, OP_BELIEVES, OP_COMMON_KNOWLEDGE, OP_KNOWS,
3};
4use crate::modalities::logic::deontic::{
5    evaluate_deontic_contract, DeonticStatus, DeonticVerdict, OP_FORBID, OP_OBLIGATE, OP_PERMIT,
6};
7use crate::{q_hash, NQuin};
8
9/// Identifies the SHACL DataType for a node shape
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum ShaclDatatype {
12    String,
13    Integer,
14    Decimal,
15    Boolean,
16    DateTime,
17}
18
19impl ShaclDatatype {
20    /// Maps an IRI to the corresponding ShaclDatatype
21    pub fn from_iri_hash(hash: u64) -> Option<Self> {
22        match hash {
23            h if h == q_hash("xsd:string") => Some(ShaclDatatype::String),
24            h if h == q_hash("xsd:integer") => Some(ShaclDatatype::Integer),
25            h if h == q_hash("xsd:decimal") => Some(ShaclDatatype::Decimal),
26            h if h == q_hash("xsd:boolean") => Some(ShaclDatatype::Boolean),
27            h if h == q_hash("xsd:dateTime") => Some(ShaclDatatype::DateTime),
28            _ => None,
29        }
30    }
31}
32
33/// Zero-heap SHACL Constraint AST
34/// Uses primitive types and FNV-1a hashes to fit within the memory ceiling.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ShaclConstraint {
37    Datatype(ShaclDatatype),
38    MinLength(u32),
39    MaxLength(u32),
40    MinCount(u32),
41    MaxCount(u32),
42    /// For sh:in, we store up to 8 permitted hashes inline to avoid allocation.
43    /// If more are needed, it would overflow to a separate memory-mapped buffer.
44    In {
45        count: u8,
46        values: [u64; 8],
47    },
48
49    // Deontic & Epistemic Extensions (from AGENTS.md Task E)
50    DeonticObligate,
51    DeonticPermit,
52    DeonticForbid,
53    DeonticNotExpired {
54        now_unix: u32,
55    },
56    EpistemicKnowledge {
57        min_certainty: u8,
58    },
59    EpistemicBelief {
60        min_certainty: u8,
61    },
62    CommonKnowledge,
63}
64
65/// Evaluates a slice of NQuins against a set of constraints for a specific target property hash.
66/// Returns true if valid, false if a constraint violation occurs.
67pub fn validate_shacl_property(
68    quins: &[NQuin],
69    target_subject: u64,
70    target_property: u64,
71    constraints: &[ShaclConstraint],
72) -> bool {
73    let mut matching_count = 0;
74
75    for quin in quins {
76        if quin.subject == target_subject && quin.predicate == target_property {
77            matching_count += 1;
78
79            for constraint in constraints {
80                match constraint {
81                    ShaclConstraint::Datatype(expected_dt) => {
82                        // Extract inline type tag from object field (bits 60-62 when MSB=0)
83                        if quin.object >> 63 != 0 {
84                            // MSB=1 implies a pointer, not a literal
85                            return false;
86                        }
87                        let type_tag = (quin.object >> 60) & 0b111;
88                        let valid = match expected_dt {
89                            ShaclDatatype::String => type_tag == 0b000,
90                            ShaclDatatype::Integer => type_tag == 0b001,
91                            ShaclDatatype::Decimal => type_tag == 0b010,
92                            ShaclDatatype::Boolean => type_tag == 0b011,
93                            ShaclDatatype::DateTime => type_tag == 0b001, // Often stored as Unix epoch int
94                        };
95                        if !valid {
96                            return false;
97                        }
98                    }
99                    ShaclConstraint::MinLength(_) | ShaclConstraint::MaxLength(_) => {
100                        // In a real system, we'd need to resolve the string length from the object buffer.
101                        // Since strings are hashed, length constraints might require looking up the lexicon.
102                        // We skip this check if the data is just hashes.
103                        // For Phase D we assume true if not available.
104                    }
105                    ShaclConstraint::In { count, values } => {
106                        let payload = quin.object & 0x0FFF_FFFF_FFFF_FFFF;
107                        let mut found = false;
108                        for i in 0..*count as usize {
109                            if values[i] == payload {
110                                found = true;
111                                break;
112                            }
113                        }
114                        if !found {
115                            return false;
116                        }
117                    }
118                    ShaclConstraint::DeonticObligate => {
119                        if !deontic_quin_matches(quins, quin, OP_OBLIGATE, DeonticStatus::Active) {
120                            return false;
121                        }
122                    }
123                    ShaclConstraint::DeonticPermit => {
124                        if !deontic_quin_matches(quins, quin, OP_PERMIT, DeonticStatus::Active) {
125                            return false;
126                        }
127                    }
128                    ShaclConstraint::DeonticForbid => {
129                        if deontic_quin_matches(quins, quin, OP_FORBID, DeonticStatus::Active) {
130                            return false;
131                        }
132                    }
133                    ShaclConstraint::DeonticNotExpired { now_unix } => {
134                        if !deontic_not_expired(quin, *now_unix) {
135                            return false;
136                        }
137                    }
138                    ShaclConstraint::EpistemicKnowledge { min_certainty } => {
139                        if !epistemic_quin_matches(
140                            quins,
141                            quin,
142                            OP_KNOWS,
143                            *min_certainty,
144                            EpistemicStatus::Active,
145                        ) {
146                            return false;
147                        }
148                    }
149                    ShaclConstraint::EpistemicBelief { min_certainty } => {
150                        if !epistemic_quin_matches(
151                            quins,
152                            quin,
153                            OP_BELIEVES,
154                            *min_certainty,
155                            EpistemicStatus::Active,
156                        ) {
157                            return false;
158                        }
159                    }
160                    ShaclConstraint::CommonKnowledge => {
161                        if !epistemic_quin_matches(
162                            quins,
163                            quin,
164                            OP_COMMON_KNOWLEDGE,
165                            0,
166                            EpistemicStatus::Active,
167                        ) {
168                            return false;
169                        }
170                    }
171                    ShaclConstraint::MinCount(_) | ShaclConstraint::MaxCount(_) => {}
172                }
173            }
174        }
175    }
176
177    // Check cardinality counts
178    for constraint in constraints {
179        match constraint {
180            ShaclConstraint::MinCount(min) => {
181                if matching_count < *min {
182                    return false;
183                }
184            }
185            ShaclConstraint::MaxCount(max) => {
186                if matching_count > *max {
187                    return false;
188                }
189            }
190            ShaclConstraint::DeonticObligate
191            | ShaclConstraint::DeonticPermit
192            | ShaclConstraint::DeonticForbid
193            | ShaclConstraint::DeonticNotExpired { .. }
194            | ShaclConstraint::EpistemicKnowledge { .. }
195            | ShaclConstraint::EpistemicBelief { .. }
196            | ShaclConstraint::CommonKnowledge => {
197                if matching_count == 0 {
198                    return false;
199                }
200            }
201            _ => {}
202        }
203    }
204
205    true
206}
207
208fn deontic_not_expired(quin: &NQuin, now_unix: u32) -> bool {
209    let expiry = (quin.metadata & 0xFFFF_FFFF) as u32;
210    expiry == 0 || now_unix <= expiry
211}
212
213fn deontic_quin_matches(
214    quins: &[NQuin],
215    focus: &NQuin,
216    expected_opcode: u8,
217    required_status: DeonticStatus,
218) -> bool {
219    let mut verdicts = [DeonticVerdict::default(); 32];
220    let now = std::time::SystemTime::now()
221        .duration_since(std::time::UNIX_EPOCH)
222        .map(|d| d.as_secs() as u32)
223        .unwrap_or(0);
224    let count = evaluate_deontic_contract(quins, now, &mut verdicts).unwrap_or(0);
225    for verdict in &verdicts[..count] {
226        if verdict.norm.subject == focus.subject
227            && verdict.norm.predicate == focus.predicate
228            && verdict.norm.object == focus.object
229            && (verdict.norm.predicate & 0xFF) as u8 == expected_opcode
230            && verdict.status == required_status
231        {
232            return true;
233        }
234    }
235    false
236}
237
238fn epistemic_quin_matches(
239    quins: &[NQuin],
240    focus: &NQuin,
241    expected_opcode: u8,
242    min_certainty: u8,
243    required_status: EpistemicStatus,
244) -> bool {
245    let mut verdicts = [epistemic::EpistemicVerdict {
246        claim: NQuin::default(),
247        status: EpistemicStatus::Skipped,
248        certainty: 0,
249    }; 32];
250    let count =
251        epistemic::evaluate_epistemic_frame(quins, focus.subject, focus.context, &mut verdicts)
252            .unwrap_or(0);
253    for verdict in &verdicts[..count] {
254        if verdict.claim.object == focus.object
255            && (verdict.claim.predicate & 0xFF) as u8 == expected_opcode
256            && verdict.status == required_status
257            && verdict.certainty >= min_certainty
258        {
259            return true;
260        }
261    }
262    false
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_shacl_datatype_integer() {
271        let subj = q_hash("did:q42:patient1");
272        let prop = q_hash("q42:age");
273
274        let quin_int = NQuin {
275            subject: subj,
276            predicate: prop,
277            object: (0b001 << 60) | 42, // Integer tag + value 42
278            context: 0,
279            metadata: 0,
280            parity: 0,
281        };
282
283        let constraints = [ShaclConstraint::Datatype(ShaclDatatype::Integer)];
284
285        assert!(validate_shacl_property(
286            &[quin_int],
287            subj,
288            prop,
289            &constraints
290        ));
291
292        // Test failure on incorrect datatype (e.g. String tag 0b000)
293        let quin_str = NQuin {
294            subject: subj,
295            predicate: prop,
296            object: (0b000 << 60) | q_hash("forty-two"),
297            context: 0,
298            metadata: 0,
299            parity: 0,
300        };
301        assert!(!validate_shacl_property(
302            &[quin_str],
303            subj,
304            prop,
305            &constraints
306        ));
307    }
308
309    #[test]
310    fn test_shacl_deontic_obligate() {
311        let subj = q_hash("did:q42:party1");
312        let prop = q_hash("q42:mustSign");
313        let obj = q_hash("contract:nda");
314        let mut norm = crate::modalities::logic::deontic::compile_norm_quin(
315            subj,
316            OP_OBLIGATE,
317            prop,
318            obj,
319            q_hash("ctx:nda"),
320            u32::MAX,
321            false,
322        );
323        norm.parity = norm.subject ^ norm.predicate ^ norm.object ^ norm.context;
324
325        let constraints = [ShaclConstraint::DeonticObligate];
326        assert!(validate_shacl_property(
327            &[norm],
328            subj,
329            norm.predicate,
330            &constraints
331        ));
332    }
333
334    #[test]
335    fn test_shacl_epistemic_knowledge() {
336        let agent = q_hash("agent_a");
337        let claim_obj = q_hash("claim:p");
338        let mut knows = NQuin {
339            subject: agent,
340            predicate: (200u64 << 8) | OP_KNOWS as u64,
341            object: claim_obj,
342            context: q_hash("world_w"),
343            metadata: 0,
344            parity: 0,
345        };
346        knows.parity = knows.subject ^ knows.predicate ^ knows.object ^ knows.context;
347
348        let prop = knows.predicate;
349        let constraints = [ShaclConstraint::EpistemicKnowledge { min_certainty: 128 }];
350        assert!(validate_shacl_property(&[knows], agent, prop, &constraints));
351    }
352
353    #[test]
354    fn test_shacl_cardinality() {
355        let subj = q_hash("did:q42:user1");
356        let prop = q_hash("schema:email");
357
358        let quin = NQuin {
359            subject: subj,
360            predicate: prop,
361            object: (0b000 << 60) | q_hash("test@example.com"),
362            context: 0,
363            metadata: 0,
364            parity: 0,
365        };
366
367        // MinCount 1 -> passes
368        assert!(validate_shacl_property(
369            &[quin.clone()],
370            subj,
371            prop,
372            &[ShaclConstraint::MinCount(1)]
373        ));
374
375        // MinCount 2 -> fails
376        assert!(!validate_shacl_property(
377            &[quin.clone()],
378            subj,
379            prop,
380            &[ShaclConstraint::MinCount(2)]
381        ));
382    }
383}