Skip to main content

apr_cli/
lib.rs

1//! apr-cli library
2//!
3//! This library is the foundation for the apr CLI binary.
4//! Exports CLI structures for testing and reuse.
5
6// PMAT-540 declared `#![cfg_attr(coverage_nightly, coverage(off))]` in twenty
7// files of this crate but never declared the feature that makes `coverage(off)`
8// legal, and nothing in the workspace did. On the pinned stable toolchain
9// (rust-toolchain.toml = 1.93.0) the `cfg_attr` is inert, so nobody noticed; the
10// moment a coverage run sets `--cfg coverage_nightly` — which is exactly what
11// `pmat quality-gates` does via `cargo +nightly llvm-cov` — the crate stops
12// compiling:
13//
14//     crates/apr-cli/src/generated_contracts.rs:9:31: error[E0658]:
15//     the `#[coverage]` attribute is an experimental feature
16//
17// and the coverage gate reports "failed to run" rather than a number. A gate
18// that cannot run is not a gate that passed. This declaration is likewise
19// gated, so stable builds are byte-identical.
20#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
21// APR-MONO: Clippy pedantic allows for monorepo transition.
22// unwrap() eliminated (524 → expect()). Style lints from 20 merged crates
23// are suppressed at crate level. Will be incrementally addressed.
24#![allow(clippy::all, clippy::pedantic, clippy::disallowed_methods)]
25#![allow(
26    unreachable_code,
27    unused_variables,
28    unused_imports,
29    dead_code,
30    unused_assignments
31)]
32
33use clap::{Parser, Subcommand, ValueEnum};
34use std::path::{Path, PathBuf};
35
36// Contract assertions from YAML (pv codegen)
37#[macro_use]
38#[allow(unused_macros, clippy::duplicated_attributes)]
39mod generated_contracts;
40
41// #2401: `--quiet` / `--verbose` control. MUST come before `mod commands;` —
42// it shadows `println!`/`print!` for every module declared after it, which is
43// how the two global flags reach ~9 000 call sites without any command having
44// to remember to forward a parameter.
45#[macro_use]
46#[allow(unused_macros)]
47pub mod verbosity;
48
49mod commands;
50pub mod error;
51mod output;
52pub mod pipe;
53
54pub use error::CliError;
55
56// Public re-exports for integration tests
57pub mod qa_types {
58    pub use crate::commands::qa::{GateResult, QaReport, SystemInfo};
59}
60
61// Public re-exports for downstream crates (whisper-apr proxies these)
62pub mod model_pull {
63    pub use crate::commands::pull::{list, run};
64}
65
66// HELIX-IDEA-009: API key auth re-exported so integration tests in
67// `tests/falsify_auth_*.rs` can construct gates and middleware without
68// reaching into the private `commands` tree.
69pub mod serve_auth {
70    #[cfg(feature = "inference")]
71    pub use crate::commands::serve::auth::layer;
72    pub use crate::commands::serve::auth::{apply, AuthGate};
73}
74
75// PMAT-923: e2e seam so `tests/ollama_api_serve_compat.rs` can build the REAL
76// `apr serve` APR-CPU router (the one mounted for a `.apr` model) and prove the
77// Ollama `/api/chat` + `/api/generate` routes are wired — not realizar's
78// `create_router`.
79#[cfg(feature = "inference")]
80pub mod serve_test_support {
81    pub use crate::commands::serve::handlers::build_demo_apr_cpu_router_for_test;
82    // PMAT-928: streaming-capable demo router whose NDJSON path is driven by a
83    // scripted token sequence, so the streaming falsifier observes real
84    // multi-chunk NDJSON without loading a model.
85    pub use crate::commands::serve::handlers::build_demo_streaming_apr_cpu_router_for_test;
86}
87
88#[cfg(feature = "inference")]
89pub mod federation;
90
91// Commands are crate-private, used internally by execute_command
92use commands::{
93    bench, canary, canary::CanaryCommands, cbtop, chat, compare_hf, compile, convert, data, debug,
94    diagnose, diff, distill, eval, explain, export, flow, hex, import, inspect, lint, mcp, merge,
95    oracle, pipeline, probar, profile, prune, publish, pull, qa, qualify, quantize, rosetta,
96    rosetta::RosettaCommands, run, serve, showcase, stamp, tensors, tokenize, trace, tree, tui,
97    validate, validate_manifest,
98};
99#[cfg(feature = "training")]
100use commands::{finetune, gpu, train, tune};
101
102#[cfg(feature = "training")]
103pub use commands::pretrain::PretrainMode;
104
105/// apr - APR Model Operations Tool
106///
107/// Inspect, debug, and manage .apr model files.
108/// Toyota Way: Genchi Genbutsu - Go and see the actual data.
109#[derive(Parser, Debug)]
110#[command(name = "apr")]
111#[command(author, version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("APR_GIT_SHA"), ")"), about, long_about = None)]
112#[command(propagate_version = true)]
113pub struct Cli {
114    #[command(subcommand)]
115    pub command: Box<Commands>,
116
117    /// Output as JSON
118    #[arg(long, global = true)]
119    pub json: bool,
120
121    /// Verbose output: dispatch resolution, plus per-command detail where there is more to show
122    #[arg(short, long, global = true)]
123    pub verbose: bool,
124
125    /// Quiet mode: suppress stdout (errors still go to stderr; --json still prints)
126    #[arg(short, long, global = true)]
127    pub quiet: bool,
128
129    /// Disable network access (Sovereign AI compliance, Section 9)
130    #[arg(long, global = true)]
131    pub offline: bool,
132
133    /// Skip tensor contract validation (PMAT-237: use with diagnostic tooling)
134    #[arg(long, global = true)]
135    pub skip_contract: bool,
136}
137
138include!("commands_enum.rs");
139include!("model_ops_commands.rs");
140include!("extended_commands.rs");
141include!("tool_commands.rs");
142include!("data_commands.rs");
143#[cfg(feature = "training")]
144include!("train_commands.rs");
145include!("serve_commands.rs");
146include!("tokenize_commands.rs");
147include!("pipeline_commands.rs");
148include!("validate.rs");
149include!("dispatch_run.rs");
150include!("dispatch.rs");
151include!("dispatch_analysis.rs");
152include!("lib_07.rs");
153
154/// Full CLI entry point for `cargo install aprender`.
155///
156/// This function encapsulates the complete `apr` binary logic so that
157/// the `aprender` facade crate can produce the same binary via
158/// `cargo install aprender` (in addition to `cargo install apr-cli`).
159pub fn cli_main() -> std::process::ExitCode {
160    // GH-667: Reset SIGPIPE to default so piping to head/less doesn't panic.
161    #[cfg(unix)]
162    #[allow(unsafe_code)]
163    unsafe {
164        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
165    }
166
167    // GH-646: Clear FPCR.FZ16 on aarch64 so f16 subnormals work.
168    #[cfg(target_arch = "aarch64")]
169    #[allow(unsafe_code)]
170    unsafe {
171        let fpcr: u64;
172        core::arch::asm!("mrs {}, fpcr", out(reg) fpcr);
173        if fpcr & (1 << 19) != 0 {
174            let new_fpcr = fpcr & !(1 << 19);
175            core::arch::asm!("msr fpcr, {}", in(reg) new_fpcr);
176        }
177    }
178
179    // GH-662: Respect NO_COLOR env var and non-TTY output.
180    let no_color = std::env::var("NO_COLOR").is_ok();
181    let is_tty = std::io::IsTerminal::is_terminal(&std::io::stdout());
182    if no_color || !is_tty {
183        colored::control::set_override(false);
184    }
185
186    // FALSIFY-GPUTRAIN-007 / INV-GPUTRAIN-007 — `apr --version --json` MUST
187    // emit a machine-parseable object containing at least the three keys
188    // { cuda_feature, cuda_runtime_available, visible_devices }. Intercept
189    // this flag combo BEFORE clap's default `--version` handler exits the
190    // process with a plain string. Bound by `gputrain_007.rs` — see
191    // `AC_GPUTRAIN_007_REQUIRED_VERSION_JSON_KEYS`.
192    let raw: Vec<String> = std::env::args().collect();
193    if raw.iter().any(|a| a == "--version") && raw.iter().any(|a| a == "--json") {
194        emit_version_json();
195        return std::process::ExitCode::SUCCESS;
196    }
197
198    let cli = Cli::parse();
199    match execute_command(&cli) {
200        Ok(()) => std::process::ExitCode::SUCCESS,
201        Err(e) => {
202            eprintln!("error: {e}");
203            e.exit_code()
204        }
205    }
206}
207
208/// FALSIFY-GPUTRAIN-007 — emit `apr --version --json` output with the
209/// 3-key schema required by `AC_GPUTRAIN_007_REQUIRED_VERSION_JSON_KEYS`.
210///
211/// Schema:
212/// ```json
213/// {
214///   "name":    "apr",
215///   "version": "<semver>",
216///   "git_sha": "<commit>",
217///   "cuda_feature":           <bool>,   // was the binary built --features cuda?
218///   "cuda_runtime_available": <bool>,   // does cudaRuntimeGetVersion succeed?
219///   "visible_devices":        ["0", "1", ...]  // nvidia-smi -L indices, empty if no runtime
220/// }
221/// ```
222///
223/// Consumers (`entrenar::train::gputrain_007::verdict_from_version_json_keys`
224/// and `verdict_from_version_json_fields`) parse this and assert schema
225/// completeness + field invariants (`visible_devices.len() <= 16`, no
226/// `cuda_feature && !cuda_runtime_available` inconsistency).
227pub fn emit_version_json() {
228    let cuda_feature = cfg!(feature = "cuda");
229
230    // cuda_runtime_available: try nvidia-smi -L. Present-and-exits-0 ⇒ true.
231    // This matches how gputrain_003 queries nvidia-smi — keep the probe
232    // consistent with the residency check.
233    let cuda_runtime_available = std::process::Command::new("nvidia-smi")
234        .arg("-L")
235        .output()
236        .map(|o| o.status.success())
237        .unwrap_or(false);
238
239    // visible_devices: if the runtime is available, parse nvidia-smi -L
240    // output (one GPU per line, "GPU 0: ...", "GPU 1: ..."). Emit the
241    // index strings to match INV-GPUTRAIN-001 grammar (:0..:15).
242    let visible_devices: Vec<String> = if cuda_runtime_available {
243        std::process::Command::new("nvidia-smi")
244            .arg("-L")
245            .output()
246            .ok()
247            .and_then(|o| String::from_utf8(o.stdout).ok())
248            .map(|s| {
249                s.lines()
250                    .filter_map(|line| {
251                        // Expect "GPU <idx>: <name> (UUID: <uuid>)"
252                        line.strip_prefix("GPU ").and_then(|rest| {
253                            rest.split_once(':').map(|(idx, _)| idx.trim().to_string())
254                        })
255                    })
256                    .collect()
257            })
258            .unwrap_or_default()
259    } else {
260        Vec::new()
261    };
262
263    let body = serde_json::json!({
264        "name": "apr",
265        "version": env!("CARGO_PKG_VERSION"),
266        "git_sha": env!("APR_GIT_SHA"),
267        "cuda_feature": cuda_feature,
268        "cuda_runtime_available": cuda_runtime_available,
269        "visible_devices": visible_devices,
270    });
271
272    // Emit pretty-printed JSON on stdout so it's grep-friendly and
273    // round-trippable through `| jq .cuda_feature`.
274    println!(
275        "{}",
276        serde_json::to_string_pretty(&body).expect("build version json")
277    );
278}