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("agent.started"),
62    p("agent.plan"),
63    p("agent.unasked"),
64    p("said"),
65    p("reasoning"),
66    p("notice"),
67    p("result"),
68    d("bug.recorded"),
69    p("bug.reproduce_try"),
70    p("bug.reproduced"),
71    p("bug.confirm_waiting"),
72    d("bug.confirmed"),
73    d("bug.confirm_rejected"),
74    p("bug.unchanged_selected"),
75    p("bug.discrimination"),
76    d("bug.outcome"),
77    p("oracle.stage"),
78    d("oracle.frozen"),
79    d("oracle.lapsed"),
80    d("collection.checked"),
81    p("regression.bisected"),
82    d("project.created"),
83    d("project.undone"),
84    p("scaffold.started"),
85    p("scaffold.finished"),
86    p("planning.stopped"),
87];
88
89/// The registered name `name`.
90#[must_use]
91pub fn lookup(name: &str) -> Option<&'static Name> {
92    REGISTRY.iter().find(|n| n.name == name)
93}
94
95/// A new name's rule: lowercase words joined by one dot
96/// (`[a-z_]+\.[a-z_]+`).
97#[must_use]
98pub fn well_formed(name: &str) -> bool {
99    let word = |w: &str| !w.is_empty() && w.chars().all(|c| c.is_ascii_lowercase() || c == '_');
100    match name.split_once('.') {
101        Some((a, b)) => word(a) && word(b),
102        None => false,
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn every_name_is_registered_once_and_new_ones_follow_the_rule() {
112        let mut seen = std::collections::BTreeSet::new();
113        for n in REGISTRY {
114            assert!(seen.insert(n.name), "{} twice", n.name);
115            assert!(
116                well_formed(n.name) || GRANDFATHERED.contains(&n.name),
117                "{} breaks the name rule",
118                n.name
119            );
120        }
121        for g in GRANDFATHERED {
122            assert!(lookup(g).is_some(), "{g}");
123        }
124        assert!(well_formed("bug.reproduce_try"));
125        assert!(!well_formed("bug_outcome"));
126        assert!(!well_formed("Bug.Outcome"));
127        assert!(!well_formed("bug.out.come"));
128        assert_eq!(lookup("bug.outcome").map(|n| n.class), Some(Class::Domain));
129        assert!(lookup("bug.unknown").is_none());
130    }
131}