Skip to main content

speccoder_core/
inbox.rs

1//! The kinds of Needs You items: one registry, each kind with its group
2//! and urgency rank. A feature adds a kind here, never a free
3//! string; the runtime's list is derived from it.
4
5use serde::{Deserialize, Serialize};
6
7/// Where an item puts the work.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum Group {
11    /// Nothing moves until the person decides.
12    BlockedOnYou,
13    /// Work is done or held; the person reviews it.
14    ReadyToReview,
15}
16
17impl Group {
18    /// Its recorded name.
19    #[must_use]
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::BlockedOnYou => "blocked_on_you",
23            Self::ReadyToReview => "ready_to_review",
24        }
25    }
26
27    /// As people read it.
28    #[must_use]
29    pub const fn title(self) -> &'static str {
30        match self {
31            Self::BlockedOnYou => "Blocked on you",
32            Self::ReadyToReview => "Ready to review",
33        }
34    }
35}
36
37/// A kind of decision waiting for a person, most urgent first.
38#[derive(
39    Debug,
40    Clone,
41    Copy,
42    PartialEq,
43    Eq,
44    Hash,
45    PartialOrd,
46    Ord,
47    Serialize,
48    Deserialize,
49    schemars::JsonSchema,
50)]
51#[serde(rename_all = "snake_case")]
52pub enum Kind {
53    /// The calls waiting in a journal could not be read.
54    Unknown,
55    /// A run waits on an approval.
56    Approval,
57    /// A run's batch of calls waits for one decision.
58    ApprovalBatch,
59    /// A verified attempt is held: its tests reject what the spec allows.
60    Ruling,
61    /// A bug's reproductions wait for the person's confirmation.
62    BugConfirm,
63    /// A bug could not be reproduced.
64    BugCannotReproduce,
65    /// An *Unchanged* statement of a bug does not hold today.
66    BugUnchangedNotHolding,
67    /// A special case survived a bug's reproductions.
68    BugReproWeak,
69    /// The monitor suspects a shortcut.
70    Gaming,
71    /// Most of a feature's ticks are regression-only: one diagnosis.
72    Unverified,
73    /// Proven against an earlier wording.
74    Stale,
75    /// The spec leaves questions open.
76    Question,
77    /// Two independent test authors read a requirement differently: the
78    /// person says which reading is meant, or rewords it.
79    Ambiguous,
80    /// Requirements not approved on their current wording.
81    Review,
82    /// Changes requested on the spec.
83    Revise,
84    /// A step only a person can take: a `[YOU]` task, or one whose attempt
85    /// stopped `BLOCKED` — I did it, a hint and a new attempt, or skip.
86    Yours,
87    /// The last attempt did not pass.
88    Stuck,
89    /// An autopilot run refused calls it would have asked about: listed
90    /// for the person afterwards.
91    AutopilotDenied,
92    /// A bug is already fixed: keep its reproduction, or drop it.
93    BugAlreadySatisfied,
94    /// Changes a run set aside.
95    Aside,
96    /// Planning files a run cannot read yet.
97    Commit,
98    /// Every task done: review and hand it off.
99    Handoff,
100}
101
102impl Kind {
103    /// Every kind, the most urgent first.
104    pub const ALL: [Self; 22] = [
105        Self::Unknown,
106        Self::Approval,
107        Self::ApprovalBatch,
108        Self::Ruling,
109        Self::BugConfirm,
110        Self::BugCannotReproduce,
111        Self::BugUnchangedNotHolding,
112        Self::BugReproWeak,
113        Self::Gaming,
114        Self::Unverified,
115        Self::Stale,
116        Self::Question,
117        Self::Ambiguous,
118        Self::Review,
119        Self::Revise,
120        Self::Yours,
121        Self::Stuck,
122        Self::AutopilotDenied,
123        Self::BugAlreadySatisfied,
124        Self::Aside,
125        Self::Commit,
126        Self::Handoff,
127    ];
128
129    /// Its recorded name.
130    #[must_use]
131    pub const fn as_str(self) -> &'static str {
132        match self {
133            Self::Unknown => "unknown",
134            Self::Approval => "approval",
135            Self::ApprovalBatch => "approval_batch",
136            Self::Ruling => "ruling",
137            Self::BugConfirm => "bug_confirm",
138            Self::BugCannotReproduce => "bug_cannot_reproduce",
139            Self::BugUnchangedNotHolding => "bug_unchanged_not_holding",
140            Self::BugReproWeak => "bug_repro_weak",
141            Self::Gaming => "gaming",
142            Self::Unverified => "unverified",
143            Self::Stale => "stale",
144            Self::Question => "question",
145            Self::Ambiguous => "ambiguous",
146            Self::Review => "review",
147            Self::Revise => "revise",
148            Self::Yours => "yours",
149            Self::Stuck => "stuck",
150            Self::AutopilotDenied => "autopilot_denied",
151            Self::BugAlreadySatisfied => "bug_already_satisfied",
152            Self::Aside => "aside",
153            Self::Commit => "commit",
154            Self::Handoff => "handoff",
155        }
156    }
157
158    /// Its group.
159    #[must_use]
160    pub const fn group(self) -> Group {
161        match self {
162            Self::Unknown
163            | Self::Approval
164            | Self::ApprovalBatch
165            | Self::Ruling
166            | Self::BugConfirm
167            | Self::BugCannotReproduce
168            | Self::BugUnchangedNotHolding
169            | Self::BugReproWeak
170            | Self::Gaming
171            | Self::Question
172            | Self::Ambiguous
173            | Self::Revise
174            | Self::Yours
175            | Self::Stuck
176            | Self::Commit => Group::BlockedOnYou,
177            Self::Unverified
178            | Self::Stale
179            | Self::AutopilotDenied
180            | Self::Review
181            | Self::BugAlreadySatisfied
182            | Self::Aside
183            | Self::Handoff => Group::ReadyToReview,
184        }
185    }
186
187    /// Its urgency: lower is more urgent.
188    #[must_use]
189    pub fn rank(self) -> usize {
190        Self::ALL
191            .iter()
192            .position(|k| *k == self)
193            .unwrap_or(Self::ALL.len())
194    }
195
196    /// The kind recorded as `name`.
197    #[must_use]
198    pub fn named(name: &str) -> Option<Self> {
199        Self::ALL.iter().copied().find(|k| k.as_str() == name)
200    }
201
202    /// Every recorded name, the most urgent first.
203    #[must_use]
204    pub fn names() -> [&'static str; 22] {
205        Self::ALL.map(Self::as_str)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn every_kind_has_one_name_a_group_and_its_place() {
215        let schema = serde_json::to_value(schemars::schema_for!(Kind)).unwrap_or_default();
216        let mut named: Vec<String> = schema["oneOf"]
217            .as_array()
218            .into_iter()
219            .flatten()
220            .filter_map(|v| v["const"].as_str().map(str::to_owned))
221            .collect();
222        named.sort();
223        let mut listed: Vec<String> = Kind::names().iter().map(|n| (*n).to_owned()).collect();
224        listed.sort();
225        assert_eq!(named, listed, "Kind::ALL is not the enum");
226        for (i, k) in Kind::ALL.iter().enumerate() {
227            assert_eq!(k.rank(), i);
228            assert_eq!(Kind::named(k.as_str()), Some(*k));
229            assert_eq!(serde_json::to_value(k).unwrap_or_default(), k.as_str());
230        }
231        // Today's kinds keep their order among themselves.
232        let today: Vec<&str> = Kind::names()
233            .into_iter()
234            .filter(|n| {
235                !n.starts_with("bug_")
236                    && !matches!(
237                        *n,
238                        "ambiguous" | "autopilot_denied" | "unverified" | "yours"
239                    )
240            })
241            .collect();
242        assert_eq!(
243            today,
244            [
245                "unknown",
246                "approval",
247                "approval_batch",
248                "ruling",
249                "gaming",
250                "stale",
251                "question",
252                "review",
253                "revise",
254                "stuck",
255                "aside",
256                "commit",
257                "handoff"
258            ]
259        );
260        assert_eq!(Kind::BugAlreadySatisfied.group(), Group::ReadyToReview);
261        assert_eq!(Kind::Ambiguous.group(), Group::BlockedOnYou);
262        assert_eq!(Kind::AutopilotDenied.group(), Group::ReadyToReview);
263        // First of its group: before `stale`.
264        assert_eq!(Kind::Unverified.group(), Group::ReadyToReview);
265        assert_eq!(Kind::Unverified.rank() + 1, Kind::Stale.rank());
266        for k in [
267            Kind::BugConfirm,
268            Kind::BugCannotReproduce,
269            Kind::BugUnchangedNotHolding,
270            Kind::BugReproWeak,
271        ] {
272            assert_eq!(k.group(), Group::BlockedOnYou, "{k:?}");
273        }
274    }
275}