feat(cli): improve autocorrect with --no-color expansion and --lock flag
Add NoColorExpansion correction rule that rewrites --no-color into the two-arg form --color never, matching clap's expected syntax. The caller detects the rule variant and inserts the second arg. Also: add --lock to the sync command's known flags, and remove --format from the notes command's known flags (format selection was removed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ pub enum CorrectionRule {
|
||||
ValueNormalization,
|
||||
ValueFuzzy,
|
||||
FlagPrefix,
|
||||
NoColorExpansion,
|
||||
}
|
||||
|
||||
/// Result of the correction pass over raw args.
|
||||
@@ -128,6 +129,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--dry-run",
|
||||
"--no-dry-run",
|
||||
"--timings",
|
||||
"--lock",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -203,7 +205,6 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
&[
|
||||
"--limit",
|
||||
"--fields",
|
||||
"--format",
|
||||
"--author",
|
||||
"--note-type",
|
||||
"--contains",
|
||||
@@ -424,9 +425,21 @@ pub fn correct_args(raw: Vec<String>, strict: bool) -> CorrectionResult {
|
||||
}
|
||||
|
||||
if let Some(fixed) = try_correct(&arg, &valid, strict) {
|
||||
if fixed.rule == CorrectionRule::NoColorExpansion {
|
||||
// Expand --no-color → --color never
|
||||
corrections.push(Correction {
|
||||
original: fixed.original,
|
||||
corrected: "--color never".to_string(),
|
||||
rule: CorrectionRule::NoColorExpansion,
|
||||
confidence: 1.0,
|
||||
});
|
||||
corrected.push("--color".to_string());
|
||||
corrected.push("never".to_string());
|
||||
} else {
|
||||
let s = fixed.corrected.clone();
|
||||
corrections.push(fixed);
|
||||
corrected.push(s);
|
||||
}
|
||||
} else {
|
||||
corrected.push(arg);
|
||||
}
|
||||
@@ -611,12 +624,27 @@ const CLAP_BUILTINS: &[&str] = &["--help", "--version"];
|
||||
///
|
||||
/// When `strict` is true, fuzzy matching is disabled — only deterministic
|
||||
/// corrections (single-dash fix, case normalization) are applied.
|
||||
///
|
||||
/// Special case: `--no-color` is rewritten to `--color never` by returning
|
||||
/// the `--color` correction and letting the caller handle arg insertion.
|
||||
/// However, since we correct one arg at a time, we use `NoColorExpansion`
|
||||
/// to signal that the next phase should insert `never` after this arg.
|
||||
fn try_correct(arg: &str, valid_flags: &[&str], strict: bool) -> Option<Correction> {
|
||||
// Only attempt correction on flag-like args (starts with `-`)
|
||||
if !arg.starts_with('-') {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Special case: --no-color → --color never (common agent/user expectation)
|
||||
if arg.eq_ignore_ascii_case("--no-color") {
|
||||
return Some(Correction {
|
||||
original: arg.to_string(),
|
||||
corrected: "--no-color".to_string(), // sentinel; expanded in correct_args
|
||||
rule: CorrectionRule::NoColorExpansion,
|
||||
confidence: 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
// B2: Never correct clap built-in flags (--help, --version)
|
||||
let flag_part_for_builtin = if let Some(eq_pos) = arg.find('=') {
|
||||
&arg[..eq_pos]
|
||||
@@ -766,9 +794,21 @@ fn try_correct(arg: &str, valid_flags: &[&str], strict: bool) -> Option<Correcti
|
||||
}
|
||||
|
||||
/// Find the best fuzzy match among valid flags for a given (lowercased) input.
|
||||
///
|
||||
/// Applies a length guard to prevent short candidates (e.g. `--for`, 5 chars
|
||||
/// including dashes) from inflating Jaro-Winkler scores against long inputs.
|
||||
/// When the input is more than 40% longer than a candidate, that candidate is
|
||||
/// excluded from fuzzy consideration (it can still match via prefix rule).
|
||||
fn best_fuzzy_match<'a>(input: &str, valid_flags: &[&'a str]) -> Option<(&'a str, f64)> {
|
||||
valid_flags
|
||||
.iter()
|
||||
.filter(|&&flag| {
|
||||
// Guard: skip short candidates when input is much longer.
|
||||
// e.g. "--foobar" (8 chars) should not fuzzy-match "--for" (5 chars)
|
||||
// Ratio: input must be within 1.4x the candidate length.
|
||||
let max_input_len = (flag.len() as f64 * 1.4) as usize;
|
||||
input.len() <= max_input_len
|
||||
})
|
||||
.map(|&flag| (flag, jaro_winkler(input, flag)))
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
}
|
||||
@@ -846,6 +886,9 @@ pub fn format_teaching_note(correction: &Correction) -> String {
|
||||
correction.corrected, correction.original
|
||||
)
|
||||
}
|
||||
CorrectionRule::NoColorExpansion => {
|
||||
"Use `--color never` instead of `--no-color`".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1286,6 +1329,53 @@ mod tests {
|
||||
assert!(note.contains("full flag name"));
|
||||
}
|
||||
|
||||
// ---- --no-color expansion ----
|
||||
|
||||
#[test]
|
||||
fn no_color_expands_to_color_never() {
|
||||
let result = correct_args(args("lore --no-color health"), false);
|
||||
assert_eq!(result.corrections.len(), 1);
|
||||
assert_eq!(result.corrections[0].rule, CorrectionRule::NoColorExpansion);
|
||||
assert_eq!(result.args, args("lore --color never health"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_color_case_insensitive() {
|
||||
let result = correct_args(args("lore --No-Color issues"), false);
|
||||
assert_eq!(result.corrections.len(), 1);
|
||||
assert_eq!(result.args, args("lore --color never issues"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_color_with_robot_mode() {
|
||||
let result = correct_args(args("lore --robot --no-color health"), true);
|
||||
assert_eq!(result.corrections.len(), 1);
|
||||
assert_eq!(result.args, args("lore --robot --color never health"));
|
||||
}
|
||||
|
||||
// ---- Fuzzy matching length guard ----
|
||||
|
||||
#[test]
|
||||
fn foobar_does_not_match_for() {
|
||||
// --foobar (8 chars) should NOT fuzzy-match --for (5 chars)
|
||||
let result = correct_args(args("lore count --foobar issues"), false);
|
||||
assert!(
|
||||
!result.corrections.iter().any(|c| c.corrected == "--for"),
|
||||
"expected --foobar not to match --for"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fro_still_matches_for() {
|
||||
// --fro (5 chars) is short enough to fuzzy-match --for (5 chars)
|
||||
// and also qualifies as a prefix match
|
||||
let result = correct_args(args("lore count --fro issues"), false);
|
||||
assert!(
|
||||
result.corrections.iter().any(|c| c.corrected == "--for"),
|
||||
"expected --fro to match --for"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Post-clap suggestion helpers ----
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user