1use 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
13pub const MAX_PERMITTED_DOMAINS: usize = 8;
15pub const MAX_QAPP_REGISTRY: usize = 32;
17
18#[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#[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 #[serde(default, rename = "max_sensitivity_clearance")]
40 pub max_sensitivity_clearance: String,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub required_pinn_models: Vec<String>,
44 #[serde(default)]
46 pub supports_ternary_quantization: bool,
47}
48
49#[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#[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
100pub 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
112pub 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
142pub 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
159pub 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
166pub 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
194pub 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
206pub 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
265pub fn remote_endpoints_for_app(app_id_hash: u64) -> Vec<String> {
267 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
288pub 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 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
307pub fn validate_pinn_model_config(
309 config: &crate::qapp_registry::QappPinnModelConfig,
310) -> Result<(), QappInstallError> {
311 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 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 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 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 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
351pub 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
357pub fn get_pinn_memory_requirements(manifest: &QappManifest) -> Option<u32> {
359 if supports_ternary_pinn_models(manifest) {
362 Some(256) } else {
364 None
365 }
366}
367
368pub fn validate_pinn_compatibility(
370 app_manifest: &QappManifest,
371 model_config: &crate::qapp_registry::QappPinnModelConfig,
372) -> Result<(), QappInstallError> {
373 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 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 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}