Skip to main content

qualia_core_db/modalities/
deontic_compose.rs

1//! Deontic compositions (Phase 4, DEONTIC_LOGIC_PLAN §4) — cluster A (zero-heap).
2//!
3//! Phase 0 proved the modality engines real but **uncomposed**. This module wires the
4//! deontic verdict together with the temporal, epistemic, linear and DL/spatial engines —
5//! the joins that turn standalone logics into legal reasoning:
6//!
7//! * **deontic × temporal** — `O(Gφ)` "must hold throughout", `O(φ U ψ)` "must hold until",
8//!   via `temporal_ltl::evaluate_ltl_trace`.
9//! * **deontic × epistemic** — *mens rea*: classify a violation as knowing vs ignorant
10//!   (and ignorance-is-no-excuse when there was a duty to know), via the epistemic encoding.
11//! * **deontic × linear** — an obligation discharged by fulfilment *consumes* the duty
12//!   (`linear::consume_quin`): a resource spent once, not reusable.
13//! * **deontic × spatial** — an obligation in force in a jurisdiction applies in every
14//!   sub-jurisdiction `jur:within` it, via `dl::check_subsumption_quin`.
15//!
16//! All zero-heap (slice in / scalar or slice out). The heavier reasoning joins
17//! (argumentation, probabilistic/fuzzy, ASP/abductive) land in cluster B.
18
19use crate::modalities::dl::check_subsumption_quin;
20use crate::modalities::epistemic::OP_KNOWS;
21use crate::modalities::linear::consume_quin;
22use crate::modalities::logic::deontic::{
23    extract_deontic_opcode, DeonticStatus, OP_FORBID, OP_OBLIGATE,
24};
25use crate::modalities::stit::brought_about;
26use crate::modalities::temporal_ltl::{evaluate_ltl_trace, LtlFormula};
27use crate::NQuin;
28
29// ─── deontic × temporal ─────────────────────────────────────────────────────────
30
31/// `O(Gφ)` — the obligation that property `prop` holds **globally** across a state trace.
32/// Discharged iff `prop` holds in every state; otherwise Violated. (Continuous protections:
33/// "no one shall be subjected to torture" must hold in every state, not just eventually.)
34pub fn obligation_globally(prop: u64, trace: &[NQuin]) -> DeonticStatus {
35    if evaluate_ltl_trace(trace, &LtlFormula::Globally(prop)) {
36        DeonticStatus::Discharged
37    } else {
38        DeonticStatus::Violated
39    }
40}
41
42/// `O(φ U ψ)` — the obligation that `ante` holds **until** `consequent` becomes true
43/// (provisional measures: "detention standards apply until release"). Discharged iff the
44/// until-formula holds over the trace; otherwise Violated.
45pub fn obligation_until(ante: u64, consequent: u64, trace: &[NQuin]) -> DeonticStatus {
46    if evaluate_ltl_trace(trace, &LtlFormula::Until { ante, consequent }) {
47        DeonticStatus::Discharged
48    } else {
49        DeonticStatus::Violated
50    }
51}
52
53// ─── deontic × epistemic (mens rea) ───────────────────────────────────────────────
54
55/// The mental state accompanying a deontic violation — the *mens rea* axis legal
56/// instruments use to grade culpability.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MensRea {
59    /// No violation occurred.
60    NoViolation,
61    /// The agent violated the norm **knowing** it applied — full culpability.
62    Knowing,
63    /// The agent violated without knowing the norm existed, and had **no duty to know**.
64    Ignorant,
65    /// Violated in ignorance, but a duty to know was in force — *ignorantia juris non
66    /// excusat*: ignorance is no excuse.
67    InexcusableIgnorance,
68}
69
70/// Did `agent` (per the epistemic frame) KNOW `claim`? An epistemic quin with
71/// `predicate[0..7] == OP_KNOWS`, `subject == agent`, `object == claim`.
72pub fn agent_knows(epistemic: &[NQuin], agent: u64, claim: u64) -> bool {
73    epistemic
74        .iter()
75        .any(|q| q.subject == agent && (q.predicate & 0xFF) as u8 == OP_KNOWS && q.object == claim)
76}
77
78/// Classify the *mens rea* of a possible violation of `norm` by its bearer:
79/// `F[α stit φ]` is violated when α brought φ about; `O[α stit φ]` when α did not (omission).
80/// A violation is `Knowing` if α knew the forbidden/obligatory content, else `Ignorant` —
81/// upgraded to `InexcusableIgnorance` when `had_duty_to_know`.
82pub fn classify_mens_rea(
83    norm: &NQuin,
84    facts: &[NQuin],
85    epistemic: &[NQuin],
86    had_duty_to_know: bool,
87) -> MensRea {
88    let agent = norm.subject;
89    let content = norm.object;
90    let violated = match extract_deontic_opcode(norm.predicate) {
91        OP_FORBID => brought_about(facts, agent, content),
92        OP_OBLIGATE => !brought_about(facts, agent, content),
93        _ => false,
94    };
95    if !violated {
96        return MensRea::NoViolation;
97    }
98    if agent_knows(epistemic, agent, content) {
99        MensRea::Knowing
100    } else if had_duty_to_know {
101        MensRea::InexcusableIgnorance
102    } else {
103        MensRea::Ignorant
104    }
105}
106
107// ─── deontic × linear (discharge consumes the duty) ───────────────────────────────
108
109/// Discharge an obligation by fulfilment: if the bearer brought the obligation's content
110/// about, the duty is `Discharged` **and consumed** (`linear::consume_quin` — a duty paid
111/// is spent once, not reusable). Returns the status; mutates `norm` to mark consumption on
112/// discharge. A non-obligation, or an unfulfilled one, is left unconsumed.
113pub fn discharge_obligation(norm: &mut NQuin, facts: &[NQuin]) -> DeonticStatus {
114    if extract_deontic_opcode(norm.predicate) != OP_OBLIGATE {
115        return DeonticStatus::Active;
116    }
117    if brought_about(facts, norm.subject, norm.object) {
118        consume_quin(norm);
119        DeonticStatus::Discharged
120    } else {
121        DeonticStatus::Active
122    }
123}
124
125// ─── deontic × spatial (jurisdictional subsumption) ───────────────────────────────
126
127/// Locative obligation subsumption: an obligation in force in `norm_jurisdiction` applies
128/// in `target_jurisdiction` iff the target is `jur:within` the norm's jurisdiction
129/// (transitively). `within` holds `jur:within` Quins (`subject within object`); the check
130/// reuses the DL transitive-closure search. (RCC-8 region *geometry* is not encodable in a
131/// 48-byte NQuin — we use the jurisdiction hierarchy, per the plan §1.)
132pub fn obligation_applies_in(
133    norm_jurisdiction: u64,
134    target_jurisdiction: u64,
135    within: &[NQuin],
136) -> bool {
137    // target within norm_jurisdiction  ⟺  subsumption(target, norm_jurisdiction) over `within`.
138    check_subsumption_quin(target_jurisdiction, norm_jurisdiction, within)
139}
140
141// ════════════════════════════════════════════════════════════════════════════════
142// Cluster B — reasoning-engine joins (all zero-heap; bounded fixed-array backends)
143// ════════════════════════════════════════════════════════════════════════════════
144
145// ─── deontic × argumentation (conflict → grounded extension → verdict) ────────────
146
147/// Resolve a normative conflict by Dung's grounded semantics: given the competing norm IDs
148/// and the `attacks` pairs `(attacker, target)`, does `goal` SURVIVE (belong to the grounded
149/// extension)? The survivor is the objectively defensible verdict after all attacks and
150/// defences resolve — e.g. a general duty reinstated when an emergency override defeats its
151/// exception. Composes `argumentation::grounded_contains` (bounded, zero-heap).
152pub fn norm_survives_conflict(norm_ids: &[u64], attacks: &[(u64, u64)], goal: u64) -> bool {
153    crate::modalities::argumentation::grounded_contains(norm_ids, attacks, goal)
154}
155
156// ─── deontic × fuzzy / probabilistic (partial fulfilment, trust) ──────────────────
157
158/// Degree to which a *progressively-realised* obligation is fulfilled: the Gödel t-norm
159/// (min) of its sub-requirements' truth degrees — the weakest link gates the whole (the
160/// ICESCR "progressive realization" reading). Each requirement carries its degree in
161/// `metadata`. Composes `fuzzy::conjunction`.
162pub fn fulfilment_degree(requirements: &[NQuin]) -> f32 {
163    crate::modalities::fuzzy::conjunction(requirements)
164}
165
166/// Is a progressively-realised obligation met to at least `threshold` ∈ [0,1]?
167pub fn obligation_fuzzily_met(requirements: &[NQuin], threshold: f32) -> bool {
168    fulfilment_degree(requirements) >= threshold
169}
170
171/// Behavioural-trust gate: a permission/capability activates only when the holder's derived
172/// trust `weight` exceeds `threshold` τ. Composes `probabilistic::evaluate_threshold`.
173pub fn trust_gate(weight: f32, threshold: f32) -> bool {
174    crate::modalities::probabilistic::evaluate_threshold(weight, threshold)
175}
176
177// ─── deontic × ASP / abductive (multi-remedy scenarios, breach diagnosis) ─────────
178
179/// Enumerate the valid compliance scenarios when an instrument under-determines the remedy
180/// ("the State shall provide remedy X, Y, or Z"): the stable models (answer sets) of the
181/// remedy `rules` over `atoms`, written to `out` as bitmasks over atom indices. Composes
182/// `asp::compute_answer_sets`.
183pub fn remedy_scenarios(
184    atoms: &[u64],
185    rules: &[crate::modalities::asp::AspRule],
186    out: &mut [u64],
187) -> usize {
188    crate::modalities::asp::compute_answer_sets(atoms, rules, out)
189}
190
191/// Abductive breach diagnosis: walk backward from an observed `violation` along the
192/// explanatory `rules` (predicate == `explains`) to the root cause — the missing duty or bad
193/// act that accounts for it. Composes `abductive::abductive_explanation`.
194pub fn diagnose_breach(rules: &[NQuin], violation: u64, explains: u64) -> Option<u64> {
195    crate::modalities::abductive::abductive_explanation(rules, violation, explains)
196}
197
198// ─── deontic × verification / zk / remedy ─────────────────────────────────────────
199
200/// **Formal verification of a composed norm:** a temporal constraint must not *void a
201/// non-derogable right*. A composition that places a temporal limit (expiry / window) on a
202/// `non_derogable` obligation is INVALID — non-derogable protections do not expire. Any other
203/// composition is valid. Returns `true` iff the composition preserves the right.
204pub fn composition_preserves_right(non_derogable: bool, has_temporal_limit: bool) -> bool {
205    !(non_derogable && has_temporal_limit)
206}
207
208/// **Zero-knowledge–wrapped composition:** a complex composition's verdict is applied only if a
209/// zk proof of its premises verifies (the private witnesses — the underlying facts — stay hidden).
210/// Returns the `verdict` gated on `proof_verified`; an unverified proof yields `None` (the
211/// composition is withheld). Mirrors `legal_compose::zk_eligibility`.
212pub fn zk_wrapped_composition(
213    proof_verified: bool,
214    verdict: DeonticStatus,
215) -> Option<DeonticStatus> {
216    if proof_verified {
217        Some(verdict)
218    } else {
219        None
220    }
221}
222
223/// **Automated remedy generation:** when a composed norm is breached, generate the secondary
224/// obligation `O(reparation)` on the breaching `party` (the contrary-to-duty remedy). Composes
225/// `deontic::compile_norm_quin`; the caller records/enforces it like any obligation.
226pub fn generate_remedy_obligation(
227    party: u64,
228    reparation_path: u64,
229    reparation_action: u64,
230    frame: u64,
231) -> NQuin {
232    crate::modalities::logic::deontic::compile_norm_quin(
233        party,
234        OP_OBLIGATE,
235        reparation_path,
236        reparation_action,
237        frame,
238        0,
239        false,
240    )
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::modalities::linear::is_consumed;
247    use crate::modalities::logic::deontic::compile_norm_quin;
248    use crate::q_hash;
249
250    fn state(pred: u64) -> NQuin {
251        let mut q = NQuin {
252            subject: 0,
253            predicate: pred,
254            object: 0,
255            context: 0,
256            metadata: 0,
257            parity: 0,
258        };
259        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
260        q
261    }
262    fn fact(s: u64, p: u64, o: u64) -> NQuin {
263        let mut q = NQuin {
264            subject: s,
265            predicate: p,
266            object: o,
267            context: 0,
268            metadata: 0,
269            parity: 0,
270        };
271        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
272        q
273    }
274
275    #[test]
276    fn temporal_globally_obligation() {
277        let no_torture = q_hash("q42:freeFromTorture");
278        // A trace where the protection holds in every state → Discharged.
279        let good = [state(no_torture), state(no_torture), state(no_torture)];
280        assert_eq!(
281            obligation_globally(no_torture, &good),
282            DeonticStatus::Discharged
283        );
284        // A trace with one breaching state → Violated.
285        let bad = [
286            state(no_torture),
287            state(q_hash("q42:torture")),
288            state(no_torture),
289        ];
290        assert_eq!(
291            obligation_globally(no_torture, &bad),
292            DeonticStatus::Violated
293        );
294    }
295
296    #[test]
297    fn temporal_until_obligation() {
298        let standards = q_hash("q42:detentionStandards");
299        let release = q_hash("q42:release");
300        // standards hold until release → Discharged.
301        let trace = [state(standards), state(standards), state(release)];
302        assert_eq!(
303            obligation_until(standards, release, &trace),
304            DeonticStatus::Discharged
305        );
306        // standards lapse before release → Violated.
307        let bad = [
308            state(standards),
309            state(q_hash("q42:neglect")),
310            state(release),
311        ];
312        assert_eq!(
313            obligation_until(standards, release, &bad),
314            DeonticStatus::Violated
315        );
316    }
317
318    #[test]
319    fn mens_rea_knowing_vs_ignorant() {
320        let agent = q_hash("did:agent");
321        let forbidden = q_hash("q42:launderMoney");
322        let norm = compile_norm_quin(
323            agent,
324            OP_FORBID,
325            q_hash("q42:noLaunder"),
326            forbidden,
327            q_hash("frame"),
328            0,
329            false,
330        );
331        let did_it = [fact(agent, q_hash("q42:broughtAbout"), forbidden)];
332
333        // No violation if not done.
334        assert_eq!(
335            classify_mens_rea(&norm, &[], &[], false),
336            MensRea::NoViolation
337        );
338        // Did it, knew it was forbidden → Knowing.
339        let knows = [fact(agent, OP_KNOWS as u64, forbidden)];
340        assert_eq!(
341            classify_mens_rea(&norm, &did_it, &knows, false),
342            MensRea::Knowing
343        );
344        // Did it, didn't know, no duty to know → Ignorant.
345        assert_eq!(
346            classify_mens_rea(&norm, &did_it, &[], false),
347            MensRea::Ignorant
348        );
349        // Did it, didn't know, BUT had a duty to know → ignorance is no excuse.
350        assert_eq!(
351            classify_mens_rea(&norm, &did_it, &[], true),
352            MensRea::InexcusableIgnorance
353        );
354    }
355
356    #[test]
357    fn discharge_consumes_the_duty() {
358        let debtor = q_hash("did:debtor");
359        let payment = q_hash("q42:payDebt");
360        let mut norm = compile_norm_quin(
361            debtor,
362            OP_OBLIGATE,
363            q_hash("q42:debtDuty"),
364            payment,
365            q_hash("loan"),
366            0,
367            false,
368        );
369        assert!(!is_consumed(&norm));
370        // Unpaid → Active, not consumed.
371        assert_eq!(discharge_obligation(&mut norm, &[]), DeonticStatus::Active);
372        assert!(!is_consumed(&norm));
373        // Paid → Discharged AND consumed (a duty paid is spent once).
374        let paid = [fact(debtor, q_hash("q42:broughtAbout"), payment)];
375        assert_eq!(
376            discharge_obligation(&mut norm, &paid),
377            DeonticStatus::Discharged
378        );
379        assert!(is_consumed(&norm));
380    }
381
382    #[test]
383    fn jurisdictional_subsumption() {
384        let au = q_hash("jur:Commonwealth-of-Australia");
385        let vic = q_hash("jur:Victoria");
386        let melbourne = q_hash("jur:Melbourne");
387        let nz = q_hash("jur:New-Zealand");
388        let within = q_hash("https://ns.webcivics.net/jurisdiction/within");
389        let e = |s: u64, o: u64| {
390            let mut q = NQuin {
391                subject: s,
392                predicate: within,
393                object: o,
394                context: 0,
395                metadata: 0,
396                parity: 0,
397            };
398            q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
399            q
400        };
401        let graph = [e(vic, au), e(melbourne, vic)]; // Melbourne within VIC within AU
402                                                     // An ICCPR obligation in force for AU applies in VIC and (transitively) Melbourne.
403        assert!(obligation_applies_in(au, vic, &graph));
404        assert!(obligation_applies_in(au, melbourne, &graph));
405        // It does not reach a different State.
406        assert!(!obligation_applies_in(au, nz, &graph));
407    }
408
409    // ─── Cluster B ──────────────────────────────────────────────────────────────
410
411    #[test]
412    fn argumentation_resolves_norm_conflict() {
413        // General duty A is attacked by exception E; E is attacked by override O (unattacked).
414        // Grounded: O survives → defeats E → A is reinstated.
415        let a = q_hash("norm:dutyToStop");
416        let e = q_hash("norm:exceptionPolice");
417        let o = q_hash("norm:overrideEmergency");
418        let ids = [a, e, o];
419        let attacks = [(e, a), (o, e)];
420        assert!(
421            norm_survives_conflict(&ids, &attacks, a),
422            "A reinstated by O defeating E"
423        );
424        assert!(
425            norm_survives_conflict(&ids, &attacks, o),
426            "O is unattacked → survives"
427        );
428        assert!(
429            !norm_survives_conflict(&ids, &attacks, e),
430            "E is defeated by O"
431        );
432    }
433
434    fn deg(d: f32) -> NQuin {
435        let mut q = NQuin::default();
436        q.metadata = d.to_bits() as u64; // truth degree in metadata (fuzzy::degree)
437        q
438    }
439
440    #[test]
441    fn fuzzy_partial_fulfilment_and_trust() {
442        // Progressive realization: the weakest sub-requirement gates the whole.
443        let reqs = [deg(0.9), deg(0.6), deg(0.8)];
444        assert!((fulfilment_degree(&reqs) - 0.6).abs() < 1e-6);
445        assert!(obligation_fuzzily_met(&reqs, 0.5));
446        assert!(!obligation_fuzzily_met(&reqs, 0.7));
447        // Behavioural-trust gate.
448        assert!(trust_gate(0.85, 0.7));
449        assert!(!trust_gate(0.5, 0.7));
450    }
451
452    #[test]
453    fn asp_multi_remedy_scenarios() {
454        use crate::modalities::asp::AspRule;
455        // Under-determined remedy: x :- not y ; y :- not x → two stable models {x}, {y}.
456        let x = q_hash("remedy:compensation");
457        let y = q_hash("remedy:restitution");
458        let atoms = [x, y];
459        let rules = [AspRule::new(x, &[], &[y]), AspRule::new(y, &[], &[x])];
460        let mut out = [0u64; 8];
461        let n = remedy_scenarios(&atoms, &rules, &mut out);
462        assert_eq!(n, 2, "two valid remedy scenarios");
463    }
464
465    #[test]
466    fn abductive_breach_diagnosis() {
467        // missing-funding → no-staff → service-failure (the observed breach). Root = missing-funding.
468        let explains = q_hash("q42:explains");
469        let edge = |h: u64, eff: u64| {
470            let mut q = NQuin {
471                subject: h,
472                predicate: explains,
473                object: eff,
474                context: 0,
475                metadata: 0,
476                parity: 0,
477            };
478            q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
479            q
480        };
481        let breach = q_hash("breach:serviceFailure");
482        let nostaff = q_hash("cause:noStaff");
483        let nofunding = q_hash("cause:missingFunding");
484        let rules = [edge(nofunding, nostaff), edge(nostaff, breach)];
485        assert_eq!(
486            diagnose_breach(&rules, breach, explains),
487            Some(nofunding),
488            "root cause surfaced"
489        );
490    }
491
492    #[test]
493    fn composed_norm_verification_protects_non_derogable_rights() {
494        // A temporal limit on a non-derogable right is INVALID (the right cannot expire).
495        assert!(!composition_preserves_right(true, true));
496        // A temporal limit on a derogable obligation is fine.
497        assert!(composition_preserves_right(false, true));
498        // A non-derogable right with NO temporal limit is fine.
499        assert!(composition_preserves_right(true, false));
500    }
501
502    #[test]
503    fn zk_wrapping_withholds_unproven_compositions() {
504        assert_eq!(
505            zk_wrapped_composition(true, DeonticStatus::Discharged),
506            Some(DeonticStatus::Discharged)
507        );
508        assert_eq!(zk_wrapped_composition(false, DeonticStatus::Violated), None);
509    }
510
511    #[test]
512    fn remedy_generation_emits_a_secondary_obligation() {
513        let party = q_hash("did:breacher");
514        let reparation = q_hash("q42:compensate");
515        let frame = q_hash("frame:remedy");
516        let remedy = generate_remedy_obligation(party, q_hash("q42:remedyDuty"), reparation, frame);
517        assert_eq!(remedy.subject, party);
518        assert_eq!(remedy.object, reparation);
519        assert_eq!(
520            extract_deontic_opcode(remedy.predicate),
521            OP_OBLIGATE,
522            "the remedy is an obligation"
523        );
524    }
525}