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