Skip to main content

apr_cli/
data_commands.rs

1
2/// Parse an overlap threshold, rejecting values outside the documented
3/// 0.0-1.0 range.
4///
5/// A threshold above 1.0 makes the per-sample overlap test unsatisfiable and
6/// silently turns the AC-016 contamination gate into an unconditional pass;
7/// below 0.0 it flags everything. Neither is a meaningful ratio.
8fn parse_unit_interval(raw: &str) -> Result<f64, String> {
9    let value: f64 = raw
10        .parse()
11        .map_err(|_| format!("'{raw}' is not a number"))?;
12    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
13        return Err(format!(
14            "'{raw}' is outside the valid range 0.0-1.0 (an overlap ratio)"
15        ));
16    }
17    Ok(value)
18}
19
20/// Parse an n-gram size, rejecting 0.
21///
22/// A zero-width window is not a window: `--ngram 0` reached
23/// `slice::windows(0)` inside the decontamination scan and aborted the
24/// process with "window size must be non-zero" (exit 101).
25fn parse_ngram_size(raw: &str) -> Result<usize, String> {
26    let value: usize = raw
27        .parse()
28        .map_err(|_| format!("'{raw}' is not a non-negative integer"))?;
29    if value == 0 {
30        return Err("n-gram size must be >= 1; a zero-width window compares nothing".to_string());
31    }
32    Ok(value)
33}
34
35/// Data quality pipeline subcommands (powered by alimentar).
36///
37/// Thin CLI wrappers around alimentar's data utilities.
38#[derive(Subcommand, Debug)]
39pub enum DataCommands {
40    /// Every alimentar data command: convert, info, head, schema, mix, fim,
41    /// filter-text, view, import, hub, registry, drift, quality, fed, doctest,
42    /// extract, merge.
43    ///
44    /// APR-MONO consolidated alimentar in-tree, but its capability stayed
45    /// reachable only through the standalone `alimentar` binary -- `apr data`
46    /// shipped 5 commands against alimentar's 20, so 18 had no route through
47    /// `apr` at all. This dispatches the SAME `alimentar::cli::dispatch`, so
48    /// there is one implementation behind two names rather than a second clap
49    /// tree that can drift from the first.
50    #[command(subcommand, name = "x")]
51    Alimentar(alimentar::cli::Commands),
52
53    /// Audit a JSONL classification dataset for quality issues
54    Audit {
55        /// Path to JSONL data file
56        #[arg(value_name = "FILE")]
57        file: PathBuf,
58        /// Number of output classes (for label range validation)
59        #[arg(long, default_value = "5")]
60        num_classes: usize,
61        /// Input text column name
62        #[arg(long, default_value = "input")]
63        input_column: String,
64        /// Label column name
65        #[arg(long, default_value = "label")]
66        label_column: String,
67        /// Preamble prefix to detect (e.g., "#!/")
68        #[arg(long, default_value = "#!/")]
69        preamble_prefix: Option<String>,
70    },
71    /// Stratified train/val/test split preserving class proportions
72    Split {
73        /// Path to JSONL data file
74        #[arg(value_name = "FILE")]
75        file: PathBuf,
76        /// Training set fraction
77        #[arg(long, default_value = "0.8")]
78        train: f64,
79        /// Validation set fraction
80        #[arg(long, default_value = "0.1")]
81        val: f64,
82        /// Test set fraction
83        #[arg(long, default_value = "0.1")]
84        test: f64,
85        /// Label column name for stratification
86        #[arg(long, default_value = "label")]
87        label_column: String,
88        /// Random seed for deterministic split
89        #[arg(long, default_value = "42")]
90        seed: u64,
91        /// Output directory for split files
92        #[arg(short, long)]
93        output: PathBuf,
94    },
95    /// Check training data for benchmark contamination via n-gram overlap
96    Decontaminate {
97        /// Path to training JSONL data file
98        #[arg(value_name = "FILE")]
99        file: PathBuf,
100        /// Reference benchmark JSONL files to check against
101        #[arg(long, required = true, num_args = 1..)]
102        reference: Vec<PathBuf>,
103        /// N-gram size for overlap detection (must be >= 1)
104        #[arg(long, default_value = "10", value_parser = parse_ngram_size)]
105        ngram: usize,
106        /// Overlap threshold (0.0-1.0) above which a sample is flagged
107        #[arg(long, default_value = "0.5", value_parser = parse_unit_interval)]
108        threshold: f64,
109        /// Output as JSON
110        #[arg(long)]
111        json: bool,
112    },
113    /// Remove exact duplicate rows from a JSONL dataset
114    Dedup {
115        /// Path to JSONL data file
116        #[arg(value_name = "FILE")]
117        file: PathBuf,
118        /// Output file path for the deduplicated dataset
119        #[arg(short, long)]
120        output: PathBuf,
121        /// Output as JSON
122        #[arg(long)]
123        json: bool,
124    },
125    /// Resample dataset to address class imbalance
126    Balance {
127        /// Path to JSONL data file
128        #[arg(value_name = "FILE")]
129        file: PathBuf,
130        /// Rebalancing strategy: oversample, undersample, sqrt-inverse
131        #[arg(long, default_value = "oversample")]
132        strategy: String,
133        /// Label column name
134        #[arg(long, default_value = "label")]
135        label_column: String,
136        /// Number of classes (for sqrt-inverse weight computation)
137        #[arg(long)]
138        num_classes: Option<usize>,
139        /// Random seed
140        #[arg(long, default_value = "42")]
141        seed: u64,
142        /// Output file path (required for oversample/undersample)
143        #[arg(short, long)]
144        output: Option<PathBuf>,
145    },
146}