fix(core): tighten policy verdicts
This commit is contained in:
@@ -112,14 +112,6 @@ pub enum HdrFormat {
|
||||
DolbyVision(DolbyVisionProfile),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum HdrClaim {
|
||||
Hdr10,
|
||||
Hdr10Plus,
|
||||
Hlg,
|
||||
DolbyVision,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RequiredAudio {
|
||||
OriginalLanguage,
|
||||
@@ -204,9 +196,7 @@ pub enum Rule {
|
||||
RequiredAudio,
|
||||
DubBlacklist(Language),
|
||||
DolbyVisionProfile(DolbyVisionProfile),
|
||||
ResolutionUnknown,
|
||||
Resolution(Resolution),
|
||||
SourceUnknown,
|
||||
Source(Source),
|
||||
Size,
|
||||
Other(String),
|
||||
|
||||
+101
-90
@@ -60,11 +60,38 @@ pub enum RuleOutcome {
|
||||
HardFail,
|
||||
}
|
||||
|
||||
/// The result of evaluating one named rule.
|
||||
/// A rule's identity when no violation exists to carry concrete evidence.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RuleEvaluation {
|
||||
pub rule: Rule,
|
||||
pub outcome: RuleOutcome,
|
||||
pub enum RuleKind {
|
||||
RequiredAudio,
|
||||
DubBlacklist,
|
||||
DolbyVisionProfile,
|
||||
Resolution,
|
||||
Source,
|
||||
Size,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// The result of evaluating one rule.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RuleEvaluation {
|
||||
Pass(RuleKind),
|
||||
Unknown(RuleKind),
|
||||
SoftFail(Rule),
|
||||
HardFail(Rule),
|
||||
}
|
||||
|
||||
impl RuleEvaluation {
|
||||
/// The outcome category used by verdict reduction.
|
||||
#[must_use]
|
||||
pub const fn outcome(&self) -> RuleOutcome {
|
||||
match self {
|
||||
Self::Pass(_) => RuleOutcome::Pass,
|
||||
Self::Unknown(_) => RuleOutcome::Unknown,
|
||||
Self::SoftFail(_) => RuleOutcome::SoftFail,
|
||||
Self::HardFail(_) => RuleOutcome::HardFail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A policy rule that can participate in the verdict pipeline.
|
||||
@@ -79,15 +106,9 @@ pub struct Evaluation {
|
||||
pub rules: Vec<RuleEvaluation>,
|
||||
}
|
||||
|
||||
/// Apply the built-in filter rules and return the candidate's bucket.
|
||||
/// Apply the built-in filter rules and retain the verdict and every rule result.
|
||||
#[must_use]
|
||||
pub fn evaluate(policy: &Policy, overrides: &MovieOverrides, candidate: Candidate<'_>) -> Verdict {
|
||||
evaluate_with_report(policy, overrides, candidate).verdict
|
||||
}
|
||||
|
||||
/// Apply the built-in filter rules and retain each rule's result.
|
||||
#[must_use]
|
||||
pub fn evaluate_with_report(
|
||||
pub fn evaluate(
|
||||
policy: &Policy,
|
||||
overrides: &MovieOverrides,
|
||||
candidate: Candidate<'_>,
|
||||
@@ -104,20 +125,31 @@ pub fn evaluate_with_report(
|
||||
/// Reduce an ordered set of policy rules to a verdict.
|
||||
///
|
||||
/// The first hard failure decides rejection. If none exists, the first soft
|
||||
/// failure decides the waiver. Unknown evidence never becomes a pass.
|
||||
/// failure decides the waiver. Unknown evaluations abstain from reduction and
|
||||
/// remain visible in the returned report.
|
||||
#[must_use]
|
||||
pub fn evaluate_rules(context: &EvaluationContext<'_>, rules: &[&dyn PolicyRule]) -> Evaluation {
|
||||
let evaluations: Vec<RuleEvaluation> =
|
||||
rules.iter().map(|rule| rule.evaluate(context)).collect();
|
||||
|
||||
let deciding_rule = |outcome| {
|
||||
evaluations
|
||||
.iter()
|
||||
.find(|evaluation| evaluation.outcome == outcome)
|
||||
.map(|evaluation| evaluation.rule.clone())
|
||||
let hard_failure = evaluations.iter().find_map(|evaluation| {
|
||||
if let RuleEvaluation::HardFail(rule) = evaluation {
|
||||
Some(rule.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let soft_failure = || {
|
||||
evaluations.iter().find_map(|evaluation| {
|
||||
if let RuleEvaluation::SoftFail(rule) = evaluation {
|
||||
Some(rule.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
let verdict = deciding_rule(RuleOutcome::HardFail).map_or_else(
|
||||
|| deciding_rule(RuleOutcome::SoftFail).map_or(Verdict::Eligible, Verdict::Waived),
|
||||
let verdict = hard_failure.map_or_else(
|
||||
|| soft_failure().map_or(Verdict::Eligible, Verdict::Waived),
|
||||
Verdict::Rejected,
|
||||
);
|
||||
|
||||
@@ -134,25 +166,19 @@ pub struct ResolutionRule;
|
||||
impl PolicyRule for ResolutionRule {
|
||||
fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||
let Some(resolution) = context.candidate.resolution() else {
|
||||
return RuleEvaluation {
|
||||
rule: Rule::ResolutionUnknown,
|
||||
outcome: RuleOutcome::Unknown,
|
||||
};
|
||||
return RuleEvaluation::Unknown(RuleKind::Resolution);
|
||||
};
|
||||
|
||||
let accepted = context.policy.resolution_preference.contains(&resolution)
|
||||
&& (!context.overrides.only_4k || resolution == Resolution::R2160p);
|
||||
let outcome = if accepted {
|
||||
RuleOutcome::Pass
|
||||
} else if context.candidate.phase() == EvaluationPhase::PostDownload {
|
||||
RuleOutcome::SoftFail
|
||||
let allowed_by_root = context.policy.resolution_preference.contains(&resolution);
|
||||
if allowed_by_root && (!context.overrides.only_4k || resolution == Resolution::R2160p) {
|
||||
RuleEvaluation::Pass(RuleKind::Resolution)
|
||||
} else if allowed_by_root
|
||||
&& context.overrides.only_4k
|
||||
&& context.candidate.phase() == EvaluationPhase::PostDownload
|
||||
{
|
||||
RuleEvaluation::SoftFail(Rule::Resolution(resolution))
|
||||
} else {
|
||||
RuleOutcome::HardFail
|
||||
};
|
||||
|
||||
RuleEvaluation {
|
||||
rule: Rule::Resolution(resolution),
|
||||
outcome,
|
||||
RuleEvaluation::HardFail(Rule::Resolution(resolution))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,26 +190,20 @@ pub struct SourceRule;
|
||||
impl PolicyRule for SourceRule {
|
||||
fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||
let Some(source) = context.candidate.source() else {
|
||||
return RuleEvaluation {
|
||||
rule: Rule::SourceUnknown,
|
||||
outcome: RuleOutcome::Unknown,
|
||||
};
|
||||
return RuleEvaluation::Unknown(RuleKind::Source);
|
||||
};
|
||||
let outcome = match source {
|
||||
Source::Cam | Source::Telesync | Source::Screener => RuleOutcome::HardFail,
|
||||
Source::Other => RuleOutcome::Unknown,
|
||||
match source {
|
||||
Source::Cam | Source::Telesync | Source::Screener => {
|
||||
RuleEvaluation::HardFail(Rule::Source(source))
|
||||
}
|
||||
Source::Other => RuleEvaluation::Unknown(RuleKind::Source),
|
||||
Source::Dvd
|
||||
| Source::Telecine
|
||||
| Source::Hdtv
|
||||
| Source::WebRip
|
||||
| Source::WebDl
|
||||
| Source::BluRay
|
||||
| Source::Remux => RuleOutcome::Pass,
|
||||
};
|
||||
|
||||
RuleEvaluation {
|
||||
rule: Rule::Source(source),
|
||||
outcome,
|
||||
| Source::Remux => RuleEvaluation::Pass(RuleKind::Source),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,11 +250,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn verdict(policy: &Policy, overrides: &MovieOverrides, candidate: Candidate<'_>) -> Verdict {
|
||||
evaluate(policy, overrides, candidate).verdict
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_pre_grab_evidence_stays_unknown() {
|
||||
let policy = policy();
|
||||
let claims = claims(None, None);
|
||||
let report = evaluate_with_report(
|
||||
let report = evaluate(
|
||||
&policy,
|
||||
&MovieOverrides::default(),
|
||||
Candidate::PreGrab(&claims),
|
||||
@@ -244,14 +268,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
report.rules,
|
||||
vec![
|
||||
RuleEvaluation {
|
||||
rule: Rule::ResolutionUnknown,
|
||||
outcome: RuleOutcome::Unknown,
|
||||
},
|
||||
RuleEvaluation {
|
||||
rule: Rule::SourceUnknown,
|
||||
outcome: RuleOutcome::Unknown,
|
||||
},
|
||||
RuleEvaluation::Unknown(RuleKind::Resolution),
|
||||
RuleEvaluation::Unknown(RuleKind::Source),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -264,21 +282,21 @@ mod tests {
|
||||
for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] {
|
||||
let claims = claims(Some(claimed), Some(ClaimedSource::WebDl));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
Verdict::Eligible
|
||||
);
|
||||
}
|
||||
for resolution in [Resolution::R2160p, Resolution::R1080p] {
|
||||
let media = probed(resolution, Some(Source::WebDl));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
Verdict::Eligible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_disallowed_resolution_is_hard_before_grab_and_soft_after_download() {
|
||||
fn resolution_outside_root_policy_hard_fails_in_both_phases() {
|
||||
let policy = policy();
|
||||
let overrides = MovieOverrides::default();
|
||||
let cases = [
|
||||
@@ -291,12 +309,12 @@ mod tests {
|
||||
let claims = claims(Some(claimed_resolution), Some(ClaimedSource::WebDl));
|
||||
let media = probed(resolution, Some(Source::WebDl));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
Verdict::Rejected(Rule::Resolution(resolution))
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
Verdict::Waived(Rule::Resolution(resolution))
|
||||
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
Verdict::Rejected(Rule::Resolution(resolution))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -312,17 +330,17 @@ mod tests {
|
||||
let media = probed(Resolution::R1080p, Some(Source::WebDl));
|
||||
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims_1080)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims_1080)),
|
||||
Verdict::Rejected(Rule::Resolution(Resolution::R1080p))
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
Verdict::Waived(Rule::Resolution(Resolution::R1080p))
|
||||
);
|
||||
|
||||
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
Verdict::Eligible
|
||||
);
|
||||
}
|
||||
@@ -338,7 +356,7 @@ mod tests {
|
||||
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
||||
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
Verdict::Rejected(Rule::Resolution(Resolution::R2160p))
|
||||
);
|
||||
}
|
||||
@@ -358,11 +376,11 @@ mod tests {
|
||||
let media = probed(Resolution::R2160p, Some(source));
|
||||
let expected = Verdict::Rejected(Rule::Source(source));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
expected
|
||||
);
|
||||
}
|
||||
@@ -374,7 +392,7 @@ mod tests {
|
||||
let claims = arr_parse::parse("Movie.2024.1080p.TS-GROUP");
|
||||
|
||||
assert_eq!(
|
||||
evaluate(
|
||||
verdict(
|
||||
&policy,
|
||||
&MovieOverrides::default(),
|
||||
Candidate::PreGrab(&claims),
|
||||
@@ -401,43 +419,40 @@ mod tests {
|
||||
let claims = claims(Some(ClaimedResolution::P2160), Some(claimed_source));
|
||||
let media = probed(Resolution::R2160p, Some(source));
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||
Verdict::Eligible
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||
Verdict::Eligible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_post_download_source_is_not_treated_as_a_pass() {
|
||||
fn unknown_post_download_source_is_preserved_in_report() {
|
||||
let policy = policy();
|
||||
for source in [None, Some(Source::Other)] {
|
||||
let media = probed(Resolution::R2160p, source);
|
||||
let report = evaluate_with_report(
|
||||
let report = evaluate(
|
||||
&policy,
|
||||
&MovieOverrides::default(),
|
||||
Candidate::PostDownload(&media),
|
||||
);
|
||||
|
||||
assert_eq!(report.rules[1].outcome, RuleOutcome::Unknown);
|
||||
assert_eq!(report.verdict, Verdict::Eligible);
|
||||
assert_eq!(report.rules[1], RuleEvaluation::Unknown(RuleKind::Source));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FixedRule {
|
||||
rule: Rule,
|
||||
outcome: RuleOutcome,
|
||||
evaluation: RuleEvaluation,
|
||||
}
|
||||
|
||||
impl PolicyRule for FixedRule {
|
||||
fn evaluate(&self, _context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||
RuleEvaluation {
|
||||
rule: self.rule.clone(),
|
||||
outcome: self.outcome,
|
||||
}
|
||||
self.evaluation.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,12 +467,10 @@ mod tests {
|
||||
candidate: Candidate::PreGrab(&claims),
|
||||
};
|
||||
let soft = FixedRule {
|
||||
rule: Rule::Other("soft".to_owned()),
|
||||
outcome: RuleOutcome::SoftFail,
|
||||
evaluation: RuleEvaluation::SoftFail(Rule::Other("soft".to_owned())),
|
||||
};
|
||||
let hard = FixedRule {
|
||||
rule: Rule::Other("hard".to_owned()),
|
||||
outcome: RuleOutcome::HardFail,
|
||||
evaluation: RuleEvaluation::HardFail(Rule::Other("hard".to_owned())),
|
||||
};
|
||||
|
||||
let report = evaluate_rules(&context, &[&soft, &hard]);
|
||||
@@ -479,12 +492,10 @@ mod tests {
|
||||
candidate: Candidate::PreGrab(&claims),
|
||||
};
|
||||
let first = FixedRule {
|
||||
rule: Rule::Other("first".to_owned()),
|
||||
outcome: RuleOutcome::HardFail,
|
||||
evaluation: RuleEvaluation::HardFail(Rule::Other("first".to_owned())),
|
||||
};
|
||||
let second = FixedRule {
|
||||
rule: Rule::Other("second".to_owned()),
|
||||
outcome: RuleOutcome::HardFail,
|
||||
evaluation: RuleEvaluation::HardFail(Rule::Other("second".to_owned())),
|
||||
};
|
||||
|
||||
let report = evaluate_rules(&context, &[&first, &second]);
|
||||
|
||||
Reference in New Issue
Block a user