qualia_core_db/query/lexicon.rs
1//! Lexicon Dictionary Manager
2//! Maps multi-modal semantic concepts (Text, Audio, Visual) into the deterministic 60-bit integers
3//! required by the NQuin data structure.
4
5// Re-export the embedded triple tag from resolver for SPARQL-Star support
6pub use crate::resolver::{TAG_EMBEDDED, TAG_WEBIZEN};
7
8/// Represents the pluralistic forms that a semantic concept can take.
9/// We explicitly reject the assumption that knowledge is exclusively bound to Unicode strings.
10pub enum SemanticModality<'a> {
11 Text(&'a str),
12 AudioHash(&'a [u8]), // For mother tongues / oral traditions
13 CeremonialVisual(&'a [u8]), // For heraldry / visual concepts
14 PhoneticSchema(&'a [u8]), // For non-western phonetics
15 Visual(&'a [u8]), // Generalized RGB/RGBA images & video frames
16 Spatial3D(&'a [u8]), // 3D MeshIR, STL/3MF, point clouds, volumetric grids
17 Biosignal(&'a [u8]), // PPG/rPPG, ECG, EEG, respiratory telemetry
18 Molecular(&'a [u8]), // SMILES, InChI, protein sequences, chemical structures
19 QuantumPhysical(&'a [u8]), // Quantum state vectors, density functional grids, ODE fields
20 SpatioTemporalTrace(&'a [u8]), // LTL temporal traces, geospatial paths, motion kinematics
21 Custom { tag: u32, bytes: &'a [u8] }, // Arbitrary domain modality with custom tag
22}
23
24/// Generates a deterministic, collision-resistant 60-bit token from a raw byte stream.
25/// Uses a custom FNV-1a inspired hash restricted to 60 bits.
26#[inline(always)]
27pub fn generate_60bit_token(bytes: &[u8]) -> u64 {
28 let mut hash: u64 = 0xcbf29ce484222325; // FNV offset basis
29 for &byte in bytes {
30 hash ^= byte as u64;
31 hash = hash.wrapping_mul(0x100000001b3); // FNV prime
32 }
33 // Truncate to 60 bits (the top 4 bits are reserved for datatype tags in the O vector)
34 hash & 0x0FFF_FFFF_FFFF_FFFF
35}
36
37/// Generates a Virtual ID for a SPARQL-Star embedded triple <<s p o>>.
38///
39/// This function serializes the three u64 component IDs into a 24-byte array
40/// and hashes them using FNV-1a, then tags the result with TAG_EMBEDDED.
41///
42/// # Arguments
43/// * subject - The subject u64 ID
44/// * predicate - The predicate u64 ID
45/// * object - The object u64 ID
46///
47/// # Returns
48/// A 64-bit Virtual ID with the TAG_EMBEDDED bit set, suitable for
49/// storage in the Subject or Object position of a NQuin.
50#[inline(always)]
51pub fn generate_embedded_triple_id(subject: u64, predicate: u64, object: u64) -> u64 {
52 // Serialize the three u64 IDs into a 24-byte array
53 let mut bytes = [0u8; 24];
54 bytes[0..8].copy_from_slice(&subject.to_le_bytes());
55 bytes[8..16].copy_from_slice(&predicate.to_le_bytes());
56 bytes[16..24].copy_from_slice(&object.to_le_bytes());
57
58 // Hash the bytes and tag with EMBEDDED marker
59 generate_60bit_token(&bytes) | TAG_EMBEDDED
60}
61
62/// In-memory Lexicon manager to handle reverse lookups in the future.
63/// For now, ingestion purely maps forward (Bytes -> u64) via the hash.
64pub struct LexiconManager {
65 // A production database would memory-map a reverse lookup file here (u64 -> Modality)
66}
67
68impl LexiconManager {
69 pub fn new() -> Self {
70 Self {}
71 }
72
73 /// Converts a multi-modal semantic concept into its 60-bit hardware representation.
74 pub fn tokenize_modal(&self, modality: &SemanticModality) -> u64 {
75 match modality {
76 SemanticModality::Text(text) => generate_60bit_token(text.as_bytes()),
77 SemanticModality::AudioHash(bytes) => generate_60bit_token(bytes),
78 SemanticModality::CeremonialVisual(bytes) => generate_60bit_token(bytes),
79 SemanticModality::PhoneticSchema(bytes) => generate_60bit_token(bytes),
80 SemanticModality::Visual(bytes) => generate_60bit_token(bytes),
81 SemanticModality::Spatial3D(bytes) => generate_60bit_token(bytes),
82 SemanticModality::Biosignal(bytes) => generate_60bit_token(bytes),
83 SemanticModality::Molecular(bytes) => generate_60bit_token(bytes),
84 SemanticModality::QuantumPhysical(bytes) => generate_60bit_token(bytes),
85 SemanticModality::SpatioTemporalTrace(bytes) => generate_60bit_token(bytes),
86 SemanticModality::Custom { tag, bytes } => {
87 let mut combined = Vec::with_capacity(4 + bytes.len());
88 combined.extend_from_slice(&tag.to_le_bytes());
89 combined.extend_from_slice(bytes);
90 generate_60bit_token(&combined)
91 }
92 }
93 }
94
95 /// Legacy support for text strings
96 pub fn tokenize(&self, literal: &str) -> u64 {
97 self.tokenize_modal(&SemanticModality::Text(literal))
98 }
99}
100
101use std::collections::HashMap;
102
103/// Outcome of interning a `(handle, value)` pair into a collision-aware lexicon.
104#[derive(Debug, PartialEq, Eq, Clone, Copy)]
105pub enum Intern {
106 /// First time this handle is seen.
107 New,
108 /// Same handle, SAME value — an idempotent re-intern.
109 Seen,
110 /// Same handle, DIFFERENT value — a genuine handle collision (both values kept).
111 Collision,
112}
113
114/// Collision-aware string interner — the lexicon collision backstop (task #22).
115///
116/// The plain build-time map (`HashMap<u64, String>` in `external_sort`) silently
117/// OVERWRITES on a handle collision (two distinct strings hashing to the same 60-bit
118/// token), losing data with no signal. This interner DETECTS the collision at intern
119/// time — O(1), off the resolution hot path — and KEEPS BOTH values, so a collision
120/// becomes loud + recoverable rather than silent corruption. Resolution stays
121/// single-value and fast for the (overwhelmingly common) collision-free handles; only
122/// a flagged handle pays a comparison over its small bucket (length-gated by `str` eq),
123/// from memory. This is a host-side structure (`HashMap`/`String`), separate from the
124/// 42 MB `SlgArena` — its allocations are one-time intern cost, not per-resolution.
125#[derive(Default)]
126pub struct LexiconInterner {
127 /// handle -> first-interned value (the fast, common path).
128 map: HashMap<u64, String>,
129 /// handle -> all distinct values, populated ONLY when a collision occurs.
130 buckets: HashMap<u64, Vec<String>>,
131}
132
133impl LexiconInterner {
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 /// Intern `(generate_60bit_token(value), value)`. Returns the collision outcome.
139 pub fn intern_str(&mut self, value: &str) -> Intern {
140 self.intern(generate_60bit_token(value.as_bytes()), value)
141 }
142
143 /// Intern an explicit `(handle, value)` pair — the handle need not be
144 /// `generate_60bit_token(value)` (e.g. did:q42 / Webizen handles). Detects a
145 /// same-handle / different-value collision and preserves both values.
146 pub fn intern(&mut self, handle: u64, value: &str) -> Intern {
147 match self.map.get(&handle) {
148 None => {
149 self.map.insert(handle, value.to_string());
150 Intern::New
151 }
152 Some(existing) if existing == value => Intern::Seen,
153 Some(existing) => {
154 // Genuine collision: preserve BOTH values; never silently overwrite.
155 let bucket = self
156 .buckets
157 .entry(handle)
158 .or_insert_with(|| vec![existing.clone()]);
159 if !bucket.iter().any(|v| v == value) {
160 bucket.push(value.to_string());
161 }
162 Intern::Collision
163 }
164 }
165 }
166
167 /// Whether `handle` has more than one distinct interned value (a collision).
168 #[inline]
169 pub fn is_collision(&self, handle: u64) -> bool {
170 self.buckets.contains_key(&handle)
171 }
172
173 /// Resolve `handle` -> value with collision awareness. Collision-free (common): the
174 /// single value, no comparison. Collided (rare): `None` — there is no single answer,
175 /// so the caller disambiguates with the query value via [`Self::resolve_value`]
176 /// rather than receiving a silently-wrong one.
177 pub fn resolve(&self, handle: u64) -> Option<&str> {
178 if self.buckets.contains_key(&handle) {
179 return None;
180 }
181 self.map.get(&handle).map(String::as_str)
182 }
183
184 /// Disambiguating resolve — the backstop. Returns the stored value equal to `query`
185 /// iff `handle` interns it. `str` equality length-gates before the byte compare, so
186 /// the rare collision path stays cheap.
187 pub fn resolve_value(&self, handle: u64, query: &str) -> Option<&str> {
188 if let Some(bucket) = self.buckets.get(&handle) {
189 return bucket.iter().map(String::as_str).find(|&s| s == query);
190 }
191 self.map
192 .get(&handle)
193 .map(String::as_str)
194 .filter(|&s| s == query)
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_60bit_truncation() {
204 let uri = "https://mediaprophet.github.io/qualiaDB/user/123";
205 let token = generate_60bit_token(uri.as_bytes());
206
207 // Ensure the top 4 bits are strictly 0 (Lexicon ID datatype tag)
208 assert_eq!(token >> 60, 0, "Token spilled over 60 bits");
209 }
210
211 /// Regression guard for the hash-space unification (task #14): the compile-time
212 /// `q_hash` and the runtime `generate_60bit_token` MUST be one identity space,
213 /// or compile-time-baked URIs (deontic/values/MCP) won't join runtime-parsed/
214 /// ingested URIs (turtle/SPARQL/SHACL/corpus). If this ever fails, the two
215 /// drifted apart again — fix the divergence, don't relax the test.
216 #[test]
217 fn q_hash_and_generate_60bit_token_are_one_space() {
218 for s in [
219 "",
220 "Bob",
221 "http://www.w3.org/ns/shacl#maxInclusive",
222 "https://ns.webcivics.net/values/NaturalPerson",
223 "q42:Principal",
224 "naïve—prière—母語", // non-ASCII must agree too
225 ] {
226 assert_eq!(
227 crate::q_hash(s),
228 generate_60bit_token(s.as_bytes()),
229 "q_hash and generate_60bit_token diverged for {s:?}",
230 );
231 // Both are pure 60-bit identifiers: top 4 bits reserved for the tag overlay.
232 assert_eq!(
233 crate::q_hash(s) >> 60,
234 0,
235 "q_hash spilled past 60 bits for {s:?}"
236 );
237 }
238 }
239
240 #[test]
241 fn interner_detects_collision_and_keeps_both_values() {
242 let mut lx = LexiconInterner::new();
243 // Force a handle collision (same handle, different values) by interning explicit
244 // handles — a natural 60-bit FNV collision is infeasible to find in a test.
245 let h = 0x0ABC_DEF0_1234_5678;
246 assert_eq!(lx.intern(h, "alpha"), Intern::New);
247 assert_eq!(lx.intern(h, "alpha"), Intern::Seen); // idempotent
248 assert_eq!(lx.intern(h, "beta"), Intern::Collision); // genuine collision
249 assert!(lx.is_collision(h));
250
251 // Both values survive and are recoverable via the disambiguating resolve —
252 // unlike a plain HashMap, where "alpha" would have been silently overwritten.
253 assert_eq!(lx.resolve_value(h, "alpha"), Some("alpha"));
254 assert_eq!(lx.resolve_value(h, "beta"), Some("beta"));
255 assert_eq!(lx.resolve_value(h, "gamma"), None);
256
257 // Bare resolve refuses to guess on an ambiguous handle (no silent wrong answer).
258 assert_eq!(lx.resolve(h), None);
259 }
260
261 #[test]
262 fn interner_collision_free_is_the_fast_single_value_path() {
263 let mut lx = LexiconInterner::new();
264 let iri = "https://ns.webcivics.net/values/State";
265 assert_eq!(lx.intern_str(iri), Intern::New);
266 assert_eq!(lx.intern_str(iri), Intern::Seen);
267
268 // q_hash == generate_60bit_token (post-#14), so this is the interned handle.
269 let h = crate::q_hash(iri);
270 assert!(!lx.is_collision(h));
271 // Collision-free: bare resolve returns the single value directly, no comparison.
272 assert_eq!(lx.resolve(h), Some(iri));
273 }
274
275 #[test]
276 fn test_determinism() {
277 let uri = "qualia:guardian";
278 let t1 = generate_60bit_token(uri.as_bytes());
279 let t2 = generate_60bit_token(uri.as_bytes());
280 assert_eq!(t1, t2, "Tokens are not deterministic");
281 }
282
283 #[test]
284 fn test_linguistic_plurality() {
285 let lexicon = LexiconManager::new();
286
287 // Simulating a written concept
288 let written = SemanticModality::Text("peace_infrastructure");
289 let t1 = lexicon.tokenize_modal(&written);
290
291 // Simulating the exact same concept represented as a cryptographic audio hash of a spoken prayer
292 let audio_hash = vec![0x1a, 0x2b, 0x3c, 0x4d, 0x5e];
293 let oral = SemanticModality::AudioHash(&audio_hash);
294 let t2 = lexicon.tokenize_modal(&oral);
295
296 // Simulating a ceremonial SVG file representation
297 let svg_bytes = b"<svg>Heraldry</svg>";
298 let visual = SemanticModality::CeremonialVisual(svg_bytes);
299 let t3 = lexicon.tokenize_modal(&visual);
300
301 // Prove that the database treats all modalities as valid 60-bit structural Quins
302 assert!(t1 <= 0x0FFF_FFFF_FFFF_FFFF);
303 assert!(t2 <= 0x0FFF_FFFF_FFFF_FFFF);
304 assert!(t3 <= 0x0FFF_FFFF_FFFF_FFFF);
305
306 assert_ne!(t1, t2);
307 assert_ne!(t1, t3);
308
309 // Test non-identical bytes produce different hashes
310 let altered_audio = vec![0x1a, 0x2b, 0x3c, 0x4d, 0x5f];
311 let altered_oral = SemanticModality::AudioHash(&altered_audio);
312 let t4 = lexicon.tokenize_modal(&altered_oral);
313
314 assert_ne!(t2, t4);
315 }
316
317 #[test]
318 fn test_generalized_multimodal_plurality() {
319 let lexicon = LexiconManager::new();
320
321 let spatial = SemanticModality::Spatial3D(b"stl_mesh_data_3d");
322 let bio = SemanticModality::Biosignal(b"rppg_waveform_data");
323 let chem = SemanticModality::Molecular(b"CCO_ethanol_smiles");
324 let qphys = SemanticModality::QuantumPhysical(b"state_vector_psi");
325 let trace = SemanticModality::SpatioTemporalTrace(b"ltl_trace_ltl");
326 let custom = SemanticModality::Custom {
327 tag: 0x42,
328 bytes: b"custom_payload",
329 };
330
331 for m in [&spatial, &bio, &chem, &qphys, &trace, &custom] {
332 let tok = lexicon.tokenize_modal(m);
333 assert!(tok <= 0x0FFF_FFFF_FFFF_FFFF, "token within 60-bit bound");
334 assert_ne!(tok, 0);
335 }
336 }
337}