memstead_cli/commands/
verify_anchors.rs1use clap::Parser;
7use serde_json::json;
8
9use crate::CliError;
10use crate::output::{print_json, print_markdown};
11use crate::setup::CliContext;
12
13#[derive(Parser, Debug)]
23pub struct Args {
24 #[arg(long = "mem", value_name = "NAME")]
26 pub mem_name: String,
27}
28
29pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
30 let cli_engine = ctx.cli_engine()?;
31 let engine = cli_engine.base();
32
33 let report = engine
34 .verify_mem_anchors(&args.mem_name)
35 .map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
36
37 let persisted = engine
44 .workspace_root()
45 .map(|root| {
46 memstead_base::ingest::findings::record_standalone_findings(root, &report).map_err(
47 |e| {
48 anyhow::Error::from(CliError::new(
49 crate::output::ExitKind::Generic,
50 "FINDINGS_STORE_ERROR",
51 e.to_string(),
52 ))
53 },
54 )
55 })
56 .transpose()?;
57
58 if ctx.json {
59 let findings = persisted.as_ref().map(|fs| {
60 json!({
61 "new": fs.iter().filter(|f| !f.already_seen).count(),
62 "already_seen": fs.iter().filter(|f| f.already_seen).count(),
63 "items": fs,
64 })
65 });
66 print_json(&json!({
67 "mem": report.mem,
68 "resolved": report.resolved,
69 "drifted": report.drifted,
70 "recheck": report.recheck,
71 "unresolvable": report.unresolvable,
72 "anchors": report.anchors,
73 "findings": findings,
74 }))?;
75 } else {
76 let mut out = format!(
77 "# Anchor verification — `{}`\n\n- Resolved: {}\n- Drifted: {}\n- Recheck: {}\n- Unresolvable: {}\n",
78 report.mem, report.resolved, report.drifted, report.recheck, report.unresolvable,
79 );
80 if report.anchors.is_empty() {
81 out.push_str("\n_(no anchors in this mem)_\n");
82 } else {
83 let flagged: Vec<_> = report
87 .anchors
88 .iter()
89 .filter(|a| a.state != "resolved")
90 .collect();
91 if !flagged.is_empty() {
92 out.push_str("\n## Flagged anchors\n\n");
93 for a in flagged {
94 out.push_str(&format!(
95 "- **{}**: `{}` → `{}` ({} {})\n",
96 a.state, a.entity_id, a.artifact, a.class, a.grain,
97 ));
98 }
99 }
100 }
101 match &persisted {
102 Some(fs) => {
103 let new = fs.iter().filter(|f| !f.already_seen).count();
104 let seen = fs.len() - new;
105 out.push_str(&format!(
106 "\nFindings persisted (standalone store): {new} new, {seen} already seen.\n"
107 ));
108 }
109 None => out.push_str("\n_Findings not persisted — engine has no workspace root._\n"),
110 }
111 print_markdown(&out);
112 }
113 Ok(())
114}