#![allow(dead_code)]
use crate::db::memory::Memory;
use chrono::{DateTime, Duration, Utc};
use thiserror::Error;
pub const MIN_CONFIDENCE_TO_AUTO_APPLY: f64 = 0.7;
pub const REJECT_COUNT_TO_DEPRIORITIZE: u32 = 3;
pub const IGNORE_COUNT_TO_SUGGEST_STRONG: u32 = 5;
pub const ARCHIVE_INACTIVITY_DAYS: i64 = 90;
pub const MAX_CONSOLIDATION_BATCH_SIZE: usize = 10;
pub const MAX_CLAUDE_CALLS_PER_HOUR: u32 = 60;
pub const MAX_MEMORY_WRITES_PER_HOUR: u32 = 100;
#[derive(Debug, Error)]
pub enum PolicyError {
#[error("user-authored memory cannot be auto-modified or deleted")]
UserAuthoredProtected,
#[error("constraint-priority memory cannot be auto-changed")]
ConstraintProtected,
#[error("archive only allowed for priority='info'")]
NotInfoPriority,
#[error("memory was used recently — cannot archive yet")]
RecentlyUsed,
#[error("no semantically-better alternative — archive blocked")]
NoBetterAlternative,
#[error("confidence {got:.2} below threshold {min:.2}")]
LowConfidence { got: f64, min: f64 },
#[error("batch size {got} exceeds max {max}")]
BatchSizeExceeded { got: usize, max: usize },
#[error("rate limit: {scope} ({rate}/hour)")]
RateLimitExceeded { scope: &'static str, rate: u32 },
#[error("hard delete forbidden — use soft delete (status='deleted')")]
HardDeleteForbidden,
}
pub fn ensure_can_auto_delete(memory: &Memory) -> Result<(), PolicyError> {
if memory.source == "user" {
return Err(PolicyError::UserAuthoredProtected);
}
if memory.priority == "constraint" {
return Err(PolicyError::ConstraintProtected);
}
Ok(())
}
pub fn ensure_can_auto_modify_text(memory: &Memory) -> Result<(), PolicyError> {
if memory.source == "user" {
return Err(PolicyError::UserAuthoredProtected);
}
Ok(())
}
pub fn ensure_can_archive(
memory: &Memory,
last_used: Option<DateTime<Utc>>,
has_better_alternative: bool,
) -> Result<(), PolicyError> {
ensure_can_auto_delete(memory)?; if memory.priority != "info" {
return Err(PolicyError::NotInfoPriority);
}
if !has_better_alternative {
return Err(PolicyError::NoBetterAlternative);
}
if let Some(t) = last_used {
let cutoff = Utc::now() - Duration::days(ARCHIVE_INACTIVITY_DAYS);
if t > cutoff {
return Err(PolicyError::RecentlyUsed);
}
}
Ok(())
}
pub fn ensure_confidence_for_auto_apply(confidence: f64) -> Result<(), PolicyError> {
if confidence < MIN_CONFIDENCE_TO_AUTO_APPLY {
return Err(PolicyError::LowConfidence {
got: confidence,
min: MIN_CONFIDENCE_TO_AUTO_APPLY,
});
}
Ok(())
}
pub fn ensure_batch_size(batch: usize) -> Result<(), PolicyError> {
if batch > MAX_CONSOLIDATION_BATCH_SIZE {
return Err(PolicyError::BatchSizeExceeded {
got: batch,
max: MAX_CONSOLIDATION_BATCH_SIZE,
});
}
Ok(())
}
pub fn should_deprioritize_pattern(reject_count: u32) -> bool {
reject_count >= REJECT_COUNT_TO_DEPRIORITIZE
}
pub fn should_propose_as_strong(ignore_count: u32) -> bool {
ignore_count >= IGNORE_COUNT_TO_SUGGEST_STRONG
}
pub const USER_ISOLATION_NOTE: &str =
"All queries MUST filter by user_id. See db/memory.rs for enforcement.";
pub fn can_inject_into_advice(memory: &Memory, current_project: &str) -> bool {
if memory.status != "active" {
return false;
}
match memory.scope.as_str() {
"project" => memory.project.as_deref() == Some(current_project),
"user" | "tech" => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_memory(source: &str, priority: &str, scope: &str, project: Option<&str>) -> Memory {
Memory {
id: "test".into(),
user_id: "alice".into(),
text: "x".into(),
scope: scope.into(),
priority: priority.into(),
source: source.into(),
project: project.map(String::from),
tech: None,
metadata: serde_json::json!({}),
status: "active".into(),
created_at: "now".into(),
updated_at: "now".into(),
deleted_at: None,
}
}
#[test]
fn user_authored_cannot_be_deleted() {
let m = make_memory("user", "info", "user", None);
assert!(matches!(
ensure_can_auto_delete(&m),
Err(PolicyError::UserAuthoredProtected)
));
}
#[test]
fn constraint_cannot_be_deleted() {
let m = make_memory("asurada", "constraint", "user", None);
assert!(matches!(
ensure_can_auto_delete(&m),
Err(PolicyError::ConstraintProtected)
));
}
#[test]
fn asurada_info_can_be_deleted() {
let m = make_memory("asurada", "info", "user", None);
assert!(ensure_can_auto_delete(&m).is_ok());
}
#[test]
fn archive_blocked_when_recently_used() {
let m = make_memory("asurada", "info", "user", None);
let recent = Utc::now() - Duration::days(10);
assert!(matches!(
ensure_can_archive(&m, Some(recent), true),
Err(PolicyError::RecentlyUsed)
));
}
#[test]
fn archive_allowed_when_old_and_better_alt() {
let m = make_memory("asurada", "info", "user", None);
let old = Utc::now() - Duration::days(120);
assert!(ensure_can_archive(&m, Some(old), true).is_ok());
}
#[test]
fn archive_blocked_for_non_info() {
let m = make_memory("asurada", "strong", "user", None);
let old = Utc::now() - Duration::days(120);
assert!(matches!(
ensure_can_archive(&m, Some(old), true),
Err(PolicyError::NotInfoPriority)
));
}
#[test]
fn archive_blocked_without_better_alternative() {
let m = make_memory("asurada", "info", "user", None);
let old = Utc::now() - Duration::days(120);
assert!(matches!(
ensure_can_archive(&m, Some(old), false),
Err(PolicyError::NoBetterAlternative)
));
}
#[test]
fn project_scope_isolates_to_same_project() {
let m = make_memory("asurada", "strong", "project", Some("Devist"));
assert!(can_inject_into_advice(&m, "Devist"));
assert!(!can_inject_into_advice(&m, "WC-Core"));
}
#[test]
fn user_scope_crosses_projects() {
let m = make_memory("asurada", "strong", "user", None);
assert!(can_inject_into_advice(&m, "Devist"));
assert!(can_inject_into_advice(&m, "WC-Core"));
}
#[test]
fn confidence_threshold() {
assert!(ensure_confidence_for_auto_apply(0.9).is_ok());
assert!(matches!(
ensure_confidence_for_auto_apply(0.5),
Err(PolicyError::LowConfidence { .. })
));
}
#[test]
fn batch_size_limit() {
assert!(ensure_batch_size(MAX_CONSOLIDATION_BATCH_SIZE).is_ok());
assert!(matches!(
ensure_batch_size(MAX_CONSOLIDATION_BATCH_SIZE + 1),
Err(PolicyError::BatchSizeExceeded { .. })
));
}
#[test]
fn deprioritize_threshold() {
assert!(!should_deprioritize_pattern(REJECT_COUNT_TO_DEPRIORITIZE - 1));
assert!(should_deprioritize_pattern(REJECT_COUNT_TO_DEPRIORITIZE));
}
}