Skip to main content

qualia_client_core/wellfair/
ingest_guardian.rs

1//! **Guardian notification on flagged ingest** — the guardianship hook of the hypermedia semantic library
2//! (Timothy, 2026-07-07: *"if there's a guardianship relation, it might notify the guardian if the content
3//! raises particular flags"*).
4//!
5//! When an asset is ingested, a processor may raise [`Flag`]s (a semantic descriptor bound to the asset — see
6//! `qualia_core_db::hypermedia`). If the **principal is under a guardianship relation**, this layer turns the
7//! flags at or above a chosen severity into [`GuardianNotification`]s **and records each in the tamper-evident
8//! accountability ledger** — so a flagged ingest is both a notification to the guardian *and* an auditable,
9//! un-erasable event (who was notified, about what, when). The "is the principal under guardianship + who is
10//! the guardian" lookup is the guardianship / care-relationship layer's job; this takes the resolved guardian
11//! and does the honest, recordable thing with the flags.
12
13use qualia_core_db::hypermedia::{Flag, FlagSeverity};
14use serde::{Deserialize, Serialize};
15
16/// A notification to a guardian that a flagged asset was ingested for a principal under their guardianship.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct GuardianNotification {
19    pub guardian_did: String,
20    pub principal_did: String,
21    pub asset_uri: String,
22    pub flag_kind: String,
23    /// 0 Info · 1 Notice · 2 Concern · 3 Urgent.
24    pub severity_level: u64,
25    pub detail: String,
26    pub time_unix: u64,
27}
28
29/// Produce guardian notifications for the flags that meet or exceed `min_severity`. Pure — the caller (host)
30/// records each to the ledger and delivers it. A principal *not* under guardianship yields none (the caller
31/// passes no guardian).
32pub fn guardian_notifications(
33    flags: &[Flag],
34    asset_uri: &str,
35    guardian_did: &str,
36    principal_did: &str,
37    min_severity: FlagSeverity,
38    now_unix: u64,
39) -> Vec<GuardianNotification> {
40    flags
41        .iter()
42        .filter(|f| f.severity.level() >= min_severity.level())
43        .map(|f| GuardianNotification {
44            guardian_did: guardian_did.to_string(),
45            principal_did: principal_did.to_string(),
46            asset_uri: asset_uri.to_string(),
47            flag_kind: f.kind.clone(),
48            severity_level: f.severity.level(),
49            detail: f.detail.clone(),
50            time_unix: now_unix,
51        })
52        .collect()
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn only_flags_at_or_above_the_threshold_notify_the_guardian() {
61        let flags = vec![
62            Flag {
63                kind: "sensitive-medical".into(),
64                severity: FlagSeverity::Concern,
65                detail: "x-ray".into(),
66            },
67            Flag {
68                kind: "minor-note".into(),
69                severity: FlagSeverity::Info,
70                detail: String::new(),
71            },
72        ];
73        let ns = guardian_notifications(
74            &flags,
75            "urn:doc:scan",
76            "did:wf:guardian",
77            "did:wf:child",
78            FlagSeverity::Notice,
79            1_000,
80        );
81        // The Concern flag (level 2 ≥ Notice level 1) notifies; the Info flag (0) does not.
82        assert_eq!(ns.len(), 1);
83        assert_eq!(ns[0].flag_kind, "sensitive-medical");
84        assert_eq!(ns[0].guardian_did, "did:wf:guardian");
85        assert_eq!(ns[0].principal_did, "did:wf:child");
86        assert_eq!(ns[0].severity_level, 2);
87    }
88
89    #[test]
90    fn urgent_only_threshold_filters_out_concern() {
91        let flags = vec![Flag {
92            kind: "x".into(),
93            severity: FlagSeverity::Concern,
94            detail: String::new(),
95        }];
96        assert!(guardian_notifications(&flags, "u", "g", "p", FlagSeverity::Urgent, 0).is_empty());
97    }
98}