feat(who): expand expert + overlap queries with mr_file_changes and mr_reviewers

Chain: bd-jec (config flag) -> bd-2yo (fetch MR diffs) -> bd-3qn6 (rewrite who queries)

- Add fetch_mr_file_changes config option and --no-file-changes CLI flag
- Add GitLab MR diffs API fetch pipeline with watermark-based sync
- Create migration 020 for diffs_synced_for_updated_at watermark column
- Rewrite query_expert() and query_overlap() to use 4-signal UNION ALL:
  DiffNote reviewers, DiffNote MR authors, file-change authors, file-change reviewers
- Deduplicate across signal types via COUNT(DISTINCT CASE WHEN ... THEN mr_id END)
- Add insert_file_change test helper, 8 new who tests, all 397 tests pass
- Also includes: list performance migration 019, autocorrect module, README updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-08 13:35:14 -05:00
parent 435a208c93
commit 95b7183add
19 changed files with 2139 additions and 291 deletions

View File

@@ -1,3 +1,4 @@
pub mod autocorrect;
pub mod commands;
pub mod progress;
pub mod robot;
@@ -81,13 +82,18 @@ impl Cli {
/// Detect robot mode from environment before parsing succeeds.
/// Used for structured error output when clap parsing fails.
/// Also catches common agent typos like `-robot` and `--Robot`.
pub fn detect_robot_mode_from_env() -> bool {
let args: Vec<String> = std::env::args().collect();
args.iter()
.any(|a| a == "--robot" || a == "-J" || a == "--json")
|| std::env::var("LORE_ROBOT")
.ok()
.is_some_and(|v| !v.is_empty() && v != "0" && v != "false")
args.iter().any(|a| {
a == "-J"
|| a.eq_ignore_ascii_case("--robot")
|| a.eq_ignore_ascii_case("-robot")
|| a.eq_ignore_ascii_case("--json")
|| a.eq_ignore_ascii_case("-json")
}) || std::env::var("LORE_ROBOT")
.ok()
.is_some_and(|v| !v.is_empty() && v != "0" && v != "false")
|| !std::io::stdout().is_terminal()
}
}
@@ -608,6 +614,10 @@ pub struct SyncArgs {
#[arg(long = "no-events")]
pub no_events: bool,
/// Skip MR file change fetching (overrides config)
#[arg(long = "no-file-changes")]
pub no_file_changes: bool,
/// Preview what would be synced without making changes
#[arg(long, overrides_with = "no_dry_run")]
pub dry_run: bool,