Skip to main content

qualia_client_core/wellfair/api/
mod.rs

1use super::policy::PolicyDecisionService;
2use super::vault::VaultService;
3use ed25519_dalek::SigningKey;
4use wellfare_core::projects::Contribution;
5
6const QAPP_SHELL: &str = "wellfair-shell";
7const QAPP_LIFE: &str = "wellfair-life";
8const QAPP_WELLBEING: &str = "wellfair-wellbeing";
9const QAPP_FINANCE: &str = "wellfair-finance";
10const QAPP_PROJECTS: &str = "wellfair-projects";
11const QAPP_CREDENTIALS: &str = "wellfair-credentials";
12const QAPP_CLINICAL: &str = "wellfair-clinical";
13const QAPP_WELFARE: &str = "wellfair-welfare";
14const SOURCE_PERSONAL: &str = "wellfair:personal";
15const SOURCE_LIFE: &str = "wellfair:life";
16const SOURCE_WELLBEING: &str = "wellfair:wellbeing";
17const SOURCE_FINANCE: &str = "wellfair:finance";
18const SOURCE_PROJECTS: &str = "wellfair:projects";
19const SOURCE_CREDENTIALS: &str = "wellfair:credentials";
20const SOURCE_CLINICAL: &str = "wellfair:clinical";
21const SOURCE_WELFARE: &str = "wellfair:welfare";
22const QAPP_COOPERATIVE: &str = "qualia-cooperative";
23const SOURCE_COOPERATIVE: &str = "qualia:cooperative";
24const QAPP_GUARDIANSHIP: &str = "wellfair-guardianship";
25const SOURCE_GUARDIANSHIP: &str = "wellfair:guardianship";
26
27/// Reconstruct a `Contribution` from a stored/transmitted summary JSON. The record id (which
28/// is the dedup anchor for obligation derivation) is supplied by the caller — the journal row
29/// id locally, or the sync operation's `record_id` for an inbound op.
30fn contribution_from_summary(
31    id: String,
32    summary: &str,
33    occurred_at_unix: u32,
34) -> Option<Contribution> {
35    let v: serde_json::Value = serde_json::from_str(summary).ok()?;
36    Some(Contribution {
37        id,
38        project_id: v
39            .get("project_id")
40            .and_then(|x| x.as_str())
41            .unwrap_or_default()
42            .to_string(),
43        contributor_did: v
44            .get("contributor_did")
45            .and_then(|x| x.as_str())
46            .unwrap_or_default()
47            .to_string(),
48        description: String::new(),
49        effort_minutes: v
50            .get("effort_minutes")
51            .and_then(|x| x.as_u64())
52            .unwrap_or(0) as u32,
53        capital_cents: v.get("capital_cents").and_then(|x| x.as_u64()).unwrap_or(0),
54        roi_multiplier: v
55            .get("roi_multiplier")
56            .and_then(|x| x.as_f64())
57            .map(|f| f as f32)
58            .unwrap_or(1.0),
59        privacy_level: Default::default(),
60        occurred_at_unix,
61        predecessor_id: None,
62    })
63}
64
65/// A per-entry summary of a hypermedia library entry for the UI (drops the raw quins).
66pub(crate) fn library_summary(e: &super::hypermedia_store::LibraryEntry) -> serde_json::Value {
67    serde_json::json!({
68        "asset_uri": e.asset_uri,
69        "media_type": e.media_type,
70        "topics": e.topics,
71        "projects": e.projects,
72        "purposes": e.purposes,
73        "place": e.place,
74        "occurred_at": e.occurred_at,
75        "lat": e.lat,
76        "lon": e.lon,
77        "flags": e.flags,
78        "ingested_unix": e.ingested_unix,
79        "excerpt": e.excerpt,
80        "sensitivity": e.sensitivity,
81        "section": e.section,
82        "commons_visibility": e.commons_visibility,
83        "is_secret": e.is_secret(),
84        "cml_signals": e.cml_signals,
85        "cml_concept_count": e.cml_concept_count,
86        "cml_n3_chars": e.cml_n3.len(),
87        "quin_count": e.quins.len(),
88        "cof_segment_count": e.cof_segment_count,
89        "cof_segment_index": e.cof_segment_index,
90        "cof_profile": e.cof_profile,
91        "cof_html_chars": e.cof_html.len(),
92        "has_cof": !e.cof_html.is_empty(),
93    })
94}
95
96/// Facets a **person** attaches to an asset at ingest — the "software provides the means, the person
97/// authors the meaning" path. These merge *on top of* whatever a processor derived automatically (a photo's
98/// EXIF still wins for its own time/place); they let a plain document be placed on the **timeline** (a date)
99/// or the **map** (coordinates), or collected under a **project** / **purpose** — none of it imposed.
100#[derive(Debug, Clone, Default)]
101pub struct ManualFacets {
102    pub occurred_at: Option<i64>,
103    pub place_label: Option<String>,
104    pub lat: Option<f32>,
105    pub lon: Option<f32>,
106    pub projects: Vec<String>,
107    pub purposes: Vec<String>,
108    /// `public` | `restricted` | `classified` — high sensitivity forces Secret section.
109    pub sensitivity: Option<String>,
110    /// Preferred product section: secret | wellfair | personal | work | tools | software | commons.
111    pub section: Option<String>,
112    /// `none` | `peers` | `commons` — social / micro-commons visibility.
113    pub commons_visibility: Option<String>,
114}
115
116impl ManualFacets {
117    fn is_empty(&self) -> bool {
118        self.occurred_at.is_none()
119            && self.place_label.is_none()
120            && self.lat.is_none()
121            && self.projects.is_empty()
122            && self.purposes.is_empty()
123            && self.sensitivity.is_none()
124            && self.section.is_none()
125            && self.commons_visibility.is_none()
126    }
127}
128
129/// Decode a lowercase/uppercase hex string to bytes (the desktop passes binary assets — a JPEG is not utf-8 —
130/// as hex across the command boundary). Dependency-free; rejects odd length / non-hex.
131fn decode_hex(s: &str) -> Result<Vec<u8>, String> {
132    let s = s.trim();
133    if s.len() % 2 != 0 {
134        return Err("odd-length hex".to_string());
135    }
136    let val = |c: u8| -> Result<u8, String> {
137        match c {
138            b'0'..=b'9' => Ok(c - b'0'),
139            b'a'..=b'f' => Ok(c - b'a' + 10),
140            b'A'..=b'F' => Ok(c - b'A' + 10),
141            _ => Err(format!("non-hex byte {:#x}", c)),
142        }
143    };
144    let b = s.as_bytes();
145    let mut out = Vec::with_capacity(b.len() / 2);
146    let mut i = 0;
147    while i < b.len() {
148        out.push((val(b[i])? << 4) | val(b[i + 1])?);
149        i += 2;
150    }
151    Ok(out)
152}
153
154/// Parse a model string (`"male"` / `"female"`, case-insensitive) into an [`AnatomyModel`].
155pub fn parse_anatomy_model(s: &str) -> Result<wellfare_core::anatomy::AnatomyModel, String> {
156    match s.trim().to_ascii_lowercase().as_str() {
157        "male" | "m" | "xy" => Ok(wellfare_core::anatomy::AnatomyModel::Male),
158        "female" | "f" | "xx" => Ok(wellfare_core::anatomy::AnatomyModel::Female),
159        _ => Err(format!(
160            "unknown anatomy model '{s}' (expected male/female)"
161        )),
162    }
163}
164
165/// Transport-neutral Host API exported for UI and qApps.
166pub struct WebizenHostApi {
167    vault: VaultService,
168    policy: PolicyDecisionService,
169    signing_key: SigningKey,
170    owner_did: String,
171    author_did: String,
172    storage_root: std::path::PathBuf,
173}
174
175mod accountability;
176mod anatomy;
177mod host_core;
178mod library;
179/// Vault-free hypermedia library reads (storage path; no Sanctuary HostApi required).
180pub use library::{
181    library_stats_at, list_library_section_at, query_library_faceted_at, search_library_at,
182    search_library_text_at, search_library_time_at,
183};
184mod agency;
185mod backup_clinical;
186mod coop;
187mod disclosure;
188mod encryption;
189mod guardianship;
190mod pwa;
191mod sanctuary_vault;
192mod sync;
193mod types;
194mod welfare_work;
195
196pub use types::*;