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