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:`.
It spread the event's snapshot, which still carried the sfNav stamp lib/history's own listener had
just taken off the entry, so a later landing there (an overlay closing) refocused a stale item. It
now spreads the live entry (test red on the snapshot). Also: format.test's comment named a test
image that never runs vitest; it is the gate host's locale.
The chat cut removed the only WebSocket and CI went before it, but comments still leaned on both:
`?account=` was justified by the socket, job polling by an in-process WS registry, the e2e flows as
"proxy-safe (none use the WebSocket)", and pyrightconfig named a CI container the engine contract
test never checked. Each now says what holds today (the wallet gate, no push channel, the two
contract test halves, the image build's `npm ci`, the gate).
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.
The fallback rendered the feed but left the history entry and the address bar on the dead page, so
a Feed click was a no-op (the same-page guard), filter mirroring kept `?page=messages` and the nav
diagnostics logged a page that does not exist. The entry is now replaced with the feed's, keeping
every other URL key (test red without it).
The registration language is read today (preferences.language, the mail language); the Modal and
the players order against popovers through layerStack, not the FloatingTree; the upload's auth
dependency is current_user; the module of access tests covers onboarding and access, not only the
canonical email. The dev proxy's WebSocket upgrade served the removed messages socket and goes; the
test compose header says a pure-logic run needs SIFTLODE_TEST_REQUIRE_DB=0; the speed measure no
longer reports an unreachable database as "no share link".
- planCodes: the quote and slash lookarounds let the commonest form through (a docstring or test
title opening with a code, a slash pair); the two real exceptions (an episode titled "E1", ruff's
E4/E7/E9) are now named and cut first, with cases that fail on the old pattern. audit_metrics
counts by the same definition.
- button-primitive: both file lists live in the rule body, out of sight of the snapshot and the
register; each is now pinned to its exact set of ui/ files (red when either is widened).
- check.sh --brief shows a short log whole and keeps the line before each hit, so ruff's rule and
eslint's file path survive the excerpt.
- lint-changed runs prettier over every changed frontend file, as the gate does, not only code.
- CLAUDE.md and gates.md no longer claim the basedpyright baseline was found harmless: it was never
triaged, and the reportOptional* entries are the likeliest real bugs.
0098 deleted the reset job's run rows but not the scheduler_interval rows that changing its interval
wrote; with the job's label gone they rendered as a raw id. The delete now takes every job-targeted
row for it, and a test seeds both kinds below 0098 and upgrades (red on the old filter). The test
has its own file so the removed-features guard can name it.
A history entry pushed while the Messages page existed still names it after a reload onto the new
build; popstate took sfPage unchecked and the shell's module lookup threw. It is now validated with
isPage, like every initial-load path already was.
planCodes.contract.test.ts scans every tracked text file and every new one not yet added (comments,
docstrings, test titles, lint messages alike) for an epic or roadmap code (`E28`, `E9.5`, `R14`) and
for a sprint or review round cited on its own (`(S6)`, `S3e2`, `round 4`, `deep-review`). Not
`E2E`, not a ruff rule family (`E4/E7/E9`), not a TV episode (`S2 · E5`, a test episode titled
"E1"), not `S3h` (the media-card decision docs/DESIGN.md is headed by). Markdown and the Alembic
migrations keep their history and are not scanned.
One sweep test rather than an ESLint rule plus a ruff one: the codes sat in Python, TS, CSS, shell,
TOML, YAML and lint messages, and no single linter reads all of them. Seen red on a planted
backend/app file (all three plan-code lines named, its allowed line not).
audit_metrics.py counts epic codes by the same definition (it used to miss `E9.5` and `R6`), and
CLAUDE.md's "comment the why" rule names the test.
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
The seven gallery shots dated from 2026-07 and still showed the old
labelled sidebar with the removed Messages item. Retaken at the same size
(1200x675 @2x, dark) from the localdev stack with the E2E account (no
personal account name on screen), filled for the shoot with a copy of a
real account's subscriptions, playlists and finished downloads, removed
again afterwards.
The DB lane provokes constraint errors on purpose, and their ERROR: lines
from the testdb container came first in a red pytest excerpt, ahead of the
failing test (seen in E35 S3). The excerpt now skips the testdb-N lines.
gates.snapshot.json listed ~800 file paths under their config, so every
new or deleted frontend file regenerated it (31% of commits, E34 S2) and
the regen diff buried the gate changes it exists to show. Now a scope is
named by path (scopeOf: src, src tests, ui, lib tests, …), the snapshot
holds one effective config per scope (2345 → 1456 lines), and a new test
fails when a file's config is not its scope's.
Not narrowed: a new file in a known place changes nothing (it gets its
scope's config, checked), a glob that moves one file into another config
fails (seen: an override on Feed.tsx → "src: src/components/Feed.tsx"), a
scope whose rules change is a snapshot diff, and a file no config lints
is still a register miss.
Both lanes printed a coverage total with no threshold (owner decision E32
D4). No real hit as a lane: the one time the number found anything (5 bugs,
E32) it was read by hand, and since then it was a line nobody acted on. It
was also the most expensive part of the vitest lane, and the run that
flaked in E35 S1. The vitest lane runs without --coverage, the pytest lane
runs plain pytest; docs/development.md gives both on-demand commands, and
the register row says why.
Two rules looked at the same <button>: button-primitive refused a raw one
outside the control primitives, focus-safe-disabled refused a raw
`disabled` one everywhere but FocusSafeButton and AnchoredPopover. They are
one rule now, with the same reach: outside the primitives a raw button is
refused (a disabled one with the focus message); inside a primitive and in
a test only a raw `<button disabled>` is, and the two native-disabled owners
are named in the rule rather than in a config override. One override block,
one rule entry and two register rows go; the snapshot loses a config group.
focusSafe.contract.test.ts pins every case, the primitive and test ones new.
BOX_PX and GRID_MIN_PX were two esquery patterns with two messages for one
rule (DESIGN.md: a px box does not scale with --font-scale). They are one
alternation now, with one message that names both fixes. Nothing it refused
before passes now; pxBox.contract.test.ts pins both shapes unchanged.
Both came out of the E28 claim audit (0521149c named z-levels, e13d93a3
static labels as flat Badges) after the audit itself had fixed the sites.
No real hit since: 0 matches in every lint run from E28 to E34 (S2/S3
measurement). Each still cost a probe test (zLayer.contract, the edged-label
half of hoverAndLink.contract) and a place in the gate snapshot. The design
rules stay in DESIGN.md (Tokens: no ad-hoc z-index; Static vs pressable),
with a decision-log row saying the selectors went and why.
react-hooks/exhaustive-effect-dependencies, memoized-effect-dependencies and
rule-suppression were switched on in E32 "for the plugin version that wires
them up". Read in the plugin source and probed (E32 review): the first's
validation is compiled off, nothing emits the second's category, and lint
mode hands the third no suppression rules. 0 hits by construction — a rule
that cannot report reads as coverage it does not give. The comment keeps
their names and why, so a plugin upgrade can re-check them.
The production pass (7ed2ac4e, E32) asked "is this export used by what
ships?". No real hit: its only findings were 37 exports a test imports,
answered with 36 `@internal` JSDoc tags (E34 S2) — test-only exports, not
defects. Since then it has only ever asked for another tag. The tags go
with it (36, no longer read by anything), and so do the production-only
`!…!` exclusions in knip.jsonc and their register rows. Plain knip (unused
files, exports and dependencies) keeps running in the gate.
sourceSize.contract.test.ts, its test kit and backend/tests/test_source_size.py
held 13 FE / 9 BE file ceilings and 64 FE / 32 BE function ceilings. No real
hit (E34 S2/S4): its history is its own birth (751e1ae5), two rounds of
fixes to its own logic (e432dc27, d1df76e8), the snapshot move (182ec311)
and ceiling edits inside feature fixes (5806adda, df4a79a1, and the S2/S3
cuts here). It caught no defect, went red 8 times on its own limits and drew
5 review findings about itself. The numbers stay visible: audit_metrics
reports file size and complexity, and an epic compares before and after.
docs/gates.md: the two size rows go; the C901 row says where it is read now.
The demo account — one shared user that whitelisted emails entered from
the login page, reset on a schedule — goes end to end (owner decision,
E34): demo.py and the reset job, users.is_demo and demo_whitelist
(migration 0098 deletes the demo user, its audit rows, the reset job's
override and notices, then drops the column and table; the downgrade
restores the empty schema), the /auth/demo entry, the admin whitelist and
reset, the Demo tab, the banner, the login page's "Try the demo", and
every is_demo branch in routes, userscope, scheduler and the UI.
require_human only ever refused the demo account, so its 87 uses become
current_user (owner-approved); require_write_scope keeps its own check.
The login page's "or" divider now shows only beside the Google button:
without the demo entry under it, an instance without Google would have
said "or" to nothing.
A contract test (removedFeatures) fails on the word anywhere in the
backend, frontend, e2e or scripts — seen red on a planted hit. Size
ceilings tightened for the modules that shrank; admin.py is back under
the limit.
E2EE direct messages, the chat dock, the Messages module, the message
WebSocket and its realtime registry, and the messages / message_keys
tables (migration 0097; the downgrade restores the empty schema). Prod
held two welcome notes, no message between people and one key — never
used (owner decision, E34). WebSocket now has no owner module: the lint
bans it everywhere. Privacy policy updated: it no longer promises E2EE
for a feature that is gone.
POST /auth/request-access had no caller left: the login page's form was
gone, and registration and a refused Google sign-in already land in the
same admin approval. A third public write path with no protection of
its own (owner decision, E34).
Its one guard moves to where every path passes: upsert_pending_invite no
longer opens a pending row for an address the env already lets in. The
registration path lacked it — registering b.ob@gmail.com beside an
env-granted bob@gmail.com opened a pending row, and is_allowed answers
from the invite rows once any exist, so the real owner was locked out.
The tests that drove the endpoint now drive registration or the shared
function.
Under a full coverage run on every core, a test file's first cold import
(0.6-0.8 s alone) outran the 5 s case timeout and the half-done import
failed every later case in the file: the vitest lane went red in 2 of 5
runs with no defect. A beforeAll warm-up in the 4 files of that pattern;
5 of 5 full runs green after.
The scheduler row read 'quota reaped 0' — the raw key, humanised — because the new result key had no
label (found in the browser check; nothing tests result-key labels).
It always said a live search spends the shared API quota, while the default source (the website's
own endpoint, scrape) spends none — the owner avoided a free search because of it. /api/me now names
the running search_source, and the tooltip names the cost under it: none for scrape (the enrich step
costs a unit or two), 100 units a page for the official API.
quota_events had no retention: one row per API spend plus a zero-unit row per live search, kept
forever. The daily history cleanup (audit_gc) now also prunes it past quota_retention_days (a new
Quota setting, default 365 so the admin usage view's longest window stays whole; 0 = forever). The
floor of two days keeps today's rows, which the per-user search cap counts.
The Gitea Actions gate repeated, after the merge, what a release already runs
before it, and its runner shared the git host's container: one run there
starved Gitea of memory until it stopped answering. The lanes now live in
scripts/check.sh, run the same by the release dispatcher and by hand, and the
gate-contract test holds its lines (the ruff and pip-audit checks read it
instead of the workflow). The two audits only CI ran join the gate: pip-audit
over the lock (pinned in the test image, blocking at any severity) and npm
audit of the production dependencies. The engine contracts drop the CI
containers; the register records that pushes have no server-side gate.
A form dialog opens on the body's first control, and the composer's first
control is the Edit / Preview switch above the form: the focus landed there,
at open and when a retried load brought the form, and typing went nowhere. A
form can now mark the field to open on (data-initial-focus); every other
dialog keeps its first control. The Modal's comment on the re-armed watch now
says where the focus goes when the focused control is removed but others stay.
The register test detected narrowings by modelling config shapes, and each
review round found one more shape it did not model (a basePath global ignore,
a rule src never declares, a widened glob filed under a narrow row, a substring
check on a CI line, knip.json beside knip.jsonc). It now compares instead:
gates.snapshot.json holds ESLint's effective config for every code file and the
lines that run each lane, and any difference fails until it is regenerated.
The register is read off the snapshot's configs, which surfaced the ui/ and
lib/ test scopes the sampling never reached.
The complexity ratchet also names a wrapped function by its binding
(const onSave = useCallback(() => ...)), not as "useCallback callback".
The watch stayed armed when the form arrived while the focus was off the close button, so a
later change in the body pulled the focus off a close button the user went back to. It now
disarms on arrival and re-arms only when the focus is taken away.
rebase updated the state only, so the mirror the callbacks read still held the removed
row and an undo right after it wrote that row back for a moment.
ESLint leaves an arrow bound to a const or a property unnamed, so three ratchet keys were
(anonymous) and ranked by position again. A tiny rule reports each unnamed function's
binding (const, property, assignment, else <callee> callback) at the head complexity
reports, and the keys line up by location.
Round 2 of the E32 review found the key whitelists blind: a line regex saw neither a
[lint.per-file-ignores] table nor a quoted key, and pytest.ini's continuation lines and
key: value form slipped past. Ruff's config is now parsed (smol-toml) and held to its
key paths; pytest.ini to its whole lines; the CI ruff steps to --config backend/ruff.toml,
which is what makes that file the whole configuration.
ESLint: every glob of a block must reach a sample (reactPlugin.ts and devOnlyGuard.ts
rode along unseen), a block's own severity is held to src, a no-restricted-* list is held
to the src block's declaration rather than Feed.tsx's effective config, named global
ignores count, an ignored sample is named, and linterOptions are checked. tsconfig.node
must keep allowJs + checkJs while it lists a .js file; knip's positive glob is pinned.
ruff.toml ignored E402 everywhere for main.py's logging setup. Measured: 28 hits, of which 11
had nothing to do with it — runner.py's logger line between its imports, models.py importing
canonical_email mid-file, a test constant above its imports, and main.py's own third-party and
mimetypes imports. Those are moved up. What stays after configure_logging in main.py is marked
per statement (noqa E402, logging is configured first), its comment now says why the order
matters (a line logged while importing, not the logger's creation). The register row goes, the
ruff key whitelist loses ignore, and a late import is red again. The T20 comment counts the
third deliberate print (scripts/e2e_seed.py).
scripts/check-node-modules.mjs and gen-changelog.mjs run in check and release but sat outside
every lane: the eslint and prettier lanes run from frontend/, and an ESLint config cannot reach
past its own directory. A contract test now holds them to the js-configs bar (recommended rules
under Node, the frontend's Prettier style); an unused variable and the old formatting are each
red. 0 lint hits; both files reformatted. Registered in docs/gates.md.
eslint.config.js called every zero-hit React Compiler diagnostic a tripwire. Under
eslint-plugin-react-hooks 7.1.1 three cannot report anything: exhaustive-effect-dependencies (its
validation is compiled off), memoized-effect-dependencies (nothing emits its category) and
rule-suppression (lint mode passes no suppression rules). Probed: a missing effect dependency is
reported by exhaustive-deps only, and an inline disable is named by nothing. The header's
"33 suppressions" of exhaustive-deps is 0. reactPlugin.ts said one react-hooks rule was still
off; none is (three run at warn, which --max-warnings 0 fails).
The Plex and feed filter fixtures were typed as the filter type itself, whose scalar filters are
optional: a new optional field compiled without a test value, so its wire key went unchecked.
Both are Required<…> now (a probe field fails typecheck on each). The feed set gained the fields
it omitted (year, librarySource, the scope and show switches) and goes out with and without year,
which wins over the other date filters; a misspelt year key is now red in the wire check, which
passed it before.
The ceilings were a per-file ranked list, so one function could grow by what another in the same
file lost: Home.tsx's [34, 32] passed with Home at 32 and Hero at 34. Both twins now key every
offender as module::function (an unnamed one is (anonymous); a repeated name gets 2, 3 in source
order) and re-baseline at today's measured values. Swapping two ceilings is red on both sides
(Home 34 > 32; feedquery filtered_query 28 > 14).
The ratchet measures shipped code only. docs/gates.md said C901 covered backend + scripts; it
covers backend/app. The exclusion of tests, test kits, e2e and scripts/ is now a row with its
measured hits, and the stale counts are replaced (9 BE modules over 800, not 11).
Leaving the Playlists page and coming back painted the selected list empty ("0 videos") while
the rail counted its rows. The page re-seeds its undoable order from the cached list in an
effect; StrictMode runs that effect twice on the remount, so a reset was followed in the same
tick by a rebase. Since E32 S3 a reset is state only and the rebase read the mirror ref, which
still held the empty initial history, and wrote it back over the reset.
rebase is now a state updater, so it builds on whatever an earlier call in the tick installed.
Both tests seen RED (hook: [] for [10, 20]; page: no rows), and the bug reproduced in the
browser on the old hook.
E32 review round 1 (T1–T5, s1). The test that holds docs/gates.md to the effective configuration
could itself be passed by a narrowing it did not look at:
- ESLint: besides each scope's sample, every config block is read on its own. A block that targets
files no sample reaches fails as an "unsampled block"; a no-restricted-* list re-declared without
an entry src has, an option that lets something through (allow…/ignore…/except…), a block-level
`ignores` and EVERY global ignores object (not only the first) count. A scope switching a src rule
at warn off now counts too (it skipped them). Two live narrowings surfaced and are registered:
no-floating-promises' allowForKnownSafeCalls (src and e2e) and the three network owners'
no-restricted-globals.
- TypeScript: a strict sub-flag set false beside `strict: true`, an extends/exclude/files, a changed
include, and a frontend-root config file no project checks. eslint.config.js is that file: listed,
with the 43 errors strict checkJs reports in it (measured, none a defect).
- Ruff, basedpyright, pytest and knip are read as key whitelists (an unknown key fails until someone
decides whether it narrows); pyright's information/hint count as weak; ruff's extend-select is
pinned; pytest's three keys are pinned exactly; knip's script must run both modes.
Each new check was probed with a planted narrowing (an unsampled block, a block ignores, a second
global ignores, a warn rule off in a scope, strictNullChecks false, a ruff extend-exclude, a pytest
-m) and failed on it.
E32 review round 1 (F3–F6).
- Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled
listener set (the chat dock gets a test of its own).
- The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one
constant (REDUCED_MOTION) for the hook and the carousel's one-off read.
- A link-restored collection filter showed its raw rating_key in the picker while the titles
loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder
(labelsPending, as the channel picker does) and so does the chip row.
- The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager
showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick
still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's
row shows every pick, as a placeholder while loading and as its id after a failure.