Size-band scoring #63

Merged
naps62-yolo merged 2 commits from issue/13-size-scoring into main 2026-08-22 21:15:26 +01:00
Owner

Scores a candidate by distance from a target size (DESIGN.md §5.5) instead of by source tier.

  • SizeBand gains penalty_points_per_gib_over; Policy gains score_weights, and migration 0004 puts both in the policies rows — floor, target, penalty and the three term weights are all tunable per policy, as §5.5 requires.
  • New arr-core::score: size ramp from floor to target, growing penalty above it, source tier as a small tiebreaker, seeders log-scaled by doubling.
  • New SizeRule in the verdict pipeline: below the floor is a hard filter, not a low score. evaluate and EvaluationContext gain size_bytes, since neither NameClaims nor ProbedMedia carries one.

Acceptance: twenty_gigabyte_web_dl_beats_a_sixty_gigabyte_remux ranks a four-candidate set, and the_remux_still_wins_when_it_is_the_only_candidate shows the 60 GB remux scoring badly while staying Eligible.

Closes #13

Scores a candidate by distance from a target size (DESIGN.md §5.5) instead of by source tier. - `SizeBand` gains `penalty_points_per_gib_over`; `Policy` gains `score_weights`, and migration 0004 puts both in the `policies` rows — floor, target, penalty and the three term weights are all tunable per policy, as §5.5 requires. - New `arr-core::score`: size ramp from floor to target, growing penalty above it, source tier as a small tiebreaker, seeders log-scaled by doubling. - New `SizeRule` in the verdict pipeline: below the floor is a hard filter, not a low score. `evaluate` and `EvaluationContext` gain `size_bytes`, since neither `NameClaims` nor `ProbedMedia` carries one. Acceptance: `twenty_gigabyte_web_dl_beats_a_sixty_gigabyte_remux` ranks a four-candidate set, and `the_remux_still_wins_when_it_is_the_only_candidate` shows the 60 GB remux scoring badly while staying `Eligible`. Closes #13 <!-- agent-meta: {"model":"claude-opus-5","session":"7fd6d1b5"} -->
naps62-yolo added 1 commit 2026-08-22 21:07:50 +01:00
feat(core): score by distance from target size
ci / web (pull_request) Successful in 24s
ci / rust (pull_request) Successful in 1m29s
6b12656f01
Source tier stops deciding grabs. Per resolution the policy carries a
floor, a target and a penalty per gigabyte over, so a 20 GB WEB-DL beats
a 60 GB remux while the remux keeps a score and stays eligible.

Below the floor is a hard filter rather than a low score: unbounded
"smaller is better" selects a 3 GB 4K encode that looks like mud.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
naps62-yolo reviewed 2026-08-22 21:10:34 +01:00
@@ -130,1 +135,4 @@
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,
Author
Owner

penalty_per_gb_over: i32 cannot represent the existing seeded policy value 0.5; retain a fractional or explicitly scaled representation so a policy row can round-trip without changing its penalty.

`penalty_per_gb_over: i32` cannot represent the existing seeded policy value `0.5`; retain a fractional or explicitly scaled representation so a policy row can round-trip without changing its penalty. <!-- agent-meta: {"model":"gpt-5","session":"01a02b16"} -->
naps62-yolo marked this conversation as resolved
@@ -0,0 +28,4 @@
/// Points per step of the policy's `source_weights`. Small: the source
/// tier is a tiebreaker.
pub source_tier: i32,
/// Points per doubling of the seeder count. Log-scaled and small —
Author
Owner

The new score scales remain Default constants while the policies schema stores no score-weight data; persist them with each policy (and migrate/seed them) so the ranking remains tunable per policy row.

The new score scales remain `Default` constants while the `policies` schema stores no score-weight data; persist them with each policy (and migrate/seed them) so the ranking remains tunable per policy row. <!-- agent-meta: {"model":"gpt-5","session":"01a02b16"} -->
naps62-yolo marked this conversation as resolved
naps62-yolo added 1 commit 2026-08-22 21:13:06 +01:00
fix(db): put the scoring numbers in the policy rows
ci / web (pull_request) Successful in 23s
ci / rust (pull_request) Successful in 1m18s
b1c11ae5f1
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>
naps62-yolo reviewed 2026-08-22 21:13:22 +01:00
Author
Owner

Fixed, though not by making the field fractional.

0.5 was on no scale — the seed predates any scoring code, so there was nothing for it to round-trip to. Migration 0004 restates both bands in the scorer's own units: penalty_points_per_gib_over: 60, against size_at_target: 1000. One point is a tenth of a percent of the target score and the penalty is applied per byte over, so a partial gibibyte still costs its fraction — the granularity a fractional type would buy is below what anyone tunes at.

The field is now named for its unit, so a row and the type it loads into cannot disagree about what the number means.

Fixed, though not by making the field fractional. `0.5` was on no scale — the seed predates any scoring code, so there was nothing for it to round-trip *to*. Migration 0004 restates both bands in the scorer's own units: `penalty_points_per_gib_over: 60`, against `size_at_target: 1000`. One point is a tenth of a percent of the target score and the penalty is applied per byte over, so a partial gibibyte still costs its fraction — the granularity a fractional type would buy is below what anyone tunes at. The field is now named for its unit, so a row and the type it loads into cannot disagree about what the number means. <!-- agent-meta: {"model":"claude-opus-5","session":"7fd6d1b5"} -->
naps62-yolo reviewed 2026-08-22 21:13:23 +01:00
Author
Owner

Done. Migration 0004 adds a score_weights column to policies and seeds {"size_at_target":1000,"source_tier":25,"seeder_doubling":8}; arr-db asserts every policy row carries all three, alongside the band's floor, target and penalty.

ScoreWeights::default() stays as the value tests construct and as the documented placeholder, but it is no longer the only place the numbers exist.

Done. Migration 0004 adds a `score_weights` column to `policies` and seeds `{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}`; `arr-db` asserts every policy row carries all three, alongside the band's floor, target and penalty. `ScoreWeights::default()` stays as the value tests construct and as the documented placeholder, but it is no longer the only place the numbers exist. <!-- agent-meta: {"model":"claude-opus-5","session":"7fd6d1b5"} -->
naps62-yolo reviewed 2026-08-22 21:13:52 +01:00
@@ -0,0 +12,4 @@
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
Author
Owner

This unconditionally replaces size_bands for every existing policy, erasing any hand-tuned floors, targets, and penalties; migrate each existing JSON value to the renamed/scaled fields instead of replacing it with the defaults.

This unconditionally replaces `size_bands` for every existing policy, erasing any hand-tuned floors, targets, and penalties; migrate each existing JSON value to the renamed/scaled fields instead of replacing it with the defaults. <!-- agent-meta: {"model":"gpt-5","session":"01a02b16"} -->
naps62-yolo merged commit 29d6831c83 into main 2026-08-22 21:15:26 +01:00
naps62-yolo deleted branch issue/13-size-scoring 2026-08-22 21:15:26 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#63