use clap::Parser;
use serde_json::json;
use crate::CliError;
use crate::output::{print_json, print_markdown};
use crate::setup::CliContext;
#[derive(Parser, Debug)]
pub struct Args {
#[arg(long = "mem", value_name = "NAME")]
pub mem_name: String,
}
pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
let mut engine = ctx.cli_engine()?.into_base();
let report = engine
.verify_mem_anchors(&args.mem_name)
.map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
let backfill: Vec<memstead_base::anchor::ObservedArtifactHash> = report
.anchors
.iter()
.filter(|a| {
a.state == "recheck"
&& a.observed_hash.is_some()
&& matches!(a.class.as_str(), "anchored" | "derived")
})
.map(|a| memstead_base::anchor::ObservedArtifactHash {
entity: a.entity_id.clone(),
artifact: a.artifact.clone(),
hash: a.observed_hash.clone().expect("filtered on Some"),
})
.collect();
let backfilled = engine
.record_anchor_observed_hashes(
&args.mem_name,
&backfill,
Some("verify-anchors: first-observation hash backfill"),
)
.map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
let persisted = engine
.workspace_root()
.map(|root| {
memstead_base::ingest::findings::record_standalone_findings(root, &report).map_err(
|e| {
anyhow::Error::from(CliError::new(
crate::output::ExitKind::Generic,
"FINDINGS_STORE_ERROR",
e.to_string(),
))
},
)
})
.transpose()?;
if ctx.json {
let findings = persisted.as_ref().map(|fs| {
json!({
"new": fs.iter().filter(|f| !f.already_seen).count(),
"already_seen": fs.iter().filter(|f| f.already_seen).count(),
"items": fs,
})
});
print_json(&json!({
"verdict_coverage": crate::coverage::VERIFY_ANCHORS
.axis_coverage()
.expect("verify-anchors is a verdict surface")
.wire_line(),
"mem": report.mem,
"resolved": report.resolved,
"drifted": report.drifted,
"recheck": report.recheck,
"unresolvable": report.unresolvable,
"unobserved": report.unobserved,
"dangling": report.dangling,
"population": report.population_statement(),
"fully_adjudicated": report.fully_adjudicated(),
"entity_end_unreconciled": report.unreconciled,
"anchors": report.anchors,
"hash_backfilled": backfilled,
"findings": findings,
}))?;
} else {
let mut out = format!(
"# Anchor verification — `{}`\n\n- Resolved: {}\n- Drifted: {}\n- Recheck: {}\n\
- Unresolvable (artifact gone): {}\n- Unobserved (not measured this pass): {}\n\
- Dangling (entity gone): {}\n- Population: {}\n",
report.mem,
report.resolved,
report.drifted,
report.recheck,
report.unresolvable,
report.unobserved,
report.dangling,
report.population_statement(),
);
if let Some(cov) = crate::coverage::VERIFY_ANCHORS.axis_coverage() {
out.push_str(&format!("- Verdict coverage: {}\n", cov.wire_line()));
}
if let Some(why) = &report.unreconciled {
out.push_str(&format!(
"\n> **Entity end not reconciled** — {why}. Dangling rows would not have been \
detected, so the counts above describe the artifact end only.\n"
));
}
if report.anchors.is_empty() {
out.push_str("\n_(no anchors in this mem)_\n");
} else {
let flagged: Vec<_> = report
.anchors
.iter()
.filter(|a| a.state != "resolved")
.collect();
if !flagged.is_empty() {
out.push_str("\n## Flagged anchors\n\n");
for a in flagged {
out.push_str(&format!(
"- **{}**: `{}` → `{}` ({} {})\n",
a.state, a.entity_id, a.artifact, a.class, a.grain,
));
}
}
}
if backfilled > 0 {
out.push_str(&format!(
"\nBackfilled {backfilled} observed hash(es) onto hash-less anchors — the \
recheck queue drains on the next pass.\n"
));
}
match &persisted {
Some(fs) => {
let new = fs.iter().filter(|f| !f.already_seen).count();
let seen = fs.len() - new;
out.push_str(&format!(
"\nFindings persisted (standalone store): {new} new, {seen} already seen.\n"
));
}
None => out.push_str("\n_Findings not persisted — engine has no workspace root._\n"),
}
print_markdown(&out);
}
Ok(())
}