Skip to main content

qualia_core_db/governance/
webizen_validator.rs

1//! Webizen NQuin Validation Layer
2//!
3//! Provides shape/confidence/domain/structural rule enforcement for NQuins at
4//! ingestion and query time.  Rules are stored as dedicated Q42 blocks and
5//! validated during ingestion via [`WebValidator`].
6//!
7//! A lightweight pattern-matching VM ([`execute_query_program`]) is also
8//! provided for the CLI SPARQL query path; it is distinct from the full
9//! Webizen bytecode engine in [`crate::webizen_bytecode`].
10
11use crate::NQuin;
12use std::collections::HashSet;
13
14const OP_MATCH_SUBJ: u8 = 0x01;
15const OP_MATCH_PRED: u8 = 0x02;
16const OP_MATCH_OBJ: u8 = 0x03;
17
18const OP_BIND_VAR: u8 = 0x10;
19const SLOT_SUBJ: u8 = 0x00;
20const SLOT_PRED: u8 = 0x01;
21const SLOT_OBJ: u8 = 0x02;
22
23const OP_HALT: u8 = 0xFF;
24
25/// Stack-allocated register file for bound variable hashes (max 16 variables).
26#[derive(Debug, Default)]
27struct VmRegisters {
28    values: [u64; 16],
29    bound_flags: u16,
30}
31
32impl VmRegisters {
33    fn bind(&mut self, id: u8, value: u64) {
34        if (id as usize) < self.values.len() {
35            self.values[id as usize] = value;
36            self.bound_flags |= 1 << id;
37        }
38    }
39}
40
41/// Execute a query bytecode program against a NQuin dataset.
42///
43/// Opcodes: `MATCH_SUBJ` / `MATCH_PRED` / `MATCH_OBJ` filter the candidate
44/// set; `BIND_VAR` extracts a slot value into a register; `HALT` stops early.
45///
46/// Returns the vector of bound register values on success.
47pub fn execute_query_program(program: &[u8], dataset: &[NQuin]) -> Result<Vec<u64>, &'static str> {
48    let mut pc = 0;
49    let mut registers = VmRegisters::default();
50    let mut candidate_set: Vec<&NQuin> = dataset.iter().collect();
51
52    while pc < program.len() {
53        let opcode = program[pc];
54        pc += 1;
55
56        match opcode {
57            OP_MATCH_SUBJ => {
58                if pc + 8 > program.len() {
59                    return Err("Unexpected EOF in OP_MATCH_SUBJ");
60                }
61                let target_hash = u64::from_le_bytes(program[pc..pc + 8].try_into().unwrap());
62                pc += 8;
63                candidate_set.retain(|quin| quin.subject == target_hash);
64            }
65            OP_MATCH_PRED => {
66                if pc + 8 > program.len() {
67                    return Err("Unexpected EOF in OP_MATCH_PRED");
68                }
69                let target_hash = u64::from_le_bytes(program[pc..pc + 8].try_into().unwrap());
70                pc += 8;
71                candidate_set.retain(|quin| quin.predicate == target_hash);
72            }
73            OP_MATCH_OBJ => {
74                if pc + 8 > program.len() {
75                    return Err("Unexpected EOF in OP_MATCH_OBJ");
76                }
77                let target_hash = u64::from_le_bytes(program[pc..pc + 8].try_into().unwrap());
78                pc += 8;
79                candidate_set.retain(|quin| quin.object == target_hash);
80            }
81            OP_BIND_VAR => {
82                if pc + 2 > program.len() {
83                    return Err("Unexpected EOF in OP_BIND_VAR");
84                }
85                let slot = program[pc];
86                let reg_id = program[pc + 1];
87                pc += 2;
88                if let Some(q) = candidate_set.first() {
89                    let value = match slot {
90                        SLOT_SUBJ => q.subject,
91                        SLOT_PRED => q.predicate,
92                        SLOT_OBJ => q.object,
93                        _ => return Err("Invalid slot identifier"),
94                    };
95                    registers.bind(reg_id, value);
96                }
97            }
98            OP_HALT => break,
99            _ => return Err("Unknown Opcode"),
100        }
101    }
102
103    let mut results = Vec::new();
104    for i in 0..16 {
105        if registers.bound_flags & (1 << i) != 0 {
106            results.push(registers.values[i]);
107        }
108    }
109    Ok(results)
110}
111
112// ── Rule types ────────────────────────────────────────────────────────────────
113
114/// Unique identifier for a [`WebRule`].
115pub type WebRuleId = u64;
116
117/// Classification of a [`WebRule`].
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum WebRuleType {
120    Shape,
121    Confidence,
122    Domain,
123    Structural,
124}
125
126/// A validation rule applied by the [`WebValidator`] to incoming NQuins.
127#[repr(C)]
128#[derive(Debug, Clone)]
129pub struct WebRule {
130    pub rule_id: WebRuleId,
131    pub rule_type: WebRuleType,
132    /// Context this rule applies to; `0` means all contexts.
133    pub context_id: u64,
134    /// Predicate this rule applies to; `0` means all predicates.
135    pub predicate_hash: u64,
136    pub min_confidence: f32,
137    pub required_domain: u64,
138}
139
140impl WebRule {
141    pub fn new(rule_id: WebRuleId, rule_type: WebRuleType) -> Self {
142        Self {
143            rule_id,
144            rule_type,
145            context_id: 0,
146            predicate_hash: 0,
147            min_confidence: 0.0,
148            required_domain: 0,
149        }
150    }
151
152    pub fn with_context(mut self, context_id: u64) -> Self {
153        self.context_id = context_id;
154        self
155    }
156
157    pub fn with_predicate(mut self, predicate_hash: u64) -> Self {
158        self.predicate_hash = predicate_hash;
159        self
160    }
161
162    pub fn with_confidence(mut self, min_confidence: f32) -> Self {
163        self.min_confidence = min_confidence;
164        self
165    }
166
167    pub fn with_domain(mut self, required_domain: u64) -> Self {
168        self.required_domain = required_domain;
169        self
170    }
171}
172
173/// Outcome of a [`WebValidator::validate`] call.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum WebVerdict {
176    Pass,
177    Fail(String),
178    PassWithWarning(String),
179}
180
181// ── Validator ─────────────────────────────────────────────────────────────────
182
183/// Validates NQuins against a set of [`WebRule`]s before Q42 append.
184pub struct WebValidator {
185    rules: Vec<WebRule>,
186    active_rule_ids: HashSet<WebRuleId>,
187    confidence_threshold: f32,
188}
189
190impl WebValidator {
191    pub fn new() -> Self {
192        Self {
193            rules: Vec::new(),
194            active_rule_ids: HashSet::new(),
195            confidence_threshold: 0.5,
196        }
197    }
198
199    pub fn set_confidence_threshold(&mut self, threshold: f32) {
200        self.confidence_threshold = threshold;
201    }
202
203    pub fn add_rule(&mut self, rule: WebRule) {
204        self.active_rule_ids.insert(rule.rule_id);
205        self.rules.push(rule);
206    }
207
208    /// Returns `false` when no rules apply, allowing the fast-path to the Q42
209    /// append buffer without invoking the full validator.
210    pub fn needs_validation(&self, quin: &NQuin) -> bool {
211        for rule in &self.rules {
212            if rule.context_id == 0 || rule.context_id == quin.context {
213                if rule.predicate_hash == 0 || rule.predicate_hash == quin.predicate {
214                    return true;
215                }
216            }
217        }
218        false
219    }
220
221    /// Validate a NQuin against all active rules.
222    pub fn validate(&self, quin: &NQuin) -> WebVerdict {
223        let confidence = (quin.metadata & 0xFFFFFFFF) as f32 / u32::MAX as f32;
224
225        if confidence < self.confidence_threshold {
226            return WebVerdict::Fail(format!(
227                "Confidence {} below threshold {}",
228                confidence, self.confidence_threshold
229            ));
230        }
231
232        for rule in &self.rules {
233            if rule.context_id != 0 && rule.context_id != quin.context {
234                continue;
235            }
236            if rule.predicate_hash != 0 && rule.predicate_hash != quin.predicate {
237                continue;
238            }
239
240            match rule.rule_type {
241                WebRuleType::Confidence => {
242                    if confidence < rule.min_confidence {
243                        return WebVerdict::Fail(format!(
244                            "Confidence {} below rule threshold {}",
245                            confidence, rule.min_confidence
246                        ));
247                    }
248                }
249                WebRuleType::Domain => {
250                    if rule.required_domain != 0 && quin.subject != rule.required_domain {
251                        return WebVerdict::Fail("Subject not in required domain".to_string());
252                    }
253                }
254                WebRuleType::Shape | WebRuleType::Structural => {}
255            }
256        }
257
258        WebVerdict::Pass
259    }
260
261    /// Validate an embedded triple (Virtual ID).
262    pub fn validate_embedded_triple(
263        &self,
264        virtual_id: u64,
265        components: &[u64; 3],
266        context_id: u64,
267    ) -> WebVerdict {
268        const TAG_EMBEDDED: u64 = 0x1;
269
270        if virtual_id & TAG_EMBEDDED != TAG_EMBEDDED {
271            return WebVerdict::Fail("Not a Virtual ID".to_string());
272        }
273
274        for rule in &self.rules {
275            if rule.context_id == 0 || rule.context_id == context_id {
276                if let WebRuleType::Structural = rule.rule_type {
277                    if components[0] == 0 || components[1] == 0 || components[2] == 0 {
278                        return WebVerdict::Fail("Embedded triple has zero component".to_string());
279                    }
280                }
281            }
282        }
283
284        WebVerdict::Pass
285    }
286}
287
288impl Default for WebValidator {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294// ── Q42 storage ───────────────────────────────────────────────────────────────
295
296/// Serialises and deserialises [`WebRule`]s to/from NQuin blocks in Q42.
297#[repr(C)]
298#[derive(Debug, Clone)]
299pub struct WebRuleStorage {
300    pub block_id: u64,
301    pub rule_count: u32,
302    pub reserved: u32,
303}
304
305impl WebRuleStorage {
306    const RULE_FLAG: u64 = 0x1 << 62;
307
308    pub fn rules_to_quins(rules: &[WebRule]) -> Vec<NQuin> {
309        rules
310            .iter()
311            .map(|rule| NQuin {
312                subject: rule.rule_id,
313                predicate: (rule.rule_type as u64) << 8 | (rule.context_id & 0xFF),
314                object: rule.predicate_hash,
315                context: rule.context_id,
316                metadata: Self::RULE_FLAG
317                    | ((rule.min_confidence as u64) & 0xFFFFFFFF)
318                    | (rule.required_domain << 32),
319                parity: 0,
320            })
321            .collect()
322    }
323
324    pub fn quins_to_rules(quins: &[NQuin]) -> Vec<WebRule> {
325        quins
326            .iter()
327            .filter(|q| q.metadata & Self::RULE_FLAG != 0)
328            .map(|q| {
329                let rule_type = match (q.predicate >> 8) % 4 {
330                    0 => WebRuleType::Shape,
331                    1 => WebRuleType::Confidence,
332                    2 => WebRuleType::Domain,
333                    _ => WebRuleType::Structural,
334                };
335                WebRule {
336                    rule_id: q.subject,
337                    rule_type,
338                    context_id: q.context,
339                    predicate_hash: q.object,
340                    min_confidence: (q.metadata & 0xFFFFFFFF) as f32 / u32::MAX as f32,
341                    required_domain: (q.metadata >> 32) & 0x0FFF_FFFF,
342                }
343            })
344            .collect()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn rule_creation() {
354        let rule = WebRule::new(123, WebRuleType::Confidence).with_confidence(0.8);
355        assert_eq!(rule.rule_id, 123);
356        assert_eq!(rule.min_confidence, 0.8);
357    }
358
359    #[test]
360    fn validator_no_rules_needs_no_validation() {
361        let validator = WebValidator::new();
362        assert!(!validator.needs_validation(&NQuin {
363            subject: 1,
364            predicate: 2,
365            object: 3,
366            context: 0,
367            metadata: 0,
368            parity: 0
369        }));
370    }
371
372    #[test]
373    fn validator_add_rule_triggers_needs_validation() {
374        let mut validator = WebValidator::new();
375        let rule = WebRule::new(123, WebRuleType::Confidence)
376            .with_predicate(456)
377            .with_context(789);
378        validator.add_rule(rule);
379        let quin = NQuin {
380            subject: 1,
381            predicate: 456,
382            object: 3,
383            context: 789,
384            metadata: 0,
385            parity: 0,
386        };
387        assert!(validator.needs_validation(&quin));
388    }
389
390    #[test]
391    fn validate_pass_high_confidence() {
392        let mut validator = WebValidator::new();
393        validator.set_confidence_threshold(0.5);
394        let quin = NQuin {
395            subject: 1,
396            predicate: 2,
397            object: 3,
398            context: 0,
399            metadata: u32::MAX as u64,
400            parity: 0,
401        };
402        assert_eq!(validator.validate(&quin), WebVerdict::Pass);
403    }
404
405    #[test]
406    fn validate_fail_zero_confidence() {
407        let mut validator = WebValidator::new();
408        validator.set_confidence_threshold(0.5);
409        let quin = NQuin {
410            subject: 1,
411            predicate: 2,
412            object: 3,
413            context: 0,
414            metadata: 0,
415            parity: 0,
416        };
417        assert!(matches!(validator.validate(&quin), WebVerdict::Fail(_)));
418    }
419
420    #[test]
421    fn validate_embedded_triple_pass() {
422        let validator = WebValidator::new();
423        let virtual_id = 0x1 | 0x123456789ABCDEF0;
424        let components = [1u64, 2, 3];
425        assert_eq!(
426            validator.validate_embedded_triple(virtual_id, &components, 0),
427            WebVerdict::Pass
428        );
429    }
430
431    #[test]
432    fn storage_round_trip() {
433        let rules = vec![
434            WebRule::new(123, WebRuleType::Confidence).with_confidence(0.8),
435            WebRule::new(456, WebRuleType::Domain).with_domain(789),
436        ];
437        let quins = WebRuleStorage::rules_to_quins(&rules);
438        assert_eq!(quins.len(), 2);
439        let back = WebRuleStorage::quins_to_rules(&quins);
440        assert_eq!(back.len(), 2);
441    }
442
443    #[test]
444    fn execute_query_program_bind_var() {
445        let quin = NQuin {
446            subject: 0xAABB,
447            predicate: 0xCCDD,
448            object: 0xEEFF,
449            context: 0,
450            metadata: 0,
451            parity: 0,
452        };
453        // MATCH_SUBJ 0xAABB, then BIND_VAR slot=OBJ reg=0, HALT
454        let mut program = vec![OP_MATCH_SUBJ];
455        program.extend_from_slice(&0xAABBu64.to_le_bytes());
456        program.extend_from_slice(&[OP_BIND_VAR, SLOT_OBJ, 0, OP_HALT]);
457        let results = execute_query_program(&program, &[quin]).unwrap();
458        assert_eq!(results, vec![0xEEFF]);
459    }
460}