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    /// A run is in progress.
16    Working,
17    /// Nothing runs and nothing waits.
18    Idle,
19}
20
21impl Group {
22    /// Every group, in the order Needs You shows them.
23    pub const ALL: [Self; 4] = [
24        Self::BlockedOnYou,
25        Self::ReadyToReview,
26        Self::Working,
27        Self::Idle,
28    ];
29
30    /// Its recorded name.
31    #[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    /// As people read it.
42    #[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/// What the evidence of finished work leaves to the person, the riskiest
54/// first: read from verdicts, attestations, strength, drift and the tick
55/// commits' trailers — never from what the model said.
56#[derive(
57    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
58)]
59#[serde(rename_all = "snake_case")]
60pub enum Risk {
61    /// Ticks the regression checks alone decided.
62    RegressionOnly,
63    /// Ticks accepted on a person's word.
64    Attested,
65    /// Verified ticks whose acceptance strength is below the target.
66    StrengthBelowTarget,
67    /// Evidence that proves an earlier wording of the spec.
68    Stale,
69    /// Changes outside the paths the task names.
70    ScopeOutside,
71    /// Attempts an external agent made.
72    ExternalExecutor,
73}
74
75impl Risk {
76    /// Every reason, the riskiest first.
77    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    /// Its recorded name.
87    #[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    /// The chip for `n` ticks with this reason.
100    #[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/// A kind of decision waiting for a person, most urgent first.
115#[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    /// The calls waiting in a journal could not be read.
131    Unknown,
132    /// A run waits on an approval.
133    Approval,
134    /// A run's batch of calls waits for one decision.
135    ApprovalBatch,
136    /// A verified attempt is held: its tests reject what the spec allows.
137    Ruling,
138    /// A bug's reproductions wait for the person's confirmation.
139    BugConfirm,
140    /// A bug could not be reproduced.
141    BugCannotReproduce,
142    /// An *Unchanged* statement of a bug does not hold today.
143    BugUnchangedNotHolding,
144    /// A special case survived a bug's reproductions.
145    BugReproWeak,
146    /// The monitor suspects a shortcut.
147    Gaming,
148    /// Most of a feature's ticks are regression-only: one diagnosis.
149    Unverified,
150    /// Proven against an earlier wording.
151    Stale,
152    /// The spec leaves questions open.
153    Question,
154    /// Two independent test authors read a requirement differently: the
155    /// person says which reading is meant, or rewords it.
156    Ambiguous,
157    /// Requirements not approved on their current wording.
158    Review,
159    /// Changes requested on the spec.
160    Revise,
161    /// A step only a person can take: a `[YOU]` task, or one whose attempt
162    /// stopped `BLOCKED` — I did it, a hint and a new attempt, or skip.
163    Yours,
164    /// A gate the person declared blocked a tick.
165    GateBlocked,
166    /// A gate waits for a person's trust before any tick can pass it.
167    HookUntrusted,
168    /// An extension waits for a person's trust before any of it runs.
169    ExtensionUntrusted,
170    /// The last attempt did not pass.
171    Stuck,
172    /// An autopilot run refused calls it would have asked about: listed
173    /// for the person afterwards.
174    AutopilotDenied,
175    /// A bug is already fixed: keep its reproduction, or drop it.
176    BugAlreadySatisfied,
177    /// Changes a run set aside.
178    Aside,
179    /// A quick change or bug that grew: start a spec from it.
180    Promote,
181    /// Planning files a run cannot read yet.
182    Commit,
183    /// Every task done: review and hand it off.
184    Handoff,
185}
186
187impl Kind {
188    /// Every kind, the most urgent first.
189    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    /// Its recorded name.
219    #[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    /// Its group.
252    #[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    /// Its urgency: lower is more urgent.
285    #[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    /// The kind recorded as `name`.
294    #[must_use]
295    pub fn named(name: &str) -> Option<Self> {
296        Self::ALL.iter().copied().find(|k| k.as_str() == name)
297    }
298
299    /// Every recorded name, the most urgent first.
300    #[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        // Today's kinds keep their order among themselves.
329        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        // A kind is blocking or ready, never working or idle.
366        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        // First of its group: before `stale`.
377        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}