1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum Group {
11 BlockedOnYou,
13 ReadyToReview,
15 Working,
17 Idle,
19}
20
21impl Group {
22 pub const ALL: [Self; 4] = [
24 Self::BlockedOnYou,
25 Self::ReadyToReview,
26 Self::Working,
27 Self::Idle,
28 ];
29
30 #[must_use]
32 pub const fn as_str(self) -> &'static str {
33 match self {
34 Self::BlockedOnYou => "blocked_on_you",
35 Self::ReadyToReview => "ready_to_review",
36 Self::Working => "working",
37 Self::Idle => "idle",
38 }
39 }
40
41 #[must_use]
43 pub const fn title(self) -> &'static str {
44 match self {
45 Self::BlockedOnYou => "Blocked on you",
46 Self::ReadyToReview => "Ready to review",
47 Self::Working => "Working",
48 Self::Idle => "Idle",
49 }
50 }
51}
52
53#[derive(
57 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
58)]
59#[serde(rename_all = "snake_case")]
60pub enum Risk {
61 RegressionOnly,
63 Attested,
65 StrengthBelowTarget,
67 Stale,
69 ScopeOutside,
71 ExternalExecutor,
73}
74
75impl Risk {
76 pub const ALL: [Self; 6] = [
78 Self::RegressionOnly,
79 Self::Attested,
80 Self::StrengthBelowTarget,
81 Self::Stale,
82 Self::ScopeOutside,
83 Self::ExternalExecutor,
84 ];
85
86 #[must_use]
88 pub const fn as_str(self) -> &'static str {
89 match self {
90 Self::RegressionOnly => "regression_only",
91 Self::Attested => "attested",
92 Self::StrengthBelowTarget => "strength_below_target",
93 Self::Stale => "stale",
94 Self::ScopeOutside => "scope_outside",
95 Self::ExternalExecutor => "external_executor",
96 }
97 }
98
99 #[must_use]
101 pub fn chip(self, n: usize) -> String {
102 let ticks = if n == 1 { "tick" } else { "ticks" };
103 match self {
104 Self::RegressionOnly => format!("{n} regression-only"),
105 Self::Attested => format!("{n} attested"),
106 Self::StrengthBelowTarget => format!("{n} below strength target"),
107 Self::Stale => format!("{n} stale"),
108 Self::ScopeOutside => format!("{n} {ticks} outside scope"),
109 Self::ExternalExecutor => format!("{n} by an agent"),
110 }
111 }
112}
113
114#[derive(
116 Debug,
117 Clone,
118 Copy,
119 PartialEq,
120 Eq,
121 Hash,
122 PartialOrd,
123 Ord,
124 Serialize,
125 Deserialize,
126 schemars::JsonSchema,
127)]
128#[serde(rename_all = "snake_case")]
129pub enum Kind {
130 Unknown,
132 Approval,
134 ApprovalBatch,
136 Ruling,
138 BugConfirm,
140 BugCannotReproduce,
142 BugUnchangedNotHolding,
144 BugReproWeak,
146 Gaming,
148 Unverified,
150 Stale,
152 Question,
154 Ambiguous,
157 Review,
159 Revise,
161 Yours,
164 Stuck,
166 AutopilotDenied,
169 BugAlreadySatisfied,
171 Aside,
173 Commit,
175 Handoff,
177}
178
179impl Kind {
180 pub const ALL: [Self; 22] = [
182 Self::Unknown,
183 Self::Approval,
184 Self::ApprovalBatch,
185 Self::Ruling,
186 Self::BugConfirm,
187 Self::BugCannotReproduce,
188 Self::BugUnchangedNotHolding,
189 Self::BugReproWeak,
190 Self::Gaming,
191 Self::Unverified,
192 Self::Stale,
193 Self::Question,
194 Self::Ambiguous,
195 Self::Review,
196 Self::Revise,
197 Self::Yours,
198 Self::Stuck,
199 Self::AutopilotDenied,
200 Self::BugAlreadySatisfied,
201 Self::Aside,
202 Self::Commit,
203 Self::Handoff,
204 ];
205
206 #[must_use]
208 pub const fn as_str(self) -> &'static str {
209 match self {
210 Self::Unknown => "unknown",
211 Self::Approval => "approval",
212 Self::ApprovalBatch => "approval_batch",
213 Self::Ruling => "ruling",
214 Self::BugConfirm => "bug_confirm",
215 Self::BugCannotReproduce => "bug_cannot_reproduce",
216 Self::BugUnchangedNotHolding => "bug_unchanged_not_holding",
217 Self::BugReproWeak => "bug_repro_weak",
218 Self::Gaming => "gaming",
219 Self::Unverified => "unverified",
220 Self::Stale => "stale",
221 Self::Question => "question",
222 Self::Ambiguous => "ambiguous",
223 Self::Review => "review",
224 Self::Revise => "revise",
225 Self::Yours => "yours",
226 Self::Stuck => "stuck",
227 Self::AutopilotDenied => "autopilot_denied",
228 Self::BugAlreadySatisfied => "bug_already_satisfied",
229 Self::Aside => "aside",
230 Self::Commit => "commit",
231 Self::Handoff => "handoff",
232 }
233 }
234
235 #[must_use]
237 pub const fn group(self) -> Group {
238 match self {
239 Self::Unknown
240 | Self::Approval
241 | Self::ApprovalBatch
242 | Self::Ruling
243 | Self::BugConfirm
244 | Self::BugCannotReproduce
245 | Self::BugUnchangedNotHolding
246 | Self::BugReproWeak
247 | Self::Gaming
248 | Self::Question
249 | Self::Ambiguous
250 | Self::Revise
251 | Self::Yours
252 | Self::Stuck
253 | Self::Commit => Group::BlockedOnYou,
254 Self::Unverified
255 | Self::Stale
256 | Self::AutopilotDenied
257 | Self::Review
258 | Self::BugAlreadySatisfied
259 | Self::Aside
260 | Self::Handoff => Group::ReadyToReview,
261 }
262 }
263
264 #[must_use]
266 pub fn rank(self) -> usize {
267 Self::ALL
268 .iter()
269 .position(|k| *k == self)
270 .unwrap_or(Self::ALL.len())
271 }
272
273 #[must_use]
275 pub fn named(name: &str) -> Option<Self> {
276 Self::ALL.iter().copied().find(|k| k.as_str() == name)
277 }
278
279 #[must_use]
281 pub fn names() -> [&'static str; 22] {
282 Self::ALL.map(Self::as_str)
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn every_kind_has_one_name_a_group_and_its_place() {
292 let schema = serde_json::to_value(schemars::schema_for!(Kind)).unwrap_or_default();
293 let mut named: Vec<String> = schema["oneOf"]
294 .as_array()
295 .into_iter()
296 .flatten()
297 .filter_map(|v| v["const"].as_str().map(str::to_owned))
298 .collect();
299 named.sort();
300 let mut listed: Vec<String> = Kind::names().iter().map(|n| (*n).to_owned()).collect();
301 listed.sort();
302 assert_eq!(named, listed, "Kind::ALL is not the enum");
303 for (i, k) in Kind::ALL.iter().enumerate() {
304 assert_eq!(k.rank(), i);
305 assert_eq!(Kind::named(k.as_str()), Some(*k));
306 assert_eq!(serde_json::to_value(k).unwrap_or_default(), k.as_str());
307 }
308 let today: Vec<&str> = Kind::names()
310 .into_iter()
311 .filter(|n| {
312 !n.starts_with("bug_")
313 && !matches!(
314 *n,
315 "ambiguous" | "autopilot_denied" | "unverified" | "yours"
316 )
317 })
318 .collect();
319 assert_eq!(
320 today,
321 [
322 "unknown",
323 "approval",
324 "approval_batch",
325 "ruling",
326 "gaming",
327 "stale",
328 "question",
329 "review",
330 "revise",
331 "stuck",
332 "aside",
333 "commit",
334 "handoff"
335 ]
336 );
337 assert_eq!(Kind::BugAlreadySatisfied.group(), Group::ReadyToReview);
338 assert!(
340 Kind::ALL
341 .iter()
342 .all(|k| matches!(k.group(), Group::BlockedOnYou | Group::ReadyToReview))
343 );
344 let mut risks = Risk::ALL.to_vec();
345 risks.sort();
346 assert_eq!(risks, Risk::ALL, "Risk::ALL is the riskiest-first order");
347 assert_eq!(Kind::Ambiguous.group(), Group::BlockedOnYou);
348 assert_eq!(Kind::AutopilotDenied.group(), Group::ReadyToReview);
349 assert_eq!(Kind::Unverified.group(), Group::ReadyToReview);
351 assert_eq!(Kind::Unverified.rank() + 1, Kind::Stale.rank());
352 for k in [
353 Kind::BugConfirm,
354 Kind::BugCannotReproduce,
355 Kind::BugUnchangedNotHolding,
356 Kind::BugReproWeak,
357 ] {
358 assert_eq!(k.group(), Group::BlockedOnYou, "{k:?}");
359 }
360 }
361}