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];
78
79/// The registered name `name`.
80#[must_use]
81pub fn lookup(name: &str) -> Option<&'static Name> {
82    REGISTRY.iter().find(|n| n.name == name)
83}
84
85/// A new name's rule: lowercase words joined by one dot
86/// (`[a-z_]+\.[a-z_]+`).
87#[must_use]
88pub fn well_formed(name: &str) -> bool {
89    let word = |w: &str| !w.is_empty() && w.chars().all(|c| c.is_ascii_lowercase() || c == '_');
90    match name.split_once('.') {
91        Some((a, b)) => word(a) && word(b),
92        None => false,
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn every_name_is_registered_once_and_new_ones_follow_the_rule() {
102        let mut seen = std::collections::BTreeSet::new();
103        for n in REGISTRY {
104            assert!(seen.insert(n.name), "{} twice", n.name);
105            assert!(
106                well_formed(n.name) || GRANDFATHERED.contains(&n.name),
107                "{} breaks the name rule",
108                n.name
109            );
110        }
111        for g in GRANDFATHERED {
112            assert!(lookup(g).is_some(), "{g}");
113        }
114        assert!(well_formed("bug.reproduce_try"));
115        assert!(!well_formed("bug_outcome"));
116        assert!(!well_formed("Bug.Outcome"));
117        assert!(!well_formed("bug.out.come"));
118        assert_eq!(lookup("bug.outcome").map(|n| n.class), Some(Class::Domain));
119        assert!(lookup("bug.unknown").is_none());
120    }
121}