Skip to main content

qualia_core_db/
deontic_logic.rs

1//! Deontic logic engine — ODRL-based policy evaluation for credential-gated subgraph access.
2//!
3//! The policy hierarchy maps agent Verifiable Credentials to `SubgraphLayer` access rights.
4//! Policy evaluation is:
5//!   1. Extract `VcAttributes` from the agent's NQuin slice (credential claims).
6//!   2. Evaluate against the `SubgraphPolicy` for the requested layer.
7//!   3. If the policy passes, call `KeyVault::generate_layer_key()` and return it.
8//!
9//! The `deontic_logic` module is intentionally free of heap allocation in evaluation paths
10//! — all structures fit in fixed-size arrays.
11
12#[cfg(not(target_arch = "wasm32"))]
13use crate::key_vault::{KeyVault, SubgraphKey, SubgraphLayer};
14use crate::q_hash;
15
16// ── VC role IRI hashes ────────────────────────────────────────────────────────
17
18/// Compile-time hashes of canonical ODRL/Qualia role IRIs used in VC claims.
19pub mod vc_roles {
20    use crate::q_hash;
21
22    /// Any authenticated principal — no further VC attributes required.
23    pub const AUTHENTICATED: u64 = q_hash("urn:qualia:role:authenticated");
24    /// Professional context (e.g., organisation employee with NDA).
25    pub const PROFESSIONAL: u64 = q_hash("urn:qualia:role:professional");
26    /// Legal practitioner with privileged access to legal subgraph.
27    pub const LEGAL_PRACTITIONER: u64 = q_hash("urn:qualia:role:legal-practitioner");
28    /// Registered medical professional.
29    pub const MEDICAL_PROFESSIONAL: u64 = q_hash("urn:qualia:role:medical-professional");
30    /// Fiduciary duty holder (financial advisor, trustee, etc.).
31    pub const FIDUCIARY: u64 = q_hash("urn:qualia:role:fiduciary");
32    /// Qualia node operator — administrative role.
33    pub const NODE_OPERATOR: u64 = q_hash("urn:qualia:role:node-operator");
34}
35
36/// Predicate hash for VC role claims (`urn:qualia:vc:hasRole`).
37const P_HAS_ROLE: u64 = q_hash("urn:qualia:vc:hasRole");
38/// Predicate hash for VC clearance level (`urn:qualia:vc:clearanceLevel`).
39const P_CLEARANCE_LEVEL: u64 = q_hash("urn:qualia:vc:clearanceLevel");
40/// Predicate hash for VC issuer (`urn:qualia:vc:issuedBy`).
41const P_ISSUED_BY: u64 = q_hash("urn:qualia:vc:issuedBy");
42
43/// Maximum number of roles stored in `VcAttributes`.
44const MAX_ROLES: usize = 8;
45
46/// Parsed Verifiable Credential attributes for a single agent.
47///
48/// Extracted from the agent's NQuin slice via `VcAttributes::from_quins()`.
49#[derive(Debug, Clone, Copy)]
50pub struct VcAttributes {
51    pub did_hash: u64,
52    pub roles: [u64; MAX_ROLES],
53    pub role_count: u8,
54    /// Numeric clearance level (0=public … 4=fiduciary). Sourced from `vc:clearanceLevel`.
55    pub clearance_level: u8,
56    /// Hash of the VC issuer's DID — must be in the trusted-issuer set.
57    pub credential_issuer: u64,
58}
59
60impl VcAttributes {
61    /// Create a minimal `VcAttributes` for `did_hash` with no claims.
62    pub fn unauthenticated(did_hash: u64) -> Self {
63        Self {
64            did_hash,
65            roles: [0; MAX_ROLES],
66            role_count: 0,
67            clearance_level: 0,
68            credential_issuer: 0,
69        }
70    }
71
72    /// Parse VC claims for `agent_did` from a slice of NQuins.
73    ///
74    /// Scans for quins where `subject == agent_did` and predicate is one of
75    /// `P_HAS_ROLE`, `P_CLEARANCE_LEVEL`, or `P_ISSUED_BY`.
76    pub fn from_quins(agent_did: u64, quins: &[crate::NQuin]) -> Self {
77        let mut attrs = Self::unauthenticated(agent_did);
78        for q in quins {
79            if q.subject != agent_did {
80                continue;
81            }
82            if q.predicate == P_HAS_ROLE && (attrs.role_count as usize) < MAX_ROLES {
83                attrs.roles[attrs.role_count as usize] = q.object;
84                attrs.role_count += 1;
85            } else if q.predicate == P_CLEARANCE_LEVEL {
86                attrs.clearance_level = (q.object & 0x0F) as u8;
87            } else if q.predicate == P_ISSUED_BY {
88                attrs.credential_issuer = q.object;
89            }
90        }
91        attrs
92    }
93
94    /// Returns `true` if this VC includes `role_hash` in its role claims.
95    #[inline]
96    pub fn has_role(self, role_hash: u64) -> bool {
97        self.roles[..self.role_count as usize].contains(&role_hash)
98    }
99}
100
101#[cfg(not(target_arch = "wasm32"))]
102/// The result of a deontic policy evaluation for subgraph key release.
103#[derive(Debug)]
104pub enum DeonticResult {
105    /// Policy passed — the derived `SubgraphKey` can be released to the agent.
106    KeyRelease(SubgraphKey),
107    /// Policy denied. The key is not returned.
108    AccessDenied {
109        layer: SubgraphLayer,
110        reason: &'static str,
111    },
112}
113
114#[cfg(not(target_arch = "wasm32"))]
115impl DeonticResult {
116    pub fn is_permitted(&self) -> bool {
117        matches!(self, Self::KeyRelease(_))
118    }
119}
120
121/// Evaluate an agent's VCs against the ODRL policy for `layer` and, if permitted,
122/// return the derived `SubgraphKey`.
123///
124/// # Policy table
125///
126/// | Layer        | Minimum clearance | Accepted roles                         |
127/// |-------------|-------------------|-----------------------------------------|
128/// | Public      | 0 (any)           | (none required)                         |
129/// | Professional| 1                 | `role:professional`, `role:node-operator` |
130/// | Legal       | 2                 | `role:legal-practitioner`               |
131/// | Medical     | 3                 | `role:medical-professional`             |
132/// | Fiduciary   | 4                 | `role:fiduciary`                        |
133///
134/// Clearance level OR role match is sufficient; both are not required.
135///
136/// # Trusted issuers
137/// The `trusted_issuers` slice lists DID hashes of credential issuers the node
138/// accepts.  An empty slice disables issuer-trust enforcement (dev mode only).
139#[cfg(not(target_arch = "wasm32"))]
140pub fn evaluate_vc_for_subgraph_key_release(
141    vault: &KeyVault,
142    vc: &VcAttributes,
143    layer: SubgraphLayer,
144    trusted_issuers: &[u64],
145) -> DeonticResult {
146    // 1. Issuer trust check (skip if no trusted issuers configured).
147    if !trusted_issuers.is_empty() && !trusted_issuers.contains(&vc.credential_issuer) {
148        return DeonticResult::AccessDenied {
149            layer,
150            reason: "credential issuer not in trusted-issuer set",
151        };
152    }
153
154    // 2. Layer-specific policy.
155    let policy_passed = match layer {
156        SubgraphLayer::Public => true,
157
158        SubgraphLayer::Professional => {
159            vc.clearance_level >= 1
160                || vc.has_role(vc_roles::PROFESSIONAL)
161                || vc.has_role(vc_roles::NODE_OPERATOR)
162        }
163
164        SubgraphLayer::Legal => {
165            vc.clearance_level >= 2 || vc.has_role(vc_roles::LEGAL_PRACTITIONER)
166        }
167
168        SubgraphLayer::Medical => {
169            vc.clearance_level >= 3 || vc.has_role(vc_roles::MEDICAL_PROFESSIONAL)
170        }
171
172        SubgraphLayer::Fiduciary => vc.clearance_level >= 4 || vc.has_role(vc_roles::FIDUCIARY),
173    };
174
175    if policy_passed {
176        DeonticResult::KeyRelease(vault.generate_layer_key(layer))
177    } else {
178        DeonticResult::AccessDenied {
179            layer,
180            reason: "insufficient VC clearance or role",
181        }
182    }
183}
184
185/// Convenience: evaluate which layers the agent can access and write the permitted
186/// keys into `out`, in ascending layer order.
187///
188/// Zero-heap: writes at most `out.len().min(5)` `(SubgraphLayer, SubgraphKey)` entries
189/// (one per layer) into the caller-supplied slice and returns the count written.
190/// `SubgraphKey` is move-only (it zeroizes on drop), so `out` is a slice of `Option<…>`;
191/// written entries are `Some`. This function is not on a hot path.
192#[cfg(not(target_arch = "wasm32"))]
193pub fn evaluate_accessible_layers(
194    vault: &KeyVault,
195    vc: &VcAttributes,
196    trusted_issuers: &[u64],
197    out: &mut [Option<(SubgraphLayer, SubgraphKey)>],
198) -> usize {
199    let layers = [
200        SubgraphLayer::Public,
201        SubgraphLayer::Professional,
202        SubgraphLayer::Legal,
203        SubgraphLayer::Medical,
204        SubgraphLayer::Fiduciary,
205    ];
206    let mut n = 0usize;
207    for layer in layers {
208        if let DeonticResult::KeyRelease(key) =
209            evaluate_vc_for_subgraph_key_release(vault, vc, layer, trusted_issuers)
210        {
211            if n >= out.len() {
212                break;
213            }
214            out[n] = Some((layer, key));
215            n += 1;
216        }
217    }
218    n
219}
220
221/// Build the NQuins that record a VC credential claim for an agent, for insertion
222/// into the daemon graph.
223///
224/// Each `role_hash` becomes a `P_HAS_ROLE` quin in `CONTEXT`.
225/// `clearance` and `issuer_did` emit one quin each.
226pub fn write_vc_claim_quins(
227    agent_did: u64,
228    roles: &[u64],
229    clearance: u8,
230    issuer_did: u64,
231    ts: u64,
232) -> [crate::NQuin; 3] {
233    const VC_CONTEXT: u64 = q_hash("urn:qualia:context:vc");
234
235    let role_hash = if roles.is_empty() {
236        vc_roles::AUTHENTICATED
237    } else {
238        roles[0]
239    };
240
241    [
242        crate::NQuin {
243            subject: agent_did,
244            predicate: P_HAS_ROLE,
245            object: role_hash,
246            context: VC_CONTEXT,
247            metadata: ts & 0xFFFF_FFFF,
248            parity: agent_did ^ P_HAS_ROLE ^ role_hash ^ VC_CONTEXT,
249        },
250        crate::NQuin {
251            subject: agent_did,
252            predicate: P_CLEARANCE_LEVEL,
253            object: clearance as u64,
254            context: VC_CONTEXT,
255            metadata: ts & 0xFFFF_FFFF,
256            parity: agent_did ^ P_CLEARANCE_LEVEL ^ (clearance as u64) ^ VC_CONTEXT,
257        },
258        crate::NQuin {
259            subject: agent_did,
260            predicate: P_ISSUED_BY,
261            object: issuer_did,
262            context: VC_CONTEXT,
263            metadata: ts & 0xFFFF_FFFF,
264            parity: agent_did ^ P_ISSUED_BY ^ issuer_did ^ VC_CONTEXT,
265        },
266    ]
267}
268
269use crate::modalities::logic::deontic::{DEFEATER_BIT, OP_FORBID, OP_OBLIGATE, OP_PERMIT};
270use crate::modalities::logic::n3_compiler::CompiledRule;
271use crate::modalities::logic::n3_parser::RuleType;
272
273/// Compile an N3 rule into a norm Quin (or a defeater Quin if rule_type is Defeater).
274///
275/// Mapping:
276///   premise.triples[0].subject  → party_did_hash  (who is bound)
277///   premise.triples[0].predicate → property_path_hash  (what action/property)
278///   premise.triples[0].object   → action_object_hash  (target entity)
279///   rule.rule_type              → opcode + is_defeater flag
280///   conclusion.triples[0].subject → contract context hash
281///
282/// Returns None if the rule does not have the expected triple structure.
283pub fn compile_n3_rule_to_norm(
284    rule: &CompiledRule,
285    contract_hash: u64,
286    expiry_unix32: u32,
287) -> Option<crate::NQuin> {
288    if rule.premise.len == 0 || rule.conclusion.len == 0 {
289        return None;
290    }
291
292    let premise_triple = &rule.premise.triples[0];
293    let conclusion_triple = &rule.conclusion.triples[0];
294
295    let party_did_hash = premise_triple.subject.as_u64();
296    let property_path_hash = premise_triple.predicate.as_u64();
297    let action_object_hash = premise_triple.object.as_u64();
298
299    let mapped_contract_hash = if contract_hash == 0 {
300        conclusion_triple.subject.as_u64()
301    } else {
302        contract_hash
303    };
304
305    let mut opcode = OP_PERMIT;
306    let mut is_defeater = false;
307
308    let p_hash = property_path_hash;
309
310    let is_obligate = [
311        crate::q_hash("q42:obligate"),
312        crate::q_hash("q42:must"),
313        crate::q_hash("q42:shall"),
314    ]
315    .contains(&p_hash);
316    let is_permit = [
317        crate::q_hash("q42:permit"),
318        crate::q_hash("q42:may"),
319        crate::q_hash("q42:can"),
320    ]
321    .contains(&p_hash);
322    let is_forbid = [
323        crate::q_hash("q42:forbid"),
324        crate::q_hash("q42:not"),
325        crate::q_hash("q42:prohibit"),
326    ]
327    .contains(&p_hash);
328
329    match rule.rule_type {
330        RuleType::Strict => {
331            if is_obligate {
332                opcode = OP_OBLIGATE;
333            }
334        }
335        RuleType::Defeasible => {
336            if is_permit {
337                opcode = OP_PERMIT;
338            } else if is_forbid {
339                opcode = OP_FORBID;
340            }
341        }
342        RuleType::Defeater => {
343            opcode = OP_PERMIT;
344            is_defeater = true;
345        }
346        RuleType::Linear => {
347            if is_obligate {
348                opcode = OP_OBLIGATE;
349            }
350        }
351    }
352
353    let mut quin = crate::NQuin::default();
354    quin.subject = party_did_hash;
355
356    // Predicate: opcode in lower 8 bits, property hash shifted left 8 bits (masked to 55 bits), and DEFEATER_BIT if needed
357    let mut predicate_packed =
358        ((property_path_hash & 0x007F_FFFF_FFFF_FFFF) << 8) | (opcode as u64);
359    if is_defeater {
360        predicate_packed |= DEFEATER_BIT;
361    }
362    quin.predicate = predicate_packed;
363
364    quin.object = action_object_hash;
365    quin.context = mapped_contract_hash;
366    quin.metadata = expiry_unix32 as u64;
367    quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
368
369    Some(quin)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    fn test_vault() -> KeyVault {
377        let tmp = tempfile::tempdir().expect("tmpdir");
378        KeyVault::load_or_generate(tmp.path().to_str().unwrap()).expect("vault")
379    }
380
381    const ISSUER: u64 = 0x1550_0000_0000_CAFE;
382    const AGENT: u64 = 0xA6E4_7777_0000_0001;
383
384    fn vc_with_role(role: u64, clearance: u8) -> VcAttributes {
385        let mut vc = VcAttributes::unauthenticated(AGENT);
386        vc.roles[0] = role;
387        vc.role_count = 1;
388        vc.clearance_level = clearance;
389        vc.credential_issuer = ISSUER;
390        vc
391    }
392
393    #[test]
394    fn public_layer_always_accessible() {
395        let vault = test_vault();
396        let vc = VcAttributes::unauthenticated(AGENT);
397        let result = evaluate_vc_for_subgraph_key_release(&vault, &vc, SubgraphLayer::Public, &[]);
398        assert!(result.is_permitted());
399    }
400
401    #[test]
402    fn professional_layer_requires_role_or_clearance() {
403        let vault = test_vault();
404
405        // No role, no clearance → deny.
406        let no_vc = VcAttributes::unauthenticated(AGENT);
407        assert!(!evaluate_vc_for_subgraph_key_release(
408            &vault,
409            &no_vc,
410            SubgraphLayer::Professional,
411            &[]
412        )
413        .is_permitted());
414
415        // Professional role → permit.
416        let pro = vc_with_role(vc_roles::PROFESSIONAL, 0);
417        assert!(evaluate_vc_for_subgraph_key_release(
418            &vault,
419            &pro,
420            SubgraphLayer::Professional,
421            &[]
422        )
423        .is_permitted());
424
425        // Clearance 1 → permit.
426        let cleared = vc_with_role(0, 1);
427        assert!(evaluate_vc_for_subgraph_key_release(
428            &vault,
429            &cleared,
430            SubgraphLayer::Professional,
431            &[]
432        )
433        .is_permitted());
434    }
435
436    #[test]
437    fn medical_layer_requires_role_or_clearance_3() {
438        let vault = test_vault();
439
440        let low = vc_with_role(vc_roles::PROFESSIONAL, 0);
441        assert!(
442            !evaluate_vc_for_subgraph_key_release(&vault, &low, SubgraphLayer::Medical, &[])
443                .is_permitted()
444        );
445
446        let med_role = vc_with_role(vc_roles::MEDICAL_PROFESSIONAL, 0);
447        assert!(evaluate_vc_for_subgraph_key_release(
448            &vault,
449            &med_role,
450            SubgraphLayer::Medical,
451            &[]
452        )
453        .is_permitted());
454
455        let cleared = vc_with_role(0, 3);
456        assert!(evaluate_vc_for_subgraph_key_release(
457            &vault,
458            &cleared,
459            SubgraphLayer::Medical,
460            &[]
461        )
462        .is_permitted());
463    }
464
465    #[test]
466    fn fiduciary_layer_strictest() {
467        let vault = test_vault();
468
469        let med = vc_with_role(vc_roles::MEDICAL_PROFESSIONAL, 3);
470        assert!(
471            !evaluate_vc_for_subgraph_key_release(&vault, &med, SubgraphLayer::Fiduciary, &[])
472                .is_permitted()
473        );
474
475        let fid = vc_with_role(vc_roles::FIDUCIARY, 0);
476        assert!(
477            evaluate_vc_for_subgraph_key_release(&vault, &fid, SubgraphLayer::Fiduciary, &[])
478                .is_permitted()
479        );
480    }
481
482    #[test]
483    fn trusted_issuer_check_blocks_unknown_issuer() {
484        let vault = test_vault();
485        let trusted = [ISSUER];
486
487        let mut vc = vc_with_role(vc_roles::FIDUCIARY, 4);
488        vc.credential_issuer = 0xBAD_CAFE; // wrong issuer
489        assert!(!evaluate_vc_for_subgraph_key_release(
490            &vault,
491            &vc,
492            SubgraphLayer::Fiduciary,
493            &trusted
494        )
495        .is_permitted());
496
497        vc.credential_issuer = ISSUER; // correct issuer
498        assert!(evaluate_vc_for_subgraph_key_release(
499            &vault,
500            &vc,
501            SubgraphLayer::Fiduciary,
502            &trusted
503        )
504        .is_permitted());
505    }
506
507    #[test]
508    fn evaluate_accessible_layers_returns_permitted_set() {
509        let vault = test_vault();
510        // clearance 2 → Public + Professional + Legal, not Medical or Fiduciary.
511        let vc = vc_with_role(0, 2);
512        let mut out: [Option<(SubgraphLayer, SubgraphKey)>; 5] = [None, None, None, None, None];
513        let count = evaluate_accessible_layers(&vault, &vc, &[], &mut out);
514        assert_eq!(count, 3);
515        let layers = [
516            out[0].as_ref().unwrap().0,
517            out[1].as_ref().unwrap().0,
518            out[2].as_ref().unwrap().0,
519        ];
520        assert_eq!(
521            layers,
522            [
523                SubgraphLayer::Public,
524                SubgraphLayer::Professional,
525                SubgraphLayer::Legal
526            ]
527        );
528    }
529
530    #[test]
531    fn vc_attributes_from_quins_extracts_role_and_clearance() {
532        let quins = write_vc_claim_quins(AGENT, &[vc_roles::MEDICAL_PROFESSIONAL], 3, ISSUER, 1000);
533        let attrs = VcAttributes::from_quins(AGENT, &quins);
534        assert_eq!(attrs.role_count, 1);
535        assert!(attrs.has_role(vc_roles::MEDICAL_PROFESSIONAL));
536        assert_eq!(attrs.clearance_level, 3);
537        assert_eq!(attrs.credential_issuer, ISSUER);
538    }
539
540    #[test]
541    fn test_child_medical_data_egress_violation() {
542        use crate::mini_parser::{OP_END, OP_EVAL_PERMIT};
543        use crate::webizen_bytecode::{execute_program, GuardianshipContext, VmError};
544
545        let child_did = crate::q_hash("did:q42:child");
546        let medical_data = crate::q_hash("MedicalData");
547        let share_data = crate::q_hash("q42:shareData");
548
549        let mut intent_quin = crate::NQuin::default();
550        intent_quin.subject = child_did;
551        intent_quin.predicate = share_data;
552        intent_quin.object = medical_data;
553
554        let db = [intent_quin];
555        let mut prog = [0u8; 1024];
556        // Program just has OP_EVAL_PERMIT then OP_END
557        prog[0] = OP_EVAL_PERMIT;
558        prog[1] = OP_END;
559
560        let mut out = [crate::NQuin::default(); 10];
561
562        let context = GuardianshipContext {
563            principal_did: child_did,
564            guardian_did: None, // No active guardian signature
565        };
566
567        let result = execute_program(&prog, &db, &mut out, Some(&context));
568        assert_eq!(result, Err(VmError::HaltViolation));
569    }
570
571    #[test]
572    fn test_compile_n3_rule_to_norm() {
573        use crate::modalities::logic::n3_parser::RuleType;
574
575        use crate::modalities::logic::n3_compiler::{
576            CompiledFormula, CompiledTerm, CompiledTriple,
577        };
578        let make_rule = |rt: RuleType, pred: &str| -> CompiledRule {
579            let mut premise_triples = [CompiledTriple {
580                subject: CompiledTerm::Uri(0),
581                predicate: CompiledTerm::Uri(0),
582                object: CompiledTerm::Uri(0),
583            }; 8];
584            premise_triples[0] = CompiledTriple {
585                subject: CompiledTerm::Uri(crate::q_hash("did:party")),
586                predicate: CompiledTerm::Uri(crate::q_hash(pred)),
587                object: CompiledTerm::Uri(crate::q_hash("did:target")),
588            };
589            let mut conclusion_triples = [CompiledTriple {
590                subject: CompiledTerm::Uri(0),
591                predicate: CompiledTerm::Uri(0),
592                object: CompiledTerm::Uri(0),
593            }; 8];
594            conclusion_triples[0] = CompiledTriple {
595                subject: CompiledTerm::Uri(crate::q_hash("urn:contract")),
596                predicate: CompiledTerm::Uri(crate::q_hash("q42:boundBy")),
597                object: CompiledTerm::Uri(crate::q_hash("did:party")),
598            };
599            CompiledRule {
600                id_hash: None,
601                rule_type: rt,
602                weight: None,
603                premise: CompiledFormula {
604                    triples: premise_triples,
605                    len: 1,
606                },
607                conclusion: CompiledFormula {
608                    triples: conclusion_triples,
609                    len: 1,
610                },
611            }
612        };
613
614        let contract_hash = 12345;
615
616        // 1. Defeater
617        let defeater_rule = make_rule(RuleType::Defeater, "q42:permit");
618        let q = compile_n3_rule_to_norm(&defeater_rule, contract_hash, 0).unwrap();
619        assert_eq!(
620            q.predicate & 0xFF,
621            crate::modalities::logic::deontic::OP_PERMIT as u64
622        );
623        assert_ne!(
624            q.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
625            0
626        );
627
628        // 2. Defeasible Permit
629        let permit_rule = make_rule(RuleType::Defeasible, "q42:permit");
630        let q2 = compile_n3_rule_to_norm(&permit_rule, contract_hash, 0).unwrap();
631        assert_eq!(
632            q2.predicate & 0xFF,
633            crate::modalities::logic::deontic::OP_PERMIT as u64
634        );
635        assert_eq!(
636            q2.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
637            0
638        );
639
640        // 3. Strict Obligate
641        let obligate_rule = make_rule(RuleType::Strict, "q42:obligate");
642        let q3 = compile_n3_rule_to_norm(&obligate_rule, contract_hash, 0).unwrap();
643        assert_eq!(
644            q3.predicate & 0xFF,
645            crate::modalities::logic::deontic::OP_OBLIGATE as u64
646        );
647        assert_eq!(
648            q3.predicate & crate::modalities::logic::deontic::DEFEATER_BIT,
649            0
650        );
651
652        // 4. Malformed
653        let mut malformed = make_rule(RuleType::Strict, "q42:obligate");
654        malformed.premise.len = 0;
655        assert!(compile_n3_rule_to_norm(&malformed, contract_hash, 0).is_none());
656    }
657}