1use crate::error::{TransactionPhase, WorktreeError, WorktreeErrorCode};
2
3const MIB: usize = 1024 * 1024;
4
5pub const ABSOLUTE_MAX_WORKERS: usize = 16;
7
8pub const ABSOLUTE_MAX_JOURNAL_BYTES: usize = 16 * MIB;
10
11pub const ABSOLUTE_MAX_UNDO_RECEIPTS: usize = 1_024;
13
14pub const ABSOLUTE_MAX_UNDO_BYTES: usize = 2 * 1024 * MIB;
16
17#[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 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}