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,
|
ValueNormalization,
|
||||||
ValueFuzzy,
|
ValueFuzzy,
|
||||||
FlagPrefix,
|
FlagPrefix,
|
||||||
|
NoColorExpansion,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of the correction pass over raw args.
|
/// Result of the correction pass over raw args.
|
||||||
@@ -128,6 +129,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
"--dry-run",
|
"--dry-run",
|
||||||
"--no-dry-run",
|
"--no-dry-run",
|
||||||
"--timings",
|
"--timings",
|
||||||
|
"--lock",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -203,7 +205,6 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
|||||||
&[
|
&[
|
||||||
"--limit",
|
"--limit",
|
||||||
"--fields",
|
"--fields",
|
||||||
"--format",
|
|
||||||
"--author",
|
"--author",
|
||||||
"--note-type",
|
"--note-type",
|
||||||
"--contains",
|
"--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 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();
|
let s = fixed.corrected.clone();
|
||||||
corrections.push(fixed);
|
corrections.push(fixed);
|
||||||
corrected.push(s);
|
corrected.push(s);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
corrected.push(arg);
|
corrected.push(arg);
|
||||||
}
|
}
|
||||||
@@ -611,12 +624,27 @@ const CLAP_BUILTINS: &[&str] = &["--help", "--version"];
|
|||||||
///
|
///
|
||||||
/// When `strict` is true, fuzzy matching is disabled — only deterministic
|
/// When `strict` is true, fuzzy matching is disabled — only deterministic
|
||||||
/// corrections (single-dash fix, case normalization) are applied.
|
/// 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> {
|
fn try_correct(arg: &str, valid_flags: &[&str], strict: bool) -> Option<Correction> {
|
||||||
// Only attempt correction on flag-like args (starts with `-`)
|
// Only attempt correction on flag-like args (starts with `-`)
|
||||||
if !arg.starts_with('-') {
|
if !arg.starts_with('-') {
|
||||||
return None;
|
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)
|
// B2: Never correct clap built-in flags (--help, --version)
|
||||||
let flag_part_for_builtin = if let Some(eq_pos) = arg.find('=') {
|
let flag_part_for_builtin = if let Some(eq_pos) = arg.find('=') {
|
||||||
&arg[..eq_pos]
|
&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.
|
/// 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)> {
|
fn best_fuzzy_match<'a>(input: &str, valid_flags: &[&'a str]) -> Option<(&'a str, f64)> {
|
||||||
valid_flags
|
valid_flags
|
||||||
.iter()
|
.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)))
|
.map(|&flag| (flag, jaro_winkler(input, flag)))
|
||||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
|
.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
|
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"));
|
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 ----
|
// ---- Post-clap suggestion helpers ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user