Skip to main content

qualia_client_core/
qapp_manifest.rs

1//! Boot-time Qapp capability compilation for the 42MB Sentinel hot loop.
2//!
3//! String parsing and `Vec`/`String` are permitted **only** during install/boot.
4//! Compiled capabilities are stored as fixed-size records keyed by `q_hash(app_id)`.
5
6use qualia_core_db::{q_hash, NQuin};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{OnceLock, RwLock};
10
11use crate::qapp_registry::QappPackageManifest;
12
13/// Maximum ontology domains compiled per app (Sentinel fixed buffer).
14pub const MAX_PERMITTED_DOMAINS: usize = 8;
15/// Maximum registered qapps in the in-process registry.
16pub const MAX_QAPP_REGISTRY: usize = 32;
17
18/// Host routing metadata (Flutter shell only — not consulted in the hot loop).
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct HostMetadata {
21    #[serde(default, skip_serializing_if = "String::is_empty")]
22    pub display_name: String,
23    #[serde(default, skip_serializing_if = "String::is_empty")]
24    pub entrypoint: String,
25    #[serde(default)]
26    pub chat_handoff_supported: bool,
27    #[serde(default, skip_serializing_if = "Vec::is_empty")]
28    pub surface_requirements: Vec<String>,
29}
30
31/// Capability claims compiled into hardware-aligned Quin records at install time.
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct CapabilityClaims {
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub required_ontologies: Vec<String>,
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub optional_remote_endpoints: Vec<String>,
38    /// Hex string: `0x00` Public, `0x01` Restricted, `0x02` Classified.
39    #[serde(default, rename = "max_sensitivity_clearance")]
40    pub max_sensitivity_clearance: String,
41    /// PINN model requirements for the application
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub required_pinn_models: Vec<String>,
44    /// Whether the app supports 1.58-bit ternary quantization
45    #[serde(default)]
46    pub supports_ternary_quantization: bool,
47}
48
49/// Developer-facing manifest passed across FRB during install/boot.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct QappManifest {
52    pub app_id: String,
53    #[serde(default)]
54    pub host_metadata: HostMetadata,
55    #[serde(default)]
56    pub capability_claims: CapabilityClaims,
57}
58
59/// Zero-allocation capability record used by the Sentinel during query execution.
60#[repr(C)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct CompiledCapability {
63    pub app_id_hash: u64,
64    pub clearance_level: u8,
65    pub domain_count: u8,
66    pub permitted_domains: [u64; MAX_PERMITTED_DOMAINS],
67}
68
69impl Default for CompiledCapability {
70    fn default() -> Self {
71        Self {
72            app_id_hash: 0,
73            clearance_level: NQuin::SENSITIVITY_PUBLIC,
74            domain_count: 0,
75            permitted_domains: [0u64; MAX_PERMITTED_DOMAINS],
76        }
77    }
78}
79
80#[derive(Debug, PartialEq, Eq)]
81pub enum QappInstallError {
82    RegistryFull,
83    EmptyAppId,
84    TooManyOntologies,
85    InvalidPinnConfig(String),
86    IncompatiblePinnModel(String),
87}
88
89static QAPP_REGISTRY: OnceLock<RwLock<[CompiledCapability; MAX_QAPP_REGISTRY]>> = OnceLock::new();
90static QAPP_REGISTRY_LEN: OnceLock<RwLock<usize>> = OnceLock::new();
91
92fn registry_slots() -> &'static RwLock<[CompiledCapability; MAX_QAPP_REGISTRY]> {
93    QAPP_REGISTRY.get_or_init(|| RwLock::new([CompiledCapability::default(); MAX_QAPP_REGISTRY]))
94}
95
96fn registry_len() -> &'static RwLock<usize> {
97    QAPP_REGISTRY_LEN.get_or_init(|| RwLock::new(0))
98}
99
100/// Parse clearance hex from manifest (`0x00`, `0x01`, `0x02`).
101pub fn parse_clearance(raw: &str) -> u8 {
102    let trimmed = raw.trim();
103    if trimmed.eq_ignore_ascii_case("restricted") || trimmed == "0x01" {
104        return NQuin::SENSITIVITY_RESTRICTED;
105    }
106    if trimmed.eq_ignore_ascii_case("classified") || trimmed == "0x02" {
107        return NQuin::SENSITIVITY_CLASSIFIED;
108    }
109    NQuin::SENSITIVITY_PUBLIC
110}
111
112/// Compile manifest strings into a fixed `CompiledCapability` record.
113pub fn compile_capability_record(
114    manifest: &QappManifest,
115) -> Result<CompiledCapability, QappInstallError> {
116    if manifest.app_id.is_empty() {
117        return Err(QappInstallError::EmptyAppId);
118    }
119
120    let app_id_hash = q_hash(&manifest.app_id);
121    let clearance_level = parse_clearance(&manifest.capability_claims.max_sensitivity_clearance);
122
123    let mut permitted_domains = [0u64; MAX_PERMITTED_DOMAINS];
124    let mut domain_count = 0usize;
125
126    for ontology in &manifest.capability_claims.required_ontologies {
127        if domain_count >= MAX_PERMITTED_DOMAINS {
128            return Err(QappInstallError::TooManyOntologies);
129        }
130        permitted_domains[domain_count] = q_hash(ontology);
131        domain_count += 1;
132    }
133
134    Ok(CompiledCapability {
135        app_id_hash,
136        clearance_level,
137        domain_count: domain_count as u8,
138        permitted_domains,
139    })
140}
141
142/// Encode a compiled capability as a 48-byte Quin for `.q42.bidx` persistence.
143pub fn compile_capability_quin(cap: &CompiledCapability) -> NQuin {
144    let predicate = q_hash("q42:qappCapability");
145    let object = cap.permitted_domains[0];
146    let context = (cap.clearance_level as u64) << 56;
147    let metadata = cap.domain_count as u64;
148    let parity = cap.app_id_hash ^ predicate ^ object ^ context;
149    NQuin {
150        subject: cap.app_id_hash,
151        predicate,
152        object,
153        context,
154        metadata,
155        parity,
156    }
157}
158
159/// Register (or replace) a qapp capability record. Install/boot only.
160pub fn compile_and_register_qapp(manifest: QappManifest) -> Result<u64, QappInstallError> {
161    let compiled = compile_capability_record(&manifest)?;
162    register_compiled_capability(compiled)?;
163    Ok(compiled.app_id_hash)
164}
165
166/// Alias required by the architecture spec.
167pub fn register_qapp(manifest: QappManifest) -> Result<u64, QappInstallError> {
168    compile_and_register_qapp(manifest)
169}
170
171fn register_compiled_capability(cap: CompiledCapability) -> Result<(), QappInstallError> {
172    let slots = registry_slots();
173    let len_lock = registry_len();
174    let mut slots = slots.write().map_err(|_| QappInstallError::RegistryFull)?;
175    let mut len = len_lock
176        .write()
177        .map_err(|_| QappInstallError::RegistryFull)?;
178
179    for slot in slots.iter_mut().take(*len) {
180        if slot.app_id_hash == cap.app_id_hash {
181            *slot = cap;
182            return Ok(());
183        }
184    }
185
186    if *len >= MAX_QAPP_REGISTRY {
187        return Err(QappInstallError::RegistryFull);
188    }
189    slots[*len] = cap;
190    *len += 1;
191    Ok(())
192}
193
194/// O(1) lookup for the Sentinel hot loop.
195pub fn get_compiled_capability(app_id_hash: u64) -> Option<CompiledCapability> {
196    let slots = registry_slots().read().ok()?;
197    let len = registry_len().read().ok()?;
198    for slot in slots.iter().take(*len) {
199        if slot.app_id_hash == app_id_hash {
200            return Some(*slot);
201        }
202    }
203    None
204}
205
206/// Build a compiled `QappManifest` from an on-disk `QappPackageManifest`.
207pub fn qapp_manifest_from_package(manifest: &QappPackageManifest) -> QappManifest {
208    let x = manifest.x_qualia.as_ref();
209    let app_id = x
210        .and_then(|x| (!x.app_id.is_empty()).then_some(x.app_id.clone()))
211        .unwrap_or_else(|| {
212            format!(
213                "did:qualia:qapp:{}",
214                manifest.name.to_lowercase().replace(' ', "-")
215            )
216        });
217
218    let host_metadata = HostMetadata {
219        display_name: x
220            .map(|x| x.display_name.clone())
221            .filter(|s| !s.is_empty())
222            .unwrap_or_else(|| manifest.name.clone()),
223        entrypoint: x
224            .and_then(|x| x.entrypoints.get("web").cloned())
225            .unwrap_or_else(|| "index.html".to_string()),
226        chat_handoff_supported: x
227            .and_then(|x| x.chat_integration.as_ref())
228            .map(|c| c.supports_launch_from_chat)
229            .unwrap_or(false),
230        surface_requirements: x.map(|x| x.ui_surfaces.clone()).unwrap_or_default(),
231    };
232
233    let mut required_ontologies = x.map(|x| x.required_ontologies.clone()).unwrap_or_default();
234    for shape in &manifest.required_shapes {
235        if required_ontologies.len() < MAX_PERMITTED_DOMAINS {
236            required_ontologies.push(shape.clone());
237        }
238    }
239
240    let capability_claims = CapabilityClaims {
241        required_ontologies,
242        required_pinn_models: vec![],
243        supports_ternary_quantization: false,
244        optional_remote_endpoints: x
245            .map(|x| x.optional_remote_endpoints.clone())
246            .unwrap_or_default(),
247        max_sensitivity_clearance: x
248            .map(|x| {
249                if x.max_sensitivity_clearance.is_empty() {
250                    "0x00".to_string()
251                } else {
252                    x.max_sensitivity_clearance.clone()
253                }
254            })
255            .unwrap_or_else(|| "0x00".to_string()),
256    };
257
258    QappManifest {
259        app_id,
260        host_metadata,
261        capability_claims,
262    }
263}
264
265/// Optional remote SPARQL endpoints (host network dispatcher only).
266pub fn remote_endpoints_for_app(app_id_hash: u64) -> Vec<String> {
267    // Endpoints are not compiled into the Sentinel registry; resolved from install metadata cache.
268    REMOTE_ENDPOINT_CACHE
269        .get_or_init(|| RwLock::new(HashMap::new()))
270        .read()
271        .ok()
272        .and_then(|cache| cache.get(&app_id_hash).cloned())
273        .unwrap_or_default()
274}
275
276static REMOTE_ENDPOINT_CACHE: OnceLock<RwLock<HashMap<u64, Vec<String>>>> = OnceLock::new();
277
278fn cache_remote_endpoints(app_id_hash: u64, endpoints: Vec<String>) {
279    if endpoints.is_empty() {
280        return;
281    }
282    let cache = REMOTE_ENDPOINT_CACHE.get_or_init(|| RwLock::new(HashMap::new()));
283    if let Ok(mut guard) = cache.write() {
284        guard.insert(app_id_hash, endpoints);
285    }
286}
287
288/// Full install pipeline: compile capabilities + cache remote endpoint metadata.
289pub fn install_qapp_capabilities(manifest: &QappPackageManifest) -> Result<u64, QappInstallError> {
290    let qapp = qapp_manifest_from_package(manifest);
291    let endpoints = qapp.capability_claims.optional_remote_endpoints.clone();
292    let app_id_hash = compile_and_register_qapp(qapp)?;
293    cache_remote_endpoints(app_id_hash, endpoints);
294
295    // Validate PINN model requirements if specified
296    if let Some(pinn_config) = manifest
297        .x_qualia
298        .as_ref()
299        .and_then(|ext| ext.pinn_model.as_ref())
300    {
301        validate_pinn_model_config(pinn_config)?;
302    }
303
304    Ok(app_id_hash)
305}
306
307/// Validate PINN model configuration for 1.58-bit ternary quantization
308pub fn validate_pinn_model_config(
309    config: &crate::qapp_registry::QappPinnModelConfig,
310) -> Result<(), QappInstallError> {
311    // Check quantization bits
312    if config.quantization_bits != "1.58" && !config.quantization_bits.is_empty() {
313        return Err(QappInstallError::InvalidPinnConfig(format!(
314            "Unsupported quantization bits: {}. Only 1.58-bit ternary quantization is supported.",
315            config.quantization_bits
316        )));
317    }
318
319    // Validate compression ratio (should be > 1.0 for effective compression)
320    if config.compression_ratio <= 1.0 && config.uses_ternary_quantization {
321        return Err(QappInstallError::InvalidPinnConfig(
322            "Compression ratio must be > 1.0 for ternary quantization".to_string(),
323        ));
324    }
325
326    // Check memory requirements
327    if config.memory_requirement_mb > 512 {
328        return Err(QappInstallError::InvalidPinnConfig(
329            "Memory requirement exceeds 512MB limit for edge deployment".to_string(),
330        ));
331    }
332
333    // Validate SMX version
334    if !config.smx_version.is_empty() && config.smx_version != "1.0" {
335        return Err(QappInstallError::InvalidPinnConfig(format!(
336            "Unsupported SMX version: {}. Only version 1.0 is supported.",
337            config.smx_version
338        )));
339    }
340
341    // Check required operations
342    if config.supported_operations.is_empty() {
343        return Err(QappInstallError::InvalidPinnConfig(
344            "At least one supported operation must be specified".to_string(),
345        ));
346    }
347
348    Ok(())
349}
350
351/// Check if manifest supports 1.58-bit ternary PINN models
352pub fn supports_ternary_pinn_models(manifest: &QappManifest) -> bool {
353    manifest.capability_claims.supports_ternary_quantization
354        && !manifest.capability_claims.required_pinn_models.is_empty()
355}
356
357/// Get PINN model memory requirements from manifest
358pub fn get_pinn_memory_requirements(manifest: &QappManifest) -> Option<u32> {
359    // This would typically come from the package manifest's x_qualia extension
360    // For now, return a default based on ternary quantization support
361    if supports_ternary_pinn_models(manifest) {
362        Some(256) // 256MB for ternary quantized models
363    } else {
364        None
365    }
366}
367
368/// Validate PINN model compatibility with app requirements
369pub fn validate_pinn_compatibility(
370    app_manifest: &QappManifest,
371    model_config: &crate::qapp_registry::QappPinnModelConfig,
372) -> Result<(), QappInstallError> {
373    // Check if app supports ternary quantization when model requires it
374    if model_config.uses_ternary_quantization
375        && !app_manifest.capability_claims.supports_ternary_quantization
376    {
377        return Err(QappInstallError::IncompatiblePinnModel(
378            "App does not support ternary quantization required by model".to_string(),
379        ));
380    }
381
382    // Check memory requirements
383    if let Some(app_memory_limit) = get_pinn_memory_requirements(app_manifest) {
384        if model_config.memory_requirement_mb > app_memory_limit {
385            return Err(QappInstallError::IncompatiblePinnModel(format!(
386                "Model requires {}MB, app limit is {}MB",
387                model_config.memory_requirement_mb, app_memory_limit
388            )));
389        }
390    }
391
392    // Check required models
393    if !app_manifest
394        .capability_claims
395        .required_pinn_models
396        .is_empty()
397        && !app_manifest
398            .capability_claims
399            .required_pinn_models
400            .contains(&model_config.model_name)
401    {
402        return Err(QappInstallError::IncompatiblePinnModel(format!(
403            "Model '{}' not in app's required models list",
404            model_config.model_name
405        )));
406    }
407
408    Ok(())
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn compiles_anatomy_domains() {
417        let manifest = QappManifest {
418            app_id: "did:qualia:qapp:anatomy".to_string(),
419            host_metadata: HostMetadata::default(),
420            capability_claims: CapabilityClaims {
421                required_ontologies: vec!["q42:anatomy".to_string(), "snomed:core".to_string()],
422                optional_remote_endpoints: vec![],
423                max_sensitivity_clearance: "0x00".to_string(),
424                required_pinn_models: vec![],
425                supports_ternary_quantization: false,
426            },
427        };
428        let cap = compile_capability_record(&manifest).unwrap();
429        assert_eq!(cap.clearance_level, NQuin::SENSITIVITY_PUBLIC);
430        assert_eq!(cap.domain_count, 2);
431        assert_eq!(cap.permitted_domains[0], q_hash("q42:anatomy"));
432    }
433}