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}