Skip to main content

qualia_core_db/query/
ontology_loader.rs

1#![cfg(not(target_arch = "wasm32"))]
2//! Startup ontology loader — parses bundled Turtle/N-Triples ontology files into NQuins
3//! and seeds the daemon graph at startup.
4//!
5//! Ontology files are resolved in priority order:
6//!   1. `$QUALIA_ONTOLOGY_PATH/` environment variable if set
7//!   2. `ontologies/` relative to the current working directory (dev / workspace layout)
8//!   3. Alongside the binary: `<exe-dir>/ontologies/`
9//!
10//! Any file that cannot be read or parsed emits a log warning and is skipped — the daemon
11//! starts successfully even if ontology files are absent.
12
13use crate::{q_hash, NQuin};
14
15// Canonical named graphs for each ontology.
16const RIGHTS_GRAPH: u64 = q_hash("urn:qualia:ontology:rights");
17const COGAI_GRAPH: u64 = q_hash("urn:qualia:ontology:cogai");
18const EPISTEMIC_GRAPH: u64 = q_hash("urn:qualia:ontology:epistemic");
19const UDHR_GRAPH: u64 = q_hash("urn:qualia:ontology:udhr");
20const CRC_GRAPH: u64 = q_hash("urn:qualia:ontology:crc");
21const PLATFORMS_GRAPH: u64 = q_hash("urn:qualia:ontology:platforms");
22const EMOJI_GRAPH: u64 = q_hash("urn:qualia:ontology:emoji");
23const CRC_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:crc_annotated");
24const CRPD_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:crpd_annotated");
25const ICCPR_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:iccpr_annotated");
26const ICESCR_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:icescr_annotated");
27const UDHR_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:udhr_annotated");
28const CAT_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:cat_annotated");
29const LAWYERS_PRINCIPLES_ANNOTATED_GRAPH: u64 =
30    q_hash("urn:qualia:ontology:lawyers_principles_annotated");
31const REMEDY_REPARATION_ANNOTATED_GRAPH: u64 =
32    q_hash("urn:qualia:ontology:remedy_reparation_annotated");
33const RIGHT_DEVELOPMENT_ANNOTATED_GRAPH: u64 =
34    q_hash("urn:qualia:ontology:right_development_annotated");
35const TORTURE_DECLARATION_ANNOTATED_GRAPH: u64 =
36    q_hash("urn:qualia:ontology:torture_declaration_annotated");
37const HR_DEFENDERS_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:hr_defenders_annotated");
38const PEOPLES_PEACE_ANNOTATED_GRAPH: u64 = q_hash("urn:qualia:ontology:peoples_peace_annotated");
39const MENTAL_ILLNESS_PRINCIPLES_ANNOTATED_GRAPH: u64 =
40    q_hash("urn:qualia:ontology:mental_illness_principles_annotated");
41const ISTANBUL_PROTOCOL_ANNOTATED_GRAPH: u64 =
42    q_hash("urn:qualia:ontology:istanbul_protocol_annotated");
43const PALERMO_PROTOCOL_ANNOTATED_GRAPH: u64 =
44    q_hash("urn:qualia:ontology:palermo_protocol_annotated");
45const COMMONWEALTH_CHARTER_ANNOTATED_GRAPH: u64 =
46    q_hash("urn:qualia:ontology:commonwealth_charter_annotated");
47
48/// Files to load at startup, as `(filename, named_graph_context)` pairs.
49const STARTUP_ONTOLOGIES: &[(&str, u64)] = &[
50    ("rights_ontology.ttl", RIGHTS_GRAPH),
51    ("cogai_shapes.ttl", COGAI_GRAPH),
52    ("epistemic_shapes.ttl", EPISTEMIC_GRAPH),
53    ("udhr.ttl", UDHR_GRAPH),
54    ("crc.ttl", CRC_GRAPH),
55    ("platforms.ttl", PLATFORMS_GRAPH),
56    ("emoji.n3", EMOJI_GRAPH),
57    ("udhr_annotated.ttl", UDHR_ANNOTATED_GRAPH),
58    ("crc_annotated.ttl", CRC_ANNOTATED_GRAPH),
59    ("crpd_annotated.ttl", CRPD_ANNOTATED_GRAPH),
60    ("iccpr_annotated.ttl", ICCPR_ANNOTATED_GRAPH),
61    ("icescr_annotated.ttl", ICESCR_ANNOTATED_GRAPH),
62    ("cat_annotated.ttl", CAT_ANNOTATED_GRAPH),
63    (
64        "lawyers_principles_annotated.ttl",
65        LAWYERS_PRINCIPLES_ANNOTATED_GRAPH,
66    ),
67    (
68        "remedy_reparation_annotated.ttl",
69        REMEDY_REPARATION_ANNOTATED_GRAPH,
70    ),
71    (
72        "right_development_annotated.ttl",
73        RIGHT_DEVELOPMENT_ANNOTATED_GRAPH,
74    ),
75    (
76        "torture_declaration_annotated.ttl",
77        TORTURE_DECLARATION_ANNOTATED_GRAPH,
78    ),
79    ("hr_defenders_annotated.ttl", HR_DEFENDERS_ANNOTATED_GRAPH),
80    ("peoples_peace_annotated.ttl", PEOPLES_PEACE_ANNOTATED_GRAPH),
81    (
82        "mental_illness_principles_annotated.ttl",
83        MENTAL_ILLNESS_PRINCIPLES_ANNOTATED_GRAPH,
84    ),
85    (
86        "istanbul_protocol_annotated.ttl",
87        ISTANBUL_PROTOCOL_ANNOTATED_GRAPH,
88    ),
89    (
90        "palermo_protocol_annotated.ttl",
91        PALERMO_PROTOCOL_ANNOTATED_GRAPH,
92    ),
93    (
94        "commonwealth_charter_annotated.ttl",
95        COMMONWEALTH_CHARTER_ANNOTATED_GRAPH,
96    ),
97];
98
99/// Startup ontology catalog: `(filename, named_graph_context_hash)`.
100pub fn startup_ontology_catalog() -> &'static [(&'static str, u64)] {
101    STARTUP_ONTOLOGIES
102}
103
104/// Resolved ontologies directory, if present on disk.
105pub fn ontology_dir_path() -> Option<std::path::PathBuf> {
106    find_ontology_dir()
107}
108
109/// Discover the ontologies directory.
110fn find_ontology_dir() -> Option<std::path::PathBuf> {
111    // 1. Environment variable override.
112    if let Ok(p) = std::env::var("QUALIA_ONTOLOGY_PATH") {
113        let pb = std::path::PathBuf::from(p);
114        if pb.is_dir() {
115            return Some(pb);
116        }
117    }
118
119    // 2. `./ontologies/` (workspace root when running via `cargo run`).
120    let cwd = std::path::PathBuf::from("ontologies");
121    if cwd.is_dir() {
122        return Some(cwd);
123    }
124
125    // 3. Next to the binary.
126    if let Ok(exe) = std::env::current_exe() {
127        let sibling = exe.parent().map(|p| p.join("ontologies"));
128        if let Some(ref s) = sibling {
129            if s.is_dir() {
130                return Some(s.clone());
131            }
132        }
133    }
134
135    None
136}
137
138/// Parse a single Turtle file into NQuins, all placed in `graph_context`.
139///
140/// Each triple becomes:
141///   `NQuin { subject = q_hash(subject_iri), predicate = q_hash(pred_iri),
142///            object = q_hash(object_str), context = graph_context, ... }`
143pub fn parse_ttl_to_quins(path: &std::path::Path, graph_context: u64) -> Vec<NQuin> {
144    use std::fs::File;
145    use std::io::BufReader;
146
147    let file = match File::open(path) {
148        Ok(f) => f,
149        Err(e) => {
150            log::warn!("[ontology_loader] cannot open {:?}: {e}", path);
151            return Vec::new();
152        }
153    };
154
155    let reader = BufReader::new(file);
156    let mut parser = rio_turtle::TurtleParser::new(reader, None);
157    let mut quins = Vec::new();
158
159    let result = {
160        use rio_api::parser::TriplesParser;
161        parser.parse_all(
162            &mut |t: rio_api::model::Triple| -> Result<(), std::io::Error> {
163                let s = q_hash(&t.subject.to_string());
164                let p = q_hash(&t.predicate.to_string());
165                let o = q_hash(&t.object.to_string());
166                quins.push(NQuin {
167                    subject: s,
168                    predicate: p,
169                    object: o,
170                    context: graph_context,
171                    metadata: 0,
172                    parity: s ^ p ^ o ^ graph_context,
173                });
174                Ok(())
175            },
176        )
177    };
178
179    if let Err(e) = result {
180        log::warn!("[ontology_loader] parse error in {:?}: {e}", path);
181    }
182
183    log::info!(
184        "[ontology_loader] loaded {} quins from {:?}",
185        quins.len(),
186        path
187    );
188    quins
189}
190
191/// Load a unified `.q42` volume ontology file.
192pub fn load_q42_file(path: &std::path::Path) -> Vec<NQuin> {
193    let all_quins = match crate::q42_reader::read_q42_quins(path) {
194        Ok(quins) => quins,
195        Err(e) => {
196            log::warn!("[ontology_loader] cannot open q42 volume {:?}: {e}", path);
197            return Vec::new();
198        }
199    };
200    log::info!(
201        "[ontology_loader] loaded {} quins from unified volume {:?}",
202        all_quins.len(),
203        path
204    );
205    all_quins
206}
207
208/// Load all startup ontologies into the daemon graph.
209///
210/// Call this once, immediately after `daemon_graph::init_daemon_graph()`.
211pub fn load_startup_ontologies() {
212    let dir = match find_ontology_dir() {
213        Some(d) => d,
214        None => {
215            log::info!("[ontology_loader] no ontologies directory found — skipping");
216            return;
217        }
218    };
219
220    log::info!("[ontology_loader] loading ontologies from {:?}", dir);
221
222    let mut all_quins: Vec<NQuin> = Vec::new();
223    for (filename, context) in STARTUP_ONTOLOGIES {
224        let path = dir.join(filename);
225        if !path.exists() {
226            log::warn!("[ontology_loader] {:?} not found — skipping", path);
227            continue;
228        }
229        let quins = parse_ttl_to_quins(&path, *context);
230        all_quins.extend(quins);
231    }
232
233    // Load any binary .q42 files present in the ontologies directory
234    if let Ok(entries) = std::fs::read_dir(&dir) {
235        for entry in entries.filter_map(|e| e.ok()) {
236            let path = entry.path();
237            if path.extension().and_then(|s| s.to_str()) == Some("q42") {
238                let quins = load_q42_file(&path);
239                all_quins.extend(quins);
240            }
241        }
242    }
243
244    crate::daemon_graph::extend_with_ontology_quins(all_quins);
245    log::info!(
246        "[ontology_loader] daemon graph now has {} quins after ontology seed",
247        crate::daemon_graph::graph_quin_count(),
248    );
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use std::io::Write;
255
256    #[test]
257    fn parse_ttl_minimal() {
258        let ttl = b"@prefix ex: <http://example.org/> .\nex:Alice a ex:Person .\n";
259        let tmp = tempfile::NamedTempFile::new().expect("tmp");
260        tmp.as_file().write_all(ttl).unwrap();
261        let quins = parse_ttl_to_quins(tmp.path(), 0xCAFE);
262        assert!(!quins.is_empty());
263        assert!(quins.iter().all(|q| q.context == 0xCAFE));
264    }
265
266    #[test]
267    fn parse_ttl_missing_file_returns_empty() {
268        let quins = parse_ttl_to_quins(std::path::Path::new("/nonexistent/file.ttl"), 0);
269        assert!(quins.is_empty());
270    }
271}