qualia_client_core/wellfair/
export_package.rs1use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use super::journal::JournalEntry;
7use super::receipt::ReceiptRecord;
8
9pub const EXPORT_FORMAT_VERSION: u32 = 1;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct HealthExportManifestEntry {
13 pub id: String,
14 pub kind: String,
15 pub evidence_type: String,
16 pub sensitivity: String,
17 pub asserted_time_unix: u32,
18 pub assurance: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct HealthExportPackage {
23 pub format_version: u32,
24 pub exported_at_unix: u32,
25 pub record_count: u32,
26 pub content_sha256_hex: String,
27 pub checkpoint_hash: Option<String>,
28 pub turtle_body: String,
29 pub manifest: Vec<HealthExportManifestEntry>,
30}
31
32fn assurance_label(evidence_type: &str, kind: &str) -> &'static str {
33 if kind == "disputed_diagnosis" {
34 "disputed_self_reported_restricted"
35 } else if kind == "housing_safety" {
36 "safety_context_restricted"
37 } else if kind == "life_event" || kind == "case_task" {
38 "life_event_restricted"
39 } else if kind == "welfare_case" {
40 "welfare_case_sanctuary"
41 } else if kind == "wellbeing_observation" {
42 "wellbeing_self_report"
43 } else if kind == "therapy_note" || kind == "sanctuary_note" {
44 "sanctuary_classified"
45 } else if evidence_type.contains("SelfReported") {
46 "self_reported_restricted"
47 } else if evidence_type.contains("DeviceMeasured") {
48 "device_measured_restricted"
49 } else {
50 "asserted_restricted"
51 }
52}
53
54pub fn build_export_package(
57 entries: &[JournalEntry],
58 exported_at_unix: u32,
59 checkpoint_hash: Option<[u8; 32]>,
60) -> HealthExportPackage {
61 let mut turtle = wellfare_core::rdf::generate_rdf_prefixes();
62 let mut manifest = Vec::with_capacity(entries.len());
63
64 for entry in entries {
65 let subj = format!("<{}>", entry.id);
66 turtle.push_str(&format!("{subj} a wf:HealthRecord , fhir:Observation ;\n"));
67 turtle.push_str(&format!(" wf:kind {:?} ;\n", entry.kind));
68 turtle.push_str(&format!(
69 " wf:evidenceType {:?} ;\n",
70 entry.evidence_type
71 ));
72 turtle.push_str(&format!(" wf:sensitivity {:?} ;\n", entry.sensitivity));
73 turtle.push_str(&format!(
74 " fhir:Observation.effectiveDateTime \"{}\"^^xsd:unsignedInt ;\n",
75 entry.asserted_time_unix
76 ));
77 if let Some(ref summary) = entry.summary {
78 turtle.push_str(&format!(" schema:description {:?} ;\n", summary));
79 }
80 if let Some(ref blob) = entry.blob_hash {
81 turtle.push_str(&format!(" wf:blobHash {:?} ;\n", blob));
82 }
83 turtle.push_str(" prov:wasGeneratedBy <urn:wellfair:agent:vault> .\n\n");
84
85 manifest.push(HealthExportManifestEntry {
86 id: entry.id.clone(),
87 kind: entry.kind.clone(),
88 evidence_type: entry.evidence_type.clone(),
89 sensitivity: entry.sensitivity.clone(),
90 asserted_time_unix: entry.asserted_time_unix,
91 assurance: assurance_label(&entry.evidence_type, &entry.kind).to_string(),
92 });
93 }
94
95 let content_sha256_hex = hex::encode(Sha256::digest(turtle.as_bytes()).as_slice());
96 HealthExportPackage {
97 format_version: EXPORT_FORMAT_VERSION,
98 exported_at_unix,
99 record_count: entries.len() as u32,
100 content_sha256_hex,
101 checkpoint_hash: checkpoint_hash.map(|h| hex::encode(h)),
102 turtle_body: turtle,
103 manifest,
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub struct ExportReceipt {
109 pub export_sha256_hex: String,
110 pub record_count: u32,
111 pub checkpoint_hash: Option<String>,
112 pub exported_at_unix: u32,
113}
114
115impl ExportReceipt {
116 pub fn from_package(pkg: &HealthExportPackage) -> Self {
117 Self {
118 export_sha256_hex: pkg.content_sha256_hex.clone(),
119 record_count: pkg.record_count,
120 checkpoint_hash: pkg.checkpoint_hash.clone(),
121 exported_at_unix: pkg.exported_at_unix,
122 }
123 }
124}
125
126pub fn export_policy_receipt(pkg: &HealthExportPackage, timestamp_unix: u32) -> ReceiptRecord {
127 ReceiptRecord {
128 id: format!(
129 "export-{}",
130 pkg.content_sha256_hex.get(..8).unwrap_or("00000000")
131 ),
132 timestamp_unix,
133 qapp_id: "wellfair-shell".into(),
134 record_id: format!("urn:wellfair:export:{}", pkg.exported_at_unix),
135 decision: "Permit".into(),
136 obligations: vec![
137 "standards_readable_export".into(),
138 "typed_assurance_manifest".into(),
139 ],
140 checkpoint_hash: pkg.checkpoint_hash.clone(),
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn sample_entry(id: &str, kind: &str) -> JournalEntry {
149 JournalEntry {
150 id: id.into(),
151 kind: kind.into(),
152 asserted_time_unix: 1_700_000_000,
153 evidence_type: "DeviceMeasured".into(),
154 sensitivity: "Restricted".into(),
155 blob_hash: Some("abc".into()),
156 source: "test".into(),
157 committed_unix: 1_700_000_100,
158 summary: Some(r#"{"weight":72.0}"#.into()),
159 }
160 }
161
162 #[test]
163 fn export_package_has_turtle_and_manifest() {
164 let entries = vec![
165 sample_entry("urn:wellfair:weight:w1", "weight"),
166 sample_entry("urn:wellfair:condition:c1", "condition"),
167 ];
168 let pkg = build_export_package(&entries, 1_700_000_200, Some([7u8; 32]));
169 assert_eq!(pkg.record_count, 2);
170 assert!(pkg.turtle_body.contains("@prefix wf:"));
171 assert!(pkg.turtle_body.contains("urn:wellfair:weight:w1"));
172 assert_eq!(pkg.manifest.len(), 2);
173 assert_eq!(pkg.manifest[0].assurance, "device_measured_restricted");
174 assert!(!pkg.content_sha256_hex.is_empty());
175 assert!(pkg.checkpoint_hash.is_some());
176 }
177
178 #[test]
179 fn export_receipt_binds_checkpoint() {
180 let entries = vec![sample_entry("urn:wellfair:sleep:s1", "sleep")];
181 let pkg = build_export_package(&entries, 99, None);
182 let receipt = export_policy_receipt(&pkg, 99);
183 assert_eq!(receipt.decision, "Permit");
184 assert!(receipt
185 .obligations
186 .contains(&"standards_readable_export".into()));
187 }
188}