Skip to main content

memstead_cli/commands/
check.rs

1//! `memstead check` — record a check of one entity (agent-trust
2//! plan 14).
3//!
4//! Mirrors the MCP `memstead_check` tool 1:1. A check is the
5//! engine-recorded act of verification: verdict from the closed
6//! vocabulary (`ok` | `failed`), optional method note, plan-13
7//! provenance (actor, client, the session's `--role`), and the
8//! entity's `content_hash` at check time — appended to the
9//! workspace's append-only check ledger. Checking mutates nothing:
10//! no entity write, no mem commit. Derived check state is served by
11//! `memstead entity <id> --provenance`.
12
13use clap::Parser;
14use memstead_base::EntityId;
15use memstead_base::check::{CHECK_KINDS, CheckKind, VERDICTS, Verdict};
16use memstead_base::vcs::Actor;
17
18use crate::CliError;
19use crate::output::{ExitKind, print_json, print_markdown};
20use crate::setup::CliContext;
21
22#[derive(Parser, Debug)]
23pub struct Args {
24    /// Full entity id (`mem--slug`) of the entity that was checked.
25    pub id: String,
26
27    /// The verdict: `ok` | `failed`. The vocabulary is closed —
28    /// nuance goes in `--method` or in process-mem entities.
29    #[arg(long)]
30    pub verdict: String,
31
32    /// Free-text method note — how the check was performed. For a
33    /// conformance check, name the judging model here.
34    #[arg(long)]
35    pub method: Option<String>,
36
37    /// The check kind: `verification` (default — "I checked this
38    /// entity's content") | `conformance` (a semantic judgment
39    /// against the type's schema prose; the engine stamps the mem's
40    /// schema pin into the record, and the verdict goes stale when
41    /// the content hash moves OR the pin changes). The vocabulary is
42    /// closed.
43    #[arg(long)]
44    pub kind: Option<String>,
45}
46
47pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
48    let Some(verdict) = Verdict::from_wire(&args.verdict) else {
49        return Err(CliError::new(
50            ExitKind::Validation,
51            "INVALID_VERDICT",
52            format!(
53                "unknown verdict {:?} — the vocabulary is: {}",
54                args.verdict,
55                VERDICTS.join(", ")
56            ),
57        )
58        .into());
59    };
60    let kind = match args.kind.as_deref() {
61        None => CheckKind::Verification,
62        Some(s) => CheckKind::from_wire(s).ok_or_else(|| {
63            CliError::new(
64                ExitKind::Validation,
65                "INVALID_CHECK_KIND",
66                format!(
67                    "unknown check kind {s:?} — the vocabulary is: {}",
68                    CHECK_KINDS.join(", ")
69                ),
70            )
71        })?,
72    };
73    let id = EntityId::canonical(&args.id);
74    let mut engine = ctx.cli_engine()?.into_base();
75    let client = crate::setup::cli_client_id();
76    let record = engine
77        .record_check(
78            id.mem(),
79            id.as_ref(),
80            verdict,
81            kind,
82            args.method.as_deref(),
83            Actor::Cli,
84            Some(&client),
85        )
86        .map_err(CliError::from_engine_op)?;
87    let (state, _) = match kind {
88        CheckKind::Verification => engine.entity_check_state(id.mem(), id.as_ref()),
89        CheckKind::Conformance => engine.entity_conformance_state(id.mem(), id.as_ref()),
90    }
91    .map_err(CliError::from_engine_op)?;
92    if ctx.json {
93        print_json(&serde_json::json!({
94            "entity": record.entity,
95            "verdict": record.verdict,
96            "check_state": state.as_str(),
97            "kind": record.kind.as_deref().unwrap_or("verification"),
98            "schema_ref": record.schema_ref,
99            "role": record.role,
100            "ts": record.ts,
101            "method": record.method,
102        }))?;
103        return Ok(());
104    }
105    print_markdown(&format!(
106        "Check recorded: `{}` — kind `{}`, verdict **{}**, state `{}` (role: {})",
107        record.entity,
108        record.kind.as_deref().unwrap_or("verification"),
109        record.verdict,
110        state.as_str(),
111        record.role
112    ));
113    Ok(())
114}