fix(gate): the plan-code guard catches bare sprint and phase codes, and the purge takes the P ones

The sprint alternative matched only `(S6)` or `S3e2`, so the commonest purged form (`# S6:`,
`S3's`, `"""S6 —`, `S1 justified`) and every phase code (`P1`) would come back unflagged; the phase
codes had in fact survived the purge in 16 lines (Plex client sections, the upload's header shape),
now reworded around them.

One pattern now covers epic, roadmap, sprint and phase codes; the named exceptions are TV episode
notation, a fixture's season or episode title (`title="S1"`) and ruff's `E4/E7/E9`, and a ruff rule
like `S110` does not match. The case test lists the purged forms and no longer claims every form.
audit_metrics copies the definition (ASCII, as JavaScript's \b is) and reports it as
plan_code_lines / plan_code_files.
This commit is contained in:
2026-10-05 00:50:36 +02:00
parent 5257d57196
commit 5ca2a0e6ee
10 changed files with 47 additions and 40 deletions
@@ -14,7 +14,7 @@ import { Pressable } from "./ui/Pressable";
import { Input } from "./ui/Input";
import { SURFACE } from "./ui/surfaces";
// Admin-only "manage this movie's collections" dialog (Collections P2). Lists the library's EDITABLE
// Admin-only "manage this movie's collections" dialog. Lists the library's EDITABLE
// collections with an in/out toggle (add/remove the movie, written back to Plex), plus a field to
// create a new collection seeded with this movie. Read-only collections (smart / IMDb-TMDb auto-lists)
// never appear here. Opened from the movie info page.
+2 -2
View File
@@ -79,7 +79,7 @@ beforeEach(() => {
vi.stubGlobal("XMLHttpRequest", FakeXHR as unknown as typeof XMLHttpRequest);
});
// The upload body is a Blob and only a Blob: `FormData` was accepted until P1 and is gone, because
// The upload body is a Blob and only a Blob: `FormData` was accepted once and is gone, because
// a multipart part is a DECLARED body that FastAPI spools before the route's auth dependency runs —
// and nothing on the backend can parse one any more (`python-multipart` was dropped with it).
const body = () => new Blob(["x"]);
@@ -432,7 +432,7 @@ describe("the debug session a tab records into", () => {
});
});
// P1 — the upload's wire shape is a security boundary, not a detail. A multipart part is a DECLARED
// The upload's wire shape is a security boundary, not a detail. A multipart part is a DECLARED
// body, which the server reads and spools BEFORE its auth dependency runs; the raw body is what lets
// `current_user` resolve first. A revert to FormData here would silently re-open that, and the
// backend's own test only sees the shape the client actually sends.
+1 -1
View File
@@ -391,7 +391,7 @@ export async function req<T = unknown>(
// `HttpError` throw — so callers handle failures exactly as they do a `req` call.
//
// `body` is a `Blob`/`File` — the bytes themselves, and nothing else. `FormData` was the other
// accepted shape until P1 and is deliberately gone: a multipart part is a DECLARED body, which
// accepted shape once and is deliberately gone: a multipart part is a DECLARED body, which
// FastAPI spools to disk before the route's auth dependency runs, and the backend dropped
// `python-multipart` altogether, so there is no longer anything on the other end that could parse
// one. `cfg.headers` is how a caller sends the metadata that used to ride as sibling form fields.
+21 -11
View File
@@ -19,17 +19,17 @@ const ALLOWED = new Set([
"frontend/src/lib/planCodes.contract.test.ts",
"scripts/audit_metrics.py",
]);
// Named exceptions, cut from a line before it is matched: a test show's episode titled "E1" (the
// whole string, quotes included) and ruff's default rule families. Anything else that looks like a
// code is one — a docstring or a test title opening with `"""E30:` / `it("E36:` included.
const NOT_CODES = /"E\d{1,2}"|\bE4\/E7\/E9\b/g;
// Named exceptions, cut from a line before it is matched: TV episode notation (`S2 · E5`), a test
// fixture's season or episode title (`title="S1"`, `title: "E1"`) and ruff's default rule families.
// 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 PLAN_CODES = [
// an epic (`E28`, `E9.5`, `E28/E30`) or an older roadmap item (`R14`) — not `E2E`, not a TV
// episode (`S2 · E5`)
/(?<!S\d{1,2}\s?·?\s?)\b(?:E\d{1,2}(?:\.\d+)?|R\d{1,2})\b(?!E)/,
// a sprint cited on its own (`(S6)`, `S3e2`) or a review round (`round 4`, `deep-review`) — not
// `S3h`, the media-card decision docs/DESIGN.md is headed by and the design lab labels
/\(S\d{1,2}[a-z]?\d?\)|\bS(?!3h\b)\d{1,2}[a-z]\d?\b|\b[Rr]ound[ -]\d|deep-review/,
// an epic (`E28`, `E9.5`, `E28/E30`), an older roadmap item (`R14`), a sprint (`S6`, `S3e2`) or a
// phase (`P1`) — not `E2E`, not a ruff rule (`S110`), not `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/,
// a review round (`round 4`, `deep-review`)
/\b[Rr]ound[ -]\d|deep-review/,
];
const isPlanCode = (line: string) => {
@@ -53,7 +53,7 @@ function tracked(): string[] {
}
describe("plan codes", () => {
it("are recognised in every form a citation takes", () => {
it("are recognised in each form the purged citations took", () => {
const cited = [
'"""E30: the new sink.',
'it("E36: keeps x", () => {',
@@ -61,14 +61,24 @@ describe("plan codes", () => {
"# the roadmap's R6/R7 pair",
"// held since E9.5",
"// (S6) moved it",
"# S6: attach the acting user's cookie",
'"""S6 — per-user cookies',
"// S3's canonical upsert",
"// S1 justified that",
"// the order since P1",
'"""P1\'s invariant',
"// a sprint then a roadmap item: S2 R14",
"// deep-review round 4 found it",
];
const clean = [
'title="E1",',
'title: "S1",',
"# ruff's default rule set (E4/E7/E9 + F)",
"// an E2E run",
"// S2 · E5 is the second episode",
'expect(btn.textContent).toContain("S2 · E1");',
"// the S3h media card",
'"S110", "S112",',
];
expect(cited.filter((l) => !isPlanCode(l))).toEqual([]);
expect(clean.filter(isPlanCode)).toEqual([]);