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}