feat: let a size rejection be waived
A release below §5.5's floor was rejected with no way through, so a policy wrong about one title left three Rick and Morty S09 packs visible and none grabbable. `allow_below_floor` relaxes the floor for one title into a soft fail, never a pass: the release is waived, so automatic grabbing still skips it and the import records a §5.7 waiver. The deck offers the one click on a rejected row where the rule has an override, which is exactly what §9.3's override is for. Stored verdicts are re-derived when a title's overrides change — the deck and the daemon's grab gate both read that column, so without it the row the operator just acted on would keep reading `rejected`. Closes #210 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-9
@@ -50,6 +50,7 @@ import {
|
||||
movieFiles,
|
||||
movieReleases,
|
||||
movieSearchState,
|
||||
overridable,
|
||||
probedAttributeTags,
|
||||
queueSearch,
|
||||
removeMovie,
|
||||
@@ -59,7 +60,6 @@ import {
|
||||
totalSize,
|
||||
type WaiveOutcome,
|
||||
waiveAndGrab,
|
||||
waiverOverride,
|
||||
} from "./releases";
|
||||
import { currentRoute, navigate, type Route } from "./router";
|
||||
import {
|
||||
@@ -1352,7 +1352,7 @@ function movieMain(views: HideableView[]): MovieView {
|
||||
if (!movie) {
|
||||
return { kind: "error", detail: "page closed", overrideWritten: false };
|
||||
}
|
||||
if (bucket === "waived") {
|
||||
if (bucket !== "eligible") {
|
||||
return waiveAndGrab(movie.id, release.id, release.rejected_rule);
|
||||
}
|
||||
const outcome = await grabRelease(movie.id, release.id);
|
||||
@@ -2091,7 +2091,8 @@ function removePanel(subject: RemoveSubject, actions: RemoveActions): HTMLElemen
|
||||
interface ReleaseActions {
|
||||
reload: () => Promise<void>;
|
||||
notify: (text: string, tone?: "fault") => void;
|
||||
/** One click on grab — a plain grab, or waive-then-grab on a waived row. */
|
||||
/** One click on grab — a plain grab, or waive-then-grab where the rule
|
||||
* that failed has an override (§9.3). */
|
||||
grab: (
|
||||
release: MovieRelease,
|
||||
bucket: "eligible" | "waived" | "rejected",
|
||||
@@ -2155,10 +2156,16 @@ function paintBuckets(dom: BucketsDom, releases: MovieRelease[], actions: Releas
|
||||
dom.eligible.rows.append(releaseRow(release, "eligible", scoreStop, actions));
|
||||
}
|
||||
} else {
|
||||
// §9.3: over-strict filters must be visible, not silently absent
|
||||
// §9.3: over-strict filters must be visible, not silently absent — and
|
||||
// where a rule can be waived, the count says so rather than leaving the
|
||||
// way out folded inside a collapsed bucket.
|
||||
const waivable = releases.filter(overridable).length;
|
||||
const none = document.createElement("li");
|
||||
none.className = "rel rel-none readout dim";
|
||||
none.textContent = "none — every candidate was waived or rejected by policy";
|
||||
none.textContent =
|
||||
waivable > 0
|
||||
? `none — every candidate was waived or rejected by policy; ${waivable} can be waived`
|
||||
: "none — every candidate was waived or rejected by policy";
|
||||
dom.eligible.rows.append(none);
|
||||
}
|
||||
for (const name of ["waived", "rejected"] as const) {
|
||||
@@ -2295,8 +2302,14 @@ function releaseRow(
|
||||
);
|
||||
}
|
||||
if (bucket !== "eligible") {
|
||||
// A rejected row always names its rule; a waived one cannot — the
|
||||
// `releases` CHECK allows `rejected_rule` only on a rejection. Say
|
||||
// `waived` plainly rather than calling a classified row unclassified.
|
||||
const verdict = release.rejected_rule
|
||||
? `${bucket} · ${ruleLabel(release.rejected_rule)}`
|
||||
: bucket;
|
||||
line.append(
|
||||
chip(`${bucket} · ${ruleLabel(release.rejected_rule)}`, (span) => {
|
||||
chip(verdict, (span) => {
|
||||
span.dataset.verdict = bucket;
|
||||
}),
|
||||
);
|
||||
@@ -2313,11 +2326,14 @@ function releaseRow(
|
||||
note.setAttribute("role", "status");
|
||||
note.hidden = true;
|
||||
|
||||
if (bucket !== "rejected") {
|
||||
// A rejected row is normally inert, but §9.3's one click is exactly for
|
||||
// the rule the operator disagrees with: where an override exists, the row
|
||||
// offers it and the grab that follows stays a waiver.
|
||||
const writesOverride = bucket !== "eligible" && overridable(release);
|
||||
if (bucket !== "rejected" || writesOverride) {
|
||||
const grab = document.createElement("button");
|
||||
grab.type = "button";
|
||||
grab.className = "control rel-grab";
|
||||
const writesOverride = bucket === "waived" && waiverOverride(release.rejected_rule) !== null;
|
||||
grab.textContent = writesOverride ? "waive + grab" : "grab";
|
||||
grab.addEventListener("click", () => {
|
||||
grab.disabled = true;
|
||||
@@ -2829,7 +2845,7 @@ function tvReleasesMain(): TvReleasesView {
|
||||
if (!current) {
|
||||
return Promise.resolve({ kind: "error", detail: "deck closed", overrideWritten: false });
|
||||
}
|
||||
if (bucket === "waived") {
|
||||
if (bucket !== "eligible") {
|
||||
return waiveAndGrabTv(current.seriesId, current.target, release.id, release.rejected_rule);
|
||||
}
|
||||
return current.target
|
||||
|
||||
@@ -257,6 +257,11 @@ export async function grabRelease(movieId: number, releaseId: number): Promise<A
|
||||
* The per-title override one click on a waived row writes (§5.2, §9.3).
|
||||
* Mapping a rule name to its override is bookkeeping, not policy — the
|
||||
* verdict itself always comes from the API.
|
||||
*
|
||||
* `size` relaxes §5.5's floor for this title only, and only into a waiver:
|
||||
* the release stays out of automatic grabbing and imports on the record as
|
||||
* a §5.7 waiver. No band is right for every title, which is why the
|
||||
* override exists at all.
|
||||
*/
|
||||
export function waiverOverride(rule: string | null): Record<string, unknown> | null {
|
||||
switch (rule) {
|
||||
@@ -264,11 +269,18 @@ export function waiverOverride(rule: string | null): Record<string, unknown> | n
|
||||
return { allow_english_audio: true };
|
||||
case "resolution":
|
||||
return { only_4k: false };
|
||||
case "size":
|
||||
return { allow_below_floor: true };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether one click on this row has an override to write (§9.3). */
|
||||
export function overridable(release: { rejected_rule: string | null }): boolean {
|
||||
return waiverOverride(release.rejected_rule) !== null;
|
||||
}
|
||||
|
||||
export type WaiveOutcome =
|
||||
| { kind: "done"; overrideWritten: boolean }
|
||||
| { kind: "error"; detail: string; overrideWritten: boolean };
|
||||
|
||||
Reference in New Issue
Block a user