Skip to main content

qualia_client_core/
duty_of_inquiry.rs

1//! **Duty of inquiry** — expectations that *define* negligence.
2//!
3//! The consideration (Timothy, 2026-07-06): staff in mental-health (and other welfare) facilities are rarely
4//! experts, and often *cannot* understand complex, specialised, international, or secrecy-bound work. A real
5//! specialist's real work can look like grandiosity to someone who cannot verify it. Fairness therefore
6//! **cannot require them to understand** — but it *can* require them to **check the means when means are
7//! available**. This sets an **expectation**, and the expectation is what **defines negligence**:
8//!
9//! > *Failure to check, even given the means to do so, then acts that cause further injury* = **negligence**.
10//!
11//! And it keeps negligence **fair** by distinguishing it from the neighbours:
12//! - **No fault** — the means were genuinely *not accessible*; the actor could not reasonably have known.
13//! - **Negligent** — accessible means were *not checked*, and a harmful act followed.
14//! - (**Malfeasance** — checked / knew and harmed anyway, or wilfully avoided checking to keep deniability —
15//!   is *beyond* this inquiry primitive; it is the intent case in the accountability spectrum.)
16//!
17//! The "means to check" are exactly what the rest of the fabric provides: verifiable credentials, the durable
18//! disclosure/conduct records, and a person's [`TransparencyInvocation`](crate::incapacity_switch::TransparencyInvocation)
19//! (offering their prior-events record to be checked). This module is the pure classifier; it composes with
20//! the social-worker accountability spectrum (`docs/plans/social-worker-support-and-accountability.md` §3).
21
22use serde::{Deserialize, Serialize};
23
24/// A means by which an actor could verify a relevant fact before acting — a verifiable credential, a durable
25/// record, a transparency invocation the person offered. Carries whether it was **reasonably accessible** to
26/// the actor at the relevant time (given to them, or checkable with the means they had). If it was **not**
27/// accessible, not-checking it is *not* negligence.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct MeansToCheck {
30    pub id: String,
31    /// What it would verify, in plain terms (e.g. "a credential attesting the person's specialist role").
32    pub description: String,
33    /// Was this reasonably accessible / checkable by the actor at the relevant time?
34    pub accessible: bool,
35}
36
37/// The **duty**: a consequential act is expected to be preceded by checking the relevant means. Sets the
38/// expectation against which negligence is measured.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct DutyOfInquiry {
41    /// The consequential act (e.g. "diagnose", "medicate", "restrain", "record as unreliable").
42    pub act: String,
43    /// The means the actor was expected to check before that act.
44    pub expected_means: Vec<MeansToCheck>,
45}
46
47/// What actually happened, against the duty.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ConductAgainstDuty {
50    pub actor_did: String,
51    /// Ids of the means the actor actually checked.
52    pub checked_means_ids: Vec<String>,
53    /// Did the actor take the consequential act?
54    pub acted: bool,
55    /// Did the act cause (further) injury to the person?
56    pub caused_further_injury: bool,
57}
58
59/// The fair classification of a shortfall — the locus, tying the accountability spectrum.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum InquiryVerdict {
63    /// Every **accessible** means was checked before acting — diligent as to inquiry.
64    Diligent,
65    /// The means were **not accessible** — the actor could not reasonably have known; no fault.
66    NoFault,
67    /// Accessible means were **not checked**, but **no harmful act** followed — a procedural shortfall, not
68    /// (yet) actionable negligence. Recorded honestly rather than inflated to negligence.
69    UncheckedNoHarm,
70    /// Accessible means were **not checked** and a **harmful act followed** — **negligence** ("failure to
71    /// check, given the means, then acts that cause further injury").
72    Negligent,
73}
74
75/// Classify conduct against a duty of inquiry. Deterministic; the criteria are the definition above.
76pub fn assess(duty: &DutyOfInquiry, conduct: &ConductAgainstDuty) -> InquiryVerdict {
77    let any_accessible = duty.expected_means.iter().any(|m| m.accessible);
78    if !any_accessible {
79        // Nothing the actor could reasonably have checked → they could not have known.
80        return InquiryVerdict::NoFault;
81    }
82    let unchecked_accessible = duty
83        .expected_means
84        .iter()
85        .any(|m| m.accessible && !conduct.checked_means_ids.iter().any(|id| id == &m.id));
86    if !unchecked_accessible {
87        // Everything accessible was checked.
88        return InquiryVerdict::Diligent;
89    }
90    if conduct.acted && conduct.caused_further_injury {
91        InquiryVerdict::Negligent
92    } else {
93        InquiryVerdict::UncheckedNoHarm
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn means(id: &str, accessible: bool) -> MeansToCheck {
102        MeansToCheck {
103            id: id.into(),
104            description: format!("means {id}"),
105            accessible,
106        }
107    }
108
109    fn duty(means: Vec<MeansToCheck>) -> DutyOfInquiry {
110        DutyOfInquiry {
111            act: "record the person as unreliable / medicate".into(),
112            expected_means: means,
113        }
114    }
115
116    fn conduct(checked: &[&str], acted: bool, injury: bool) -> ConductAgainstDuty {
117        ConductAgainstDuty {
118            actor_did: "did:wf:facility-staff".into(),
119            checked_means_ids: checked.iter().map(|s| s.to_string()).collect(),
120            acted,
121            caused_further_injury: injury,
122        }
123    }
124
125    #[test]
126    fn no_fault_when_the_means_were_not_accessible() {
127        // The person's specialist credential existed but was NOT accessible to the staff (secrecy / no route
128        // to verify) — they could not reasonably have known. Not negligence.
129        let d = duty(vec![means("cred:specialist", false)]);
130        assert_eq!(
131            assess(&d, &conduct(&[], true, true)),
132            InquiryVerdict::NoFault
133        );
134    }
135
136    #[test]
137    fn diligent_when_the_accessible_means_were_checked() {
138        let d = duty(vec![
139            means("cred:specialist", true),
140            means("record:timeline", true),
141        ]);
142        // Both accessible means checked before acting.
143        assert_eq!(
144            assess(
145                &d,
146                &conduct(&["cred:specialist", "record:timeline"], true, false)
147            ),
148            InquiryVerdict::Diligent
149        );
150    }
151
152    #[test]
153    fn negligent_when_accessible_means_unchecked_and_a_harmful_act_follows() {
154        // The person offered a transparency invocation + a verifiable credential (accessible); staff did NOT
155        // check, then acted in a way that caused further injury. This is the definition of negligence.
156        let d = duty(vec![
157            means("transparency:timeline", true),
158            means("cred:specialist", true),
159        ]);
160        assert_eq!(
161            assess(&d, &conduct(&[], true, true)),
162            InquiryVerdict::Negligent
163        );
164        // Even checking ONE of two accessible means, if the unchecked one was material and harm followed:
165        assert_eq!(
166            assess(&d, &conduct(&["cred:specialist"], true, true)),
167            InquiryVerdict::Negligent
168        );
169    }
170
171    #[test]
172    fn unchecked_but_no_harm_is_a_shortfall_not_inflated_to_negligence() {
173        // Accessible means unchecked, but no harmful act followed — honestly a gap, not (yet) negligence.
174        let d = duty(vec![means("cred:specialist", true)]);
175        assert_eq!(
176            assess(&d, &conduct(&[], false, false)),
177            InquiryVerdict::UncheckedNoHarm
178        );
179        // Acted, but no further injury → still not negligence.
180        assert_eq!(
181            assess(&d, &conduct(&[], true, false)),
182            InquiryVerdict::UncheckedNoHarm
183        );
184    }
185
186    #[test]
187    fn serde_round_trips() {
188        let d = duty(vec![means("m", true)]);
189        let back: DutyOfInquiry =
190            serde_json::from_str(&serde_json::to_string(&d).unwrap()).unwrap();
191        assert_eq!(d, back);
192        assert_eq!(
193            serde_json::from_str::<InquiryVerdict>(
194                &serde_json::to_string(&InquiryVerdict::Negligent).unwrap()
195            )
196            .unwrap(),
197            InquiryVerdict::Negligent
198        );
199    }
200}