use clap::Parser;
use memstead_base::vcs::Actor;
use crate::CliError;
use crate::output::{print_json, print_markdown};
use crate::setup::CliContext;
#[derive(Parser, Debug)]
pub struct Args {
#[arg(long)]
pub note: Option<String>,
}
pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
let mut engine = crate::setup::pro_engine(ctx)?;
let note_ref = args.note.as_deref();
let report = engine
.apply_parse_recovery(Actor::Cli, None, note_ref)
.map_err(CliError::from_engine_op)?;
let (removed, skipped, failed) = recovery_counts(&report);
if ctx.json {
print_json(&recovery_json_envelope(&report))?;
return Ok(());
}
let mut lines = vec![
format!(
"# Parse-recovery — {} removed, {} skipped, {} failed",
removed, skipped, failed
),
String::new(),
];
if report.entries.is_empty() {
lines.push("_(workspace already clean — no parse-time drops)_".into());
} else {
for entry in &report.entries {
let marker = match entry.outcome.as_str() {
"removed" => "✓",
"skipped" => "·",
_ => "✗",
};
let reason = entry
.reason
.as_ref()
.map(|r| format!(" — {r}"))
.unwrap_or_default();
lines.push(format!(
"- {marker} `{}` `{}` → `{}` ({}){}",
entry.entity_id, entry.rel_type, entry.target, entry.outcome, reason,
));
}
if !report.commit_sha.is_empty() {
lines.push(String::new());
lines.push(format!("Last commit: `{}`", report.commit_sha));
}
}
print_markdown(&lines.join("\n"));
Ok(())
}
fn recovery_counts(report: &memstead_base::ops::ParseRecoveryReport) -> (usize, usize, usize) {
let removed = report
.entries
.iter()
.filter(|e| e.outcome == "removed")
.count();
let skipped = report
.entries
.iter()
.filter(|e| e.outcome == "skipped")
.count();
let failed = report
.entries
.iter()
.filter(|e| e.outcome == "failed")
.count();
(removed, skipped, failed)
}
fn recovery_json_envelope(report: &memstead_base::ops::ParseRecoveryReport) -> serde_json::Value {
let (removed, skipped, failed) = recovery_counts(report);
let mut obj = serde_json::json!({
"removed": removed,
"skipped": skipped,
"failed": failed,
"entries": report.entries,
});
if !report.commit_sha.is_empty() {
obj["commit_sha"] = serde_json::json!(report.commit_sha);
}
obj
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recovery_json_envelope_clean_workspace_carries_zero_counters_and_empty_entries() {
let report = memstead_base::ops::ParseRecoveryReport::default();
let json = recovery_json_envelope(&report);
assert_eq!(json["removed"], 0);
assert_eq!(json["skipped"], 0);
assert_eq!(json["failed"], 0);
assert_eq!(json["entries"], serde_json::json!([]));
assert!(json.get("commit_sha").is_none());
assert_ne!(json, serde_json::json!({}), "must not be the empty object");
}
}