Skip to main content

qualia_client_core/
bundled_ontologies.rs

1//! Seed and resolve ontology sources bundled with the desktop app.
2//!
3//! These sources provide an offline fallback for essential ontologies whose
4//! catalog URLs may be unavailable at runtime. They are also seeded into
5//! `{storage}/Index/` on startup so readiness checks can treat them as present.
6//!
7//! **Solid stack** (from Timothy's W3C ns archive + Solid Terms vocab):
8//! `ldp`, `acl`, `solid-terms`, `solid-oidc`, `pim-space`, `foaf` under
9//! `bundled/ontologies/w3c-archives/`.
10
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use crate::resource_import;
15
16struct BundledOntologySpec {
17    id: &'static str,
18    rel_path: &'static str,
19}
20
21const BUNDLED_ONTOLOGIES: &[BundledOntologySpec] = &[
22    BundledOntologySpec {
23        id: "shacl",
24        rel_path: "bundled/ontologies/shacl.ttl",
25    },
26    // Solid-OutPost / personal pod essentials (w3c-ns archive)
27    BundledOntologySpec {
28        id: "ldp",
29        rel_path: "bundled/ontologies/w3c-archives/ldp.ttl",
30    },
31    BundledOntologySpec {
32        id: "acl",
33        rel_path: "bundled/ontologies/w3c-archives/auth-acl.ttl",
34    },
35    BundledOntologySpec {
36        id: "solid-terms",
37        rel_path: "bundled/ontologies/w3c-archives/solid-terms.ttl",
38    },
39    BundledOntologySpec {
40        id: "solid-oidc",
41        rel_path: "bundled/ontologies/w3c-archives/solid-oidc.ttl",
42    },
43    BundledOntologySpec {
44        id: "pim-space",
45        rel_path: "bundled/ontologies/w3c-archives/pim-space.ttl",
46    },
47    BundledOntologySpec {
48        id: "foaf",
49        rel_path: "bundled/ontologies/w3c-archives/foaf.ttl",
50    },
51    // Core W3C + perception-adjacent (offline Index seed)
52    BundledOntologySpec {
53        id: "prov",
54        rel_path: "bundled/ontologies/w3c/prov.ttl",
55    },
56    BundledOntologySpec {
57        id: "skos",
58        rel_path: "bundled/ontologies/w3c/skos.ttl",
59    },
60    BundledOntologySpec {
61        id: "owl",
62        rel_path: "bundled/ontologies/w3c/owl.ttl",
63    },
64    BundledOntologySpec {
65        id: "rdfs",
66        rel_path: "bundled/ontologies/w3c/rdfs.ttl",
67    },
68    BundledOntologySpec {
69        id: "time",
70        rel_path: "bundled/ontologies/w3c/time.ttl",
71    },
72    BundledOntologySpec {
73        id: "sosa",
74        rel_path: "bundled/ontologies/w3c/sosa.ttl",
75    },
76    BundledOntologySpec {
77        id: "music",
78        rel_path: "bundled/ontologies/purl/music.ttl",
79    },
80    BundledOntologySpec {
81        id: "consent",
82        rel_path: "bundled/ontologies/purl/consent.ttl",
83    },
84];
85
86/// Ontologies seeded into local storage when absent.
87///
88/// SHACL + Solid stack + core W3C vocabularies used by perception / provenance paths.
89pub const DEFAULT_BUNDLED_ONTOLOGIES: &[&str] = &[
90    "shacl",
91    "ldp",
92    "acl",
93    "solid-terms",
94    "solid-oidc",
95    "pim-space",
96    "foaf",
97    "prov",
98    "skos",
99    "owl",
100    "rdfs",
101    "time",
102    "sosa",
103    "music",
104    "consent",
105];
106
107fn exe_dir() -> Option<PathBuf> {
108    std::env::current_exe()
109        .ok()
110        .and_then(|p| p.parent().map(|d| d.to_path_buf()))
111}
112
113fn ontology_spec(id: &str) -> Option<&'static BundledOntologySpec> {
114    BUNDLED_ONTOLOGIES.iter().find(|spec| spec.id == id)
115}
116
117fn join_rel(root: &Path, rel: &str) -> PathBuf {
118    let mut out = root.to_path_buf();
119    for segment in rel.split('/') {
120        out.push(segment);
121    }
122    out
123}
124
125/// Resolve a bundled ontology source file from the packaged app or repo tree.
126pub fn resolve_bundled_ontology_source(id: &str) -> Option<PathBuf> {
127    let spec = ontology_spec(id)?;
128
129    if let Ok(extra) = std::env::var("QUALIA_BUNDLED_ONTOLOGIES_DIR") {
130        let file_name = Path::new(spec.rel_path).file_name()?;
131        let candidate = PathBuf::from(&extra).join(file_name);
132        if candidate.is_file() {
133            return Some(candidate);
134        }
135        // Also accept w3c-archives/ subdir under the override root.
136        let candidate = PathBuf::from(extra).join("w3c-archives").join(file_name);
137        if candidate.is_file() {
138            return Some(candidate);
139        }
140    }
141
142    if let Some(root) = exe_dir() {
143        for rel in [
144            spec.rel_path,
145            spec.rel_path
146                .strip_prefix("bundled/")
147                .unwrap_or(spec.rel_path),
148        ] {
149            let candidate = join_rel(&root, rel);
150            if candidate.is_file() {
151                return Some(candidate);
152            }
153        }
154    }
155
156    let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
157    let candidate = join_rel(&repo_root, spec.rel_path);
158    if candidate.is_file() {
159        return Some(candidate);
160    }
161
162    None
163}
164
165fn seed_bundled_ontology_if_missing(
166    storage_path: &Path,
167    ontology_id: &str,
168) -> Result<bool, String> {
169    let q42_path = resource_import::index_dir(storage_path).join(format!("{ontology_id}.q42"));
170    if q42_path.is_file() {
171        return Ok(false);
172    }
173
174    let source = resolve_bundled_ontology_source(ontology_id)
175        .ok_or_else(|| format!("Bundled ontology source not found for {ontology_id}"))?;
176
177    let catalog = crate::api::load_workspace_catalog();
178    // Prefer short id; fall back to w3c-arch-* catalog ids used in w3c-archives/catalog.json
179    let ont = catalog
180        .find_ontology(ontology_id)
181        .or_else(|| catalog.find_ontology(&format!("w3c-arch-{ontology_id}")))
182        .or_else(|| {
183            // acl → w3c-arch-auth-acl
184            if ontology_id == "acl" {
185                catalog.find_ontology("w3c-arch-auth-acl")
186            } else {
187                None
188            }
189        });
190    resource_import::ingest_local_rdf(&source, ontology_id, storage_path, ont)
191        .map_err(|e| e.to_string())?;
192    Ok(true)
193}
194
195/// Seed bundled essential ontologies into `{storage}/Index/` when absent.
196pub fn seed_bundled_ontologies() -> Result<Vec<String>, String> {
197    let state = crate::state::APP_STATE
198        .get()
199        .ok_or("APP_STATE not initialized")?;
200    let storage = state
201        .config
202        .lock()
203        .map_err(|e| e.to_string())?
204        .storage_path
205        .clone();
206    let storage_path = PathBuf::from(storage);
207    fs::create_dir_all(resource_import::index_dir(&storage_path)).map_err(|e| e.to_string())?;
208
209    let mut seeded = Vec::new();
210    for ontology_id in DEFAULT_BUNDLED_ONTOLOGIES {
211        match seed_bundled_ontology_if_missing(&storage_path, ontology_id) {
212            Ok(true) => seeded.push((*ontology_id).to_string()),
213            Ok(false) => {}
214            Err(e) => eprintln!("[bundled_ontologies] skip {ontology_id}: {e}"),
215        }
216    }
217
218    Ok(seeded)
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn bundled_shacl_source_resolves_when_tracked() {
227        let tracked =
228            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bundled/ontologies/shacl.ttl");
229        if tracked.is_file() {
230            let src = resolve_bundled_ontology_source("shacl");
231            assert!(src.is_some(), "expected bundled SHACL path");
232        }
233    }
234
235    #[test]
236    fn solid_stack_sources_resolve() {
237        for id in [
238            "ldp",
239            "acl",
240            "solid-terms",
241            "solid-oidc",
242            "pim-space",
243            "foaf",
244        ] {
245            let src = resolve_bundled_ontology_source(id);
246            assert!(
247                src.is_some(),
248                "expected bundled solid-stack ontology {id} under w3c-archives"
249            );
250        }
251    }
252}