use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Result;
use clap::Args;
use mati_core::analysis::onboarding;
use mati_core::analysis::walker::Walker;
use mati_core::store::record::Record;
use crate::cli::proxy::StoreProxy;
const CODEOWNERS_LOCATIONS: &[&str] = &[
"CODEOWNERS",
".github/CODEOWNERS",
"docs/CODEOWNERS",
".gitlab/CODEOWNERS",
];
const MAX_SCAN_FILE_BYTES: u64 = 512 * 1024;
#[derive(Args)]
#[command(
long_about = "Propose gotcha candidates from existing repo artifacts (CODEOWNERS, \
load-bearing/security marker comments). Candidates are unconfirmed and \
surface in `mati review` for approval. Re-runnable; never overwrites \
existing records."
)]
pub struct SuggestArgs {
#[arg(short, long)]
pub path: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
}
pub async fn run(args: SuggestArgs) -> Result<()> {
let root = match args.path {
Some(p) => p,
None => std::env::current_dir()?,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let device_id = mati_core::store::stable_device_id();
let codeowners = read_codeowners(&root);
let files = read_text_files(&root);
let candidates = onboarding::build_candidates(codeowners.as_deref(), &files, device_id, 1, now);
if candidates.is_empty() {
println!(
"No onboarding candidates found (scanned CODEOWNERS + load-bearing/security markers)."
);
return Ok(());
}
if args.dry_run {
println!("Would propose {} candidate(s):", candidates.len());
for c in &candidates {
println!(" {}\n {}", c.key, c.value);
}
return Ok(());
}
let proxy = StoreProxy::open(&root).await?;
let outcome = write_candidates(&proxy, &candidates).await;
let (written, skipped) = proxy.close_with_result(outcome).await?;
if written == 0 {
println!("All {skipped} candidate(s) already present — nothing new to propose.");
} else {
let tail = if skipped > 0 {
format!(" ({skipped} already present)")
} else {
String::new()
};
println!("Proposed {written} new candidate(s){tail} — run `mati review` to approve.");
}
Ok(())
}
async fn write_candidates(proxy: &StoreProxy, candidates: &[Record]) -> Result<(usize, usize)> {
let mut written = 0;
let mut skipped = 0;
for rec in candidates {
if proxy.get(&rec.key).await?.is_some() {
skipped += 1;
continue;
}
proxy.put(&rec.key, rec).await?;
written += 1;
}
Ok((written, skipped))
}
pub(crate) fn read_codeowners(root: &Path) -> Option<String> {
CODEOWNERS_LOCATIONS
.iter()
.find_map(|loc| std::fs::read_to_string(root.join(loc)).ok())
}
fn read_text_files(root: &Path) -> Vec<(String, String)> {
let Ok(files) = Walker::new(root).walk() else {
return Vec::new();
};
files
.into_iter()
.filter(|f| f.size_bytes <= MAX_SCAN_FILE_BYTES)
.filter_map(|f| {
std::fs::read_to_string(&f.abs_path)
.ok()
.map(|c| (f.rel_path, c))
})
.collect()
}