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}