qualia_client_core/incapacity_switch.rs
1//! **Incapacity switch** — involuntary psychiatric admission / serious injury (more common than death), and
2//! the **discrediting-counter** that goes with it.
3//!
4//! The consideration (Timothy, 2026-07-06): the outcome for a person seeking protection is *more often* an
5//! **involuntary psychiatric admission** or a **serious injury** than death. Both incapacitate — but,
6//! unlike death, are **reversible** (the person recovers). Two mechanisms are needed:
7//!
8//! 1. **Advocacy during incapacity.** A pre-designated advocate/trustee is activated to act on the person's
9//! behalf, under a **gamified, corroborated** trigger (a quorum of participating parties attest, optionally
10//! plus an official instrument — a committal order / medical record), and **reverses** on recovery.
11//!
12//! 2. **A counter to weaponised discrediting.** The sharp part: an involuntary psychiatric committal is
13//! frequently *weaponised* — the intent is to ensure **no-one believes anything the person says** — and
14//! that discrediting is **leveraged off privacy** (the committal taints; privacy then hides the context
15//! that would exonerate). The counter is that the person (or their advocate) can **choose to make prior
16//! events transparent** — the durable, un-erasable disclosure/conduct/`cc` record (e.g. "reported to the
17//! MP, then committed" → retaliation, not madness). Crucially this is:
18//! - **the person's *choice*** — privacy is never forcibly lifted (autonomy);
19//! - **honest-contingent** — the system *enables* truthful transparency; it cannot compel honesty, and it
20//! cannot make a dishonest record true. "If the person is willing to be honest — which isn't always the
21//! case." What keeps even a *selective* disclosure bounded is that the underlying records are **durable +
22//! tamper-evident** (from the commons + disclosure-trace layers): the person can choose *what* to reveal,
23//! but cannot delete what they don't, and the invocation itself is recorded.
24//!
25//! Domain model + invariants; the key-release/advocacy wiring + the storage compose from
26//! [`crate::consent_credential`] / [`crate::disclosure_trace`] and the vault (coordinate).
27
28use serde::{Deserialize, Serialize};
29
30use crate::consent_credential::PayloadCommitment;
31
32/// The kind of incapacity a switch covers.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum IncapacityKind {
36 /// Involuntary psychiatric admission (the weaponised-discrediting case).
37 InvoluntaryPsychiatric,
38 /// A serious injury leaving the person unable to manage their affairs.
39 SeriousInjury,
40 Other(String),
41}
42
43/// The **gamified, corroborated** trigger for activating advocacy: a quorum of participating parties attest
44/// the incapacity, optionally **also** requiring an independent official instrument (a committal order /
45/// medical record). Resists a false trigger by any single party.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct IncapacityTrigger {
48 /// The advocates/friends who may attest the incapacity.
49 pub parties: Vec<String>,
50 /// At least this many **distinct participating** parties must attest.
51 pub attestation_threshold: usize,
52 /// If true, an official instrument (committal order / medical record) is **also** required — a second,
53 /// independent corroboration, so party-attestation alone cannot activate it.
54 pub require_official_instrument: bool,
55}
56
57impl IncapacityTrigger {
58 /// Satisfied iff (official instrument present, if required) AND ≥ `attestation_threshold` distinct
59 /// *participating* parties have attested.
60 pub fn is_satisfied(
61 &self,
62 attesting_parties: &[String],
63 official_instrument: Option<&str>,
64 ) -> bool {
65 if self.require_official_instrument && official_instrument.is_none() {
66 return false;
67 }
68 let distinct: std::collections::BTreeSet<&str> = attesting_parties
69 .iter()
70 .map(|s| s.as_str())
71 .filter(|d| self.parties.iter().any(|p| p == d))
72 .collect();
73 distinct.len() >= self.attestation_threshold
74 }
75}
76
77/// A reversible incapacity switch: activates a pre-designated advocate under the trigger, reverses on
78/// recovery.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct IncapacitySwitch {
81 pub principal_did: String,
82 pub kind: IncapacityKind,
83 pub trigger: IncapacityTrigger,
84 /// The advocate/trustee pre-designated to act during incapacity (scoped, revocable — a care/steward role,
85 /// never custody of the person's fabric).
86 pub advocate_did: String,
87 /// `Some(t)` while the advocate is active (incapacitated since `t`); `None` when the person has capacity.
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub active_since_unix: Option<u64>,
90}
91
92impl IncapacitySwitch {
93 /// Is the advocate currently acting (the person incapacitated)?
94 pub fn advocate_active(&self) -> bool {
95 self.active_since_unix.is_some()
96 }
97
98 /// Whether the switch can be activated now (trigger satisfied and not already active).
99 pub fn can_activate(
100 &self,
101 attesting_parties: &[String],
102 official_instrument: Option<&str>,
103 ) -> bool {
104 !self.advocate_active()
105 && self
106 .trigger
107 .is_satisfied(attesting_parties, official_instrument)
108 }
109
110 /// Activate advocacy if the trigger is satisfied. Returns whether it activated.
111 pub fn activate(
112 &mut self,
113 attesting_parties: &[String],
114 official_instrument: Option<&str>,
115 now_unix: u64,
116 ) -> bool {
117 if self.can_activate(attesting_parties, official_instrument) {
118 self.active_since_unix = Some(now_unix);
119 true
120 } else {
121 false
122 }
123 }
124
125 /// The person **regained capacity** — reverse: the advocate stands down, control reverts to the
126 /// principal. The reversibility that distinguishes incapacity from death.
127 pub fn regain_capacity(&mut self, _now_unix: u64) {
128 self.active_since_unix = None;
129 }
130}
131
132/// A **transparency invocation** — the person (or, during incapacity, their advocate) *chooses* to make a
133/// **scoped** set of prior-events records transparent, to **counter discrediting** by showing context. The
134/// counter to privacy-weaponisation: the person's own durable, un-erasable record, disclosed on **their**
135/// terms.
136///
137/// It is the person's **choice** (privacy is not forcibly lifted) and its value is **honest-contingent** —
138/// the system enables truthful transparency; it neither compels honesty nor can make a dishonest record true.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct TransparencyInvocation {
141 /// Who invoked it — the person, or their advocate during a validated incapacity.
142 pub invoker_did: String,
143 /// Whose record it is (the person).
144 pub subject_did: String,
145 /// The prior-events records (by commitment) being made transparent — a **scoped** selection the invoker
146 /// chooses.
147 pub disclosed_commitments: Vec<PayloadCommitment>,
148 /// Why (e.g. "counter discrediting after involuntary committal — show the retaliation timeline").
149 pub purpose: String,
150 pub invoked_unix: u64,
151}
152
153impl TransparencyInvocation {
154 /// Whether an advocate may invoke this on the subject's behalf — only during a *validated active*
155 /// incapacity for that subject (otherwise only the subject themselves may). Enforces "the advocate acts
156 /// only while the person cannot".
157 pub fn advocate_may_invoke(&self, switch: &IncapacitySwitch) -> bool {
158 self.invoker_did == self.subject_did
159 || (self.invoker_did == switch.advocate_did
160 && switch.principal_did == self.subject_did
161 && switch.advocate_active())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn switch() -> IncapacitySwitch {
170 IncapacitySwitch {
171 principal_did: "did:wf:person".into(),
172 kind: IncapacityKind::InvoluntaryPsychiatric,
173 trigger: IncapacityTrigger {
174 parties: vec![
175 "did:wf:alice".into(),
176 "did:wf:bob".into(),
177 "did:wf:carol".into(),
178 ],
179 attestation_threshold: 2,
180 require_official_instrument: true,
181 },
182 advocate_did: "did:wf:advocate".into(),
183 active_since_unix: None,
184 }
185 }
186
187 #[test]
188 fn requires_quorum_and_the_official_instrument_to_activate() {
189 let mut s = switch();
190 let quorum = vec!["did:wf:alice".to_string(), "did:wf:bob".to_string()];
191
192 // Quorum but NO official instrument → not satisfied (needs corroboration).
193 assert!(!s.can_activate(&quorum, None));
194 // Official instrument but only one party → below threshold.
195 assert!(!s.can_activate(&["did:wf:alice".to_string()], Some("committal-order:7")));
196 // Both → activates.
197 assert!(s.activate(&quorum, Some("committal-order:7"), 1_000));
198 assert!(s.advocate_active());
199 }
200
201 #[test]
202 fn non_party_attestations_do_not_count() {
203 let s = switch();
204 let mixed = vec!["did:wf:stranger".to_string(), "did:wf:alice".to_string()];
205 assert!(
206 !s.can_activate(&mixed, Some("order")),
207 "only participating parties count toward quorum"
208 );
209 }
210
211 #[test]
212 fn incapacity_is_reversible_the_person_recovers_and_reclaims_control() {
213 let mut s = switch();
214 let quorum = vec!["did:wf:alice".to_string(), "did:wf:bob".to_string()];
215 assert!(s.activate(&quorum, Some("order"), 1_000));
216 assert!(s.advocate_active());
217 // The person recovers → advocate stands down, control reverts.
218 s.regain_capacity(2_000);
219 assert!(!s.advocate_active(), "reversible — not death");
220 // Can re-activate if incapacitated again later.
221 assert!(s.activate(&quorum, Some("order-2"), 3_000));
222 assert!(s.advocate_active());
223 }
224
225 #[test]
226 fn the_person_can_always_invoke_transparency_to_counter_discrediting() {
227 let s = switch(); // not active — the person has capacity
228 let inv = TransparencyInvocation {
229 invoker_did: "did:wf:person".into(),
230 subject_did: "did:wf:person".into(),
231 disclosed_commitments: vec![[1u8; 32], [2u8; 32]], // the reported-to-MP timeline, e.g.
232 purpose: "counter discrediting after involuntary committal".into(),
233 invoked_unix: 1_500,
234 };
235 // The person can always make their OWN prior events transparent, on their terms.
236 assert!(inv.advocate_may_invoke(&s));
237 assert_eq!(
238 inv.disclosed_commitments.len(),
239 2,
240 "a scoped selection they choose"
241 );
242 }
243
244 #[test]
245 fn an_advocate_may_invoke_only_during_a_validated_active_incapacity() {
246 let mut s = switch();
247 let inv = TransparencyInvocation {
248 invoker_did: "did:wf:advocate".into(),
249 subject_did: "did:wf:person".into(),
250 disclosed_commitments: vec![[1u8; 32]],
251 purpose: "counter discrediting while the person is committed".into(),
252 invoked_unix: 1_500,
253 };
254 // Not active yet → the advocate may NOT invoke on the person's behalf.
255 assert!(!inv.advocate_may_invoke(&s));
256 // Once a validated incapacity is active, the advocate may.
257 s.activate(
258 &["did:wf:alice".to_string(), "did:wf:bob".to_string()],
259 Some("order"),
260 1_000,
261 );
262 assert!(inv.advocate_may_invoke(&s));
263 // And once the person recovers, the advocate may not again.
264 s.regain_capacity(2_000);
265 assert!(
266 !inv.advocate_may_invoke(&s),
267 "advocate acts only while the person cannot"
268 );
269 }
270
271 #[test]
272 fn serde_round_trips() {
273 let mut s = switch();
274 s.activate(
275 &["did:wf:alice".to_string(), "did:wf:bob".to_string()],
276 Some("order"),
277 1_000,
278 );
279 let back: IncapacitySwitch =
280 serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
281 assert_eq!(s, back);
282 }
283}