use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtractionConfig {
pub min_structuring_score: f64,
pub max_items_per_run: usize,
}
impl Default for SubtractionConfig {
fn default() -> Self {
Self {
min_structuring_score: 2.0,
max_items_per_run: 100,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CompactionStrategy {
Aggregate,
ColdStorage,
DeleteWithReceipt,
Quarantine,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtractionCandidate {
pub item_id: String,
pub structuring_score: f64,
pub recommended_action: CompactionStrategy,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtractionReceipt {
pub receipt_id: String,
pub item_id: String,
pub action: CompactionStrategy,
pub structuring_score: f64,
pub reason: String,
pub item_snapshot: serde_json::Value,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvariantCheck {
pub all_pass: bool,
pub violations: Vec<InvariantViolation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvariantViolation {
pub candidate_id: String,
pub invariant: String,
pub description: String,
}
pub fn compute_structuring_score(
has_provenance: bool,
is_referenced: bool,
is_verified: bool,
is_contradicted: bool,
has_episode: bool,
access_count: usize,
temporal_weight: f64,
) -> f64 {
let has_provenance = if has_provenance { 1.0 } else { 0.0 };
let is_referenced = if is_referenced { 1.0 } else { 0.0 };
let is_verified = if is_verified { 1.0 } else { 0.0 };
let not_contradicted = if !is_contradicted { 1.0 } else { 0.0 };
let has_episode = if has_episode { 1.0 } else { 0.0 };
let has_access = if access_count > 0 { 1.0 } else { 0.0 };
has_provenance + is_referenced + is_verified + not_contradicted + has_episode + has_access + temporal_weight
}
pub fn find_candidates(
items: &[(String, f64)],
config: &SubtractionConfig,
) -> Vec<SubtractionCandidate> {
let mut candidates: Vec<SubtractionCandidate> = items
.iter()
.filter(|(_, score)| *score < config.min_structuring_score)
.map(|(id, score)| {
let action = if *score < 1.0 {
CompactionStrategy::DeleteWithReceipt
} else if *score < 1.5 {
CompactionStrategy::Quarantine
} else {
CompactionStrategy::ColdStorage
};
let reason = format!("structuring score {score:.3} below minimum {}", config.min_structuring_score);
SubtractionCandidate {
item_id: id.clone(),
structuring_score: *score,
recommended_action: action,
reason,
}
})
.collect();
candidates.sort_by(|a, b| {
a.structuring_score
.partial_cmp(&b.structuring_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.truncate(config.max_items_per_run);
candidates
}
pub fn verify_invariants(
candidates: &[SubtractionCandidate],
active_references: &[String],
) -> InvariantCheck {
let active: std::collections::HashSet<&String> = active_references.iter().collect();
let mut violations = Vec::new();
for c in candidates {
if active.contains(&c.item_id) {
violations.push(InvariantViolation {
candidate_id: c.item_id.clone(),
invariant: "no_referenced_removal".to_string(),
description: format!(
"Item {} is actively referenced and must not be removed",
c.item_id
),
});
}
}
if !candidates.is_empty() {
}
let all_pass = violations.is_empty();
InvariantCheck {
all_pass,
violations,
}
}
pub fn execute_subtraction(
candidates: &[SubtractionCandidate],
active_references: &[String],
) -> Vec<SubtractionReceipt> {
let active: std::collections::HashSet<&String> = active_references.iter().collect();
let timestamp = "1970-01-01T00:00:00Z".to_string();
candidates
.iter()
.filter(|c| !active.contains(&c.item_id))
.enumerate()
.map(|(idx, c)| SubtractionReceipt {
receipt_id: format!("rcpt-{}-{}", c.item_id, idx),
item_id: c.item_id.clone(),
action: c.recommended_action,
structuring_score: c.structuring_score,
reason: c.reason.clone(),
item_snapshot: serde_json::json!({
"item_id": c.item_id,
"structuring_score": c.structuring_score,
"recommended_action": format!("{:?}", c.recommended_action),
}),
timestamp: timestamp.clone(),
})
.collect()
}
pub fn recover_item(receipt: &SubtractionReceipt) -> serde_json::Value {
receipt.item_snapshot.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn structuring_score_computation() {
let score = compute_structuring_score(true, true, true, false, true, 3, 0.5);
assert!((score - 6.5).abs() < f64::EPSILON);
}
#[test]
fn structuring_score_with_contradiction_and_no_access() {
let score = compute_structuring_score(false, false, false, true, false, 0, 0.0);
assert!((score - 0.0).abs() < f64::EPSILON);
}
#[test]
fn candidate_filtering_sorts_ascending() {
let items = vec![
("high".to_string(), 5.0),
("low1".to_string(), 0.5),
("low2".to_string(), 1.2),
("mid".to_string(), 3.0),
];
let cfg = SubtractionConfig::default(); let candidates = find_candidates(&items, &cfg);
assert_eq!(candidates.len(), 2);
assert!(candidates[0].structuring_score <= candidates[1].structuring_score);
assert_eq!(candidates[0].item_id, "low1");
assert_eq!(candidates[1].item_id, "low2");
assert_eq!(candidates[0].recommended_action, CompactionStrategy::DeleteWithReceipt);
assert_eq!(candidates[1].recommended_action, CompactionStrategy::Quarantine);
}
#[test]
fn invariant_check_passes_when_no_referenced() {
let candidates = vec![SubtractionCandidate {
item_id: "a".to_string(),
structuring_score: 0.5,
recommended_action: CompactionStrategy::DeleteWithReceipt,
reason: "r".to_string(),
}];
let active = vec!["b".to_string()];
let check = verify_invariants(&candidates, &active);
assert!(check.all_pass);
assert!(check.violations.is_empty());
}
#[test]
fn invariant_check_fails_when_referenced() {
let candidates = vec![SubtractionCandidate {
item_id: "a".to_string(),
structuring_score: 0.5,
recommended_action: CompactionStrategy::DeleteWithReceipt,
reason: "r".to_string(),
}];
let active = vec!["a".to_string()];
let check = verify_invariants(&candidates, &active);
assert!(!check.all_pass);
assert_eq!(check.violations.len(), 1);
assert_eq!(check.violations[0].invariant, "no_referenced_removal");
assert_eq!(check.violations[0].candidate_id, "a");
}
#[test]
fn execution_produces_receipts_and_skips_referenced() {
let candidates = vec![
SubtractionCandidate {
item_id: "keep_ref".to_string(),
structuring_score: 0.5,
recommended_action: CompactionStrategy::DeleteWithReceipt,
reason: "r".to_string(),
},
SubtractionCandidate {
item_id: "drop_me".to_string(),
structuring_score: 0.8,
recommended_action: CompactionStrategy::DeleteWithReceipt,
reason: "r".to_string(),
},
];
let active = vec!["keep_ref".to_string()];
let receipts = execute_subtraction(&candidates, &active);
assert_eq!(receipts.len(), 1);
assert_eq!(receipts[0].item_id, "drop_me");
assert_eq!(receipts[0].action, CompactionStrategy::DeleteWithReceipt);
assert_eq!(receipts[0].item_snapshot["item_id"], "drop_me");
}
#[test]
fn recovery_returns_snapshot() {
let receipt = SubtractionReceipt {
receipt_id: "rcpt-1".to_string(),
item_id: "x".to_string(),
action: CompactionStrategy::ColdStorage,
structuring_score: 1.8,
reason: "r".to_string(),
item_snapshot: serde_json::json!({"item_id": "x", "data": 42}),
timestamp: "1970-01-01T00:00:00Z".to_string(),
};
let recovered = recover_item(&receipt);
assert_eq!(recovered["item_id"], "x");
assert_eq!(recovered["data"], 42);
}
#[test]
fn background_scheduling_config_respects_max_items() {
let cfg = SubtractionConfig {
min_structuring_score: 2.0,
max_items_per_run: 2,
};
let items: Vec<(String, f64)> = (0..10)
.map(|i| (format!("item-{i}"), 0.1 * i as f64))
.collect();
let candidates = find_candidates(&items, &cfg);
assert_eq!(candidates.len(), 2);
}
#[test]
fn empty_items_produce_empty_candidates() {
let cfg = SubtractionConfig::default();
let candidates = find_candidates(&[], &cfg);
assert!(candidates.is_empty());
let check = verify_invariants(&[], &[]);
assert!(check.all_pass);
let receipts = execute_subtraction(&[], &[]);
assert!(receipts.is_empty());
}
#[test]
fn all_items_above_threshold_produces_no_candidates() {
let items = vec![
("a".to_string(), 2.5),
("b".to_string(), 3.0),
("c".to_string(), 10.0),
];
let cfg = SubtractionConfig::default(); let candidates = find_candidates(&items, &cfg);
assert!(candidates.is_empty());
}
}