use std::sync::{Arc, Mutex};
use cortex_store::repo::{MemoryRepo, PrincipleRepo};
use cortex_store::Pool;
use serde_json::{json, Value};
use crate::tool_handler::{GateId, ToolError, ToolHandler};
#[derive(Debug)]
pub struct CortexPrincipleStatusTool {
pool: Arc<Mutex<Pool>>,
}
impl CortexPrincipleStatusTool {
#[must_use]
pub fn new(pool: Arc<Mutex<Pool>>) -> Self {
Self { pool }
}
}
impl ToolHandler for CortexPrincipleStatusTool {
fn name(&self) -> &'static str {
"cortex_principle_status"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::FtsRead]
}
fn call(&self, params: Value) -> Result<Value, ToolError> {
tracing::info!("cortex_principle_status called via MCP");
let principle_id_filter = params["principle_id"]
.as_str()
.filter(|s| !s.trim().is_empty())
.map(ToOwned::to_owned);
let pool = self
.pool
.lock()
.map_err(|err| ToolError::Internal(format!("pool lock poisoned: {err}")))?;
let active_count = MemoryRepo::new(&pool)
.list_by_status("active")
.map_err(|err| {
tracing::error!(error = %err, "cortex_principle_status: failed to read active memories");
ToolError::Internal(format!("failed to read active memories: {err}"))
})?
.len();
let principle_repo = PrincipleRepo::new(&pool);
let candidates = principle_repo.list_candidates().map_err(|err| {
tracing::error!(error = %err, "cortex_principle_status: failed to list principle candidates");
ToolError::Internal(format!("failed to list principle candidates: {err}"))
})?;
let doctrine = principle_repo.list_doctrine().map_err(|err| {
tracing::error!(error = %err, "cortex_principle_status: failed to list doctrine");
ToolError::Internal(format!("failed to list doctrine: {err}"))
})?;
let candidate_count = candidates.len();
let doctrine_count = doctrine.len();
let principles: Vec<Value> = if let Some(ref id) = principle_id_filter {
let pid: cortex_core::PrincipleId = id.parse().map_err(|err| {
ToolError::InvalidParams(format!("invalid principle_id `{id}`: {err}"))
})?;
match principle_repo.get_by_id(&pid).map_err(|err| {
ToolError::Internal(format!("failed to fetch principle {id}: {err}"))
})? {
Some(row) => vec![json!({
"id": row.id.to_string(),
"claim": row.statement,
"status": row.status,
"confidence": row.confidence,
})],
None => vec![],
}
} else {
candidates
.into_iter()
.map(|row| {
json!({
"id": row.id.to_string(),
"claim": row.statement,
"status": row.status,
"confidence": row.confidence,
})
})
.collect()
};
Ok(json!({
"active_count": active_count,
"candidate_count": candidate_count,
"doctrine_count": doctrine_count,
"principles": principles,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tool() -> CortexPrincipleStatusTool {
let pool = rusqlite::Connection::open_in_memory().expect("in-memory sqlite");
cortex_store::migrate::apply_pending(&pool).expect("migrations");
CortexPrincipleStatusTool::new(Arc::new(Mutex::new(pool)))
}
#[test]
fn name_and_gate() {
let tool = make_tool();
assert_eq!(tool.name(), "cortex_principle_status");
assert_eq!(tool.gate_set(), &[GateId::FtsRead]);
}
#[test]
fn empty_store_returns_zero_counts() {
let tool = make_tool();
let result = tool.call(Value::Null).unwrap();
assert_eq!(result["active_count"], 0);
assert_eq!(result["candidate_count"], 0);
assert_eq!(result["doctrine_count"], 0);
assert_eq!(result["principles"], json!([]));
}
#[test]
fn unknown_principle_id_returns_empty_principles() {
let tool = make_tool();
let result = tool
.call(json!({ "principle_id": "prn_01JQZZZZZZZZZZZZZZZZZZZZZZ" }))
.unwrap();
assert_eq!(result["principles"], json!([]));
}
#[test]
fn invalid_principle_id_returns_invalid_params() {
let tool = make_tool();
let err = tool
.call(json!({ "principle_id": "not-a-ulid" }))
.unwrap_err();
assert!(matches!(err, ToolError::InvalidParams(_)));
}
}