fix(gate): the plan-code guard catches decision codes, and the purge takes them
A plan's decision ids (`D1`, `D4`, `D6-A`) cited in 11 comments and test titles named a document no reader of this repo has, and the guard had no pattern for them. It has now (mirrored in audit_metrics), with cases for each form the citations took and for the look-alikes it must leave alone (a JPEG marker, "3D"); the 11 lines say their why without the id. Case test seen red without the pattern, the scan red on a planted `D9:`.
This commit is contained in:
@@ -734,7 +734,7 @@ def _step_failure(err: BaseException) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _is_jpeg(path: Path) -> bool:
|
def _is_jpeg(path: Path) -> bool:
|
||||||
"""True if the file starts with the JPEG SOI marker (FF D8 FF)."""
|
"""True if the file starts with the JPEG SOI marker (0xFF 0xD8 0xFF)."""
|
||||||
try:
|
try:
|
||||||
with path.open("rb") as f:
|
with path.open("rb") as f:
|
||||||
return f.read(3) == b"\xff\xd8\xff"
|
return f.read(3) == b"\xff\xd8\xff"
|
||||||
|
|||||||
@@ -1033,7 +1033,7 @@ def list_links(
|
|||||||
|
|
||||||
class CreateLinkIn(BaseModel):
|
class CreateLinkIn(BaseModel):
|
||||||
password: str = ""
|
password: str = ""
|
||||||
# D1: a newly shared download is downloadable by default — the common intent is "let the
|
# A newly shared download is downloadable by default — the common intent is "let the
|
||||||
# recipient have the file", not stream-only. Forward-only: this only sets the default for links
|
# recipient have the file", not stream-only. Forward-only: this only sets the default for links
|
||||||
# minted from now on; existing links keep whatever `allow_download` they were created with (the
|
# minted from now on; existing links keep whatever `allow_download` they were created with (the
|
||||||
# column default stays False, so no back-catalogue link silently flips). The owner can still
|
# column default stays False, so no back-catalogue link silently flips). The owner can still
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ result (measured on real data: every untagged channel got a suggestion, most on
|
|||||||
alone). Topic overlap is what makes "you tagged similar channels this way" meaningful.
|
alone). Topic overlap is what makes "you tagged similar channels this way" meaningful.
|
||||||
|
|
||||||
Purely read-only and deterministic — the Channel Manager surfaces the result as one-tap-accept
|
Purely read-only and deterministic — the Channel Manager surfaces the result as one-tap-accept
|
||||||
"ghost" chips; nothing is written here (D4 = suggest-and-confirm, no silent auto-apply). The
|
"ghost" chips; nothing is written here (suggest-and-confirm, never a silent auto-apply). The
|
||||||
thresholds are tuning constants rather than admin knobs on purpose: they mirror how ``autotag.py``
|
thresholds are tuning constants rather than admin knobs on purpose: they mirror how ``autotag.py``
|
||||||
keeps its mappings inline, and sysconfig only stores int/str/bool (no float), so a knob would mean
|
keeps its mappings inline, and sysconfig only stores int/str/bool (no float), so a knob would mean
|
||||||
storing percentages — not worth the surface for values an operator has no reason to tune per-install.
|
storing percentages — not worth the surface for values an operator has no reason to tune per-install.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class TestPublicMetaAudioFlag:
|
|||||||
|
|
||||||
|
|
||||||
class TestCreateLinkDefault:
|
class TestCreateLinkDefault:
|
||||||
"""D1: a newly minted share link is downloadable by default. The one-click Share relies on
|
"""A newly minted share link is downloadable by default. The one-click Share relies on
|
||||||
this — it POSTs no `allow_download`, so the API default decides. Forward-only lives here (the API
|
this — it POSTs no `allow_download`, so the API default decides. Forward-only lives here (the API
|
||||||
default), NOT in a migration: the DownloadLink column default stays False, so no existing back-
|
default), NOT in a migration: the DownloadLink column default stays False, so no existing back-
|
||||||
catalogue link silently flips to downloadable."""
|
catalogue link silently flips to downloadable."""
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const PlexCollectionView = lazy(() => import("./PlexCollectionView"));
|
|||||||
|
|
||||||
const PAGE = 40;
|
const PAGE = 40;
|
||||||
|
|
||||||
// D3: the "last watched" sort auto-applied when you enter the in-progress watch-state view (most
|
// The "last watched" sort auto-applied when you enter the in-progress watch-state view (most
|
||||||
// recently watched first). Frozen for the same reason as PLEX_SORT_FALLBACK (handed out by reference).
|
// recently watched first). Frozen for the same reason as PLEX_SORT_FALLBACK (handed out by reference).
|
||||||
const LASTWATCHED_SORT: NonNullable<SortState> = Object.freeze({
|
const LASTWATCHED_SORT: NonNullable<SortState> = Object.freeze({
|
||||||
key: "lastwatched",
|
key: "lastwatched",
|
||||||
@@ -229,7 +229,7 @@ export default function PlexBrowse() {
|
|||||||
return () => window.removeEventListener("keydown", onKey);
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
}, [sub, canStepBack]);
|
}, [sub, canStepBack]);
|
||||||
|
|
||||||
// D3: entering the in-progress watch-state view auto-applies the "last watched" sort (most recently
|
// Entering the in-progress watch-state view auto-applies the "last watched" sort (most recently
|
||||||
// watched first); leaving restores the sort that was active before entering. Only the ENTER edge
|
// watched first); leaving restores the sort that was active before entering. Only the ENTER edge
|
||||||
// forces the sort, so a manual sort change while in the in-progress view still wins — it's just not
|
// forces the sort, so a manual sort change while in the in-progress view still wins — it's just not
|
||||||
// carried back out (exit returns to the pre-entry preference). We stash the raw provider `sort`, not
|
// carried back out (exit returns to the pre-entry preference). We stash the raw provider `sort`, not
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ describe("PlexDiscover", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Every rail and the genre grid are lists the keyboard walks (lib/listNav), so `j` goes on
|
// Every rail and the genre grid are lists the keyboard walks (lib/listNav), so `j` goes on
|
||||||
// from one to the next in page order (owner decision D4).
|
// from one to the next in page order (the owner's call).
|
||||||
describe("PlexDiscover · keyboard", () => {
|
describe("PlexDiscover · keyboard", () => {
|
||||||
it("registers every rail and the genre grid, and j walks on from rail to rail", async () => {
|
it("registers every rail and the genre grid, and j walks on from rail to rail", async () => {
|
||||||
plexDiscover.mockResolvedValue(fullPayload);
|
plexDiscover.mockResolvedValue(fullPayload);
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ function LinkShare({ job }: { job: DownloadJob }) {
|
|||||||
queryFn: () => api.downloadLinks(job.id),
|
queryFn: () => api.downloadLinks(job.id),
|
||||||
});
|
});
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
// D1: downloadable by default — matches the one-click Share (which relies on the backend
|
// Downloadable by default — matches the one-click Share (which relies on the backend
|
||||||
// default) and the common "let them have the file" intent. The owner can uncheck it here.
|
// default) and the common "let them have the file" intent. The owner can uncheck it here.
|
||||||
const [allowDownload, setAllowDownload] = useState(true);
|
const [allowDownload, setAllowDownload] = useState(true);
|
||||||
const [expiryDays, setExpiryDays] = useState<number | null>(null);
|
const [expiryDays, setExpiryDays] = useState<number | null>(null);
|
||||||
@@ -260,7 +260,7 @@ function LinkShare({ job }: { job: DownloadJob }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open with a link already generated: sharing is almost always the goal, so mint a default
|
// Open with a link already generated: sharing is almost always the goal, so mint a default
|
||||||
// (no password, no expiry, downloadable per D1) link the moment the dialog finds none — as if the
|
// (no password, no expiry, downloadable) link the moment the dialog finds none — as if the
|
||||||
// user had pressed "New link" → "Create". All further controls (password, download toggle, expiry,
|
// user had pressed "New link" → "Create". All further controls (password, download toggle, expiry,
|
||||||
// extra links, revoke) stay available on the created link and via the "New link" button.
|
// extra links, revoke) stay available on the created link and via the "New link" button.
|
||||||
const autoTried = useRef(false);
|
const autoTried = useRef(false);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Button } from "./ui/Button";
|
|||||||
import { IconButton } from "./ui/IconButton";
|
import { IconButton } from "./ui/IconButton";
|
||||||
|
|
||||||
// Up-next / end-card. Shown at episode end when a next item exists and the pref is on, INSTEAD
|
// Up-next / end-card. Shown at episode end when a next item exists and the pref is on, INSTEAD
|
||||||
// of the old instant jump — so binge stays under the viewer's control (D6-A). Counts down ~10s then
|
// of the old instant jump — so binge stays under the viewer's control. Counts down ~10s then
|
||||||
// advances; "Play now" advances immediately; Cancel/✕ dismisses and stays on the finished frame. It only
|
// advances; "Play now" advances immediately; Cancel/✕ dismisses and stays on the finished frame. It only
|
||||||
// lives on the play-to-the-end path; credits-auto-skip advances earlier and never reaches this card.
|
// lives on the play-to-the-end path; credits-auto-skip advances earlier and never reaches this card.
|
||||||
const COUNTDOWN_S = 10;
|
const COUNTDOWN_S = 10;
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ describe("EpisodeCard a11y", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// In a keyboard-walkable grid or rail the WHOLE card is the item (the ring goes round the
|
// In a keyboard-walkable grid or rail the WHOLE card is the item (the ring goes round the
|
||||||
// poster and its title, D2) and the poster — the card's own role=button — is what Enter opens.
|
// poster and its title) and the poster — the card's own role=button — is what Enter opens.
|
||||||
describe("Plex cards in a keyboard-walkable list", () => {
|
describe("Plex cards in a keyboard-walkable list", () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ describe("two-step sequences", () => {
|
|||||||
expect(calls).toEqual(["goDownloads"]);
|
expect(calls).toEqual(["goDownloads"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// No second step needs a modifier (D1), so a chord with one is the reader changing their mind: it
|
// No second step needs a modifier (see the page-level rows below), so a chord with one is the reader changing their mind: it
|
||||||
// must reach its own row. Swallowed, `g` then Ctrl+K opened the browser's search, not the palette.
|
// must reach its own row. Swallowed, `g` then Ctrl+K opened the browser's search, not the palette.
|
||||||
it("lets a modifier chord abandon the sequence and match as itself", () => {
|
it("lets a modifier chord abandon the sequence and match as itself", () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
@@ -148,7 +148,7 @@ describe("page-level scopes under a layer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("the page-level rows", () => {
|
describe("the page-level rows", () => {
|
||||||
// Owner rule D1, enforced rather than commented: on the Hungarian layout the owner types on, a
|
// The owner's rule, enforced rather than commented: on the Hungarian layout the owner types on, a
|
||||||
// chord with a modifier (or `/`, `?`, which ARE Shift chords there) is not a quick key.
|
// chord with a modifier (or `/`, `?`, which ARE Shift chords there) is not a quick key.
|
||||||
it("need no modifier and no Shift-only character", () => {
|
it("need no modifier and no Shift-only character", () => {
|
||||||
for (const s of SHORTCUTS) {
|
for (const s of SHORTCUTS) {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ const ALLOWED = new Set([
|
|||||||
// Anything else that looks like a code is one — `"""E30:`, `it("E36:`, `# S6:` and `S3's` included.
|
// Anything else that looks like a code is one — `"""E30:`, `it("E36:`, `# S6:` and `S3's` included.
|
||||||
const NOT_CODES = /\bS\d{1,2} ?·? ?E\d+|\btitle[=:] ?"[ES]\d{1,2}"|\bE4\/E7\/E9\b/g;
|
const NOT_CODES = /\bS\d{1,2} ?·? ?E\d+|\btitle[=:] ?"[ES]\d{1,2}"|\bE4\/E7\/E9\b/g;
|
||||||
const PLAN_CODES = [
|
const PLAN_CODES = [
|
||||||
// an epic (`E28`, `E9.5`, `E28/E30`), an older roadmap item (`R14`), a sprint (`S6`, `S3e2`) or a
|
// an epic (`E28`, `E9.5`, `E28/E30`), an older roadmap item (`R14`), a sprint (`S6`, `S3e2`), a
|
||||||
// phase (`P1`) — not `E2E`, not a ruff rule (`S110`), not `S3h`, the media-card decision
|
// phase (`P1`) or a plan's decision (`D1`, `D6-A`) — not `E2E`, not a ruff rule (`S110`), not
|
||||||
// docs/DESIGN.md is headed by
|
// `S3h`, the media-card decision docs/DESIGN.md is headed by
|
||||||
/\b(?:E\d{1,2}(?:\.\d+)?|R\d{1,2}|S(?!3h\b)\d{1,2}(?:[a-z]\d?)?|P\d)\b/,
|
/\b(?:E\d{1,2}(?:\.\d+)?|R\d{1,2}|S(?!3h\b)\d{1,2}(?:[a-z]\d?)?|P\d|D\d{1,2})\b/,
|
||||||
// a review round (`round 4`, `deep-review`)
|
// a review round (`round 4`, `deep-review`)
|
||||||
/\b[Rr]ound[ -]\d|deep-review/,
|
/\b[Rr]ound[ -]\d|deep-review/,
|
||||||
];
|
];
|
||||||
@@ -58,6 +58,9 @@ describe("plan codes", () => {
|
|||||||
'"""E30: the new sink.',
|
'"""E30: the new sink.',
|
||||||
'it("E36: keeps x", () => {',
|
'it("E36: keeps x", () => {',
|
||||||
"// both sides (E28/E30)",
|
"// both sides (E28/E30)",
|
||||||
|
"# D1: a newly shared download is downloadable by default",
|
||||||
|
"// binge stays under the viewer's control (D6-A)",
|
||||||
|
"// (owner decision D4)",
|
||||||
"# the roadmap's R6/R7 pair",
|
"# the roadmap's R6/R7 pair",
|
||||||
"// held since E9.5",
|
"// held since E9.5",
|
||||||
"// (S6) moved it",
|
"// (S6) moved it",
|
||||||
@@ -79,6 +82,8 @@ describe("plan codes", () => {
|
|||||||
'expect(btn.textContent).toContain("S2 · E1");',
|
'expect(btn.textContent).toContain("S2 · E1");',
|
||||||
"// the S3h media card",
|
"// the S3h media card",
|
||||||
'"S110", "S112",',
|
'"S110", "S112",',
|
||||||
|
"# the JPEG SOI marker (0xFF 0xD8 0xFF)",
|
||||||
|
"// a 3D view",
|
||||||
];
|
];
|
||||||
expect(cited.filter((l) => !isPlanCode(l))).toEqual([]);
|
expect(cited.filter((l) => !isPlanCode(l))).toEqual([]);
|
||||||
expect(clean.filter(isPlanCode)).toEqual([]);
|
expect(clean.filter(isPlanCode)).toEqual([]);
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ BIG_FILE = 800
|
|||||||
BE_COMPLEXITY = 10 # ruff's C901 default
|
BE_COMPLEXITY = 10 # ruff's C901 default
|
||||||
FE_COMPLEXITY = 20 # the frontend's line
|
FE_COMPLEXITY = 20 # the frontend's line
|
||||||
|
|
||||||
# planCodes.contract.test.ts's definition, copied: an epic, roadmap item, sprint or phase code, or a
|
# planCodes.contract.test.ts's definition, copied: an epic, roadmap item, sprint, phase or decision
|
||||||
# review round; NOT_CODES are its named exceptions, cut first (TV episode notation, a fixture's
|
# code, or a review round; NOT_CODES are its named exceptions, cut first (TV episode notation, a
|
||||||
# season or episode title, ruff's default `E4/E7/E9`). ASCII, because JavaScript's `\b` is.
|
# fixture's season or episode title, ruff's default `E4/E7/E9`). ASCII, because JavaScript's `\b` is.
|
||||||
NOT_CODES = re.compile(r'\bS\d{1,2} ?·? ?E\d+|\btitle[=:] ?"[ES]\d{1,2}"|\bE4/E7/E9\b', re.ASCII)
|
NOT_CODES = re.compile(r'\bS\d{1,2} ?·? ?E\d+|\btitle[=:] ?"[ES]\d{1,2}"|\bE4/E7/E9\b', re.ASCII)
|
||||||
PLAN_CODE = re.compile(
|
PLAN_CODE = re.compile(
|
||||||
r"\b(?:E\d{1,2}(?:\.\d+)?|R\d{1,2}|S(?!3h\b)\d{1,2}(?:[a-z]\d?)?|P\d)\b|\b[Rr]ound[ -]\d|deep-review",
|
r"\b(?:E\d{1,2}(?:\.\d+)?|R\d{1,2}|S(?!3h\b)\d{1,2}(?:[a-z]\d?)?|P\d|D\d{1,2})\b"
|
||||||
|
r"|\b[Rr]ound[ -]\d|deep-review",
|
||||||
re.ASCII,
|
re.ASCII,
|
||||||
)
|
)
|
||||||
NARRATION = re.compile(r"\b(used to|no longer)\b", re.IGNORECASE)
|
NARRATION = re.compile(r"\b(used to|no longer)\b", re.IGNORECASE)
|
||||||
|
|||||||
Reference in New Issue
Block a user