refactor(search): rename --after/--updated-after to --since/--updated-since

The --since naming is more intuitive (matches git log --since) and
consistent with the list commands which already use --since. Renames
the CLI flags, SearchCliFilters fields, SearchFilters fields,
autocorrect registry, and robot-docs manifest. No behavioral change.

Affected paths:
- cli/mod.rs: SearchArgs field + clap attribute rename
- cli/commands/search.rs: SearchCliFilters + run_search plumbing
- search/filters.rs: SearchFilters struct + apply_filters logic
- main.rs: handle_search + robot-docs JSON
- cli/autocorrect.rs: COMMAND_FLAGS entry for search

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-08 14:33:24 -05:00
parent 7dd86d5433
commit 940a96375a
5 changed files with 228 additions and 73 deletions

View File

@@ -131,8 +131,8 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
"--project", "--project",
"--label", "--label",
"--path", "--path",
"--after", "--since",
"--updated-after", "--updated-since",
"--limit", "--limit",
"--explain", "--explain",
"--no-explain", "--no-explain",
@@ -294,16 +294,33 @@ fn valid_flags_for(subcommand: Option<&str>) -> Vec<&'static str> {
/// Run the pre-clap correction pass on raw args. /// Run the pre-clap correction pass on raw args.
/// ///
/// When `strict` is true (robot mode), only deterministic corrections are applied
/// (single-dash long flags, case normalization). Fuzzy matching is disabled to
/// prevent misleading agents with speculative corrections.
///
/// Returns the (possibly modified) args and any corrections applied. /// Returns the (possibly modified) args and any corrections applied.
pub fn correct_args(raw: Vec<String>) -> CorrectionResult { pub fn correct_args(raw: Vec<String>, strict: bool) -> CorrectionResult {
let subcommand = detect_subcommand(&raw); let subcommand = detect_subcommand(&raw);
let valid = valid_flags_for(subcommand); let valid = valid_flags_for(subcommand);
let mut corrected = Vec::with_capacity(raw.len()); let mut corrected = Vec::with_capacity(raw.len());
let mut corrections = Vec::new(); let mut corrections = Vec::new();
let mut past_terminator = false;
for arg in raw { for arg in raw {
if let Some(fixed) = try_correct(&arg, &valid) { // B1: Stop correcting after POSIX `--` option terminator
if arg == "--" {
past_terminator = true;
corrected.push(arg);
continue;
}
if past_terminator {
corrected.push(arg);
continue;
}
if let Some(fixed) = try_correct(&arg, &valid, strict) {
let s = fixed.corrected.clone(); let s = fixed.corrected.clone();
corrections.push(fixed); corrections.push(fixed);
corrected.push(s); corrected.push(s);
@@ -318,13 +335,33 @@ pub fn correct_args(raw: Vec<String>) -> CorrectionResult {
} }
} }
/// Clap built-in flags that should never be corrected. These are handled by clap
/// directly and are not in our GLOBAL_FLAGS registry.
const CLAP_BUILTINS: &[&str] = &["--help", "--version"];
/// Try to correct a single arg. Returns `None` if no correction needed. /// Try to correct a single arg. Returns `None` if no correction needed.
fn try_correct(arg: &str, valid_flags: &[&str]) -> Option<Correction> { ///
/// When `strict` is true, fuzzy matching is disabled — only deterministic
/// corrections (single-dash fix, case normalization) are applied.
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;
} }
// B2: Never correct clap built-in flags (--help, --version)
let flag_part_for_builtin = if let Some(eq_pos) = arg.find('=') {
&arg[..eq_pos]
} else {
arg
};
if CLAP_BUILTINS
.iter()
.any(|b| b.eq_ignore_ascii_case(flag_part_for_builtin))
{
return None;
}
// Skip short flags — they're unambiguous single chars (-p, -n, -v, -J) // Skip short flags — they're unambiguous single chars (-p, -n, -v, -J)
// Also skip stacked short flags (-vvv) // Also skip stacked short flags (-vvv)
if !arg.starts_with("--") { if !arg.starts_with("--") {
@@ -371,8 +408,9 @@ fn try_correct(arg: &str, valid_flags: &[&str]) -> Option<Correction> {
}); });
} }
// Try fuzzy on the single-dash candidate // Try fuzzy on the single-dash candidate (skip in strict mode)
if let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags) if !strict
&& let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags)
&& score >= FUZZY_FLAG_THRESHOLD && score >= FUZZY_FLAG_THRESHOLD
{ {
return Some(Correction { return Some(Correction {
@@ -415,8 +453,9 @@ fn try_correct(arg: &str, valid_flags: &[&str]) -> Option<Correction> {
}); });
} }
// Rule 3: Fuzzy flag match — `--staate` -> `--state` // Rule 3: Fuzzy flag match — `--staate` -> `--state` (skip in strict mode)
if let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags) if !strict
&& let Some((best_flag, score)) = best_fuzzy_match(&lower, valid_flags)
&& score >= FUZZY_FLAG_THRESHOLD && score >= FUZZY_FLAG_THRESHOLD
{ {
let corrected = match value_suffix { let corrected = match value_suffix {
@@ -510,7 +549,7 @@ mod tests {
#[test] #[test]
fn single_dash_robot() { fn single_dash_robot() {
let result = correct_args(args("lore -robot issues -n 5")); let result = correct_args(args("lore -robot issues -n 5"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].original, "-robot"); assert_eq!(result.corrections[0].original, "-robot");
assert_eq!(result.corrections[0].corrected, "--robot"); assert_eq!(result.corrections[0].corrected, "--robot");
@@ -523,7 +562,7 @@ mod tests {
#[test] #[test]
fn single_dash_state() { fn single_dash_state() {
let result = correct_args(args("lore --robot issues -state opened")); let result = correct_args(args("lore --robot issues -state opened"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--state"); assert_eq!(result.corrections[0].corrected, "--state");
} }
@@ -532,7 +571,7 @@ mod tests {
#[test] #[test]
fn case_robot() { fn case_robot() {
let result = correct_args(args("lore --Robot issues")); let result = correct_args(args("lore --Robot issues"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--robot"); assert_eq!(result.corrections[0].corrected, "--robot");
assert_eq!( assert_eq!(
@@ -543,7 +582,7 @@ mod tests {
#[test] #[test]
fn case_state_upper() { fn case_state_upper() {
let result = correct_args(args("lore --robot issues --State opened")); let result = correct_args(args("lore --robot issues --State opened"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--state"); assert_eq!(result.corrections[0].corrected, "--state");
assert_eq!( assert_eq!(
@@ -554,7 +593,7 @@ mod tests {
#[test] #[test]
fn case_all_upper() { fn case_all_upper() {
let result = correct_args(args("lore --ROBOT issues --STATE opened")); let result = correct_args(args("lore --ROBOT issues --STATE opened"), false);
assert_eq!(result.corrections.len(), 2); assert_eq!(result.corrections.len(), 2);
assert_eq!(result.corrections[0].corrected, "--robot"); assert_eq!(result.corrections[0].corrected, "--robot");
assert_eq!(result.corrections[1].corrected, "--state"); assert_eq!(result.corrections[1].corrected, "--state");
@@ -564,7 +603,7 @@ mod tests {
#[test] #[test]
fn fuzzy_staate() { fn fuzzy_staate() {
let result = correct_args(args("lore --robot issues --staate opened")); let result = correct_args(args("lore --robot issues --staate opened"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--state"); assert_eq!(result.corrections[0].corrected, "--state");
assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag); assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag);
@@ -572,7 +611,7 @@ mod tests {
#[test] #[test]
fn fuzzy_projct() { fn fuzzy_projct() {
let result = correct_args(args("lore --robot issues --projct group/repo")); let result = correct_args(args("lore --robot issues --projct group/repo"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--project"); assert_eq!(result.corrections[0].corrected, "--project");
assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag); assert_eq!(result.corrections[0].rule, CorrectionRule::FuzzyFlag);
@@ -583,7 +622,7 @@ mod tests {
#[test] #[test]
fn already_correct() { fn already_correct() {
let original = args("lore --robot issues --state opened -n 10"); let original = args("lore --robot issues --state opened -n 10");
let result = correct_args(original.clone()); let result = correct_args(original.clone(), false);
assert!(result.corrections.is_empty()); assert!(result.corrections.is_empty());
assert_eq!(result.args, original); assert_eq!(result.args, original);
} }
@@ -591,27 +630,27 @@ mod tests {
#[test] #[test]
fn short_flags_untouched() { fn short_flags_untouched() {
let original = args("lore -J issues -n 10 -s opened -p group/repo"); let original = args("lore -J issues -n 10 -s opened -p group/repo");
let result = correct_args(original.clone()); let result = correct_args(original.clone(), false);
assert!(result.corrections.is_empty()); assert!(result.corrections.is_empty());
} }
#[test] #[test]
fn stacked_short_flags_untouched() { fn stacked_short_flags_untouched() {
let original = args("lore -vvv issues"); let original = args("lore -vvv issues");
let result = correct_args(original.clone()); let result = correct_args(original.clone(), false);
assert!(result.corrections.is_empty()); assert!(result.corrections.is_empty());
} }
#[test] #[test]
fn positional_args_untouched() { fn positional_args_untouched() {
let result = correct_args(args("lore --robot search authentication")); let result = correct_args(args("lore --robot search authentication"), false);
assert!(result.corrections.is_empty()); assert!(result.corrections.is_empty());
} }
#[test] #[test]
fn wildly_wrong_flag_not_corrected() { fn wildly_wrong_flag_not_corrected() {
// `--xyzzy` shouldn't match anything above 0.8 // `--xyzzy` shouldn't match anything above 0.8
let result = correct_args(args("lore --robot issues --xyzzy foo")); let result = correct_args(args("lore --robot issues --xyzzy foo"), false);
assert!(result.corrections.is_empty()); assert!(result.corrections.is_empty());
} }
@@ -619,7 +658,7 @@ mod tests {
#[test] #[test]
fn flag_eq_value_case_correction() { fn flag_eq_value_case_correction() {
let result = correct_args(args("lore --robot issues --State=opened")); let result = correct_args(args("lore --robot issues --State=opened"), false);
assert_eq!(result.corrections.len(), 1); assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--state=opened"); assert_eq!(result.corrections[0].corrected, "--state=opened");
} }
@@ -628,15 +667,81 @@ mod tests {
#[test] #[test]
fn multiple_corrections() { fn multiple_corrections() {
let result = correct_args(args( let result = correct_args(
"lore -robot issues --State opened --projct group/repo", args("lore -robot issues --State opened --projct group/repo"),
)); false,
);
assert_eq!(result.corrections.len(), 3); assert_eq!(result.corrections.len(), 3);
assert_eq!(result.args[1], "--robot"); assert_eq!(result.args[1], "--robot");
assert_eq!(result.args[3], "--state"); assert_eq!(result.args[3], "--state");
assert_eq!(result.args[5], "--project"); assert_eq!(result.args[5], "--project");
} }
// ---- B1: POSIX -- option terminator ----
#[test]
fn option_terminator_stops_corrections() {
let result = correct_args(args("lore issues -- --staate --projct"), false);
// Nothing after `--` should be corrected
assert!(result.corrections.is_empty());
assert_eq!(result.args[2], "--");
assert_eq!(result.args[3], "--staate");
assert_eq!(result.args[4], "--projct");
}
#[test]
fn correction_before_terminator_still_works() {
let result = correct_args(args("lore --Robot issues -- --staate"), false);
assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--robot");
assert_eq!(result.args[4], "--staate"); // untouched after --
}
// ---- B2: Clap built-in flags not corrected ----
#[test]
fn version_flag_not_corrected() {
let result = correct_args(args("lore --version"), false);
assert!(result.corrections.is_empty());
assert_eq!(result.args[1], "--version");
}
#[test]
fn help_flag_not_corrected() {
let result = correct_args(args("lore --help"), false);
assert!(result.corrections.is_empty());
assert_eq!(result.args[1], "--help");
}
// ---- I6: Strict mode (robot) disables fuzzy matching ----
#[test]
fn strict_mode_disables_fuzzy() {
// Fuzzy match works in non-strict
let non_strict = correct_args(args("lore --robot issues --staate opened"), false);
assert_eq!(non_strict.corrections.len(), 1);
assert_eq!(non_strict.corrections[0].rule, CorrectionRule::FuzzyFlag);
// Fuzzy match disabled in strict
let strict = correct_args(args("lore --robot issues --staate opened"), true);
assert!(strict.corrections.is_empty());
}
#[test]
fn strict_mode_still_fixes_case() {
let result = correct_args(args("lore --Robot issues --State opened"), true);
assert_eq!(result.corrections.len(), 2);
assert_eq!(result.corrections[0].corrected, "--robot");
assert_eq!(result.corrections[1].corrected, "--state");
}
#[test]
fn strict_mode_still_fixes_single_dash() {
let result = correct_args(args("lore -robot issues"), true);
assert_eq!(result.corrections.len(), 1);
assert_eq!(result.corrections[0].corrected, "--robot");
}
// ---- Teaching notes ---- // ---- Teaching notes ----
#[test] #[test]

View File

@@ -53,8 +53,8 @@ pub struct SearchCliFilters {
pub project: Option<String>, pub project: Option<String>,
pub labels: Vec<String>, pub labels: Vec<String>,
pub path: Option<String>, pub path: Option<String>,
pub after: Option<String>, pub since: Option<String>,
pub updated_after: Option<String>, pub updated_since: Option<String>,
pub limit: usize, pub limit: usize,
} }
@@ -63,22 +63,36 @@ pub fn run_search(
query: &str, query: &str,
cli_filters: SearchCliFilters, cli_filters: SearchCliFilters,
fts_mode: FtsQueryMode, fts_mode: FtsQueryMode,
requested_mode: &str,
explain: bool, explain: bool,
) -> Result<SearchResponse> { ) -> Result<SearchResponse> {
let db_path = get_db_path(config.storage.db_path.as_deref()); let db_path = get_db_path(config.storage.db_path.as_deref());
let conn = create_connection(&db_path)?; let conn = create_connection(&db_path)?;
let mut warnings: Vec<String> = Vec::new();
// Determine actual mode: vector search requires embeddings, which need async + Ollama.
// Until hybrid/semantic are wired up, we run lexical and warn if the user asked for more.
let actual_mode = "lexical";
if requested_mode != "lexical" {
warnings.push(format!(
"Requested mode '{}' is not yet available; falling back to lexical search.",
requested_mode
));
}
let doc_count: i64 = conn let doc_count: i64 = conn
.query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0)) .query_row("SELECT COUNT(*) FROM documents", [], |row| row.get(0))
.unwrap_or(0); .unwrap_or(0);
if doc_count == 0 { if doc_count == 0 {
warnings.push("No documents indexed. Run 'lore generate-docs' first.".to_string());
return Ok(SearchResponse { return Ok(SearchResponse {
query: query.to_string(), query: query.to_string(),
mode: "lexical".to_string(), mode: actual_mode.to_string(),
total_results: 0, total_results: 0,
results: vec![], results: vec![],
warnings: vec!["No documents indexed. Run 'lore generate-docs' first.".to_string()], warnings,
}); });
} }
@@ -93,25 +107,25 @@ pub fn run_search(
.map(|p| resolve_project(&conn, p)) .map(|p| resolve_project(&conn, p))
.transpose()?; .transpose()?;
let after = cli_filters let since = cli_filters
.after .since
.as_deref() .as_deref()
.map(|s| { .map(|s| {
parse_since(s).ok_or_else(|| { parse_since(s).ok_or_else(|| {
LoreError::Other(format!( LoreError::Other(format!(
"Invalid --after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.", "Invalid --since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
s s
)) ))
}) })
}) })
.transpose()?; .transpose()?;
let updated_after = cli_filters let updated_since = cli_filters
.updated_after .updated_since
.as_deref() .as_deref()
.map(|s| { .map(|s| {
parse_since(s).ok_or_else(|| { parse_since(s).ok_or_else(|| {
LoreError::Other(format!( LoreError::Other(format!(
"Invalid --updated-after value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.", "Invalid --updated-since value '{}'. Use relative (7d, 2w, 1m) or absolute (YYYY-MM-DD) format.",
s s
)) ))
}) })
@@ -130,8 +144,8 @@ pub fn run_search(
source_type, source_type,
author: cli_filters.author, author: cli_filters.author,
project_id, project_id,
after, since,
updated_after, updated_since,
labels: cli_filters.labels, labels: cli_filters.labels,
path, path,
limit: cli_filters.limit, limit: cli_filters.limit,
@@ -163,10 +177,10 @@ pub fn run_search(
if filtered_ids.is_empty() { if filtered_ids.is_empty() {
return Ok(SearchResponse { return Ok(SearchResponse {
query: query.to_string(), query: query.to_string(),
mode: "lexical".to_string(), mode: actual_mode.to_string(),
total_results: 0, total_results: 0,
results: vec![], results: vec![],
warnings: vec![], warnings,
}); });
} }
@@ -210,10 +224,10 @@ pub fn run_search(
Ok(SearchResponse { Ok(SearchResponse {
query: query.to_string(), query: query.to_string(),
mode: "lexical".to_string(), mode: actual_mode.to_string(),
total_results: results.len(), total_results: results.len(),
results, results,
warnings: vec![], warnings,
}) })
} }

View File

@@ -10,6 +10,11 @@ use std::io::IsTerminal;
#[command(name = "lore")] #[command(name = "lore")]
#[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)] #[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)]
#[command(subcommand_required = false)] #[command(subcommand_required = false)]
#[command(after_long_help = "\x1b[1mEnvironment:\x1b[0m
GITLAB_TOKEN GitLab personal access token (or name set in config)
LORE_ROBOT Enable robot/JSON mode (non-empty, non-zero value)
LORE_CONFIG_PATH Override config file location
NO_COLOR Disable color output (any non-empty value)")]
pub struct Cli { pub struct Cli {
/// Path to config file /// Path to config file
#[arg(short = 'c', long, global = true, help = "Path to config file")] #[arg(short = 'c', long, global = true, help = "Path to config file")]
@@ -541,13 +546,13 @@ pub struct SearchArgs {
#[arg(long, help_heading = "Filters")] #[arg(long, help_heading = "Filters")]
pub path: Option<String>, pub path: Option<String>,
/// Filter by created after (7d, 2w, or YYYY-MM-DD) /// Filter by created since (7d, 2w, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")] #[arg(long, help_heading = "Filters")]
pub after: Option<String>, pub since: Option<String>,
/// Filter by updated after (7d, 2w, or YYYY-MM-DD) /// Filter by updated since (7d, 2w, or YYYY-MM-DD)
#[arg(long = "updated-after", help_heading = "Filters")] #[arg(long = "updated-since", help_heading = "Filters")]
pub updated_after: Option<String>, pub updated_since: Option<String>,
/// Maximum results (default 20, max 100) /// Maximum results (default 20, max 100)
#[arg( #[arg(

View File

@@ -52,7 +52,7 @@ async fn main() {
// Phase 1.5: Pre-clap arg correction for agent typo tolerance // Phase 1.5: Pre-clap arg correction for agent typo tolerance
let raw_args: Vec<String> = std::env::args().collect(); let raw_args: Vec<String> = std::env::args().collect();
let correction_result = autocorrect::correct_args(raw_args); let correction_result = autocorrect::correct_args(raw_args, robot_mode_early);
// Emit correction warnings to stderr (before clap parsing, so they appear // Emit correction warnings to stderr (before clap parsing, so they appear
// even if clap still fails on something else) // even if clap still fails on something else)
@@ -142,12 +142,17 @@ async fn main() {
} }
} }
match cli.color.as_str() { // I1: Respect NO_COLOR convention (https://no-color.org/)
"never" => console::set_colors_enabled(false), if std::env::var("NO_COLOR").is_ok_and(|v| !v.is_empty()) {
"always" => console::set_colors_enabled(true), console::set_colors_enabled(false);
"auto" => {} } else {
other => { match cli.color.as_str() {
eprintln!("Warning: unknown color mode '{}', using auto", other); "never" => console::set_colors_enabled(false),
"always" => console::set_colors_enabled(true),
"auto" => {}
other => {
eprintln!("Warning: unknown color mode '{}', using auto", other);
}
} }
} }
@@ -451,11 +456,13 @@ fn handle_clap_error(e: clap::Error, robot_mode: bool, corrections: &CorrectionR
if robot_mode { if robot_mode {
let error_code = map_clap_error_kind(e.kind()); let error_code = map_clap_error_kind(e.kind());
let message = e let full_msg = e.to_string();
.to_string() let message = full_msg
.lines() .lines()
.next() .take(3)
.unwrap_or("Parse error") .collect::<Vec<_>>()
.join("; ")
.trim()
.to_string(); .to_string();
let (suggestion, correction, valid_values) = match e.kind() { let (suggestion, correction, valid_values) = match e.kind() {
@@ -684,10 +691,11 @@ fn handle_issues(
print_show_issue(&result); print_show_issue(&result);
} }
} else { } else {
let state_normalized = args.state.as_deref().map(str::to_lowercase);
let filters = ListFilters { let filters = ListFilters {
limit: args.limit, limit: args.limit,
project: args.project.as_deref(), project: args.project.as_deref(),
state: args.state.as_deref(), state: state_normalized.as_deref(),
author: args.author.as_deref(), author: args.author.as_deref(),
assignee: args.assignee.as_deref(), assignee: args.assignee.as_deref(),
labels: args.label.as_deref(), labels: args.label.as_deref(),
@@ -736,10 +744,11 @@ fn handle_mrs(
print_show_mr(&result); print_show_mr(&result);
} }
} else { } else {
let state_normalized = args.state.as_deref().map(str::to_lowercase);
let filters = MrListFilters { let filters = MrListFilters {
limit: args.limit, limit: args.limit,
project: args.project.as_deref(), project: args.project.as_deref(),
state: args.state.as_deref(), state: state_normalized.as_deref(),
author: args.author.as_deref(), author: args.author.as_deref(),
assignee: args.assignee.as_deref(), assignee: args.assignee.as_deref(),
reviewer: args.reviewer.as_deref(), reviewer: args.reviewer.as_deref(),
@@ -1714,13 +1723,20 @@ async fn handle_search(
project: args.project, project: args.project,
labels: args.label, labels: args.label,
path: args.path, path: args.path,
after: args.after, since: args.since,
updated_after: args.updated_after, updated_since: args.updated_since,
limit: args.limit, limit: args.limit,
}; };
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let response = run_search(&config, &args.query, cli_filters, fts_mode, explain)?; let response = run_search(
&config,
&args.query,
cli_filters,
fts_mode,
&args.mode,
explain,
)?;
let elapsed_ms = start.elapsed().as_millis() as u64; let elapsed_ms = start.elapsed().as_millis() as u64;
if robot_mode { if robot_mode {
@@ -1895,6 +1911,8 @@ struct HealthData {
db_found: bool, db_found: bool,
schema_current: bool, schema_current: bool,
schema_version: i32, schema_version: i32,
#[serde(skip_serializing_if = "Vec::is_empty")]
actions: Vec<String>,
} }
async fn handle_health( async fn handle_health(
@@ -1929,6 +1947,17 @@ async fn handle_health(
let healthy = config_found && db_found && schema_current; let healthy = config_found && db_found && schema_current;
let mut actions = Vec::new();
if !config_found {
actions.push("lore init".to_string());
}
if !db_found && config_found {
actions.push("lore sync".to_string());
}
if db_found && !schema_current {
actions.push("lore migrate".to_string());
}
if robot_mode { if robot_mode {
let output = HealthOutput { let output = HealthOutput {
ok: true, ok: true,
@@ -1938,6 +1967,7 @@ async fn handle_health(
db_found, db_found,
schema_current, schema_current,
schema_version, schema_version,
actions,
}, },
meta: RobotMeta { meta: RobotMeta {
elapsed_ms: start.elapsed().as_millis() as u64, elapsed_ms: start.elapsed().as_millis() as u64,
@@ -2111,7 +2141,7 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
}, },
"search": { "search": {
"description": "Search indexed documents (lexical, hybrid, semantic)", "description": "Search indexed documents (lexical, hybrid, semantic)",
"flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--after", "--updated-after", "-n/--limit", "--explain", "--no-explain", "--fts-mode"], "flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--since", "--updated-since", "-n/--limit", "--explain", "--no-explain", "--fts-mode"],
"example": "lore --robot search 'authentication bug' --mode hybrid --limit 10", "example": "lore --robot search 'authentication bug' --mode hybrid --limit 10",
"response_schema": { "response_schema": {
"ok": "bool", "ok": "bool",
@@ -2385,12 +2415,13 @@ async fn handle_list_compat(
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let config = Config::load(config_override)?; let config = Config::load(config_override)?;
let state_normalized = state_filter.map(str::to_lowercase);
match entity { match entity {
"issues" => { "issues" => {
let filters = ListFilters { let filters = ListFilters {
limit, limit,
project: project_filter, project: project_filter,
state: state_filter, state: state_normalized.as_deref(),
author: author_filter, author: author_filter,
assignee: assignee_filter, assignee: assignee_filter,
labels: label_filter, labels: label_filter,
@@ -2418,7 +2449,7 @@ async fn handle_list_compat(
let filters = MrListFilters { let filters = MrListFilters {
limit, limit,
project: project_filter, project: project_filter,
state: state_filter, state: state_normalized.as_deref(),
author: author_filter, author: author_filter,
assignee: assignee_filter, assignee: assignee_filter,
reviewer: reviewer_filter, reviewer: reviewer_filter,

View File

@@ -16,8 +16,8 @@ pub struct SearchFilters {
pub source_type: Option<SourceType>, pub source_type: Option<SourceType>,
pub author: Option<String>, pub author: Option<String>,
pub project_id: Option<i64>, pub project_id: Option<i64>,
pub after: Option<i64>, pub since: Option<i64>,
pub updated_after: Option<i64>, pub updated_since: Option<i64>,
pub labels: Vec<String>, pub labels: Vec<String>,
pub path: Option<PathFilter>, pub path: Option<PathFilter>,
pub limit: usize, pub limit: usize,
@@ -28,8 +28,8 @@ impl SearchFilters {
self.source_type.is_some() self.source_type.is_some()
|| self.author.is_some() || self.author.is_some()
|| self.project_id.is_some() || self.project_id.is_some()
|| self.after.is_some() || self.since.is_some()
|| self.updated_after.is_some() || self.updated_since.is_some()
|| !self.labels.is_empty() || !self.labels.is_empty()
|| self.path.is_some() || self.path.is_some()
} }
@@ -85,15 +85,15 @@ pub fn apply_filters(
param_idx += 1; param_idx += 1;
} }
if let Some(after) = filters.after { if let Some(since) = filters.since {
sql.push_str(&format!(" AND d.created_at >= ?{}", param_idx)); sql.push_str(&format!(" AND d.created_at >= ?{}", param_idx));
params.push(Box::new(after)); params.push(Box::new(since));
param_idx += 1; param_idx += 1;
} }
if let Some(updated_after) = filters.updated_after { if let Some(updated_since) = filters.updated_since {
sql.push_str(&format!(" AND d.updated_at >= ?{}", param_idx)); sql.push_str(&format!(" AND d.updated_at >= ?{}", param_idx));
params.push(Box::new(updated_after)); params.push(Box::new(updated_since));
param_idx += 1; param_idx += 1;
} }