supercode_reduce/engine/
stub.rs1use super::{ReductionKind, REDUCTION_SENTINEL};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Kind {
24 ToolOutput,
26 FileRead,
28 Image,
30 TurnsCleared,
32 ToolInput,
34 OutputNormalized,
36 FileReadDiffed,
38 Duplicate,
40 Superseded,
42}
43
44impl Kind {
45 pub const fn as_str(self) -> &'static str {
50 match self {
51 Kind::ToolOutput => "tool-output",
52 Kind::FileRead => "file-read",
53 Kind::Image => "image",
54 Kind::TurnsCleared => "turns-cleared",
55 Kind::ToolInput => "tool-input",
56 Kind::OutputNormalized => "output-normalized",
57 Kind::FileReadDiffed => "file-read-diffed",
58 Kind::Duplicate => "duplicate",
59 Kind::Superseded => "superseded",
60 }
61 }
62
63 fn from_str(s: &str) -> Option<Self> {
64 Some(match s {
65 "tool-output" => Kind::ToolOutput,
66 "file-read" => Kind::FileRead,
67 "image" => Kind::Image,
68 "turns-cleared" => Kind::TurnsCleared,
69 "tool-input" => Kind::ToolInput,
70 "output-normalized" => Kind::OutputNormalized,
71 "file-read-diffed" => Kind::FileReadDiffed,
72 "duplicate" => Kind::Duplicate,
73 "superseded" => Kind::Superseded,
74 _ => return None,
75 })
76 }
77}
78
79impl From<&ReductionKind> for Kind {
80 fn from(k: &ReductionKind) -> Self {
81 match k {
82 ReductionKind::ToolOutputTruncated { .. } => Kind::ToolOutput,
83 ReductionKind::FileReadElided { .. } => Kind::FileRead,
84 ReductionKind::ImageRedacted { .. } => Kind::Image,
85 ReductionKind::TurnsCleared { .. } => Kind::TurnsCleared,
86 ReductionKind::ToolInputElided { .. } => Kind::ToolInput,
87 ReductionKind::OutputNormalized { .. } => Kind::OutputNormalized,
88 ReductionKind::FileReadDiffed { .. } => Kind::FileReadDiffed,
89 ReductionKind::DuplicateOutput { .. } => Kind::Duplicate,
90 ReductionKind::Superseded { .. } => Kind::Superseded,
91 }
92 }
93}
94
95fn is_valid_id(id: &str) -> bool {
97 let Some(rest) = id.strip_prefix('r') else {
98 return false;
99 };
100 let bytes = rest.as_bytes();
101 if bytes.len() != 9 || bytes[4] != b'-' {
102 return false;
103 }
104 bytes[..4].iter().all(u8::is_ascii_digit)
105 && bytes[5..]
106 .iter()
107 .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
108}
109
110pub fn format(kind: Kind, id: &str, summary: &str) -> String {
117 debug_assert!(is_valid_id(id), "invalid reduction id: {id:?}");
118 debug_assert!(!summary.contains(']'), "summary contains `]`: {summary:?}");
119 debug_assert!(
120 !summary.contains('\n'),
121 "summary is not one line: {summary:?}"
122 );
123 format!("{REDUCTION_SENTINEL} {} {id}: {summary}]", kind.as_str())
124}
125
126pub fn parse(line: &str) -> Option<(Kind, String, String)> {
132 let body = line
133 .strip_prefix(REDUCTION_SENTINEL)?
134 .strip_prefix(' ')?
135 .strip_suffix(']')?;
136 if body.contains(']') {
138 return None;
139 }
140 let (kind_str, rest) = body.split_once(' ')?;
141 let kind = Kind::from_str(kind_str)?;
142 let (id, summary) = rest.split_once(": ")?;
143 if !is_valid_id(id) || summary.is_empty() || summary.contains('\n') {
144 return None;
145 }
146 Some((kind, id.to_string(), summary.to_string()))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn format_matches_grammar_and_round_trips() {
155 let re = regex::Regex::new(
159 r"^\[sc-reduced (tool-output|file-read|image|turns-cleared|tool-input|output-normalized|file-read-diffed|duplicate|superseded) r\d{4}-[0-9a-f]{4}: [^\]]+\]$",
160 )
161 .unwrap();
162 for kind in [
163 Kind::ToolOutput,
164 Kind::FileRead,
165 Kind::Image,
166 Kind::TurnsCleared,
167 Kind::ToolInput,
168 Kind::OutputNormalized,
169 Kind::FileReadDiffed,
170 Kind::Duplicate,
171 Kind::Superseded,
172 ] {
173 let id = super::super::make_id(42, "9f3cabcd");
174 let line = format(kind, &id, "a one-line summary");
175 assert!(re.is_match(&line), "{line:?} does not match the grammar");
176 assert_eq!(
177 parse(&line),
178 Some((kind, id, "a one-line summary".to_string()))
179 );
180 }
181 }
182
183 #[test]
184 fn parse_rejects_malformed_input() {
185 assert_eq!(parse("no bracket at all"), None);
186 assert_eq!(parse("[sc-reduced bogus-kind r0001-aaaa: x]"), None);
187 assert_eq!(parse("[sc-reduced tool-output not-an-id: x]"), None);
188 assert_eq!(
189 parse("[sc-reduced tool-output r0001-aaaa: has ] bracket]"),
190 None
191 );
192 assert_eq!(parse("[sc-reduced tool-output r0001-aaaa: ]"), None);
193 assert_eq!(parse("[sc-reduced tool-output r0001-aaZZ: x]"), None);
194 }
195}