Skip to main content

apr_cli/
extended_commands.rs

1/// Attention implementation under test for `apr kernel parity --impl`.
2///
3/// `Flash2` names the pinned `hf-kernels-community:flash-attn2@<sha>` CUDA
4/// kernel. This binary embeds no such kernel, so selecting it is REFUSED —
5/// never quietly answered by `Tiled` under flash2's name, which is the
6/// fabricated-provenance failure CRUX-L-02 exists to prevent.
7#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
8pub enum KernelImpl {
9    /// In-tree tiled online-softmax kernel (`realizar::brick::FlashAttentionBrick`).
10    Tiled,
11    /// Pinned hf-kernels-community flash-attn2 CUDA kernel (not embedded here).
12    Flash2,
13}
14
15/// Reference implementation for `apr kernel parity --ref`.
16#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
17pub enum KernelRef {
18    /// Materialised-score softmax attention, computed in f32 on the CPU.
19    Naive,
20}
21
22/// 2-D projection for `apr debug embed-viz --projection`.
23#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
24pub enum EmbedProjection {
25    /// Exact PCA onto the top 2 principal components (deterministic).
26    Pca,
27    /// Seeded Johnson–Lindenstrauss random projection (deterministic in --seed).
28    Random,
29    /// Not implemented in this binary — selecting it is refused, not substituted.
30    Umap,
31}
32
33/// Extended CLI commands (analysis, profiling, QA, benchmarks, and advanced tools).
34///
35/// Flattened into `Commands` via `#[command(flatten)]` so all subcommands remain
36/// top-level from the user's perspective (e.g., `apr chat`, `apr profile`).
37#[derive(Subcommand, Debug)]
38pub enum ExtendedCommands {
39    /// Interactive chat with language model
40    Chat {
41        /// Path to .apr model file
42        #[arg(value_name = "FILE")]
43        file: PathBuf,
44        /// Sampling temperature (0 = greedy, higher = more random)
45        #[arg(long, default_value = "0.7")]
46        temperature: f32,
47        /// Nucleus sampling threshold
48        #[arg(long, default_value = "0.9")]
49        top_p: f32,
50        /// Maximum tokens to generate per response
51        #[arg(long, default_value = "512")]
52        max_tokens: usize,
53        /// System prompt to set model behavior
54        #[arg(long)]
55        system: Option<String>,
56        /// Show inspection info (top-k probs, tokens/sec)
57        #[arg(long)]
58        inspect: bool,
59        /// Disable GPU acceleration (use CPU)
60        #[arg(long)]
61        no_gpu: bool,
62        /// Force GPU acceleration (requires CUDA)
63        #[arg(long)]
64        gpu: bool,
65        /// Enable inference tracing (APR-TRACE-001)
66        #[arg(long)]
67        trace: bool,
68        /// Trace specific steps only (comma-separated)
69        #[arg(long, value_delimiter = ',')]
70        trace_steps: Option<Vec<String>>,
71        /// Verbose tracing
72        #[arg(long)]
73        trace_verbose: bool,
74        /// Save trace output to JSON file
75        #[arg(long, value_name = "FILE")]
76        trace_output: Option<PathBuf>,
77        /// Trace detail level (none, basic, layer, payload)
78        #[arg(long, value_name = "LEVEL", default_value = "basic", value_parser = TRACE_LEVEL_VALUES)]
79        trace_level: String,
80        /// Enable inline Roofline profiling (PMAT-SHOWCASE-METHODOLOGY-001)
81        #[arg(long)]
82        profile: bool,
83        // PMAT-488 / #2583: shared `--backend` declaration (see `BackendArg`).
84        #[command(flatten)]
85        backend: BackendArg,
86    },
87    /// Benchmark throughput (spec H12: >= 10 tok/s)
88    Bench {
89        /// Path to model file
90        #[arg(value_name = "FILE")]
91        file: PathBuf,
92        /// Number of warmup iterations
93        #[arg(long, default_value = "3")]
94        warmup: usize,
95        /// Number of measurement iterations
96        #[arg(long, default_value = "5")]
97        iterations: usize,
98        /// Max tokens to generate per iteration
99        #[arg(long, default_value = "32")]
100        max_tokens: usize,
101        /// Test prompt
102        #[arg(long)]
103        prompt: Option<String>,
104        /// Use realizar for fast inference (vs aprender baseline)
105        #[arg(long)]
106        fast: bool,
107        /// Benchmark specific brick
108        #[arg(long)]
109        brick: Option<String>,
110        /// Comma-separated latency percentile points for JSON output
111        /// (CRUX-E-07). Default: `50,95,99`. Values must be in (0, 100].
112        #[arg(
113            long,
114            value_delimiter = ',',
115            default_value = "50,95,99",
116            value_parser = crate::commands::bench::parse_percentile
117        )]
118        percentiles: Vec<f64>,
119    },
120    /// Evaluate model perplexity (spec H13: PPL <= 20) or classification metrics
121    Eval {
122        /// Path to model file or checkpoint directory
123        #[arg(value_name = "FILE")]
124        file: PathBuf,
125        /// Dataset: wikitext-2, lambada, or custom
126        #[arg(long, default_value = "wikitext-2")]
127        dataset: String,
128        /// Custom text (when dataset=custom)
129        #[arg(long)]
130        text: Option<String>,
131        /// Maximum tokens to evaluate
132        #[arg(long, default_value = "512")]
133        max_tokens: usize,
134        /// Perplexity threshold for pass/fail
135        #[arg(long, default_value = "20.0",
136              value_parser = commands::threshold_arg::parse_tolerance_f32)]
137        threshold: f32,
138        /// Task type: omit for perplexity, "classify" for classification eval
139        #[arg(long)]
140        task: Option<String>,
141        /// Test data file (JSONL) for classification evaluation
142        #[arg(long, value_name = "FILE")]
143        data: Option<PathBuf>,
144        /// Model size hint: "0.5B", "tiny" (for classification eval)
145        #[arg(long)]
146        model_size: Option<String>,
147        /// Number of output classes (default: 5)
148        #[arg(long, default_value = "5")]
149        num_classes: usize,
150        /// Generate HuggingFace model card (README.md) in checkpoint dir
151        #[arg(long)]
152        generate_card: bool,
153        /// Device for inference: "cpu" (default) or "cuda" (GPU-accelerated, ALB-089).
154        /// Applies to --task humaneval/mbpp; perplexity evaluation is CPU-only.
155        #[arg(long, default_value = "cpu", value_parser = ["cpu", "cuda"])]
156        device: String,
157        /// Number of samples per problem for pass@k (ALB-088, default: 1)
158        #[arg(long, default_value = "1")]
159        samples: usize,
160        /// Sampling temperature (0.0 = greedy, 0.8 = standard for pass@k>1)
161        #[arg(long, default_value = "0.0")]
162        temperature: f32,
163    },
164    /// Deep profiling with Roofline analysis
165    Profile {
166        /// Path to model file
167        #[arg(value_name = "FILE")]
168        file: PathBuf,
169        /// Layer-by-layer granular analysis
170        #[arg(long)]
171        granular: bool,
172        /// Output format (human, json, flamegraph)
173        #[arg(long, default_value = "human")]
174        format: String,
175        /// Focus on specific operation
176        #[arg(long)]
177        focus: Option<String>,
178        /// Detect naive implementations
179        #[arg(long)]
180        detect_naive: bool,
181        /// Achieved-GFLOPS floor below which the run is reported as naive
182        #[arg(long, default_value = "10.0",
183              value_parser = commands::threshold_arg::parse_tolerance)]
184        threshold: f64,
185        /// [NOT IMPLEMENTED — accepted and ignored] Compare against HuggingFace baseline
186        #[arg(long)]
187        compare_hf: Option<String>,
188        /// [NOT IMPLEMENTED — accepted and ignored] Measure energy consumption (requires RAPL)
189        #[arg(long)]
190        energy: bool,
191        /// Compute performance grade (vs Ollama baseline)
192        #[arg(long)]
193        perf_grade: bool,
194        /// [NOT IMPLEMENTED — accepted and ignored] Show call graph
195        #[arg(long)]
196        callgraph: bool,
197        /// Exit non-zero if naive implementation detected (implies --detect-naive)
198        #[arg(long)]
199        fail_on_naive: bool,
200        /// Output file path for flamegraph SVG (GH-174, PMAT-182)
201        #[arg(long, short = 'o')]
202        output: Option<PathBuf>,
203
204        // PMAT-192: CI Assertion Mode (GH-180)
205        /// Enable CI mode with assertion checks (exits 1 on failure)
206        #[arg(long)]
207        ci: bool,
208        /// Minimum throughput in tok/s (CI assertion, exits 1 if below)
209        #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
210        assert_throughput: Option<f64>,
211        /// Maximum p99 latency in ms (CI assertion, exits 1 if above)
212        #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
213        assert_p99: Option<f64>,
214        /// Maximum p50 latency in ms (CI assertion, exits 1 if above)
215        #[arg(long, value_parser = commands::threshold_arg::parse_tolerance)]
216        assert_p50: Option<f64>,
217        /// Warmup passes before measurement (default: 3)
218        #[arg(long, default_value = "3")]
219        warmup: usize,
220        /// Measurement passes (default: 10)
221        #[arg(long, default_value = "10")]
222        measure: usize,
223        /// Tokens generated per measurement pass — GPU and --ollama paths only;
224        /// the CPU per-operation profiler measures one forward pass per pass
225        #[arg(long, default_value = "32")]
226        tokens: usize,
227        /// Compare against Ollama baseline (runs ollama for comparison)
228        #[arg(long)]
229        ollama: bool,
230        /// Disable GPU (force CPU-only profiling)
231        #[arg(long)]
232        no_gpu: bool,
233        /// Compare against another model format (F-PROFILE-011)
234        #[arg(long, value_name = "FILE")]
235        compare: Option<PathBuf>,
236    },
237    /// Falsifiable QA checklist for model releases
238    Qa {
239        /// Path to model file
240        #[arg(value_name = "FILE")]
241        file: PathBuf,
242        /// Minimum throughput threshold in tok/s
243        #[arg(long, value_name = "TPS",
244              value_parser = commands::threshold_arg::parse_tolerance)]
245        assert_tps: Option<f64>,
246        /// Minimum speedup vs Ollama
247        #[arg(long, value_name = "SPEEDUP",
248              value_parser = commands::threshold_arg::parse_tolerance)]
249        assert_speedup: Option<f64>,
250        /// Minimum GPU vs CPU speedup (F-PERF-042)
251        #[arg(long, value_name = "SPEEDUP",
252              value_parser = commands::threshold_arg::parse_tolerance)]
253        assert_gpu_speedup: Option<f64>,
254        /// Skip golden output test
255        #[arg(long)]
256        skip_golden: bool,
257        /// Skip throughput benchmark
258        #[arg(long)]
259        skip_throughput: bool,
260        /// Skip Ollama parity comparison
261        #[arg(long)]
262        skip_ollama: bool,
263        /// Skip GPU vs CPU speedup test (F-PERF-042)
264        #[arg(long)]
265        skip_gpu_speedup: bool,
266        /// Skip tensor contract validation (PMAT-235)
267        #[arg(long)]
268        skip_contract: bool,
269        /// Skip cross-format parity test (F-QUAL-032)
270        #[arg(long)]
271        skip_format_parity: bool,
272        /// Skip PTX parity validation (GH-219)
273        #[arg(long)]
274        skip_ptx_parity: bool,
275        /// SafeTensors model path for cross-format parity test (F-QUAL-032)
276        #[arg(long, value_name = "PATH")]
277        safetensors_path: Option<PathBuf>,
278        /// Number of benchmark iterations
279        #[arg(long, default_value = "10")]
280        iterations: usize,
281        /// Number of warmup iterations
282        #[arg(long, default_value = "3")]
283        warmup: usize,
284        /// Maximum tokens to generate
285        #[arg(long, default_value = "32")]
286        max_tokens: usize,
287        /// Output as JSON (for CI integration)
288        #[arg(long)]
289        json: bool,
290        /// Verbose output
291        #[arg(short, long)]
292        verbose: bool,
293        /// Minimum number of gates that must execute (fail if fewer)
294        #[arg(long, value_name = "N")]
295        min_executed: Option<usize>,
296        /// Previous QA report for regression detection
297        #[arg(long, value_name = "FILE")]
298        previous_report: Option<PathBuf>,
299        /// Maximum allowed performance regression ratio (default: 0.10 = 10%)
300        #[arg(long, value_name = "RATIO",
301              value_parser = commands::threshold_arg::parse_fraction)]
302        regression_threshold: Option<f64>,
303        /// Skip GPU state isolation test
304        #[arg(long)]
305        skip_gpu_state: bool,
306        /// Skip metadata plausibility validation (Bug 210, GH-222)
307        #[arg(long)]
308        skip_metadata: bool,
309        /// Skip GPU capability match gate (GH-280)
310        #[arg(long)]
311        skip_capability: bool,
312        /// Assert classifier head presence and shape (F-CLASS-004)
313        #[arg(long)]
314        assert_classifier_head: bool,
315    },
316    /// GPU/CPU parity check (PMAT-232: genchi genbutsu — see where GPU diverges)
317    Parity {
318        /// Path to GGUF model file
319        #[arg(value_name = "FILE")]
320        file: PathBuf,
321        /// Prompt text (default: "What is 2+2?")
322        #[arg(short, long, default_value = "What is 2+2?")]
323        prompt: String,
324        /// Assert parity (exit non-zero on divergence)
325        #[arg(long)]
326        assert: bool,
327    },
328    /// Model-to-PTX source mapping (Mieruka: make GPU kernel dispatch visible)
329    #[command(name = "ptx-map")]
330    PtxMap {
331        /// Path to GGUF model file
332        #[arg(value_name = "FILE")]
333        file: PathBuf,
334        /// Filter to specific kernel (e.g., --kernel Q4KGemv)
335        #[arg(long)]
336        kernel: Option<String>,
337        /// Reverse lookup: kernel name -> which layers/steps use it
338        #[arg(long)]
339        reverse: Option<String>,
340        /// Output as JSON
341        #[arg(long)]
342        json: bool,
343        /// Full PTX snippets and detailed analysis
344        #[arg(short, long)]
345        verbose: bool,
346        /// Show batched prefill kernel variants instead of decode
347        #[arg(long)]
348        prefill: bool,
349    },
350    /// PTX analysis and bug detection (register pressure, roofline)
351    ///
352    /// #2399 finding 1: on a build without the analyzer this line is the only
353    /// thing a user sees before running the command, so it has to say so.
354    #[cfg_attr(feature = "trueno-explain", command(name = "ptx"))]
355    #[cfg_attr(
356        not(feature = "trueno-explain"),
357        command(
358            name = "ptx",
359            about = "PTX analysis and bug detection [unavailable in this build: cargo install aprender --features ptx]"
360        )
361    )]
362    Ptx {
363        /// Path to a PTX source file
364        #[arg(value_name = "FILE")]
365        file: Option<PathBuf>,
366        /// Analyze a named kernel from trueno-gpu
367        #[arg(long, short)]
368        kernel: Option<String>,
369        /// Strict mode (no performance whitelist)
370        #[arg(long)]
371        strict: bool,
372        /// Show only bug analysis (skip register/memory/roofline)
373        #[arg(long)]
374        bugs: bool,
375        /// Output as JSON
376        #[arg(long)]
377        json: bool,
378        /// Verbose output (include PTX source listing)
379        #[arg(short, long)]
380        verbose: bool,
381    },
382    /// ML tuning: LoRA/QLoRA configuration, memory planning, and HPO (GH-176, SPEC-TUNE-2026-001)
383    #[cfg(feature = "training")]
384    Tune {
385        /// Path to model file (optional if using --model)
386        #[arg(value_name = "FILE")]
387        file: Option<PathBuf>,
388        /// Tuning method: auto, full, lora, qlora
389        #[arg(long, short = 'm', default_value = "auto")]
390        method: String,
391        /// LoRA rank (default: auto-selected)
392        #[arg(long, short = 'r')]
393        rank: Option<u32>,
394        /// Available VRAM in GB
395        #[arg(long, default_value = "16.0")]
396        vram: f64,
397        /// Only plan configuration, don't train
398        #[arg(long)]
399        plan: bool,
400        /// Model size for planning (e.g., "7B", "1.5B")
401        #[arg(long, value_name = "SIZE")]
402        model: Option<String>,
403        /// Freeze base model weights
404        #[arg(long)]
405        freeze_base: bool,
406        /// Training data file (JSONL format)
407        #[arg(long, value_name = "FILE")]
408        train_data: Option<PathBuf>,
409        /// Output as JSON (for CI integration)
410        #[arg(long)]
411        json: bool,
412        /// Task type for HPO: classify (SPEC-TUNE-2026-001)
413        #[arg(long)]
414        task: Option<String>,
415        /// Number of HPO trials (default: 10)
416        #[arg(long, default_value = "10")]
417        budget: usize,
418        /// HPO search strategy: tpe, grid, random
419        #[arg(long, default_value = "tpe")]
420        strategy: String,
421        /// HPO scheduler: asha, median, none
422        #[arg(long, default_value = "asha")]
423        scheduler: String,
424        /// Scout mode: 1 epoch per trial for fast exploration
425        #[arg(long)]
426        scout: bool,
427        /// Training data file for HPO (JSONL format)
428        #[arg(long, value_name = "FILE")]
429        data: Option<PathBuf>,
430        /// Number of output classes for classification
431        #[arg(long, default_value = "5")]
432        num_classes: usize,
433        /// Model size hint for HPO (e.g., "0.5B", "1.5B")
434        #[arg(long)]
435        model_size: Option<String>,
436        /// Warm-start from scout phase results directory
437        #[arg(long, value_name = "DIR")]
438        from_scout: Option<PathBuf>,
439        /// Maximum epochs per trial (full mode, default: 20)
440        #[arg(long, default_value = "20")]
441        max_epochs: usize,
442        /// Maximum wall-clock time (e.g., "8h", "30m")
443        #[arg(long)]
444        time_limit: Option<String>,
445    },
446    /// Attach live TUI to a running training session
447    #[cfg(feature = "training")]
448    Monitor {
449        /// Experiment output directory (same as finetune -o)
450        #[arg(value_name = "DIR")]
451        dir: Option<PathBuf>,
452        /// Refresh interval in milliseconds
453        #[arg(long, default_value = "500")]
454        refresh_ms: u64,
455        /// Compact display mode
456        #[arg(long)]
457        compact: bool,
458        /// Output JSON lines instead of TUI (for LLM agents and CI)
459        #[arg(long)]
460        json: bool,
461        /// Output format: tui (default), json, text
462        #[arg(long, default_value = "tui")]
463        format: String,
464    },
465    /// List, show, and compare training experiment runs
466    #[cfg(feature = "training")]
467    Runs {
468        #[command(subcommand)]
469        command: RunsCommands,
470    },
471    /// Interactive experiment browser (TUI with loss curves)
472    #[cfg(feature = "training")]
473    Experiment {
474        #[command(subcommand)]
475        command: ExperimentCommands,
476    },
477    /// ComputeBrick pipeline monitor (cbtop)
478    Cbtop {
479        /// Model name (e.g., qwen2.5-coder-1.5b)
480        #[arg(long)]
481        model: Option<String>,
482        /// Attach to running realizar process
483        #[arg(long)]
484        attach: Option<String>,
485        /// Path to GGUF model file for real profiling
486        #[arg(long, value_name = "MODEL")]
487        model_path: Option<PathBuf>,
488        /// Run in headless mode (no TUI, for CI/automation)
489        #[arg(long)]
490        headless: bool,
491        /// Output JSON format (requires --headless)
492        #[arg(long, requires = "headless")]
493        json: bool,
494        /// Output file path (requires --headless)
495        #[arg(long, value_name = "FILE", requires = "headless")]
496        output: Option<PathBuf>,
497        /// CI mode: exit non-zero if thresholds are not met or the report status is FAIL
498        #[arg(long)]
499        ci: bool,
500        /// Minimum throughput threshold in tok/s (for --ci)
501        #[arg(long, value_name = "TOK_S",
502              value_parser = commands::threshold_arg::parse_tolerance)]
503        throughput: Option<f64>,
504        /// Minimum brick score threshold 0-100 (for --ci)
505        #[arg(long, value_name = "SCORE")]
506        brick_score: Option<u32>,
507        /// Number of warmup iterations before measurement
508        #[arg(long, default_value = "10")]
509        warmup: usize,
510        /// Number of measurement iterations (must be >= 1)
511        #[arg(long, default_value = "100", value_parser = parse_cbtop_iterations)]
512        iterations: usize,
513        /// PAR-100: Enable speculative decoding benchmark
514        #[arg(long)]
515        speculative: bool,
516        /// PAR-100: Number of tokens to draft speculatively (default: 4)
517        #[arg(long, default_value = "4")]
518        speculation_k: usize,
519        /// PAR-099: Path to draft model for speculative decoding
520        #[arg(long, value_name = "DRAFT_MODEL")]
521        draft_model: Option<PathBuf>,
522        /// PAR-102: Number of concurrent requests
523        #[arg(long, default_value = "1")]
524        concurrent: usize,
525        /// Use simulated data (for CI testing only)
526        #[arg(long)]
527        simulated: bool,
528    },
529    /// Test harness for web, LLM, media and replay — powered by probador.
530    ///
531    /// Named for what it tests, not for the act of testing. `probar` is Spanish
532    /// for "to try"; it named the VERB, so `apr probar --help` told a reader
533    /// nothing about the subject. This follows the precedent already set by
534    /// `apr data` ("Data quality pipeline ... powered by alimentar"): a plain
535    /// noun for the user-facing command, the Spanish name kept for the engine
536    /// and credited in the description.
537    ///
538    /// The harness covers four distinct things, and the subcommands group by
539    /// what is UNDER TEST rather than by verb:
540    ///
541    ///   web     the WASM/browser build and its runtime behaviour
542    ///           (serve, build, watch, comply, stress)
543    ///   llm     inference correctness, throughput and cost against an endpoint
544    ///           (test, load, bench, sweep, score, experiment, data-audit)
545    ///   media   rendered output against ground truth
546    ///           (av-sync, audio, video, animation)
547    ///   replay  the runner itself — recording, state machines, reporting
548    ///           (record, playbook, coverage, report)
549    ///
550    /// Only `tensor` is routed today (PMAT-481 visual regression); the rest
551    /// land as they are delegated to the probador library. Renaming now costs
552    /// one path — after those land it is a breaking change across the whole
553    /// testing surface.
554    ///
555    /// `apr probar` stays as a hidden alias so existing scripts keep working.
556    #[command(alias = "probar")]
557    Test {
558        #[command(subcommand)]
559        command: TestSubcommand,
560    },
561    /// Compare APR model against HuggingFace source
562    #[command(name = "compare-hf")]
563    CompareHf {
564        /// Path to .apr model file
565        #[arg(value_name = "FILE")]
566        file: PathBuf,
567        /// HuggingFace repo ID (e.g., openai/whisper-tiny)
568        #[arg(long)]
569        hf: String,
570        /// Filter tensors by name pattern
571        #[arg(long)]
572        tensor: Option<String>,
573        /// Comparison threshold (default: 1e-5)
574        #[arg(long, default_value = "1e-5",
575              value_parser = commands::threshold_arg::parse_tolerance)]
576        threshold: f64,
577        /// Output as JSON
578        #[arg(long)]
579        json: bool,
580    },
581    /// CRUX-K-11: parse Ollama-style Modelfile DSL into apr config.
582    Modelfile {
583        #[command(subcommand)]
584        command: ModelfileSubcommand,
585    },
586    /// Format-aware binary forensics (10X better than xxd)
587    Hex {
588        /// Path to model file (APR, GGUF, or SafeTensors)
589        #[arg(value_name = "FILE")]
590        file: PathBuf,
591        /// Filter tensors by name pattern
592        #[arg(long)]
593        tensor: Option<String>,
594        /// Limit bytes/values to display
595        #[arg(long, default_value = "64")]
596        limit: usize,
597        /// Show tensor statistics
598        #[arg(long)]
599        stats: bool,
600        /// List tensor names only
601        #[arg(long)]
602        list: bool,
603        /// Output as JSON
604        #[arg(long)]
605        json: bool,
606        /// Annotated file header (magic, version, tensor count, metadata)
607        #[arg(long)]
608        header: bool,
609        /// Q4K/Q6K/Q8_0 super-block structure with field annotations
610        #[arg(long)]
611        blocks: bool,
612        /// Value histogram + entropy + kurtosis analysis
613        #[arg(long)]
614        distribution: bool,
615        /// Layout contract verification overlay per tensor
616        #[arg(long)]
617        contract: bool,
618        /// Per-region byte entropy analysis
619        #[arg(long)]
620        entropy: bool,
621        /// Raw bytes (like xxd but format-aware, with ASCII column)
622        #[arg(long)]
623        raw: bool,
624        /// Start at byte offset (supports 0x prefix for hex)
625        #[arg(long, default_value = "0")]
626        offset: String,
627        /// Bytes per row for raw output (default: 16)
628        #[arg(long, default_value = "16")]
629        width: usize,
630        /// Slice range for partial tensor reads (e.g., 0:3 for first 3 elements)
631        #[arg(long)]
632        slice: Option<String>,
633    },
634    /// Model architecture tree view
635    Tree {
636        /// Path to .apr model file
637        #[arg(value_name = "FILE")]
638        file: PathBuf,
639        /// Filter by component pattern
640        #[arg(long)]
641        filter: Option<String>,
642        /// Output format: ascii, dot, mermaid, json
643        ///
644        /// #2394 finding 15: this was a `String` that the dispatcher parsed
645        /// with `.unwrap_or(TreeFormat::Ascii)`, so `--format bogusvalue`
646        /// silently rendered ascii and exited 0 — a typo'd `--format josn` in
647        /// a pipeline produced a tree instead of JSON, with no warning. Parsing
648        /// at the CLI boundary makes the unparseable value unrepresentable
649        /// downstream: clap rejects it before any command runs.
650        #[arg(long, default_value = "ascii")]
651        format: crate::commands::tree::TreeFormat,
652        /// Show tensor sizes
653        #[arg(long)]
654        sizes: bool,
655        /// Maximum tree depth
656        #[arg(long)]
657        depth: Option<usize>,
658    },
659    /// Data flow visualization
660    Flow {
661        /// Path to .apr model file
662        #[arg(value_name = "FILE")]
663        file: PathBuf,
664        /// Filter by layer pattern
665        #[arg(long)]
666        layer: Option<String>,
667        /// Component to visualize: full, encoder, decoder, etc.
668        #[arg(long, default_value = "full")]
669        component: String,
670        /// Verbose output with statistics
671        #[arg(short, long)]
672        verbose: bool,
673        /// Output as JSON
674        #[arg(long)]
675        json: bool,
676    },
677    /// Cross-subcommand smoke test (does every tool handle this model?)
678    Qualify {
679        /// Path to model file (APR, GGUF, or SafeTensors)
680        #[arg(value_name = "FILE")]
681        file: PathBuf,
682        /// Testing tier: smoke (Phase 1), standard (+contracts), full (+playbook)
683        #[arg(long, default_value = "smoke")]
684        tier: String,
685        /// Timeout per gate in seconds
686        #[arg(long, default_value = "120")]
687        timeout: u64,
688        /// Output as JSON
689        #[arg(long)]
690        json: bool,
691        /// Show subcommand output (disable stdout suppression)
692        #[arg(short, long)]
693        verbose: bool,
694        /// Skip specific gates (comma-separated)
695        #[arg(long, value_delimiter = ',')]
696        skip: Option<Vec<String>>,
697    },
698    /// Training pipeline (plan/apply) — forjar-style pre-flight validation
699    #[cfg(feature = "training")]
700    Train {
701        #[command(subcommand)]
702        command: TrainCommands,
703    },
704    /// Pretraining loop driver (SHIP-TWO-001 MODEL-2).
705    ///
706    /// Wires the pretraining loop shape defined by
707    /// `contracts/training-loop-pretrain-v1.yaml`. Executes a synthetic
708    /// decreasing-loss drive by default so GATE-TRAIN-005 / -007 / -008
709    /// divergence-and-NaN guards can be exercised without an actual
710    /// 370M compute run. Real corpus wiring is a follow-up ticket.
711    #[cfg(feature = "training")]
712    Pretrain {
713        /// Dataset path (tokenized shard index or raw corpus).
714        #[arg(long, value_name = "PATH")]
715        dataset: PathBuf,
716        /// Tokenizer directory (vocab.json + merges.txt).
717        #[arg(long, value_name = "DIR")]
718        tokenizer: PathBuf,
719        /// Run output directory — checkpoints + metadata go to `{run_dir}/ckpt/`.
720        #[arg(long, value_name = "DIR")]
721        run_dir: PathBuf,
722        /// Training regime — finetune (MODEL-1) or from-scratch (MODEL-2 cold start).
723        /// Per contract training-loop-pretrain-v1 §hyperparameter_defaults,
724        /// this atomically flips (regime, lr_max, warmup_steps, target_val_loss)
725        /// unless explicit --lr / --warmup-steps / --target-val-loss override.
726        #[arg(long, value_enum, default_value = "finetune")]
727        mode: PretrainMode,
728        /// Peak learning rate after warmup. Omit to inherit mode default
729        /// (finetune: 5e-5, from-scratch: 3e-4).
730        #[arg(long)]
731        lr: Option<f32>,
732        /// Warmup + cosine decay total steps.
733        #[arg(long, default_value = "1000")]
734        num_steps: usize,
735        /// Number of warmup steps. Omit to inherit mode default
736        /// (finetune: 100, from-scratch: 1000).
737        #[arg(long)]
738        warmup_steps: Option<usize>,
739        /// Micro-batch size.
740        #[arg(long, default_value = "16")]
741        batch_size: usize,
742        /// Sequence length per example.
743        #[arg(long, default_value = "1024")]
744        seq_length: usize,
745        /// Steps per epoch — controls per-epoch artifact cadence.
746        #[arg(long, default_value = "100")]
747        steps_per_epoch: usize,
748        /// GATE-TRAIN-006 fixed RNG seed.
749        #[arg(long, default_value = "42")]
750        seed: u64,
751        /// Target val_loss. Omit to inherit mode default
752        /// (finetune: 2.2, from-scratch: 3.0).
753        #[arg(long, value_parser = commands::threshold_arg::parse_tolerance_f32)]
754        target_val_loss: Option<f32>,
755        /// Vocabulary size (required for `--mode from-scratch` INV-TRAIN-005
756        /// regime-dependent cap: 2·ln(vocab_size)). MODEL-2 uses 50257.
757        #[arg(long, default_value = "50257")]
758        vocab_size: u32,
759        /// Synthetic-drive only — do not attempt real compute, exercise loop gates only.
760        /// INV-TRAIN-010: absent = real compute (drive_real), present = synthetic (drive_synthetic).
761        #[arg(long, action = clap::ArgAction::SetTrue)]
762        synthetic: bool,
763        /// Training backend. Grammar (contract gpu-training-backend-v1
764        /// INV-GPUTRAIN-001): `^(cpu|cuda(:[0-9]|:1[0-5])?|auto)$`.
765        /// Default `auto` uses CUDA if available, else CPU (the only
766        /// spelling that may fall back silently — all other values
767        /// hard-fail on missing runtime per GATE-GPUTRAIN-002).
768        #[arg(long, default_value = "auto")]
769        device: String,
770        /// Initial weights from a pretrained APR file
771        /// (contract `apr-pretrain-from-init-v1`). Per spec §49's
772        /// MODEL-2 pretrained-init pivot: when present, load weights
773        /// from `<PATH>` instead of random-init. Composes with
774        /// `--mode finetune` (canonical) or `--mode from-scratch`
775        /// (allowed but non-canonical — emits a warning). Missing,
776        /// corrupted, or arch-mismatched APR files exit non-zero
777        /// before step 1 (no silent random-init fallback).
778        #[arg(long, value_name = "PATH")]
779        init: Option<PathBuf>,
780        /// SPEC §83 P0-J: bypass the Chinchilla compute-optimal hard
781        /// gate (`chinchilla-gate-v1`). Default is fail-fast when
782        /// D/N < 10× (severely under-provisioned per Hoffmann et al.
783        /// 2022). Pass this flag to acknowledge the under-provisioning
784        /// and proceed anyway (e.g. for ablation studies, resumed
785        /// runs, or smoke tests).
786        #[arg(long, action = clap::ArgAction::SetTrue)]
787        force_under_provisioned: bool,
788        /// SPEC §84 P2-F: shared held-out validation shard.
789        ///
790        /// When provided, the val-loss eval reads `HELD_OUT_BATCHES`
791        /// batches from this separate `.bin`-shards directory instead
792        /// of stealing the first 16 batches of `--dataset`. This makes
793        /// `val_loss` comparable across runs whose `--dataset`
794        /// composition changes (P2-C's audit-falsified result was
795        /// confounded by val sets being drawn from different corpus
796        /// distributions — qwen-v2 = codeparrot only, qwen-v3 =
797        /// codeparrot + the-stack-dedup).
798        ///
799        /// Path semantics: directory of `.bin` shards (same format as
800        /// `--dataset`). Operator tokenizes the held-out corpus
801        /// independently via `apr tokenize encode-corpus --max-docs N`
802        /// to a separate output dir, then passes that dir here. The
803        /// shard contract is `contracts/dataset-thestack-python-v1.yaml`.
804        ///
805        /// When omitted, falls back to the historical "first 16
806        /// batches of --dataset" behaviour for backwards compatibility.
807        #[arg(long, value_name = "DIR")]
808        val_shard: Option<PathBuf>,
809    },
810    /// Tokenizer training pipeline (plan/apply) — BPE vocabulary learning
811    Tokenize {
812        #[command(subcommand)]
813        command: TokenizeCommands,
814    },
815    /// Data quality pipeline (audit, split, balance) — powered by alimentar
816    Data {
817        #[command(subcommand)]
818        command: DataCommands,
819    },
820    /// Pipeline orchestration (plan/apply/status) — wraps forjar DAG engine
821    Pipeline {
822        #[command(subcommand)]
823        command: PipelineCommands,
824    },
825    /// Automated Five Whys diagnosis on a training checkpoint
826    Diagnose {
827        /// Path to checkpoint directory
828        #[arg(value_name = "CHECKPOINT_DIR")]
829        checkpoint_dir: PathBuf,
830        /// Test data file (JSONL) for evaluation
831        #[arg(long, value_name = "FILE")]
832        data: Option<PathBuf>,
833        /// Model size hint: "0.5B", "tiny"
834        #[arg(long)]
835        model_size: Option<String>,
836        /// Number of output classes (default: 5)
837        #[arg(long, default_value = "5")]
838        num_classes: usize,
839    },
840    /// Lint an Ollama /api/chat response for schema + NDJSON invariants (CRUX-C-04)
841    OllamaChatLint {
842        /// Path to captured /api/chat response (JSON object, or NDJSON if --stream)
843        #[arg(long, value_name = "FILE")]
844        response_file: PathBuf,
845        /// Treat input as NDJSON stream (one frame per line)
846        #[arg(long)]
847        stream: bool,
848    },
849    /// Lint an Ollama /api/chat function-calling response (CRUX-I-04)
850    OllamaToolsLint {
851        /// Path to captured /api/chat response (JSON object, or NDJSON if --stream)
852        #[arg(long, value_name = "FILE")]
853        response_file: PathBuf,
854        /// Captured request JSON, required unless --stream — supplies the
855        /// tool-name allowlist (every called tool name must appear in
856        /// request.tools[*].function.name)
857        #[arg(long, value_name = "FILE")]
858        request_file: Option<PathBuf>,
859        /// Treat input as NDJSON stream (one frame per line)
860        #[arg(long)]
861        stream: bool,
862    },
863    /// Lint a captured DRY-sampling observation (CRUX-C-23)
864    DrySamplingLint {
865        /// Path to observation JSON
866        #[arg(long, value_name = "FILE")]
867        observation_file: PathBuf,
868    },
869    /// Lint a captured AWQ quality/compression/flags observation (CRUX-B-08)
870    AwqLint {
871        /// Path to captured AWQ observation JSON
872        #[arg(long, value_name = "FILE")]
873        observation_file: PathBuf,
874    },
875    /// Lint a captured FP8 (E4M3) round-trip + SM-capability observation (CRUX-B-11)
876    Fp8Lint {
877        /// Path to captured observation JSON (frobenius, capability blocks)
878        #[arg(long, value_name = "FILE")]
879        observation_file: PathBuf,
880    },
881    /// Lint a captured NF4 codebook/roundtrip/storage/parity observation (CRUX-B-10)
882    Nf4Lint {
883        /// Path to captured NF4 observation JSON
884        #[arg(long, value_name = "FILE")]
885        observation_file: PathBuf,
886    },
887    /// Lint a captured GPTQ compression/cosine/flags observation (CRUX-B-09)
888    GptqLint {
889        /// Path to captured GPTQ observation JSON
890        #[arg(long, value_name = "FILE")]
891        observation_file: PathBuf,
892    },
893    /// Lint a captured CUDA OOM postmortem report (CRUX-F-13)
894    OomLint {
895        /// Path to captured OOM postmortem JSON (e.g. /tmp/apr-oom-<ts>.json)
896        #[arg(long, value_name = "FILE")]
897        report_file: PathBuf,
898        /// Optional captured stderr log to verify the OOM_REPORT breadcrumb
899        #[arg(long, value_name = "FILE")]
900        stderr_file: Option<PathBuf>,
901    },
902    /// Lint a captured NCCL failure-diagnostics JSON from stderr (CRUX-F-15)
903    NcclDiagLint {
904        /// Path to captured stderr JSON diagnostic
905        #[arg(long, value_name = "FILE")]
906        diag_file: PathBuf,
907        /// Optional observed exit code (gate: >= 128 = NCCL class)
908        #[arg(long, value_name = "I32")]
909        exit_code: Option<i32>,
910        /// Require the `suggest` field to cite an nvidia.com / NVIDIA/nccl URL
911        #[arg(long)]
912        require_doc_link: bool,
913    },
914    /// Lint an externally captured ReAct loop trace JSON (CRUX-I-06 — no apr producer yet)
915    ReactTraceLint {
916        /// Path to captured trace JSON
917        #[arg(long, value_name = "FILE")]
918        trace_file: PathBuf,
919        /// Optional max_iterations budget the trace was produced under
920        #[arg(long, value_name = "N")]
921        max_iterations: Option<i64>,
922        /// Require the scratchpad to parse cleanly as Thought/Action/Observation blocks
923        #[arg(long)]
924        require_grammar: bool,
925    },
926    /// Lint a captured `$APR_TRACE_DIR` hang stack-dump directory (CRUX-F-14)
927    HangTraceLint {
928        /// Path to the captured trace directory
929        #[arg(long, value_name = "DIR")]
930        trace_dir: PathBuf,
931        /// Inspection mode: `timeout` (expects per-rank dumps) or `success` (expects empty dir)
932        #[arg(long, value_name = "MODE", default_value = "timeout")]
933        mode: String,
934        /// Expected world_size when mode=timeout (number of rank{N}.py.txt files)
935        #[arg(long, value_name = "N", default_value_t = 2)]
936        world_size: usize,
937        /// Actual exit code from the run under inspection (for exit-code gate)
938        #[arg(long, value_name = "I32")]
939        exit_code: Option<i32>,
940        /// Expected exit code (typically 124 for timeout, 1 for other error, 0 for success)
941        #[arg(long, value_name = "I32")]
942        expected_exit_code: Option<i32>,
943    },
944    /// Lint two externally captured DDP metrics JSONs, N=1 and N=k (CRUX-D-11 — no apr producer yet)
945    DdpMetricsLint {
946        /// Path to N=1 metrics JSON
947        #[arg(long, value_name = "FILE")]
948        metrics_1gpu_file: PathBuf,
949        /// Path to N=world_size metrics JSON
950        #[arg(long, value_name = "FILE")]
951        metrics_ngpu_file: PathBuf,
952        /// World size used for --metrics-ngpu-file run (>= 2)
953        #[arg(long, value_name = "N")]
954        world_size: i64,
955        /// Scaling-efficiency floor (default 0.85, PyTorch DDP convention)
956        #[arg(long, value_name = "F", default_value_t = 0.85,
957              value_parser = commands::threshold_arg::parse_fraction)]
958        scaling_floor: f64,
959        /// Loss-parity relative tolerance (default 0.01)
960        #[arg(long, value_name = "F", default_value_t = 0.01,
961              value_parser = commands::threshold_arg::parse_tolerance)]
962        loss_tolerance: f64,
963    },
964    /// Dataset inspection tools (CRUX-H-13)
965    Dataset {
966        #[command(subcommand)]
967        command: DatasetCommands,
968    },
969    /// Kernel-level parity measurements (CRUX-L-02)
970    Kernel {
971        #[command(subcommand)]
972        command: KernelCommands,
973    },
974    /// Lint an audio-inspect JSON body, e.g. from
975    /// `apr dataset audio-inspect clip.wav --format json -o audio.json` (CRUX-H-13)
976    AudioInspectLint {
977        /// Path to the JSON body written by `apr dataset audio-inspect --format json`
978        #[arg(long, value_name = "FILE")]
979        json_file: PathBuf,
980        /// Optional expected sample_rate (typically the `--resample-to` arg)
981        #[arg(long, value_name = "U32")]
982        expected_sample_rate: Option<u32>,
983        /// Optional expected channel count (1 = mono after --mono)
984        #[arg(long, value_name = "U32")]
985        expected_channels: Option<u32>,
986    },
987    /// Lint attention parity + provenance JSON, e.g. from
988    /// `apr kernel parity --impl tiled --ref naive --json -o parity.json` (CRUX-L-02)
989    AttnParityLint {
990        /// Parity JSON body (`max_abs_diff`, `cosine_sim`), as written by
991        /// `apr kernel parity --json`
992        #[arg(long, value_name = "FILE")]
993        parity_file: Option<PathBuf>,
994        /// Provenance JSON body (`attn_impl`, `kernel_source`, `fallback`).
995        /// `apr kernel parity --json` writes both gates' fields into one body,
996        /// so the same file may be passed here and to --parity-file
997        #[arg(long, value_name = "FILE")]
998        provenance_file: Option<PathBuf>,
999        /// head_dim refusal JSON, as written by
1000        /// `apr kernel parity --impl flash2 --head-dim 96 --json` (which exits non-zero)
1001        #[arg(long, value_name = "FILE")]
1002        head_dim_error_file: Option<PathBuf>,
1003        /// Max absolute diff tolerance (default 5e-3, FlashAttention-2 bound)
1004        #[arg(long, value_name = "F", default_value_t = 5e-3,
1005              value_parser = commands::threshold_arg::parse_tolerance)]
1006        tol_abs: f64,
1007        /// Min cosine similarity floor (default 0.9999)
1008        #[arg(long, value_name = "F", default_value_t = 0.9999,
1009              value_parser = commands::threshold_arg::parse_cosine)]
1010        tol_cos: f64,
1011    },
1012    /// Lint an externally captured attention dump (CRUX-F-17 — no apr producer yet)
1013    AttnVizLint {
1014        /// Path to attention dump in JSON form (4-D [layers][heads][rows][cols] floats)
1015        #[arg(long, value_name = "FILE")]
1016        attn_file: Option<PathBuf>,
1017        /// Path to HTML heatmap output
1018        #[arg(long, value_name = "FILE")]
1019        html_file: Option<PathBuf>,
1020        /// Minimum <svg|<canvas open-tag count expected in HTML (|layers|*|heads|)
1021        #[arg(long, value_name = "N", default_value_t = 1)]
1022        expected_heatmaps: usize,
1023        /// Row-softmax normalization tolerance (default 1e-5)
1024        #[arg(long, value_name = "F64", default_value_t = 1e-5,
1025              value_parser = commands::threshold_arg::parse_tolerance)]
1026        tolerance: f64,
1027        /// Causal-mask zero epsilon (default 1e-9)
1028        #[arg(long, value_name = "F64", default_value_t = 1e-9,
1029              value_parser = commands::threshold_arg::parse_tolerance)]
1030        epsilon: f64,
1031    },
1032    /// Lint an externally captured check-finite error and/or coverage JSON (CRUX-F-11 — no apr producer yet)
1033    CheckFiniteLint {
1034        /// Externally captured check-finite stderr JSON from a poisoned model
1035        #[arg(long, value_name = "FILE")]
1036        error_file: Option<PathBuf>,
1037        /// Externally captured check-finite layer-coverage JSON
1038        #[arg(long, value_name = "FILE")]
1039        list_file: Option<PathBuf>,
1040        /// Minimum layer-coverage count when `--list-file` is supplied (default 100)
1041        #[arg(long, value_name = "N", default_value_t = 100)]
1042        min_layers: usize,
1043    },
1044    /// Lint an embedding-projection CSV, e.g. from
1045    /// `apr debug embed-viz --model model.apr --seed 42 -o emb.csv` (CRUX-F-18)
1046    EmbedVizLint {
1047        /// Path to the `token_id,token_str,x,y` CSV written by `apr debug embed-viz`
1048        #[arg(long, value_name = "FILE")]
1049        csv_file: PathBuf,
1050        /// Expected row count == vocab_size (optional)
1051        #[arg(long, value_name = "N")]
1052        expected_vocab_size: Option<usize>,
1053        /// Second CSV from a rerun at the same --seed, for the determinism gate (optional)
1054        #[arg(long, value_name = "FILE")]
1055        csv_file_b: Option<PathBuf>,
1056    },
1057    /// Lint an externally captured token-selection JSONL trace (CRUX-F-19 — no apr producer yet)
1058    ExplainTokenLint {
1059        /// Path to captured JSONL body (one sampled-token record per line)
1060        #[arg(long, value_name = "FILE")]
1061        jsonl_file: PathBuf,
1062        /// Tolerance for `Σ post_prob ≈ 1.0` (default 1e-5)
1063        #[arg(long, value_name = "F64", default_value_t = 1e-5,
1064              value_parser = commands::threshold_arg::parse_tolerance)]
1065        tolerance: f64,
1066        /// Assert greedy decoding: sampled_id must equal argmax(pre_prob)
1067        #[arg(long)]
1068        require_greedy: bool,
1069    },
1070    /// Lint a captured GPU memory Chrome Trace Event Format JSON (CRUX-F-07)
1071    GpuMemtraceLint {
1072        /// Path to an externally captured GPU-memory Chrome Trace JSON (no apr producer yet)
1073        #[arg(long, value_name = "FILE")]
1074        trace_file: PathBuf,
1075    },
1076    /// Lint a captured KV-cache utilization timeline (CRUX-F-06)
1077    KvTimelineLint {
1078        /// Path to an externally captured KV-cache timeline JSON body (no apr producer yet)
1079        #[arg(long, value_name = "FILE")]
1080        timeline_file: PathBuf,
1081        /// Preemption threshold (default 0.95, vLLM canonical)
1082        #[arg(long, value_name = "FRACTION", default_value_t = 0.95,
1083              value_parser = commands::threshold_arg::parse_fraction)]
1084        preempt_threshold: f64,
1085    },
1086    /// Lint a captured OTLP/JSON ExportTraceServiceRequest body (CRUX-K-08).
1087    ///
1088    /// At least one gate flag is required: every check is opt-in, so a bare
1089    /// invocation would check nothing and exit 0 for any parseable JSON.
1090    OtlpLint {
1091        /// Path to captured OTLP/JSON export body
1092        #[arg(long, value_name = "FILE")]
1093        otlp_file: PathBuf,
1094        /// Require at least one `apr.inference` span to be present
1095        #[arg(long)]
1096        require_apr_span: bool,
1097        /// Require gen_ai.* and apr.tokens.* attribute keys on some span
1098        #[arg(long)]
1099        require_genai_attrs: bool,
1100        /// Verify W3C trace-context propagation: expect this 32-hex traceId
1101        #[arg(long, value_name = "HEX32")]
1102        expect_trace_id: Option<String>,
1103    },
1104    /// Lint a captured Prometheus /metrics response (CRUX-K-07)
1105    PrometheusLint {
1106        /// Path to captured /metrics response body (text/plain; version=0.0.4)
1107        #[arg(long, value_name = "FILE")]
1108        metrics_file: PathBuf,
1109        /// Optional captured Content-Type header to verify against version=0.0.4
1110        #[arg(long, value_name = "HEADER")]
1111        content_type: Option<String>,
1112        /// Require the K-07 metric set (apr_num_requests_running, ...) to be present
1113        #[arg(long)]
1114        require_k07_metrics: bool,
1115    },
1116    /// Lint a captured OpenAI tool-use response (CRUX-C-11)
1117    ToolUseLint {
1118        /// Path to captured OpenAI tool-use response JSON
1119        #[arg(long, value_name = "FILE")]
1120        observation_file: PathBuf,
1121    },
1122    /// Lint a GBNF grammar-constrained observation (CRUX-C-10)
1123    GbnfLint {
1124        /// Path to captured GBNF observation JSON
1125        #[arg(long, value_name = "FILE")]
1126        observation_file: PathBuf,
1127    },
1128    /// Lint a typical-p sampling observation (CRUX-C-22)
1129    TypicalPLint {
1130        /// Path to captured typical-p observation JSON, with any of the
1131        /// sections range/identity/mass/sort/renorm
1132        #[arg(long, value_name = "FILE")]
1133        observation_file: PathBuf,
1134    },
1135    /// Gradient-norm telemetry analysis (CRUX-F-09)
1136    GradNorm {
1137        /// Path to JSON file of per-step grad-norm records
1138        #[arg(long, value_name = "FILE")]
1139        history_file: PathBuf,
1140        /// Maximum allowed clipped grad-norm (for cap-violation check)
1141        #[arg(long, value_name = "M",
1142              value_parser = commands::threshold_arg::parse_tolerance)]
1143        max_grad_norm: Option<f64>,
1144        /// Rolling-median window size for spike detection (in steps)
1145        #[arg(long, default_value = "16")]
1146        spike_window: usize,
1147        /// Multiplier threshold for spike detection
1148        #[arg(long, default_value = "10.0",
1149              value_parser = commands::threshold_arg::parse_tolerance)]
1150        spike_multiplier: f64,
1151    },
1152    /// Lint a captured registry byte-quota observation (CRUX-A-22)
1153    RegistryQuotaLint {
1154        /// Path to captured quota/atomic/ceiling observation JSON
1155        #[arg(long, value_name = "FILE")]
1156        observation_file: PathBuf,
1157    },
1158    /// Lint a captured imatrix calibration observation (CRUX-B-07)
1159    ImatrixLint {
1160        /// Path to captured imatrix observation JSON
1161        #[arg(long, value_name = "FILE")]
1162        observation_file: PathBuf,
1163    },
1164    /// Lint a captured /v1/embeddings observation (CRUX-C-13)
1165    EmbeddingsLint {
1166        /// Path to captured /v1/embeddings observation JSON, with any of the
1167        /// sections shape/determinism/usage/flag
1168        #[arg(long, value_name = "FILE")]
1169        observation_file: PathBuf,
1170    },
1171    /// Lint a captured Hub+local unified-search merge observation (CRUX-A-23)
1172    UnifiedSearchLint {
1173        /// Path to captured unified-search observation JSON
1174        #[arg(long, value_name = "FILE")]
1175        observation_file: PathBuf,
1176    },
1177    /// Lint a captured `apr rm` / externally captured gc blob-GC observation (CRUX-A-25)
1178    RmGcLint {
1179        /// Path to captured rm/gc observation JSON
1180        #[arg(long, value_name = "FILE")]
1181        observation_file: PathBuf,
1182    },
1183    /// Lint a captured APR_MODELS shared-cache observation (CRUX-A-21)
1184    SharedCacheLint {
1185        /// Path to captured dedup/permission observation JSON
1186        #[arg(long, value_name = "FILE")]
1187        observation_file: PathBuf,
1188    },
1189    /// Perplexity classifier (CRUX-E-02)
1190    Ppl {
1191        /// JSON file containing an array of per-token natural-log
1192        /// probabilities (e.g. `[-1.2, -0.5, -2.1, ...]`). Required.
1193        #[arg(long, value_name = "FILE")]
1194        log_probs_file: PathBuf,
1195    },
1196    /// Validate dequant→requant metadata preservation (CRUX-B-19)
1197    QuantPreservationLint {
1198        /// Reference GGUF (pre-roundtrip)
1199        #[arg(long, value_name = "REF.gguf")]
1200        reference: PathBuf,
1201        /// Requantized GGUF (post-roundtrip)
1202        #[arg(long, value_name = "REQ.gguf")]
1203        requant: PathBuf,
1204    },
1205    /// Split a safetensors file into shards + weight-map index (CRUX-B-05)
1206    Shard {
1207        /// Single-file safetensors model to split
1208        #[arg(value_name = "FILE")]
1209        file: PathBuf,
1210        /// Maximum size of each shard (e.g. 5GB, 500MB, 1.5GiB)
1211        #[arg(long, value_name = "SIZE", default_value = "5GB")]
1212        max_shard_size: String,
1213        /// Output directory for shards + model.safetensors.index.json
1214        #[arg(short, long, value_name = "DIR")]
1215        output: PathBuf,
1216        /// #2392: Overwrite an existing shard set in the output directory
1217        #[arg(short, long)]
1218        force: bool,
1219    },
1220    /// Reconstruct a single safetensors file from a sharded directory (CRUX-B-05)
1221    Unshard {
1222        /// Sharded directory containing model.safetensors.index.json
1223        #[arg(value_name = "DIR")]
1224        input: PathBuf,
1225        /// Output single-file safetensors path
1226        #[arg(short, long, value_name = "FILE")]
1227        output: PathBuf,
1228        /// #2392: Overwrite an existing output file (refused without it)
1229        #[arg(short, long)]
1230        force: bool,
1231    },
1232    /// Publishing, conversion, and analysis tools
1233    #[command(flatten)]
1234    Tools(ToolCommands),
1235    /// Score a query/passage pair (or rank multiple passages) with a BERT
1236    /// cross-encoder loaded from an APR v2 file (GH-326 Phase 3).
1237    ///
1238    /// Wraps `aprender_core::models::bert::CrossEncoder::load_from_reader`
1239    /// + `score()`. The APR must contain the canonical HF BERT tensor
1240    /// names (see `models::bert::expected_bert_tensor_names`).
1241    ///
1242    /// Tokenisation is NOT applied here — caller passes pre-tokenised
1243    /// `input_ids` + `token_type_ids` as comma-delimited u32 lists. A
1244    /// dedicated tokeniser-aware mode is Phase 3b follow-up scope.
1245    Rerank {
1246        /// Path to the APR file containing the cross-encoder weights.
1247        #[arg(value_name = "MODEL")]
1248        model: PathBuf,
1249        /// Pre-tokenised input ids (comma-separated `u32`s). Mutually
1250        /// exclusive with `--query`+`--passage`+`--vocab` (Phase 3b).
1251        /// Example: `--input-ids 101,2024,102,3456,102` for `[CLS] q [SEP] p [SEP]`.
1252        #[arg(long, value_name = "IDS")]
1253        input_ids: Option<String>,
1254        /// Pre-tokenised token-type ids (comma-separated `u32`s).
1255        /// Same length as `--input-ids`. 0 for query side, 1 for passage.
1256        #[arg(long, value_name = "IDS")]
1257        token_type_ids: Option<String>,
1258        /// Phase 3b — query text. Pair with `--passage` + `--vocab` to enable
1259        /// in-process WordPiece tokenisation. The tokeniser builds
1260        /// `[CLS] query [SEP] passage [SEP]` with `token_type_ids = 0` for
1261        /// the query side and `1` for the passage side.
1262        #[arg(long, value_name = "TEXT")]
1263        query: Option<String>,
1264        /// Phase 3b — passage text. Required when `--query` is supplied
1265        /// in single-pair mode (use `--passages` for batch ranking).
1266        #[arg(long, value_name = "TEXT")]
1267        passage: Option<String>,
1268        /// Phase 5 — batch ranking mode (#326). Passage candidates to
1269        /// score against `--query`. May be supplied multiple times:
1270        /// `apr rerank model.apr --query "..." --passages "p1" --passages "p2"`.
1271        /// Mutually exclusive with `--passage`. Output is one
1272        /// `score[i]` line per passage in input order, OR a JSON array
1273        /// of `{passage, logit, score}` objects sorted by descending
1274        /// score when `--sort` is set.
1275        #[arg(long, value_name = "TEXT")]
1276        passages: Vec<String>,
1277        /// Phase 5 — sort batch output by descending score (highest
1278        /// relevance first). Only meaningful with `--passages` and
1279        /// `--json`. Default: preserve input order.
1280        #[arg(long)]
1281        sort: bool,
1282        /// Phase 5 — limit to top-K passages after sorting. Implies
1283        /// `--sort`. Default 0 (no limit).
1284        #[arg(long, default_value_t = 0)]
1285        top_k: usize,
1286        /// Phase 3b — path to a WordPiece `vocab.txt` (one token per line,
1287        /// line index = token id). Required when `--query` is supplied.
1288        /// Must contain entries for `[CLS]`, `[SEP]`, and `[UNK]`.
1289        /// Phase 4 accepts HuggingFace `tokenizer.json` (extension-detected).
1290        #[arg(long, value_name = "FILE")]
1291        vocab: Option<PathBuf>,
1292        /// Override hidden_dim (default: 384 / MiniLM-L-6).
1293        #[arg(long, default_value_t = 384)]
1294        hidden_dim: usize,
1295        /// Override num_layers (default: 6 / MiniLM-L-6).
1296        #[arg(long, default_value_t = 6)]
1297        num_layers: usize,
1298        /// Override num_heads (default: 12 / MiniLM-L-6).
1299        #[arg(long, default_value_t = 12)]
1300        num_heads: usize,
1301        /// Override intermediate_dim (default: 1536 / MiniLM-L-6).
1302        #[arg(long, default_value_t = 1536)]
1303        intermediate_dim: usize,
1304        /// Override vocab_size (default: 30522 / bert-base-uncased).
1305        #[arg(long, default_value_t = 30522)]
1306        vocab_size: usize,
1307        /// Override max_position_embeddings (default: 512).
1308        #[arg(long, default_value_t = 512)]
1309        max_position_embeddings: usize,
1310        /// Override type_vocab_size (default: 2).
1311        #[arg(long, default_value_t = 2)]
1312        type_vocab_size: usize,
1313        /// Number of labels in the classifier head (default: 1 for
1314        /// regression-style relevance scoring).
1315        #[arg(long, default_value_t = 1)]
1316        num_labels: usize,
1317        /// Load the optional BERT pooler dense layer (default: true).
1318        /// Cross-encoders that skip the pooler should pass `--with-pooler false`.
1319        ///
1320        /// Takes an optional value: `--with-pooler` (bare) and an omitted flag
1321        /// both mean true; `--with-pooler false` / `--with-pooler=false` turn
1322        /// the pooler off. A bare `bool` here would compile to a SetTrue switch
1323        /// and make the documented `false` unreachable.
1324        #[arg(
1325            long,
1326            num_args = 0..=1,
1327            default_value_t = true,
1328            default_missing_value = "true",
1329            action = clap::ArgAction::Set,
1330        )]
1331        with_pooler: bool,
1332        /// Emit the raw logit instead of the sigmoid-mapped relevance score.
1333        #[arg(long)]
1334        raw_logit: bool,
1335        /// Output as JSON.
1336        #[arg(long)]
1337        json: bool,
1338    },
1339    /// Produce sentence embeddings from a BERT bi-encoder (GH-326 Phase 6).
1340    ///
1341    /// First-stage dense retrieval companion to `apr rerank`. Loads an
1342    /// encoder-only BertModel (e.g. `sentence-transformers/all-MiniLM-L6-v2`),
1343    /// tokenises the input text with WordPiece, runs the full encoder
1344    /// forward, then pools the hidden states with one of:
1345    ///   `--pool cls`  — take the [CLS] hidden state
1346    ///   `--pool mean` — mean over non-padding token positions (default;
1347    ///                   sentence-transformers convention)
1348    /// Optionally L2-normalises the result (`--normalize`, default true,
1349    /// matches sentence-transformers).
1350    Embed {
1351        /// Path to the APR file containing the encoder weights (BertModel).
1352        #[arg(value_name = "MODEL")]
1353        model: PathBuf,
1354        /// Text to encode. Repeatable: `apr embed model.apr --text "a" --text "b" --vocab tok.json`.
1355        #[arg(long, value_name = "TEXT")]
1356        text: Vec<String>,
1357        /// Phase 7 (GH-326) — read texts from a file, one per line.
1358        /// Concatenated with `--text` inputs in order: `--text` first,
1359        /// then `--text-file` rows. Blank lines and lines starting
1360        /// with `#` are skipped. Useful for RAG-style first-stage
1361        /// retrieval where the second-stage rerank candidate set
1362        /// (50-100 documents) is the embed input.
1363        #[arg(long, value_name = "FILE")]
1364        text_file: Option<PathBuf>,
1365        /// Path to a WordPiece `vocab.txt` or HF `tokenizer.json`.
1366        #[arg(long, value_name = "FILE")]
1367        vocab: PathBuf,
1368        /// Pooling strategy (`cls` or `mean`). Default: `mean`
1369        /// (matches sentence-transformers convention).
1370        #[arg(long, default_value = "mean")]
1371        pool: String,
1372        /// L2-normalise the output embedding. Default: true (matches
1373        /// sentence-transformers convention). Pass `--normalize false`
1374        /// to keep raw magnitudes.
1375        ///
1376        /// Takes an optional value: `--normalize` (bare) and an omitted flag
1377        /// both mean true; `--normalize false` / `--normalize=false` keep the
1378        /// raw magnitudes. A bare `bool` here would compile to a SetTrue switch
1379        /// and make the documented `false` unreachable.
1380        ///
1381        /// Because the value is optional, do not place a bare `--normalize`
1382        /// immediately before the MODEL positional — write
1383        /// `apr embed MODEL --normalize` or `--normalize=true MODEL`.
1384        #[arg(
1385            long,
1386            num_args = 0..=1,
1387            default_value_t = true,
1388            default_missing_value = "true",
1389            action = clap::ArgAction::Set,
1390        )]
1391        normalize: bool,
1392        /// Override hidden_dim (default: 384 / MiniLM).
1393        #[arg(long, default_value_t = 384)]
1394        hidden_dim: usize,
1395        /// Override num_layers (default: 6 / MiniLM-L-6).
1396        #[arg(long, default_value_t = 6)]
1397        num_layers: usize,
1398        /// Override num_heads.
1399        #[arg(long, default_value_t = 12)]
1400        num_heads: usize,
1401        /// Override intermediate_dim.
1402        #[arg(long, default_value_t = 1536)]
1403        intermediate_dim: usize,
1404        /// Override vocab_size.
1405        #[arg(long, default_value_t = 30522)]
1406        vocab_size: usize,
1407        /// Override max_position_embeddings.
1408        #[arg(long, default_value_t = 512)]
1409        max_position_embeddings: usize,
1410        /// Override type_vocab_size.
1411        #[arg(long, default_value_t = 2)]
1412        type_vocab_size: usize,
1413        /// Output as JSON.
1414        #[arg(long)]
1415        json: bool,
1416    },
1417}
1418
1419/// Subcommands for `apr dataset` — dataset inspection (aprender#2377 finding 3).
1420///
1421/// `audio-inspect` is the PRODUCER for `apr audio-inspect-lint`: CRUX-H-13
1422/// shipped the lint with help pointing at a command the binary did not have,
1423/// so its gates had never run on real data.
1424#[derive(Subcommand, Debug)]
1425pub enum DatasetCommands {
1426    /// Decode an uncompressed RIFF/WAVE file and report its measured shape and
1427    /// amplitude extrema — the observation `apr audio-inspect-lint` reads.
1428    ///
1429    /// Supports PCM u8/i16/i24/i32 and IEEE float32. Compressed containers
1430    /// (FLAC, MP3, Ogg) and codecs it cannot decode are REFUSED with a non-zero
1431    /// exit; no resampling and no channel mixdown are performed, so the reported
1432    /// `sample_rate` and `channels` are always the file's own.
1433    AudioInspect {
1434        /// Path to the .wav file to decode
1435        #[arg(value_name = "FILE")]
1436        file: PathBuf,
1437        /// Output format: `json` for the lint-readable body, `text` for humans
1438        #[arg(long, value_name = "FORMAT", default_value = "text",
1439              value_parser = ["json", "text"])]
1440        format: String,
1441        /// Write the observation here instead of stdout
1442        #[arg(short, long, value_name = "FILE")]
1443        output: Option<PathBuf>,
1444        /// Overwrite an existing --output file (refused without it)
1445        #[arg(short, long)]
1446        force: bool,
1447    },
1448}
1449
1450/// Subcommands for `apr kernel` — kernel-level measurements (aprender#2377 finding 3).
1451///
1452/// `parity` is the PRODUCER for `apr attn-parity-lint`: CRUX-L-02 shipped the
1453/// lint with help pointing at `apr kernel parity`, which did not exist.
1454#[derive(Subcommand, Debug)]
1455pub enum KernelCommands {
1456    /// Measure a tiled attention kernel against a naive reference on seeded
1457    /// Q/K/V, emitting the parity + provenance body `apr attn-parity-lint` reads.
1458    ///
1459    /// `--impl tiled` runs the in-tree `realizar::brick::FlashAttentionBrick`
1460    /// online-softmax kernel. `--impl flash2` means the pinned
1461    /// `hf-kernels-community:flash-attn2@<sha>` CUDA kernel, which this binary
1462    /// does not embed: asking for it is REFUSED with a non-zero exit rather
1463    /// than answered by a different kernel under a borrowed name.
1464    Parity {
1465        /// Attention implementation under test
1466        #[arg(long = "impl", value_name = "IMPL", value_enum, default_value_t = KernelImpl::Tiled)]
1467        kernel: KernelImpl,
1468        /// Reference implementation to compare against
1469        #[arg(long = "ref", value_name = "REF", value_enum, default_value_t = KernelRef::Naive)]
1470        reference: KernelRef,
1471        /// KV cache length to attend over
1472        #[arg(long, value_name = "N", default_value_t = 128)]
1473        seq_len: usize,
1474        /// Number of query heads
1475        #[arg(long, value_name = "N", default_value_t = 8)]
1476        num_heads: usize,
1477        /// Number of key/value heads (GQA groups when smaller than --num-heads)
1478        #[arg(long, value_name = "N", default_value_t = 8)]
1479        num_kv_heads: usize,
1480        /// Per-head dimension. flash2 dispatches only 64 or 128
1481        #[arg(long, value_name = "N", default_value_t = 64)]
1482        head_dim: usize,
1483        /// Seed pinning the Q/K/V draw, so a run is reproducible
1484        #[arg(long, value_name = "N", default_value_t = 0)]
1485        seed: u64,
1486        /// Emit the observation as JSON (required to capture it for the lint)
1487        #[arg(long)]
1488        json: bool,
1489        /// Write the observation here instead of stdout
1490        #[arg(short, long, value_name = "FILE")]
1491        output: Option<PathBuf>,
1492        /// Overwrite an existing --output file (refused without it)
1493        #[arg(short, long)]
1494        force: bool,
1495    },
1496}
1497
1498#[cfg(feature = "training")]
1499/// Subcommands for `apr runs` — experiment run management (ALB-050/051)
1500#[derive(Subcommand, Debug)]
1501pub enum RunsCommands {
1502    /// List all training experiment runs (with inline loss sparklines)
1503    Ls {
1504        /// Directory to scan for experiments (default: current dir)
1505        #[arg(long, value_name = "DIR")]
1506        dir: Option<PathBuf>,
1507        /// Read from global experiment registry (~/.entrenar/experiments.db)
1508        #[arg(long)]
1509        global: bool,
1510        /// Filter by status: all, pending, running, completed, failed, cancelled
1511        #[arg(long, default_value = "all")]
1512        status: String,
1513        /// Output as JSON
1514        #[arg(long)]
1515        json: bool,
1516        /// Maximum number of runs to show
1517        #[arg(long, default_value = "50")]
1518        limit: usize,
1519    },
1520    /// Show detailed metrics for a specific run (with braille loss curve)
1521    Show {
1522        /// Run ID
1523        #[arg(value_name = "RUN_ID")]
1524        run_id: String,
1525        /// Directory containing experiment DB
1526        #[arg(long, value_name = "DIR")]
1527        dir: Option<PathBuf>,
1528        /// Read from global registry
1529        #[arg(long)]
1530        global: bool,
1531        /// Output as JSON
1532        #[arg(long)]
1533        json: bool,
1534    },
1535    /// Compare two runs side-by-side (loss curves, config diff, metrics)
1536    Diff {
1537        /// First run ID
1538        #[arg(value_name = "RUN_A")]
1539        run_a: String,
1540        /// Second run ID
1541        #[arg(value_name = "RUN_B")]
1542        run_b: String,
1543        /// Directory containing experiment DB
1544        #[arg(long, value_name = "DIR")]
1545        dir: Option<PathBuf>,
1546        /// Read from global registry
1547        #[arg(long)]
1548        global: bool,
1549        /// Output as JSON
1550        #[arg(long)]
1551        json: bool,
1552    },
1553}
1554
1555#[cfg(feature = "training")]
1556/// Subcommands for `apr experiment` — interactive experiment browser (ALB-024)
1557#[derive(Subcommand, Debug)]
1558pub enum ExperimentCommands {
1559    /// Browse experiment history with interactive TUI (loss curves, params)
1560    View {
1561        /// Path to experiment database file
1562        #[arg(long, value_name = "FILE")]
1563        db: Option<PathBuf>,
1564        /// Read from global experiment registry (~/.entrenar/experiments.db)
1565        #[arg(long)]
1566        global: bool,
1567        /// Output as JSON (non-interactive)
1568        #[arg(long)]
1569        json: bool,
1570    },
1571}
1572
1573/// CRUX-K-11: Subcommands for `apr modelfile`.
1574#[derive(Subcommand, Debug)]
1575pub enum ModelfileSubcommand {
1576    /// Parse an Ollama-style Modelfile and emit the parsed config.
1577    ///
1578    /// Grammar: `FROM`, `PARAMETER`, `TEMPLATE`, `SYSTEM`, `LICENSE`,
1579    /// `MESSAGE`, `ADAPTER` directives. Triple-quoted blocks supported.
1580    /// Directive names are case-insensitive. Unknown directives raise
1581    /// `file:line:col` errors.
1582    Parse {
1583        /// Path to the Modelfile
1584        #[arg(value_name = "FILE")]
1585        file: PathBuf,
1586        /// Output format: `json` or `human`
1587        #[arg(long, default_value = "json")]
1588        format: String,
1589    },
1590}
1591
1592/// GH-876: Subcommands for `apr probar` — consolidates the probador testing
1593/// framework under `apr`. Milestone 1 ships only `tensor` (the migrated
1594/// existing behavior). Subsequent milestones add the remaining 14 probador
1595/// subcommands as separate PRs that delegate to the probador library.
1596#[derive(Subcommand, Debug)]
1597pub enum TestSubcommand {
1598    /// Export tensor activations for visual regression testing (PMAT-481).
1599    ///
1600    /// Generates JSON/PNG per-layer test artifacts that can be compared
1601    /// against a golden reference directory to detect regressions in
1602    /// model behavior after weight updates, quantization, or refactors.
1603    Tensor {
1604        /// Path to .apr model file
1605        #[arg(value_name = "FILE")]
1606        file: PathBuf,
1607        /// Output directory for test artifacts
1608        #[arg(short, long, default_value = "./probar-export")]
1609        output: PathBuf,
1610        /// Export format: json, png, or both
1611        #[arg(long, default_value = "both")]
1612        format: String,
1613        /// Golden reference directory for comparison
1614        #[arg(long)]
1615        golden: Option<PathBuf>,
1616        /// Filter layers by name pattern
1617        #[arg(long)]
1618        layer: Option<String>,
1619        /// Exit non-zero on golden divergence (CI mode, PMAT-481)
1620        #[arg(long)]
1621        assert: bool,
1622        /// Cosine similarity threshold for golden comparison (default: 0.98)
1623        #[arg(long, default_value = "0.98",
1624              value_parser = commands::threshold_arg::parse_cosine_f32)]
1625        tolerance: f32,
1626    },
1627}
1628
1629/// Parse `apr cbtop --iterations`, rejecting 0.
1630///
1631/// With zero measurement iterations every brick keeps zero samples, so its
1632/// measured time is 0.0µs, its gap factor is 0.00x and it scores a perfect
1633/// 100/A — a green report attesting to measurements that never ran. Reject the
1634/// value where the user typed it rather than emitting the fabricated report.
1635fn parse_cbtop_iterations(s: &str) -> std::result::Result<usize, String> {
1636    let n: usize = s
1637        .parse()
1638        .map_err(|_| format!("`{s}` is not a valid iteration count"))?;
1639    if n == 0 {
1640        return Err(
1641            "must be at least 1 — a zero-iteration run measures nothing and would report every \
1642             brick as a perfect 100/A from zero samples"
1643                .to_string(),
1644        );
1645    }
1646    Ok(n)
1647}