Skip to main content

ai_memory/cli/commands/
reflection_origin.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! v0.7.0 ARCH-3 / FX-C3 (batch2) — `ai-memory reflection-origin`
5//! CLI subcommand.
6//!
7//! Closes the three-surface-parity gap on `memory_reflection_origin`
8//! (v0.7.0 L2-2 / S6-M1). The MCP tool
9//! ([`crate::mcp::handle_reflection_origin`]) and the HTTP route
10//! landed previously; this module wires the CLI surface so operators
11//! can inspect the cross-peer federation provenance of a reflection
12//! memory from a terminal.
13
14use crate::models::field_names;
15use anyhow::Result;
16use clap::Args;
17use serde_json::{Value, json};
18
19use crate::cli::CliOutput;
20use crate::storage as db;
21
22/// CLI args for `ai-memory reflection-origin`.
23#[derive(Args, Debug, Clone)]
24pub struct ReflectionOriginArgs {
25    /// Memory id whose origin to inspect.
26    #[arg(long = "memory-id", value_name = "ID")]
27    pub memory_id: String,
28
29    /// Emit the raw JSON envelope.
30    #[arg(long)]
31    pub json: bool,
32}
33
34/// `ai-memory reflection-origin` dispatch entry.
35///
36/// # Errors
37///
38/// - The DB at `db_path` cannot be opened.
39/// - The substrate refuses the call (validation, id not found).
40/// - `serde_json::to_string` cannot serialise the envelope.
41pub fn cmd_reflection_origin(
42    db_path: &std::path::Path,
43    args: &ReflectionOriginArgs,
44    out: &mut CliOutput<'_>,
45) -> Result<()> {
46    let conn = db::open(db_path)?;
47    let params = json!({"memory_id": args.memory_id});
48
49    let envelope = crate::mcp::handle_reflection_origin(&conn, &params)
50        .map_err(|e| anyhow::anyhow!("reflection-origin: {e}"))?;
51
52    if args.json {
53        writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
54        return Ok(());
55    }
56
57    let is_refl = envelope
58        .get(field_names::IS_REFLECTION)
59        .and_then(Value::as_bool)
60        .unwrap_or(false);
61    let peer = envelope
62        .get(field_names::PEER_ORIGIN)
63        .and_then(Value::as_str)
64        .unwrap_or("");
65    let depth = envelope
66        .get(field_names::ORIGINAL_DEPTH)
67        .and_then(Value::as_i64)
68        .unwrap_or(0);
69    writeln!(
70        out.stdout,
71        "reflection-origin: is_reflection={is_refl}  peer_origin={peer}  original_depth={depth}"
72    )?;
73    Ok(())
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::cli::test_utils::{TestEnv, seed_memory};
80
81    #[test]
82    fn reflection_origin_cli_known_id_returns_envelope() {
83        let mut env = TestEnv::fresh();
84        let db = env.db_path.clone();
85        let mid = seed_memory(&db, "ns", "plain", "body");
86        let args = ReflectionOriginArgs {
87            memory_id: mid,
88            json: true,
89        };
90        {
91            let mut out = env.output();
92            cmd_reflection_origin(&db, &args, &mut out).expect("ok");
93        }
94        let stdout = env.stdout_str();
95        let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
96        // Plain (non-reflection) memory → envelope with is_reflection=false.
97        assert_eq!(envelope["is_reflection"].as_bool(), Some(false));
98    }
99
100    #[test]
101    fn reflection_origin_cli_empty_id_returns_err() {
102        let mut env = TestEnv::fresh();
103        let db = env.db_path.clone();
104        let args = ReflectionOriginArgs {
105            memory_id: String::new(),
106            json: true,
107        };
108        let mut out = env.output();
109        let err = cmd_reflection_origin(&db, &args, &mut out).expect_err("must fail");
110        assert!(err.to_string().contains("reflection-origin"), "got: {err}");
111    }
112
113    #[test]
114    fn reflection_origin_cli_text_output() {
115        let mut env = TestEnv::fresh();
116        let db = env.db_path.clone();
117        let mid = seed_memory(&db, "ns", "plain-text", "body");
118        let args = ReflectionOriginArgs {
119            memory_id: mid,
120            json: false,
121        };
122        {
123            let mut out = env.output();
124            cmd_reflection_origin(&db, &args, &mut out).expect("ok");
125        }
126        let stdout = env.stdout_str();
127        assert!(
128            stdout.contains("reflection-origin: is_reflection=false"),
129            "got: {stdout}"
130        );
131        assert!(stdout.contains("peer_origin="), "got: {stdout}");
132        assert!(stdout.contains("original_depth="), "got: {stdout}");
133    }
134}