use crate::learn::discriminator::ClassVerdict;
use crate::learn::life_record::{LifeEvent, LifeRecord};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum CurationAction {
Keep,
Hold,
ReArm,
RouteToHuman,
Forget,
}
#[must_use]
pub const fn curate(verdict: ClassVerdict) -> CurationAction {
match verdict {
ClassVerdict::WellDefended => CurationAction::Keep,
ClassVerdict::Dormant => CurationAction::Hold,
ClassVerdict::Evaded => CurationAction::ReArm,
ClassVerdict::RouteToHuman => CurationAction::RouteToHuman,
ClassVerdict::Obsolete => {
if verdict.is_auto_forgettable() {
CurationAction::Forget
} else {
CurationAction::RouteToHuman
}
},
}
}
pub fn apply(action: CurationAction, record: &mut LifeRecord) -> Option<LifeEvent> {
if matches!(action, CurationAction::Forget) && record.is_retired() {
return None;
}
let event = match action {
CurationAction::Forget => Some(LifeEvent::Retired),
CurationAction::ReArm => Some(LifeEvent::Drifted),
CurationAction::Keep | CurationAction::Hold | CurationAction::RouteToHuman => None,
};
if let Some(ref e) = event {
record.append(e.clone());
}
event
}
#[cfg(test)]
mod tests {
use super::*;
use crate::learn::reader::SilentStatus;
#[test]
fn forget_is_reachable_from_obsolete_only() {
let all = [
ClassVerdict::Evaded,
ClassVerdict::WellDefended,
ClassVerdict::Obsolete,
ClassVerdict::Dormant,
ClassVerdict::RouteToHuman,
];
for v in all {
let forgets = curate(v) == CurationAction::Forget;
let expected = v == ClassVerdict::Obsolete;
assert_eq!(
forgets, expected,
"Forget must be reachable from Obsolete ONLY — {v:?} produced \
Forget={forgets} (expected {expected}). A forget from any other \
verdict discards a still-needed defense (RoutingTableStale).",
);
}
}
#[test]
fn forget_agrees_with_the_auto_forget_contract() {
let all = [
ClassVerdict::Evaded,
ClassVerdict::WellDefended,
ClassVerdict::Obsolete,
ClassVerdict::Dormant,
ClassVerdict::RouteToHuman,
];
for v in all {
assert_eq!(
curate(v) == CurationAction::Forget,
v.is_auto_forgettable(),
"CURATE forgets a verdict iff the discriminator marks it \
auto-forgettable; {v:?} broke that agreement.",
);
}
}
#[test]
fn every_verdict_routes_to_its_ladder_rung() {
assert_eq!(curate(ClassVerdict::WellDefended), CurationAction::Keep);
assert_eq!(curate(ClassVerdict::Dormant), CurationAction::Hold);
assert_eq!(curate(ClassVerdict::Evaded), CurationAction::ReArm);
assert_eq!(
curate(ClassVerdict::RouteToHuman),
CurationAction::RouteToHuman
);
assert_eq!(curate(ClassVerdict::Obsolete), CurationAction::Forget);
}
#[test]
fn shape_gone_but_defended_is_kept_not_forgotten() {
use crate::learn::discriminator::classify;
let verdict = classify(SilentStatus::Obsolete, true);
assert_eq!(verdict, ClassVerdict::WellDefended);
assert_eq!(
curate(verdict),
CurationAction::Keep,
"a class whose shape is gone but which still carries a live witness must be \
KEPT — the witness is why the shape is gone; forgetting it discards a \
working immunity.",
);
let undefended = classify(SilentStatus::Obsolete, false);
assert_eq!(undefended, ClassVerdict::Obsolete);
assert_eq!(curate(undefended), CurationAction::Forget);
}
#[test]
fn forget_appends_a_retired_tombstone() {
let mut rec = LifeRecord::new("obsolete-class");
rec.append(LifeEvent::Born);
assert!(!rec.is_retired());
let appended = apply(curate(ClassVerdict::Obsolete), &mut rec);
assert_eq!(appended, Some(LifeEvent::Retired));
assert!(
rec.is_retired(),
"after Forget the class is retired — the tombstone persists in history",
);
assert!(rec.events().iter().any(|e| matches!(e, LifeEvent::Retired)));
}
#[test]
fn rearm_records_drift_without_retiring() {
let mut rec = LifeRecord::new("evading-class");
rec.append(LifeEvent::Born);
let appended = apply(curate(ClassVerdict::Evaded), &mut rec);
assert_eq!(appended, Some(LifeEvent::Drifted));
assert!(
!rec.is_retired(),
"ReArm must NOT retire the class — it broadens/re-arms, discarding nothing",
);
}
#[test]
fn keep_hold_and_route_record_nothing_and_never_retire() {
for verdict in [
ClassVerdict::WellDefended, ClassVerdict::Dormant, ClassVerdict::RouteToHuman, ] {
let mut rec = LifeRecord::new("kept-class");
rec.append(LifeEvent::Born);
let before = rec.events().len();
let appended = apply(curate(verdict), &mut rec);
assert_eq!(
appended, None,
"{verdict:?} is a keep-as-is / escalate action — it records no event",
);
assert_eq!(
rec.events().len(),
before,
"{verdict:?} must not grow the autobiography (no lifecycle transition)",
);
assert!(
!rec.is_retired(),
"{verdict:?} must never retire the class — it is a reversible action",
);
}
}
#[test]
fn only_obsolete_can_ever_retire_a_class() {
let all = [
ClassVerdict::Evaded,
ClassVerdict::WellDefended,
ClassVerdict::Obsolete,
ClassVerdict::Dormant,
ClassVerdict::RouteToHuman,
];
for v in all {
let mut rec = LifeRecord::new("c");
apply(curate(v), &mut rec);
let retired = rec.is_retired();
let expected = v == ClassVerdict::Obsolete;
assert_eq!(
retired, expected,
"a class is retired through CURATE iff its verdict is Obsolete; \
{v:?} retired={retired} (expected {expected})",
);
}
}
}