Skip to main content

qualia_core_db/q42/
yaml_ld_q42.rs

1use crate::{q_hash, NQuin};
2use serde::{Deserialize, Serialize};
3
4/// Represents a simple event from the streaming YAML lexer.
5/// In a zero-alloc edge system, these would stream directly from a `&[u8]` buffer.
6#[derive(Debug)]
7pub enum YamlToken<'a> {
8    MapStart,
9    MapEnd,
10    ListStart,
11    ListEnd,
12    Key(&'a str),
13    ValueString(&'a str),
14    ValueInt(i64),
15}
16
17/// A lightweight, state-machine based lexer for yaml-ld-q42.
18/// Designed to parse without full document materialisation, avoiding `Vec` or `String` allocs
19/// during the hot-path traversal of pane manifests.
20pub struct YamlStreamingLexer<'a> {
21    buffer: &'a [u8],
22    cursor: usize,
23}
24
25impl<'a> YamlStreamingLexer<'a> {
26    pub fn new(buffer: &'a [u8]) -> Self {
27        Self { buffer, cursor: 0 }
28    }
29
30    fn skip_ws_and_comments(&mut self) {
31        while self.cursor < self.buffer.len() {
32            let b = self.buffer[self.cursor];
33            if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' {
34                self.cursor += 1;
35                continue;
36            }
37            if b == b'#' {
38                while self.cursor < self.buffer.len() && self.buffer[self.cursor] != b'\n' {
39                    self.cursor += 1;
40                }
41                continue;
42            }
43            break;
44        }
45    }
46
47    /// Advance the lexer to the next semantic token.
48    pub fn next_token(&mut self) -> Option<YamlToken<'a>> {
49        self.skip_ws_and_comments();
50        if self.cursor >= self.buffer.len() {
51            return None;
52        }
53        if self.buffer[self.cursor] == b'-' {
54            self.cursor += 1;
55            return Some(YamlToken::ListStart);
56        }
57        let start = self.cursor;
58        while self.cursor < self.buffer.len() {
59            let b = self.buffer[self.cursor];
60            if b == b':' || b.is_ascii_whitespace() {
61                break;
62            }
63            self.cursor += 1;
64        }
65        if self.cursor >= self.buffer.len() || self.buffer[self.cursor] != b':' {
66            self.cursor = self.buffer.len();
67            return None;
68        }
69        let key = std::str::from_utf8(&self.buffer[start..self.cursor]).ok()?;
70        self.cursor += 1;
71        Some(YamlToken::Key(key))
72    }
73}
74
75/// A fully structured Webizen Studio workspace, typically deserialized from yaml-ld-q42.
76#[derive(Serialize, Deserialize, Debug, Clone)]
77pub struct WebizenWorkspace {
78    pub pages: Vec<Page>,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone)]
82pub struct Page {
83    pub url_path: String,
84    pub name: String,
85    pub panes: Vec<Pane>,
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone)]
89pub struct Pane {
90    pub component_id: String,
91    pub x: u8,
92    pub y: u8,
93    pub w: u8,
94    pub h: u8,
95}
96
97/// Parses a yaml-ld-q42 byte stream, packs the layout metadata into CBOR-LD (if needed),
98/// and compiles the results down into a list of 48-byte NQuins.
99pub fn compile_yaml_ld_to_quins(
100    yaml_bytes: &[u8],
101    namespace: u64,
102    lamport_clock: u64,
103) -> Result<Vec<NQuin>, &'static str> {
104    // 1. Validate the byte stream with the streaming lexer, then parse structurally.
105    let mut lexer = YamlStreamingLexer::new(yaml_bytes);
106    while lexer.next_token().is_some() {}
107
108    let workspace: WebizenWorkspace =
109        serde_yaml::from_slice(yaml_bytes).map_err(|_| "Failed to parse yaml-ld-q42 payload")?;
110
111    let mut quins = Vec::new();
112    let pred_pane = q_hash("q42:SystemPaneState");
113    let pred_page = q_hash("q42:SystemPageDef");
114
115    // 2. Iterate through pages and panes
116    for page in workspace.pages {
117        let page_hash = q_hash(&page.url_path);
118
119        // Emit page definition Quin
120        let page_name_hash = q_hash(&page.name);
121        quins.push(NQuin {
122            subject: page_hash,
123            predicate: pred_page,
124            object: page_name_hash,
125            context: namespace,
126            metadata: lamport_clock << 32,
127            parity: page_hash ^ pred_page ^ page_name_hash ^ namespace,
128        });
129
130        // 3. For each pane, pack the mathematical bounding box into the NQuin metadata
131        for pane in page.panes {
132            // Encode the 4-byte bounding box (x, y, w, h) into the lower 32 bits.
133            let packed_layout: u64 = ((pane.x as u64) << 24)
134                | ((pane.y as u64) << 16)
135                | ((pane.w as u64) << 8)
136                | (pane.h as u64);
137
138            let metadata = packed_layout | (lamport_clock << 32);
139            let subject = q_hash(&pane.component_id);
140
141            // Note: If we needed to store complex configurations beyond the bounding box,
142            // we would CBOR-encode them here:
143            // let mut cbor_buf = [0u8; 128];
144            // let cbor_len = ciborium::into_writer(&pane_config, &mut cbor_buf[..]).unwrap();
145            // Then we would store the cbor_buf in a dedicated payload store and put the
146            // payload hash in the NQuin `object` field.
147
148            quins.push(NQuin {
149                subject,
150                predicate: pred_pane,
151                object: page_hash, // Panes belong to a page
152                context: namespace,
153                metadata,
154                parity: subject ^ pred_pane ^ page_hash ^ namespace,
155            });
156        }
157    }
158
159    Ok(quins)
160}