ralph/contracts/config/queue.rs
1//! Queue configuration structs and aging thresholds.
2//!
3//! Responsibilities:
4//! - Define queue-related configuration structs and merge behavior.
5//!
6//! Not handled here:
7//! - Queue file IO (see `crate::queue` module).
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13/// Aging threshold configuration for `ralph queue aging`.
14///
15/// Controls the day thresholds for categorizing tasks by age.
16/// Ordering invariant: warning_days < stale_days < rotten_days
17#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
18#[serde(default, deny_unknown_fields)]
19pub struct QueueAgingThresholds {
20 /// Warn when task age is strictly greater than this many days.
21 #[schemars(range(min = 0, max = 3650))]
22 pub warning_days: Option<u32>,
23
24 /// Mark as stale when age is strictly greater than this many days.
25 #[schemars(range(min = 0, max = 3650))]
26 pub stale_days: Option<u32>,
27
28 /// Mark as rotten when age is strictly greater than this many days.
29 #[schemars(range(min = 0, max = 3650))]
30 pub rotten_days: Option<u32>,
31}
32
33/// Queue-related configuration.
34#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
35#[serde(default, deny_unknown_fields)]
36pub struct QueueConfig {
37 /// Path to the JSON queue file, relative to repo root.
38 ///
39 /// Paths are intended to be repo-root relative. Parallel mode requires the
40 /// resolved path to be under the repo root (no `..`) so it can be copied
41 /// into workspace clones.
42 pub file: Option<PathBuf>,
43
44 /// Path to the JSON done archive file, relative to repo root.
45 ///
46 /// Paths are intended to be repo-root relative. Parallel mode requires the
47 /// resolved path to be under the repo root (no `..`) so it can be copied
48 /// into workspace clones.
49 pub done_file: Option<PathBuf>,
50
51 /// ID prefix (default: "RQ").
52 pub id_prefix: Option<String>,
53
54 /// Zero pad width for the numeric suffix (default: 4 -> RQ-0001).
55 #[schemars(range(min = 1, max = 255))]
56 pub id_width: Option<u8>,
57
58 /// Warning threshold for queue file size in KB (default: 500).
59 #[schemars(range(min = 100, max = 10000))]
60 pub size_warning_threshold_kb: Option<u32>,
61
62 /// Warning threshold for number of tasks in queue (default: 500).
63 #[schemars(range(min = 50, max = 5000))]
64 pub task_count_warning_threshold: Option<u32>,
65
66 /// Maximum allowed dependency chain depth before warning (default: 10).
67 #[schemars(range(min = 1, max = 100))]
68 pub max_dependency_depth: Option<u8>,
69
70 /// Auto-archive terminal tasks (done/rejected) from queue to done after this many days.
71 ///
72 /// Semantics:
73 /// - None: disabled (default)
74 /// - Some(0): archive immediately when the sweep runs
75 /// - Some(N): archive when completed_at is at least N days old
76 ///
77 /// The sweep runs after selected queue mutation operations (e.g., task edits and run supervision).
78 /// Tasks with missing or invalid completed_at timestamps are not moved when N > 0.
79 #[schemars(range(min = 0, max = 3650))]
80 pub auto_archive_terminal_after_days: Option<u32>,
81
82 /// Thresholds for `ralph queue aging` buckets.
83 ///
84 /// Default: warning>7d, stale>14d, rotten>30d.
85 /// Ordering must satisfy: warning_days < stale_days < rotten_days.
86 pub aging_thresholds: Option<QueueAgingThresholds>,
87}
88
89impl QueueConfig {
90 pub fn merge_from(&mut self, other: Self) {
91 if other.file.is_some() {
92 self.file = other.file;
93 }
94 if other.done_file.is_some() {
95 self.done_file = other.done_file;
96 }
97 if other.id_prefix.is_some() {
98 self.id_prefix = other.id_prefix;
99 }
100 if other.id_width.is_some() {
101 self.id_width = other.id_width;
102 }
103 if other.size_warning_threshold_kb.is_some() {
104 self.size_warning_threshold_kb = other.size_warning_threshold_kb;
105 }
106 if other.task_count_warning_threshold.is_some() {
107 self.task_count_warning_threshold = other.task_count_warning_threshold;
108 }
109 if other.max_dependency_depth.is_some() {
110 self.max_dependency_depth = other.max_dependency_depth;
111 }
112 if other.auto_archive_terminal_after_days.is_some() {
113 self.auto_archive_terminal_after_days = other.auto_archive_terminal_after_days;
114 }
115 if other.aging_thresholds.is_some() {
116 self.aging_thresholds = other.aging_thresholds;
117 }
118 }
119}