Skip to main content

qualia_client_core/
qapp_api.rs

1//! Zero-allocation scoped query execution boundary for Flutter qapps.
2//!
3//! Hot-path functions use stack/fixed buffers only. Heap allocation is confined
4//! to the FRB wrapper that copies geometry into a Dart `Float32List`.
5
6use qualia_core_db::{
7    daemon_graph, mini_parser, q_hash, wal::WriteAheadLog, webizen_bytecode, NQuin,
8};
9
10use crate::qapp_manifest::{get_compiled_capability, CompiledCapability};
11
12/// Maximum Quin results per scoped query (42MB Sentinel budget).
13pub const MAX_QUERY_RESULTS: usize = 2048;
14/// Float32 values emitted per matched Quin (six u64 lanes, no inline float tagging).
15pub const FLOATS_PER_QUIN: usize = 6;
16pub const SCOPED_QUERY_MAX_FLOATS: usize = MAX_QUERY_RESULTS * FLOATS_PER_QUIN;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ExecutionError {
20    UnregisteredApp,
21    SanctuaryOverrideRequired,
22    OutputBufferFull,
23    InvalidBytecode,
24    ClearanceViolation,
25}
26
27impl ExecutionError {
28    pub fn as_str(self) -> &'static str {
29        match self {
30            Self::UnregisteredApp => "unregistered_app",
31            Self::SanctuaryOverrideRequired => "sanctuary_override_required",
32            Self::OutputBufferFull => "output_buffer_full",
33            Self::InvalidBytecode => "invalid_bytecode",
34            Self::ClearanceViolation => "clearance_violation",
35        }
36    }
37}
38
39/// Walk compiled bytecode and ensure bound operands stay inside declared ontology domains.
40pub fn verify_execution_scope(
41    caps: &CompiledCapability,
42    query_bytecode: &[u8],
43) -> Result<(), ExecutionError> {
44    let mut i = 0usize;
45    while i < query_bytecode.len() {
46        let opcode = query_bytecode[i];
47        match opcode {
48            mini_parser::OP_MATCH_SUBJECT
49            | mini_parser::OP_MATCH_PREDICATE
50            | mini_parser::OP_MATCH_OBJECT => {
51                if i + 9 > query_bytecode.len() {
52                    return Err(ExecutionError::InvalidBytecode);
53                }
54                let mut bytes = [0u8; 8];
55                bytes.copy_from_slice(&query_bytecode[i + 1..i + 9]);
56                let operand = u64::from_le_bytes(bytes);
57                if operand != 0 && !domain_permitted(caps, operand) {
58                    return Err(ExecutionError::SanctuaryOverrideRequired);
59                }
60                i += 9;
61            }
62            mini_parser::OP_HALT_IF_FALSE => i += 1,
63            mini_parser::OP_END => break,
64            _ => return Err(ExecutionError::InvalidBytecode),
65        }
66    }
67    Ok(())
68}
69
70#[inline]
71fn domain_permitted(caps: &CompiledCapability, hash: u64) -> bool {
72    if hash == caps.app_id_hash {
73        return true;
74    }
75    let count = caps.domain_count as usize;
76    for slot in caps.permitted_domains.iter().take(count) {
77        if *slot == hash {
78            return true;
79        }
80    }
81    false
82}
83
84fn wal_path() -> Option<String> {
85    let state = crate::state::APP_STATE.get()?;
86    let storage = state.config.lock().ok()?.storage_path.clone();
87    Some(format!("{storage}/qualia_global.wal"))
88}
89
90/// Append a conduct-violation Quin when a qapp breaches its compiled capability scope.
91pub fn append_sanctuary_violation(app_id_hash: u64, reason: &'static str) {
92    let mut quin = NQuin::new_conduct_violation(reason.as_bytes());
93    quin.subject = app_id_hash;
94    quin.predicate = q_hash("q42:conductViolation");
95    quin.context = q_hash("q42:qappSanctuaryOverride");
96    quin.parity = quin.subject ^ quin.predicate ^ quin.object ^ quin.context;
97
98    if let Some(path) = wal_path() {
99        if let Ok(mut wal) = WriteAheadLog::open(path) {
100            let _ = wal.append_mutation(&quin);
101        }
102    }
103}
104
105/// Map matched Quins into a flat `f32` lane for Dart `Float32List` rendering.
106/// Uses `f32::from_bits` on the low 32 bits — no scalar tagging in Quin objects.
107pub fn translate_quin_to_geometry(quin: &NQuin, out: &mut [f32], base: usize) -> usize {
108    if base + FLOATS_PER_QUIN > out.len() {
109        return 0;
110    }
111    out[base] = f32::from_bits(quin.subject as u32);
112    out[base + 1] = f32::from_bits(quin.predicate as u32);
113    out[base + 2] = f32::from_bits(quin.object as u32);
114    out[base + 3] = f32::from_bits(quin.context as u32);
115    out[base + 4] = f32::from_bits(quin.metadata as u32);
116    out[base + 5] = f32::from_bits(quin.parity as u32);
117    FLOATS_PER_QUIN
118}
119
120#[inline]
121fn result_passes_clearance(quin: &NQuin, clearance: u8) -> bool {
122    quin.get_sensitivity_byte() <= clearance
123}
124
125/// Hot-path scoped query — **must not allocate heap memory**.
126pub fn execute_scoped_query_in_place(
127    app_id_hash: u64,
128    query_bytecode: &[u8],
129    geometry_out: &mut [f32],
130) -> Result<usize, ExecutionError> {
131    let caps = get_compiled_capability(app_id_hash).ok_or(ExecutionError::UnregisteredApp)?;
132
133    if let Err(err @ ExecutionError::SanctuaryOverrideRequired) =
134        verify_execution_scope(&caps, query_bytecode)
135    {
136        append_sanctuary_violation(app_id_hash, err.as_str());
137        return Err(err);
138    }
139
140    let graph_guard = daemon_graph::graph_read_guard();
141    let db = graph_guard.as_slice();
142
143    let mut result_buffer = [NQuin::default(); MAX_QUERY_RESULTS];
144    let (match_count, _vm_cycles) =
145        webizen_bytecode::execute_program(query_bytecode, db, &mut result_buffer, None).map_err(
146            |e| match e {
147                webizen_bytecode::VmError::OutputBufferFull => ExecutionError::OutputBufferFull,
148                webizen_bytecode::VmError::InvalidProgram => ExecutionError::InvalidBytecode,
149                webizen_bytecode::VmError::HaltViolation => ExecutionError::InvalidBytecode,
150            },
151        )?;
152
153    let mut float_offset = 0usize;
154    for quin in &result_buffer[..match_count] {
155        if !result_passes_clearance(quin, caps.clearance_level) {
156            append_sanctuary_violation(app_id_hash, ExecutionError::ClearanceViolation.as_str());
157            return Err(ExecutionError::ClearanceViolation);
158        }
159        let written = translate_quin_to_geometry(quin, geometry_out, float_offset);
160        if written == 0 {
161            return Err(ExecutionError::OutputBufferFull);
162        }
163        float_offset += written;
164    }
165
166    Ok(float_offset)
167}
168
169/// FRB/diagnostic wrapper — may allocate when copying geometry for Dart.
170pub fn execute_scoped_query(
171    app_id_hash: u64,
172    query_bytecode: Vec<u8>,
173) -> Result<Vec<f32>, ExecutionError> {
174    let mut scratch = [0f32; SCOPED_QUERY_MAX_FLOATS];
175    let len = execute_scoped_query_in_place(app_id_hash, &query_bytecode, &mut scratch)?;
176    Ok(scratch[..len].to_vec())
177}
178
179/// Compile a wildcard anatomy graph query into fixed bytecode (install/boot helper).
180pub fn compile_wildcard_graph_query(program: &mut [u8; 1024]) -> Result<usize, ExecutionError> {
181    mini_parser::compile_ntriples_to_bytecode(b"?subject ?predicate ?object .", program)
182        .map_err(|_| ExecutionError::InvalidBytecode)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::qapp_manifest::{
189        compile_and_register_qapp, CapabilityClaims, HostMetadata, QappManifest,
190    };
191    use qualia_core_db::daemon_graph;
192
193    fn register_anatomy_app() -> u64 {
194        let manifest = QappManifest {
195            app_id: "did:qualia:qapp:anatomy".to_string(),
196            host_metadata: HostMetadata::default(),
197            capability_claims: CapabilityClaims {
198                required_ontologies: vec![
199                    "q42:anatomy".to_string(),
200                    "https://qualia.anatomy.example/ontology/bio#".to_string(),
201                ],
202                optional_remote_endpoints: vec![],
203                max_sensitivity_clearance: "0x00".to_string(),
204                required_pinn_models: vec![],
205                supports_ternary_quantization: false,
206            },
207        };
208        compile_and_register_qapp(manifest).unwrap()
209    }
210
211    #[test]
212    fn rejects_forbidden_domain_operands() {
213        let app_id_hash = register_anatomy_app();
214        let caps = get_compiled_capability(app_id_hash).unwrap();
215
216        let mut prog = [0u8; 64];
217        prog[0] = mini_parser::OP_MATCH_SUBJECT;
218        let forbidden = q_hash("q42:financial");
219        prog[1..9].copy_from_slice(&forbidden.to_le_bytes());
220        prog[9] = mini_parser::OP_END;
221
222        assert_eq!(
223            verify_execution_scope(&caps, &prog[..10]),
224            Err(ExecutionError::SanctuaryOverrideRequired)
225        );
226    }
227
228    #[test]
229    fn zero_allocation_scoped_query_hot_path() {
230        daemon_graph::init_daemon_graph("/tmp/qualia-qapp-test");
231        let app_id_hash = register_anatomy_app();
232
233        let mut prog = [0u8; 1024];
234        let prog_len =
235            mini_parser::compile_ntriples_to_bytecode(b"?subject ?predicate ?object .", &mut prog)
236                .unwrap();
237
238        let _profiler = dhat::Profiler::builder().testing().build();
239        let mut out = [0f32; 512];
240        let result = execute_scoped_query_in_place(app_id_hash, &prog[..prog_len], &mut out);
241        assert!(result.is_ok());
242        assert!(result.unwrap() > 0);
243
244        let stats = dhat::HeapStats::get();
245        assert_eq!(
246            stats.curr_blocks, 0,
247            "execute_scoped_query_in_place must not allocate on the heap"
248        );
249        assert_eq!(stats.curr_bytes, 0);
250    }
251}
252
253// ── DICOM split-ingest (Core 3) + volume query (FRB boundary) ─────────────────
254
255fn storage_root() -> Result<std::path::PathBuf, String> {
256    let state = crate::state::APP_STATE
257        .get()
258        .ok_or("APP_STATE not initialized")?;
259    Ok(std::path::PathBuf::from(
260        state
261            .config
262            .lock()
263            .map_err(|e| e.to_string())?
264            .storage_path
265            .clone(),
266    ))
267}
268
269/// Submit `.dcm` split-ingest to Core 3; returns lock-free job id.
270pub fn submit_dicom_ingest(file_path: String, patient_did_hash: u64) -> Result<u64, String> {
271    let root = storage_root()?;
272    qualia_core_db::dicom_ingest::init_core3_dicom_worker(root.clone());
273    qualia_core_db::dicom_ingest::submit_dicom_ingest(
274        &root,
275        std::path::Path::new(&file_path),
276        patient_did_hash,
277    )
278    .map_err(|e| e.to_string())
279}
280
281/// Poll Core 3 ingest status: 0=pending, 1=complete, 2=failed.
282pub fn dicom_ingest_status(job_id: u64) -> u8 {
283    qualia_core_db::dicom_ingest::dicom_ingest_status(job_id)
284}
285
286/// Memory-map read of ingested pixel payload (single copy for Dart `Uint8List`).
287pub fn execute_dicom_volume_query(
288    patient_did_hash: u64,
289    series_hash: u64,
290) -> Result<Vec<u8>, String> {
291    let root = storage_root()?;
292    let record = qualia_core_db::dicom_ingest::find_series_record(patient_did_hash, series_hash)
293        .ok_or_else(|| "DICOM series not found in ingest registry".to_string())?;
294    qualia_core_db::dicom_ingest::read_volume_bytes(&root, &record).map_err(|e| e.to_string())
295}
296
297/// Comorbidity verdicts using the in-process daemon graph (FRB-friendly).
298pub fn eval_comorbidity_json_from_daemon(
299    patient_did_hash: u64,
300    target_organ_hash: u64,
301) -> Result<String, String> {
302    let graph_guard = qualia_core_db::daemon_graph::graph_read_guard();
303    eval_comorbidity_json(patient_did_hash, target_organ_hash, graph_guard.as_slice())
304}
305
306/// Hot-path comorbidity evaluation — stack buffers only inside engine call.
307pub fn eval_comorbidity_json(
308    patient_did_hash: u64,
309    target_organ_hash: u64,
310    graph_quins: &[qualia_core_db::NQuin],
311) -> Result<String, String> {
312    use qualia_core_db::comorbidity_eval::{
313        eval_comorbidity, ComorbidityVerdict, MAX_COMORBIDITY_VERDICTS,
314    };
315
316    let mut out = [ComorbidityVerdict {
317        condition_hash: 0,
318        compounded_risk_milli: 0,
319        status: qualia_core_db::comorbidity_eval::ComorbidityStatus::Active,
320        _pad: [0; 3],
321    }; MAX_COMORBIDITY_VERDICTS];
322
323    let count = eval_comorbidity(patient_did_hash, target_organ_hash, graph_quins, &mut out)
324        .map_err(|e| format!("{e:?}"))?;
325
326    let verdicts: Vec<serde_json::Value> = out[..count]
327        .iter()
328        .map(|v| {
329            serde_json::json!({
330                "conditionHash": format!("{:#018x}", v.condition_hash),
331                "compoundedRiskMilli": v.compounded_risk_milli,
332                "status": v.status as u8,
333            })
334        })
335        .collect();
336
337    serde_json::to_string(&serde_json::json!({ "verdicts": verdicts })).map_err(|e| e.to_string())
338}