Skip to main content

qualia_core_db/modalities/
stit.rs

1//! STIT agency — "α Sees To It That φ" (Phase 3, DEONTIC_LOGIC_PLAN §5).
2//!
3//! Standard deontic logic makes *states of affairs* obligatory (`O(φ)`). Legal instruments
4//! make *specific agents* obligated to **act** ("the State shall ensure…"). STIT binds the
5//! deontic force to the agent who is the causal force, which lets the engine:
6//!   * distinguish a **duty-bearer** from a **bystander** ([`is_duty_bearer`]);
7//!   * detect **omission** — an in-force obligation the bearer did not bring about
8//!     ([`agentive_status`] → `Violated`); and
9//!   * model **joint action / shared liability** — `O[{α,β} stit φ]` discharged iff *any*
10//!     member brings φ about, else *all* members share liability
11//!     ([`joint_discharged`], [`joint_liable_members`]).
12//!
13//! A STIT-bound norm reuses the deontic norm Quin: `subject` is the agent α (the causal
14//! force), `object` is the brought-about content φ. The causal fact convention is
15//! `(α, q42:broughtAbout, φ)`. This module is a *post-hoc accountability* reading over a
16//! deontic norm (evaluated when the duty is due/closed), complementing the live-status
17//! `deontic::norm_lifecycle_status`. Zero-heap throughout.
18
19use crate::modalities::logic::deontic::{
20    extract_deontic_opcode, DeonticStatus, OP_FORBID, OP_OBLIGATE,
21};
22use crate::{q_hash, NQuin};
23
24/// Did `agent` see to it that `content` — i.e. is the causal fact `(agent, q42:broughtAbout,
25/// content)` present?
26pub fn brought_about(facts: &[NQuin], agent: u64, content: u64) -> bool {
27    let p = q_hash("q42:broughtAbout");
28    facts
29        .iter()
30        .any(|q| q.subject == agent && q.predicate == p && q.object == content)
31}
32
33/// True iff `agent` is the bearer (causal subject) of the agentive norm — a duty-bearer
34/// rather than a bystander. (Accountability: the obligation attaches to the actor, not to
35/// everyone who could have acted.)
36#[inline]
37pub fn is_duty_bearer(norm: &NQuin, agent: u64) -> bool {
38    norm.subject == agent
39}
40
41/// Post-hoc accountability status of an agentive norm `O[α stit φ]` / `F[α stit φ]`,
42/// evaluated when the duty is due/closed:
43/// * `OP_OBLIGATE`: the bearer brought φ about → [`Discharged`](DeonticStatus::Discharged);
44///   otherwise it is an **omission** → [`Violated`](DeonticStatus::Violated).
45/// * `OP_FORBID`: the bearer brought the forbidden φ about → `Violated`; else `Active`.
46/// * anything else (e.g. a permission): `Active` — a liberty cannot be omitted.
47pub fn agentive_status(norm: &NQuin, facts: &[NQuin]) -> DeonticStatus {
48    let agent = norm.subject;
49    let content = norm.object;
50    match extract_deontic_opcode(norm.predicate) {
51        OP_OBLIGATE => {
52            if brought_about(facts, agent, content) {
53                DeonticStatus::Discharged
54            } else {
55                DeonticStatus::Violated // omission: O[α stit φ] ∧ ¬[α stit φ]
56            }
57        }
58        OP_FORBID => {
59            if brought_about(facts, agent, content) {
60                DeonticStatus::Violated
61            } else {
62                DeonticStatus::Active
63            }
64        }
65        _ => DeonticStatus::Active,
66    }
67}
68
69/// Joint obligation `O[{members} stit φ]`: discharged iff **any** member saw to it that φ
70/// (joint sufficiency). Zero-heap.
71pub fn joint_discharged(members: &[u64], content: u64, facts: &[NQuin]) -> bool {
72    members.iter().any(|&m| brought_about(facts, m, content))
73}
74
75/// Shared liability: if the joint obligation is NOT discharged, **every** member shares
76/// liability — write them into `out` and return the count. Returns `0` when discharged
77/// (no one is liable). Zero-heap (caller-supplied `out`).
78pub fn joint_liable_members(
79    members: &[u64],
80    content: u64,
81    facts: &[NQuin],
82    out: &mut [u64],
83) -> usize {
84    if joint_discharged(members, content, facts) {
85        return 0;
86    }
87    let mut n = 0usize;
88    for &m in members {
89        if n >= out.len() {
90            break;
91        }
92        out[n] = m;
93        n += 1;
94    }
95    n
96}
97
98// ─── Branching time + choice partitioning (cstit / dstit) ─────────────────────────
99//
100// On a branching-time tree of histories, φ is "settled true" at a moment if it holds on EVERY
101// history through it (the agent had no choice about it). STIT distinguishes:
102//   * **cstit** (Chellas):    α saw to it that φ — φ holds because of α's choice.
103//   * **dstit** (deliberative): cstit AND φ was NOT settled — α had a genuine alternative (could
104//     have done otherwise). Deliberative agency is what grounds blame/praise.
105
106/// Is φ **settled** — true on all histories (the agent had no real choice)? `could_do_otherwise`
107/// is whether an alternative history avoided φ.
108#[inline]
109pub fn is_settled(could_do_otherwise: bool) -> bool {
110    !could_do_otherwise
111}
112
113/// **Chellas STIT** `[α cstit φ]`: α saw to it that φ — here, α brought φ about.
114#[inline]
115pub fn chellas_stit(brought_about: bool) -> bool {
116    brought_about
117}
118
119/// **Deliberative STIT** `[α dstit φ]`: α brought φ about AND φ was not settled (α could have
120/// done otherwise) — the genuine-choice reading that grounds moral responsibility.
121#[inline]
122pub fn deliberative_stit(brought_about: bool, could_do_otherwise: bool) -> bool {
123    brought_about && could_do_otherwise
124}
125
126/// **Counterfactual omission** ("could have prevented X but did not"): the agent had the
127/// `ability` and the `opportunity` to bring about the prevention, yet did not act. The
128/// counterfactual that turns a bare omission into a culpable one.
129#[inline]
130pub fn could_have_prevented(had_ability: bool, had_opportunity: bool, did_act: bool) -> bool {
131    had_ability && had_opportunity && !did_act
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::modalities::logic::deontic::compile_norm_quin;
138
139    fn fact(s: u64, p: u64, o: u64) -> NQuin {
140        let mut q = NQuin {
141            subject: s,
142            predicate: p,
143            object: o,
144            context: 0,
145            metadata: 0,
146            parity: 0,
147        };
148        q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
149        q
150    }
151
152    #[test]
153    fn duty_bearer_vs_bystander() {
154        let (state, citizen) = (q_hash("did:state"), q_hash("did:citizen"));
155        let ensure = q_hash("q42:ensureRemedy");
156        let norm = compile_norm_quin(
157            state,
158            OP_OBLIGATE,
159            ensure,
160            q_hash("q42:victim"),
161            q_hash("frame"),
162            0,
163            false,
164        );
165        assert!(is_duty_bearer(&norm, state), "the State bears the duty");
166        assert!(
167            !is_duty_bearer(&norm, citizen),
168            "a citizen is a bystander to this duty"
169        );
170    }
171
172    #[test]
173    fn obligation_brought_about_is_discharged_else_omission() {
174        let state = q_hash("did:state");
175        let outcome = q_hash("q42:provideRemedy");
176        let norm = compile_norm_quin(
177            state,
178            OP_OBLIGATE,
179            q_hash("q42:remedyDuty"),
180            outcome,
181            q_hash("frame"),
182            0,
183            false,
184        );
185        // Brought about → Discharged.
186        let done = [fact(state, q_hash("q42:broughtAbout"), outcome)];
187        assert_eq!(agentive_status(&norm, &done), DeonticStatus::Discharged);
188        // Not brought about → omission → Violated.
189        assert_eq!(agentive_status(&norm, &[]), DeonticStatus::Violated);
190    }
191
192    #[test]
193    fn forbidden_act_brought_about_is_violation() {
194        let platform = q_hash("did:platformAgent");
195        let manipulate = q_hash("q42:manipulateUser");
196        let norm = compile_norm_quin(
197            platform,
198            OP_FORBID,
199            q_hash("q42:noManip"),
200            manipulate,
201            q_hash("frame"),
202            0,
203            false,
204        );
205        // Performed the forbidden act → Violated.
206        let did = [fact(platform, q_hash("q42:broughtAbout"), manipulate)];
207        assert_eq!(agentive_status(&norm, &did), DeonticStatus::Violated);
208        // Did not → Active.
209        assert_eq!(agentive_status(&norm, &[]), DeonticStatus::Active);
210    }
211
212    #[test]
213    fn joint_action_shared_liability() {
214        let principal = q_hash("did:principal");
215        let platform = q_hash("did:platformAgent");
216        let members = [principal, platform];
217        let content = q_hash("q42:protectUserData");
218
219        // Neither brought it about → joint obligation undischarged, BOTH share liability.
220        let mut out = [0u64; 4];
221        assert!(!joint_discharged(&members, content, &[]));
222        let n = joint_liable_members(&members, content, &[], &mut out);
223        assert_eq!(n, 2, "both members share liability");
224        assert!(out[..n].contains(&principal) && out[..n].contains(&platform));
225
226        // One member brings it about → discharged, no one liable (joint sufficiency).
227        let done = [fact(platform, q_hash("q42:broughtAbout"), content)];
228        assert!(joint_discharged(&members, content, &done));
229        assert_eq!(joint_liable_members(&members, content, &done, &mut out), 0);
230    }
231
232    #[test]
233    fn cstit_dstit_and_counterfactual_prevention() {
234        // cstit: brought about → saw to it.
235        assert!(chellas_stit(true));
236        assert!(!chellas_stit(false));
237        // Settled-ness: no alternative history ⇒ settled.
238        assert!(is_settled(false));
239        assert!(!is_settled(true));
240        // dstit: brought about AND could have done otherwise (genuine choice).
241        assert!(deliberative_stit(true, true));
242        assert!(
243            !deliberative_stit(true, false),
244            "settled outcome → no deliberative agency"
245        );
246        assert!(!deliberative_stit(false, true));
247        // Counterfactual omission: could have prevented (ability + opportunity, did not act).
248        assert!(could_have_prevented(true, true, false));
249        assert!(
250            !could_have_prevented(true, true, true),
251            "acted → no culpable omission"
252        );
253        assert!(
254            !could_have_prevented(false, true, false),
255            "no ability → not culpable"
256        );
257    }
258}