Skip to main content

kernel/discard/
request.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Discard requests: restoration routes and the planned/unplanned shapes.
7//! The part `crates/kernel/spec/Discard.lean` specifies this module with the
8//! rest of `kernel::discard`.
9
10use serde::{Deserialize, Serialize};
11
12use crate::address::Address;
13use crate::budget::ByteLen;
14use crate::error::{AxCode, AxError};
15use crate::locator::Locator;
16use crate::taint::TaintSet;
17
18/// Tracked rides git (`file:`), Interred rides CAS (`cas:`), Rebuildable
19/// names its reason. No fourth storage authority exists.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub enum Restoration {
24    Tracked(Locator),
25    Interred(Locator),
26    Rebuildable { reason: String },
27}
28
29/// A planned discard. Private fields; the sole constructor checks the
30/// plan's scheme — C14's type half.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Discard {
33    paths: Vec<Address>,
34    plan: Restoration,
35    taint: TaintSet,
36    total_bytes: ByteLen,
37}
38
39impl Discard {
40    /// Restoration is mandatory and scheme-checked: Tracked wants
41    /// `file:`, Interred wants `cas:`, Rebuildable wants a non-empty
42    /// reason — violations are `E_DISCARD_IRREVERSIBLE` (an unparsable
43    /// plan is no plan). Empty paths are `E_INVALID_ARGS`.
44    pub fn new(
45        paths: Vec<Address>,
46        plan: Restoration,
47        taint: TaintSet,
48        total_bytes: ByteLen,
49    ) -> Result<Discard, AxError> {
50        if paths.is_empty() {
51            return Err(AxError::failure(
52                AxCode::InvalidArgs,
53                "construct discard",
54                "empty path list",
55            )
56            .with_recovery("name at least one path in the `paths` argument of the call"));
57        }
58        let plan_ok = match &plan {
59            Restoration::Tracked(locator) => matches!(locator, Locator::File { .. }),
60            Restoration::Interred(locator) => matches!(locator, Locator::Cas { .. }),
61            Restoration::Rebuildable { reason } => !reason.is_empty(),
62        };
63        if !plan_ok {
64            return Err(AxError::failure(
65                AxCode::DiscardIrreversible,
66                "construct discard",
67                "restoration plan does not resolve",
68            )
69            .with_recovery(
70                "Tracked cites a file: locator, Interred a cas: locator, \
71                 Rebuildable a non-empty reason",
72            ));
73        }
74        Ok(Discard {
75            paths,
76            plan,
77            taint,
78            total_bytes,
79        })
80    }
81
82    pub fn paths(&self) -> &[Address] {
83        &self.paths
84    }
85
86    pub fn plan(&self) -> &Restoration {
87        &self.plan
88    }
89
90    pub fn taint(&self) -> &TaintSet {
91        &self.taint
92    }
93
94    pub fn total_bytes(&self) -> ByteLen {
95        self.total_bytes
96    }
97}
98
99/// What reaches the door: a planned discard, or an unplanned request
100/// from the exec forecast path (text prediction cannot mint plans).
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum DiscardRequest {
103    Planned(Discard),
104    Unplanned {
105        paths: Vec<Address>,
106        taint: TaintSet,
107        total_bytes: ByteLen,
108    },
109}
110
111/// Why a discard is refused. Two reasons, and both are rules a person
112/// can read: a delete with no way back, and a delete a run asked for
113/// because something outside the city told it to.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum DenyReason {
116    NoRestoration,
117    /// C15: an effect derived from outside content is refused rather
118    /// than carried out, whatever its size.
119    Tainted,
120}
121
122#[cfg(test)]
123#[allow(
124    clippy::unwrap_used,
125    clippy::expect_used,
126    clippy::panic,
127    clippy::indexing_slicing,
128    reason = "test code"
129)]
130mod tests {
131    use super::*;
132    fn addr(raw: &str) -> Address {
133        Address::parse(raw).unwrap()
134    }
135    fn tracked() -> Restoration {
136        Restoration::Tracked(Locator::parse(&format!("file:b/x.md@{}", "ab".repeat(20))).unwrap())
137    }
138    fn interred() -> Restoration {
139        Restoration::Interred(Locator::parse(&format!("cas:b3-{}", "cd".repeat(32))).unwrap())
140    }
141    #[test]
142    fn restoration_schemes_are_checked_at_construction() {
143        assert!(
144            Discard::new(
145                vec![addr("b/x.md")],
146                tracked(),
147                TaintSet::empty(),
148                ByteLen::new(1)
149            )
150            .is_ok()
151        );
152        assert!(
153            Discard::new(
154                vec![addr("b/x.md")],
155                interred(),
156                TaintSet::empty(),
157                ByteLen::new(1)
158            )
159            .is_ok()
160        );
161        // Tracked with a cas: locator is an unresolvable plan.
162        let wrong =
163            Restoration::Tracked(Locator::parse(&format!("cas:b3-{}", "cd".repeat(32))).unwrap());
164        let err = Discard::new(
165            vec![addr("b/x.md")],
166            wrong,
167            TaintSet::empty(),
168            ByteLen::new(1),
169        )
170        .unwrap_err();
171        assert_eq!(err.code(), &AxCode::DiscardIrreversible);
172        let empty_reason = Restoration::Rebuildable {
173            reason: String::new(),
174        };
175        assert!(
176            Discard::new(
177                vec![addr("b/x.md")],
178                empty_reason,
179                TaintSet::empty(),
180                ByteLen::new(1)
181            )
182            .is_err()
183        );
184        let no_paths = Discard::new(vec![], tracked(), TaintSet::empty(), ByteLen::new(1));
185        assert_eq!(no_paths.unwrap_err().code(), &AxCode::InvalidArgs);
186    }
187}