Skip to main content

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}