use crate::models::field_names;
use anyhow::Result;
use clap::Args;
use serde_json::{Value, json};
use crate::cli::CliOutput;
use crate::storage as db;
#[derive(Args, Debug, Clone)]
pub struct ReflectArgs {
#[arg(long = "source-ids", value_name = "CSV", value_delimiter = ',')]
pub source_ids: Vec<String>,
#[arg(long, value_name = "TEXT")]
pub title: String,
#[arg(long, value_name = "TEXT")]
pub content: String,
#[arg(long, value_name = "TIER")]
pub tier: Option<String>,
#[arg(long, value_name = "NS")]
pub namespace: Option<String>,
#[arg(long, value_name = "N")]
pub priority: Option<i64>,
#[arg(long, value_name = "F32")]
pub confidence: Option<f64>,
#[arg(long, value_name = "CSV", value_delimiter = ',')]
pub tags: Vec<String>,
#[arg(long, value_name = "N")]
pub depth: Option<i64>,
#[arg(long = "agent-id", value_name = "AGENT_ID")]
pub agent_id: Option<String>,
#[arg(long)]
pub json: bool,
}
pub fn cmd_reflect(
db_path: &std::path::Path,
args: &ReflectArgs,
out: &mut CliOutput<'_>,
) -> Result<()> {
if args.source_ids.is_empty() {
anyhow::bail!("reflect: --source-ids is required (comma-separated list)");
}
let conn = db::open(db_path)?;
let mut params = json!({
(field_names::SOURCE_IDS): args.source_ids,
"title": args.title,
"content": args.content,
});
if let Some(t) = &args.tier {
params["tier"] = json!(t);
}
if let Some(ns) = &args.namespace {
params["namespace"] = json!(ns);
}
if let Some(p) = args.priority {
params["priority"] = json!(p);
}
if let Some(c) = args.confidence {
params[field_names::CONFIDENCE] = json!(c);
}
if !args.tags.is_empty() {
params["tags"] = json!(args.tags);
}
if let Some(d) = args.depth {
params["depth"] = json!(d);
}
if let Some(a) = &args.agent_id {
params["agent_id"] = json!(a);
}
let envelope = crate::mcp::handle_reflect(&conn, db_path, ¶ms, None, None, None, None)
.map_err(|e| anyhow::anyhow!("reflect: {e}"))?;
if args.json {
writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
return Ok(());
}
let id = envelope.get("id").and_then(Value::as_str).unwrap_or("?");
let depth = envelope
.get(field_names::REFLECTION_DEPTH)
.and_then(Value::as_i64)
.unwrap_or(0);
writeln!(out.stdout, "reflect: id={id} depth={depth}")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn reflect_cli_missing_source_ids_returns_err() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ReflectArgs {
source_ids: vec![],
title: "t".into(),
content: "c".into(),
tier: None,
namespace: None,
priority: None,
confidence: None,
tags: vec![],
depth: None,
agent_id: None,
json: true,
};
let mut out = env.output();
let err = cmd_reflect(&db, &args, &mut out).expect_err("must fail");
assert!(err.to_string().contains("source-ids"), "got: {err}");
}
#[test]
fn reflect_cli_happy_path_writes_envelope() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let s = seed_memory(&db, "rns", "reflect-src", "source content");
let args = ReflectArgs {
source_ids: vec![s],
title: "synthesis".into(),
content: "reflection body".into(),
tier: Some("mid".into()),
namespace: None,
priority: None,
confidence: None,
tags: vec![],
depth: None,
agent_id: None,
json: true,
};
{
let mut out = env.output();
cmd_reflect(&db, &args, &mut out).expect("reflect ok");
}
let stdout = env.stdout_str();
let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
assert!(envelope.get("id").and_then(Value::as_str).is_some());
}
#[test]
fn reflect_cli_text_output_with_all_params() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let s = seed_memory(&db, "rns", "reflect-src2", "source content");
let args = ReflectArgs {
source_ids: vec![s],
title: "synthesis-text".into(),
content: "reflection body".into(),
tier: Some("mid".into()),
namespace: Some("rns".into()),
priority: Some(7),
confidence: Some(0.8),
tags: vec!["t1".into(), "t2".into()],
depth: Some(1),
agent_id: Some("ai:reflector".into()),
json: false,
};
{
let mut out = env.output();
cmd_reflect(&db, &args, &mut out).expect("reflect ok");
}
let stdout = env.stdout_str();
assert!(stdout.contains("reflect: id="), "got: {stdout}");
assert!(stdout.contains("depth="), "got: {stdout}");
}
}