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}