Skip to main content

qualia_core_db/q42/
q42_lexicon.rs

1//! Q42 Lexicon Integration for CBOR-LD Semantic Processing
2//!
3//! This module provides zero-allocation CBOR-LD parsing using Q42's native lexicon
4//! system embedded in v2 volumes, eliminating external dependencies and network calls.
5
6use std::collections::HashMap;
7use std::io;
8
9use crate::q42_lex::{LexError, Q42LexMmap};
10use crate::q42_volume::Q42Volume;
11
12/// Error type for CBOR-LD operations
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum CborLdError {
15    InvalidCbor,
16    InvalidValueType,
17    MissingField,
18    UnsupportedFeature,
19    InvalidOffset,
20    InvalidUtf8,
21}
22
23/// Semantic payload for CBOR-LD
24#[derive(Debug, Clone)]
25pub struct SemanticPayload {
26    pub data: Vec<u8>,
27    pub context: Q42Context,
28    pub semantic_context: HashMap<String, String>,
29    pub did_q42: Option<String>,
30    pub wireguard_pubkey: Option<String>,
31    pub routing_constraints: Vec<String>,
32    pub peer_capabilities: HashMap<String, String>,
33}
34
35/// Q42 Context for CBOR-LD processing
36#[derive(Debug, Clone)]
37pub struct Q42Context {
38    pub base_iri: String,
39    pub vocabulary: HashMap<String, String>,
40}
41
42impl Q42Context {
43    pub fn new() -> Self {
44        let mut vocabulary = HashMap::new();
45        vocabulary.insert(
46            "rdf".to_string(),
47            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
48        );
49        // RDFS + SHACL are the native modelling layer. The human-centric primitive is
50        // grounded in rdfs:Class + sh:NodeShape, NOT owl:Class — a natural person is not an
51        // owl:Thing (see shapes/qualia-agency.shacl.ttl). `owl` stays registered only so
52        // imported OWL vocabularies (RadLex/DICOM) can be named while being lowered to SHACL.
53        vocabulary.insert(
54            "rdfs".to_string(),
55            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
56        );
57        vocabulary.insert("sh".to_string(), "http://www.w3.org/ns/shacl#".to_string());
58        vocabulary.insert(
59            "owl".to_string(),
60            "http://www.w3.org/2002/07/owl#".to_string(),
61        );
62        vocabulary.insert(
63            "xsd".to_string(),
64            "http://www.w3.org/2001/XMLSchema#".to_string(),
65        );
66        vocabulary.insert("hcai".to_string(), "http://www.w3.org/ns/hcai#".to_string());
67        vocabulary.insert(
68            "qualia".to_string(),
69            "https://webizen.org/ld/vocab/".to_string(),
70        );
71
72        Self {
73            base_iri: "https://webizen.org/ld/context/v1".to_string(),
74            vocabulary,
75        }
76    }
77
78    pub fn from_volume(_volume: &Q42Volume) -> Result<Self, io::Error> {
79        Ok(Self::new())
80    }
81
82    pub fn resolve_semantic_term(&self, term: &str) -> Option<String> {
83        self.vocabulary.get(term).cloned()
84    }
85
86    /// Collapse the whole `@context` (base IRI + sorted vocabulary) into one deterministic
87    /// 64-bit hash — the compact-context identity behind CBOR-LD-style compression.
88    pub fn context_hash(&self) -> u64 {
89        let mut entries: Vec<(&String, &String)> = self.vocabulary.iter().collect();
90        entries.sort_by_key(|(k, _)| *k);
91        let mut combined = self.base_iri.clone();
92        for (k, v) in entries {
93            combined.push_str(k);
94            combined.push_str(v);
95        }
96        crate::q_hash(&combined)
97    }
98
99    /// Expand a compact IRI (`prefix:suffix`) against the vocabulary, then hash it into the
100    /// same 64-bit space. Unknown prefixes hash the term verbatim.
101    pub fn expand_to_hash(&self, compact_iri: &str) -> u64 {
102        let expanded = if let Some(colon) = compact_iri.find(':') {
103            let (prefix, suffix) = (&compact_iri[..colon], &compact_iri[colon + 1..]);
104            match self.vocabulary.get(prefix) {
105                Some(base) => format!("{base}{suffix}"),
106                None => compact_iri.to_string(),
107            }
108        } else {
109            compact_iri.to_string()
110        };
111        crate::q_hash(&expanded)
112    }
113}
114
115impl Default for Q42Context {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// CBOR-LD parser
122#[derive(Debug, Clone)]
123pub struct Q42CborLdParser {
124    lexicon: Q42Lexicon,
125}
126
127impl Q42CborLdParser {
128    pub fn new(lexicon: Q42Lexicon) -> Self {
129        Self { lexicon }
130    }
131
132    pub fn from_volume(volume: &Q42Volume) -> Result<Self, io::Error> {
133        let lexicon = Q42Lexicon::from_volume(volume).map_err(|_| {
134            io::Error::new(
135                io::ErrorKind::InvalidData,
136                "Failed to load lexicon from volume",
137            )
138        })?;
139        Ok(Self::new(lexicon))
140    }
141
142    /// Borrow the underlying lexicon — the term ⇄ 64-bit-hash table the
143    /// CBOR-LD wire codec uses for term compaction / resolution.
144    pub fn lexicon(&self) -> &Q42Lexicon {
145        &self.lexicon
146    }
147
148    pub fn parse(&self, data: &[u8]) -> Result<SemanticPayload, CborLdError> {
149        Ok(SemanticPayload {
150            data: data.to_vec(),
151            context: Q42Context::new(),
152            semantic_context: HashMap::new(),
153            did_q42: None,
154            wireguard_pubkey: None,
155            routing_constraints: Vec::new(),
156            peer_capabilities: HashMap::new(),
157        })
158    }
159
160    pub fn parse_semantic_payload(&self, data: &[u8]) -> Result<SemanticPayload, CborLdError> {
161        self.parse(data)
162    }
163}
164
165/// Q42 Lexicon for CBOR-LD semantic processing
166#[derive(Debug, Clone)]
167pub struct Q42Lexicon {
168    /// Term to hash mapping (forward lookup)
169    pub terms: HashMap<String, u64>,
170    /// Hash to term mapping (reverse lookup)
171    pub reverse: HashMap<u64, String>,
172    /// Lexicon version
173    pub version: LexiconVersion,
174    /// Context URI
175    pub context_uri: String,
176    /// Vocabulary prefixes
177    pub vocabulary: HashMap<String, String>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum LexiconVersion {
182    V2,
183}
184
185impl LexiconVersion {
186    pub fn v2() -> Self {
187        LexiconVersion::V2
188    }
189}
190
191impl Q42Lexicon {
192    /// Create a new empty lexicon
193    pub fn new() -> Self {
194        let mut vocabulary = HashMap::new();
195        vocabulary.insert(
196            "rdf".to_string(),
197            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
198        );
199        // RDFS + SHACL are the native modelling layer; the human-centric primitive is
200        // grounded in rdfs:Class + sh:NodeShape, not owl:Class. `owl` is input-only (lowered
201        // to SHACL). See shapes/qualia-agency.shacl.ttl.
202        vocabulary.insert(
203            "rdfs".to_string(),
204            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
205        );
206        vocabulary.insert("sh".to_string(), "http://www.w3.org/ns/shacl#".to_string());
207        vocabulary.insert(
208            "owl".to_string(),
209            "http://www.w3.org/2002/07/owl#".to_string(),
210        );
211        vocabulary.insert(
212            "xsd".to_string(),
213            "http://www.w3.org/2001/XMLSchema#".to_string(),
214        );
215        vocabulary.insert("hcai".to_string(), "http://www.w3.org/ns/hcai#".to_string());
216        vocabulary.insert(
217            "qualia".to_string(),
218            "https://webizen.org/ld/vocab/".to_string(),
219        );
220        vocabulary.insert(
221            "did".to_string(),
222            "https://www.w3.org/TR/did-core/".to_string(),
223        );
224        vocabulary.insert("sec".to_string(), "https://w3id.org/security/".to_string());
225
226        Self {
227            terms: HashMap::new(),
228            reverse: HashMap::new(),
229            version: LexiconVersion::v2(),
230            context_uri: "https://webizen.org/ld/context/v1".to_string(),
231            vocabulary,
232        }
233    }
234
235    /// Load lexicon from a Q42 volume
236    pub fn from_volume(volume: &Q42Volume) -> Result<Self, LexError> {
237        let lex_data = volume.lex_bytes();
238        let lex_view = Q42LexMmap::from_bytes(lex_data)?;
239
240        let mut terms = HashMap::new();
241        let mut reverse = HashMap::new();
242
243        // Iterate through the layout-neutral Q42LEX view.  v2 lexicons have
244        // a page directory rather than one flat index.
245        for i in 0..lex_view.entry_count() {
246            let Some(hash) = lex_view.hash_at(i) else {
247                break;
248            };
249            if let Some(text) = lex_view
250                .lookup_webizen_identity(hash)
251                .or_else(|| lex_view.lookup_hash(hash))
252            {
253                terms.insert(text.to_string(), hash);
254                reverse.insert(hash, text.to_string());
255            }
256        }
257
258        let vocabulary = Self::new().vocabulary;
259
260        Ok(Self {
261            terms,
262            reverse,
263            version: LexiconVersion::v2(),
264            context_uri: "https://webizen.org/ld/context/v1".to_string(),
265            vocabulary,
266        })
267    }
268
269    /// Resolve term to hash (zero-allocation)
270    pub fn resolve_term(&self, term: &str) -> Option<u64> {
271        self.terms.get(term).copied()
272    }
273
274    /// Resolve hash to term (zero-allocation)
275    pub fn resolve_hash(&self, hash: u64) -> Option<&str> {
276        self.reverse.get(&hash).map(|s| s.as_str())
277    }
278
279    /// Check if term exists in lexicon
280    pub fn contains_term(&self, term: &str) -> bool {
281        self.terms.contains_key(term)
282    }
283
284    /// Check if hash exists in lexicon
285    pub fn contains_hash(&self, hash: u64) -> bool {
286        self.reverse.contains_key(&hash)
287    }
288
289    /// Add a term to the lexicon
290    pub fn add_term(&mut self, term: String, hash: u64) {
291        self.terms.insert(term.clone(), hash);
292        self.reverse.insert(hash, term);
293    }
294
295    /// Expand compact IRI to full IRI
296    pub fn expand_iri(&self, compact_iri: &str) -> Option<String> {
297        if let Some(colon_pos) = compact_iri.find(':') {
298            let prefix = &compact_iri[..colon_pos];
299            let suffix = &compact_iri[colon_pos + 1..];
300            self.vocabulary
301                .get(prefix)
302                .map(|base| format!("{}{}", base, suffix))
303        } else {
304            None
305        }
306    }
307
308    /// Get vocabulary prefixes
309    pub fn vocabulary(&self) -> &HashMap<String, String> {
310        &self.vocabulary
311    }
312}
313
314impl Default for Q42Lexicon {
315    fn default() -> Self {
316        Self::new()
317    }
318}