1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[non_exhaustive]
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum WorkUnitKind {
8 Feature,
9 Issue,
10 Review,
11 Ops,
12}
13
14impl WorkUnitKind {
15 pub const fn as_str(self) -> &'static str {
16 match self {
17 Self::Feature => "feature",
18 Self::Issue => "issue",
19 Self::Review => "review",
20 Self::Ops => "ops",
21 }
22 }
23
24 pub fn parse(raw: &str) -> Option<Self> {
25 let normalized = raw.trim().to_ascii_lowercase();
26 match normalized.as_str() {
27 "feature" => Some(Self::Feature),
28 "issue" => Some(Self::Issue),
29 "review" => Some(Self::Review),
30 "ops" => Some(Self::Ops),
31 _ => None,
32 }
33 }
34}
35
36#[non_exhaustive]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum WorkSourceKind {
40 Manual,
41 Discord,
42 Github,
43}
44
45impl WorkSourceKind {
46 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Manual => "manual",
49 Self::Discord => "discord",
50 Self::Github => "github",
51 }
52 }
53
54 pub fn parse(raw: &str) -> Option<Self> {
55 let normalized = raw.trim().to_ascii_lowercase();
56 match normalized.as_str() {
57 "manual" => Some(Self::Manual),
58 "discord" => Some(Self::Discord),
59 "github" => Some(Self::Github),
60 _ => None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct WorkUnitSourceRef {
67 pub source_kind: WorkSourceKind,
68 pub project_id: Option<String>,
69 pub channel_id: Option<String>,
70 pub thread_id: Option<String>,
71 pub message_id: Option<String>,
72 pub external_ref: Option<String>,
73 pub source_url: Option<String>,
74}
75
76impl Default for WorkUnitSourceRef {
77 fn default() -> Self {
78 Self {
79 source_kind: WorkSourceKind::Manual,
80 project_id: None,
81 channel_id: None,
82 thread_id: None,
83 message_id: None,
84 external_ref: None,
85 source_url: None,
86 }
87 }
88}
89
90#[non_exhaustive]
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum WorkUnitPriority {
94 Low,
95 Normal,
96 High,
97 Critical,
98}
99
100impl WorkUnitPriority {
101 pub const fn as_str(self) -> &'static str {
102 match self {
103 Self::Low => "low",
104 Self::Normal => "normal",
105 Self::High => "high",
106 Self::Critical => "critical",
107 }
108 }
109
110 pub fn parse(raw: &str) -> Option<Self> {
111 let normalized = raw.trim().to_ascii_lowercase();
112 match normalized.as_str() {
113 "low" => Some(Self::Low),
114 "normal" => Some(Self::Normal),
115 "high" => Some(Self::High),
116 "critical" => Some(Self::Critical),
117 _ => None,
118 }
119 }
120}
121
122#[non_exhaustive]
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum WorkUnitStatus {
126 Captured,
127 Triaged,
128 Ready,
129 Leased,
130 Running,
131 WaitingExternal,
132 WaitingReview,
133 RetryPending,
134 Completed,
135 FailedTerminal,
136 Cancelled,
137 Archived,
138}
139
140impl WorkUnitStatus {
141 pub const fn as_str(self) -> &'static str {
142 match self {
143 Self::Captured => "captured",
144 Self::Triaged => "triaged",
145 Self::Ready => "ready",
146 Self::Leased => "leased",
147 Self::Running => "running",
148 Self::WaitingExternal => "waiting_external",
149 Self::WaitingReview => "waiting_review",
150 Self::RetryPending => "retry_pending",
151 Self::Completed => "completed",
152 Self::FailedTerminal => "failed_terminal",
153 Self::Cancelled => "cancelled",
154 Self::Archived => "archived",
155 }
156 }
157
158 pub fn parse(raw: &str) -> Option<Self> {
159 let normalized = raw.trim().to_ascii_lowercase();
160 match normalized.as_str() {
161 "captured" => Some(Self::Captured),
162 "triaged" => Some(Self::Triaged),
163 "ready" => Some(Self::Ready),
164 "leased" => Some(Self::Leased),
165 "running" => Some(Self::Running),
166 "waiting_external" => Some(Self::WaitingExternal),
167 "waiting_review" => Some(Self::WaitingReview),
168 "retry_pending" => Some(Self::RetryPending),
169 "completed" => Some(Self::Completed),
170 "failed_terminal" => Some(Self::FailedTerminal),
171 "cancelled" => Some(Self::Cancelled),
172 "archived" => Some(Self::Archived),
173 _ => None,
174 }
175 }
176
177 pub const fn is_terminal(self) -> bool {
178 matches!(
179 self,
180 Self::Completed | Self::FailedTerminal | Self::Cancelled | Self::Archived
181 )
182 }
183
184 pub const fn is_ready_for_lease(self) -> bool {
185 matches!(self, Self::Ready | Self::RetryPending)
186 }
187
188 pub const fn is_blocked(self) -> bool {
189 matches!(self, Self::WaitingExternal | Self::WaitingReview)
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct WorkUnitRetryPolicy {
195 pub max_attempts: u32,
196 pub initial_backoff_ms: u64,
197 pub max_backoff_ms: u64,
198}
199
200impl Default for WorkUnitRetryPolicy {
201 fn default() -> Self {
202 Self {
203 max_attempts: 3,
204 initial_backoff_ms: 1_000,
205 max_backoff_ms: 60_000,
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct WorkUnitRecord {
212 pub work_unit_id: String,
213 pub kind: WorkUnitKind,
214 pub title: String,
215 pub description: String,
216 pub source_ref: WorkUnitSourceRef,
217 pub status: WorkUnitStatus,
218 pub priority: WorkUnitPriority,
219 pub assigned_to: Option<String>,
220 pub retry_policy: WorkUnitRetryPolicy,
221 pub attempt_count: u32,
222 pub next_run_at_ms: i64,
223 pub last_error: Option<String>,
224 pub blocking_reason: Option<String>,
225 pub parent_work_unit_id: Option<String>,
226 pub blocks_work_unit_ids: Vec<String>,
227 pub blocked_by_work_unit_ids: Vec<String>,
228 pub result_payload_json: Option<Value>,
229 pub created_at_ms: i64,
230 pub updated_at_ms: i64,
231 pub archived_at_ms: Option<i64>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct WorkUnitLeaseRecord {
236 pub work_unit_id: String,
237 pub owner: String,
238 pub lease_version: u64,
239 pub acquired_at_ms: i64,
240 pub heartbeat_at_ms: i64,
241 pub expires_at_ms: i64,
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct WorkUnitSnapshot {
246 pub work_unit: WorkUnitRecord,
247 pub lease: Option<WorkUnitLeaseRecord>,
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251pub struct WorkUnitEventRecord {
252 pub sequence_id: i64,
253 pub work_unit_id: String,
254 pub event_kind: String,
255 pub actor: Option<String>,
256 pub payload_json: Value,
257 pub recorded_at_ms: i64,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261pub struct WorkRuntimeHealthSnapshot {
262 pub total_count: usize,
263 pub ready_count: usize,
264 pub leased_count: usize,
265 pub running_count: usize,
266 pub blocked_count: usize,
267 pub retry_pending_count: usize,
268 pub terminal_count: usize,
269 pub archived_count: usize,
270 pub expired_lease_count: usize,
271}