Skip to main content

aidens_kernel_kit/
lawful_subtraction.rs

1//! P16 — Lawful subtraction, compaction, and invariant-preserving reduction.
2//!
3//! Subtraction is the dual of accumulation: removal/compaction must preserve
4//! declared invariants and emit receipts.
5
6use serde::{Deserialize, Serialize};
7
8/// Type of subtractive operation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum SubtractionOperation {
11    /// Remove exact duplicates.
12    Dedupe,
13    /// Replace detail with summary.
14    Summarize,
15    /// Compress stored representation.
16    Compact,
17    /// Remove outdated artifacts.
18    Retire,
19    /// Move to quarantine (still queryable but excluded).
20    Quarantine,
21    /// Reduce to minimal support set.
22    Minimize,
23    /// Extract support core (minimal retained support).
24    SupportCoreExtraction,
25}
26
27/// A plan for a subtractive operation. Declares what will be removed
28/// and what invariants must be preserved.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SubtractionPlanV1 {
31    /// Plan identifier.
32    pub plan_id: String,
33    /// Operation type.
34    pub operation: SubtractionOperation,
35    /// Target artifact IDs to subtract from.
36    pub target_ids: Vec<String>,
37    /// Invariants that must be preserved.
38    pub preserved_invariants: Vec<InvariantBudgetV1>,
39    /// Whether this is a dry-run (no actual mutation).
40    pub dry_run: bool,
41}
42
43/// Budget for what can be lost during subtraction.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct InvariantBudgetV1 {
46    /// What kind of budget.
47    pub budget_type: BudgetType,
48    /// Maximum items that can be lost.
49    pub max_loss: usize,
50    /// Current items at risk.
51    pub current_at_risk: usize,
52    /// Whether the budget is exceeded.
53    pub exceeded: bool,
54}
55
56/// Types of invariant budgets.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub enum BudgetType {
59    /// Replay budget (can we still replay history?).
60    Replay,
61    /// As-of query budget (can we still answer temporal queries?).
62    AsOfQuery,
63    /// Claim support budget (can we still support accepted claims?).
64    ClaimSupport,
65    /// Receipt lineage budget (can we still trace receipts?).
66    ReceiptLineage,
67    /// Legal retention budget (are we legally required to keep this?).
68    LegalRetention,
69}
70
71/// Minimal retained support preserving an invariant.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SupportCoreV1 {
74    /// The invariant this support core preserves.
75    pub invariant: String,
76    /// Minimal artifact IDs that must be retained.
77    pub retained_ids: Vec<String>,
78    /// Artifact IDs that can be safely removed.
79    pub removable_ids: Vec<String>,
80    /// Whether the support core was verified.
81    pub verified: bool,
82}
83
84/// Minimal removal that breaks an invariant. Used for dry-run checking.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RemovalFrontierV1 {
87    /// The invariant that would be broken.
88    pub invariant: String,
89    /// The minimal set of removals that breaks it.
90    pub removal_set: Vec<String>,
91    /// Whether the frontier was found (false = invariant is robust).
92    pub found: bool,
93}
94
95/// Receipt emitted after a compaction operation.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CompactionReceiptV1 {
98    /// Receipt ID.
99    pub receipt_id: String,
100    /// Digest before compaction.
101    pub before_digest: String,
102    /// Digest after compaction.
103    pub after_digest: String,
104    /// What was lost in compaction.
105    pub what_lost: Vec<String>,
106    /// What was preserved.
107    pub what_preserved: Vec<String>,
108    /// Whether as-of queries remain correct.
109    pub as_of_queries_correct: bool,
110    /// Timestamp.
111    pub timestamp: String,
112}
113
114impl SubtractionPlanV1 {
115    /// Create a new subtraction plan.
116    pub fn new(operation: SubtractionOperation, target_ids: Vec<String>) -> Self {
117        Self {
118            plan_id: format!(
119                "sp:{}:{}",
120                match operation {
121                    SubtractionOperation::Dedupe => "dedupe",
122                    SubtractionOperation::Summarize => "summarize",
123                    SubtractionOperation::Compact => "compact",
124                    SubtractionOperation::Retire => "retire",
125                    SubtractionOperation::Quarantine => "quarantine",
126                    SubtractionOperation::Minimize => "minimize",
127                    SubtractionOperation::SupportCoreExtraction => "support-core",
128                },
129                chrono::Utc::now().timestamp()
130            ),
131            operation,
132            target_ids,
133            preserved_invariants: vec![],
134            dry_run: true,
135        }
136    }
137
138    /// Add an invariant budget.
139    pub fn with_invariant(mut self, budget: InvariantBudgetV1) -> Self {
140        self.preserved_invariants.push(budget);
141        self
142    }
143
144    /// Check if the plan can proceed (no budgets exceeded).
145    pub fn can_proceed(&self) -> bool {
146        !self.preserved_invariants.iter().any(|b| b.exceeded)
147    }
148}
149
150impl SupportCoreV1 {
151    /// Check if removing an artifact would break this support core.
152    pub fn would_break(&self, artifact_id: &str) -> bool {
153        self.retained_ids.contains(&artifact_id.to_string())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn subtraction_plan_starts_as_dry_run() {
163        let plan = SubtractionPlanV1::new(SubtractionOperation::Compact, vec!["a1".into()]);
164        assert!(plan.dry_run);
165        assert!(plan.can_proceed());
166    }
167
168    #[test]
169    fn plan_blocked_when_budget_exceeded() {
170        let plan = SubtractionPlanV1::new(SubtractionOperation::Compact, vec!["a1".into()])
171            .with_invariant(InvariantBudgetV1 {
172                budget_type: BudgetType::Replay,
173                max_loss: 10,
174                current_at_risk: 15,
175                exceeded: true,
176            });
177        assert!(!plan.can_proceed());
178    }
179
180    #[test]
181    fn support_core_would_break_on_retained() {
182        let core = SupportCoreV1 {
183            invariant: "replay".into(),
184            retained_ids: vec!["a1".into(), "a2".into()],
185            removable_ids: vec!["a3".into()],
186            verified: true,
187        };
188        assert!(core.would_break("a1"));
189        assert!(!core.would_break("a3"));
190    }
191
192    #[test]
193    fn removal_frontier_found() {
194        let frontier = RemovalFrontierV1 {
195            invariant: "claim_support".into(),
196            removal_set: vec!["a1".into()],
197            found: true,
198        };
199        assert!(frontier.found);
200    }
201
202    #[test]
203    fn compaction_receipt_records_before_after() {
204        let receipt = CompactionReceiptV1 {
205            receipt_id: "cr:1".into(),
206            before_digest: "abc".into(),
207            after_digest: "def".into(),
208            what_lost: vec!["old_receipts".into()],
209            what_preserved: vec!["current_claims".into()],
210            as_of_queries_correct: true,
211            timestamp: chrono::Utc::now().to_rfc3339(),
212        };
213        assert_ne!(receipt.before_digest, receipt.after_digest);
214        assert!(receipt.as_of_queries_correct);
215    }
216
217    #[test]
218    fn subtraction_preserves_invariant_or_fails() {
219        // If budget is exceeded, plan cannot proceed
220        let plan = SubtractionPlanV1::new(SubtractionOperation::Retire, vec!["a1".into()])
221            .with_invariant(InvariantBudgetV1 {
222                budget_type: BudgetType::ClaimSupport,
223                max_loss: 0,
224                current_at_risk: 1,
225                exceeded: true,
226            });
227        assert!(
228            !plan.can_proceed(),
229            "subtraction must not proceed when invariant budget is exceeded"
230        );
231    }
232}