use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubtractionOperation {
Dedupe,
Summarize,
Compact,
Retire,
Quarantine,
Minimize,
SupportCoreExtraction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubtractionPlanV1 {
pub plan_id: String,
pub operation: SubtractionOperation,
pub target_ids: Vec<String>,
pub preserved_invariants: Vec<InvariantBudgetV1>,
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvariantBudgetV1 {
pub budget_type: BudgetType,
pub max_loss: usize,
pub current_at_risk: usize,
pub exceeded: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BudgetType {
Replay,
AsOfQuery,
ClaimSupport,
ReceiptLineage,
LegalRetention,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportCoreV1 {
pub invariant: String,
pub retained_ids: Vec<String>,
pub removable_ids: Vec<String>,
pub verified: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemovalFrontierV1 {
pub invariant: String,
pub removal_set: Vec<String>,
pub found: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompactionReceiptV1 {
pub receipt_id: String,
pub before_digest: String,
pub after_digest: String,
pub what_lost: Vec<String>,
pub what_preserved: Vec<String>,
pub as_of_queries_correct: bool,
pub timestamp: String,
}
impl SubtractionPlanV1 {
pub fn new(operation: SubtractionOperation, target_ids: Vec<String>) -> Self {
Self {
plan_id: format!(
"sp:{}:{}",
match operation {
SubtractionOperation::Dedupe => "dedupe",
SubtractionOperation::Summarize => "summarize",
SubtractionOperation::Compact => "compact",
SubtractionOperation::Retire => "retire",
SubtractionOperation::Quarantine => "quarantine",
SubtractionOperation::Minimize => "minimize",
SubtractionOperation::SupportCoreExtraction => "support-core",
},
chrono::Utc::now().timestamp()
),
operation,
target_ids,
preserved_invariants: vec![],
dry_run: true,
}
}
pub fn with_invariant(mut self, budget: InvariantBudgetV1) -> Self {
self.preserved_invariants.push(budget);
self
}
pub fn can_proceed(&self) -> bool {
!self.preserved_invariants.iter().any(|b| b.exceeded)
}
}
impl SupportCoreV1 {
pub fn would_break(&self, artifact_id: &str) -> bool {
self.retained_ids.contains(&artifact_id.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subtraction_plan_starts_as_dry_run() {
let plan = SubtractionPlanV1::new(SubtractionOperation::Compact, vec!["a1".into()]);
assert!(plan.dry_run);
assert!(plan.can_proceed());
}
#[test]
fn plan_blocked_when_budget_exceeded() {
let plan = SubtractionPlanV1::new(SubtractionOperation::Compact, vec!["a1".into()])
.with_invariant(InvariantBudgetV1 {
budget_type: BudgetType::Replay,
max_loss: 10,
current_at_risk: 15,
exceeded: true,
});
assert!(!plan.can_proceed());
}
#[test]
fn support_core_would_break_on_retained() {
let core = SupportCoreV1 {
invariant: "replay".into(),
retained_ids: vec!["a1".into(), "a2".into()],
removable_ids: vec!["a3".into()],
verified: true,
};
assert!(core.would_break("a1"));
assert!(!core.would_break("a3"));
}
#[test]
fn removal_frontier_found() {
let frontier = RemovalFrontierV1 {
invariant: "claim_support".into(),
removal_set: vec!["a1".into()],
found: true,
};
assert!(frontier.found);
}
#[test]
fn compaction_receipt_records_before_after() {
let receipt = CompactionReceiptV1 {
receipt_id: "cr:1".into(),
before_digest: "abc".into(),
after_digest: "def".into(),
what_lost: vec!["old_receipts".into()],
what_preserved: vec!["current_claims".into()],
as_of_queries_correct: true,
timestamp: chrono::Utc::now().to_rfc3339(),
};
assert_ne!(receipt.before_digest, receipt.after_digest);
assert!(receipt.as_of_queries_correct);
}
#[test]
fn subtraction_preserves_invariant_or_fails() {
let plan = SubtractionPlanV1::new(SubtractionOperation::Retire, vec!["a1".into()])
.with_invariant(InvariantBudgetV1 {
budget_type: BudgetType::ClaimSupport,
max_loss: 0,
current_at_risk: 1,
exceeded: true,
});
assert!(
!plan.can_proceed(),
"subtraction must not proceed when invariant budget is exceeded"
);
}
}