use crate::error::{TransactionPhase, WorktreeError, WorktreeErrorCode};
const MIB: usize = 1024 * 1024;
pub const ABSOLUTE_MAX_WORKERS: usize = 16;
pub const ABSOLUTE_MAX_JOURNAL_BYTES: usize = 16 * MIB;
pub const ABSOLUTE_MAX_UNDO_RECEIPTS: usize = 1_024;
pub const ABSOLUTE_MAX_UNDO_BYTES: usize = 2 * 1024 * MIB;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorktreeLimits {
pub max_files: usize,
pub max_edits_per_file: usize,
pub max_source_bytes_per_file: usize,
pub max_output_bytes_per_file: usize,
pub max_total_source_bytes: usize,
pub max_total_output_bytes: usize,
pub max_total_artifact_bytes: usize,
pub max_journal_bytes: usize,
pub max_undo_receipts: usize,
pub max_total_undo_bytes: usize,
pub max_operation_bytes: usize,
pub max_extension_bytes: usize,
pub max_extension_nodes: usize,
pub max_extension_depth: usize,
pub max_evidence_entries: usize,
pub max_evidence_text_bytes: usize,
pub max_code_bytes: usize,
pub max_workers: usize,
}
impl Default for WorktreeLimits {
fn default() -> Self {
Self {
max_files: 64,
max_edits_per_file: 2_000,
max_source_bytes_per_file: 16 * MIB,
max_output_bytes_per_file: 64 * MIB,
max_total_source_bytes: 128 * MIB,
max_total_output_bytes: 256 * MIB,
max_total_artifact_bytes: 384 * MIB,
max_journal_bytes: MIB,
max_undo_receipts: 32,
max_total_undo_bytes: 384 * MIB,
max_operation_bytes: 4_096,
max_extension_bytes: 256 * 1024,
max_extension_nodes: 4_096,
max_extension_depth: 32,
max_evidence_entries: 10_000,
max_evidence_text_bytes: 8 * MIB,
max_code_bytes: 256,
max_workers: 16,
}
}
}
impl WorktreeLimits {
pub fn validate(&self) -> Result<(), WorktreeError> {
for (name, value) in [
("max_files", self.max_files),
("max_edits_per_file", self.max_edits_per_file),
("max_source_bytes_per_file", self.max_source_bytes_per_file),
("max_output_bytes_per_file", self.max_output_bytes_per_file),
("max_total_source_bytes", self.max_total_source_bytes),
("max_total_output_bytes", self.max_total_output_bytes),
("max_total_artifact_bytes", self.max_total_artifact_bytes),
("max_journal_bytes", self.max_journal_bytes),
("max_undo_receipts", self.max_undo_receipts),
("max_total_undo_bytes", self.max_total_undo_bytes),
("max_operation_bytes", self.max_operation_bytes),
("max_extension_bytes", self.max_extension_bytes),
("max_extension_nodes", self.max_extension_nodes),
("max_extension_depth", self.max_extension_depth),
("max_evidence_entries", self.max_evidence_entries),
("max_evidence_text_bytes", self.max_evidence_text_bytes),
("max_code_bytes", self.max_code_bytes),
("max_workers", self.max_workers),
] {
if value == 0 {
return Err(invalid(format!("{name} must be nonzero")));
}
}
if self.max_workers > ABSOLUTE_MAX_WORKERS {
return Err(invalid(format!(
"max_workers may not exceed {ABSOLUTE_MAX_WORKERS}"
)));
}
if self.max_journal_bytes > ABSOLUTE_MAX_JOURNAL_BYTES {
return Err(invalid(format!(
"max_journal_bytes may not exceed {ABSOLUTE_MAX_JOURNAL_BYTES}"
)));
}
if self.max_undo_receipts > ABSOLUTE_MAX_UNDO_RECEIPTS {
return Err(invalid(format!(
"max_undo_receipts may not exceed {ABSOLUTE_MAX_UNDO_RECEIPTS}"
)));
}
if self.max_total_undo_bytes > ABSOLUTE_MAX_UNDO_BYTES {
return Err(invalid(format!(
"max_total_undo_bytes may not exceed {ABSOLUTE_MAX_UNDO_BYTES}"
)));
}
if self.max_total_source_bytes < self.max_source_bytes_per_file {
return Err(invalid(
"max_total_source_bytes must cover one maximum-size source",
));
}
if self.max_total_output_bytes < self.max_output_bytes_per_file {
return Err(invalid(
"max_total_output_bytes must cover one maximum-size output",
));
}
let required_artifacts = self
.max_total_source_bytes
.checked_add(self.max_total_output_bytes)
.ok_or_else(|| invalid("source and output transaction limits overflow"))?;
if self.max_total_artifact_bytes < required_artifacts {
return Err(invalid(
"max_total_artifact_bytes must cover total source plus output bytes",
));
}
Ok(())
}
}
fn invalid(message: impl Into<String>) -> WorktreeError {
WorktreeError::new(
WorktreeErrorCode::InvalidOptions,
TransactionPhase::Validate,
message,
)
}
#[cfg(test)]
mod tests {
use crate::limits::{ABSOLUTE_MAX_WORKERS, WorktreeLimits};
#[test]
fn defaults_match_the_transaction_contract() {
let limits = WorktreeLimits::default();
assert_eq!(limits.max_files, 64);
assert_eq!(limits.max_workers, ABSOLUTE_MAX_WORKERS);
assert_eq!(limits.max_total_artifact_bytes, 384 * 1024 * 1024);
assert!(limits.validate().is_ok());
}
#[test]
fn rejects_inconsistent_and_absolute_limits() {
let mut limits = WorktreeLimits::default();
limits.max_total_source_bytes = limits.max_source_bytes_per_file - 1;
assert!(limits.validate().is_err());
let limits = WorktreeLimits {
max_workers: ABSOLUTE_MAX_WORKERS + 1,
..WorktreeLimits::default()
};
assert!(limits.validate().is_err());
}
}