qualia_core_db/modalities/interaction_governance.rs
1//! Interaction governance (Phase 6, DEONTIC_LOGIC_PLAN §15) — the final stage that maps an
2//! abstract [`DeonticVerdict`] to a concrete runtime action in the Webizen VM.
3//!
4//! Once the deontic / spatial / argumentation logics yield a verdict, *something must
5//! happen*. This module is the **pure decision layer**: verdict (+ a little classification)
6//! → [`PolicyMode`]. The side effects each mode implies are performed by the caller:
7//! * [`PolicyMode::PreventiveBlock`] → the VM injects a `DenyRollback` and halts the
8//! transaction *before* harm (non-derogable violations: child safety, the ICCPR core).
9//! * [`PolicyMode::PermissiveAudit`] → the transaction proceeds but a `BreachRecord` is
10//! written to the WAL ([`super::meta_deontic::record_breach_to_wal`]) for the evidentiary
11//! trail (system utility preserved, conduct still recorded).
12//! * [`PolicyMode::Prioritize`] → QoS / routing preference for `hict:HumanitarianICT`
13//! (peace infrastructure, medical access).
14//! * [`PolicyMode::Interactive`] → halt and ask the human for a `sense:HumanCorrection`
15//! (ambiguous or uninterpretable mappings — agency over meaning stays human).
16//! * [`PolicyMode::Allow`] → nothing special (in force / no longer binding).
17//!
18//! Keeping the decision pure and separate from the effect is what makes the gate auditable
19//! and the same logic reusable by both the VM and the MCP cooperation interface (Track M).
20
21use crate::modalities::logic::deontic::{DeonticStatus, DeonticVerdict};
22
23/// What the runtime should DO about a verdict.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum PolicyMode {
26 /// Halt before harm — `DenyRollback`. For non-derogable violations.
27 PreventiveBlock,
28 /// Allow, but log an immutable `BreachRecord` to the WAL. For non-critical violations.
29 PermissiveAudit,
30 /// Grant QoS / routing / UI priority. For in-force humanitarian norms.
31 Prioritize,
32 /// Halt and request a human correction. For ambiguous / uninterpretable mappings.
33 Interactive,
34 /// No special action — proceed.
35 Allow,
36}
37
38/// The classification a verdict needs beyond its status to be governed: is the norm
39/// non-derogable (a Hohfeldian Immunity / MandatoryBaseline), humanitarian, and is its
40/// mapping ambiguous? These come from the graph (the `values:nonDerogable` overlay,
41/// `hict:HumanitarianICT`, and the resolver), not invented here.
42#[derive(Debug, Clone, Copy, Default)]
43pub struct Governance {
44 pub non_derogable: bool,
45 pub humanitarian: bool,
46 pub ambiguous: bool,
47}
48
49/// Map a deontic status + classification to the runtime [`PolicyMode`].
50///
51/// Order of precedence: ambiguity (ask the human) → violation (block if non-derogable, else
52/// audit) → in-force humanitarian (prioritize) → otherwise allow.
53pub fn map_policy(status: DeonticStatus, g: Governance) -> PolicyMode {
54 if g.ambiguous {
55 return PolicyMode::Interactive;
56 }
57 match status {
58 DeonticStatus::Violated => {
59 if g.non_derogable {
60 PolicyMode::PreventiveBlock
61 } else {
62 PolicyMode::PermissiveAudit
63 }
64 }
65 DeonticStatus::Active | DeonticStatus::Discharged => {
66 if g.humanitarian {
67 PolicyMode::Prioritize
68 } else {
69 PolicyMode::Allow
70 }
71 }
72 // Not yet binding, no longer binding, or uninterpretable.
73 DeonticStatus::Pending | DeonticStatus::Defeated | DeonticStatus::Expired => {
74 PolicyMode::Allow
75 }
76 DeonticStatus::Malformed => PolicyMode::Interactive, // cannot interpret → ask a human
77 }
78}
79
80/// Govern a full verdict (convenience over [`map_policy`]).
81#[inline]
82pub fn govern_verdict(verdict: &DeonticVerdict, g: Governance) -> PolicyMode {
83 map_policy(verdict.status, g)
84}
85
86/// Whether this mode lets the transaction proceed (audit/prioritize/allow) vs halts it
87/// (block/interactive). The VM uses this as the go/no-go bit.
88#[inline]
89pub fn permits_execution(mode: PolicyMode) -> bool {
90 matches!(
91 mode,
92 PolicyMode::PermissiveAudit | PolicyMode::Prioritize | PolicyMode::Allow
93 )
94}
95
96/// A short, stable label for logs / MCP responses.
97pub const fn policy_action(mode: PolicyMode) -> &'static str {
98 match mode {
99 PolicyMode::PreventiveBlock => "DenyRollback",
100 PolicyMode::PermissiveAudit => "AllowAndAuditToWAL",
101 PolicyMode::Prioritize => "GrantPriority",
102 PolicyMode::Interactive => "RequestHumanCorrection",
103 PolicyMode::Allow => "Allow",
104 }
105}
106
107// ─── Dynamic overriding rules (humanitarian emergency) ────────────────────────────
108
109/// A humanitarian emergency may downgrade a [`PolicyMode::PreventiveBlock`] to
110/// [`PolicyMode::PermissiveAudit`] (proceed but record) — EXCEPT for the non-overridable
111/// **hard core** (torture, child safety, the non-derogable absolute prohibitions), which never
112/// bypasses. All other modes pass through unchanged. This is the structured emergency exception,
113/// not an open backdoor.
114pub fn apply_emergency_override(base: PolicyMode, emergency: bool, hard_core: bool) -> PolicyMode {
115 if emergency && base == PolicyMode::PreventiveBlock && !hard_core {
116 PolicyMode::PermissiveAudit
117 } else {
118 base
119 }
120}
121
122// ─── Multi-stakeholder M-of-N threshold ───────────────────────────────────────────
123
124/// A multi-stakeholder governance decision is authorized iff at least `m` stakeholders approved
125/// (an M-of-N threshold; `approvals` is the count of approving stakeholders). `m == 0` is never
126/// authorized (a decision needs at least one approver).
127pub fn threshold_authorized(approvals: usize, m: usize) -> bool {
128 m > 0 && approvals >= m
129}
130
131// ─── Systemic circuit breaker (paraconsistent inconsistency spike) ────────────────
132
133/// Trip the systemic circuit breaker when inconsistency `saturation` (from
134/// `paraconsistent::local_saturation` / `global_saturation`) reaches `threshold`: the system
135/// halts into [`PolicyMode::Interactive`] (ask a human) rather than act on a saturated,
136/// self-contradictory graph. Returns the override mode if tripped, else `None`.
137pub fn circuit_breaker(saturation: f32, threshold: f32) -> Option<PolicyMode> {
138 if saturation >= threshold {
139 Some(PolicyMode::Interactive)
140 } else {
141 None
142 }
143}
144
145// ─── Proportionality binding (human-rights instruments) ───────────────────────────
146
147/// A governance action that **restricts individual agency** is justified only if proportionate —
148/// its `marginal_harm` to the person is strictly less than the `advantage` it secures. Binds
149/// algorithmic governance to the proportionality test of the human-rights instruments. A
150/// non-restricting action is always permitted.
151pub fn restriction_proportionate(
152 restricts_agency: bool,
153 marginal_harm: f64,
154 advantage: f64,
155) -> bool {
156 !restricts_agency || marginal_harm < advantage
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn g(non_derogable: bool, humanitarian: bool, ambiguous: bool) -> Governance {
164 Governance {
165 non_derogable,
166 humanitarian,
167 ambiguous,
168 }
169 }
170
171 #[test]
172 fn non_derogable_violation_is_preventive_block() {
173 let m = map_policy(DeonticStatus::Violated, g(true, false, false));
174 assert_eq!(m, PolicyMode::PreventiveBlock);
175 assert!(
176 !permits_execution(m),
177 "a non-derogable breach must NOT proceed"
178 );
179 assert_eq!(policy_action(m), "DenyRollback");
180 }
181
182 #[test]
183 fn ordinary_violation_is_permissive_audit() {
184 let m = map_policy(DeonticStatus::Violated, g(false, false, false));
185 assert_eq!(m, PolicyMode::PermissiveAudit);
186 assert!(
187 permits_execution(m),
188 "a non-critical breach proceeds but is recorded"
189 );
190 }
191
192 #[test]
193 fn humanitarian_in_force_is_prioritized() {
194 assert_eq!(
195 map_policy(DeonticStatus::Active, g(false, true, false)),
196 PolicyMode::Prioritize
197 );
198 // Non-humanitarian in force → just allow.
199 assert_eq!(
200 map_policy(DeonticStatus::Active, g(false, false, false)),
201 PolicyMode::Allow
202 );
203 }
204
205 #[test]
206 fn ambiguity_always_defers_to_a_human() {
207 // Ambiguity wins even over a non-derogable violation — the human decides the mapping.
208 assert_eq!(
209 map_policy(DeonticStatus::Violated, g(true, false, true)),
210 PolicyMode::Interactive
211 );
212 assert_eq!(
213 map_policy(DeonticStatus::Active, g(false, true, true)),
214 PolicyMode::Interactive
215 );
216 // Malformed verdicts also route to a human.
217 assert_eq!(
218 map_policy(DeonticStatus::Malformed, g(false, false, false)),
219 PolicyMode::Interactive
220 );
221 }
222
223 #[test]
224 fn non_binding_statuses_allow() {
225 for s in [
226 DeonticStatus::Pending,
227 DeonticStatus::Defeated,
228 DeonticStatus::Expired,
229 ] {
230 assert_eq!(map_policy(s, g(false, false, false)), PolicyMode::Allow);
231 }
232 // Discharged duty, humanitarian context → still prioritized.
233 assert_eq!(
234 map_policy(DeonticStatus::Discharged, g(false, true, false)),
235 PolicyMode::Prioritize
236 );
237 }
238
239 #[test]
240 fn humanitarian_emergency_overrides_non_core_blocks_only() {
241 // An ordinary non-derogable block downgrades to audit under emergency…
242 assert_eq!(
243 apply_emergency_override(PolicyMode::PreventiveBlock, true, false),
244 PolicyMode::PermissiveAudit
245 );
246 // …but the hard core (torture / child safety) NEVER bypasses.
247 assert_eq!(
248 apply_emergency_override(PolicyMode::PreventiveBlock, true, true),
249 PolicyMode::PreventiveBlock
250 );
251 // No emergency → unchanged; non-block modes pass through.
252 assert_eq!(
253 apply_emergency_override(PolicyMode::PreventiveBlock, false, false),
254 PolicyMode::PreventiveBlock
255 );
256 assert_eq!(
257 apply_emergency_override(PolicyMode::Allow, true, false),
258 PolicyMode::Allow
259 );
260 }
261
262 #[test]
263 fn m_of_n_threshold_governance() {
264 assert!(threshold_authorized(3, 3));
265 assert!(threshold_authorized(4, 3));
266 assert!(!threshold_authorized(2, 3));
267 assert!(
268 !threshold_authorized(0, 0),
269 "a decision needs at least one approver"
270 );
271 }
272
273 #[test]
274 fn circuit_breaker_trips_on_inconsistency_spike() {
275 assert_eq!(circuit_breaker(0.9, 0.8), Some(PolicyMode::Interactive));
276 assert_eq!(circuit_breaker(0.5, 0.8), None);
277 }
278
279 #[test]
280 fn agency_restriction_must_be_proportionate() {
281 // A restriction whose harm < advantage is justified; harm ≥ advantage is not.
282 assert!(restriction_proportionate(true, 1.0, 5.0));
283 assert!(!restriction_proportionate(true, 5.0, 1.0));
284 // A non-restricting action is always permitted.
285 assert!(restriction_proportionate(false, 100.0, 0.0));
286 }
287}