Skip to main content

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}