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 Promote,
175 Commit,
177 Handoff,
179}
180
181impl Kind {
182 pub const ALL: [Self; 23] = [
184 Self::Unknown,
185 Self::Approval,
186 Self::ApprovalBatch,
187 Self::Ruling,
188 Self::BugConfirm,
189 Self::BugCannotReproduce,
190 Self::BugUnchangedNotHolding,
191 Self::BugReproWeak,
192 Self::Gaming,
193 Self::Unverified,
194 Self::Stale,
195 Self::Question,
196 Self::Ambiguous,
197 Self::Review,
198 Self::Revise,
199 Self::Yours,
200 Self::Stuck,
201 Self::AutopilotDenied,
202 Self::BugAlreadySatisfied,
203 Self::Aside,
204 Self::Promote,
205 Self::Commit,
206 Self::Handoff,
207 ];
208
209 #[must_use]
211 pub const fn as_str(self) -> &'static str {
212 match self {
213 Self::Unknown => "unknown",
214 Self::Approval => "approval",
215 Self::ApprovalBatch => "approval_batch",
216 Self::Ruling => "ruling",
217 Self::BugConfirm => "bug_confirm",
218 Self::BugCannotReproduce => "bug_cannot_reproduce",
219 Self::BugUnchangedNotHolding => "bug_unchanged_not_holding",
220 Self::BugReproWeak => "bug_repro_weak",
221 Self::Gaming => "gaming",
222 Self::Unverified => "unverified",
223 Self::Stale => "stale",
224 Self::Question => "question",
225 Self::Ambiguous => "ambiguous",
226 Self::Review => "review",
227 Self::Revise => "revise",
228 Self::Yours => "yours",
229 Self::Stuck => "stuck",
230 Self::AutopilotDenied => "autopilot_denied",
231 Self::BugAlreadySatisfied => "bug_already_satisfied",
232 Self::Aside => "aside",
233 Self::Promote => "promote",
234 Self::Commit => "commit",
235 Self::Handoff => "handoff",
236 }
237 }
238
239 #[must_use]
241 pub const fn group(self) -> Group {
242 match self {
243 Self::Unknown
244 | Self::Approval
245 | Self::ApprovalBatch
246 | Self::Ruling
247 | Self::BugConfirm
248 | Self::BugCannotReproduce
249 | Self::BugUnchangedNotHolding
250 | Self::BugReproWeak
251 | Self::Gaming
252 | Self::Question
253 | Self::Ambiguous
254 | Self::Revise
255 | Self::Yours
256 | Self::Stuck
257 | Self::Commit => Group::BlockedOnYou,
258 Self::Unverified
259 | Self::Stale
260 | Self::AutopilotDenied
261 | Self::Review
262 | Self::BugAlreadySatisfied
263 | Self::Aside
264 | Self::Promote
265 | Self::Handoff => Group::ReadyToReview,
266 }
267 }
268
269 #[must_use]
271 pub fn rank(self) -> usize {
272 Self::ALL
273 .iter()
274 .position(|k| *k == self)
275 .unwrap_or(Self::ALL.len())
276 }
277
278 #[must_use]
280 pub fn named(name: &str) -> Option<Self> {
281 Self::ALL.iter().copied().find(|k| k.as_str() == name)
282 }
283
284 #[must_use]
286 pub fn names() -> [&'static str; 23] {
287 Self::ALL.map(Self::as_str)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn every_kind_has_one_name_a_group_and_its_place() {
297 let schema = serde_json::to_value(schemars::schema_for!(Kind)).unwrap_or_default();
298 let mut named: Vec<String> = schema["oneOf"]
299 .as_array()
300 .into_iter()
301 .flatten()
302 .filter_map(|v| v["const"].as_str().map(str::to_owned))
303 .collect();
304 named.sort();
305 let mut listed: Vec<String> = Kind::names().iter().map(|n| (*n).to_owned()).collect();
306 listed.sort();
307 assert_eq!(named, listed, "Kind::ALL is not the enum");
308 for (i, k) in Kind::ALL.iter().enumerate() {
309 assert_eq!(k.rank(), i);
310 assert_eq!(Kind::named(k.as_str()), Some(*k));
311 assert_eq!(serde_json::to_value(k).unwrap_or_default(), k.as_str());
312 }
313 let today: Vec<&str> = Kind::names()
315 .into_iter()
316 .filter(|n| {
317 !n.starts_with("bug_")
318 && !matches!(
319 *n,
320 "ambiguous" | "autopilot_denied" | "unverified" | "yours" | "promote"
321 )
322 })
323 .collect();
324 assert_eq!(
325 today,
326 [
327 "unknown",
328 "approval",
329 "approval_batch",
330 "ruling",
331 "gaming",
332 "stale",
333 "question",
334 "review",
335 "revise",
336 "stuck",
337 "aside",
338 "commit",
339 "handoff"
340 ]
341 );
342 assert_eq!(Kind::BugAlreadySatisfied.group(), Group::ReadyToReview);
343 assert!(
345 Kind::ALL
346 .iter()
347 .all(|k| matches!(k.group(), Group::BlockedOnYou | Group::ReadyToReview))
348 );
349 let mut risks = Risk::ALL.to_vec();
350 risks.sort();
351 assert_eq!(risks, Risk::ALL, "Risk::ALL is the riskiest-first order");
352 assert_eq!(Kind::Ambiguous.group(), Group::BlockedOnYou);
353 assert_eq!(Kind::AutopilotDenied.group(), Group::ReadyToReview);
354 assert_eq!(Kind::Unverified.group(), Group::ReadyToReview);
356 assert_eq!(Kind::Unverified.rank() + 1, Kind::Stale.rank());
357 for k in [
358 Kind::BugConfirm,
359 Kind::BugCannotReproduce,
360 Kind::BugUnchangedNotHolding,
361 Kind::BugReproWeak,
362 ] {
363 assert_eq!(k.group(), Group::BlockedOnYou, "{k:?}");
364 }
365 }
366}