use crate::models::field_names;
use serde_json::{Value, json};
pub fn handle_reflection_origin(
conn: &rusqlite::Connection,
params: &Value,
) -> Result<Value, String> {
let memory_id = params["memory_id"]
.as_str()
.ok_or(crate::errors::msg::MEMORY_ID_REQUIRED)?;
if memory_id.is_empty() {
return Err(crate::errors::msg::MEMORY_ID_EMPTY.to_string());
}
let origin = crate::federation::reflection_bookkeeping::reflection_origin(conn, memory_id)
.map_err(|e| format!("reflection_origin substrate error: {e}"))?;
match origin {
Some(record) => Ok(json!({
"memory_id": record.memory_id,
(field_names::PEER_ORIGIN): record.peer_origin,
(field_names::SIGNING_AGENT): record.signing_agent,
(field_names::ORIGINAL_DEPTH): record.original_depth,
(field_names::LOCAL_DEPTH_AT_ARRIVAL): record.local_depth_at_arrival,
(field_names::IS_REFLECTION): record.is_reflection,
})),
None => Err(crate::errors::msg::memory_not_found(memory_id)),
}
}
use crate::mcp::registry::McpTool;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct ReflectionOriginRequest {
pub memory_id: String,
}
#[allow(dead_code)]
pub struct ReflectionOriginTool;
impl McpTool for ReflectionOriginTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_REFLECTION_ORIGIN
}
fn description() -> &'static str {
"Inspect the cross-peer provenance of a reflection memory."
}
fn docs() -> &'static str {
"L2-2 (S6-M1): {memory_id, peer_origin, signing_agent, original_depth, local_depth_at_arrival, is_reflection}. Non-reflections return envelope with is_reflection=false. Unknown ids => error."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<ReflectionOriginRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Power.name()
}
}
#[cfg(test)]
mod d1_5_986_tests {
use super::*;
use crate::mcp::parity_test_helpers::{
assert_descriptions_match, assert_property_set_parity, derived_props_for,
};
#[test]
fn reflection_origin_parity_986() {
let derived = derived_props_for::<ReflectionOriginRequest>();
assert_property_set_parity("memory_reflection_origin", &derived);
assert_descriptions_match("memory_reflection_origin", &derived);
}
#[test]
fn reflection_origin_tool_metadata_986() {
assert_eq!(ReflectionOriginTool::name(), "memory_reflection_origin");
assert_eq!(ReflectionOriginTool::family(), "power");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage as db;
fn fresh_db() -> rusqlite::Connection {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
db::open(tmp.path()).expect("db::open")
}
#[test]
fn handle_unknown_id_returns_not_found() {
let conn = fresh_db();
let err = handle_reflection_origin(&conn, &json!({"memory_id": "nope-id"})).unwrap_err();
assert!(err.contains("not found"), "expected not-found error: {err}");
}
#[test]
fn handle_missing_param_returns_error() {
let conn = fresh_db();
let err = handle_reflection_origin(&conn, &json!({})).unwrap_err();
assert!(err.contains("memory_id"), "expected param error: {err}");
}
#[test]
fn handle_non_reflection_returns_envelope_with_flag() {
let conn = fresh_db();
let now = chrono::Utc::now().to_rfc3339();
let mem = crate::models::Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: crate::models::Tier::Mid,
namespace: "test".to_string(),
title: "plain".to_string(),
content: "body".to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: serde_json::json!({"agent_id": "ai:test"}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).expect("insert");
let out = handle_reflection_origin(&conn, &json!({"memory_id": id})).unwrap();
assert_eq!(out["is_reflection"].as_bool(), Some(false));
assert_eq!(out["original_depth"].as_i64(), Some(0));
}
}