Skip to main content

qualia_core_db/modalities/
jural.rs

1//! Hohfeldian jural relations (Phase 2, DEONTIC_LOGIC_PLAN §6).
2//!
3//! Wesley Newcomb Hohfeld decomposed the ambiguous word "right" into eight strict
4//! fundamental positions arranged as **correlatives** and **opposites**. This is the
5//! computational bridge for legal ontologies: a vague "right" becomes a precise position
6//! held by one agent *toward another*, with a **necessary correlative** the counterparty
7//! must hold. A Claim with no correlative Duty-bearer is therefore a *legible structural
8//! gap*, not silence ([`find_unmet_correlatives`]).
9//!
10//! First-order (rules of conduct):  Claim↔Duty,    Privilege↔No-Right
11//! Second-order (rules of control): Power↔Liability, Immunity↔Disability
12//! Opposites:  Claim/No-Right · Duty/Privilege · Power/Disability · Immunity/Liability
13//!
14//! ## NQuin encoding
15//! A jural relation "`holder` holds `position` over `content` toward `counterparty` in
16//! `frame`" packs as: `subject = holder`, `object = counterparty`, `context = frame`,
17//! `predicate = (content << 8) | position` (low byte = position opcode, bits [8..62] =
18//! content path — same convention as `deontic.rs`). Zero-heap throughout.
19
20use crate::agent::A_NATURAL_PERSON;
21use crate::NQuin;
22
23// ─── The eight positions (opcode block 0x30–0x37; distinct from deontic 0x10–0x1F
24//     and epistemic 0x20–0x26) ──────────────────────────────────────────────────
25pub const JURAL_CLAIM: u8 = 0x30;
26pub const JURAL_DUTY: u8 = 0x31;
27pub const JURAL_PRIVILEGE: u8 = 0x32;
28pub const JURAL_NO_RIGHT: u8 = 0x33;
29pub const JURAL_POWER: u8 = 0x34;
30pub const JURAL_LIABILITY: u8 = 0x35;
31pub const JURAL_IMMUNITY: u8 = 0x36;
32pub const JURAL_DISABILITY: u8 = 0x37;
33
34/// Content-path mask: bits [8..62] of the predicate (opcode byte + bit 63 excluded).
35const CONTENT_MASK: u64 = 0x7FFF_FFFF_FFFF_FF00;
36
37/// The **correlative** position the counterparty necessarily holds. If A holds `pos`
38/// toward B over content φ, then B holds `correlative(pos)` toward A over φ.
39pub const fn correlative(pos: u8) -> u8 {
40    match pos {
41        JURAL_CLAIM => JURAL_DUTY,
42        JURAL_DUTY => JURAL_CLAIM,
43        JURAL_PRIVILEGE => JURAL_NO_RIGHT,
44        JURAL_NO_RIGHT => JURAL_PRIVILEGE,
45        JURAL_POWER => JURAL_LIABILITY,
46        JURAL_LIABILITY => JURAL_POWER,
47        JURAL_IMMUNITY => JURAL_DISABILITY,
48        JURAL_DISABILITY => JURAL_IMMUNITY,
49        other => other,
50    }
51}
52
53/// The **jural opposite** (the position whose presence negates this one for the holder).
54pub const fn jural_opposite(pos: u8) -> u8 {
55    match pos {
56        JURAL_CLAIM => JURAL_NO_RIGHT,
57        JURAL_NO_RIGHT => JURAL_CLAIM,
58        JURAL_DUTY => JURAL_PRIVILEGE,
59        JURAL_PRIVILEGE => JURAL_DUTY,
60        JURAL_POWER => JURAL_DISABILITY,
61        JURAL_DISABILITY => JURAL_POWER,
62        JURAL_IMMUNITY => JURAL_LIABILITY,
63        JURAL_LIABILITY => JURAL_IMMUNITY,
64        other => other,
65    }
66}
67
68/// Is `pos` one of the eight jural positions?
69#[inline]
70pub const fn is_jural_position(pos: u8) -> bool {
71    matches!(
72        pos,
73        JURAL_CLAIM
74            | JURAL_DUTY
75            | JURAL_PRIVILEGE
76            | JURAL_NO_RIGHT
77            | JURAL_POWER
78            | JURAL_LIABILITY
79            | JURAL_IMMUNITY
80            | JURAL_DISABILITY
81    )
82}
83
84/// First-order positions are rules of *conduct*; second-order are rules of *control*.
85#[inline]
86pub const fn is_first_order(pos: u8) -> bool {
87    matches!(
88        pos,
89        JURAL_CLAIM | JURAL_DUTY | JURAL_PRIVILEGE | JURAL_NO_RIGHT
90    )
91}
92
93/// Readable name for a jural position.
94pub const fn position_name(pos: u8) -> Option<&'static str> {
95    Some(match pos {
96        JURAL_CLAIM => "Claim",
97        JURAL_DUTY => "Duty",
98        JURAL_PRIVILEGE => "Privilege",
99        JURAL_NO_RIGHT => "No-Right",
100        JURAL_POWER => "Power",
101        JURAL_LIABILITY => "Liability",
102        JURAL_IMMUNITY => "Immunity",
103        JURAL_DISABILITY => "Disability",
104        _ => return None,
105    })
106}
107
108#[inline]
109pub const fn jural_position(predicate: u64) -> u8 {
110    (predicate & 0xFF) as u8
111}
112
113#[inline]
114pub const fn jural_content(predicate: u64) -> u64 {
115    predicate & CONTENT_MASK
116}
117
118/// Build a jural-relation Quin: `holder` holds `position` over `content` toward
119/// `counterparty` within `frame`.
120pub fn compile_jural_quin(
121    holder: u64,
122    position: u8,
123    content_path: u64,
124    counterparty: u64,
125    frame: u64,
126) -> NQuin {
127    let predicate = ((content_path << 8) & CONTENT_MASK) | (position as u64);
128    let parity = holder ^ predicate ^ counterparty ^ frame;
129    NQuin {
130        subject: holder,
131        predicate,
132        object: counterparty,
133        context: frame,
134        metadata: 0,
135        parity,
136    }
137}
138
139/// The relation the counterparty NECESSARILY holds: swap holder/counterparty and map the
140/// position to its correlative, keeping the same content and frame.
141pub fn correlative_quin(rel: &NQuin) -> NQuin {
142    let pos = jural_position(rel.predicate);
143    let predicate = jural_content(rel.predicate) | (correlative(pos) as u64);
144    let holder = rel.object; // counterparty becomes holder of the correlative
145    let counterparty = rel.subject;
146    let frame = rel.context;
147    let parity = holder ^ predicate ^ counterparty ^ frame;
148    NQuin {
149        subject: holder,
150        predicate,
151        object: counterparty,
152        context: frame,
153        metadata: 0,
154        parity,
155    }
156}
157
158/// Does `graph` already contain the necessary correlative of `rel`?
159pub fn jural_correlativity_holds(rel: &NQuin, graph: &[NQuin]) -> bool {
160    let expected = correlative_quin(rel);
161    graph.iter().any(|q| {
162        q.subject == expected.subject
163            && q.object == expected.object
164            && q.predicate == expected.predicate
165            && q.context == expected.context
166    })
167}
168
169/// "Make the absence legible." For every jural relation whose correlative is NOT present
170/// in `rels`, emit the *expected* (missing) correlative into `out` — e.g. a Claim to a
171/// resource with no funded Duty-bearer surfaces the duty that ought to exist. Returns the
172/// count written. Zero-heap (caller-supplied `out`).
173pub fn find_unmet_correlatives(rels: &[NQuin], out: &mut [NQuin]) -> usize {
174    let mut n = 0usize;
175    for rel in rels {
176        if !is_jural_position(jural_position(rel.predicate)) {
177            continue;
178        }
179        if !jural_correlativity_holds(rel, rels) {
180            if n >= out.len() {
181                break;
182            }
183            out[n] = correlative_quin(rel);
184            n += 1;
185        }
186    }
187    n
188}
189
190/// Personhood **category-error** guard (composes `dl::check_subsumption_quin`): a benefit
191/// position (Claim / Privilege / Immunity) over a content that is exclusive to natural
192/// persons (e.g. `values:inherentDignity`) is a category error when the holder is **not**
193/// subsumed by `values:NaturalPerson` — i.e. a corporate/legal/artificial person asserting
194/// a human-only right. Returns `true` when the error fires.
195///
196/// * `holder_class` — the holder's declared `rdf:type` class hash (see `agent::agent_type`).
197/// * `content_is_np_exclusive` — whether the claimed content is natural-person-only.
198/// * `position` — the jural position asserted.
199/// * `tbox` — `rdfs:subClassOf` Quins for the subsumption check.
200pub fn personhood_category_error(
201    holder_class: u64,
202    content_is_np_exclusive: bool,
203    position: u8,
204    tbox: &[NQuin],
205) -> bool {
206    if !content_is_np_exclusive {
207        return false;
208    }
209    // Only benefit-holding positions over a human-only content can be a category error.
210    if !matches!(position, JURAL_CLAIM | JURAL_PRIVILEGE | JURAL_IMMUNITY) {
211        return false;
212    }
213    // Error iff the holder is NOT a NaturalPerson (nor a subclass of one).
214    !crate::modalities::dl::check_subsumption_quin(holder_class, A_NATURAL_PERSON, tbox)
215}
216
217// ─── Multi-party jural chains (A's Power over B's Duty to C) ─────────────────────────
218
219/// Second-order **control** positions (Power/Liability/Immunity/Disability) are the only ones
220/// that can govern — alter, or be immune to alteration of — another agent's relations.
221#[inline]
222pub const fn is_second_order(pos: u8) -> bool {
223    matches!(
224        pos,
225        JURAL_POWER | JURAL_LIABILITY | JURAL_IMMUNITY | JURAL_DISABILITY
226    )
227}
228
229/// A multi-party chain link: a second-order control relation `upstream` (held by A toward B)
230/// governs `downstream` (a relation held by B toward C). Valid iff `upstream` is a
231/// second-order position AND the pivot matches — A's counterparty (`upstream.object`) is the
232/// holder of the downstream relation (`downstream.subject`). Models "A has Power over B's
233/// Duty to C".
234pub fn jural_chain_links(upstream: &NQuin, downstream: &NQuin) -> bool {
235    is_second_order(jural_position(upstream.predicate))
236        && is_jural_position(jural_position(downstream.predicate))
237        && upstream.object == downstream.subject
238}
239
240/// The pivot party B of a valid chain link (`upstream.object == downstream.subject`), else `None`.
241pub fn jural_chain_pivot(upstream: &NQuin, downstream: &NQuin) -> Option<u64> {
242    if jural_chain_links(upstream, downstream) {
243        Some(upstream.object)
244    } else {
245        None
246    }
247}
248
249/// Confirm an ordered chain A→B→C→… is fully connected: every adjacent pair links. A single
250/// relation (or empty) is trivially valid. Zero-heap (slice windows, no allocation).
251pub fn jural_chain_valid(rels: &[NQuin]) -> bool {
252    rels.windows(2).all(|w| jural_chain_links(&w[0], &w[1]))
253}
254
255// ─── Rights-collision conflict resolution ───────────────────────────────────────────
256
257/// The outcome of resolving a collision between two jural relations.
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum CollisionResolution {
260    /// `a` prevails — it is grounded in a non-derogable human right and `b` is not.
261    FirstPrevails,
262    /// `b` prevails — it is grounded in a non-derogable human right and `a` is not.
263    SecondPrevails,
264    /// A genuine proportionality conflict (both, or neither, non-derogable). Never
265    /// auto-flattened — routed to human review per the Curation Directive.
266    RequiresHumanReview,
267    /// The inputs do not actually collide.
268    NoCollision,
269}
270
271/// Two jural relations **collide** iff the same holder is assigned a position and its jural
272/// *opposite* over the same content within the same frame (e.g. a Duty to φ and a Privilege
273/// not to do φ) — a direct contradiction in that holder's normative position.
274pub fn jural_collision(a: &NQuin, b: &NQuin) -> bool {
275    a.subject == b.subject
276        && a.context == b.context
277        && is_jural_position(jural_position(a.predicate))
278        && jural_content(a.predicate) == jural_content(b.predicate)
279        && jural_opposite(jural_position(a.predicate)) == jural_position(b.predicate)
280}
281
282/// Resolve a rights collision. `a_nonderogable` / `b_nonderogable` mark whether each relation
283/// is grounded in a **non-derogable** human-rights instrument (the ingest non-derogable-set).
284/// A non-derogable right defeats a derogable counterpart; two non-derogable (or two derogable)
285/// positions in genuine conflict are **never auto-flattened** — they route to human review.
286/// The engine proposes; the human disposes.
287pub fn resolve_collision(
288    a: &NQuin,
289    b: &NQuin,
290    a_nonderogable: bool,
291    b_nonderogable: bool,
292) -> CollisionResolution {
293    if !jural_collision(a, b) {
294        return CollisionResolution::NoCollision;
295    }
296    match (a_nonderogable, b_nonderogable) {
297        (true, false) => CollisionResolution::FirstPrevails,
298        (false, true) => CollisionResolution::SecondPrevails,
299        _ => CollisionResolution::RequiresHumanReview,
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use crate::q_hash;
307
308    #[test]
309    fn correlatives_are_involutive_and_paired() {
310        for &p in &[
311            JURAL_CLAIM,
312            JURAL_DUTY,
313            JURAL_PRIVILEGE,
314            JURAL_NO_RIGHT,
315            JURAL_POWER,
316            JURAL_LIABILITY,
317            JURAL_IMMUNITY,
318            JURAL_DISABILITY,
319        ] {
320            // correlative of correlative is the original (involution).
321            assert_eq!(correlative(correlative(p)), p, "{:?}", position_name(p));
322            assert_eq!(jural_opposite(jural_opposite(p)), p);
323            // correlative and opposite are themselves distinct from the position.
324            assert_ne!(correlative(p), p);
325            assert_ne!(jural_opposite(p), p);
326        }
327        assert_eq!(correlative(JURAL_CLAIM), JURAL_DUTY);
328        assert_eq!(correlative(JURAL_POWER), JURAL_LIABILITY);
329        assert_eq!(correlative(JURAL_IMMUNITY), JURAL_DISABILITY);
330        assert_eq!(jural_opposite(JURAL_CLAIM), JURAL_NO_RIGHT);
331    }
332
333    #[test]
334    fn claim_implies_correlative_duty() {
335        let (alice, bob, frame) = (q_hash("alice"), q_hash("bob"), q_hash("nda"));
336        let content = q_hash("q42:repayLoan");
337        // Alice holds a Claim toward Bob that he repay.
338        let claim = compile_jural_quin(alice, JURAL_CLAIM, content, bob, frame);
339        // The correlative is: Bob holds a Duty toward Alice to repay.
340        let duty = correlative_quin(&claim);
341        assert_eq!(jural_position(duty.predicate), JURAL_DUTY);
342        assert_eq!(duty.subject, bob);
343        assert_eq!(duty.object, alice);
344        assert_eq!(
345            jural_content(duty.predicate),
346            jural_content(claim.predicate)
347        );
348        assert_eq!(duty.context, frame);
349        // Parity is a valid XOR fold.
350        assert_eq!(
351            duty.parity,
352            duty.subject ^ duty.predicate ^ duty.object ^ duty.context
353        );
354    }
355
356    #[test]
357    fn correlativity_holds_when_duty_is_present() {
358        let (alice, bob, frame) = (q_hash("alice"), q_hash("bob"), q_hash("nda"));
359        let content = q_hash("q42:repayLoan");
360        let claim = compile_jural_quin(alice, JURAL_CLAIM, content, bob, frame);
361        let duty = correlative_quin(&claim);
362        // Graph with the duty present → correlativity holds.
363        assert!(jural_correlativity_holds(&claim, &[claim, duty]));
364        // Graph without it → does not hold.
365        assert!(!jural_correlativity_holds(&claim, &[claim]));
366    }
367
368    #[test]
369    fn unmet_correlative_duty_is_made_legible() {
370        let (alice, state, frame) = (q_hash("alice"), q_hash("state"), q_hash("icescr"));
371        let content = q_hash("q42:adequateHousing");
372        // Alice holds a Claim to housing toward the State — but no Duty is recorded.
373        let claim = compile_jural_quin(alice, JURAL_CLAIM, content, state, frame);
374        let mut out = [NQuin::default(); 4];
375        let n = find_unmet_correlatives(&[claim], &mut out);
376        assert_eq!(n, 1, "the missing duty must be surfaced");
377        assert_eq!(jural_position(out[0].predicate), JURAL_DUTY);
378        assert_eq!(
379            out[0].subject, state,
380            "the State is the would-be duty-bearer"
381        );
382        assert_eq!(out[0].object, alice);
383
384        // Once the duty exists, nothing is unmet.
385        let duty = correlative_quin(&claim);
386        assert_eq!(find_unmet_correlatives(&[claim, duty], &mut out), 0);
387    }
388
389    #[test]
390    fn corporate_person_claiming_human_only_right_is_category_error() {
391        // TBox: CorporatePerson ⊑ LegalPerson; NaturalPerson ⊑ Agent (disjoint branches).
392        let np = A_NATURAL_PERSON;
393        let legal = q_hash("https://ns.webcivics.net/values/LegalPerson");
394        let corp = q_hash("https://ns.webcivics.net/values/CorporatePerson");
395        let agent = q_hash("https://ns.webcivics.net/values/Agent");
396        let sub = q_hash("http://www.w3.org/2000/01/rdf-schema#subClassOf");
397        let e = |s: u64, o: u64| NQuin {
398            subject: s,
399            predicate: sub,
400            object: o,
401            context: 0,
402            metadata: 0,
403            parity: 0,
404        };
405        let tbox = [e(corp, legal), e(legal, agent), e(np, agent)];
406
407        // A CorporatePerson asserting a Claim to a human-only right → category error.
408        assert!(personhood_category_error(corp, true, JURAL_CLAIM, &tbox));
409        // A NaturalPerson asserting the same → fine.
410        assert!(!personhood_category_error(np, true, JURAL_CLAIM, &tbox));
411        // A CorporatePerson over a NON-exclusive content → not an error.
412        assert!(!personhood_category_error(corp, false, JURAL_CLAIM, &tbox));
413        // A Duty (burden, not benefit) borne by a CorporatePerson → never a category error.
414        assert!(!personhood_category_error(corp, true, JURAL_DUTY, &tbox));
415    }
416
417    #[test]
418    fn multi_party_chain_a_power_over_b_duty_to_c() {
419        let (a, b, c, frame) = (q_hash("A"), q_hash("B"), q_hash("C"), q_hash("frame"));
420        let content = q_hash("q42:performService");
421        // A holds a Power toward B; B holds a Duty toward C.
422        let a_power = compile_jural_quin(a, JURAL_POWER, content, b, frame);
423        let b_duty = compile_jural_quin(b, JURAL_DUTY, content, c, frame);
424        assert!(
425            jural_chain_links(&a_power, &b_duty),
426            "A's power over B governs B's duty to C"
427        );
428        assert_eq!(
429            jural_chain_pivot(&a_power, &b_duty),
430            Some(b),
431            "the pivot is B"
432        );
433        assert!(jural_chain_valid(&[a_power, b_duty]));
434
435        // A first-order Claim cannot be the governing upstream (not a control position).
436        let a_claim = compile_jural_quin(a, JURAL_CLAIM, content, b, frame);
437        assert!(!jural_chain_links(&a_claim, &b_duty));
438        // Broken pivot: B's duty is toward C, but the downstream is held by someone else.
439        let x_duty = compile_jural_quin(q_hash("X"), JURAL_DUTY, content, c, frame);
440        assert!(!jural_chain_links(&a_power, &x_duty));
441        assert_eq!(jural_chain_pivot(&a_power, &x_duty), None);
442    }
443
444    #[test]
445    fn colliding_rights_resolve_by_non_derogability_else_human_review() {
446        let (holder, cp, frame) = (q_hash("holder"), q_hash("counter"), q_hash("frame"));
447        let content = q_hash("q42:speak");
448        // Holder has a Duty to φ AND (from another source) a Privilege not to do φ — opposites.
449        let duty = compile_jural_quin(holder, JURAL_DUTY, content, cp, frame);
450        let privilege = compile_jural_quin(holder, JURAL_PRIVILEGE, content, cp, frame);
451        assert!(
452            jural_collision(&duty, &privilege),
453            "Duty vs Privilege over same content = collision"
454        );
455
456        // The non-derogable right prevails over the derogable one.
457        assert_eq!(
458            resolve_collision(&duty, &privilege, true, false),
459            CollisionResolution::FirstPrevails
460        );
461        assert_eq!(
462            resolve_collision(&duty, &privilege, false, true),
463            CollisionResolution::SecondPrevails
464        );
465        // Both (or neither) non-derogable → genuine proportionality conflict → human review, never flattened.
466        assert_eq!(
467            resolve_collision(&duty, &privilege, true, true),
468            CollisionResolution::RequiresHumanReview
469        );
470        assert_eq!(
471            resolve_collision(&duty, &privilege, false, false),
472            CollisionResolution::RequiresHumanReview
473        );
474
475        // No collision when positions are not opposites (Duty vs Duty).
476        let duty2 = compile_jural_quin(holder, JURAL_DUTY, content, cp, frame);
477        assert!(!jural_collision(&duty, &duty2));
478        assert_eq!(
479            resolve_collision(&duty, &duty2, true, false),
480            CollisionResolution::NoCollision
481        );
482    }
483}