use super::rule::{DegradeRule, Loss};
use crate::domain::ProtocolVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TranslationStep {
pub rule: DegradeRule,
pub target: ProtocolVersion,
}
impl TranslationStep {
#[must_use]
pub const fn new(rule: DegradeRule, target: ProtocolVersion) -> Self {
Self { rule, target }
}
#[must_use]
pub const fn loss(&self) -> Loss {
self.rule.loss()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TranslationReport {
steps: Vec<TranslationStep>,
target: Option<ProtocolVersion>,
}
impl TranslationReport {
#[must_use]
pub const fn new(target: ProtocolVersion) -> Self {
Self {
steps: Vec::new(),
target: Some(target),
}
}
pub fn record(&mut self, rule: DegradeRule) {
let target = self.target.unwrap_or_else(ProtocolVersion::latest_known);
self.steps.push(TranslationStep::new(rule, target));
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub const fn len(&self) -> usize {
self.steps.len()
}
#[must_use]
pub fn steps(&self) -> &[TranslationStep] {
&self.steps
}
#[must_use]
pub fn worst_loss(&self) -> Loss {
self.steps
.iter()
.map(TranslationStep::loss)
.max()
.unwrap_or(Loss::None)
}
#[must_use]
pub fn is_lossless_or_flagged(&self) -> bool {
self.worst_loss().is_acceptable()
}
#[must_use]
pub fn is_lossless(&self) -> bool {
self.worst_loss().is_lossless()
}
#[must_use]
pub fn lost_features(&self) -> Vec<crate::domain::Feature> {
let mut out: Vec<crate::domain::Feature> = Vec::new();
for step in &self.steps {
let Some(feature) = step.rule.feature() else {
continue;
};
if !out.contains(&feature) {
out.push(feature);
}
}
out
}
pub fn absorb(&mut self, other: &Self) {
if self.target.is_none() {
self.target = other.target;
}
self.steps.extend_from_slice(&other.steps);
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::panic,
reason = "tests panic with the failing precondition, which is the useful part"
)]
use super::TranslationReport;
use crate::domain::ir::lower::rule::{DegradeRule, Loss};
use crate::domain::{Feature, ProtocolVersion};
fn report() -> TranslationReport {
TranslationReport::new(ProtocolVersion::V2025_03_26)
}
#[test]
fn a_fresh_report_is_lossless() {
let report = report();
assert!(report.is_empty());
assert_eq!(report.len(), 0);
assert_eq!(report.worst_loss(), Loss::None);
assert!(report.is_lossless());
assert!(report.is_lossless_or_flagged());
}
#[test]
fn a_lossless_step_stays_lossless() {
let mut report = report();
report.record(DegradeRule::StructuredContentToText);
assert_eq!(report.len(), 1);
assert!(report.is_lossless());
assert!(report.is_lossless_or_flagged());
assert!(!report.is_empty());
}
#[test]
fn flagged_is_not_lossless_but_is_acceptable() {
let mut report = report();
report.record(DegradeRule::DropAnnotations);
assert_eq!(report.worst_loss(), Loss::Flagged);
assert!(!report.is_lossless());
assert!(report.is_lossless_or_flagged());
}
#[test]
fn a_destructive_step_is_neither() {
let mut report = report();
report.record(DegradeRule::AudioToTextPlaceholder);
assert_eq!(report.worst_loss(), Loss::Destructive);
assert!(!report.is_lossless());
assert!(!report.is_lossless_or_flagged());
}
#[test]
fn the_worst_loss_dominates() {
let mut report = report();
report.record(DegradeRule::StructuredContentToText);
report.record(DegradeRule::AudioToTextPlaceholder);
report.record(DegradeRule::DropAnnotations);
assert_eq!(report.len(), 3);
assert_eq!(report.worst_loss(), Loss::Destructive);
assert!(!report.is_lossless_or_flagged());
}
#[test]
fn every_step_keeps_the_target_revision() {
let mut report = report();
report.record(DegradeRule::DropAudio);
let Some(step) = report.steps().first() else {
panic!("expected one step");
};
assert_eq!(step.target, ProtocolVersion::V2025_03_26);
}
#[test]
fn steps_accumulate_and_features_deduplicate() {
let mut report = report();
report.record(DegradeRule::DropAudio);
report.record(DegradeRule::AudioToTextPlaceholder);
report.record(DegradeRule::DropImage);
assert_eq!(report.len(), 3);
let lost = report.lost_features();
assert_eq!(lost, vec![Feature::ToolAudioContent]);
}
#[test]
fn absorbing_merges_steps() {
let mut first = report();
first.record(DegradeRule::DropAnnotations);
let mut second = TranslationReport::new(ProtocolVersion::V2025_03_26);
second.record(DegradeRule::StructuredContentToText);
first.absorb(&second);
assert_eq!(first.len(), 2);
assert!(first.is_lossless_or_flagged());
}
}