use super::*;
pub(super) fn build_mem_set_command(params: &MemSetParams) -> Result<Command, String> {
match params.action.as_str() {
"confirm" => {
if params.key.starts_with("policy:") {
return Err("policies are activated with `mati policy enable <slug>`".into());
}
if !params.key.starts_with("gotcha:") {
return Err("confirm action only applies to gotcha: keys".into());
}
Ok(Command::GotchaConfirm(GotchaConfirmInput {
key: params.key.clone(),
via_elicitation: false,
}))
}
"delete" => {
if params.key.starts_with("policy:") {
return Err("policies are removed with `mati policy delete <slug>`".into());
}
if !params.key.starts_with("gotcha:") {
return Err("delete action only applies to gotcha: keys".into());
}
Ok(Command::GotchaTombstone(GotchaTombstoneInput {
key: params.key.clone(),
}))
}
"write" | "" => build_mem_set_write_command(params),
other => Err(format!(
"unknown action: {other}. Valid: write, confirm, delete"
)),
}
}
fn build_mem_set_write_command(params: &MemSetParams) -> Result<Command, String> {
let payload = match ¶ms.payload {
serde_json::Value::String(s) => {
serde_json::from_str::<serde_json::Value>(s).unwrap_or_else(|_| params.payload.clone())
}
other => other.clone(),
};
let priority = parse_protocol_priority(¶ms.priority);
if let Some(slug) = params.key.strip_prefix("policy:") {
if slug.is_empty() {
return Err("policy key must not be just the prefix".into());
}
let mut policy: crate::store::PolicyRecord =
serde_json::from_value(payload).map_err(|error| {
format!("policy payload must deserialize into PolicyRecord: {error}")
})?;
if !matches!(policy.stage, crate::store::PolicyStage::Off) {
policy.stage = crate::store::PolicyStage::Off;
}
return Ok(Command::PolicyWrite(
crate::mcp::protocol::PolicyWriteInput {
op: crate::mcp::protocol::PolicyWriteOp::Create,
key: params.key.clone(),
policy: Some(policy),
stage: None,
},
));
}
if let Some(stripped) = params.key.strip_prefix("gotcha:") {
if stripped.is_empty() {
return Err("gotcha key must not be just the prefix".into());
}
let rule = field_string(&payload, "rule")
.ok_or_else(|| "gotcha payload requires non-empty 'rule'".to_string())?;
let reason = field_string(&payload, "reason")
.ok_or_else(|| "gotcha payload requires non-empty 'reason'".to_string())?;
let severity = parse_protocol_severity(payload.get("severity").and_then(|v| v.as_str()));
let affected_files = field_string_list(&payload, "affected_files");
let ref_url = payload
.get("ref_url")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Ok(Command::GotchaUpsert(GotchaDraftInput {
key: params.key.clone(),
rule,
reason,
severity,
affected_files,
ref_url,
tags: params.tags.clone(),
priority,
source: None,
confirmed: false,
}));
}
if let Some(slug) = params.key.strip_prefix("decision:") {
if slug.is_empty() {
return Err("decision key must not be just the prefix".into());
}
let summary = field_string(&payload, "summary")
.ok_or_else(|| "decision payload requires non-empty 'summary'".to_string())?;
let rationale = field_string(&payload, "rationale")
.ok_or_else(|| "decision payload requires non-empty 'rationale'".to_string())?;
return Ok(Command::DecisionUpsert(DecisionUpsertInput {
slug: slug.to_string(),
value: params.value.clone(),
summary,
rationale,
tags: params.tags.clone(),
priority,
}));
}
if let Some(stripped) = params.key.strip_prefix("dev_note:") {
if stripped.is_empty() {
return Err("dev_note key must not be just the prefix".into());
}
if params.value.is_empty() {
return Err("dev_note requires non-empty value".into());
}
return Ok(Command::DevNoteUpsert(DevNoteUpsertInput {
key: Some(params.key.clone()),
text: params.value.clone(),
tags: params.tags.clone(),
priority,
}));
}
Err("mem_set write requires key with gotcha:/decision:/dev_note:/policy: prefix".into())
}
fn field_string(payload: &serde_json::Value, field: &str) -> Option<String> {
payload
.get(field)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
}
fn field_string_list(payload: &serde_json::Value, field: &str) -> Vec<String> {
payload
.get(field)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
fn parse_protocol_priority(s: &str) -> proto::Priority {
match s {
"Critical" | "critical" => proto::Priority::Critical,
"High" | "high" => proto::Priority::High,
"Low" | "low" => proto::Priority::Low,
_ => proto::Priority::Normal,
}
}
fn parse_protocol_severity(s: Option<&str>) -> proto::Severity {
match s.map(|s| s.to_ascii_lowercase()).as_deref() {
Some("critical") => proto::Severity::Critical,
Some("high") => proto::Severity::High,
Some("low") => proto::Severity::Low,
_ => proto::Severity::Normal,
}
}