fix(db): put the scoring numbers in the policy rows
The size bands seeded before scoring existed carried a penalty on no particular scale, and the weights that keep source tier and seeders small against the size term had nowhere to live but a Default impl. §5.5 says these are policy rows, tuned by hand. `SizeBand::penalty_points_per_gib_over` names its unit, so a row and the type it loads into agree on what the number means. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6b12656f01
commit
b1c11ae5f1
@@ -128,14 +128,17 @@ pub struct HdrRules {
|
||||
|
||||
/// One resolution's size band (`DESIGN.md` §5.5). Below the floor is a hard
|
||||
/// filter; the target is where the score peaks; above it the penalty grows
|
||||
/// with every gigabyte.
|
||||
/// with every gibibyte.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct SizeBand {
|
||||
pub floor_bytes: u64,
|
||||
pub target_bytes: u64,
|
||||
/// Points lost per gigabyte above `target_bytes`, against the scale set
|
||||
/// by [`ScoreWeights::size_at_target`].
|
||||
pub penalty_per_gb_over: i32,
|
||||
/// Points lost per gibibyte above `target_bytes`, on the scale set by
|
||||
/// [`ScoreWeights::size_at_target`] — 1000 points at target means one
|
||||
/// point is a tenth of a percent, fine enough to tune by hand without a
|
||||
/// fractional type. Applied per byte over, so a partial gibibyte costs
|
||||
/// its fraction.
|
||||
pub penalty_points_per_gib_over: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -116,7 +116,8 @@ fn size_points(band: &SizeBand, weights: &ScoreWeights, size_bytes: u64) -> i64
|
||||
|
||||
if size > target {
|
||||
let over = size.saturating_sub(target);
|
||||
let penalty = i64::from(band.penalty_per_gb_over).saturating_mul(over) / BYTES_PER_GIB;
|
||||
let penalty =
|
||||
i64::from(band.penalty_points_per_gib_over).saturating_mul(over) / BYTES_PER_GIB;
|
||||
return at_target.saturating_sub(penalty);
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ mod tests {
|
||||
SizeBand {
|
||||
floor_bytes: gib(8),
|
||||
target_bytes: gib(22),
|
||||
penalty_per_gb_over: 60,
|
||||
penalty_points_per_gib_over: 60,
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -189,7 +190,7 @@ mod tests {
|
||||
SizeBand {
|
||||
floor_bytes: gib(3),
|
||||
target_bytes: gib(8),
|
||||
penalty_per_gb_over: 60,
|
||||
penalty_points_per_gib_over: 60,
|
||||
},
|
||||
),
|
||||
]),
|
||||
@@ -351,7 +352,7 @@ mod tests {
|
||||
SizeBand {
|
||||
floor_bytes: gib(10),
|
||||
target_bytes: gib(10),
|
||||
penalty_per_gb_over: 60,
|
||||
penalty_points_per_gib_over: 60,
|
||||
},
|
||||
);
|
||||
let claims = claims(ClaimedSource::WebDl);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- §5.5 scoring. The size bands seeded in 0002 predate the scoring model and
|
||||
-- carry a penalty on no particular scale; the weights that make the terms
|
||||
-- comparable had nowhere to live at all. Both are policy rows, tuned by hand,
|
||||
-- so both belong here rather than in code.
|
||||
--
|
||||
-- Points are the unit. A release at its band's target scores `size_at_target`;
|
||||
-- every other term is small against that. Numbers stay placeholders pending
|
||||
-- the remux playback test in §14.
|
||||
|
||||
ALTER TABLE policies ADD COLUMN score_weights TEXT NOT NULL
|
||||
DEFAULT '{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}'
|
||||
CHECK (json_valid(score_weights));
|
||||
|
||||
-- Sizes in gibibytes, matching the bytes the scorer compares, and the penalty
|
||||
-- restated in points: 60 points per gibibyte over target puts a 60 GiB remux
|
||||
-- far below a 20 GiB WEB-DL while leaving it eligible.
|
||||
UPDATE policies SET size_bands = json('{
|
||||
"2160p": {"floor_gib": 8, "target_gib": 22, "penalty_points_per_gib_over": 60},
|
||||
"1080p": {"floor_gib": 3, "target_gib": 8, "penalty_points_per_gib_over": 60}
|
||||
}');
|
||||
@@ -191,6 +191,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_policy_carries_its_scoring_numbers() {
|
||||
let (_dir, db) = fresh().await;
|
||||
|
||||
let rows = sqlx::query("SELECT size_bands, score_weights FROM policies")
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
.expect("policies");
|
||||
|
||||
assert_eq!(rows.len(), 2);
|
||||
for row in rows {
|
||||
let size_bands: String = row.get(0);
|
||||
let score_weights: String = row.get(1);
|
||||
|
||||
// §5.5: floor, target and the penalty above target, per resolution.
|
||||
assert!(size_bands.contains("floor_gib"), "{size_bands}");
|
||||
assert!(size_bands.contains("target_gib"), "{size_bands}");
|
||||
assert!(
|
||||
size_bands.contains("penalty_points_per_gib_over"),
|
||||
"{size_bands}"
|
||||
);
|
||||
// §5.5: the weights that keep source tier and seeders small
|
||||
// against the size term are tuned by hand too.
|
||||
assert!(score_weights.contains("size_at_target"), "{score_weights}");
|
||||
assert!(score_weights.contains("source_tier"), "{score_weights}");
|
||||
assert!(score_weights.contains("seeder_doubling"), "{score_weights}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_keys_are_enforced() {
|
||||
let (_dir, db) = fresh().await;
|
||||
|
||||
Reference in New Issue
Block a user