use serde_json::{Value, json};
use crate::confidence::calibrate::{DEFAULT_WINDOW_DAYS, calibrate_from_shadow};
pub fn handle_calibrate_confidence(
conn: &rusqlite::Connection,
params: &Value,
) -> Result<Value, String> {
let days = params
.get("days")
.and_then(Value::as_i64)
.unwrap_or(DEFAULT_WINDOW_DAYS);
if days <= 0 {
return Err("days must be a positive integer".to_string());
}
let report = calibrate_from_shadow(conn, days, chrono::Utc::now())
.map_err(|e| format!("memory_calibrate_confidence substrate error: {e}"))?;
Ok(json!({ "report": report }))
}
use crate::mcp::registry::McpTool;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct CalibrateConfidenceRequest {
#[serde(default)]
pub days: Option<i64>,
#[serde(default)]
pub output_format: Option<String>,
}
#[allow(dead_code)]
pub struct CalibrateConfidenceTool;
impl McpTool for CalibrateConfidenceTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_CALIBRATE_CONFIDENCE
}
fn description() -> &'static str {
"Scan confidence_shadow_observations and emit per-source baselines (Form 5)."
}
fn docs() -> &'static str {
"Form 5 (#758): read-only calibration sweep over shadow-mode observations (AI_MEMORY_CONFIDENCE_SHADOW=1). Returns CalibrationReport {window_days, total_observations, baselines:[{namespace, source, count, median, mean, buckets}]}. Default window 30d. Family::Power — refuses on keyword tier."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<CalibrateConfidenceRequest>()
}
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 calibrate_confidence_parity_986() {
let derived = derived_props_for::<CalibrateConfidenceRequest>();
assert_property_set_parity("memory_calibrate_confidence", &derived);
assert_descriptions_match("memory_calibrate_confidence", &derived);
}
#[test]
fn calibrate_confidence_tool_metadata_986() {
assert_eq!(
CalibrateConfidenceTool::name(),
"memory_calibrate_confidence"
);
assert_eq!(CalibrateConfidenceTool::family(), "power");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::open as open_storage;
use rusqlite::Connection;
use serde_json::json;
fn open_tmp() -> (Connection, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tmpdir");
let path = dir.path().join("test.db");
let _ = open_storage(&path).expect("open storage");
let conn = Connection::open(&path).expect("open conn");
(conn, dir)
}
#[test]
fn empty_db_returns_empty_baselines() {
let (conn, _dir) = open_tmp();
let v = handle_calibrate_confidence(&conn, &json!({})).expect("ok");
assert_eq!(v["report"]["total_observations"], 0);
assert!(v["report"]["baselines"].as_array().unwrap().is_empty());
}
#[test]
fn rejects_non_positive_days() {
let (conn, _dir) = open_tmp();
let err = handle_calibrate_confidence(&conn, &json!({"days": 0})).expect_err("must reject");
assert!(err.contains("positive integer"));
let err =
handle_calibrate_confidence(&conn, &json!({"days": -1})).expect_err("must reject");
assert!(err.contains("positive integer"));
}
#[test]
fn default_days_used_when_omitted() {
let (conn, _dir) = open_tmp();
let v = handle_calibrate_confidence(&conn, &json!({})).expect("ok");
assert_eq!(
v["report"]["window_days"].as_i64().unwrap(),
DEFAULT_WINDOW_DAYS
);
}
}