Skip to main content

speccoder_core/
event.rs

1//! The names of `--events` lines: one closed list. A line whose type is
2//! not here is never emitted; a new name is lowercase `noun.verb`. Domain
3//! events (a record backs them) publish a projection schema; progress
4//! events are stream-only.
5
6/// What kind of event a name is.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Class {
9    /// A record backs it; its public projection is published.
10    Domain,
11    /// Stream-only.
12    Progress,
13}
14
15/// One registered name.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Name {
18    /// The `type` of the line.
19    pub name: &'static str,
20    /// Domain or progress.
21    pub class: Class,
22}
23
24const fn p(name: &'static str) -> Name {
25    Name {
26        name,
27        class: Class::Progress,
28    }
29}
30
31const fn d(name: &'static str) -> Name {
32    Name {
33        name,
34        class: Class::Domain,
35    }
36}
37
38/// Names that predate the rule (single words, an underscore); kept.
39pub const GRANDFATHERED: &[&str] = &[
40    "tool_call",
41    "usage",
42    "said",
43    "reasoning",
44    "notice",
45    "result",
46];
47
48/// Every name a stream may carry.
49pub const REGISTRY: &[Name] = &[
50    p("session.started"),
51    p("run.started"),
52    p("task.started"),
53    p("stage.started"),
54    p("stage.finished"),
55    p("task.resumed"),
56    p("tool_call"),
57    p("approval.requested"),
58    p("approval.decided"),
59    p("usage"),
60    p("task.finished"),
61    p("said"),
62    p("reasoning"),
63    p("notice"),
64    p("result"),
65    d("bug.recorded"),
66    p("bug.reproduce_try"),
67    p("bug.reproduced"),
68    p("bug.confirm_waiting"),
69    d("bug.confirmed"),
70    d("bug.confirm_rejected"),
71    p("bug.unchanged_selected"),
72    p("bug.discrimination"),
73    d("bug.outcome"),
74    p("oracle.stage"),
75    d("oracle.frozen"),
76    d("oracle.lapsed"),
77    d("collection.checked"),
78    p("regression.bisected"),
79    d("project.created"),
80    d("project.undone"),
81    p("scaffold.started"),
82    p("scaffold.finished"),
83    p("planning.stopped"),
84];
85
86/// The registered name `name`.
87#[must_use]
88pub fn lookup(name: &str) -> Option<&'static Name> {
89    REGISTRY.iter().find(|n| n.name == name)
90}
91
92/// A new name's rule: lowercase words joined by one dot
93/// (`[a-z_]+\.[a-z_]+`).
94#[must_use]
95pub fn well_formed(name: &str) -> bool {
96    let word = |w: &str| !w.is_empty() && w.chars().all(|c| c.is_ascii_lowercase() || c == '_');
97    match name.split_once('.') {
98        Some((a, b)) => word(a) && word(b),
99        None => false,
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn every_name_is_registered_once_and_new_ones_follow_the_rule() {
109        let mut seen = std::collections::BTreeSet::new();
110        for n in REGISTRY {
111            assert!(seen.insert(n.name), "{} twice", n.name);
112            assert!(
113                well_formed(n.name) || GRANDFATHERED.contains(&n.name),
114                "{} breaks the name rule",
115                n.name
116            );
117        }
118        for g in GRANDFATHERED {
119            assert!(lookup(g).is_some(), "{g}");
120        }
121        assert!(well_formed("bug.reproduce_try"));
122        assert!(!well_formed("bug_outcome"));
123        assert!(!well_formed("Bug.Outcome"));
124        assert!(!well_formed("bug.out.come"));
125        assert_eq!(lookup("bug.outcome").map(|n| n.class), Some(Class::Domain));
126        assert!(lookup("bug.unknown").is_none());
127    }
128}