qualia_core_db/modalities/capacity.rs
1//! Juridical capacity & state-transition (§18, legal_logic.md).
2//!
3//! Obligations and stipulations are only valid if the asserting agent had the legal/cognitive
4//! capacity to form them. This module is a **conservative** engine over the *existing*
5//! guardianship ontology vocabulary (`values:juridicalCapacity`, `CoercedConsentFlag`,
6//! `VoidableStipulation`, `values:guardian`, `survivesDeath`) — it wires terms Timothy already
7//! coined; it does not invent new sensitive vocabulary.
8//!
9//! Handled with gravity, two deliberate semantic choices (both from the spec, both the
10//! legally-careful reading — flagged here, not silently assumed):
11//! * **Duress → VOIDABLE, not void.** A stipulation made under coercion is voidable *at the
12//! victim's election* — it is NOT automatically nullified. Auto-nullifying would strip the
13//! victim of the choice to keep or undo it. (`◇Void`, not `Void`.)
14//! * **Guardianship carries the dependent's weight, it does not replace the dependent.** A
15//! guardian acts *on behalf of* the dependent; the legal weight is the dependent's.
16
17/// An agent's juridical capacity for an act.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum CapacityStatus {
20 /// Full legal/cognitive capacity — stipulations are binding.
21 #[default]
22 Intact,
23 /// Capacity is impaired (e.g. minority, incapacity) — stipulations are not binding.
24 Impaired,
25 /// Capacity present but the act was coerced — the stipulation is *voidable*.
26 UnderDuress,
27}
28
29/// A stipulation by the agent is **binding** only when capacity is `Intact`.
30#[inline]
31pub fn stipulation_binding(capacity: CapacityStatus) -> bool {
32 matches!(capacity, CapacityStatus::Intact)
33}
34
35/// A stipulation made `UnderDuress` is **voidable at the victim's election** (CoercedConsentFlag
36/// → VoidableStipulation). Returns true iff the agent may elect to void it — NOT that it is
37/// already void (that choice stays with the victim).
38#[inline]
39pub fn stipulation_voidable(capacity: CapacityStatus) -> bool {
40 matches!(capacity, CapacityStatus::UnderDuress)
41}
42
43/// Guardianship / delegation: when a guardianship relation holds, a guardian's act carries the
44/// **dependent's** legal weight (`values:actsOnBehalfOf`). Returns the identity whose weight
45/// the act bears — the dependent under guardianship, else the actor themselves.
46#[inline]
47pub fn effective_principal(actor: u64, dependent: u64, has_guardianship: bool) -> u64 {
48 if has_guardianship {
49 dependent
50 } else {
51 actor
52 }
53}
54
55/// Posthumous standing: a representative may prosecute the **surviving** claims of a deceased
56/// agent (`BreachRecord(survivesDeath)` + representative standing). True iff the agent is
57/// deceased AND a representative stands for them.
58#[inline]
59pub fn posthumous_standing(deceased: bool, has_representative: bool) -> bool {
60 deceased && has_representative
61}
62
63// ─── Jurisdiction-specific capacity thresholds ──────────────────────────────────────
64
65/// Does `age_years` meet a jurisdiction's `majority_age`? The threshold is **supplied by the
66/// caller** (jurisdiction-specific — 18 in most, 21 in some, mental-health-act variations) so
67/// the engine never bakes one jurisdiction's law in as universal.
68#[inline]
69pub fn meets_age_of_majority(age_years: u32, majority_age: u32) -> bool {
70 age_years >= majority_age
71}
72
73/// Derive capacity from age against a jurisdiction threshold: below majority → `Impaired`
74/// (minority); at/above → `Intact`. Coercion is layered separately ([`capacity_under_pressure`]).
75pub fn capacity_from_age(age_years: u32, majority_age: u32) -> CapacityStatus {
76 if meets_age_of_majority(age_years, majority_age) {
77 CapacityStatus::Intact
78 } else {
79 CapacityStatus::Impaired
80 }
81}
82
83// ─── Coercion / duress detection (relational imbalance → voidable) ──────────────────
84
85/// Map a relational power-imbalance signal to a duress finding. `imbalance` is a normalised
86/// `[0,1]` measure of relational asymmetry (dependency / authority / economic capture); an
87/// `explicit_threat` forces duress regardless. At/above `threshold`, or under an explicit
88/// threat, the act is coerced → the resulting stipulation is *voidable* (never auto-void; the
89/// election stays with the victim — see module header).
90pub fn detect_duress(imbalance: f32, explicit_threat: bool, threshold: f32) -> bool {
91 explicit_threat || imbalance >= threshold
92}
93
94/// Capacity under relational pressure: an otherwise-`Intact` agent whose act is coerced becomes
95/// `UnderDuress` (voidable). Duress never upgrades an already-`Impaired` capacity.
96pub fn capacity_under_pressure(
97 base: CapacityStatus,
98 imbalance: f32,
99 explicit_threat: bool,
100 threshold: f32,
101) -> CapacityStatus {
102 if base == CapacityStatus::Intact && detect_duress(imbalance, explicit_threat, threshold) {
103 CapacityStatus::UnderDuress
104 } else {
105 base
106 }
107}
108
109// ─── Temporary impairment with time-decay (e.g. intoxication clearance) ─────────────
110
111/// A transient impairment decaying linearly toward zero — a conservative clearance model
112/// (e.g. intoxication). `initial` is the level in `[0,1]` at t0; `rate` is decay per elapsed
113/// time unit. Returns the clamped residual level at `elapsed` units.
114pub fn decayed_impairment(initial: f32, elapsed: f32, rate: f32) -> f32 {
115 (initial - rate * elapsed).clamp(0.0, 1.0)
116}
117
118/// Capacity under a *transient* impairment: while the decayed level is at/above `threshold` the
119/// agent is `Impaired`; once it decays below, capacity self-clears to `Intact`. Distinct from
120/// durable incapacity (which does not decay).
121pub fn transient_capacity(initial: f32, elapsed: f32, rate: f32, threshold: f32) -> CapacityStatus {
122 if decayed_impairment(initial, elapsed, rate) >= threshold {
123 CapacityStatus::Impaired
124 } else {
125 CapacityStatus::Intact
126 }
127}
128
129// ─── Selective right-delegation (guardianship mechanism) ────────────────────────────
130//
131// NOTE: the *vocabulary* of guardianship domains (the 17+ domains of agency in Timothy's
132// CopyOfGuardianShipRelations design) is his to coin. This module deliberately operates over
133// OPAQUE caller-supplied domain identifiers (u64 hashes) — wiring the *mechanism* of selective
134// delegation without inventing sensitive guardianship vocabulary.
135
136/// Selective delegation: `authorized_domains` enumerate exactly the domains of agency delegated
137/// to a guardian. The guardian may act in `requested_domain` iff it is among them — no domain
138/// ⇒ no authority (selective, never plenary).
139pub fn guardianship_authorized(authorized_domains: &[u64], requested_domain: u64) -> bool {
140 authorized_domains.contains(&requested_domain)
141}
142
143/// Domain-scoped effective principal: a guardian's act carries the dependent's weight ONLY
144/// within a delegated domain; outside the delegated set the guardian cannot bind the dependent
145/// (the act falls back to the actor's own weight).
146pub fn effective_principal_scoped(
147 actor: u64,
148 dependent: u64,
149 authorized_domains: &[u64],
150 requested_domain: u64,
151) -> u64 {
152 if guardianship_authorized(authorized_domains, requested_domain) {
153 dependent
154 } else {
155 actor
156 }
157}
158
159// ─── Delegation chains: attenuation + cascading revocation (ZCAP/Macaroon-style) ────
160//
161// Still mechanism-only over opaque domain ids — no guardianship vocabulary coined here. A
162// delegatee can never gain MORE authority than the delegator (attenuation), and revoking a
163// domain withdraws it immediately wherever it appears (cascading revocation).
164
165/// **Attenuation:** a sub-delegation's `child_domains` are valid only if a SUBSET of the
166/// delegator's `parent_domains` — a delegatee never receives more authority than the delegator
167/// holds. (Empty child set trivially attenuates.)
168pub fn delegation_attenuates(parent_domains: &[u64], child_domains: &[u64]) -> bool {
169 child_domains.iter().all(|d| parent_domains.contains(d))
170}
171
172/// Authority after **cascading revocation**: a guardian may act in `requested_domain` iff it is
173/// authorized AND not present in the `revoked_domains` set (revocation withdraws it immediately).
174pub fn authorized_after_revocation(authorized: &[u64], revoked: &[u64], requested: u64) -> bool {
175 guardianship_authorized(authorized, requested) && !revoked.contains(&requested)
176}
177
178/// A multi-link delegation **chain** authorizes `requested_domain` iff: the root holds it, every
179/// link attenuates its predecessor (subset), and the domain survives at every level (no link
180/// silently re-broadens authority). `chain[0]` is the root delegation; each later link is a
181/// sub-delegation. Zero-heap (slice of slices).
182pub fn chain_authorizes(chain: &[&[u64]], requested_domain: u64) -> bool {
183 if chain.is_empty() || !chain[0].contains(&requested_domain) {
184 return false;
185 }
186 for w in chain.windows(2) {
187 if !delegation_attenuates(w[0], w[1]) || !w[1].contains(&requested_domain) {
188 return false;
189 }
190 }
191 true
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn capacity_gates_binding() {
200 assert!(stipulation_binding(CapacityStatus::Intact));
201 assert!(!stipulation_binding(CapacityStatus::Impaired));
202 assert!(!stipulation_binding(CapacityStatus::UnderDuress));
203 }
204
205 #[test]
206 fn duress_is_voidable_not_void() {
207 // Under duress: the victim MAY void — not auto-nullified, and not binding either.
208 assert!(stipulation_voidable(CapacityStatus::UnderDuress));
209 assert!(!stipulation_binding(CapacityStatus::UnderDuress));
210 // Intact / impaired are not "voidable for duress".
211 assert!(!stipulation_voidable(CapacityStatus::Intact));
212 assert!(!stipulation_voidable(CapacityStatus::Impaired));
213 }
214
215 #[test]
216 fn guardianship_carries_the_dependents_weight() {
217 let guardian = 0xA1;
218 let dependent = 0xB2;
219 assert_eq!(effective_principal(guardian, dependent, true), dependent);
220 assert_eq!(effective_principal(guardian, dependent, false), guardian);
221 }
222
223 #[test]
224 fn posthumous_claims_need_a_representative() {
225 assert!(posthumous_standing(true, true));
226 assert!(!posthumous_standing(true, false)); // deceased, no representative → no standing
227 assert!(!posthumous_standing(false, true)); // alive → they hold their own standing
228 }
229
230 #[test]
231 fn age_of_majority_is_jurisdiction_parametric() {
232 // 18-majority jurisdiction.
233 assert_eq!(capacity_from_age(17, 18), CapacityStatus::Impaired);
234 assert_eq!(capacity_from_age(18, 18), CapacityStatus::Intact);
235 // 21-majority jurisdiction: the same 18-year-old is a minor.
236 assert_eq!(capacity_from_age(18, 21), CapacityStatus::Impaired);
237 assert!(!stipulation_binding(capacity_from_age(17, 18)));
238 }
239
240 #[test]
241 fn relational_imbalance_and_threats_yield_voidable_duress() {
242 // High relational imbalance → duress → voidable (not auto-void, not binding).
243 let c = capacity_under_pressure(CapacityStatus::Intact, 0.9, false, 0.7);
244 assert_eq!(c, CapacityStatus::UnderDuress);
245 assert!(stipulation_voidable(c));
246 assert!(!stipulation_binding(c));
247 // An explicit threat forces duress regardless of measured imbalance.
248 assert!(detect_duress(0.0, true, 0.7));
249 // Below threshold, no threat → capacity unchanged.
250 assert_eq!(
251 capacity_under_pressure(CapacityStatus::Intact, 0.3, false, 0.7),
252 CapacityStatus::Intact
253 );
254 // Duress never "upgrades" an already-impaired (minor) agent.
255 assert_eq!(
256 capacity_under_pressure(CapacityStatus::Impaired, 0.9, true, 0.7),
257 CapacityStatus::Impaired
258 );
259 }
260
261 #[test]
262 fn transient_impairment_decays_and_self_clears() {
263 // Fully impaired at t0, decaying 0.1/unit, threshold 0.5.
264 assert!(decayed_impairment(1.0, 0.0, 0.1) > 0.99);
265 assert_eq!(
266 transient_capacity(1.0, 0.0, 0.1, 0.5),
267 CapacityStatus::Impaired
268 );
269 // After 6 units → level 0.4 < 0.5 → self-cleared.
270 assert!((decayed_impairment(1.0, 6.0, 0.1) - 0.4).abs() < 1e-6);
271 assert_eq!(
272 transient_capacity(1.0, 6.0, 0.1, 0.5),
273 CapacityStatus::Intact
274 );
275 // Never goes negative.
276 assert_eq!(decayed_impairment(0.2, 100.0, 0.1), 0.0);
277 }
278
279 #[test]
280 fn guardianship_delegation_is_selective_not_plenary() {
281 let (guardian, dependent) = (0xA1u64, 0xB2u64);
282 let medical = crate::q_hash("domain:medical");
283 let financial = crate::q_hash("domain:financial");
284 let legal = crate::q_hash("domain:legal");
285 let delegated = [medical, financial]; // legal NOT delegated
286
287 assert!(guardianship_authorized(&delegated, medical));
288 assert!(!guardianship_authorized(&delegated, legal));
289 // In a delegated domain the guardian carries the dependent's weight…
290 assert_eq!(
291 effective_principal_scoped(guardian, dependent, &delegated, financial),
292 dependent
293 );
294 // …but outside the delegated set they cannot bind the dependent.
295 assert_eq!(
296 effective_principal_scoped(guardian, dependent, &delegated, legal),
297 guardian
298 );
299 }
300
301 #[test]
302 fn delegation_attenuates_revokes_and_chains() {
303 let medical = crate::q_hash("domain:medical");
304 let financial = crate::q_hash("domain:financial");
305 let legal = crate::q_hash("domain:legal");
306
307 // Attenuation: a sub-delegation must be a subset of the parent's authority.
308 assert!(delegation_attenuates(
309 &[medical, financial, legal],
310 &[medical, financial]
311 ));
312 assert!(
313 !delegation_attenuates(&[medical], &[medical, legal]),
314 "cannot broaden authority"
315 );
316 assert!(delegation_attenuates(&[medical], &[]));
317
318 // Cascading revocation withdraws a domain immediately.
319 let authorized = [medical, financial];
320 assert!(authorized_after_revocation(&authorized, &[], medical));
321 assert!(
322 !authorized_after_revocation(&authorized, &[medical], medical),
323 "revoked → withdrawn"
324 );
325 assert!(authorized_after_revocation(
326 &authorized,
327 &[medical],
328 financial
329 ));
330
331 // A delegation chain: root{med,fin,legal} → sub{med,fin} → subsub{med}.
332 let root: &[u64] = &[medical, financial, legal];
333 let sub: &[u64] = &[medical, financial];
334 let subsub: &[u64] = &[medical];
335 let chain = [root, sub, subsub];
336 assert!(
337 chain_authorizes(&chain, medical),
338 "medical survives the whole chain"
339 );
340 assert!(
341 !chain_authorizes(&chain, financial),
342 "financial dropped at the last link"
343 );
344 assert!(
345 !chain_authorizes(&chain, legal),
346 "legal dropped after the root"
347 );
348 // A chain that tries to RE-BROADEN (sub adds legal the parent lacks) fails attenuation.
349 let bad_sub: &[u64] = &[medical, legal];
350 assert!(!chain_authorizes(&[sub, bad_sub], legal));
351 assert!(!chain_authorizes(&[], medical));
352 }
353}