diff --git a/src/main.rs b/src/main.rs index 1fd5635..c4c26be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -651,27 +651,37 @@ fn extract_invalid_value_context(e: &clap::Error) -> (Option, Option String { - const VALID_COMMANDS: &[&str] = &[ - "issues", - "mrs", - "search", - "sync", - "ingest", - "count", - "status", - "auth", - "doctor", - "version", - "init", - "stats", - "generate-docs", - "embed", - "migrate", - "health", - "robot-docs", - "completions", - "timeline", - "who", + // Primary commands + common aliases for fuzzy matching + const VALID_COMMANDS: &[(&str, &str)] = &[ + ("issues", "issues"), + ("issue", "issues"), + ("mrs", "mrs"), + ("mr", "mrs"), + ("merge-requests", "mrs"), + ("search", "search"), + ("find", "search"), + ("query", "search"), + ("sync", "sync"), + ("ingest", "ingest"), + ("count", "count"), + ("status", "status"), + ("auth", "auth"), + ("doctor", "doctor"), + ("version", "version"), + ("init", "init"), + ("stats", "stats"), + ("stat", "stats"), + ("generate-docs", "generate-docs"), + ("embed", "embed"), + ("migrate", "migrate"), + ("health", "health"), + ("robot-docs", "robot-docs"), + ("completions", "completions"), + ("timeline", "timeline"), + ("who", "who"), + ("notes", "notes"), + ("note", "notes"), + ("drift", "drift"), ]; let invalid_lower = invalid.to_lowercase(); @@ -679,19 +689,43 @@ fn suggest_similar_command(invalid: &str) -> String { // Find the best match using Jaro-Winkler similarity let best_match = VALID_COMMANDS .iter() - .map(|cmd| (*cmd, jaro_winkler(&invalid_lower, cmd))) + .map(|(alias, canonical)| (*canonical, jaro_winkler(&invalid_lower, alias))) .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); if let Some((cmd, score)) = best_match && score > 0.7 { + let example = command_example(cmd); return format!( - "Did you mean 'lore {}'? Run 'lore robot-docs' for all commands", - cmd + "Did you mean 'lore {cmd}'? Example: {example}. Run 'lore robot-docs' for all commands" ); } - "Run 'lore robot-docs' for valid commands".to_string() + "Run 'lore robot-docs' for valid commands. Common: issues, mrs, search, sync, timeline, who" + .to_string() +} + +/// Return a contextual usage example for a command. +fn command_example(cmd: &str) -> &'static str { + match cmd { + "issues" => "lore --robot issues -n 10", + "mrs" => "lore --robot mrs -n 10", + "search" => "lore --robot search \"auth bug\"", + "sync" => "lore --robot sync", + "ingest" => "lore --robot ingest issues", + "notes" => "lore --robot notes --for-issue 123", + "count" => "lore --robot count issues", + "status" => "lore --robot status", + "stats" => "lore --robot stats", + "timeline" => "lore --robot timeline \"auth flow\"", + "who" => "lore --robot who --path src/", + "health" => "lore --robot health", + "generate-docs" => "lore --robot generate-docs", + "embed" => "lore --robot embed", + "robot-docs" => "lore robot-docs", + "init" => "lore init", + _ => "lore --robot ", + } } fn handle_issues( @@ -2135,6 +2169,8 @@ struct RobotDocsData { commands: serde_json::Value, /// Deprecated command aliases (old -> new) aliases: serde_json::Value, + /// Pre-clap error tolerance: what the CLI auto-corrects + error_tolerance: serde_json::Value, exit_codes: serde_json::Value, /// Error codes emitted by clap parse failures clap_error_codes: serde_json::Value, @@ -2489,12 +2525,54 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box": "issues ", - "show mr ": "mrs ", - "auth-test": "auth", - "sync-status": "status" + "deprecated_commands": { + "list issues": "issues", + "list mrs": "mrs", + "show issue ": "issues ", + "show mr ": "mrs ", + "auth-test": "auth", + "sync-status": "status" + }, + "command_aliases": { + "issue": "issues", + "mr": "mrs", + "merge-requests": "mrs", + "merge-request": "mrs", + "note": "notes", + "find": "search", + "query": "search", + "stat": "stats", + "st": "status" + }, + "pre_clap_aliases": { + "note": "Underscore/no-separator forms auto-corrected before parsing", + "merge_requests": "mrs", + "merge_request": "mrs", + "mergerequests": "mrs", + "mergerequest": "mrs", + "generate_docs": "generate-docs", + "generatedocs": "generate-docs", + "gendocs": "generate-docs", + "gen-docs": "generate-docs", + "robot_docs": "robot-docs", + "robotdocs": "robot-docs" + }, + "prefix_matching": "Enabled via infer_subcommands. Unambiguous prefixes work: 'iss' -> issues, 'time' -> timeline, 'sea' -> search" + }); + + let error_tolerance = serde_json::json!({ + "note": "The CLI auto-corrects common mistakes before parsing. Corrections are applied silently with a teaching note on stderr.", + "auto_corrections": [ + {"type": "single_dash_long_flag", "example": "-robot -> --robot", "mode": "all"}, + {"type": "case_normalization", "example": "--Robot -> --robot, --State -> --state", "mode": "all"}, + {"type": "flag_prefix", "example": "--proj -> --project (when unambiguous)", "mode": "all"}, + {"type": "fuzzy_flag", "example": "--projct -> --project", "mode": "all (threshold 0.9 in robot, 0.8 in human)"}, + {"type": "subcommand_alias", "example": "merge_requests -> mrs, robotdocs -> robot-docs", "mode": "all"}, + {"type": "value_normalization", "example": "--state Opened -> --state opened", "mode": "all"}, + {"type": "value_fuzzy", "example": "--state opend -> --state opened", "mode": "all"}, + {"type": "prefix_matching", "example": "lore iss -> lore issues, lore time -> lore timeline", "mode": "all (via clap infer_subcommands)"} + ], + "teaching_notes": "Auto-corrections emit a JSON warning on stderr: {\"warning\":{\"type\":\"ARG_CORRECTED\",\"corrections\":[...],\"teaching\":[...]}}" }); // Phase 3: Clap error codes (emitted by handle_clap_error) @@ -2533,6 +2611,7 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box