Skip to main content

qualia_core_db/modalities/logic/
deontic.rs

1//! Deontic Logic extension for the Qualia Bytecode VM.
2//!
3//! Implements a defeasible deontic contract evaluator over a `&[NQuin]` slice.
4//! Conforms to the 42 MB Prolog Sentinel memory ceiling, the 48-byte Super-Quin
5//! invariant, and the zero-heap-allocation mandate (no `Vec`, `String`, or `Box`).
6//!
7//! # Opcodes
8//!
9//! Three raw `u8` constants define the deontic modality, packed into **bits 0–7** of
10//! the `predicate` field of every norm Quin:
11//!
12//! | Constant      | Value | SDL formula | Meaning                          |
13//! |---------------|-------|-------------|----------------------------------|
14//! | `OP_OBLIGATE` | 0x10  | O(φ)        | Party *must* perform action φ    |
15//! | `OP_PERMIT`   | 0x11  | P(φ)        | Party *may* perform action φ     |
16//! | `OP_FORBID`   | 0x12  | F(φ)=O(¬φ)  | Party *must not* perform action φ|
17//!
18//! # 48-byte Norm Quin Layout
19//!
20//! ```text
21//! ┌──────────┬──────────────────────────────────────────────────────────────────┐
22//! │ Field    │ Bit layout                                                       │
23//! ├──────────┼──────────────────────────────────────────────────────────────────┤
24//! │ subject  │ [63]=0 (rsvd)  │ [0..62] = FNV-1a hash of the bound party DID  │
25//! │ predicate│ [63]=DEFEATER  │ [8..62] = property-path hash (action/norm URI) │
26//! │          │                │ [0..7]  = deontic opcode (OP_OBLIGATE etc.)    │
27//! │ object   │ [63]=0 (rsvd)  │ [0..62] = FNV-1a hash of the action object    │
28//! │ context  │ [56..63] = sensitivity class (from NQuin::SENSITIVITY_*)   │
29//! │          │ [0..55]  = q_hash of the contract/graph DID                     │
30//! │ metadata │ [61..62] = PermissiveRoutingLane bits                           │
31//! │          │ [32..60] = Lamport logical clock                                │
32//! │          │ [0..31]  = expiry as truncated Unix-32 timestamp                │
33//! │ parity   │ XOR fold of subject ⊕ predicate ⊕ object ⊕ context (ECC check)  │
34//! └──────────┴──────────────────────────────────────────────────────────────────┘
35//! ```
36//!
37//! # Defeater Nodes — `q42:unless`
38//!
39//! A Quin with **bit 63 of `predicate` set** (`DEFEATER_BIT`) is not a primary norm;
40//! it is a `q42:unless` exception node that defeats any norm sharing the same
41//! (subject, context, property-path) fingerprint.  This supports non-monotonic,
42//! defeasible reasoning:
43//!
44//! > *Alice is forbidden from disclosing project data — **unless** she is speaking
45//! > to a certified auditor.*
46//!
47//! The evaluator performs a two-phase linear scan:
48//! 1. **Defeater harvest** — collect up to `MAX_DEFEATER_SLOTS` fingerprints into
49//!    a fixed `[u64; 64]` stack buffer (512 bytes, one cache-line group on Cortex-A78).
50//! 2. **Norm evaluation** — for each non-defeater Quin: check expiry, probe the
51//!    defeater buffer, emit a `DeonticVerdict` into the caller-supplied `out` slice.
52//!
53//! # Legal SHACL Blueprint
54//!
55//! ## Non-Disclosure Agreement (NDA)
56//!
57//! NDA between `did:web:alice.example` and `did:web:bob.example`, covering
58//! confidential project-X data, valid until 2028-01-01 (Unix epoch 1 830 297 600).
59//! Three Quins fully encode the agreement and its auditor exception:
60//!
61//! ```text
62//! // Quin 1 — Alice's confidentiality prohibition
63//! subject   = q_hash("did:web:alice.example")
64//! predicate = OP_FORBID as u64
65//!           | (q_hash("q42:disclose") << 8)           // property-path in [8..62]
66//! object    = q_hash("q42:data:project-x:confidential")
67//! context   = q_hash("did:web:nda:contract-001")      // contract graph
68//! metadata  = 1_830_297_600_u64                        // expiry in bits [0..31]
69//! parity    = subject ^ predicate ^ object ^ context   // ECC fold
70//!
71//! // Quin 2 — Bob's symmetric prohibition (identical structure, different subject)
72//! subject   = q_hash("did:web:bob.example")
73//! predicate = OP_FORBID as u64 | (q_hash("q42:disclose") << 8)
74//! object    = q_hash("q42:data:project-x:confidential")
75//! context   = q_hash("did:web:nda:contract-001")
76//! metadata  = 1_830_297_600_u64
77//! parity    = subject ^ predicate ^ object ^ context
78//!
79//! // Quin 3 — Defeater: Alice MAY disclose to a certified auditor (q42:unless)
80//! subject   = q_hash("did:web:alice.example")
81//! predicate = DEFEATER_BIT                             // bit 63 marks q42:unless
82//!           | OP_PERMIT as u64
83//!           | (q_hash("q42:disclose") << 8)           // same property-path as Quin 1
84//! object    = q_hash("q42:role:certified-auditor")    // excepted entity class
85//! context   = q_hash("did:web:nda:contract-001")      // same contract graph
86//! metadata  = 1_830_297_600_u64
87//! parity    = subject ^ predicate ^ object ^ context
88//! ```
89//!
90//! Quin 3 shares (subject, context, property-path) with Quin 1, so the evaluator
91//! marks Quin 1 as `DeonticStatus::Defeated` when an auditor invokes the exception.
92//!
93//! ## Guardianship Contract
94//!
95//! Ward: `did:web:ward.example`, Guardian: `did:web:guardian.example`.
96//! Guardianship expires at majority (2030-01-01, epoch 1 893 456 000):
97//!
98//! ```text
99//! // Quin 1 — Guardian obligated to act in the ward's best interest
100//! subject   = q_hash("did:web:guardian.example")
101//! predicate = OP_OBLIGATE as u64
102//!           | (q_hash("q42:actInBestInterest") << 8)
103//! object    = q_hash("did:web:ward.example")
104//! context   = q_hash("did:web:guardianship:contract-002")
105//! metadata  = 1_893_456_000_u64   // contract expires when ward reaches majority
106//! parity    = subject ^ predicate ^ object ^ context
107//!
108//! // Quin 2 — Temporal defeater: ward may self-determine after majority age
109//! subject   = q_hash("did:web:ward.example")
110//! predicate = DEFEATER_BIT
111//!           | OP_PERMIT as u64
112//!           | (q_hash("q42:actInBestInterest") << 8)  // defeats the same obligation path
113//! object    = q_hash("did:web:ward.example")
114//! context   = q_hash("did:web:guardianship:contract-002")
115//! metadata  = 1_893_456_000_u64   // carries same timestamp; expiry semantics applied
116//! parity    = subject ^ predicate ^ object ^ context
117//! ```
118//!
119//! After majority, `now_unix > 1_893_456_000` causes Quin 1 to emit
120//! `DeonticStatus::Expired`, and the defeater Quin 2 itself becomes moot —
121//! demonstrating how temporal bounds compose naturally with defeasibility in a
122//! single linear scan without branching stacks.
123//!
124//! # Edge-native 3-core CPU triad
125//!
126//! The two-phase design maps to the triad naturally:
127//! - **Core 0** — defeater harvest (Phase 1, read-only, highly prefetchable).
128//! - **Core 1** — norm evaluation (Phase 2, linear probe of the 512-byte buffer).
129//! - **Core 2** — verdict dispatch / downstream enforcement routing.
130//!
131//! Cache-line pressure is bounded: the `[u64; MAX_DEFEATER_SLOTS]` buffer fits in
132//! 8 × 64-byte cache lines; each `DeonticVerdict` is 64 bytes (one cache line).
133
134#[cfg(any(
135    not(target_arch = "wasm32"),
136    feature = "wasm-scientific",
137    feature = "wasm-full"
138))]
139use crate::modalities::logic::n3_parser::{RuleType, Term};
140use crate::q_hash;
141use crate::NQuin;
142
143// ─── Deontic Opcodes ─────────────────────────────────────────────────────────
144//
145// These u8 constants are packed into bits [0..7] of the `predicate` field of
146// every norm Quin.  Values 0x10–0x12 are chosen above the mini_parser opcode
147// range (0x00–0x04) to allow mixed Quin databases without collision.
148
149/// O(φ) — the subject party *must* perform the action.
150pub const OP_OBLIGATE: u8 = 0x10;
151
152/// P(φ) — the subject party *may* perform the action.
153pub const OP_PERMIT: u8 = 0x11;
154
155/// F(φ) = O(¬φ) — the subject party *must not* perform the action.
156pub const OP_FORBID: u8 = 0x12;
157
158// ─── SDL⁺ extension opcodes (deontic block 0x13–0x1F, per DEONTIC_LOGIC_PLAN §3) ──
159
160/// U(φ) — optionality / indifference: `¬O(φ) ∧ ¬F(φ)`. The system is indifferent;
161/// neither doing nor omitting φ is a violation. May be asserted or derived
162/// (see [`is_optional`]).
163pub const OP_OPTIONAL: u8 = 0x13;
164
165/// G(φ) — gratuitousness: `¬O(φ)`. The agent is free to omit φ (it may still be
166/// permitted or forbidden). May be asserted or derived (see [`is_gratuitous`]).
167pub const OP_GRATUITOUS: u8 = 0x14;
168
169/// O(q | p) — the head of a dyadic / conditional obligation: q is obligatory
170/// *given* condition p. Evaluation is fact-driven (see [`evaluate_conditional_obligation`]);
171/// contrary-to-duty is the special case p = "primary breached".
172pub const OP_CONDITIONAL: u8 = 0x15;
173
174/// Reserved for Phase 3 (STIT agency): `O[α stit φ]`. Declared here to fence the
175/// opcode so nothing else claims it before agency lands.
176pub const OP_STIT: u8 = 0x16;
177
178/// An *undercutting* defeater: combined with [`DEFEATER_BIT`] it invalidates the
179/// inference link `p ⇒ Oq` without asserting `¬Oq` (vs a *rebutting* defeater — a
180/// `DEFEATER_BIT` node with an O/P/F opcode — which asserts the contrary). The
181/// fingerprint match is identical (the opcode byte is masked out); only the
182/// classification in [`DefeatKind`] differs.
183pub const OP_UNDERCUT: u8 = 0x17;
184
185/// Bit 63 of `predicate`: marks a `q42:unless` defeater / exception node.
186/// When set the Quin is *not* a primary norm and defeats matching obligations.
187/// Canonical bit position lives in the FrameLayout ABI (single source of truth).
188pub use crate::frame_layout::DEFEATER_BIT;
189
190/// Stack capacity for defeater fingerprints per evaluation call.
191/// 64 slots × 8 bytes = 512 bytes — fits within a single L1 cache-line group.
192pub const MAX_DEFEATER_SLOTS: usize = 64;
193
194// ─── DeonticStatus ────────────────────────────────────────────────────────────
195
196/// The result of evaluating a single norm Quin against temporal bounds and defeaters.
197#[repr(u8)]
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
199pub enum DeonticStatus {
200    #[default]
201    /// Norm is temporally valid and has no active defeater.
202    Active = 0x00,
203    /// A matching `q42:unless` defeater node was found; obligation is overridden.
204    Defeated = 0x01,
205    /// Current timestamp exceeds the expiry embedded in `metadata[0..31]`.
206    Expired = 0x02,
207    /// The Quin's predicate carries an unrecognised opcode byte; skipped by caller.
208    Malformed = 0x03,
209    /// Norm is parsed and valid, but its effectivity window has not yet begun
210    /// (`now < effective_from`). Not yet binding. (Lifecycle, Phase 1.)
211    Pending = 0x04,
212    /// An in-force obligation whose action was not performed (or a prohibition that
213    /// was breached), per the supplied facts. Triggers CTD / sanction routing.
214    Violated = 0x05,
215    /// An in-force obligation that has been fulfilled per the supplied facts; the
216    /// specific duty terminates.
217    Discharged = 0x06,
218}
219
220/// How a norm came to be [`DeonticStatus::Defeated`] — Hart/Pollock's rebutting vs
221/// undercutting distinction. A *rebutting* defeater asserts the contrary conclusion
222/// (`DEFEATER_BIT` + an O/P/F opcode); an *undercutting* defeater ([`OP_UNDERCUT`])
223/// severs the rule's support without asserting the contrary.
224#[repr(u8)]
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
226pub enum DefeatKind {
227    #[default]
228    /// Not defeated.
229    None = 0x00,
230    /// Defeated by a contrary norm (rebutting).
231    Rebutting = 0x01,
232    /// Defeated by link-invalidation (undercutting).
233    Undercutting = 0x02,
234}
235
236// ─── DeonticVerdict ───────────────────────────────────────────────────────────
237
238/// A verdict emitted for one norm Quin.  Exactly 64 bytes — one cache line.
239///
240/// Layout: 48-byte norm Quin + 1-byte status + 1-byte opcode + 6-byte pad = 56 B
241/// aligned to 8, padded by the compiler to the nearest power-of-two boundary.
242#[repr(C, align(8))]
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244pub struct DeonticVerdict {
245    /// The original norm Quin that was evaluated.
246    pub norm: NQuin,
247    /// Outcome of the evaluation.
248    pub status: DeonticStatus,
249    /// Deontic opcode extracted from `norm.predicate[0..7]`.
250    pub opcode: u8,
251    /// When `status == Defeated`, *how* it was defeated (rebutting vs undercutting);
252    /// `None` otherwise.
253    pub defeat_kind: DefeatKind,
254    _pad: [u8; 5],
255}
256
257// ─── DeonticError ─────────────────────────────────────────────────────────────
258
259#[derive(Debug, PartialEq)]
260pub enum DeonticError {
261    /// The caller-supplied `out` slice was exhausted before the scan completed.
262    OutputBufferFull,
263}
264
265// ─── Bit-field helpers ────────────────────────────────────────────────────────
266
267/// Extracts the deontic opcode from bits [0..7] of a `predicate` word.
268#[inline(always)]
269pub fn extract_deontic_opcode(predicate: u64) -> u8 {
270    (predicate & 0xFF) as u8
271}
272
273/// Extracts the 32-bit expiry from bits [0..31] of a `metadata` word.
274/// A zero value means "no expiry set" and is always treated as valid.
275#[inline(always)]
276pub fn extract_expiry_unix32(metadata: u64) -> u32 {
277    (metadata & 0xFFFF_FFFF) as u32
278}
279
280/// Produces the defeater-matching fingerprint for any Quin (norm or defeater).
281///
282/// Two Quins share a fingerprint iff they bind the same party (`subject`),
283/// the same contract graph (`context`), and the same property-path
284/// (`predicate[8..62]` — the portion above the opcode byte and below the
285/// defeater bit).  The opcode byte and defeater bit are masked out so that a
286/// `q42:unless` node correctly matches the norm it defeats.
287#[inline(always)]
288pub fn defeater_fingerprint(q: &NQuin) -> u64 {
289    // Strip defeater bit (63) and opcode byte (0..7); retain property-path (8..62).
290    let path_bits = q.predicate & 0x7FFF_FFFF_FFFF_FF00;
291    q.subject ^ q.context ^ path_bits
292}
293
294/// Harvest `q42:unless` defeater fingerprints from a contract slice (Phase 1 only).
295pub fn harvest_defeater_fingerprints(quins: &[NQuin], out: &mut [u64]) -> usize {
296    let mut count = 0usize;
297    for &q in quins {
298        if q.predicate & DEFEATER_BIT == 0 {
299            continue;
300        }
301        let expected_parity = q.subject ^ q.predicate ^ q.object ^ q.context;
302        if q.parity == expected_parity && count < out.len() {
303            out[count] = defeater_fingerprint(&q);
304            count += 1;
305        }
306    }
307    count
308}
309
310/// Returns `true` if the defeater buffer contains a fingerprint that matches `norm`.
311#[inline]
312pub fn norm_has_active_defeater(norm: &NQuin, defeaters: &[u64]) -> bool {
313    has_defeater(defeaters, norm)
314}
315
316#[inline]
317fn has_defeater(defeaters: &[u64], norm: &NQuin) -> bool {
318    let key = defeater_fingerprint(norm);
319    let mut i = 0;
320    while i < defeaters.len() {
321        if defeaters[i] == key {
322            return true;
323        }
324        i += 1;
325    }
326    false
327}
328
329/// Like [`has_defeater`], but returns *which kind* of defeater matched (rebutting vs
330/// undercutting), or [`DefeatKind::None`] if the norm is undefeated. `kinds[i]` is the
331/// kind of `defeaters[i]` (parallel arrays harvested together).
332#[inline]
333fn defeater_kind_for(defeaters: &[u64], kinds: &[DefeatKind], norm: &NQuin) -> DefeatKind {
334    let key = defeater_fingerprint(norm);
335    let mut i = 0;
336    while i < defeaters.len() {
337        if defeaters[i] == key {
338            return kinds[i];
339        }
340        i += 1;
341    }
342    DefeatKind::None
343}
344
345// ─── evaluate_deontic_contract ────────────────────────────────────────────────
346
347/// Evaluate a deontic contract encoded as a `&[NQuin]` slice.
348///
349/// ## Algorithm
350///
351/// **Phase 1 — Defeater harvest** (single forward pass, O(n)):
352/// Every Quin whose `predicate` has `DEFEATER_BIT` set is identified as a
353/// `q42:unless` defeater.  Its fingerprint is written into the fixed-capacity
354/// `[u64; MAX_DEFEATER_SLOTS]` stack buffer.  Excess defeaters beyond
355/// `MAX_DEFEATER_SLOTS` are silently dropped (contracts this large exceed the
356/// 42 MB Prolog Sentinel and are rejected at ingest time).
357///
358/// **Phase 2 — Norm evaluation** (single forward pass, O(n)):
359/// Every Quin whose `predicate[0..7]` ∈ {`OP_OBLIGATE`, `OP_PERMIT`, `OP_FORBID`}
360/// and whose `DEFEATER_BIT` is **clear** is a primary norm.  For each:
361/// 1. Temporal check: if `expiry != 0 && now_unix > expiry` → `Expired`.
362/// 2. Defeater probe: if `has_defeater(buffer, quin)` → `Defeated`.
363/// 3. Otherwise → `Active`.
364///
365/// Non-deontic Quins (opcode not in the set above) are skipped silently.
366///
367/// ## Constraints
368///
369/// - Zero heap allocation: all state lives in registers and the caller-supplied
370///   `out` slice.
371/// - Stack budget: `8 × MAX_DEFEATER_SLOTS` bytes (512 B) + frame overhead.
372/// - Deterministic O(n²) worst-case defeater probe, O(n) amortised for contracts
373///   with few exceptions (the common case in legal documents).
374///
375/// ## Parameters
376///
377/// * `quins`     — the deontic contract encoded as a Quin slice.
378/// * `now_unix`  — current time as a truncated 32-bit Unix timestamp.
379/// * `out`       — caller-supplied verdict buffer; must be `≥` the number of
380///                 norm Quins in `quins` to avoid `OutputBufferFull`.
381///
382/// ## Returns
383///
384/// `Ok(n)` where `n` is the number of verdicts written to `out[..n]`.
385pub fn evaluate_deontic_contract(
386    quins: &[NQuin],
387    now_unix: u32,
388    out: &mut [DeonticVerdict],
389) -> Result<usize, DeonticError> {
390    // ── Phase 1: harvest defeater fingerprints ─────────────────────────────────
391    //
392    // Stack-allocated; fits in < 1 KB, well within any thread stack.
393    let mut defeater_buf = [0u64; MAX_DEFEATER_SLOTS];
394    let mut kind_buf = [DefeatKind::Rebutting; MAX_DEFEATER_SLOTS];
395    let mut defeater_count = 0usize;
396
397    for &q in quins {
398        if q.predicate & DEFEATER_BIT != 0 {
399            // ECC Parity XOR fold check
400            let expected_parity = q.subject ^ q.predicate ^ q.object ^ q.context;
401            if q.parity == expected_parity {
402                if defeater_count < MAX_DEFEATER_SLOTS {
403                    defeater_buf[defeater_count] = defeater_fingerprint(&q);
404                    // OP_UNDERCUT severs the rule link; any other opcode rebuts.
405                    kind_buf[defeater_count] = if extract_deontic_opcode(q.predicate) == OP_UNDERCUT
406                    {
407                        DefeatKind::Undercutting
408                    } else {
409                        DefeatKind::Rebutting
410                    };
411                    defeater_count += 1;
412                }
413                // Excess defeaters are dropped; contracts this dense are rejected upstream.
414            }
415        }
416    }
417
418    let active_defeaters = &defeater_buf[..defeater_count];
419    let active_kinds = &kind_buf[..defeater_count];
420
421    // ── Phase 2: evaluate norm Quins ──────────────────────────────────────────
422    let mut verdict_count = 0usize;
423
424    for &q in quins {
425        // Defeater nodes are not norms; skip them in the second pass.
426        if q.predicate & DEFEATER_BIT != 0 {
427            continue;
428        }
429
430        let expected_parity = q.subject ^ q.predicate ^ q.object ^ q.context;
431        if q.parity != expected_parity {
432            if verdict_count >= out.len() {
433                return Err(DeonticError::OutputBufferFull);
434            }
435            out[verdict_count] = DeonticVerdict {
436                norm: q,
437                status: DeonticStatus::Malformed,
438                opcode: extract_deontic_opcode(q.predicate),
439                defeat_kind: DefeatKind::None,
440                _pad: [0u8; 5],
441            };
442            verdict_count += 1;
443            continue;
444        }
445
446        let opcode = extract_deontic_opcode(q.predicate);
447
448        let mut defeat_kind = DefeatKind::None;
449        let status = match opcode {
450            OP_OBLIGATE | OP_PERMIT | OP_FORBID => {
451                let expiry = extract_expiry_unix32(q.metadata);
452                if expiry != 0 && now_unix > expiry {
453                    DeonticStatus::Expired
454                } else {
455                    let k = defeater_kind_for(active_defeaters, active_kinds, &q);
456                    if k != DefeatKind::None {
457                        defeat_kind = k;
458                        DeonticStatus::Defeated
459                    } else {
460                        DeonticStatus::Active
461                    }
462                }
463            }
464            // Not a deontic Quin — skip silently (e.g. SHACL shape Quins coexist).
465            _ => continue,
466        };
467
468        if verdict_count >= out.len() {
469            return Err(DeonticError::OutputBufferFull);
470        }
471
472        out[verdict_count] = DeonticVerdict {
473            norm: q,
474            status,
475            opcode,
476            defeat_kind,
477            _pad: [0u8; 5],
478        };
479        verdict_count += 1;
480    }
481
482    Ok(verdict_count)
483}
484
485// ─── N3 → deontic norm bridge ───────────────────────────────────────────────
486
487#[cfg(any(
488    not(target_arch = "wasm32"),
489    feature = "wasm-scientific",
490    feature = "wasm-full"
491))]
492pub fn term_uri_hash(term: &Term) -> Option<u64> {
493    crate::modalities::logic::n3_parser::term_uri_hash(term)
494}
495
496// Canonical `values:` deontic operators. The registry stores a compiled rule
497// (hashes only - the predicate IRI string is gone), so the deontic opcode is
498// recovered by matching the premise predicate hash against these. Both the full
499// IRI and the CURIE token are listed, because `@prefix` is not expanded on the
500// parsed-from-file path (matching is by raw token via `q_hash`).
501#[cfg(any(
502    not(target_arch = "wasm32"),
503    feature = "wasm-scientific",
504    feature = "wasm-full"
505))]
506const FORBID_HASHES: [u64; 2] = [
507    q_hash("https://ns.webcivics.net/values/forbids"),
508    q_hash("values:forbids"),
509];
510#[cfg(any(
511    not(target_arch = "wasm32"),
512    feature = "wasm-scientific",
513    feature = "wasm-full"
514))]
515const PERMIT_HASHES: [u64; 2] = [
516    q_hash("https://ns.webcivics.net/values/permits"),
517    q_hash("values:permits"),
518];
519#[cfg(any(
520    not(target_arch = "wasm32"),
521    feature = "wasm-scientific",
522    feature = "wasm-full"
523))]
524const OBLIGATE_HASHES: [u64; 4] = [
525    q_hash("https://ns.webcivics.net/values/requires"),
526    q_hash("values:requires"),
527    q_hash("https://ns.webcivics.net/values/obligates"),
528    q_hash("values:obligates"),
529];
530
531/// Classify a premise-predicate hash into a deontic opcode (+ defeater flag).
532///
533/// A `Defeater` (`^>`) rule is always a `q42:unless` permit-defeater. Otherwise a
534/// recognised `values:` operator picks the opcode; an unrecognised predicate
535/// falls back to the rule-type default (Strict/Linear => obligation, Defeasible =>
536/// permission), preserving behaviour for non-`values` contract predicates.
537#[cfg(any(
538    not(target_arch = "wasm32"),
539    feature = "wasm-scientific",
540    feature = "wasm-full"
541))]
542fn opcode_from_predicate_hash(pred_hash: u64, rule_type: RuleType) -> (u8, bool) {
543    if matches!(rule_type, RuleType::Defeater) {
544        return (OP_PERMIT, true);
545    }
546    if FORBID_HASHES.contains(&pred_hash) {
547        return (OP_FORBID, false);
548    }
549    if PERMIT_HASHES.contains(&pred_hash) {
550        return (OP_PERMIT, false);
551    }
552    if OBLIGATE_HASHES.contains(&pred_hash) {
553        return (OP_OBLIGATE, false);
554    }
555    match rule_type {
556        RuleType::Strict | RuleType::Linear => (OP_OBLIGATE, false),
557        RuleType::Defeasible => (OP_PERMIT, false),
558        RuleType::Defeater => (OP_PERMIT, true),
559    }
560}
561
562/// Compile an N3 [`Rule`] into a norm Quin (or defeater Quin for `^>` rules).
563///
564/// Maps premise triple → party / property / action; `rule_type` → opcode + defeater flag.
565#[cfg(any(
566    not(target_arch = "wasm32"),
567    feature = "wasm-scientific",
568    feature = "wasm-full"
569))]
570pub fn compile_n3_rule_to_norm(
571    rule: &crate::modalities::logic::n3_compiler::CompiledRule,
572    contract_hash: u64,
573    expiry_unix32: u32,
574) -> Option<NQuin> {
575    // `triples` is a fixed `[_; 8]` array, so `.first()` is always `Some`; an
576    // empty rule must be rejected on `len`, not on `.first()`.
577    if rule.premise.len == 0 {
578        return None;
579    }
580    let premise = rule.premise.triples.first()?;
581    let party = premise.subject.as_u64();
582    let property_path = premise.predicate.as_u64();
583    let action_object = premise.object.as_u64();
584
585    // Recover the deontic opcode from the premise predicate hash (the compiled
586    // rule no longer carries the IRI string).
587    let (opcode, is_defeater) = opcode_from_predicate_hash(property_path, rule.rule_type);
588
589    Some(compile_norm_quin(
590        party,
591        opcode,
592        property_path,
593        action_object,
594        contract_hash,
595        expiry_unix32,
596        is_defeater,
597    ))
598}
599
600// ─── compile_norm_quin ────────────────────────────────────────────────────────
601
602/// Build a norm Quin from its logical components.
603///
604/// Convenience constructor that packs the deontic opcode and property-path hash
605/// into `predicate`, stores the contract DID in `context`, and sets the ECC
606/// parity to the XOR fold of the four semantic fields.
607///
608/// # Parameters
609/// * `party_did_hash`    — `q_hash` of the bound party's DID.
610/// * `opcode`            — `OP_OBLIGATE`, `OP_PERMIT`, or `OP_FORBID`.
611/// * `property_path_hash`— `q_hash` of the obligation/action URI.
612/// * `action_object_hash`— `q_hash` of the action's target entity/data.
613/// * `contract_hash`     — `q_hash` of the contract graph DID.
614/// * `expiry_unix32`     — 32-bit Unix timestamp for the norm's expiry (0 = no expiry).
615/// * `is_defeater`       — when `true`, sets `DEFEATER_BIT` making this a `q42:unless` node.
616#[inline]
617pub fn compile_norm_quin(
618    party_did_hash: u64,
619    opcode: u8,
620    property_path_hash: u64,
621    action_object_hash: u64,
622    contract_hash: u64,
623    expiry_unix32: u32,
624    is_defeater: bool,
625) -> NQuin {
626    let defeater_flag = if is_defeater { DEFEATER_BIT } else { 0u64 };
627    // Mask DEFEATER_BIT from the shifted path so only `is_defeater` controls bit 63.
628    let path_bits = (property_path_hash << 8) & !DEFEATER_BIT;
629    let predicate = defeater_flag | path_bits | (opcode as u64);
630    let metadata = expiry_unix32 as u64; // bits [0..31]; Lamport/routing bits left zero
631    let parity = party_did_hash ^ predicate ^ action_object_hash ^ contract_hash;
632
633    NQuin {
634        subject: party_did_hash,
635        predicate,
636        object: action_object_hash,
637        context: contract_hash,
638        metadata,
639        parity,
640    }
641}
642
643// ─── Contrary-to-duty (dyadic deontic) ──────────────────────────────────────────
644
645/// Contrary-to-duty obligation `O(reparation / breach)`: a *secondary* obligation
646/// that arises precisely because a *primary* obligation was breached (the
647/// remedy/reparation logic — Geneva/ICCPR remedy instruments). Returns `true` iff
648/// the CTD is satisfied: either the primary was NOT breached by the party (the
649/// CTD is not triggered), or it was breached AND the reparation has been fulfilled.
650///
651/// Facts convention: a breach is `(party, q42:breached, primary)`; a fulfilled
652/// reparation is `(party, q42:fulfilled, reparation)`. Zero-heap (linear scans).
653pub fn evaluate_contrary_to_duty(
654    facts: &[NQuin],
655    party: u64,
656    primary: u64,
657    reparation: u64,
658) -> bool {
659    // CTD is the dyadic obligation O(reparation | breached(primary)).
660    evaluate_conditional_obligation(facts, party, q_hash("q42:breached"), primary, reparation)
661}
662
663/// General dyadic / conditional obligation `O(obligation | condition)`: the obligation
664/// is binding only *given* the condition holds. Returns `true` iff the conditional is
665/// satisfied — either the condition does not hold (vacuously satisfied), or it holds
666/// AND the obligation has been fulfilled.
667///
668/// Facts convention: the condition holds iff `(party, condition_pred, condition_obj)` is
669/// present; the obligation is fulfilled iff `(party, q42:fulfilled, obligation_obj)` is.
670/// Contrary-to-duty is the special case `condition_pred = q42:breached`. Zero-heap.
671pub fn evaluate_conditional_obligation(
672    facts: &[NQuin],
673    party: u64,
674    condition_pred: u64,
675    condition_obj: u64,
676    obligation_obj: u64,
677) -> bool {
678    let triggered = facts
679        .iter()
680        .any(|q| q.subject == party && q.predicate == condition_pred && q.object == condition_obj);
681    if !triggered {
682        return true; // condition absent → conditional obligation not triggered
683    }
684    let fulfilled = q_hash("q42:fulfilled");
685    facts
686        .iter()
687        .any(|q| q.subject == party && q.predicate == fulfilled && q.object == obligation_obj)
688}
689
690// ─── Deontic lifecycle (Pending → Active → {Violated, Discharged, Defeated, Expired}) ─
691
692/// Compute the full lifecycle status of a single norm against an effectivity window,
693/// the current time, the harvested defeaters, and a fact slice.
694///
695/// Transition order (first match wins):
696/// 1. `effective_from != 0 && now < effective_from` → [`Pending`](DeonticStatus::Pending).
697/// 2. `expiry != 0 && now > expiry` → [`Expired`](DeonticStatus::Expired).
698/// 3. a matching defeater → [`Defeated`](DeonticStatus::Defeated).
699/// 4. in-force, then the facts decide:
700///    - `OP_OBLIGATE`: `(party, q42:fulfilled, action)` → [`Discharged`]; else
701///      `(party, q42:breached, action)` → [`Violated`]; else [`Active`].
702///    - `OP_FORBID`: `(party, q42:performed, action)` → [`Violated`]; else [`Active`].
703///    - `OP_PERMIT`: always [`Active`] (a liberty cannot be violated or discharged).
704///
705/// Zero-heap (linear scans). `active_defeaters` is the buffer from
706/// [`harvest_defeater_fingerprints`].
707pub fn norm_lifecycle_status(
708    norm: &NQuin,
709    now_unix: u32,
710    effective_from: u32,
711    active_defeaters: &[u64],
712    facts: &[NQuin],
713) -> DeonticStatus {
714    if effective_from != 0 && now_unix < effective_from {
715        return DeonticStatus::Pending;
716    }
717    let expiry = extract_expiry_unix32(norm.metadata);
718    if expiry != 0 && now_unix > expiry {
719        return DeonticStatus::Expired;
720    }
721    if has_defeater(active_defeaters, norm) {
722        return DeonticStatus::Defeated;
723    }
724    let party = norm.subject;
725    let action = norm.object;
726    let opcode = extract_deontic_opcode(norm.predicate);
727    let fact_present = |pred: u64| {
728        facts
729            .iter()
730            .any(|q| q.subject == party && q.predicate == pred && q.object == action)
731    };
732    match opcode {
733        OP_OBLIGATE => {
734            if fact_present(q_hash("q42:fulfilled")) {
735                DeonticStatus::Discharged
736            } else if fact_present(q_hash("q42:breached")) {
737                DeonticStatus::Violated
738            } else {
739                DeonticStatus::Active
740            }
741        }
742        OP_FORBID => {
743            if fact_present(q_hash("q42:performed")) {
744                DeonticStatus::Violated
745            } else {
746                DeonticStatus::Active
747            }
748        }
749        _ => DeonticStatus::Active, // OP_PERMIT and others: a liberty cannot be violated
750    }
751}
752
753// ─── Optionality (U) and Gratuitousness (G) — derived modalities ────────────────
754
755/// True iff action φ is **optional / indifferent** for `party`: `¬O(φ) ∧ ¬F(φ)` — no
756/// active (non-defeater) obligation and no prohibition over `(party, action)` in the
757/// norm slice. (An explicit `OP_OPTIONAL` assertion also counts.)
758pub fn is_optional(norms: &[NQuin], party: u64, action: u64) -> bool {
759    !has_active_norm(norms, party, action, OP_OBLIGATE)
760        && !has_active_norm(norms, party, action, OP_FORBID)
761}
762
763/// True iff action φ is **gratuitous** (non-obligatory) for `party`: `¬O(φ)` — no
764/// active obligation over `(party, action)` (it may still be permitted or forbidden).
765pub fn is_gratuitous(norms: &[NQuin], party: u64, action: u64) -> bool {
766    !has_active_norm(norms, party, action, OP_OBLIGATE)
767}
768
769/// Helper: is there a non-defeater norm with `opcode` binding `party` to `action`?
770/// Matches the explicit modality opcode (`OP_OPTIONAL`/`OP_GRATUITOUS` short-circuit).
771fn has_active_norm(norms: &[NQuin], party: u64, action: u64, opcode: u8) -> bool {
772    norms.iter().any(|q| {
773        q.predicate & DEFEATER_BIT == 0
774            && q.subject == party
775            && q.object == action
776            && extract_deontic_opcode(q.predicate) == opcode
777    })
778}
779
780// ─── Norm-conflict resolution (proportionality + human-rights priority) ──────────
781
782/// Do two deontic OPCODES conflict — an obligation/permission to do φ vs a prohibition of φ?
783pub fn opcodes_conflict(a: u8, b: u8) -> bool {
784    let permits = |op: u8| op == OP_OBLIGATE || op == OP_PERMIT;
785    (permits(a) && b == OP_FORBID) || (permits(b) && a == OP_FORBID)
786}
787
788/// Do two norms CONFLICT — same party (`subject`) over the same action (`object`), with
789/// deontically opposed opcodes? Active (non-defeater) norms only.
790pub fn norms_conflict(a: &NQuin, b: &NQuin) -> bool {
791    a.predicate & DEFEATER_BIT == 0
792        && b.predicate & DEFEATER_BIT == 0
793        && a.subject == b.subject
794        && a.object == b.object
795        && opcodes_conflict(
796            extract_deontic_opcode(a.predicate),
797            extract_deontic_opcode(b.predicate),
798        )
799}
800
801/// The outcome of resolving a norm conflict.
802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
803pub enum NormResolution {
804    /// The first norm prevails.
805    FirstPrevails,
806    /// The second norm prevails.
807    SecondPrevails,
808    /// A genuine conflict — routed to human review (never auto-flattened).
809    RequiresHumanReview,
810}
811
812/// Resolve a norm conflict by, in strict order:
813///  1. **Non-derogable human-rights priority** — a norm grounded in a non-derogable instrument
814///     defeats a derogable one (never weaken a non-derogable principle).
815///  2. **Proportionality** — if neither/both are non-derogable, the norm whose action is
816///     *proportionate* (`legal_compose::proportionality_met`: marginal harm < advantage) prevails
817///     over a disproportionate one.
818///  3. Otherwise **human review** — a contested norm is never auto-flattened.
819///
820/// `a_proportionate`/`b_proportionate` are the proportionality verdicts (`None` = unmodelled).
821pub fn resolve_norm_conflict(
822    a_nonderogable: bool,
823    b_nonderogable: bool,
824    a_proportionate: Option<bool>,
825    b_proportionate: Option<bool>,
826) -> NormResolution {
827    match (a_nonderogable, b_nonderogable) {
828        (true, false) => return NormResolution::FirstPrevails,
829        (false, true) => return NormResolution::SecondPrevails,
830        _ => {}
831    }
832    match (a_proportionate, b_proportionate) {
833        (Some(true), Some(false)) => NormResolution::FirstPrevails,
834        (Some(false), Some(true)) => NormResolution::SecondPrevails,
835        _ => NormResolution::RequiresHumanReview,
836    }
837}
838
839// ─── Permissions as non-fungible cryptographic constraints ──────────────────────
840
841/// A collision-resistant (BLAKE3) fingerprint over a Quin's six fields — the cryptographic
842/// binding anchor. Changing any field changes the fingerprint.
843pub fn nquin_binding_hash(q: &NQuin) -> u64 {
844    let mut bytes = [0u8; 48];
845    bytes[0..8].copy_from_slice(&q.subject.to_le_bytes());
846    bytes[8..16].copy_from_slice(&q.predicate.to_le_bytes());
847    bytes[16..24].copy_from_slice(&q.object.to_le_bytes());
848    bytes[24..32].copy_from_slice(&q.context.to_le_bytes());
849    bytes[32..40].copy_from_slice(&q.metadata.to_le_bytes());
850    bytes[40..48].copy_from_slice(&q.parity.to_le_bytes());
851    let h = blake3::hash(&bytes);
852    u64::from_le_bytes(h.as_bytes()[..8].try_into().unwrap())
853}
854
855/// Compile a permission into a **non-fungible cryptographic constraint** bound to a *specific*
856/// target nquin: the constraint carries, in `context`, a BLAKE3 binding to `target`, so the
857/// permission cannot be detached and reused for a different nquin — it travels persistently with
858/// that exact one. The identity layer SIGNS this envelope (the engine never holds keys — see
859/// `meta_deontic::endorsement_credential`); this constructs the bound, verifiable constraint.
860pub fn compile_permission_constraint(action: u64, principal: u64, target: &NQuin) -> NQuin {
861    let binding = nquin_binding_hash(target);
862    let mut c = NQuin {
863        subject: principal,
864        predicate: OP_PERMIT as u64,
865        object: action,
866        context: binding,
867        metadata: 0,
868        parity: 0,
869    };
870    c.parity = c.subject ^ c.predicate ^ c.object ^ c.context;
871    c
872}
873
874/// Verify that a permission `constraint` is bound to `target` (the non-fungibility check): its
875/// `context` binding must match `target`'s current fingerprint. Tampering with `target`, or moving
876/// the constraint to a different nquin, breaks the binding.
877pub fn permission_binds_to(constraint: &NQuin, target: &NQuin) -> bool {
878    constraint.context == nquin_binding_hash(target)
879}
880
881// ─── Tests ────────────────────────────────────────────────────────────────────
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886
887    #[test]
888    fn norm_conflict_detection_and_proportional_resolution() {
889        let party = q_hash("did:party");
890        let action = q_hash("act:disclose");
891        let mk = |op: u8| {
892            let mut q = NQuin {
893                subject: party,
894                predicate: op as u64,
895                object: action,
896                context: 0,
897                metadata: 0,
898                parity: 0,
899            };
900            q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
901            q
902        };
903        // Obligate-disclose vs Forbid-disclose for the same party/action → conflict.
904        assert!(norms_conflict(&mk(OP_OBLIGATE), &mk(OP_FORBID)));
905        assert!(opcodes_conflict(OP_PERMIT, OP_FORBID));
906        // Two obligations don't conflict; different actions don't.
907        assert!(!norms_conflict(&mk(OP_OBLIGATE), &mk(OP_OBLIGATE)));
908        let mut other = mk(OP_FORBID);
909        other.object = q_hash("act:other");
910        assert!(!norms_conflict(&mk(OP_OBLIGATE), &other));
911
912        // Resolution: non-derogable beats derogable.
913        assert_eq!(
914            resolve_norm_conflict(true, false, None, None),
915            NormResolution::FirstPrevails
916        );
917        assert_eq!(
918            resolve_norm_conflict(false, true, None, None),
919            NormResolution::SecondPrevails
920        );
921        // Neither non-derogable → proportionality decides.
922        assert_eq!(
923            resolve_norm_conflict(false, false, Some(true), Some(false)),
924            NormResolution::FirstPrevails
925        );
926        assert_eq!(
927            resolve_norm_conflict(false, false, Some(false), Some(true)),
928            NormResolution::SecondPrevails
929        );
930        // Both non-derogable, or proportionality unmodelled → human review.
931        assert_eq!(
932            resolve_norm_conflict(true, true, None, None),
933            NormResolution::RequiresHumanReview
934        );
935        assert_eq!(
936            resolve_norm_conflict(false, false, None, None),
937            NormResolution::RequiresHumanReview
938        );
939    }
940
941    #[test]
942    fn permission_is_non_fungibly_bound_to_its_nquin() {
943        let principal = q_hash("did:principal");
944        let action = q_hash("act:read");
945        let mut target = NQuin {
946            subject: q_hash("doc:42"),
947            predicate: q_hash("q42:hasContent"),
948            object: q_hash("blob:abc"),
949            context: 7,
950            metadata: 0,
951            parity: 0,
952        };
953        target.parity = target.subject ^ target.predicate ^ target.object ^ target.context;
954
955        let c = compile_permission_constraint(action, principal, &target);
956        assert!(
957            permission_binds_to(&c, &target),
958            "the permission binds to its target nquin"
959        );
960        assert_eq!(extract_deontic_opcode(c.predicate), OP_PERMIT);
961
962        // Tampering with the target breaks the binding (non-fungible / tamper-evident).
963        let mut tampered = target;
964        tampered.object ^= 0x1;
965        assert!(
966            !permission_binds_to(&c, &tampered),
967            "any edit to the target breaks the binding"
968        );
969
970        // The constraint cannot be reused for a DIFFERENT nquin.
971        let mut other = target;
972        other.subject = q_hash("doc:99");
973        other.parity = other.subject ^ other.predicate ^ other.object ^ other.context;
974        assert!(
975            !permission_binds_to(&c, &other),
976            "permission is not fungible across nquins"
977        );
978    }
979    use crate::q_hash;
980
981    #[test]
982    fn contrary_to_duty_requires_reparation_after_breach() {
983        let party = q_hash("did:web:acme");
984        let primary = q_hash("q42:protectData");
985        let reparation = q_hash("q42:notifyAndRemedy");
986        let mk = |s: u64, p: u64, o: u64| {
987            let mut q = NQuin {
988                subject: s,
989                predicate: p,
990                object: o,
991                context: 0,
992                metadata: 0,
993                parity: 0,
994            };
995            q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
996            q
997        };
998        // No breach → satisfied (CTD not triggered).
999        assert!(evaluate_contrary_to_duty(&[], party, primary, reparation));
1000        // Breach without reparation → NOT satisfied.
1001        let breach = [mk(party, q_hash("q42:breached"), primary)];
1002        assert!(!evaluate_contrary_to_duty(
1003            &breach, party, primary, reparation
1004        ));
1005        // Breach WITH reparation → satisfied.
1006        let repaired = [
1007            mk(party, q_hash("q42:breached"), primary),
1008            mk(party, q_hash("q42:fulfilled"), reparation),
1009        ];
1010        assert!(evaluate_contrary_to_duty(
1011            &repaired, party, primary, reparation
1012        ));
1013    }
1014
1015    /// Webizen values-credential smoke test (PLAN §11.3 / §17.1) — THE KEYSTONE.
1016    ///
1017    /// Proves a real values prohibition flows the live deontic lane:
1018    ///   N3 `Rule` (ns.webcivics.net) → `compile_n3_rule_to_norm` → `evaluate_deontic_contract`
1019    ///   → `DeonticVerdict`  (this is exactly what the `NativeDeonticEval` opcode dispatches to).
1020    /// And that the engine's NATIVE defeasibility flips Active → Defeated when a `q42:unless`
1021    /// defeater is present. No `n3logic.rs::infer_logic_bindings` on this path.
1022    #[test]
1023    fn values_credential_deontic_smoke() {
1024        use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType, Term, Triple};
1025
1026        // A values prohibition (UDHR Art 30 family) as a parsed-shape N3 rule:
1027        //   { values:Agent  values:forbids  values:DestructionOfRights }
1028        let prohibition = Rule {
1029            id: Some("UDHR-Art30-smoke"),
1030            rule_type: RuleType::Strict,
1031            weight: None,
1032            premise: Formula {
1033                triples: vec![Triple {
1034                    subject: Term::Uri("https://ns.webcivics.net/values/Agent"),
1035                    predicate: Term::Uri("https://ns.webcivics.net/values/forbids"),
1036                    object: Term::Uri("https://ns.webcivics.net/values/DestructionOfRights"),
1037                }],
1038            },
1039            conclusion: Formula { triples: vec![] },
1040        };
1041        let contract = q_hash("contract:udhr-smoke");
1042
1043        // ── values rule → norm Quin (the values→deontic bridge) ──
1044        let norm = compile_n3_rule_to_norm(
1045            &crate::modalities::logic::n3_compiler::compile_rule_to_zero_heap(&prohibition),
1046            contract,
1047            0,
1048        )
1049        .expect("a values prohibition must compile to a norm Quin");
1050        assert_eq!(
1051            extract_deontic_opcode(norm.predicate),
1052            OP_FORBID,
1053            "a `values:forbids` rule must compile to an OP_FORBID norm"
1054        );
1055
1056        // ── evaluate via the native deontic VM path → the prohibition is Active (live) ──
1057        let mut out = [DeonticVerdict::default(); 4];
1058        let n = evaluate_deontic_contract(&[norm], NOW, &mut out)
1059            .expect("deontic evaluation must succeed");
1060        assert_eq!(n, 1, "exactly one norm verdict expected");
1061        assert_eq!(
1062            out[0].status,
1063            DeonticStatus::Active,
1064            "the values prohibition holds (Active) — it is live in the engine, not bot-faked"
1065        );
1066        assert_eq!(out[0].opcode, OP_FORBID);
1067
1068        // ── native defeasibility: a `q42:unless` defeater on the same party+path+contract
1069        //    flips Active → Defeated ("forbidden ... UNLESS lawfully authorised"). ──
1070        let party = q_hash("https://ns.webcivics.net/values/Agent");
1071        let path = q_hash("https://ns.webcivics.net/values/forbids");
1072        let defeater = compile_norm_quin(
1073            party,
1074            OP_PERMIT,
1075            path,
1076            q_hash("https://ns.webcivics.net/values/lawfullyAuthorised"),
1077            contract,
1078            0,
1079            /* is_defeater = */ true,
1080        );
1081        let mut out2 = [DeonticVerdict::default(); 4];
1082        let n2 = evaluate_deontic_contract(&[norm, defeater], NOW, &mut out2)
1083            .expect("deontic evaluation with defeater must succeed");
1084        assert_eq!(
1085            n2, 1,
1086            "the defeater is not a primary norm; one verdict expected"
1087        );
1088        assert_eq!(
1089            out2[0].status,
1090            DeonticStatus::Defeated,
1091            "an `unless` defeater on the same party+path must defeat the prohibition"
1092        );
1093    }
1094
1095    fn alice() -> u64 {
1096        q_hash("did:web:alice.example")
1097    }
1098    fn bob() -> u64 {
1099        q_hash("did:web:bob.example")
1100    }
1101    fn nda() -> u64 {
1102        q_hash("did:web:nda:contract-001")
1103    }
1104    fn disclose_path() -> u64 {
1105        q_hash("q42:disclose")
1106    }
1107    fn conf_data() -> u64 {
1108        q_hash("q42:data:project-x:confidential")
1109    }
1110
1111    const NOW: u32 = 1_717_200_000; // ~2024-06-01 — well before NDA expiry
1112    const EXPIRY_NDA: u32 = 1_830_297_600; // 2028-01-01
1113
1114    fn nda_quins() -> [NQuin; 3] {
1115        [
1116            // Quin 0: Alice FORBID disclose (active)
1117            compile_norm_quin(
1118                alice(),
1119                OP_FORBID,
1120                disclose_path(),
1121                conf_data(),
1122                nda(),
1123                EXPIRY_NDA,
1124                false,
1125            ),
1126            // Quin 1: Bob FORBID disclose (active)
1127            compile_norm_quin(
1128                bob(),
1129                OP_FORBID,
1130                disclose_path(),
1131                conf_data(),
1132                nda(),
1133                EXPIRY_NDA,
1134                false,
1135            ),
1136            // Quin 2: q42:unless — Alice PERMIT disclose to auditors (defeater for Quin 0)
1137            compile_norm_quin(
1138                alice(),
1139                OP_PERMIT,
1140                disclose_path(),
1141                q_hash("q42:role:certified-auditor"),
1142                nda(),
1143                EXPIRY_NDA,
1144                true,
1145            ),
1146        ]
1147    }
1148
1149    #[test]
1150    fn nda_alice_is_defeated_bob_is_active() {
1151        let quins = nda_quins();
1152        let mut out = [DeonticVerdict {
1153            norm: NQuin::default(),
1154            status: DeonticStatus::Malformed,
1155            opcode: 0,
1156            defeat_kind: DefeatKind::None,
1157            _pad: [0u8; 5],
1158        }; 8];
1159
1160        let n = evaluate_deontic_contract(&quins, NOW, &mut out).unwrap();
1161
1162        // Two norm Quins (Alice + Bob); defeater is not a norm.
1163        assert_eq!(n, 2, "expected exactly two verdicts");
1164
1165        // Alice's prohibition should be defeated by Quin 2.
1166        let alice_verdict = out[..n].iter().find(|v| v.norm.subject == alice()).unwrap();
1167        assert_eq!(
1168            alice_verdict.status,
1169            DeonticStatus::Defeated,
1170            "Alice obligation should be defeated"
1171        );
1172        assert_eq!(alice_verdict.opcode, OP_FORBID);
1173
1174        // Bob has no defeater — should be active.
1175        let bob_verdict = out[..n].iter().find(|v| v.norm.subject == bob()).unwrap();
1176        assert_eq!(
1177            bob_verdict.status,
1178            DeonticStatus::Active,
1179            "Bob obligation should be active"
1180        );
1181    }
1182
1183    #[test]
1184    fn expired_norm_is_detected() {
1185        let past_expiry: u32 = 1_000_000; // Unix epoch far in the past
1186        let norm = compile_norm_quin(
1187            alice(),
1188            OP_OBLIGATE,
1189            disclose_path(),
1190            conf_data(),
1191            nda(),
1192            past_expiry,
1193            false,
1194        );
1195        let quins = [norm];
1196        let mut out = [DeonticVerdict {
1197            norm: NQuin::default(),
1198            status: DeonticStatus::Malformed,
1199            opcode: 0,
1200            defeat_kind: DefeatKind::None,
1201            _pad: [0u8; 5],
1202        }; 4];
1203
1204        let n = evaluate_deontic_contract(&quins, NOW, &mut out).unwrap();
1205        assert_eq!(n, 1);
1206        assert_eq!(out[0].status, DeonticStatus::Expired);
1207    }
1208
1209    #[test]
1210    fn no_expiry_zero_is_always_valid() {
1211        let norm = compile_norm_quin(
1212            alice(),
1213            OP_PERMIT,
1214            disclose_path(),
1215            conf_data(),
1216            nda(),
1217            0,
1218            false,
1219        );
1220        let quins = [norm];
1221        let mut out = [DeonticVerdict {
1222            norm: NQuin::default(),
1223            status: DeonticStatus::Malformed,
1224            opcode: 0,
1225            defeat_kind: DefeatKind::None,
1226            _pad: [0u8; 5],
1227        }; 4];
1228
1229        let n = evaluate_deontic_contract(&quins, u32::MAX, &mut out).unwrap();
1230        assert_eq!(n, 1);
1231        assert_eq!(
1232            out[0].status,
1233            DeonticStatus::Active,
1234            "zero expiry should never expire"
1235        );
1236    }
1237
1238    #[test]
1239    fn non_deontic_quins_are_skipped() {
1240        // Plain SHACL/data Quin with opcode 0x00 — should produce no verdicts.
1241        let plain = NQuin {
1242            subject: 1,
1243            predicate: 0x00,
1244            object: 2,
1245            context: 3,
1246            metadata: 0,
1247            parity: 0,
1248        };
1249        let mut out = [DeonticVerdict {
1250            norm: NQuin::default(),
1251            status: DeonticStatus::Malformed,
1252            opcode: 0,
1253            defeat_kind: DefeatKind::None,
1254            _pad: [0u8; 5],
1255        }; 4];
1256
1257        let n = evaluate_deontic_contract(&[plain], NOW, &mut out).unwrap();
1258        assert_eq!(n, 0, "non-deontic Quins must be silently skipped");
1259    }
1260
1261    #[test]
1262    fn output_buffer_full_returns_error() {
1263        let quins = nda_quins(); // 2 norm Quins
1264        let mut out = [DeonticVerdict {
1265            norm: NQuin::default(),
1266            status: DeonticStatus::Malformed,
1267            opcode: 0,
1268            defeat_kind: DefeatKind::None,
1269            _pad: [0u8; 5],
1270        }; 1]; // one slot — too small
1271
1272        assert_eq!(
1273            evaluate_deontic_contract(&quins, NOW, &mut out),
1274            Err(DeonticError::OutputBufferFull)
1275        );
1276    }
1277
1278    #[test]
1279    fn empty_slice_returns_zero_verdicts() {
1280        let mut out = [DeonticVerdict {
1281            norm: NQuin::default(),
1282            status: DeonticStatus::Malformed,
1283            opcode: 0,
1284            defeat_kind: DefeatKind::None,
1285            _pad: [0u8; 5],
1286        }; 4];
1287        let n = evaluate_deontic_contract(&[], NOW, &mut out).unwrap();
1288        assert_eq!(n, 0);
1289    }
1290
1291    #[test]
1292    fn guardianship_contract_temporal_expiry() {
1293        let guardian = q_hash("did:web:guardian.example");
1294        let ward = q_hash("did:web:ward.example");
1295        let contract = q_hash("did:web:guardianship:contract-002");
1296        let path = q_hash("q42:actInBestInterest");
1297        let majority_epoch: u32 = 1_893_456_000; // 2030-01-01
1298
1299        let obligation = compile_norm_quin(
1300            guardian,
1301            OP_OBLIGATE,
1302            path,
1303            ward,
1304            contract,
1305            majority_epoch,
1306            false,
1307        );
1308        let quins = [obligation];
1309
1310        let mut out = [DeonticVerdict {
1311            norm: NQuin::default(),
1312            status: DeonticStatus::Malformed,
1313            opcode: 0,
1314            defeat_kind: DefeatKind::None,
1315            _pad: [0u8; 5],
1316        }; 4];
1317
1318        // Before majority — obligation is active.
1319        let n = evaluate_deontic_contract(&quins, NOW, &mut out).unwrap();
1320        assert_eq!(n, 1);
1321        assert_eq!(out[0].status, DeonticStatus::Active);
1322
1323        // After majority — obligation has expired.
1324        let n = evaluate_deontic_contract(&quins, majority_epoch + 1, &mut out).unwrap();
1325        assert_eq!(n, 1);
1326        assert_eq!(out[0].status, DeonticStatus::Expired);
1327    }
1328
1329    #[test]
1330    fn opcode_constants_are_distinct_from_mini_parser_range() {
1331        // mini_parser uses 0x00–0x04; deontic opcodes must not collide.
1332        assert!(OP_OBLIGATE > 0x04);
1333        assert!(OP_PERMIT > 0x04);
1334        assert!(OP_FORBID > 0x04);
1335        assert_ne!(OP_OBLIGATE, OP_PERMIT);
1336        assert_ne!(OP_PERMIT, OP_FORBID);
1337        assert_ne!(OP_OBLIGATE, OP_FORBID);
1338    }
1339
1340    #[test]
1341    fn defeater_bit_is_msb() {
1342        assert_eq!(DEFEATER_BIT, 1u64 << 63);
1343    }
1344
1345    #[test]
1346    fn compile_norm_quin_parity_is_xor_fold() {
1347        let q = compile_norm_quin(
1348            alice(),
1349            OP_FORBID,
1350            disclose_path(),
1351            conf_data(),
1352            nda(),
1353            EXPIRY_NDA,
1354            false,
1355        );
1356        let expected = q.subject ^ q.predicate ^ q.object ^ q.context;
1357        assert_eq!(
1358            q.parity, expected,
1359            "parity must be XOR fold of semantic fields"
1360        );
1361    }
1362
1363    #[test]
1364    fn compile_n3_defeater_sets_defeater_bit() {
1365        use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType, Triple};
1366        let rule = Rule {
1367            id: None,
1368            rule_type: RuleType::Defeater,
1369            weight: None,
1370            premise: Formula {
1371                triples: vec![Triple {
1372                    subject: Term::Uri("did:web:alice.example".into()),
1373                    predicate: Term::Uri("q42:disclose".into()),
1374                    object: Term::Uri("q42:role:certified-auditor".into()),
1375                }],
1376            },
1377            conclusion: Formula {
1378                triples: vec![Triple {
1379                    subject: Term::Uri("did:web:alice.example".into()),
1380                    predicate: Term::Uri("q42:disclose".into()),
1381                    object: Term::Uri("true".into()),
1382                }],
1383            },
1384        };
1385        let q = compile_n3_rule_to_norm(
1386            &crate::modalities::logic::n3_compiler::compile_rule_to_zero_heap(&rule),
1387            nda(),
1388            EXPIRY_NDA,
1389        )
1390        .unwrap();
1391        assert_ne!(q.predicate & DEFEATER_BIT, 0);
1392    }
1393
1394    #[test]
1395    fn compile_n3_defeasible_permit_rule() {
1396        use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType, Triple};
1397        let rule = Rule {
1398            id: None,
1399            rule_type: RuleType::Defeasible,
1400            weight: None,
1401            premise: Formula {
1402                triples: vec![Triple {
1403                    subject: Term::Uri("did:web:bob.example".into()),
1404                    predicate: Term::Uri("q42:permitAccess".into()),
1405                    object: Term::Uri("q42:data:project-x".into()),
1406                }],
1407            },
1408            conclusion: Formula { triples: vec![] },
1409        };
1410        let q = compile_n3_rule_to_norm(
1411            &crate::modalities::logic::n3_compiler::compile_rule_to_zero_heap(&rule),
1412            nda(),
1413            0,
1414        )
1415        .unwrap();
1416        assert_eq!(extract_deontic_opcode(q.predicate), OP_PERMIT);
1417        assert_eq!(q.predicate & DEFEATER_BIT, 0);
1418    }
1419
1420    #[test]
1421    fn compile_n3_malformed_rule_returns_none() {
1422        use crate::modalities::logic::n3_parser::{Formula, Rule, RuleType};
1423        let rule = Rule {
1424            id: None,
1425            rule_type: RuleType::Strict,
1426            weight: None,
1427            premise: Formula { triples: vec![] },
1428            conclusion: Formula { triples: vec![] },
1429        };
1430        assert!(compile_n3_rule_to_norm(
1431            &crate::modalities::logic::n3_compiler::compile_rule_to_zero_heap(&rule),
1432            nda(),
1433            0
1434        )
1435        .is_none());
1436    }
1437
1438    // ─── Phase 1: SDL⁺ extensions (DEONTIC_LOGIC_PLAN §4) ───────────────────────
1439
1440    fn mkfact(s: u64, p: u64, o: u64) -> NQuin {
1441        let mut q = NQuin {
1442            subject: s,
1443            predicate: p,
1444            object: o,
1445            context: 0,
1446            metadata: 0,
1447            parity: 0,
1448        };
1449        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
1450        q
1451    }
1452
1453    #[test]
1454    fn lifecycle_pending_active_discharged_violated() {
1455        let party = alice();
1456        let action = conf_data();
1457        let duty = compile_norm_quin(party, OP_OBLIGATE, disclose_path(), action, nda(), 0, false);
1458
1459        // effective_from in the future → Pending.
1460        assert_eq!(
1461            norm_lifecycle_status(&duty, NOW, NOW + 1000, &[], &[]),
1462            DeonticStatus::Pending
1463        );
1464        // in force, no facts → Active.
1465        assert_eq!(
1466            norm_lifecycle_status(&duty, NOW, 0, &[], &[]),
1467            DeonticStatus::Active
1468        );
1469        // fulfilled fact → Discharged.
1470        let fulfilled = [mkfact(party, q_hash("q42:fulfilled"), action)];
1471        assert_eq!(
1472            norm_lifecycle_status(&duty, NOW, 0, &[], &fulfilled),
1473            DeonticStatus::Discharged
1474        );
1475        // breached fact → Violated.
1476        let breached = [mkfact(party, q_hash("q42:breached"), action)];
1477        assert_eq!(
1478            norm_lifecycle_status(&duty, NOW, 0, &[], &breached),
1479            DeonticStatus::Violated
1480        );
1481    }
1482
1483    #[test]
1484    fn lifecycle_forbid_violated_by_performance() {
1485        let party = bob();
1486        let action = conf_data();
1487        let prohibition =
1488            compile_norm_quin(party, OP_FORBID, disclose_path(), action, nda(), 0, false);
1489        let performed = [mkfact(party, q_hash("q42:performed"), action)];
1490        assert_eq!(
1491            norm_lifecycle_status(&prohibition, NOW, 0, &[], &performed),
1492            DeonticStatus::Violated
1493        );
1494        assert_eq!(
1495            norm_lifecycle_status(&prohibition, NOW, 0, &[], &[]),
1496            DeonticStatus::Active
1497        );
1498    }
1499
1500    #[test]
1501    fn lifecycle_expiry_and_defeater_precedence() {
1502        let party = alice();
1503        let action = conf_data();
1504        let duty = compile_norm_quin(
1505            party,
1506            OP_OBLIGATE,
1507            disclose_path(),
1508            action,
1509            nda(),
1510            EXPIRY_NDA,
1511            false,
1512        );
1513        let fulfilled = [mkfact(party, q_hash("q42:fulfilled"), action)];
1514        // past expiry → Expired (temporal precedes facts).
1515        assert_eq!(
1516            norm_lifecycle_status(&duty, EXPIRY_NDA + 1, 0, &[], &fulfilled),
1517            DeonticStatus::Expired
1518        );
1519        // matching defeater → Defeated (precedes facts).
1520        let df = defeater_fingerprint(&duty);
1521        assert_eq!(
1522            norm_lifecycle_status(&duty, NOW, 0, &[df], &fulfilled),
1523            DeonticStatus::Defeated
1524        );
1525    }
1526
1527    #[test]
1528    fn optionality_and_gratuitousness() {
1529        let party = alice();
1530        let action = q_hash("q42:donate");
1531        // no norms → optional and gratuitous.
1532        assert!(is_optional(&[], party, action));
1533        assert!(is_gratuitous(&[], party, action));
1534        // obligation → neither.
1535        let oblig = compile_norm_quin(party, OP_OBLIGATE, disclose_path(), action, nda(), 0, false);
1536        assert!(!is_optional(&[oblig], party, action));
1537        assert!(!is_gratuitous(&[oblig], party, action));
1538        // permission alone → optional and gratuitous.
1539        let perm = compile_norm_quin(party, OP_PERMIT, disclose_path(), action, nda(), 0, false);
1540        assert!(is_optional(&[perm], party, action));
1541        assert!(is_gratuitous(&[perm], party, action));
1542        // prohibition → gratuitous (not obliged) but NOT optional (forbidden).
1543        let forbid = compile_norm_quin(party, OP_FORBID, disclose_path(), action, nda(), 0, false);
1544        assert!(!is_optional(&[forbid], party, action));
1545        assert!(is_gratuitous(&[forbid], party, action));
1546    }
1547
1548    #[test]
1549    fn undercutting_vs_rebutting_defeater_kind() {
1550        let party = alice();
1551        let action = conf_data();
1552        let duty = compile_norm_quin(party, OP_OBLIGATE, disclose_path(), action, nda(), 0, false);
1553        let mut out = [DeonticVerdict::default(); 4];
1554
1555        // Rebutting: DEFEATER_BIT + a contrary opcode (PERMIT) on the same path.
1556        let rebut = compile_norm_quin(
1557            party,
1558            OP_PERMIT,
1559            disclose_path(),
1560            q_hash("q42:exc"),
1561            nda(),
1562            0,
1563            true,
1564        );
1565        let n = evaluate_deontic_contract(&[duty, rebut], NOW, &mut out).unwrap();
1566        assert_eq!(n, 1);
1567        assert_eq!(out[0].status, DeonticStatus::Defeated);
1568        assert_eq!(out[0].defeat_kind, DefeatKind::Rebutting);
1569
1570        // Undercutting: DEFEATER_BIT + OP_UNDERCUT on the same path → link-invalidation.
1571        let undercut = compile_norm_quin(
1572            party,
1573            OP_UNDERCUT,
1574            disclose_path(),
1575            q_hash("q42:exc"),
1576            nda(),
1577            0,
1578            true,
1579        );
1580        let n = evaluate_deontic_contract(&[duty, undercut], NOW, &mut out).unwrap();
1581        assert_eq!(n, 1);
1582        assert_eq!(out[0].status, DeonticStatus::Defeated);
1583        assert_eq!(out[0].defeat_kind, DefeatKind::Undercutting);
1584    }
1585
1586    #[test]
1587    fn dyadic_conditional_obligation() {
1588        let party = alice();
1589        let condition = q_hash("q42:dataCollected");
1590        let obligation = q_hash("q42:obtainConsent");
1591        let cond_pred = q_hash("q42:holds");
1592        // condition absent → vacuously satisfied.
1593        assert!(evaluate_conditional_obligation(
1594            &[],
1595            party,
1596            cond_pred,
1597            condition,
1598            obligation
1599        ));
1600        // condition present, unfulfilled → not satisfied.
1601        let triggered = [mkfact(party, cond_pred, condition)];
1602        assert!(!evaluate_conditional_obligation(
1603            &triggered, party, cond_pred, condition, obligation
1604        ));
1605        // condition present, fulfilled → satisfied.
1606        let done = [
1607            mkfact(party, cond_pred, condition),
1608            mkfact(party, q_hash("q42:fulfilled"), obligation),
1609        ];
1610        assert!(evaluate_conditional_obligation(
1611            &done, party, cond_pred, condition, obligation
1612        ));
1613    }
1614}