use crate::style;
use crate::support::util::exit_code;
pub(crate) mod audit_history;
pub(crate) mod drain;
mod embedding_degradation;
pub(crate) mod fix;
pub(crate) mod labels;
pub(crate) mod memory_snapshot;
pub(crate) mod probes;
pub(crate) mod report;
pub(crate) mod table;
mod util;
#[derive(Debug)]
pub(crate) struct DoctorArgs {
pub report: Option<String>,
pub fix: bool,
pub drain_abandoned: bool,
pub older_than: String,
pub no_dry_run: bool,
pub json: bool,
}
pub(crate) async fn handle_doctor(ctx: &crate::runtime::CommandContext, args: DoctorArgs) {
let DoctorArgs {
report,
fix: fix_mode,
drain_abandoned,
older_than,
no_dry_run,
json,
} = args;
if drain_abandoned {
let cutoff = match drain::parse_older_than(&older_than) {
Ok(d) => d,
Err(msg) => {
style::report_error(&msg, "", &[]);
exit_code(2);
}
};
let dry_run = !no_dry_run;
match drain::run_drain(ctx, cutoff, dry_run).await {
Ok(outcome) => drain::render_outcome(&outcome, json),
Err(msg) => {
style::report_error(&msg, "", &[]);
exit_code(1);
}
}
return;
}
if let Some(report_target) = report {
let md = report::build_doctor_report(ctx).await;
write_report(&report_target, &md);
} else {
let rendered = table::render_table(ctx).await;
print!("{rendered}");
if let Some(warning) = slow_drain_warning(ctx).await {
println!();
println!(" {warning}");
}
if fix_mode {
fix::run_fix_pass();
} else if fix::has_fixable() {
println!();
println!(
" {} {} {} {}",
style::emerald(style::sym::TIP),
style::pewter("Run"),
style::cmd("difflore doctor --fix"),
style::pewter("to auto-repair these."),
);
}
}
}
fn write_report(target: &str, md: &str) {
if target == "-" {
print!("{md}");
return;
}
let path = if target.is_empty() {
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
let dir = match difflore_core::infra::paths::data_home() {
Ok(d) => d.join("reports"),
Err(_) => std::path::PathBuf::from("."),
};
dir.join(format!("difflore-bug-report-{ts}.md"))
} else {
std::path::PathBuf::from(target)
};
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = std::fs::create_dir_all(parent)
{
style::report_error(
&format!("Failed to create reports dir {}: {e}", parent.display()),
"",
&[],
);
exit_code(1);
}
match std::fs::write(&path, md) {
Ok(()) => println!(
"{} Bug report written to {}",
style::emerald(style::sym::OK),
style::ident(&path.display().to_string())
),
Err(e) => {
style::report_error(&format!("Failed to write report: {e}"), "", &[]);
exit_code(1);
}
}
}
const SLOW_DRAIN_CLOUD_THRESHOLD: i64 = 500;
const SLOW_DRAIN_OBSERVATION_THRESHOLD: i64 = 200;
async fn slow_drain_warning(ctx: &crate::runtime::CommandContext) -> Option<String> {
use difflore_core::cloud::observations::ObservationEmitter;
use difflore_core::cloud::outbox::OutboxQueue;
let outbox = OutboxQueue::new(ctx.db.clone());
let cloud_counts = outbox.pending_counts_by_kind().await.ok()?;
let cloud_total: i64 = cloud_counts.iter().map(|(_, n)| *n).sum();
let obs_pending = match ObservationEmitter::open_default().await {
Ok(e) => e.pending_upload_count().await.unwrap_or(0),
Err(_) => 0,
};
let cloud_hot = cloud_total > SLOW_DRAIN_CLOUD_THRESHOLD;
let obs_hot = obs_pending > SLOW_DRAIN_OBSERVATION_THRESHOLD;
if !cloud_hot && !obs_hot {
return None;
}
let mut parts = Vec::new();
if cloud_hot {
parts.push(format!("{cloud_total} cloud upload{}", plural(cloud_total)));
}
if obs_hot && obs_pending > 0 {
parts.push(format!("{obs_pending} agent event{}", plural(obs_pending)));
}
let command = if cloud_hot {
"run `difflore cloud sync --include-observations --include-candidates --include-telemetry` (raw queues are opt-in)"
} else {
"run `difflore cloud sync`"
};
Some(format!(
"{} {} {}; {command}; if it stays queued, attach `difflore doctor --report`.",
style::amber(style::sym::WARN),
style::pewter("upload queue:"),
parts.join(" + "),
))
}
const fn plural(n: i64) -> &'static str {
if n == 1 { "" } else { "s" }
}