Skip to main content

apr_cli/
commands_enum.rs

1
2/// Compute backends `--backend` accepts on `apr run` / `apr chat`.
3///
4/// The flag used to be a free-form `String`: `--backend banana` printed
5/// `Backend override: banana` and then quietly ran the default backend. That is
6/// the exact failure the `--backend cuda` guard in `dispatch.rs` exists to
7/// prevent — a run whose throughput number was taken through a backend the
8/// caller did not ask for — so a typo must be rejected by the parser, not
9/// echoed back.
10pub const BACKEND_VALUES: [&str; 3] = ["cuda", "cpu", "wgpu"];
11
12/// The ONE `--backend` declaration, flattened into every command that offers the
13/// inference backend override (`apr run`, `apr chat`, `apr serve run`).
14///
15/// #2583: the `value_parser` above was applied by hand at each site, so
16/// `apr serve run` — added later — declared a bare
17/// `#[arg(long, value_name = "BACKEND")]` and accepted ANY string:
18/// `apr serve run --backend nonsense` parsed, `ServerConfig.backend` carried
19/// `"nonsense"`, and the server came up on whatever backend it would have picked
20/// anyway. Copying the `value_parser` to that third site would have left the same
21/// hand-copy hazard for the fourth (cf. #2585, where two `apr` bin targets were
22/// duplicated by hand and had already diverged), so the declaration itself is
23/// shared: a new consumer writes `#[command(flatten)] backend: BackendArg` and
24/// cannot express the unvalidated form.
25#[derive(clap::Args, Debug, Clone, Default, PartialEq, Eq)]
26pub struct BackendArg {
27    /// Compute backend override (cuda, cpu, wgpu)
28    #[arg(long, value_name = "BACKEND", value_parser = BACKEND_VALUES)]
29    pub backend: Option<String>,
30}
31
32/// Backends `apr finetune --gpu-backend` accepts.
33///
34/// #2583 follow-up: this site is the SAME silent-wrong-backend defect as
35/// `apr serve run --backend`, one command over. It was declared
36/// `#[arg(long, default_value = "auto")] gpu_backend: String` with no
37/// `value_parser`, and `gpu_backend_notice()` (commands/finetune.rs:265)
38/// dispatches on it with a catch-all `_ =>` arm that means "auto". So
39/// `--gpu-backend cudaa` parsed, printed `auto → …`, and trained on whatever
40/// backend auto-selection picked — the typo was indistinguishable from not
41/// passing the flag at all.
42///
43/// It is deliberately NOT `BackendArg`: the domains genuinely differ. `auto` is
44/// a valid selection here and is absent from `BACKEND_VALUES`; `cpu` is not a
45/// selectable training backend and is present there. Sharing the *declaration*
46/// across two different value sets would have to widen both, re-admitting
47/// `--backend auto` on `apr run` (unvalidated again, by a different route) and
48/// `--gpu-backend cpu` on `apr finetune` (silently the `_ =>` auto arm again).
49/// What is shared instead is the *invariant*, enforced by
50/// `test_every_backend_arg_advertises_its_values_2583` over the built clap tree.
51pub const FINETUNE_GPU_BACKEND_VALUES: [&str; 3] = ["auto", "cuda", "wgpu"];
52
53/// Provider backends `apr distill --backend` accepts.
54///
55/// Unlike the two above this one was NOT silently accepted: `distill::run()`
56/// (commands/distill.rs:464-503) matches on it before any I/O and returns
57/// `ValidationFailed` naming both valid values, covered by
58/// `distill_run_unknown_backend_errors`. Wiring the same list into clap is
59/// defence in depth and makes `--help` advertise the set, but it closes a
60/// help-text gap, not a silent-wrong-backend defect. The runtime check stays.
61pub const DISTILL_BACKEND_VALUES: [&str; 2] = ["fixture", "cuda"];
62
63/// Trace detail levels `--trace-level` accepts.
64///
65/// Each value is dispatched on by string equality in `run_entry.rs`; an
66/// unrecognised value silently selected "no extra trace output at all" while
67/// printing `Trace level: <typo>` as though it had taken effect.
68pub const TRACE_LEVEL_VALUES: [&str; 5] = ["none", "basic", "layer", "payload", "chrome"];
69
70/// Output formats `apr run -f/--format` accepts.
71pub const RUN_FORMAT_VALUES: [&str; 4] = ["text", "json", "srt", "vtt"];
72
73/// Output format for `apr code` non-interactive mode (PMAT-CODE-OUTPUT-FORMAT-001).
74/// Mirrors Claude Code's `claude -p --output-format <fmt>` parity row.
75#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
76pub enum CodeOutputFormat {
77    /// Plain assistant text on stdout (default; existing behavior).
78    #[default]
79    Text,
80    /// Structured JSON envelope: `{type:"result", subtype:"success", result, session_id, duration_ms}`.
81    Json,
82}
83
84/// Input format for `apr code` non-interactive mode (PMAT-CODE-INPUT-FORMAT-001).
85/// `--input-format json` reads `{"role":"user","content":"..."}` from stdin instead
86/// of treating stdin as raw prompt text.
87#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
88pub enum CodeInputFormat {
89    /// Raw prompt text from positional args or stdin (default; existing behavior).
90    #[default]
91    Text,
92    /// JSON message envelope on stdin: `{"role":"user","content":"..."}`.
93    Json,
94}
95
96#[derive(Subcommand, Debug)]
97pub enum Commands {
98    /// Run model directly (auto-download, cache, execute)
99    Run {
100        /// Model source: local path, hf://org/repo, or URL
101        #[arg(value_name = "SOURCE")]
102        source: String,
103        /// Text prompt (positional): `apr run model.gguf "What is 2+2?"`
104        #[arg(value_name = "PROMPT")]
105        positional_prompt: Option<String>,
106        /// Input file (audio, text, etc.)
107        #[arg(short, long)]
108        input: Option<PathBuf>,
109        /// Text prompt for generation (for LLM models)
110        #[arg(short, long)]
111        prompt: Option<String>,
112        /// Maximum tokens to generate (default: 32)
113        #[arg(short = 'n', long, default_value = "32")]
114        max_tokens: usize,
115        /// Enable streaming output
116        #[arg(long)]
117        stream: bool,
118        /// Language code (for ASR models)
119        #[arg(short, long)]
120        language: Option<String>,
121        /// Task (transcribe, translate)
122        #[arg(short, long)]
123        task: Option<String>,
124        /// Output format (text, json, srt, vtt)
125        #[arg(short = 'f', long, default_value = "text", value_parser = RUN_FORMAT_VALUES)]
126        format: String,
127        /// Disable GPU acceleration (force CPU-only inference)
128        #[arg(long, alias = "cpu", conflicts_with = "gpu")]
129        no_gpu: bool,
130        /// Force GPU acceleration
131        #[arg(long, conflicts_with = "no_gpu")]
132        gpu: bool,
133        /// Offline mode: block all network access (Sovereign AI compliance)
134        #[arg(long)]
135        offline: bool,
136        /// Benchmark mode: output performance metrics (tok/s, latency)
137        #[arg(long)]
138        benchmark: bool,
139        /// Enable inference tracing (APR-TRACE-001)
140        #[arg(long)]
141        trace: bool,
142        /// Trace specific steps only (comma-separated)
143        #[arg(long, value_delimiter = ',')]
144        trace_steps: Option<Vec<String>>,
145        /// Verbose tracing (show tensor values)
146        #[arg(long)]
147        trace_verbose: bool,
148        /// Save trace output to JSON file
149        #[arg(long, value_name = "FILE")]
150        trace_output: Option<PathBuf>,
151        /// Trace detail level (none, basic, layer, payload, chrome)
152        /// "chrome" outputs chrome://tracing JSON integrating layer trace + brick profile.
153        /// F-CLIPARITY-01 / PMAT-386 / paiml/aprender#574
154        #[arg(long, value_name = "LEVEL", default_value = "basic", value_parser = TRACE_LEVEL_VALUES)]
155        trace_level: String,
156        /// Shorthand for --trace --trace-level payload (tensor value inspection)
157        #[arg(long)]
158        trace_payload: bool,
159        /// Enable inline Roofline profiling (PMAT-SHOWCASE-METHODOLOGY-001)
160        #[arg(long)]
161        profile: bool,
162        /// Apply chat template for Instruct models (GAP-UX-001)
163        ///
164        /// Wraps prompt in ChatML format for Qwen2, LLaMA, Mistral Instruct models.
165        /// Format: <|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n
166        #[arg(long)]
167        chat: bool,
168        /// Sampling temperature (0.0 = greedy, default: 0.0)
169        #[arg(long, default_value = "0.0")]
170        temperature: f32,
171        /// Top-k sampling (default: 1 = greedy)
172        #[arg(long, default_value = "1")]
173        top_k: usize,
174        /// Top-p nucleus sampling (0.0 = disabled). When set with --top-k, applies top-k first then top-p.
175        /// F-CLIPARITY-01 / PMAT-381 / paiml/aprender#569
176        #[arg(long)]
177        top_p: Option<f32>,
178        /// RNG seed for deterministic sampling (default: 299792458, matching Candle)
179        /// F-CLIPARITY-01 / PMAT-382 / paiml/aprender#570
180        #[arg(long, default_value = "299792458")]
181        seed: u64,
182        /// Repetition penalty (1.0 = no penalty, >1.0 penalizes repeats)
183        /// F-CLIPARITY-01 / PMAT-383 / paiml/aprender#571
184        #[arg(long, default_value = "1.0")]
185        repeat_penalty: f32,
186        /// Context window for repetition penalty (number of recent tokens to check)
187        /// F-CLIPARITY-01 / PMAT-384 / paiml/aprender#571
188        #[arg(long, default_value = "64")]
189        repeat_last_n: usize,
190        /// Process prompt tokens one-by-one instead of batched prefill.
191        /// Useful for debugging prefill correctness (comparing per-token attention).
192        /// F-CLIPARITY-01 / PMAT-385 / paiml/aprender#572
193        #[arg(long)]
194        split_prompt: bool,
195        /// Batch mode: read prompts from JSONL, output results as JSONL.
196        /// Model loads once, processes all prompts sequentially.
197        /// Each input line: {"prompt": "...", "task_id": "..."}
198        /// Chat template is applied automatically.
199        #[arg(long, value_name = "FILE")]
200        batch_jsonl: Option<PathBuf>,
201        /// Show verbose output (model loading, backend info)
202        #[arg(short, long)]
203        verbose: bool,
204        // PMAT-488 / #2583: shared `--backend` declaration (see `BackendArg`).
205        #[command(flatten)]
206        backend: BackendArg,
207    },
208    /// Inference server (plan/run)
209    Serve {
210        #[command(subcommand)]
211        command: ServeCommands,
212    },
213    /// Inspect model metadata, vocab, and structure
214    Inspect {
215        /// Path to .apr model file
216        #[arg(value_name = "FILE")]
217        file: PathBuf,
218        /// Show vocabulary details
219        #[arg(long)]
220        vocab: bool,
221        /// Show filter/security details
222        #[arg(long)]
223        filters: bool,
224        /// Show weight statistics
225        #[arg(long)]
226        weights: bool,
227        /// Output as JSON
228        #[arg(long)]
229        json: bool,
230        /// Emit a 0-100 model quality score block.
231        ///
232        /// Per SPEC-SHIP-TWO-001 §84 P3-A (AC-SHIP2-007 quality
233        /// threshold ≥ 90). The score aggregates: physics checks
234        /// (no NaN/Inf, no all-zero tensors), structural
235        /// completeness (architecture / hidden_size / num_layers
236        /// metadata present), provenance (license + data_source +
237        /// data_license non-empty), HF identity (hf_architecture
238        /// stamped per PMAT-690 P0-K), and tokenizer presence
239        /// (has_vocab + embedded merges). A ship-blocking model
240        /// MUST score ≥ 90 by this rubric.
241        #[arg(long)]
242        quality: bool,
243    },
244    /// Simple debugging output ("drama" mode available), or a debug subcommand
245    ///
246    /// `apr debug model.apr` dumps the file. `apr debug embed-viz --model M`
247    /// projects the model's token-embedding table to 2-D — the producer
248    /// `apr embed-viz-lint` reads (aprender#2377 finding 3).
249    Debug {
250        /// Path to .apr model file (omit only when using a subcommand)
251        #[arg(value_name = "FILE")]
252        file: Option<PathBuf>,
253        /// Debug subcommand, e.g. `embed-viz`
254        #[command(subcommand)]
255        action: Option<DebugCommands>,
256        /// Theatrical "drama" mode output
257        #[arg(long)]
258        drama: bool,
259        /// Show hex dump
260        #[arg(long)]
261        hex: bool,
262        /// Extract ASCII strings
263        #[arg(long)]
264        strings: bool,
265        /// Limit output lines
266        #[arg(long, default_value = "256")]
267        limit: usize,
268    },
269    /// Validate model integrity and quality
270    Validate {
271        /// Path to .apr model file
272        #[arg(value_name = "FILE")]
273        file: PathBuf,
274        /// Show 100-point quality assessment
275        #[arg(long)]
276        quality: bool,
277        /// Strict validation (fail on warnings)
278        #[arg(long)]
279        strict: bool,
280        /// Minimum score to pass (0-100)
281        #[arg(long)]
282        min_score: Option<u8>,
283    },
284    /// Validate a publish manifest (FALSIFY-PM-001..006).
285    ///
286    /// Contract: `contracts/publish-manifest-v1.yaml`
287    /// Spec:     SPEC-SHIP-TWO-001 §12.3 AC-EX-004
288    ValidateManifest {
289        /// Path to manifest YAML
290        #[arg(value_name = "MANIFEST")]
291        file: PathBuf,
292        /// Optional local .apr artifact to discharge FALSIFY-PM-002 (sha256 match)
293        #[arg(long, value_name = "APR_FILE")]
294        artifact: Option<PathBuf>,
295        /// Discharge FALSIFY-PM-003 via network: HTTP HEAD + streaming sha256.
296        /// Default is DEFERRED (offline-safe). Ignored when --offline is set.
297        /// Closes F-PUBLISH-EXTRA-001::dogfood_ex05 (no Python in ex-05).
298        #[arg(long)]
299        live: bool,
300    },
301    /// Compare two models
302    Diff {
303        /// First model file
304        #[arg(value_name = "FILE1")]
305        file1: PathBuf,
306        /// Second model file
307        #[arg(value_name = "FILE2")]
308        file2: PathBuf,
309        /// Show weight-level differences
310        #[arg(long)]
311        weights: bool,
312        /// Compare actual tensor values with statistical analysis
313        #[arg(long)]
314        values: bool,
315        /// Filter tensors by name pattern (for --values)
316        #[arg(long)]
317        filter: Option<String>,
318        /// Maximum number of tensors to compare (for --values)
319        #[arg(long, default_value = "10")]
320        limit: usize,
321        /// Account for transpose when comparing (GGUF col-major vs APR row-major)
322        #[arg(long)]
323        transpose_aware: bool,
324        /// Output as JSON
325        #[arg(long)]
326        json: bool,
327        /// CRUX-B-20: per-tensor quant roundtrip error report (RMSE / cosine / max_abs).
328        /// FILE1 is the reference (fp16/fp32/bf16); FILE2 is the quantized variant.
329        #[arg(long)]
330        quant_roundtrip: bool,
331        /// CRUX-B-20: cosine threshold for the quant-roundtrip exit-code gate.
332        /// Any tensor with cosine < threshold makes the command exit non-zero.
333        #[arg(long, default_value = "0.95",
334              value_parser = crate::commands::threshold_arg::parse_cosine_f32)]
335        threshold: f32,
336        /// CRUX-B-20: suppress the threshold exit-code gate (still emits the report).
337        #[arg(long)]
338        no_threshold: bool,
339    },
340    /// List tensor names and shapes
341    Tensors {
342        /// Path to .apr model file
343        #[arg(value_name = "FILE")]
344        file: PathBuf,
345        /// Show tensor statistics (mean, std, min, max)
346        #[arg(long)]
347        stats: bool,
348        /// Filter tensors by name pattern
349        #[arg(long)]
350        filter: Option<String>,
351        /// Limit number of tensors shown (0 = unlimited)
352        #[arg(long, default_value = "0")]
353        limit: usize,
354        /// Output as JSON
355        #[arg(long)]
356        json: bool,
357    },
358    /// Layer-by-layer trace analysis
359    Trace {
360        /// Path to .apr model file
361        #[arg(value_name = "FILE")]
362        file: PathBuf,
363        /// Filter layers by name pattern
364        #[arg(long)]
365        layer: Option<String>,
366        /// Compare with reference model
367        #[arg(long)]
368        reference: Option<PathBuf>,
369        /// Output as JSON
370        #[arg(long)]
371        json: bool,
372        /// Verbose output with per-layer stats
373        #[arg(short, long)]
374        verbose: bool,
375        /// Trace payload through model
376        #[arg(long)]
377        payload: bool,
378        /// Diff mode
379        #[arg(long)]
380        diff: bool,
381        /// Interactive mode
382        #[arg(long)]
383        interactive: bool,
384        /// Save per-stage F32 tensors during trace for SHIP-007 layer-0
385        /// element-wise diff. Comma-separated stage names from
386        /// `apr-cli-trace-save-tensor-v1.yaml` (e.g.
387        /// `embedding,qkv_matmul,attention`). Pass `all` to save every
388        /// stage. Output goes to `--save-tensor-dir` if provided,
389        /// else `<file_dir>/trace-tensors/<run_id>/`.
390        #[arg(long, value_name = "STAGES")]
391        save_tensor: Option<String>,
392        /// Output directory for `--save-tensor` (default: sibling
393        /// `trace-tensors/<run_id>/`).
394        #[arg(long, value_name = "DIR")]
395        save_tensor_dir: Option<PathBuf>,
396        /// Layer-id range for `--save-tensor` (default: 0..1, i.e.
397        /// layer 0 only). Format: `START..END` (Rust range syntax,
398        /// END exclusive).
399        #[arg(long, value_name = "RANGE", default_value = "0..1")]
400        save_tensor_layers: String,
401    },
402    /// Check for best practices and conventions
403    Lint {
404        /// Path to .apr model file
405        #[arg(value_name = "FILE")]
406        file: PathBuf,
407        /// Fail on warnings as well as errors.
408        ///
409        /// By default only ERROR-level findings fail the run. Every real model
410        /// carries advisory metadata warnings (missing license, model_card,
411        /// provenance), so gating on warnings meant `apr lint` could not exit 0
412        /// on anything and its exit code told you nothing.
413        #[arg(long)]
414        strict: bool,
415    },
416    /// Evaluate a BeatBenchmark contract against a measured value (PMAT-741)
417    #[command(name = "beat-run")]
418    BeatRun {
419        /// Path to a beat-benchmark contract YAML (e.g. contracts/beat-sklearn-iris-v1.yaml)
420        #[arg(value_name = "CONTRACT")]
421        contract: PathBuf,
422        /// Measured metric value; when given, emit a WON/REGRESSED verdict and
423        /// exit non-zero on regression. Omit to just report the pinned baseline.
424        #[arg(long, value_name = "VALUE")]
425        measured: Option<f64>,
426    },
427    /// Emit a SHA-256 manifest of input files (CRUX-G-05)
428    Manifest {
429        /// Files to include in the manifest (one entry per file)
430        #[arg(value_name = "FILES", num_args = 1..)]
431        files: Vec<PathBuf>,
432        /// Output JSON manifest path
433        #[arg(short, long, value_name = "MAN_JSON")]
434        output: PathBuf,
435    },
436    /// Explain errors, architecture, tensors, and kernel dispatch
437    Explain {
438        /// Error code, model file path, or family name (auto-detected)
439        #[arg(value_name = "CODE_OR_FILE")]
440        code_or_file: Option<String>,
441        /// Path to .apr model file (optional context for --tensor)
442        #[arg(short, long)]
443        file: Option<PathBuf>,
444        /// Explain a specific tensor
445        #[arg(long)]
446        tensor: Option<String>,
447        /// Explain kernel dispatch pipeline for architecture
448        #[arg(long)]
449        kernel: bool,
450        /// Output as JSON
451        #[arg(long)]
452        json: bool,
453        /// Show kernel contract details and proof obligations
454        #[arg(short, long)]
455        verbose: bool,
456        /// Show per-kernel proof status from contract tests
457        #[arg(long)]
458        proof_status: bool,
459    },
460    /// Manage canary tests for regression
461    Canary {
462        #[command(subcommand)]
463        command: CanaryCommands,
464    },
465    /// Export model to other formats
466    Export {
467        /// Path to .apr model file
468        #[arg(value_name = "FILE", required_unless_present = "list_formats")]
469        file: Option<PathBuf>,
470        /// Output format (safetensors, gguf, mlx, onnx, openvino, coreml)
471        #[arg(long, default_value = "safetensors")]
472        format: String,
473        /// Output file/directory path
474        #[arg(short, long)]
475        output: Option<PathBuf>,
476        /// Apply quantization during export (int8, int4, fp16)
477        #[arg(long)]
478        quantize: Option<String>,
479        /// List all supported export formats
480        #[arg(long)]
481        list_formats: bool,
482        /// Batch export to multiple formats (comma-separated: gguf,mlx,safetensors)
483        #[arg(long)]
484        batch: Option<String>,
485        /// Output in JSON format
486        #[arg(long)]
487        json: bool,
488        /// Plan mode (validate inputs, show export plan, no execution)
489        #[arg(long)]
490        plan: bool,
491        /// #2392: Overwrite an existing output file (refused without it)
492        #[arg(short, long)]
493        force: bool,
494    },
495    /// Import from external formats (hf://org/repo, local files, URLs)
496    Import {
497        /// Source: hf://org/repo, local file, or URL
498        #[arg(value_name = "SOURCE")]
499        source: String,
500        /// Output .apr file path (default: derived from source name)
501        #[arg(short, long)]
502        output: Option<PathBuf>,
503        /// Model architecture (whisper, llama, bert, qwen2, qwen3, gpt2, starcoder, gpt-neox, opt, phi, gemma, falcon, mamba, t5, auto)
504        #[arg(long, default_value = "auto")]
505        arch: String,
506        /// Quantization (int8, int4, fp16)
507        #[arg(long)]
508        quantize: Option<String>,
509        /// Strict mode: reject unverified architectures and fail on validation errors
510        #[arg(long)]
511        strict: bool,
512        /// Preserve Q4K quantization for fused kernel inference (GGUF only)
513        /// Uses realizar's Q4K converter instead of dequantizing to F32
514        #[arg(long)]
515        preserve_q4k: bool,
516        /// PMAT-232: External tokenizer.json for weights-only GGUF files.
517        /// Required if the GGUF has no embedded tokenizer vocabulary.
518        #[arg(long)]
519        tokenizer: Option<PathBuf>,
520        /// F-GT-001: Enforce provenance chain. Rejects pre-baked GGUF imports
521        /// (only SafeTensors sources allowed). Ensures single-provenance testing.
522        #[arg(long)]
523        enforce_provenance: bool,
524        /// GH-223: Allow import without config.json (default: error).
525        /// Without config.json, hyperparameters like rope_theta are inferred from
526        /// tensor shapes and may be wrong, producing garbage output.
527        #[arg(long)]
528        allow_no_config: bool,
529    },
530    /// Download and cache model OR HuggingFace dataset (Ollama-like UX)
531    Pull {
532        /// Model reference (alias, hf:// URI, or org/repo) OR "dataset"
533        /// asset-type discriminator. When this value is the literal
534        /// string "dataset", the next positional `repo` is the
535        /// HuggingFace dataset repo and dataset-pull semantics apply.
536        #[arg(value_name = "MODEL_OR_ASSET_TYPE")]
537        model_ref: String,
538        /// Dataset repository (used only when model_ref == "dataset").
539        /// Per `apr-cli-pull-dataset-v1.yaml`.
540        #[arg(value_name = "REPO")]
541        repo: Option<String>,
542        /// Force re-download even if cached
543        #[arg(long)]
544        force: bool,
545        /// Verify an already-cached model by re-hashing every file against the
546        /// BLAKE3 checksums recorded in `.apr-manifest.json` at download time.
547        ///
548        /// `apr pull` has always RECORDED those hashes and never checked them:
549        /// the only integrity check in the tree compares file SIZE. Size cannot
550        /// see a same-length corruption (a 7.1 GB SafeTensors blob was found
551        /// with 27 of 339 tensors zeroed, byte-length exactly correct). This
552        /// costs O(bytes) by design, which is why it is opt-in. Performs no
553        /// network I/O.
554        #[arg(long)]
555        verify: bool,
556        /// CRUX-A-01: resolve short name to canonical URL and exit without
557        /// performing any network I/O.
558        #[arg(long)]
559        dry_run: bool,
560        /// CRUX-A-03: pin to a specific branch, tag, or git SHA on the remote
561        /// (HuggingFace Hub). Defaults to "main" when omitted.
562        #[arg(long, value_name = "REV")]
563        revision: Option<String>,
564        /// CRUX-A-20: offline mode — forbid any outbound network I/O.
565        /// Equivalent to APR_OFFLINE=1 or HF_HUB_OFFLINE=1 in the environment.
566        #[arg(long)]
567        offline: bool,
568        /// (dataset mode) Glob pattern for shard selection. May be passed
569        /// multiple times; matches are unioned. fnmatch-compatible
570        /// (`*`, `?`, `[a-z]`). No-match is fail-fast.
571        #[arg(long, value_name = "GLOB")]
572        include: Vec<String>,
573        /// (dataset mode) Output directory. Default:
574        /// `~/.cache/aprender/datasets/<repo>/`.
575        #[arg(short = 'o', long)]
576        output: Option<PathBuf>,
577    },
578    /// Registry operations (CRUX-A-01): inspect alias map, etc.
579    Registry {
580        #[command(subcommand)]
581        command: crate::commands::registry::RegistryCommands,
582    },
583    /// List cached models
584    #[command(name = "list", alias = "ls")]
585    List,
586    /// Remove model from cache
587    #[command(name = "rm", alias = "remove")]
588    Rm {
589        /// Model reference to remove
590        #[arg(value_name = "MODEL")]
591        model_ref: String,
592    },
593    /// Convert/optimize model
594    Convert {
595        /// Path to .apr model file
596        #[arg(value_name = "FILE")]
597        file: PathBuf,
598        /// Quantize to format (int8, int4, fp16, q4k)
599        #[arg(long)]
600        quantize: Option<String>,
601        /// Compress output (none, zstd, zstd-max, lz4)
602        #[arg(long)]
603        compress: Option<String>,
604        /// Output file path
605        #[arg(short, long)]
606        output: PathBuf,
607        /// Force overwrite existing files
608        #[arg(short, long)]
609        force: bool,
610    },
611    /// Stamp provenance fields (license, data_source, data_license) onto an existing .apr file
612    ///
613    /// SHIP-009 full-discharge enabler — patches the three provenance fields on
614    /// a pre-built APR v2 artifact (e.g., the shipped MODEL-1 teacher whose
615    /// fields are all (missing) because it was built before GATE-APR-PROV-001..003
616    /// shipped). Tensor bytes and header flags are preserved verbatim.
617    Stamp {
618        /// Path to input .apr model file
619        #[arg(value_name = "FILE")]
620        file: PathBuf,
621        /// SPDX license identifier (e.g., Apache-2.0)
622        #[arg(long)]
623        license: Option<String>,
624        /// Training-data source (e.g., huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct)
625        #[arg(long = "data-source")]
626        data_source: Option<String>,
627        /// SPDX license for data_source (e.g., Apache-2.0)
628        #[arg(long = "data-license")]
629        data_license: Option<String>,
630        /// HuggingFace class name (e.g., Qwen2ForCausalLM, LlamaForCausalLM).
631        ///
632        /// PMAT-690 P0-K extension (SPEC §86): patch the upstream
633        /// `architectures[0]` stamp on a pre-P0-K APR so downstream
634        /// consumers (apr inspect --quality, apr pretrain --init,
635        /// apr export → llama-cli) see the correct HF identity.
636        #[arg(long = "hf-architecture")]
637        hf_architecture: Option<String>,
638        /// HuggingFace model_type slug (e.g., qwen2, llama).
639        ///
640        /// PMAT-690 P0-K extension (SPEC §86).
641        #[arg(long = "hf-model-type")]
642        hf_model_type: Option<String>,
643        /// Lowercase architecture family slug (e.g., qwen2, llama).
644        ///
645        /// PMAT-690 P0-K extension (SPEC §86). This is the field
646        /// `apr pretrain --init` reads for arch dispatch — without
647        /// patching it, pre-P0-K checkpoints with the P0-H "LlamaForCausalLM"
648        /// fallback in this field cannot be loaded as Qwen2 inits.
649        #[arg(long)]
650        architecture: Option<String>,
651        /// Directory containing tokenizer files (vocab.json + merges.txt
652        /// OR tokenizer.json). When provided, embeds the vocabulary +
653        /// BPE merges into the APR's `custom.tokenizer.vocabulary` /
654        /// `custom.tokenizer.merges` JSON metadata AND sets the
655        /// HAS_VOCAB header flag.
656        ///
657        /// PMAT-690 P3-C-prep defect 1 fix (2026-05-17): pre-P0-K APRs
658        /// trained from inits without embedded tokenizers fail `apr run`
659        /// with PMAT-172. This flag lets the §86 salvage recipe embed
660        /// the tokenizer post-hoc so the artifact is self-contained
661        /// for inference (the apr binary's headline use case).
662        #[arg(long = "tokenizer", value_name = "DIR")]
663        tokenizer_dir: Option<PathBuf>,
664        /// Output file path
665        #[arg(short, long)]
666        output: PathBuf,
667        /// Force overwrite existing files
668        #[arg(short, long)]
669        force: bool,
670    },
671    /// Compile model into standalone executable (APR-SPEC §4.16)
672    Compile {
673        /// Input .apr model file
674        #[arg(value_name = "FILE", required_unless_present = "list_targets")]
675        file: Option<PathBuf>,
676        /// Output binary path (default: derived from model name)
677        #[arg(short, long)]
678        output: Option<PathBuf>,
679        /// Target triple (e.g., x86_64-unknown-linux-musl)
680        #[arg(long)]
681        target: Option<String>,
682        /// Quantize weights before embedding (int8, int4, fp16)
683        #[arg(long)]
684        quantize: Option<String>,
685        /// Release mode (optimized)
686        #[arg(long)]
687        release: bool,
688        /// Strip debug symbols
689        #[arg(long)]
690        strip: bool,
691        /// Enable LTO (Link-Time Optimization)
692        #[arg(long)]
693        lto: bool,
694        /// List available compilation targets
695        #[arg(long)]
696        list_targets: bool,
697    },
698    /// Merge multiple models
699    Merge {
700        /// Model files to merge
701        #[arg(value_name = "FILES", num_args = 2..)]
702        files: Vec<PathBuf>,
703        /// Merge strategy (average, weighted, slerp, ties, dare)
704        #[arg(long, default_value = "average")]
705        strategy: String,
706        /// Output file path (optional in --plan mode)
707        #[arg(short, long, required_unless_present = "plan")]
708        output: Option<PathBuf>,
709        /// Weights for weighted merge (comma-separated, e.g., "0.7,0.3")
710        #[arg(long, value_delimiter = ',')]
711        weights: Option<Vec<f32>>,
712        /// Base model for TIES/DARE (task vectors computed as delta from base)
713        #[arg(long)]
714        base_model: Option<PathBuf>,
715        /// DARE drop probability (default: 0.9)
716        #[arg(long, default_value = "0.9")]
717        drop_rate: f32,
718        /// TIES trim density threshold (default: 0.2)
719        #[arg(long, default_value = "0.2")]
720        density: f32,
721        /// RNG seed for DARE (default: 42)
722        #[arg(long, default_value = "42")]
723        seed: u64,
724        /// Plan mode (validate inputs, show merge plan, no execution)
725        #[arg(long)]
726        plan: bool,
727        /// #2392: Overwrite an existing output file (refused without it)
728        #[arg(short, long)]
729        force: bool,
730    },
731    /// Quantize model weights (GH-243)
732    Quantize {
733        /// Input model file
734        #[arg(value_name = "FILE")]
735        file: PathBuf,
736        /// Quantization scheme: int8, int4, fp16, q4k
737        #[arg(long, short = 's', default_value = "int4")]
738        scheme: String,
739        /// Output file path (required unless --plan)
740        #[arg(short, long)]
741        output: Option<PathBuf>,
742        /// Output format override (apr, gguf, safetensors)
743        #[arg(long)]
744        format: Option<String>,
745        /// Batch quantization (comma-separated schemes)
746        #[arg(long)]
747        batch: Option<String>,
748        /// Plan mode (estimate only, no execution)
749        #[arg(long)]
750        plan: bool,
751        /// Force overwrite existing files
752        #[arg(short, long)]
753        force: bool,
754    },
755    /// Model optimization commands (fine-tune, prune, distill)
756    #[command(flatten)]
757    ModelOps(ModelOpsCommands),
758    /// Start the MCP (Model Context Protocol) server over stdio
759    ///
760    /// Exposes `apr` as MCP tools for Claude Code, Cursor, Cline, and other
761    /// MCP clients. Configure via `.mcp.json` with `{"command":"apr","args":["mcp"]}`.
762    Mcp {},
763    /// Interactive terminal UI
764    Tui {
765        /// Path to .apr model file
766        #[arg(value_name = "FILE")]
767        file: Option<PathBuf>,
768    },
769    /// Model self-test: 10-stage pipeline integrity check (APR-TRACE-001)
770    Check {
771        /// Path to model file
772        #[arg(value_name = "FILE")]
773        file: PathBuf,
774        /// Disable GPU acceleration
775        #[arg(long)]
776        no_gpu: bool,
777        /// Output as JSON
778        #[arg(long)]
779        json: bool,
780    },
781    /// GPU status and VRAM reservation management (GPU-SHARE-001)
782    #[cfg(feature = "training")]
783    Gpu {
784        /// Show reservations as JSON
785        #[arg(long)]
786        json: bool,
787    },
788    /// Sovereign AI coding assistant — all inference local via realizar (PMAT-182)
789    
790    Code {
791        /// Path to local GGUF/APR model file (prefers .apr format)
792        #[arg(long)]
793        model: Option<PathBuf>,
794
795        /// Project directory (loads APR.md/CLAUDE.md from this path)
796        #[arg(long, default_value = ".")]
797        project: PathBuf,
798
799        /// Resume previous session (optionally by ID)
800        #[arg(long)]
801        resume: Option<Option<String>>,
802
803        /// Agent manifest (advanced — overrides defaults)
804        #[arg(long)]
805        manifest: Option<PathBuf>,
806
807        /// Initial prompt (non-interactive: print response and exit)
808        #[arg(short, long)]
809        print: bool,
810
811        /// Prompt text (positional, for -p mode).
812        ///
813        /// NOT `trailing_var_arg`: that made clap absorb everything after the
814        /// first prompt word into the prompt, so `apr code -p "hi" --model X`
815        /// silently discarded `--model` and ran whatever auto-discovery found
816        /// — a wrong-model execution with no diagnostic — and typo'd flags
817        /// produced no parse error at all. Options are now parsed in any
818        /// position; a prompt that genuinely starts with `-` needs `--`.
819        prompt: Vec<String>,
820
821        /// Max turns before stopping
822        #[arg(long, default_value = "50")]
823        max_turns: u32,
824
825        /// Emit a `ccpa-trace.jsonl` describing the run to this path.
826        /// Format mirrors the schema at
827        /// <https://github.com/paiml/claude-code-parity-apr/blob/main/contracts/claude-code-parity-apr-v1.yaml>
828        /// (`§ trace_schema`). Used by `ccpa measure` to score apr-code
829        /// against canonical Claude Code reference fixtures.
830        #[arg(long)]
831        emit_trace: Option<PathBuf>,
832
833        /// Output format for non-interactive (`-p`) mode (PMAT-CODE-OUTPUT-FORMAT-001).
834        /// `text` (default): plain assistant text.
835        /// `json`: structured `{type:"result", subtype:"success", result, session_id, duration_ms}`
836        /// envelope matching Claude Code's `claude -p --output-format json` shape.
837        #[arg(long, value_enum, default_value_t = CodeOutputFormat::Text)]
838        output_format: CodeOutputFormat,
839
840        /// Input format for non-interactive stdin (PMAT-CODE-INPUT-FORMAT-001).
841        /// `text` (default): treat stdin as raw prompt text.
842        /// `json`: parse `{"role":"user","content":"..."}` from stdin and use `content`
843        /// as the prompt. Matches Claude Code's `claude -p --input-format json` shape.
844        #[arg(long, value_enum, default_value_t = CodeInputFormat::Text)]
845        input_format: CodeInputFormat,
846    },
847    /// Extended analysis, profiling, QA, and visualization commands
848    #[command(flatten)]
849    Extended(ExtendedCommands),
850
851    /// Monorepo management (publish, shims, audit, archive) [dev-only]
852    #[cfg(feature = "dev")]
853    #[command(subcommand)]
854    Mono(crate::commands::mono::MonoCommands),
855
856    /// RAG pipeline: index, query, transcribe (was the `trueno-rag` binary)
857    #[command(subcommand)]
858    Rag(aprender_rag_cli::Commands),
859
860    /// zram device management (was the `trueno-zram` binary)
861    #[command(subcommand)]
862    Zram(aprender_zram_cli::Commands),
863
864    /// Discrete-event simulation: run, render, validate, verify, emc-check
865    /// (was the `simular` binary)
866    // disable_help_subcommand: simular's `Commands` carries an explicit `Help`
867    // variant. Standalone that is fine -- its own `Cli` sets
868    // disable_help_subcommand = true, so clap does not also generate one. That
869    // attribute lives on `Cli`, which apr never constructs: it embeds the
870    // `Commands` enum directly, so clap's auto-`help` came back and collided:
871    //     Command sim: command name `help` is duplicated
872    // clap's duplicate check is #[cfg(debug_assertions)], so a release build
873    // would have SHIPPED the ambiguity instead of panicking.
874    #[command(subcommand, disable_help_subcommand = true)]
875    Sim(simular::cli::Commands),
876
877    /// Compute-graph profiling: profile, bench, roofline, doctor
878    /// (was the `aprender-cgp` binary)
879    #[command(subcommand)]
880    Cgp(cgp::cli::Commands),
881
882    /// Provable-contracts: validate, lint, score, kani, proof-status
883    /// (the `pv` binary keeps shipping under its own name; this is the
884    /// in-apr route to the same commands)
885    #[command(subcommand)]
886    Pv(aprender_contracts_cli::cli::Commands),
887}
888
889/// Subcommands for `apr debug` (aprender#2377 finding 3).
890///
891/// `embed-viz` is the PRODUCER for `apr embed-viz-lint`: CRUX-F-18 shipped the
892/// lint with help pointing at `apr debug embed-viz`, which did not exist, so
893/// its schema / row-count / determinism gates had never run on real data.
894#[derive(Subcommand, Debug)]
895pub enum DebugCommands {
896    /// Project a model's token-embedding table to 2-D and write the
897    /// `token_id,token_str,x,y` CSV `apr embed-viz-lint` reads.
898    ///
899    /// Reads the real embedding tensor (GGUF / APR / SafeTensors, dequantising
900    /// as needed). `--projection umap` is REFUSED with a non-zero exit rather
901    /// than labelling a different algorithm's output "umap".
902    EmbedViz {
903        /// Model file holding the embedding table
904        #[arg(long, value_name = "FILE")]
905        model: PathBuf,
906        /// Embedding tensor name (default: auto-detect the known names)
907        #[arg(long, value_name = "NAME")]
908        tensor: Option<String>,
909        /// Projection method: exact `pca`, seeded `random`, or `umap` (refused)
910        #[arg(long, value_enum, default_value_t = EmbedProjection::Pca)]
911        projection: EmbedProjection,
912        /// Seed pinning the random projection, so a rerun is byte-identical
913        #[arg(long, value_name = "N", default_value_t = 0)]
914        seed: u64,
915        /// Project only the first N vocabulary rows (default: all)
916        #[arg(long, value_name = "N")]
917        limit: Option<usize>,
918        /// Token text, one per line, for the `token_str` column. Without it apr
919        /// reads the GGUF vocabulary, or writes `<unresolved>`
920        #[arg(long, value_name = "FILE")]
921        tokens: Option<PathBuf>,
922        /// Write the CSV here instead of stdout
923        #[arg(short, long, value_name = "FILE")]
924        output: Option<PathBuf>,
925        /// Overwrite an existing --output file (refused without it)
926        #[arg(short, long)]
927        force: bool,
928    },
929}