Skip to main content

weavatrix_worktree/
limits.rs

1use crate::error::{TransactionPhase, WorktreeError, WorktreeErrorCode};
2
3const MIB: usize = 1024 * 1024;
4
5/// Absolute worker ceiling retained even when callers raise transaction limits.
6pub const ABSOLUTE_MAX_WORKERS: usize = 16;
7
8/// Absolute journal ceiling retained even when callers customize limits.
9pub const ABSOLUTE_MAX_JOURNAL_BYTES: usize = 16 * MIB;
10
11/// Absolute ceiling for retained undo receipts in one worktree.
12pub const ABSOLUTE_MAX_UNDO_RECEIPTS: usize = 1_024;
13
14/// Absolute cumulative byte ceiling for retained rollback artifacts.
15pub const ABSOLUTE_MAX_UNDO_BYTES: usize = 2 * 1024 * MIB;
16
17/// Hard resource ceilings for one worktree transaction.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct WorktreeLimits {
20    pub max_files: usize,
21    pub max_edits_per_file: usize,
22    pub max_source_bytes_per_file: usize,
23    pub max_output_bytes_per_file: usize,
24    pub max_total_source_bytes: usize,
25    pub max_total_output_bytes: usize,
26    pub max_total_artifact_bytes: usize,
27    pub max_journal_bytes: usize,
28    pub max_undo_receipts: usize,
29    pub max_total_undo_bytes: usize,
30    pub max_operation_bytes: usize,
31    pub max_extension_bytes: usize,
32    pub max_extension_nodes: usize,
33    pub max_extension_depth: usize,
34    pub max_evidence_entries: usize,
35    pub max_evidence_text_bytes: usize,
36    pub max_code_bytes: usize,
37    pub max_workers: usize,
38}
39
40impl Default for WorktreeLimits {
41    fn default() -> Self {
42        Self {
43            max_files: 64,
44            max_edits_per_file: 2_000,
45            max_source_bytes_per_file: 16 * MIB,
46            max_output_bytes_per_file: 64 * MIB,
47            max_total_source_bytes: 128 * MIB,
48            max_total_output_bytes: 256 * MIB,
49            max_total_artifact_bytes: 384 * MIB,
50            max_journal_bytes: MIB,
51            max_undo_receipts: 32,
52            max_total_undo_bytes: 384 * MIB,
53            max_operation_bytes: 4_096,
54            max_extension_bytes: 256 * 1024,
55            max_extension_nodes: 4_096,
56            max_extension_depth: 32,
57            max_evidence_entries: 10_000,
58            max_evidence_text_bytes: 8 * MIB,
59            max_code_bytes: 256,
60            max_workers: 16,
61        }
62    }
63}
64
65impl WorktreeLimits {
66    /// Validates that every limit is nonzero and internally consistent.
67    pub fn validate(&self) -> Result<(), WorktreeError> {
68        for (name, value) in [
69            ("max_files", self.max_files),
70            ("max_edits_per_file", self.max_edits_per_file),
71            ("max_source_bytes_per_file", self.max_source_bytes_per_file),
72            ("max_output_bytes_per_file", self.max_output_bytes_per_file),
73            ("max_total_source_bytes", self.max_total_source_bytes),
74            ("max_total_output_bytes", self.max_total_output_bytes),
75            ("max_total_artifact_bytes", self.max_total_artifact_bytes),
76            ("max_journal_bytes", self.max_journal_bytes),
77            ("max_undo_receipts", self.max_undo_receipts),
78            ("max_total_undo_bytes", self.max_total_undo_bytes),
79            ("max_operation_bytes", self.max_operation_bytes),
80            ("max_extension_bytes", self.max_extension_bytes),
81            ("max_extension_nodes", self.max_extension_nodes),
82            ("max_extension_depth", self.max_extension_depth),
83            ("max_evidence_entries", self.max_evidence_entries),
84            ("max_evidence_text_bytes", self.max_evidence_text_bytes),
85            ("max_code_bytes", self.max_code_bytes),
86            ("max_workers", self.max_workers),
87        ] {
88            if value == 0 {
89                return Err(invalid(format!("{name} must be nonzero")));
90            }
91        }
92
93        if self.max_workers > ABSOLUTE_MAX_WORKERS {
94            return Err(invalid(format!(
95                "max_workers may not exceed {ABSOLUTE_MAX_WORKERS}"
96            )));
97        }
98        if self.max_journal_bytes > ABSOLUTE_MAX_JOURNAL_BYTES {
99            return Err(invalid(format!(
100                "max_journal_bytes may not exceed {ABSOLUTE_MAX_JOURNAL_BYTES}"
101            )));
102        }
103        if self.max_undo_receipts > ABSOLUTE_MAX_UNDO_RECEIPTS {
104            return Err(invalid(format!(
105                "max_undo_receipts may not exceed {ABSOLUTE_MAX_UNDO_RECEIPTS}"
106            )));
107        }
108        if self.max_total_undo_bytes > ABSOLUTE_MAX_UNDO_BYTES {
109            return Err(invalid(format!(
110                "max_total_undo_bytes may not exceed {ABSOLUTE_MAX_UNDO_BYTES}"
111            )));
112        }
113        if self.max_total_source_bytes < self.max_source_bytes_per_file {
114            return Err(invalid(
115                "max_total_source_bytes must cover one maximum-size source",
116            ));
117        }
118        if self.max_total_output_bytes < self.max_output_bytes_per_file {
119            return Err(invalid(
120                "max_total_output_bytes must cover one maximum-size output",
121            ));
122        }
123
124        let required_artifacts = self
125            .max_total_source_bytes
126            .checked_add(self.max_total_output_bytes)
127            .ok_or_else(|| invalid("source and output transaction limits overflow"))?;
128        if self.max_total_artifact_bytes < required_artifacts {
129            return Err(invalid(
130                "max_total_artifact_bytes must cover total source plus output bytes",
131            ));
132        }
133        Ok(())
134    }
135}
136
137fn invalid(message: impl Into<String>) -> WorktreeError {
138    WorktreeError::new(
139        WorktreeErrorCode::InvalidOptions,
140        TransactionPhase::Validate,
141        message,
142    )
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::limits::{ABSOLUTE_MAX_WORKERS, WorktreeLimits};
148
149    #[test]
150    fn defaults_match_the_transaction_contract() {
151        let limits = WorktreeLimits::default();
152        assert_eq!(limits.max_files, 64);
153        assert_eq!(limits.max_workers, ABSOLUTE_MAX_WORKERS);
154        assert_eq!(limits.max_total_artifact_bytes, 384 * 1024 * 1024);
155        assert!(limits.validate().is_ok());
156    }
157
158    #[test]
159    fn rejects_inconsistent_and_absolute_limits() {
160        let mut limits = WorktreeLimits::default();
161        limits.max_total_source_bytes = limits.max_source_bytes_per_file - 1;
162        assert!(limits.validate().is_err());
163
164        let limits = WorktreeLimits {
165            max_workers: ABSOLUTE_MAX_WORKERS + 1,
166            ..WorktreeLimits::default()
167        };
168        assert!(limits.validate().is_err());
169    }
170}