use super::{ReductionKind, REDUCTION_SENTINEL};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
ToolOutput,
FileRead,
Image,
TurnsCleared,
ToolInput,
OutputNormalized,
FileReadDiffed,
Duplicate,
Superseded,
}
impl Kind {
pub const fn as_str(self) -> &'static str {
match self {
Kind::ToolOutput => "tool-output",
Kind::FileRead => "file-read",
Kind::Image => "image",
Kind::TurnsCleared => "turns-cleared",
Kind::ToolInput => "tool-input",
Kind::OutputNormalized => "output-normalized",
Kind::FileReadDiffed => "file-read-diffed",
Kind::Duplicate => "duplicate",
Kind::Superseded => "superseded",
}
}
fn from_str(s: &str) -> Option<Self> {
Some(match s {
"tool-output" => Kind::ToolOutput,
"file-read" => Kind::FileRead,
"image" => Kind::Image,
"turns-cleared" => Kind::TurnsCleared,
"tool-input" => Kind::ToolInput,
"output-normalized" => Kind::OutputNormalized,
"file-read-diffed" => Kind::FileReadDiffed,
"duplicate" => Kind::Duplicate,
"superseded" => Kind::Superseded,
_ => return None,
})
}
}
impl From<&ReductionKind> for Kind {
fn from(k: &ReductionKind) -> Self {
match k {
ReductionKind::ToolOutputTruncated { .. } => Kind::ToolOutput,
ReductionKind::FileReadElided { .. } => Kind::FileRead,
ReductionKind::ImageRedacted { .. } => Kind::Image,
ReductionKind::TurnsCleared { .. } => Kind::TurnsCleared,
ReductionKind::ToolInputElided { .. } => Kind::ToolInput,
ReductionKind::OutputNormalized { .. } => Kind::OutputNormalized,
ReductionKind::FileReadDiffed { .. } => Kind::FileReadDiffed,
ReductionKind::DuplicateOutput { .. } => Kind::Duplicate,
ReductionKind::Superseded { .. } => Kind::Superseded,
}
}
}
fn is_valid_id(id: &str) -> bool {
let Some(rest) = id.strip_prefix('r') else {
return false;
};
let bytes = rest.as_bytes();
if bytes.len() != 9 || bytes[4] != b'-' {
return false;
}
bytes[..4].iter().all(u8::is_ascii_digit)
&& bytes[5..]
.iter()
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
pub fn format(kind: Kind, id: &str, summary: &str) -> String {
debug_assert!(is_valid_id(id), "invalid reduction id: {id:?}");
debug_assert!(!summary.contains(']'), "summary contains `]`: {summary:?}");
debug_assert!(
!summary.contains('\n'),
"summary is not one line: {summary:?}"
);
format!("{REDUCTION_SENTINEL} {} {id}: {summary}]", kind.as_str())
}
pub fn parse(line: &str) -> Option<(Kind, String, String)> {
let body = line
.strip_prefix(REDUCTION_SENTINEL)?
.strip_prefix(' ')?
.strip_suffix(']')?;
if body.contains(']') {
return None;
}
let (kind_str, rest) = body.split_once(' ')?;
let kind = Kind::from_str(kind_str)?;
let (id, summary) = rest.split_once(": ")?;
if !is_valid_id(id) || summary.is_empty() || summary.contains('\n') {
return None;
}
Some((kind, id.to_string(), summary.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_matches_grammar_and_round_trips() {
let re = regex::Regex::new(
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}: [^\]]+\]$",
)
.unwrap();
for kind in [
Kind::ToolOutput,
Kind::FileRead,
Kind::Image,
Kind::TurnsCleared,
Kind::ToolInput,
Kind::OutputNormalized,
Kind::FileReadDiffed,
Kind::Duplicate,
Kind::Superseded,
] {
let id = super::super::make_id(42, "9f3cabcd");
let line = format(kind, &id, "a one-line summary");
assert!(re.is_match(&line), "{line:?} does not match the grammar");
assert_eq!(
parse(&line),
Some((kind, id, "a one-line summary".to_string()))
);
}
}
#[test]
fn parse_rejects_malformed_input() {
assert_eq!(parse("no bracket at all"), None);
assert_eq!(parse("[sc-reduced bogus-kind r0001-aaaa: x]"), None);
assert_eq!(parse("[sc-reduced tool-output not-an-id: x]"), None);
assert_eq!(
parse("[sc-reduced tool-output r0001-aaaa: has ] bracket]"),
None
);
assert_eq!(parse("[sc-reduced tool-output r0001-aaaa: ]"), None);
assert_eq!(parse("[sc-reduced tool-output r0001-aaZZ: x]"), None);
}
}