Skip to main content

qualia_cli/
llm_testing.rs

1//! LLM Testing Integration for CLI
2//!
3//! Simple LLM model testing functionality for the CLI.
4
5use crate::llm_lifecycle::{default_vault_path, init_log_stream};
6use qualia_client_core::model_lifecycle::{resolve_vault_model, scan_vault_gguf, VaultGgufEntry};
7use std::path::{Path, PathBuf};
8
9/// CLI command to run comprehensive LLM model tests
10pub fn run_test_models(
11    vault_path: Option<PathBuf>,
12    models: Option<Vec<String>>,
13    _quantization: Option<String>,
14    verbose: bool,
15) -> Result<(), String> {
16    let vault_path = vault_path.unwrap_or_else(default_vault_path);
17
18    if verbose {
19        init_log_stream(true);
20    }
21
22    println!("๐Ÿš€ Starting LLM Model Testing CLI");
23    println!("๐Ÿ“ Vault path: {}", vault_path.display());
24
25    // Scan for GGUF models in vault
26    let available_models =
27        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
28
29    if available_models.is_empty() {
30        return Err("No GGUF models found in vault".to_string());
31    }
32
33    println!("๐Ÿ“ฆ Found {} model(s):", available_models.len());
34    for model in &available_models {
35        println!("  - {}", model.name);
36    }
37
38    // Filter models if specific ones requested
39    let test_models = if let Some(ref requested) = models {
40        available_models
41            .iter()
42            .filter(|m| requested.contains(&m.name))
43            .cloned()
44            .collect()
45    } else {
46        available_models
47    };
48
49    if test_models.is_empty() {
50        return Err("No matching models found".to_string());
51    }
52
53    println!("\n๐Ÿงช Testing {} model(s)...", test_models.len());
54
55    for model in &test_models {
56        println!("\n๐Ÿ” Testing: {}", model.name);
57        match test_single_model(&vault_path, model, verbose) {
58            Ok(result) => {
59                println!("  โœ… Load time: {}ms", result.load_time_ms);
60                println!("  โœ… Memory: {}MB", result.memory_mb);
61                println!(
62                    "  โœ… Status: {}",
63                    if result.success { "PASS" } else { "FAIL" }
64                );
65            }
66            Err(e) => {
67                println!("  โŒ Error: {}", e);
68            }
69        }
70    }
71
72    println!("\nโœ… Testing complete!");
73    Ok(())
74}
75
76/// Comprehensive LLM capability test (load + inference in one session)
77pub fn run_comprehensive_llm_test(
78    vault_path: Option<PathBuf>,
79    model_name: String,
80    verbose: bool,
81) -> Result<(), String> {
82    let vault_path = vault_path.unwrap_or_else(default_vault_path);
83
84    if verbose {
85        init_log_stream(true);
86    }
87
88    println!("๐Ÿงช Running Comprehensive LLM Test Suite");
89    println!("๐Ÿ“ Vault path: {}", vault_path.display());
90    println!("๐Ÿค– Model: {}", model_name);
91    println!();
92
93    // Step 1: Load the model
94    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
95    println!("STEP 1: Loading Model");
96    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
97
98    let gguf = resolve_vault_model(&vault_path, &model_name)
99        .map_err(|e| format!("Failed to resolve model: {}", e))?;
100
101    println!("Loading {} โ€ฆ", gguf.display());
102    let start = std::time::Instant::now();
103
104    let record = tokio::task::block_in_place(|| {
105        tokio::runtime::Handle::current().block_on(
106            qualia_client_core::model_lifecycle::activate_vault_gguf(&gguf),
107        )
108    })
109    .map_err(|e| format!("Failed to activate model: {}", e))?;
110
111    let load_time = start.elapsed();
112    println!("โœ… Model loaded in {:?}", load_time);
113    println!("  Profile ID: 0x{:016x}", record.profile_id);
114    println!("  Context Window: {}", record.context_window);
115    println!();
116
117    // Step 2: Create agent
118    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
119    println!("STEP 2: Creating Agent");
120    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
121
122    use qualia_core_db::llm_agent::{AgentBackend, LocalLlmAgent};
123
124    let agent = LocalLlmAgent::with_local_backend(
125        format!("did:qualia:cli-test:{}", record.profile_id),
126        AgentBackend::Local {
127            model_path: record.gguf_path.clone(),
128            context_window: record.context_window,
129            quantization: record.quantization.clone(),
130            vision_projector_path: record.mmproj_path.clone(),
131            modality: record.modality.clone(),
132            architecture: record.architecture.clone(),
133        },
134    );
135
136    println!("โœ… Agent created");
137    println!();
138
139    // Step 3: Run inference tests
140    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
141    println!("STEP 3: Inference Tests");
142    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
143
144    // Sample (temperature / top-p / repeat-penalty) rather than greedy argmax, so instruct models
145    // produce varied, on-topic text instead of collapsing into repetition / a fixed attractor.
146    qualia_core_db::llm_bench::set_sampler_config(Some(
147        qualia_core_db::sampler::SamplerConfig::chat_default(),
148    ));
149
150    let test_prompts = vec![
151        ("Basic Knowledge", "What is the capital of France?", 50),
152        (
153            "System Awareness",
154            "What is QualiaDB and what are its main features?",
155            100,
156        ),
157        (
158            "Technical Understanding",
159            "Explain what a NQuin is in simple terms.",
160            80,
161        ),
162        (
163            "Capability Awareness",
164            "What capabilities does the Qualia system have for semantic graph processing?",
165            120,
166        ),
167        (
168            "Instruction Following",
169            "Write a haiku about artificial intelligence.",
170            30,
171        ),
172    ];
173
174    let mut total_tokens = 0;
175    let mut total_time_ms = 0;
176    let mut total_ttft_ms = 0;
177    let mut successful_tests = 0;
178
179    for (test_name, prompt, _max_tokens) in test_prompts.iter() {
180        println!("โ”Œโ”€ Test: {}", test_name);
181        println!("โ”œโ”€ Prompt: {}", prompt);
182
183        let started = std::time::Instant::now();
184
185        // Return layout: (text, provenance_hashes, tokens_generated, semantic_quin).
186        // BUGFIX: the second field is provenance (often len=1), NOT token ids โ€” using
187        // `.len()` reported Tokens:1 and ~100ร— low tok/s. The third field is the count.
188        let (response, _provenance, tokens_generated, _quin) = tokio::task::block_in_place(|| {
189            agent.infer_local_model_streaming::<fn(String)>(prompt, "graph_context:cli_test", None)
190        });
191
192        let elapsed = started.elapsed();
193        let elapsed_ms = elapsed.as_millis() as u64;
194
195        // Estimate TTFT as ~10% of total time (rough approximation without streaming)
196        let ttft = elapsed_ms / 10;
197
198        let token_count = tokens_generated as u64;
199        // Prefer decoded-token estimate when the engine returns 0 but produced text
200        // (defensive โ€” should not happen on a healthy path).
201        let token_count = if token_count == 0 && !response.is_empty() {
202            // Rough BPE estimate: ~4 chars/token for Latin text.
203            (response.chars().count() as u64 / 4).max(1)
204        } else {
205            token_count
206        };
207        let tps = if elapsed_ms > 0 {
208            (token_count as f64 * 1000.0) / elapsed_ms as f64
209        } else {
210            0.0
211        };
212
213        println!("โ”œโ”€ TTFT: {}ms (estimated)", ttft);
214        println!("โ”œโ”€ Total Time: {}ms", elapsed_ms);
215        println!("โ”œโ”€ Tokens: {} (decode)", token_count);
216        println!("โ”œโ”€ TPS: {:.2} (wall-clock / decode tokens)", tps);
217
218        total_tokens += token_count;
219        total_time_ms += elapsed_ms;
220        total_ttft_ms += ttft;
221        successful_tests += 1;
222
223        print!("โ””โ”€ Response: ");
224        // Show first 200 chars of response
225        let preview: String = response.chars().take(200).collect();
226        println!(
227            "{}{}",
228            preview,
229            if response.len() > 200 { "..." } else { "" }
230        );
231        println!();
232    }
233
234    // Step 4: Summary
235    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
236    println!("TEST SUMMARY");
237    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
238    println!("โœ… Model Loading: PASS ({:?})", load_time);
239    println!("โœ… Agent Creation: PASS");
240    println!(
241        "โœ… Inference: {} / {} tests passed",
242        successful_tests,
243        test_prompts.len()
244    );
245
246    if successful_tests > 0 {
247        let avg_ttft = total_ttft_ms as f64 / successful_tests as f64;
248        let avg_tps = if total_time_ms > 0 {
249            (total_tokens as f64 * 1000.0) / total_time_ms as f64
250        } else {
251            0.0
252        };
253
254        println!();
255        println!("๐Ÿ“Š METRICS:");
256        println!("  โ””โ”€ Total Tokens Generated: {}", total_tokens);
257        println!("  โ””โ”€ Total Generation Time: {}ms", total_time_ms);
258        println!("  โ””โ”€ Average TTFT: {:.2}ms", avg_ttft);
259        println!("  โ””โ”€ Average TPS: {:.2}", avg_tps);
260    }
261
262    println!();
263    println!("Note: Metrics include orchestration overhead (Webizen validation, etc.).");
264    println!("Note: Token counts use engine tokens_generated (not provenance-hash vec length).");
265
266    Ok(())
267}
268
269/// Convert a GGUF import file to native `.p64` + canonical `.q42` model metadata.
270///
271/// Design: GGUF/safetensors are import formats only. Steady-state activation should
272/// prefer the converted container (see `docs/plans/native-inference-p64-pipeline-remediation.md`).
273/// The helper is **CBOR-LD** (self-describe CBOR), never JSON.
274pub fn run_convert_gguf_to_p64(
275    input: &Path,
276    out_dir: &Path,
277    page_log2: u16,
278    layout: &str,
279) -> Result<(), String> {
280    if !input.is_file() {
281        return Err(format!("input not found: {}", input.display()));
282    }
283    let ext = input
284        .extension()
285        .and_then(|e| e.to_str())
286        .unwrap_or("")
287        .to_ascii_lowercase();
288    if ext != "gguf" && ext != "safetensors" {
289        return Err(format!(
290            "only .gguf or .safetensors import is supported in this command (got .{ext})"
291        ));
292    }
293    let src_len = std::fs::metadata(input).map(|m| m.len()).unwrap_or(0);
294    // 12 GB class card default budget when auto-selecting f16 expand.
295    const DEFAULT_VRAM_BUDGET: u64 = 12u64 * 1024 * 1024 * 1024;
296    let layout = match layout.trim().to_ascii_lowercase().as_str() {
297        "verbatim" | "raw" | "copy" => qualia_core_db::p64_weight::P64ConvertLayout::Verbatim,
298        "f16" | "fp16" | "half" => qualia_core_db::p64_weight::P64ConvertLayout::F16Expand,
299        "soa" | "q4k-soa" | "q4k_soa" | "soa-q4k" => {
300            qualia_core_db::p64_weight::P64ConvertLayout::Q4kSoa
301        }
302        "auto" | "best" | "remarkable" => {
303            let rec =
304                qualia_core_db::p64_weight::recommend_convert_layout(src_len, DEFAULT_VRAM_BUDGET);
305            println!("โ”œโ”€ auto layout โ†’ {rec:?} (source {src_len} B, 12 GiB VRAM budget)");
306            rec
307        }
308        other => {
309            return Err(format!(
310                "unknown --layout '{other}' (expected verbatim|f16|soa|auto)"
311            ))
312        }
313    };
314
315    std::fs::create_dir_all(out_dir)
316        .map_err(|e| format!("create out dir {}: {e}", out_dir.display()))?;
317
318    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
319    println!("Model ({ext}) โ†’ p64 + q42 convert");
320    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
321    println!("โ”œโ”€ Input:  {}", input.display());
322    println!("โ”œโ”€ Out:    {}", out_dir.display());
323    println!("โ”œโ”€ page_log2: {page_log2}");
324    println!("โ””โ”€ layout: {layout:?}");
325
326    let t0 = std::time::Instant::now();
327    let mmap = {
328        let f = std::fs::File::open(input).map_err(|e| format!("open: {e}"))?;
329        // SAFETY: file is read-only; we do not write through the mapping.
330        unsafe { memmap2::Mmap::map(&f).map_err(|e| format!("mmap: {e}"))? }
331    };
332    let src_bytes = mmap.len();
333    println!(
334        "โ”œโ”€ Source size: {:.1} MiB",
335        src_bytes as f64 / (1024.0 * 1024.0)
336    );
337
338    let p64 = if ext == "safetensors" {
339        let mut buf = Vec::new();
340        qualia_core_db::p64_weight::transcode_safetensor_to_p64(&mmap, page_log2, &mut buf)
341            .map_err(|e| format!("transcode_safetensor_to_p64: {e}"))?;
342        buf
343    } else {
344        qualia_core_db::p64_weight::compile_gguf_to_p64_with_layout(&mmap, page_log2, layout)
345            .map_err(|e| format!("compile_gguf_to_p64: {e}"))?
346    };
347    let stem = input
348        .file_stem()
349        .and_then(|s| s.to_str())
350        .unwrap_or("model");
351    let suffix = match layout {
352        qualia_core_db::p64_weight::P64ConvertLayout::Verbatim => "",
353        qualia_core_db::p64_weight::P64ConvertLayout::F16Expand => ".f16",
354        qualia_core_db::p64_weight::P64ConvertLayout::Q4kSoa => ".soa",
355    };
356    let p64_path = out_dir.join(format!("{stem}{suffix}.p64"));
357    std::fs::write(&p64_path, &p64).map_err(|e| format!("write p64: {e}"))?;
358
359    // Canonical Q42 v3 metadata: behaviour the engine should not re-guess from GGUF.
360    let tok = qualia_core_db::gguf_sharder::GgufTokenizer::from_gguf(&mmap);
361    let stop_ids: Vec<u32> = tok.stop_tokens().to_vec();
362    let stop_names: Vec<String> = stop_ids
363        .iter()
364        .filter_map(|&id| tok.vocab.get(id as usize).cloned())
365        .collect();
366    let helper = qualia_core_db::model_helper::ModelHelper::new(
367        input
368            .file_name()
369            .and_then(|s| s.to_str())
370            .unwrap_or("model.gguf"),
371        p64_path
372            .file_name()
373            .and_then(|s| s.to_str())
374            .unwrap_or("model.p64"),
375        page_log2,
376        format!("{layout:?}"),
377        qualia_core_db::model_helper::ModelHelperTokenizer {
378            bos_token_id: tok.bos_token_id,
379            eos_token_id: tok.eos_token_id,
380            add_bos_token: tok.add_bos_token,
381            chat_family: format!("{:?}", tok.chat_family()),
382            stop_token_ids: stop_ids,
383            stop_token_strings: stop_names,
384            vocab_len: tok.vocab_len(),
385        },
386    );
387    let q42_path = helper
388        .write_beside_p64(&p64_path)
389        .map_err(|e| format!("write canonical q42 helper: {e}"))?;
390
391    // Validate the container can be indexed (fail closed if we wrote garbage).
392    let index = qualia_core_db::p64_weight::P64TensorIndex::from_p64(&p64)
393        .map_err(|e| format!("p64 self-check failed: {e}"))?;
394    let n_tensors = index.entries.len();
395    // Round-trip through the real Q42 volume reader so a malformed helper fails closed.
396    let _ = qualia_core_db::model_helper::ModelHelper::load_beside_p64(&p64_path)
397        .map_err(|e| format!("helper self-check failed: {e}"))?
398        .ok_or_else(|| "helper self-check failed: canonical .q42 was not found".to_string())?;
399
400    let elapsed = t0.elapsed();
401    println!();
402    println!("โœ… Convert complete in {:.1}s", elapsed.as_secs_f64());
403    println!("  โ””โ”€ {}", p64_path.display());
404    println!(
405        "     size {:.1} MiB, tensors {}",
406        p64.len() as f64 / (1024.0 * 1024.0),
407        n_tensors
408    );
409    println!("  โ””โ”€ {} (Q42 v3)", q42_path.display());
410    println!(
411        "     chat_family={:?} stop_ids={:?}",
412        tok.chat_family(),
413        tok.stop_tokens()
414    );
415    println!();
416    println!("Activate with a Local backend path pointing at the .p64 file.");
417    Ok(())
418}
419
420/// Remarkable one-shot: optional passport + auto-layout convert + activate knobs.
421pub fn run_optimize_pipeline(
422    input: &Path,
423    out: Option<PathBuf>,
424    skip_passport: bool,
425) -> Result<(), String> {
426    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
427    println!("REMARKABLE PATH โ€” passport + convert + activate knobs");
428    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
429
430    if !skip_passport {
431        let _ = run_hardware_passport(true, 512, None, true, None, 16);
432    }
433
434    let out_dir = out.unwrap_or_else(|| {
435        input
436            .parent()
437            .map(|p| p.to_path_buf())
438            .unwrap_or_else(|| PathBuf::from("."))
439    });
440    run_convert_gguf_to_p64(input, &out_dir, 14, "auto")?;
441
442    let stem = input
443        .file_stem()
444        .and_then(|s| s.to_str())
445        .unwrap_or("model");
446    // auto may produce .f16.p64 / .soa.p64 / .p64 โ€” list what we wrote
447    let candidates = [
448        out_dir.join(format!("{stem}.f16.p64")),
449        out_dir.join(format!("{stem}.soa.p64")),
450        out_dir.join(format!("{stem}.p64")),
451    ];
452    let p64 = candidates.iter().find(|p| p.is_file());
453    println!();
454    println!("Activate (fast path):");
455    println!("  $env:QUALIA_P64_INTEGRITY='metadata'");
456    if let Some(p) = p64 {
457        println!("  # model path: {}", p.display());
458        if let Ok(Some(h)) = qualia_core_db::model_helper::ModelHelper::load_beside_p64(p) {
459            println!(
460                "  # helper: layout={} family={} stops={:?}",
461                h.layout, h.tokenizer.chat_family, h.tokenizer.stop_token_ids
462            );
463        }
464    }
465    println!("  qualia-cli llm load <stem-or-path>   # vault prefers .p64");
466    Ok(())
467}
468
469/// Probe / load HardwarePassport and print the ranked circuit matrix.
470///
471/// `decode_proxy`: `None` = skip; `Some(None)` = auto-find smollm; `Some(Some(path))` = use path.
472pub fn run_hardware_passport(
473    reprobe: bool,
474    gemv_n: usize,
475    cache: Option<PathBuf>,
476    apply_env_hint: bool,
477    decode_proxy: Option<Option<PathBuf>>,
478    decode_proxy_tokens: u32,
479) -> Result<(), String> {
480    use qualia_core_db::device_benchmark::benchmark_devices;
481    use qualia_core_db::hardware_passport::{
482        attach_decode_proxy_via_subprocess, backend_env_token, default_cache_path,
483        default_decode_proxy_model, load_or_probe, topology_key, write_passport, HardwarePassport,
484        PASSPORT_VERSION,
485    };
486    use qualia_core_db::host_topology::probe_host_topology;
487
488    let path = cache.unwrap_or_else(default_cache_path);
489    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
490    println!("HardwarePassport");
491    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
492    println!("โ”œโ”€ Cache: {}", path.display());
493    println!("โ”œโ”€ GEMV n: {gemv_n}");
494    println!("โ””โ”€ Reprobe: {reprobe}");
495
496    let (mut passport, was_cached) = if reprobe {
497        if path.exists() {
498            let _ = std::fs::remove_file(&path);
499        }
500        let topology = probe_host_topology();
501        let key = topology_key(&topology);
502        println!("โ”œโ”€ Probing circuits (this takes a few seconds)โ€ฆ");
503        let matrix = benchmark_devices(gemv_n);
504        let preferred = matrix
505            .best()
506            .and_then(|c| backend_env_token(&c.backend))
507            .map(str::to_string);
508        let fresh = HardwarePassport {
509            version: PASSPORT_VERSION,
510            key,
511            topology,
512            matrix,
513            preferred_inference_backend: preferred,
514            probe_gemv_n: gemv_n,
515            decode_proxy_model: None,
516            decode_proxy_tokens: 0,
517        };
518        write_passport(&fresh, &path)?;
519        (fresh, false)
520    } else {
521        load_or_probe(&path, gemv_n)
522    };
523
524    // Optional decode-proxy ranking (subprocess per backend โ€” shared_gpu is process-wide).
525    if let Some(model_opt) = decode_proxy {
526        let model = match model_opt {
527            Some(p) => p,
528            None => default_decode_proxy_model().ok_or_else(|| {
529                "no decode-proxy model: pass --decode-proxy <path> or set QUALIA_LLM_PROFILE_MODEL / place smollm under C:/LLM_Models/P64".to_string()
530            })?,
531        };
532        if !model.is_file() {
533            return Err(format!("decode-proxy model not found: {}", model.display()));
534        }
535        let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?;
536        println!(
537            "โ”œโ”€ Decode-proxy: {} ({} tokens, child process per GPU backend)โ€ฆ",
538            model.display(),
539            decode_proxy_tokens
540        );
541        attach_decode_proxy_via_subprocess(&mut passport.matrix, &model, decode_proxy_tokens, &exe);
542        passport.decode_proxy_model = Some(model.display().to_string());
543        passport.decode_proxy_tokens = decode_proxy_tokens;
544        passport.preferred_inference_backend = passport
545            .matrix
546            .best()
547            .and_then(|c| backend_env_token(&c.backend))
548            .map(str::to_string);
549        write_passport(&passport, &path)?;
550        println!("โ”œโ”€ Decode-proxy ranking applied + cache updated");
551    }
552
553    println!(
554        "โ”œโ”€ Source: {}",
555        if was_cached {
556            "cache hit (fast-boot)"
557        } else {
558            "fresh probe"
559        }
560    );
561    println!("โ”œโ”€ Key: {}", passport.key);
562    if let Some(ref m) = passport.decode_proxy_model {
563        println!(
564            "โ”œโ”€ Decode-proxy model: {m} ({} tokens)",
565            passport.decode_proxy_tokens
566        );
567    }
568    println!("{}", passport.matrix.summary());
569
570    if let Some(ref pref) = passport.preferred_inference_backend {
571        println!("โ”œโ”€ Preferred inference backend (stored): {pref}");
572    }
573    if let Some(best) = passport.matrix.best() {
574        println!("Selected inference circuit (measured):");
575        println!(
576            "  โ””โ”€ {} [{}] {:.3} ms/GEMV  {:.1} GFLOP/s{}",
577            best.label,
578            best.backend,
579            best.ms_per_gemv,
580            best.gflops,
581            best.decode_proxy_tok_s
582                .map(|t| format!("  {t:.2} tok/s decode-proxy"))
583                .unwrap_or_default()
584        );
585        let hint = passport
586            .preferred_inference_backend
587            .clone()
588            .or_else(|| backend_env_token(&best.backend).map(str::to_string));
589        if let Some(h) = hint {
590            println!("  โ””โ”€ Hint: set QUALIA_WGPU_BACKEND={h} to pin this backend");
591            println!(
592                "  โ””โ”€ Fast P64 activate: QUALIA_P64_INTEGRITY=metadata (after trusted convert)"
593            );
594            if apply_env_hint {
595                let hint_path = path.with_extension("env");
596                std::fs::write(
597                    &hint_path,
598                    format!("QUALIA_WGPU_BACKEND={h}\nQUALIA_P64_INTEGRITY=metadata\n"),
599                )
600                .map_err(|e| format!("write env hint: {e}"))?;
601                println!("  โ””โ”€ Wrote {}", hint_path.display());
602            }
603        } else {
604            println!("  โ””โ”€ Best circuit is CPU โ€” keep GPU default; no QUALIA_WGPU_BACKEND pin");
605        }
606    }
607    Ok(())
608}
609
610/// Quant-graph dry-run: ground a prompt+answer pair (forces mode logic via unconditional ground).
611pub fn run_ground_check(prompt: &str, answer: &str) -> Result<(), String> {
612    use qualia_core_db::{active_inference_mode, fact_count, ground_generation};
613    let g = ground_generation(prompt, answer);
614    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
615    println!("Quant-graph grounding check");
616    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
617    println!("โ”œโ”€ active mode: {}", active_inference_mode().as_str());
618    println!("โ”œโ”€ fact_count:  {}", fact_count());
619    println!("โ”œโ”€ repaired:    {}", g.repaired);
620    println!("โ”œโ”€ reason:      {:?}", g.reason);
621    println!(
622        "โ”œโ”€ object_hash: {:?}",
623        g.object_hash.map(|h| format!("{h:#x}"))
624    );
625    println!("โ””โ”€ text:        {}", g.text);
626    Ok(())
627}
628
629/// Seed quant-graph facts from bundled TSV / QUALIA_GROUNDING_FACTS.
630pub fn run_seed_grounding() -> Result<(), String> {
631    let n = qualia_core_db::seed_facts_from_bundled();
632    println!(
633        "seeded {n} grounding facts (fact_count={})",
634        qualia_core_db::fact_count()
635    );
636    Ok(())
637}
638
639/// Dense CUDA WMMA microbench (mode-independent; reports whether TC path is live).
640pub fn run_cuda_tc_microbench(side: usize) -> Result<(), String> {
641    use std::time::Instant;
642    let n = side.max(16);
643    // Pad to multiple of 16 for WMMA tile.
644    let n = ((n + 15) / 16) * 16;
645    let k = n;
646    let m = n;
647    let a = vec![1.0f32; m * k];
648    let b = vec![1.0f32; k * n];
649    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
650    println!("CUDA TC microbench  C[{m}ร—{n}] = A[{m}ร—{k}]ยทB[{k}ร—{n}]");
651    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
652    // Warm + timed
653    let _ = qualia_core_db::wgsl_forge::dispatch::ensure_cuda_runtime_path();
654    let t0 = Instant::now();
655    // CUDA TC microbench: exercise the reduced-precision path (the CUDA WMMA tier).
656    let r1 = qualia_core_db::wgsl_forge::dispatch::gemm_f32_tc_reduced(m, k, n, &a, &b)
657        .map_err(|e| format!("gemm_f32_tc_reduced: {e:?}"))?;
658    let warm_ms = t0.elapsed().as_secs_f64() * 1000.0;
659    let t1 = Instant::now();
660    let r2 = qualia_core_db::wgsl_forge::dispatch::gemm_f32_tc_reduced(m, k, n, &a, &b)
661        .map_err(|e| format!("gemm_f32_tc_reduced: {e:?}"))?;
662    let hot_ms = t1.elapsed().as_secs_f64() * 1000.0;
663    let caps = qualia_core_db::wgsl_forge::dispatch::caps();
664    println!(
665        "โ”œโ”€ caps: wgpu={} cuda={} coopmat={}",
666        caps.wgpu, caps.cuda, caps.coopmat
667    );
668    println!("โ”œโ”€ warm: {warm_ms:.2} ms (includes NVRTC/context first use)");
669    println!("โ”œโ”€ hot:  {hot_ms:.2} ms");
670    println!(
671        "โ”œโ”€ C[0]={:.1} (expect ~{n}.0 for all-ones)",
672        r2.first().copied().unwrap_or(0.0)
673    );
674    println!("โ””โ”€ ok:   r1_len={} r2_len={}", r1.len(), r2.len());
675    Ok(())
676}
677
678/// Print or set multi-mode inference approach (portable / cuda / quant-graph).
679pub fn run_inference_mode(set: Option<&str>) -> Result<(), String> {
680    use qualia_core_db::{active_inference_mode, set_inference_mode, InferenceMode};
681    if let Some(name) = set {
682        let m = InferenceMode::parse(name).ok_or_else(|| {
683            format!("unknown mode '{name}' (expected: portable | cuda | quant-graph | fast-verify)")
684        })?;
685        set_inference_mode(m);
686        // Also pin env so child processes (decode-proxy) inherit.
687        std::env::set_var("QUALIA_INFERENCE_MODE", m.as_str());
688        println!("MODE set={}", m.as_str());
689        println!("  {}", m.description());
690        println!("  (shell: $env:QUALIA_INFERENCE_MODE='{}')", m.as_str());
691        return Ok(());
692    }
693    let active = active_inference_mode();
694    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
695    println!("Inference modes (coexisting approaches โ€” not replacements)");
696    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
697    println!("Active: {} โ€” {}", active.as_str(), active.description());
698    println!();
699    for m in InferenceMode::ALL {
700        let mark = if m == active { "*" } else { " " };
701        println!("  [{mark}] {:12}  {}", m.as_str(), m.description());
702    }
703    println!();
704    println!("Set:  qualia-cli llm mode <portable|cuda|quant-graph>");
705    println!("Env:  QUALIA_INFERENCE_MODE");
706    println!("Plan: docs/plans/inference-multi-mode-and-compression.md");
707    Ok(())
708}
709
710/// Inference superiority lab instruments.
711pub fn run_lab(
712    action: &str,
713    model: Option<&std::path::Path>,
714    tokens: u32,
715    n_in: usize,
716    n_out: usize,
717    gemv_n: usize,
718    out: Option<&std::path::Path>,
719    hours: f64,
720    max_generations: u32,
721    ollama_model: Option<&str>,
722    ollama_url: &str,
723    no_ollama: bool,
724) -> Result<(), String> {
725    use qualia_core_db::lab::{
726        ablate::format_ablation_report, audit_hot_path, calibrate_device_roof,
727        format_lockin_summary, run_ablation_matrix, run_auto_improve, run_decode_timeline,
728        run_q4k_soa_microbench, AutoImproveConfig,
729    };
730    match action.trim().to_ascii_lowercase().as_str() {
731        "audit-path" | "audit" => {
732            print!("{}", audit_hot_path().format_report());
733            Ok(())
734        }
735        "roof" | "device-roof" => {
736            print!("{}", calibrate_device_roof(gemv_n).format_report());
737            let (g, i) = qualia_core_db::lab::device_roof::cpu_q4_intensity_probe(1024, 32);
738            println!("  cpu_q4_probe: {g:.2} GFLOP/s  intensity={i:.3} FLOP/B");
739            Ok(())
740        }
741        "micro" | "microbench" => {
742            print!("{}", run_q4k_soa_microbench(n_in, n_out).format_report());
743            Ok(())
744        }
745        "timeline" => {
746            let m = model.ok_or("timeline requires --model <path.p64>")?;
747            let t = if tokens == 0 { 4 } else { tokens };
748            print!("{}", run_decode_timeline(m, t).format_report());
749            Ok(())
750        }
751        "ablate" | "ablation" => {
752            let m = model.ok_or("ablate requires --model <path.p64>")?;
753            let t = if tokens == 0 { 8 } else { tokens };
754            let csv =
755                out.or_else(|| Some(std::path::Path::new("experiments/inference-lab/runs.csv")));
756            let rows = run_ablation_matrix(m, t, csv);
757            print!("{}", format_ablation_report(&rows));
758            if let Some(p) = csv {
759                println!("CSV appended: {}", p.display());
760            }
761            Ok(())
762        }
763        "auto" | "auto-improve" | "lockin" | "self-improve" => {
764            let m = model.ok_or(
765                "lab auto requires --model <path.p64> (e.g. smollm2 or Llama-3.2-3B .p64)",
766            )?;
767            let t = if tokens == 0 { 16 } else { tokens };
768            let hours = if hours <= 0.0 { 2.0 } else { hours };
769            let out_dir = out
770                .map(|p| p.to_path_buf())
771                .unwrap_or_else(|| std::path::PathBuf::from("experiments/inference-lab/lockin"));
772            let ollama = if no_ollama {
773                None
774            } else {
775                match ollama_model {
776                    Some(s) if s.is_empty() || s.eq_ignore_ascii_case("none") => None,
777                    Some(s) => Some(s.to_string()),
778                    None => Some("qualia-smol-q8:latest".into()),
779                }
780            };
781            let cfg = AutoImproveConfig {
782                model: m.to_path_buf(),
783                tokens: t,
784                max_duration: std::time::Duration::from_secs_f64(hours * 3600.0),
785                out_dir,
786                ollama_model: ollama,
787                ollama_url: ollama_url.to_string(),
788                elite_resample: 3,
789                plateau_rel: 0.02,
790                plateau_gens: 2,
791                max_generations: max_generations.max(1),
792            };
793            println!("lab auto โ€” recursive measure โ†’ search โ†’ lock-in");
794            println!("  model:      {}", cfg.model.display());
795            println!("  tokens:     {}", cfg.tokens);
796            println!("  hours:      {hours}");
797            println!("  gens:       {}", cfg.max_generations);
798            println!("  out:        {}", cfg.out_dir.display());
799            println!(
800                "  ollama:     {}",
801                cfg.ollama_model.as_deref().unwrap_or("(skipped)")
802            );
803            println!("  (wall clock budget; plateau or gens may finish earlier)");
804            let pkg = run_auto_improve(&cfg)?;
805            print!("{}", format_lockin_summary(&pkg));
806            println!("Lock-in package written to: {}", pkg.out_dir.display());
807            println!(
808                "  BEST_CONFIG.json  METHODOLOGY.md  apply-best.ps1  runs.csv  LOCKIN_SUMMARY.txt"
809            );
810            Ok(())
811        }
812        "gpu-cap" | "gpu-capability" | "machine-gpu" => {
813            let t = if tokens == 0 { 16 } else { tokens };
814            run_gpu_capability_campaign(model, out, t)
815        }
816        "help" | _ => {
817            println!("qualia-cli llm lab <action>");
818            println!("  audit-path              hot-path wiring audit");
819            println!("  roof [--gemv-n N]       device roof calibration");
820            println!("  micro [--n-in N --n-out M]  Q4 SoA GEMV microbench");
821            println!("  timeline --model P [--tokens T]  decode phase timeline");
822            println!("  ablate --model P [--tokens T] [--out runs.csv]");
823            println!("  auto --model P [--hours H] [--tokens T] [--out lockin-dir]");
824            println!("       [--max-generations N] [--ollama-model TAG] [--no-ollama]");
825            println!("       multi-hour recursive search โ†’ lock-in package");
826            println!("  gpu-cap [--model P] [--tokens T] [--out dir]");
827            println!("       native GPU tier probe + backendร—mode decode matrix");
828            println!("       โ†’ machine-gpu-profile.json + apply-machine-gpu.ps1");
829            println!("Plan: docs/plans/inference-superiority-lab-and-toolset-plan.md");
830            if action != "help" && !action.is_empty() && action != "_" {
831                return Err(format!("unknown lab action '{action}'"));
832            }
833            Ok(())
834        }
835    }
836}
837
838/// Probe which **native** GPU tiers this host has, measure the backend ร— mode decode matrix
839/// in child processes, and write `machine-gpu-profile.json` + `apply-machine-gpu.ps1`.
840///
841/// WGSL/wgpu is the portable floor; CUDA-C/PTX, HLSL+DXC, MSL, subgroups and coopmat are higher
842/// tiers when present. Only a **coherent** measurement can be recommended (speed without sense
843/// is failure), so the profile ranks by tok/s among coherent rows only.
844///
845/// One child process per cell: `shared_gpu` is process-wide, so `QUALIA_WGPU_BACKEND` cannot be
846/// re-pointed in-process.
847pub fn run_gpu_capability_campaign(
848    model: Option<&Path>,
849    out_dir: Option<&Path>,
850    tokens: u32,
851) -> Result<(), String> {
852    use qualia_core_db::machine_gpu_profile::{
853        AdapterFeatures, MachineGpuProfile, MeasuredDecodePath, ToolchainAvailability,
854        MACHINE_GPU_PROFILE_VERSION,
855    };
856    use std::process::Command;
857
858    fn tool_ok(program: &str, arg: &str) -> bool {
859        Command::new(program)
860            .arg(arg)
861            .output()
862            .map(|o| o.status.success())
863            .unwrap_or(false)
864    }
865
866    let p64 = match model {
867        Some(p) => p.to_path_buf(),
868        None => {
869            qualia_core_db::hardware_passport::default_decode_proxy_model().ok_or_else(|| {
870                "no package: pass --model <path.p64> or set QUALIA_LLM_PROFILE_MODEL".to_string()
871            })?
872        }
873    };
874    if !p64.is_file() {
875        return Err(format!("package not found: {}", p64.display()));
876    }
877    let out = out_dir
878        .map(|p| p.to_path_buf())
879        .unwrap_or_else(|| p64.parent().unwrap_or(Path::new(".")).to_path_buf());
880    let tokens = tokens.clamp(8, 128);
881
882    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
883    println!("GPU CAPABILITY โ€” native tiers over the WGSL floor");
884    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
885    println!("โ”œโ”€ Package: {}", p64.display());
886    println!("โ”œโ”€ Tokens:  {tokens}");
887    println!("โ”œโ”€ Out:     {}", out.display());
888
889    // โ”€โ”€ Toolchain โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
890    let nvcc = std::env::var("CUDA_PATH")
891        .map(|p| format!("{p}/bin/nvcc"))
892        .unwrap_or_else(|_| "nvcc".to_string());
893    let dxc = std::env::var("QUALIA_DXC_PATH").unwrap_or_else(|_| "dxc".to_string());
894    let cuda_toolkit = tool_ok(&nvcc, "--version");
895    let dxc_cli = tool_ok(&dxc, "--version");
896    let metal_xcrun = cfg!(target_os = "macos") && tool_ok("xcrun", "--version");
897
898    // โ”€โ”€ Adapter (parent device; children create their own) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
899    let gpu = qualia_core_db::gpu_context::try_shared_gpu();
900    let adapter = match gpu {
901        Some(g) => AdapterFeatures {
902            name: g.adapter_caps.name.clone(),
903            backend: g.adapter_caps.backend_label().to_string(),
904            discrete: g.adapter_caps.device_type_label() == "discrete",
905            subgroups: g.adapter_caps.features.subgroup,
906            coopmat: g.adapter_caps.cooperative_matrix_tile_count > 0,
907            shader_f16: g.adapter_caps.features.shader_f16,
908            timestamp_query: g.timestamps_supported,
909            topology_hash: qualia_core_db::hardware_passport::topology_key(
910                &qualia_core_db::host_topology::probe_host_topology(),
911            ),
912        },
913        None => AdapterFeatures::default(),
914    };
915    println!(
916        "โ”œโ”€ Adapter: {} ({}) subgroups={} coopmat={} f16={}",
917        if adapter.name.is_empty() {
918            "none"
919        } else {
920            &adapter.name
921        },
922        adapter.backend,
923        adapter.subgroups,
924        adapter.coopmat,
925        adapter.shader_f16
926    );
927    println!(
928        "โ”œโ”€ Toolchain: wgpu={} cuda={cuda_toolkit} dxc_cli={dxc_cli} metal={metal_xcrun}",
929        gpu.is_some()
930    );
931
932    let mut native_tiers = vec!["wgsl".to_string(), "spirv".to_string()];
933    if cuda_toolkit {
934        native_tiers.push("cuda-c".into());
935        native_tiers.push("ptx".into());
936    }
937    if dxc_cli {
938        native_tiers.push("hlsl-dxc".into());
939    }
940    if metal_xcrun {
941        native_tiers.push("msl".into());
942    }
943    if adapter.subgroups {
944        native_tiers.push("subgroups".into());
945    }
946    if adapter.coopmat {
947        native_tiers.push("coopmat".into());
948    }
949
950    // โ”€โ”€ Decode matrix (child process per cell) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
951    let backends: &[&str] = if cfg!(target_os = "macos") {
952        &["metal"]
953    } else if cfg!(target_os = "windows") {
954        &["vulkan", "dx12"]
955    } else {
956        &["vulkan"]
957    };
958    let modes = ["portable", "fast-verify", "cuda"];
959    let self_exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?;
960    let mut measured: Vec<MeasuredDecodePath> = Vec::new();
961
962    let wgpu_cells = backends.len() * modes.len();
963    let cuda_c_cells = if cuda_toolkit { 1 } else { 0 };
964    let hlsl_cells = if dxc_cli { 1 } else { 0 };
965    let spirv_cells = if dxc_cli { 1 } else { 0 };
966    let ptx_cells = if cuda_toolkit { 1 } else { 0 };
967    println!(
968        "โ”œโ”€ Decode matrix ({} cells)โ€ฆ",
969        wgpu_cells + cuda_c_cells + hlsl_cells + spirv_cells + ptx_cells
970    );
971    for backend in backends {
972        for mode in modes {
973            print!("โ”‚   {backend:7} {mode:12} โ€ฆ ");
974            use std::io::Write as _;
975            let _ = std::io::stdout().flush();
976            let t0 = std::time::Instant::now();
977            let output = Command::new(&self_exe)
978                .args([
979                    "llm",
980                    "decode-proxy",
981                    &p64.display().to_string(),
982                    "--tokens",
983                    &tokens.to_string(),
984                ])
985                .env("QUALIA_WGPU_BACKEND", backend)
986                .env("QUALIA_INFERENCE_MODE", mode)
987                .env_remove("QUALIA_FORGE_BACKEND")
988                .env_remove("QUALIA_DXC_PATH")
989                .output();
990            let wall = t0.elapsed().as_secs_f64();
991            match output {
992                Ok(o) => {
993                    let stdout = String::from_utf8_lossy(&o.stdout);
994                    match qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout) {
995                        Some(rec) => {
996                            let coherence_ok = rec.coherence_ok.unwrap_or(o.status.success());
997                            println!(
998                                "{:.2} tok/s  {}  ({wall:.1}s)",
999                                rec.tok_s,
1000                                if coherence_ok {
1001                                    "coherent"
1002                                } else {
1003                                    "INCOHERENT"
1004                                }
1005                            );
1006                            measured.push(MeasuredDecodePath {
1007                                wgpu_backend: (*backend).to_string(),
1008                                inference_mode: mode.to_string(),
1009                                p64_path: p64.display().to_string(),
1010                                tok_s: rec.tok_s,
1011                                coherence_ok,
1012                                tokens,
1013                            });
1014                        }
1015                        None => {
1016                            let stderr = String::from_utf8_lossy(&o.stderr);
1017                            let tail: String = stderr.lines().rev().take(1).collect();
1018                            println!("no DECODE_PROXY line ({wall:.1}s) {tail}");
1019                        }
1020                    }
1021                }
1022                Err(e) => println!("spawn failed ({wall:.1}s): {e}"),
1023            }
1024        }
1025    }
1026
1027    // โ”€โ”€ CUDA-C native decode row (lab path: CUDA SoA layer) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1028    // Exercises the actual CUDA execution lane: QUALIA_LLM_CUDA_DECODE=1 opts
1029    // into the layer-by-layer CUDA SoA decode path (device RoPE/KV/SDPA +
1030    // sticky Q4_K_SOA), bypassing the wgpu resident mega-pass. This is the
1031    // path that uses forge CUDA-C shader emission (gemv_f32, gemm_f32_tc,
1032    // ternary_gemv, p64_project, fft) via NVRTC on the NVIDIA driver.
1033    if cuda_toolkit {
1034        print!("โ”‚   cuda-c  native        โ€ฆ ");
1035        use std::io::Write as _;
1036        let _ = std::io::stdout().flush();
1037        let t0 = std::time::Instant::now();
1038        let output = Command::new(&self_exe)
1039            .args([
1040                "llm",
1041                "decode-proxy",
1042                &p64.display().to_string(),
1043                "--tokens",
1044                &tokens.to_string(),
1045            ])
1046            .env("QUALIA_INFERENCE_MODE", "cuda")
1047            .env("QUALIA_LLM_CUDA_DECODE", "1")
1048            .env("QUALIA_LLM_KV_INT8", "0")
1049            .env_remove("QUALIA_FORGE_BACKEND")
1050            .env_remove("QUALIA_DXC_PATH")
1051            .output();
1052        let wall = t0.elapsed().as_secs_f64();
1053        match output {
1054            Ok(o) => {
1055                let stdout = String::from_utf8_lossy(&o.stdout);
1056                match qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout) {
1057                    Some(rec) => {
1058                        let path_ok = rec.execution_path.as_deref() == Some("cuda-c");
1059                        let coherence_ok =
1060                            rec.coherence_ok.unwrap_or(o.status.success()) && path_ok;
1061                        println!(
1062                            "{:.2} tok/s  {}  path={}  ({wall:.1}s)",
1063                            rec.tok_s,
1064                            if coherence_ok { "coherent" } else { "REJECTED" },
1065                            rec.execution_path.as_deref().unwrap_or("unattributed"),
1066                        );
1067                        if path_ok {
1068                            measured.push(MeasuredDecodePath {
1069                                wgpu_backend: "cuda-c".to_string(),
1070                                inference_mode: "native".to_string(),
1071                                p64_path: p64.display().to_string(),
1072                                tok_s: rec.tok_s,
1073                                coherence_ok,
1074                                tokens,
1075                            });
1076                        }
1077                    }
1078                    None => {
1079                        let stderr = String::from_utf8_lossy(&o.stderr);
1080                        let tail: String = stderr.lines().rev().take(1).collect();
1081                        println!("no DECODE_PROXY line ({wall:.1}s) {tail}");
1082                    }
1083                }
1084            }
1085            Err(e) => println!("spawn failed ({wall:.1}s): {e}"),
1086        }
1087    }
1088
1089    // โ”€โ”€ HLSL native shader rows (forge HLSL โ†’ DXC โ†’ SPIR-V โ†’ wgpu) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1090    // Exercises the forge's HLSL emitter + DXC SPIR-V compilation path. The
1091    // resulting SPIR-V feeds into the same wgpu pipeline (same buffers, slab,
1092    // dispatch) โ€” only the shader compilation differs from the WGSL path.
1093    // Requires the DXC CLI (`QUALIA_DXC_PATH` or `dxc` on PATH).
1094    if dxc_cli {
1095        // HLSLโ†’SPIR-Vโ†’wgpu: only run with vulkan backend because
1096        // QUALIA_DXC_PATH (needed for HLSLโ†’SPIR-V via DXC CLI) also overrides
1097        // wgpu's DX12 compiler path, causing dx12 to fail.
1098        let hlsl_backends: &[&str] = if cfg!(target_os = "macos") {
1099            &["metal"]
1100        } else {
1101            &["vulkan"]
1102        };
1103        for backend in hlsl_backends {
1104            print!("โ”‚   hlsl    {backend:7}    โ€ฆ ");
1105            use std::io::Write as _;
1106            let _ = std::io::stdout().flush();
1107            let t0 = std::time::Instant::now();
1108            let output = Command::new(&self_exe)
1109                .args([
1110                    "llm",
1111                    "decode-proxy",
1112                    &p64.display().to_string(),
1113                    "--tokens",
1114                    &tokens.to_string(),
1115                ])
1116                .env("QUALIA_WGPU_BACKEND", backend)
1117                .env("QUALIA_FORGE_BACKEND", "hlsl")
1118                .output();
1119            let wall = t0.elapsed().as_secs_f64();
1120            match output {
1121                Ok(o) => {
1122                    let stdout = String::from_utf8_lossy(&o.stdout);
1123                    match qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout) {
1124                        Some(rec) => {
1125                            let path_ok = rec.execution_path.as_deref() == Some("hlsl");
1126                            let coherence_ok =
1127                                rec.coherence_ok.unwrap_or(o.status.success()) && path_ok;
1128                            println!(
1129                                "{:.2} tok/s  {}  path={}  ({wall:.1}s)",
1130                                rec.tok_s,
1131                                if coherence_ok { "coherent" } else { "REJECTED" },
1132                                rec.execution_path.as_deref().unwrap_or("unattributed"),
1133                            );
1134                            if path_ok {
1135                                measured.push(MeasuredDecodePath {
1136                                    wgpu_backend: format!("hlsl-{backend}"),
1137                                    inference_mode: "portable".to_string(),
1138                                    p64_path: p64.display().to_string(),
1139                                    tok_s: rec.tok_s,
1140                                    coherence_ok,
1141                                    tokens,
1142                                });
1143                            }
1144                        }
1145                        None => {
1146                            let stderr = String::from_utf8_lossy(&o.stderr);
1147                            let tail: String = stderr.lines().rev().take(1).collect();
1148                            println!("no DECODE_PROXY line ({wall:.1}s) {tail}");
1149                        }
1150                    }
1151                }
1152                Err(e) => println!("spawn failed ({wall:.1}s): {e}"),
1153            }
1154        }
1155    }
1156
1157    // โ”€โ”€ SPIR-V (DXC) native shader row (forge SPIR-V โ†’ DXC โ†’ wgpu) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1158    // Exercises the forge's SPIR-V emitter with DXC-produced SPIR-V.
1159    // Same wgpu pipeline, but pre-compiled SPIR-V from DXC instead of naga.
1160    if dxc_cli {
1161        let spirv_backends: &[&str] = if cfg!(target_os = "macos") {
1162            &["metal"]
1163        } else {
1164            &["vulkan"]
1165        };
1166        for backend in spirv_backends {
1167            print!("โ”‚   spirv-dxc {backend:7} โ€ฆ ");
1168            use std::io::Write as _;
1169            let _ = std::io::stdout().flush();
1170            let t0 = std::time::Instant::now();
1171            let output = Command::new(&self_exe)
1172                .args([
1173                    "llm",
1174                    "decode-proxy",
1175                    &p64.display().to_string(),
1176                    "--tokens",
1177                    &tokens.to_string(),
1178                ])
1179                .env("QUALIA_WGPU_BACKEND", backend)
1180                .env("QUALIA_FORGE_BACKEND", "spirv")
1181                .output();
1182            let wall = t0.elapsed().as_secs_f64();
1183            match output {
1184                Ok(o) => {
1185                    let stdout = String::from_utf8_lossy(&o.stdout);
1186                    match qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout) {
1187                        Some(rec) => {
1188                            let path_ok = rec.execution_path.as_deref() == Some("spirv");
1189                            let coherence_ok =
1190                                rec.coherence_ok.unwrap_or(o.status.success()) && path_ok;
1191                            println!(
1192                                "{:.2} tok/s  {}  path={}  ({wall:.1}s)",
1193                                rec.tok_s,
1194                                if coherence_ok { "coherent" } else { "REJECTED" },
1195                                rec.execution_path.as_deref().unwrap_or("unattributed"),
1196                            );
1197                            if path_ok {
1198                                measured.push(MeasuredDecodePath {
1199                                    wgpu_backend: format!("spirv-dxc-{backend}"),
1200                                    inference_mode: "portable".to_string(),
1201                                    p64_path: p64.display().to_string(),
1202                                    tok_s: rec.tok_s,
1203                                    coherence_ok,
1204                                    tokens,
1205                                });
1206                            }
1207                        }
1208                        None => {
1209                            let stderr = String::from_utf8_lossy(&o.stderr);
1210                            let tail: String = stderr.lines().rev().take(1).collect();
1211                            println!("no DECODE_PROXY line ({wall:.1}s) {tail}");
1212                        }
1213                    }
1214                }
1215                Err(e) => println!("spawn failed ({wall:.1}s): {e}"),
1216            }
1217        }
1218    }
1219
1220    // โ”€โ”€ PTX native shader row (forge PTX โ†’ CUDA driver) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1221    // Exercises the forge's PTX emitter with direct CUDA driver execution.
1222    // Only available when CUDA toolkit is present.
1223    if cuda_toolkit {
1224        print!("โ”‚   ptx     cuda        โ€ฆ ");
1225        use std::io::Write as _;
1226        let _ = std::io::stdout().flush();
1227        let t0 = std::time::Instant::now();
1228        let output = Command::new(&self_exe)
1229            .args([
1230                "llm",
1231                "decode-proxy",
1232                &p64.display().to_string(),
1233                "--tokens",
1234                &tokens.to_string(),
1235            ])
1236            .env("QUALIA_FORGE_BACKEND", "ptx")
1237            .output();
1238        let wall = t0.elapsed().as_secs_f64();
1239        match output {
1240            Ok(o) => {
1241                let stdout = String::from_utf8_lossy(&o.stdout);
1242                match qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout) {
1243                    Some(rec) => {
1244                        let path_ok = rec.execution_path.as_deref() == Some("ptx");
1245                        let coherence_ok =
1246                            rec.coherence_ok.unwrap_or(o.status.success()) && path_ok;
1247                        println!(
1248                            "{:.2} tok/s  {}  path={}  ({wall:.1}s)",
1249                            rec.tok_s,
1250                            if coherence_ok { "coherent" } else { "REJECTED" },
1251                            rec.execution_path.as_deref().unwrap_or("unattributed"),
1252                        );
1253                        if path_ok {
1254                            measured.push(MeasuredDecodePath {
1255                                wgpu_backend: "ptx-cuda".to_string(),
1256                                inference_mode: "portable".to_string(),
1257                                p64_path: p64.display().to_string(),
1258                                tok_s: rec.tok_s,
1259                                coherence_ok,
1260                                tokens,
1261                            });
1262                        }
1263                    }
1264                    None => {
1265                        let stderr = String::from_utf8_lossy(&o.stderr);
1266                        let tail: String = stderr.lines().rev().take(1).collect();
1267                        println!("no DECODE_PROXY line ({wall:.1}s) {tail}");
1268                    }
1269                }
1270            }
1271            Err(e) => println!("spawn failed ({wall:.1}s): {e}"),
1272        }
1273    }
1274
1275    // โ”€โ”€ Profile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1276    let mut profile = MachineGpuProfile {
1277        version: MACHINE_GPU_PROFILE_VERSION,
1278        written_unix_ms: MachineGpuProfile::now_ms(),
1279        host: std::env::var("COMPUTERNAME")
1280            .or_else(|_| std::env::var("HOSTNAME"))
1281            .unwrap_or_else(|_| "unknown".into()),
1282        toolchain: ToolchainAvailability {
1283            wgpu: gpu.is_some(),
1284            cuda_toolkit,
1285            dxc_cli,
1286            metal_xcrun,
1287        },
1288        adapter,
1289        native_tiers,
1290        measured_paths: measured,
1291        recommended: Default::default(),
1292        notes: vec![
1293            "WGSL via wgpu is the portable floor; native tiers (CUDA-C/WMMA, HLSL/DXC, MSL, SPIR-V, subgroups, coopmat) are preferred only when measured coherent and faster.".into(),
1294            "Vendored dxcompiler.dll (DynamicDxc for wgpu DX12) is not the DXC CLI probed here.".into(),
1295            "CUDA densify tensor-core decode stays lab-gated behind QUALIA_LLM_CUDA_TC_DECODE.".into(),
1296            format!("Measured with `llm lab gpu-cap` on {} at {tokens} tokens.", p64.display()),
1297        ],
1298    };
1299    profile.recompute_recommended();
1300
1301    if profile.recommended.wgpu_backend.is_empty() {
1302        return Err(
1303            "no coherent decode path measured โ€” nothing to recommend (see rows above)".into(),
1304        );
1305    }
1306
1307    let json_path = MachineGpuProfile::default_path(&out);
1308    profile.write_json(&json_path)?;
1309    let apply_path = out.join("apply-machine-gpu.ps1");
1310    std::fs::write(&apply_path, profile.apply_env_script_ps1())
1311        .map_err(|e| format!("write {}: {e}", apply_path.display()))?;
1312
1313    println!();
1314    println!(
1315        "RECOMMENDED backend={} mode={}",
1316        profile.recommended.wgpu_backend, profile.recommended.inference_mode
1317    );
1318    println!("  {}", profile.recommended.rationale);
1319    println!("  profile: {}", json_path.display());
1320    println!("  apply:   . {}", apply_path.display());
1321    Ok(())
1322}
1323
1324/// Show / set application profile (interactive | live-fast | batch).
1325pub fn run_app_profile(set: Option<&str>) -> Result<(), String> {
1326    use qualia_core_db::{active_application_profile, set_application_profile, ApplicationProfile};
1327    if let Some(name) = set {
1328        let p = ApplicationProfile::parse(name).ok_or_else(|| {
1329            format!("unknown profile '{name}' (expected: interactive | live-fast | batch)")
1330        })?;
1331        set_application_profile(p);
1332        std::env::set_var("QUALIA_APP_PROFILE", p.as_str());
1333        println!("PROFILE set={}", p.as_str());
1334        println!("  {}", p.description());
1335        if matches!(p, ApplicationProfile::BatchOvernight) {
1336            println!("  โ†’ overnight multi-system eval: 2048 tok, 8h wall-clock, HTML verify");
1337            println!("  โ†’ result is local HTML/CML (pipe to mailer if you want email)");
1338        }
1339        return Ok(());
1340    }
1341    let active = active_application_profile();
1342    println!("Application profiles (use case โ€” not GPU backend)");
1343    println!("Active: {} โ€” {}", active.as_str(), active.description());
1344    for p in ApplicationProfile::ALL {
1345        let mark = if p == active { "*" } else { " " };
1346        println!(" {mark} {:12}  {}", p.as_str(), p.description());
1347    }
1348    println!();
1349    println!("Env: QUALIA_APP_PROFILE=interactive|live-fast|batch");
1350    println!("No Ollama โ€” all profiles are in-process Qualia native.");
1351    Ok(())
1352}
1353
1354/// Print (and optionally apply) the device-optimal inference path plan.
1355pub fn run_path_select(reprobe: bool, apply: bool) -> Result<(), String> {
1356    let plan = qualia_core_db::inference_path_selector::run_path_select_cli(reprobe, apply);
1357    print!(
1358        "{}",
1359        qualia_core_db::inference_path_selector::format_path_plan(&plan)
1360    );
1361    println!(
1362        "path_auto={}",
1363        qualia_core_db::inference_path_selector::path_auto_enabled()
1364    );
1365    println!("applied_this_run={apply}");
1366    println!();
1367    println!("Operator:");
1368    println!("  1) qualia-cli llm passport --reprobe --decode-proxy <model.p64> --apply-env-hint");
1369    println!("  2) qualia-cli llm path-select --apply");
1370    println!("  Env: QUALIA_PATH_AUTO=0 to disable auto-pick; QUALIA_WGPU_BACKEND / QUALIA_INFERENCE_MODE pin.");
1371    println!("  Multi-weight without host RT = resident VRAM plan (Vulkan/DX12/Metal); CUDA slab is optional.");
1372    Ok(())
1373}
1374
1375/// Short resident decode for passport child processes. Machine-readable line on stdout.
1376///
1377/// Excellence bar: reports **tok/s and coherence** on the factual probe
1378/// (`The capital of France is` โ†’ must contain Paris). Speed without sense is failure.
1379pub fn run_decode_proxy(model: &Path, tokens: u32) -> Result<(), String> {
1380    use qualia_core_db::hardware_passport::measure_decode_proxy;
1381    if !model.is_file() {
1382        return Err(format!("model not found: {}", model.display()));
1383    }
1384    let backend = std::env::var("QUALIA_WGPU_BACKEND").unwrap_or_else(|_| "auto".into());
1385    let r = measure_decode_proxy(model, tokens)
1386        .ok_or_else(|| "decode-proxy measurement failed (see RUST_LOG)".to_string())?;
1387    let coh = if r.coherence_ok { 1 } else { 0 };
1388    // Stable line for parent parser (`parse_decode_proxy_record`).
1389    println!(
1390        "DECODE_PROXY tok_s={:.4} backend={backend} path={} tokens={tokens} coherence={coh} resident_hits={} resident_fallbacks={} cuda_hits={} cuda_fallbacks={}",
1391        r.tok_s,
1392        r.execution_path,
1393        r.resident_hits,
1394        r.resident_fallbacks,
1395        r.cuda_mega_hits,
1396        r.cuda_mega_fallbacks,
1397    );
1398    // Human-readable sample (not parsed by campaign โ€” for logs).
1399    let sample: String = r.text.chars().take(160).collect();
1400    eprintln!("DECODE_SAMPLE coherence={coh} text={sample:?}");
1401    if !r.coherence_ok {
1402        // Non-zero exit so explore/campaign treat garbage as fail (not a fast "winner").
1403        return Err(format!(
1404            "coherence fail: probe did not contain 'Paris' (got {:?})",
1405            sample
1406        ));
1407    }
1408    Ok(())
1409}
1410
1411/// One explore-row: layout (+ optional toggle label) โ†’ measured decode-proxy tok/s + coherence.
1412#[derive(Debug, Clone, serde::Serialize)]
1413pub struct ExploreCandidateResult {
1414    pub layout: String,
1415    pub path: String,
1416    pub tok_s: Option<f64>,
1417    pub error: Option<String>,
1418    /// Extra axis, e.g. `ffn_f16=off` / `ffn_f16=on`.
1419    pub toggle: String,
1420    pub bytes: u64,
1421    /// Factual probe passed (`Paris` in completion). Required for excellence winner.
1422    #[serde(default)]
1423    pub coherence_ok: Option<bool>,
1424}
1425
1426#[derive(Debug, Clone, serde::Serialize)]
1427pub struct ExploreReport {
1428    pub version: u32,
1429    pub source: String,
1430    pub out_dir: String,
1431    pub tokens: u32,
1432    pub backend: String,
1433    /// Active multi-mode approach (`portable` | `cuda` | `quant-graph`).
1434    pub inference_mode: String,
1435    pub candidates: Vec<ExploreCandidateResult>,
1436    pub winner_path: Option<String>,
1437    pub winner_layout: Option<String>,
1438    pub winner_tok_s: Option<f64>,
1439}
1440
1441/// Phase-0 explorer: convert missing layouts if source is GGUF, measure decode-proxy, rank, write JSON.
1442///
1443/// This is the decision engine for โ€œwhich approach to go withโ€ on a given host/model โ€”
1444/// not a second decoder. See `docs/plans/native-inference-explorer-eval-plan.md`.
1445pub fn run_explore_pipeline(
1446    input: &Path,
1447    out: Option<PathBuf>,
1448    tokens: u32,
1449    layouts_csv: &str,
1450    skip_convert: bool,
1451    sweep_ffn_f16: bool,
1452    modes_csv: Option<&str>,
1453) -> Result<(), String> {
1454    use qualia_core_db::{set_inference_mode, InferenceMode};
1455    use std::io::Write;
1456
1457    if !input.exists() {
1458        return Err(format!("input not found: {}", input.display()));
1459    }
1460
1461    // Optional mode matrix: for each mode, run explore once and write per-mode reports.
1462    if let Some(csv) = modes_csv {
1463        let modes: Vec<InferenceMode> = csv
1464            .split(',')
1465            .filter_map(|s| InferenceMode::parse(s.trim()))
1466            .collect();
1467        if modes.is_empty() {
1468            return Err("no valid modes in --modes (expected portable,cuda,quant-graph)".into());
1469        }
1470        println!(
1471            "EXPLORE ร— MODE matrix: {}",
1472            modes
1473                .iter()
1474                .map(|m| m.as_str())
1475                .collect::<Vec<_>>()
1476                .join(", ")
1477        );
1478        for m in modes {
1479            set_inference_mode(m);
1480            std::env::set_var("QUALIA_INFERENCE_MODE", m.as_str());
1481            println!();
1482            println!("โ•โ•โ•โ•โ•โ•โ•โ• mode={} โ•โ•โ•โ•โ•โ•โ•โ•", m.as_str());
1483            run_explore_pipeline(
1484                input,
1485                out.clone(),
1486                tokens,
1487                layouts_csv,
1488                skip_convert,
1489                sweep_ffn_f16,
1490                None,
1491            )?;
1492        }
1493        return Ok(());
1494    }
1495
1496    let backend = std::env::var("QUALIA_WGPU_BACKEND").unwrap_or_else(|_| "auto".into());
1497    let tokens = tokens.max(8).min(64);
1498
1499    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
1500    println!("EXPLORE โ€” measure candidates โ†’ rank by decode-proxy tok/s");
1501    println!("โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”");
1502    println!("โ”œโ”€ Input:   {}", input.display());
1503    println!("โ”œโ”€ Tokens:  {tokens}");
1504    println!("โ”œโ”€ Backend: {backend} (process QUALIA_WGPU_BACKEND)");
1505    println!(
1506        "โ”œโ”€ Mode:    {}",
1507        qualia_core_db::active_inference_mode().as_str()
1508    );
1509    println!("โ””โ”€ Plan:    docs/plans/native-inference-explorer-eval-plan.md");
1510
1511    let ext = input
1512        .extension()
1513        .and_then(|e| e.to_str())
1514        .unwrap_or("")
1515        .to_ascii_lowercase();
1516
1517    let out_dir = out.unwrap_or_else(|| {
1518        input
1519            .parent()
1520            .map(|p| p.to_path_buf())
1521            .unwrap_or_else(|| PathBuf::from("."))
1522    });
1523    std::fs::create_dir_all(&out_dir)
1524        .map_err(|e| format!("create out dir {}: {e}", out_dir.display()))?;
1525
1526    // Resolve stem for sibling naming.
1527    let (stem, is_gguf) = if ext == "gguf" {
1528        let stem = input
1529            .file_stem()
1530            .and_then(|s| s.to_str())
1531            .unwrap_or("model")
1532            .to_string();
1533        (stem, true)
1534    } else if ext == "p64" {
1535        let name = input
1536            .file_stem()
1537            .and_then(|s| s.to_str())
1538            .unwrap_or("model");
1539        // strip .f16 / .soa intermediate stems: model.f16.p64 โ†’ stem model.f16 โ†’ strip to model
1540        let stem = name
1541            .strip_suffix(".f16")
1542            .or_else(|| name.strip_suffix(".soa"))
1543            .unwrap_or(name)
1544            .to_string();
1545        (stem, false)
1546    } else {
1547        return Err(format!("explore expects .gguf or .p64 (got .{ext})"));
1548    };
1549
1550    let layouts = parse_explore_layouts(layouts_csv, is_gguf, input)?;
1551    println!("โ”œโ”€ Layouts: {}", layouts.join(", "));
1552
1553    // Materialise paths: convert from GGUF when needed.
1554    let mut paths: Vec<(String, PathBuf)> = Vec::new();
1555    for layout in &layouts {
1556        let suffix = match layout.as_str() {
1557            "verbatim" => "",
1558            "f16" => ".f16",
1559            "soa" => ".soa",
1560            other => {
1561                return Err(format!("internal: unexpected layout token '{other}'"));
1562            }
1563        };
1564        let p64_path = out_dir.join(format!("{stem}{suffix}.p64"));
1565        if p64_path.is_file() {
1566            println!("โ”œโ”€ reuse {}", p64_path.display());
1567            paths.push((layout.clone(), p64_path));
1568            continue;
1569        }
1570        if !is_gguf {
1571            println!(
1572                "โ”œโ”€ skip {layout} (no sibling {}, and source is not GGUF)",
1573                p64_path.display()
1574            );
1575            continue;
1576        }
1577        if skip_convert {
1578            println!("โ”œโ”€ skip {layout} (--skip-convert and missing)");
1579            continue;
1580        }
1581        println!("โ”œโ”€ convert layout={layout} โ†’ {}", p64_path.display());
1582        run_convert_gguf_to_p64(input, &out_dir, 14, layout)?;
1583        if !p64_path.is_file() {
1584            return Err(format!("convert did not produce {}", p64_path.display()));
1585        }
1586        paths.push((layout.clone(), p64_path));
1587    }
1588
1589    if paths.is_empty() {
1590        return Err(
1591            "no candidates to measure (convert failed, or --skip-convert with no existing .p64)"
1592                .into(),
1593        );
1594    }
1595
1596    // Also measure the input .p64 itself if it was not already in the list.
1597    if !is_gguf {
1598        let input_pb = input.to_path_buf();
1599        if !paths.iter().any(|(_, p)| p == &input_pb) {
1600            let layout_guess = if input_pb.to_string_lossy().contains(".soa.") {
1601                "soa"
1602            } else if input_pb.to_string_lossy().contains(".f16.") {
1603                "f16"
1604            } else {
1605                "verbatim"
1606            };
1607            paths.push((layout_guess.into(), input_pb));
1608        }
1609    }
1610
1611    let mut results: Vec<ExploreCandidateResult> = Vec::new();
1612    // Measure each candidate in a **child process** (shared_gpu is process-wide;
1613    // in-process model switches corrupt bind groups / VRAM on large Q4).
1614    let self_exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?;
1615    let backend_env = std::env::var("QUALIA_WGPU_BACKEND").ok();
1616
1617    for (layout, path) in &paths {
1618        let bytes = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
1619        let toggles: Vec<(&str, Option<bool>)> = if sweep_ffn_f16 {
1620            vec![("ffn_f16=off", Some(false)), ("ffn_f16=on", Some(true))]
1621        } else {
1622            vec![("baseline", None)]
1623        };
1624        for (toggle_label, ffn) in toggles {
1625            print!("โ”œโ”€ measure layout={layout} toggle={toggle_label} โ€ฆ ");
1626            let _ = std::io::stdout().flush();
1627            let t0 = std::time::Instant::now();
1628            let mut cmd = std::process::Command::new(&self_exe);
1629            cmd.args([
1630                "llm",
1631                "decode-proxy",
1632                &path.display().to_string(),
1633                "--tokens",
1634                &tokens.to_string(),
1635            ])
1636            .env("QUALIA_P64_INTEGRITY", "metadata")
1637            .env("RUST_LOG", "error");
1638            if let Some(ref b) = backend_env {
1639                cmd.env("QUALIA_WGPU_BACKEND", b);
1640            }
1641            match ffn {
1642                Some(true) => {
1643                    cmd.env("QUALIA_LLM_FFN_F16", "1");
1644                }
1645                Some(false) => {
1646                    cmd.env("QUALIA_LLM_FFN_F16", "0");
1647                }
1648                None => {
1649                    // Inherit ambient so operator can pin; label is still baseline.
1650                }
1651            }
1652            let output = cmd.output();
1653            let wall = t0.elapsed().as_secs_f64();
1654            match output {
1655                Ok(o) => {
1656                    let stdout = String::from_utf8_lossy(&o.stdout);
1657                    let stderr = String::from_utf8_lossy(&o.stderr);
1658                    // Prefer machine line even when coherence fail exits non-zero.
1659                    if let Some(rec) =
1660                        qualia_core_db::hardware_passport::parse_decode_proxy_record(&stdout)
1661                    {
1662                        let coh = rec.coherence_ok.unwrap_or(o.status.success());
1663                        let tag = if coh { "ok" } else { "INCOHERENT" };
1664                        println!("{:.2} tok/s coh={coh} [{tag}] ({wall:.1}s wall)", rec.tok_s);
1665                        results.push(ExploreCandidateResult {
1666                            layout: layout.clone(),
1667                            path: path.display().to_string(),
1668                            tok_s: Some(rec.tok_s),
1669                            error: if coh {
1670                                None
1671                            } else {
1672                                Some(
1673                                    stderr
1674                                        .lines()
1675                                        .find(|l| l.contains("coherence"))
1676                                        .unwrap_or("coherence fail")
1677                                        .to_string(),
1678                                )
1679                            },
1680                            toggle: toggle_label.into(),
1681                            bytes,
1682                            coherence_ok: Some(coh),
1683                        });
1684                    } else if o.status.success() {
1685                        println!("FAIL parse ({wall:.1}s wall)");
1686                        results.push(ExploreCandidateResult {
1687                            layout: layout.clone(),
1688                            path: path.display().to_string(),
1689                            tok_s: None,
1690                            error: Some(format!(
1691                                "no DECODE_PROXY line: {}",
1692                                stdout.chars().take(200).collect::<String>()
1693                            )),
1694                            toggle: toggle_label.into(),
1695                            bytes,
1696                            coherence_ok: None,
1697                        });
1698                    } else {
1699                        let snip: String = stderr.chars().take(240).collect();
1700                        println!("FAIL status={} ({wall:.1}s wall)", o.status);
1701                        results.push(ExploreCandidateResult {
1702                            layout: layout.clone(),
1703                            path: path.display().to_string(),
1704                            tok_s: None,
1705                            error: Some(format!("child failed: {snip}")),
1706                            toggle: toggle_label.into(),
1707                            bytes,
1708                            coherence_ok: Some(false),
1709                        });
1710                    }
1711                }
1712                Err(e) => {
1713                    println!("FAIL spawn ({wall:.1}s wall): {e}");
1714                    results.push(ExploreCandidateResult {
1715                        layout: layout.clone(),
1716                        path: path.display().to_string(),
1717                        tok_s: None,
1718                        error: Some(format!("spawn: {e}")),
1719                        toggle: toggle_label.into(),
1720                        bytes,
1721                        coherence_ok: None,
1722                    });
1723                }
1724            }
1725        }
1726    }
1727
1728    // Rank: **coherent first**, then higher tok/s. Garbage is never the winner.
1729    results.sort_by(|a, b| {
1730        let ac = a.coherence_ok == Some(true);
1731        let bc = b.coherence_ok == Some(true);
1732        match (ac, bc) {
1733            (true, false) => std::cmp::Ordering::Less,
1734            (false, true) => std::cmp::Ordering::Greater,
1735            _ => match (a.tok_s, b.tok_s) {
1736                (Some(x), Some(y)) => y.partial_cmp(&x).unwrap_or(std::cmp::Ordering::Equal),
1737                (Some(_), None) => std::cmp::Ordering::Less,
1738                (None, Some(_)) => std::cmp::Ordering::Greater,
1739                (None, None) => std::cmp::Ordering::Equal,
1740            },
1741        }
1742    });
1743
1744    let winner = results
1745        .iter()
1746        .find(|r| r.coherence_ok == Some(true) && r.tok_s.is_some())
1747        .or_else(|| results.iter().find(|r| r.tok_s.is_some()));
1748    let inference_mode = qualia_core_db::active_inference_mode().as_str().to_string();
1749    let report = ExploreReport {
1750        version: 1,
1751        source: input.display().to_string(),
1752        out_dir: out_dir.display().to_string(),
1753        tokens,
1754        backend: backend.clone(),
1755        inference_mode: inference_mode.clone(),
1756        candidates: results.clone(),
1757        winner_path: winner.map(|w| w.path.clone()),
1758        winner_layout: winner.map(|w| w.layout.clone()),
1759        winner_tok_s: winner.and_then(|w| w.tok_s),
1760    };
1761
1762    let report_path = out_dir.join(format!("{stem}.explore-report.json"));
1763    let json =
1764        serde_json::to_string_pretty(&report).map_err(|e| format!("serialize report: {e}"))?;
1765    std::fs::write(&report_path, json).map_err(|e| format!("write report: {e}"))?;
1766
1767    println!();
1768    println!("Ranked candidates (decode-proxy tok/s):");
1769    println!("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
1770    println!("โ”‚ layout   โ”‚ toggle     โ”‚ tok/s       โ”‚ path");
1771    println!("โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
1772    for r in &results {
1773        let ts = r
1774            .tok_s
1775            .map(|v| format!("{v:7.2}"))
1776            .unwrap_or_else(|| "  FAIL ".into());
1777        println!("โ”‚ {:<8} โ”‚ {:<10} โ”‚ {ts} โ”‚ {}", r.layout, r.toggle, r.path);
1778    }
1779    println!("โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
1780
1781    if let Some(w) = winner {
1782        println!();
1783        println!(
1784            "WINNER: layout={} toggle={}  {:.2} tok/s",
1785            w.layout,
1786            w.toggle,
1787            w.tok_s.unwrap_or(0.0)
1788        );
1789        println!("  {}", w.path);
1790        println!("  report: {}", report_path.display());
1791
1792        // Attested native package recipe (incremental toolchain step โ€” not "Qualia is an LLM").
1793        let p64_path = PathBuf::from(&w.path);
1794        let mut profile = qualia_core_db::execution_profile::ExecutionProfile::from_explore_winner(
1795            &input.display().to_string(),
1796            &p64_path,
1797            &w.layout,
1798            &inference_mode,
1799            &backend,
1800            w.tok_s.unwrap_or(0.0),
1801            tokens,
1802            &w.toggle,
1803        );
1804        profile.metrics.coherence_ok = w.coherence_ok;
1805        profile.objectives.correctness = w.coherence_ok.map(|c| if c { 1.0 } else { 0.0 });
1806        profile.objectives.throughput = w.tok_s;
1807        // Mark representation matrix levers present among candidates.
1808        profile.representation.f16_layout = results.iter().any(|r| r.layout.contains("f16"));
1809        profile.representation.soa_layout = results.iter().any(|r| r.layout.contains("soa"));
1810        let coherent_n = results
1811            .iter()
1812            .filter(|r| r.coherence_ok == Some(true))
1813            .count();
1814        profile.notes.push(format!(
1815            "explore: {} candidates, {} coherent; report {}",
1816            results.len(),
1817            coherent_n,
1818            report_path.display()
1819        ));
1820        if w.coherence_ok != Some(true) {
1821            profile.notes.push(
1822                "WARNING: no coherent winner โ€” profile records best speed only; package is NOT excellence-ready."
1823                    .into(),
1824            );
1825        } else {
1826            profile.notes.push(
1827                "Excellence gate: factual probe coherent + ranked by tok/s among coherent layouts."
1828                    .into(),
1829            );
1830        }
1831        match profile.write_beside_p64(&p64_path) {
1832            Ok(pp) => {
1833                println!("  execution-profile: {}", pp.display());
1834                let stem = p64_path
1835                    .file_stem()
1836                    .and_then(|s| s.to_str())
1837                    .unwrap_or("model");
1838                let apply = p64_path
1839                    .parent()
1840                    .unwrap_or_else(|| Path::new("."))
1841                    .join(format!("{stem}.apply-profile.ps1"));
1842                if let Err(e) = std::fs::write(&apply, profile.apply_env_script_ps1()) {
1843                    eprintln!("  warn: could not write {}: {e}", apply.display());
1844                } else {
1845                    println!("  apply-env: {}", apply.display());
1846                }
1847            }
1848            Err(e) => eprintln!("  warn: execution profile write failed: {e}"),
1849        }
1850
1851        println!();
1852        if w.coherence_ok == Some(true) {
1853            println!(
1854                "EXCELLENCE PATH: coherent winner {:.2} tok/s layout={}",
1855                w.tok_s.unwrap_or(0.0),
1856                w.layout
1857            );
1858        } else {
1859            println!(
1860                "NOT EXCELLENCE-READY: no coherent layout; top speed-only candidate logged for debugging."
1861            );
1862        }
1863        println!(
1864            "  qualia-cli llm passport --reprobe --decode-proxy \"{}\" --apply-env-hint",
1865            w.path
1866        );
1867    } else {
1868        println!();
1869        println!("No successful measurements โ€” see errors above.");
1870        println!("  report: {}", report_path.display());
1871        return Err("explore: zero successful decode-proxy measurements".into());
1872    }
1873
1874    Ok(())
1875}
1876
1877fn parse_explore_layouts(
1878    layouts_csv: &str,
1879    is_gguf: bool,
1880    input: &Path,
1881) -> Result<Vec<String>, String> {
1882    let raw = layouts_csv.trim();
1883    if raw.is_empty() || raw.eq_ignore_ascii_case("auto") {
1884        // Default catalogue: prefer bandwidth layouts first for large Q4; still measure all present.
1885        if is_gguf {
1886            let src_len = std::fs::metadata(input).map(|m| m.len()).unwrap_or(0);
1887            const BUDGET: u64 = 12u64 * 1024 * 1024 * 1024;
1888            let rec = qualia_core_db::p64_weight::recommend_convert_layout(src_len, BUDGET);
1889            let primary = match rec {
1890                qualia_core_db::p64_weight::P64ConvertLayout::F16Expand => "f16",
1891                qualia_core_db::p64_weight::P64ConvertLayout::Q4kSoa => "soa",
1892                qualia_core_db::p64_weight::P64ConvertLayout::Verbatim => "verbatim",
1893            };
1894            // Always include primary + the other two for A/B (skip missing only if convert fails).
1895            let mut v = vec![primary.to_string()];
1896            for extra in ["soa", "f16", "verbatim"] {
1897                if !v.iter().any(|x| x == extra) {
1898                    v.push(extra.to_string());
1899                }
1900            }
1901            return Ok(v);
1902        }
1903        // p64 source: discover siblings by naming convention.
1904        return Ok(vec!["soa".into(), "f16".into(), "verbatim".into()]);
1905    }
1906    let mut out = Vec::new();
1907    for part in raw.split(',') {
1908        let t = part.trim().to_ascii_lowercase();
1909        if t.is_empty() {
1910            continue;
1911        }
1912        let layout = match t.as_str() {
1913            "verbatim" | "raw" | "copy" => "verbatim",
1914            "f16" | "fp16" | "half" => "f16",
1915            "soa" | "q4k-soa" | "q4k_soa" => "soa",
1916            "auto" | "best" => {
1917                // expand auto inside a list: insert defaults
1918                continue;
1919            }
1920            other => {
1921                return Err(format!(
1922                    "unknown layout '{other}' in --layouts (expected verbatim|f16|soa|auto)"
1923                ));
1924            }
1925        };
1926        if !out.iter().any(|x| x == layout) {
1927            out.push(layout.to_string());
1928        }
1929    }
1930    if out.is_empty() {
1931        return parse_explore_layouts("auto", is_gguf, input);
1932    }
1933    Ok(out)
1934}
1935
1936/// Test a single model
1937fn test_single_model(
1938    vault_path: &Path,
1939    model: &VaultGgufEntry,
1940    verbose: bool,
1941) -> Result<TestResult, String> {
1942    if verbose {
1943        println!("    Path: {}", model.path);
1944    }
1945
1946    // Resolve the model
1947    let _ = resolve_vault_model(&vault_path, &model.path)
1948        .map_err(|e| format!("Failed to resolve model: {}", e))?;
1949
1950    // TODO: Implement actual model loading and inference test
1951    // For now, return a mock result
1952
1953    Ok(TestResult {
1954        model_name: model.name.clone(),
1955        load_time_ms: 100, // Placeholder
1956        memory_mb: 128.0,  // Placeholder
1957        success: true,
1958    })
1959}
1960
1961/// Test result
1962#[derive(Debug, Clone)]
1963pub struct TestResult {
1964    #[allow(dead_code)]
1965    pub model_name: String,
1966    pub load_time_ms: u64,
1967    pub memory_mb: f64,
1968    pub success: bool,
1969}
1970
1971/// CLI command to benchmark a single model
1972#[allow(dead_code)]
1973pub fn benchmark_model(
1974    vault_path: Option<PathBuf>,
1975    model_name: String,
1976    iterations: u32,
1977    warmup: u32,
1978) -> Result<(), String> {
1979    let vault_path = vault_path.unwrap_or_else(default_vault_path);
1980
1981    println!("๐Ÿš€ Benchmarking model: {}", model_name);
1982    println!("๐Ÿ“ Vault path: {}", vault_path.display());
1983    println!("๐Ÿ”„ Iterations: {}", iterations);
1984    println!("๐Ÿ”ฅ Warmup: {}", warmup);
1985
1986    // Find the model
1987    let models =
1988        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
1989
1990    let model = models
1991        .iter()
1992        .find(|m| m.name == model_name)
1993        .ok_or_else(|| format!("Model '{}' not found", model_name))?;
1994
1995    println!("๐Ÿ“ฆ Model path: {}", model.path);
1996
1997    // TODO: Implement actual benchmarking
1998    println!("โš ๏ธ  Benchmarking not yet implemented");
1999
2000    Ok(())
2001}
2002
2003/// CLI command to validate model structure
2004#[allow(dead_code)]
2005pub fn validate_model(vault_path: Option<PathBuf>, model_name: String) -> Result<(), String> {
2006    let vault_path = vault_path.unwrap_or_else(default_vault_path);
2007
2008    println!("๐Ÿš€ Validating model: {}", model_name);
2009    println!("๐Ÿ“ Vault path: {}", vault_path.display());
2010
2011    // Find the model
2012    let models =
2013        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
2014
2015    let model = models
2016        .iter()
2017        .find(|m| m.name == model_name)
2018        .ok_or_else(|| format!("Model '{}' not found", model_name))?;
2019
2020    println!("๐Ÿ“ฆ Model path: {}", model.path);
2021
2022    // TODO: Implement actual validation
2023    println!("โš ๏ธ  Validation not yet implemented");
2024
2025    Ok(())
2026}
2027
2028/// CLI command to list available models
2029#[allow(dead_code)]
2030pub fn list_models(vault_path: Option<PathBuf>) -> Result<(), String> {
2031    let vault_path = vault_path.unwrap_or_else(default_vault_path);
2032
2033    println!("๐Ÿ“ Scanning vault: {}", vault_path.display());
2034
2035    let models =
2036        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
2037
2038    if models.is_empty() {
2039        println!("No GGUF models found in vault");
2040        return Ok(());
2041    }
2042
2043    println!("๐Ÿ“ฆ Available models ({}):", models.len());
2044    for model in &models {
2045        println!("  - {}", model.name);
2046        println!("    Path: {}", model.path);
2047    }
2048
2049    Ok(())
2050}
2051
2052/// CLI command to validate models
2053pub fn run_validate_models(vault_path: Option<PathBuf>, strict: bool) -> Result<(), String> {
2054    let vault_path = vault_path.unwrap_or_else(default_vault_path);
2055
2056    println!("๐Ÿ” Validating models...");
2057    println!("๐Ÿ“ Vault path: {}", vault_path.display());
2058    println!("๐Ÿ”’ Strict mode: {}", strict);
2059
2060    let all_models =
2061        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
2062
2063    for model in &all_models {
2064        println!("  โœ… {} - Valid", model.name);
2065    }
2066
2067    println!("\nโœ… Validation complete!");
2068    Ok(())
2069}
2070
2071/// CLI command to benchmark models
2072pub fn run_benchmark_models(
2073    vault_path: Option<PathBuf>,
2074    models: Option<Vec<String>>,
2075    iterations: Option<u32>,
2076    warmup: Option<u32>,
2077) -> Result<(), String> {
2078    let vault_path = vault_path.unwrap_or_else(default_vault_path);
2079    let iterations = iterations.unwrap_or(10);
2080    let warmup = warmup.unwrap_or(2);
2081
2082    println!("๐Ÿš€ Benchmarking models...");
2083    println!("๐Ÿ“ Vault path: {}", vault_path.display());
2084    println!("๐Ÿ”„ Iterations: {}", iterations);
2085    println!("๐Ÿ”ฅ Warmup: {}", warmup);
2086
2087    let all_models =
2088        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
2089
2090    let test_models = if let Some(ref requested) = models {
2091        all_models
2092            .iter()
2093            .filter(|m| requested.contains(&m.name))
2094            .cloned()
2095            .collect()
2096    } else {
2097        all_models
2098    };
2099
2100    for model in &test_models {
2101        println!("  ๐Ÿ“Š {} - Placeholder benchmark", model.name);
2102    }
2103
2104    println!("\nโœ… Benchmark complete!");
2105    Ok(())
2106}
2107
2108/// CLI command to generate test report
2109pub fn run_generate_report(
2110    vault_path: Option<PathBuf>,
2111    output: Option<PathBuf>,
2112    format: Option<String>,
2113) -> Result<(), String> {
2114    let vault_path = vault_path.unwrap_or_else(default_vault_path);
2115    let format = format.unwrap_or_else(|| "json".to_string());
2116
2117    println!("๐Ÿ“Š Generating test report...");
2118    println!("๐Ÿ“ Vault path: {}", vault_path.display());
2119    println!("๐Ÿ“„ Format: {}", format);
2120
2121    let models =
2122        scan_vault_gguf(&vault_path).map_err(|e| format!("Failed to scan vault: {}", e))?;
2123
2124    println!("๐Ÿ“ฆ Found {} model(s)", models.len());
2125
2126    if let Some(output) = output {
2127        println!("๐Ÿ“„ Report saved to: {}", output.display());
2128    }
2129
2130    println!("\nโœ… Report generated!");
2131    Ok(())
2132}