test(fence): the unit fence has no exemptions, and the owner filter is held in every module

The per-file pin list and the unit-by-unit switch go: every route file is held
to zero database work, every unit's package to its edges, and core code says
whose a row is through userscope too. CLAUDE.md, the backend README and the
gate register describe the units as they now are.
This commit is contained in:
2026-10-19 23:36:01 +02:00
parent ad6ccbac05
commit 5ce292cd28
5 changed files with 80 additions and 63 deletions
+8 -2
View File
@@ -66,6 +66,12 @@ how it gets built. Concretely:
`downloads/`, `reader/`, `discover/`, `catalog/`, `youtube/`, `issues/` (shared by routes + scheduler + worker) → flat core (`auth`, `apitokens`, `quota`, `jobruns`, `downloads/`, `reader/`, `discover/`, `catalog/`, `youtube/`, `issues/` (shared by routes + scheduler + worker) → flat core (`auth`, `apitokens`, `quota`, `jobruns`,
`sysconfig`, `state`, `audit`, `notifications`, `userscope`, `ratelimit`, `bodylimit`, `progress`, `collation`, `sysconfig`, `state`, `audit`, `notifications`, `userscope`, `ratelimit`, `bodylimit`, `progress`, `collation`,
`security`, `logs`, `logsink`, `diagnostics`, `debugsession`) → `models/db/config`. **Dependency direction is one-way: core never imports routes.** `security`, `logs`, `logsink`, `diagnostics`, `debugsession`) → `models/db/config`. **Dependency direction is one-way: core never imports routes.**
The domain is four **units**, each owning its tables: Capture (`downloads/`), Reader (`reader/`, `discover/`, the playlist
sync), Catalog (`catalog/`, the `sync/` ingests) and Plex (`plex/`). A unit's route does no database work: it reads through
the unit's `queries`, writes through its `service` (which flushes) and commits. A unit's code reads another unit's tables
only along a declared edge and writes them only through that unit's service; core reaches a unit's tables through its
`queries`. **Fence-held, with no exemptions** (`backend/tests/test_unit_fences.py`); conventions in
[`backend/app/README.md`](backend/app/README.md).
Frontend: `main.tsx` provider tree → `App.tsx` shell (a shared `/watch` link boots from its own Frontend: `main.tsx` provider tree → `App.tsx` shell (a shared `/watch` link boots from its own
entry, `watch.html` → `watchMain.tsx`, held app-free by a contract test) → `moduleRegistry` route-chunks → module entry, `watch.html` → `watchMain.tsx`, held app-free by a contract test) → `moduleRegistry` route-chunks → module
components → the **platform layer `components/ui/`** (domain-free primitives: the popover engine and components → the **platform layer `components/ui/`** (domain-free primitives: the popover engine and
@@ -87,12 +93,12 @@ CQRS, a repository/DI framework, a router library, a generic CRUD-router factory
| ORM | SQLAlchemy 2.0 `select()` | `db.query(...)` | | ORM | SQLAlchemy 2.0 `select()` | `db.query(...)` |
| Request body | a Pydantic model | `payload: dict` | | Request body | a Pydantic model | `payload: dict` |
| Request SIZE | nothing — `bodylimit` caps every route (SEC-20); need more? add a ceiling to `ROUTE_MAX_BODY_BYTES` | `capped_body` on a route that does not read raw bytes itself | | Request SIZE | nothing — `bodylimit` caps every route (SEC-20); need more? add a ceiling to `ROUTE_MAX_BODY_BYTES` | `capped_body` on a route that does not read raw bytes itself |
| Ownership check | `owned_or_404` / `get_or_404` | ad-hoc `_own_playlist`/`_owned`/`_own_job` | | Ownership check | `userscope.owned_by` (a filter) / `owned_row` (a row by id): a unit's `queries.own_x` returns None and its route answers `not_found`; a core route `owned_or_404` | an inline `Model.user_id == …` (fence-held, in every module) or a `db.get` and a compare |
| Admin gate import | `app.auth` | the `app.routes.admin` re-export | | Admin gate import | `app.auth` | the `app.routes.admin` re-export |
| Upstream failure status | `422` with JSON detail | `502` (reserve 502 for raw byte-proxies) | | Upstream failure status | `422` with JSON detail | `502` (reserve 502 for raw byte-proxies) |
| User-facing error | structured `detail` (`{code, reason}`) | a bare English string under a bilingual UI | | User-facing error | structured `detail` (`{code, reason}`) | a bare English string under a bilingual UI |
| Mutation response | `{"ok": true, …}` (`{"deleted": id}`), test-held | a bare `{"unsubscribed"}`/`{"cleared": n}`, no `ok` | | Mutation response | `{"ok": true, …}` (`{"deleted": id}`), test-held | a bare `{"unsubscribed"}`/`{"cleared": n}`, no `ok` |
| Transaction commit | the handler commits; helpers `flush` (test-held list) | helpers that commit (only `quota` and `jobruns` own their session) | | Transaction commit | the handler commits; a unit's `service` and every helper `flush` (fence-held) | a helper that commits (only `quota`, `jobruns` and the jobs the fence's `UNIT_COMMITS` names own their session) |
| Admin knob storage | `sysconfig` (values) | env / `app_state` / a table of its own (the job intervals had one) | | Admin knob storage | `sysconfig` (values) | env / `app_state` / a table of its own (the job intervals had one) |
| Job status word | the `downloads/status.py` StrEnum | loose `"canceled"`/`"cancelled"` literals | | Job status word | the `downloads/status.py` StrEnum | loose `"canceled"`/`"cancelled"` literals |
| Current time | inline `datetime.now(timezone.utc)` | new `now_utc`/`_now` helpers | | Current time | inline `datetime.now(timezone.utc)` | new `now_utc`/`_now` helpers |
+46 -16
View File
@@ -1,9 +1,8 @@
# Backend conventions # Backend conventions
The rules below are already followed across `backend/app` — they lived only in a few module The rules below are already followed across `backend/app` — they lived only in a few module
docstrings (`userscope.py`, `routes/_common.py`) until this file collected them (E3 S3, docstrings (`userscope.py`, `routes/_common.py`) until this file collected them. When you add code,
B-core-20). When you add code, match these so the next reader doesn't have to guess "which pattern do match these so the next reader doesn't have to guess "which pattern do I copy".
I copy".
## Module layering ## Module layering
@@ -13,12 +12,13 @@ Three layers, by *who* runs the code — not by feature:
response models, status-code mapping, serialization. It owns logic that runs *only* on a request. response models, status-code mapping, serialization. It owns logic that runs *only* on a request.
Business logic that a request happens to trigger does **not** live here if anything else runs it too. Business logic that a request happens to trigger does **not** live here if anything else runs it too.
2. **`plex/`, `downloads/`, `sync/`, `issues/` — domain logic shared by more than one caller.** Anything the 2. **The units and the other domain packages — logic shared by more than one caller.** A unit's
**scheduler or the worker** runs (not just a request) lives in one of these domain packages, so the database work (below), and anything the **scheduler or the worker** runs (not just a request), lives
route, the worker, and the scheduler share one definition. Examples from this codebase: in a domain package, so the route, the worker, and the scheduler share one definition. Examples from
`downloads/service.py` owns enqueue + the job state transitions (routes call it); `plex/sync.py` is this codebase: `downloads/service.py` owns enqueue + the job state transitions (routes call it);
run by both the admin route and the scheduler; `downloads/pipeline.py` is the worker's child pipeline; `plex/sync.py` is run by both the admin route and the scheduler; `downloads/pipeline.py` is the
`issues/bundle.py` is the one definition of a bug report's files, for the zip export and the tracker. worker's child pipeline; `issues/bundle.py` is the one definition of a bug report's files, for the
zip export and the tracker.
3. **Flat `app/*.py` core modules — cross-cutting primitives.** `auth` (with `apitokens`, a machine's 3. **Flat `app/*.py` core modules — cross-cutting primitives.** `auth` (with `apitokens`, a machine's
folder-scoped way into the Files library), `sysconfig`, `quota`, `audit`, `jobruns` (every folder-scoped way into the Files library), `sysconfig`, `quota`, `audit`, `jobruns` (every
@@ -34,19 +34,49 @@ Three layers, by *who* runs the code — not by feature:
a layer — into the relevant domain package, or a flat core module if it's cross-cutting. Don't a layer — into the relevant domain package, or a flat core module if it's cross-cutting. Don't
pre-abstract for a single caller; don't leave shared logic duplicated once there are two. pre-abstract for a single caller; don't leave shared logic duplicated once there are two.
## Units
The domain is four units, each owning its tables (`models.py` in its package):
| Unit | Package | Its routes |
| --- | --- | --- |
| Capture | `downloads/`, the worker | `downloads.py`, `files.py`, `public.py` |
| Reader | `reader/`, `discover/`, `feedquery.py`, the playlist sync and tag suggestions in `sync/` | `feed.py`, `playlists.py`, `saved_views.py`, `discover.py`, `tags.py` |
| Catalog | `catalog/`, the ingests in `sync/` | `channels.py`, `search.py`, `sync.py` |
| Plex | `plex/` | `plex/` |
- **A unit's route does no database work.** It validates, reads through the unit's `queries.py`,
writes through its `service.py`, serializes, and commits. It may name a unit's model as a type
(under `TYPE_CHECKING`), never use one as a value, and it passes a kind (`"movie"`), not a model, to
a query.
- **A unit's code keeps to its edges.** It reads another unit's tables only along a declared edge
(Capture and Reader read Catalog's and Plex's: the feed joins `Video` with `VideoState` in one
query) and writes them only through the owner's service. Core code reaches a unit's tables through
that unit's `queries`.
- **Whose a row is, `userscope` says** — in every module, core included: `owned_by(Model.user_id,
user)` for a filter, `owned_row(db, Model, id, user)` for a row by id (someone else's row is the
same answer as a missing one). A unit's `queries.own_x` returns None and its route answers
`not_found`; a core route calls `owned_or_404`.
All of it is held by `tests/test_unit_fences.py`, with no exemptions: the unit map, each unit's
packages and edges (`UNIT_PACKAGES`, `READS`) and the jobs that commit (`UNIT_COMMITS`) are listed
there.
## Transaction ownership ## Transaction ownership
This follows CLAUDE.md's canonical rule verbatim — **"the handler/job-wrapper commits; helpers flush This follows CLAUDE.md's canonical rule — **the handler commits; a unit's `service` and every helper
(only `quota` and `jobruns` own their session)"** — so read it that way, not as an exception to it. The commit belongs flush** — so read it that way, not as an exception to it. The commit belongs to the **operation
to the **operation owner**; anything called *inside* another transaction only stages its change owner**; anything called *inside* another transaction only stages its change
(`db.add` / `flush`) and lets the owner commit, so the whole operation is one transaction. (`db.add` / `flush`) and lets the owner commit, so the whole operation is one transaction.
`audit.record()` is the reference: it adds the audit row and the caller commits it together with the `audit.record()` is the reference: it adds the audit row and the caller commits it together with the
mutation it records, so an audit entry can never exist without its change (or vice-versa). mutation it records, so an audit entry can never exist without its change (or vice-versa).
- The **operation owner** is usually the route handler. A `service.*` operation the handler delegates a - The **operation owner** is the route handler, or a job. A unit's `service` flushes even when it
whole transition to (e.g. `service.cancel`) *is* that owner and commits its one transition — it isn't makes a whole transition (`service.cancel`): the handler commits it, together with whatever else the
a mid-operation helper. A helper that owns its OWN short-lived session (the worker's `jobdb` writes, request changed. What owns its OWN session commits it: `quota` and `jobruns`' run rows, and a unit's
`jobruns`' run rows — the same carve-out CLAUDE.md grants `quota`) commits that session. Everything else flushes. jobs (the worker's `jobdb` writes, the syncs and ingests that commit per page or batch so a later
failure loses nothing already stored) — the fence's `UNIT_COMMITS` names each. Everything else
flushes.
- `SessionLocal` is `expire_on_commit=False`: after a raw `UPDATE` + commit, an ORM object you already - `SessionLocal` is `expire_on_commit=False`: after a raw `UPDATE` + commit, an ORM object you already
loaded is **stale**. Read the fresh value from the row (RETURNING, or a re-`SELECT`), not the ORM loaded is **stale**. Read the fresh value from the row (RETURNING, or a re-`SELECT`), not the ORM
attribute — see `service.decr_ref_count`. attribute — see `service.decr_ref_count`.
+2 -2
View File
@@ -1,10 +1,10 @@
"""Whose a row is, and who an account is to the others — one place for both. """Whose a row is, and who an account is to the others — one place for both.
**Ownership.** A user-owned table says whose a row is in one column (`user_id`, or `to_user_id` for **Ownership.** A user-owned table says whose a row is in one column (`user_id`, or `to_user_id` for
something handed to them). A unit's reads and writes name that column through `owned_by` and fetch a something handed to them). Every read and write names that column through `owned_by` and fetches a
row by id through `owned_row`, never with an inline `Model.user_id == …`: the rule "a row owned by row by id through `owned_row`, never with an inline `Model.user_id == …`: the rule "a row owned by
someone else is answered like a missing one" then lives here, and a contract test someone else is answered like a missing one" then lives here, and a contract test
(`test_unit_fences.py`) holds a switched-on unit to it. (`test_unit_fences.py`) holds every module to it, core included.
**Recipients.** "An active, non-suspended account" — the download share-by-email and the recipient **Recipients.** "An active, non-suspended account" — the download share-by-email and the recipient
picker filter on it. Kept here so the two forms (SQL clauses for a query, a Python check for a loaded picker filter on it. Kept here so the two forms (SQL clauses for a query, a Python check for a loaded
+23 -41
View File
@@ -5,17 +5,14 @@ What it may not do is build SQL, call the session's data methods, change a model
import a unit's tables: when a route did those, ownership, the commit and the conditional `WHERE` import a unit's tables: when a route did those, ownership, the commit and the conditional `WHERE`
lived in a different place in each handler, and a fix in one never reached its siblings. lived in a different place in each handler, and a fix in one never reached its siblings.
The rule is switched on unit by unit. `ALLOWED` pins what each HTTP file still does today, and the The rule has no exemptions: a single `select()` in a unit's route file fails it.
pin is exact: a count that rises fails (new database work in a route), and one that falls fails
until its entry is lowered (the fence only tightens, as `baseline.json` does for basedpyright). An
empty `ALLOWED` is the end state — a file listed there is a debt, not an exemption.
A core route (identity, admin, ops) is not a unit's HTTP layer and may use the session; it may A core route (identity, admin, ops) is not a unit's HTTP layer and may use the session; it may
still not reach into a unit's tables. A route may NAME a unit's model as a type (under still not reach into a unit's tables. A route may NAME a unit's model as a type (under
`TYPE_CHECKING`), never use one as a value. `TYPE_CHECKING`), never use one as a value.
Below the HTTP layer, a fenced unit's own code keeps to its edges (`FENCED`, `READS`): see the Below the HTTP layer, a unit's own code keeps to its edges (`READS`): see the second half of this
second half of this file. file. Whose a row is, every module says through `userscope` — a core one too.
""" """
import ast import ast
@@ -107,10 +104,6 @@ UNIT_ROUTES = {
"plex/": "plex", "plex/": "plex",
} }
# What each HTTP file still does, by file: the debt a unit's sprint pays off. Exact counts. A core
# route's count is only the unit tables it imports; it is paid by the sprint of the unit it reaches.
ALLOWED: dict[str, int] = {}
# The session methods that read or write data. `commit` and `rollback` stay with the handler. # The session methods that read or write data. `commit` and `rollback` stay with the handler.
SESSION_DATA = { SESSION_DATA = {
"add", "add",
@@ -257,19 +250,13 @@ def test_every_listed_route_exists():
rels = [rel for rel, _ in _http_files()] rels = [rel for rel, _ in _http_files()]
for key in UNIT_ROUTES: for key in UNIT_ROUTES:
assert any(rel == key or (key.endswith("/") and rel.startswith(key)) for rel in rels), key assert any(rel == key or (key.endswith("/") and rel.startswith(key)) for rel in rels), key
assert set(ALLOWED) <= set(rels), set(ALLOWED) - set(rels)
def test_no_http_file_does_database_work_beyond_its_pin(): def test_no_http_file_does_database_work():
drift = {} hits = {rel: found for rel, src in _http_files() if (found := violations(src, _unit_of(rel)))}
for rel, src in _http_files(): assert hits == {}, (
found = violations(src, _unit_of(rel)) "a route does database work: reads go through the unit's queries, writes through its"
pinned = ALLOWED.get(rel, 0) " service; a core route reaches a unit's tables through that unit's queries too"
if len(found) != pinned:
drift[rel] = (pinned, len(found), found if len(found) > pinned else "lower its pin")
assert drift == {}, (
"an HTTP file's database work moved off its pin (pinned, now, what): move new work into"
" the unit's queries/service; when a count fell, lower ALLOWED to it"
) )
@@ -307,11 +294,11 @@ def test_the_detector_sees_each_kind_of_database_work():
# --- inside a unit: its package -------------------------------------------------------------- # --- inside a unit: its package --------------------------------------------------------------
# A unit whose sprint has landed is fenced beyond its HTTP layer too. Its own code reads another # Every unit is fenced beyond its HTTP layer too. Its own code reads another unit's tables only
# unit's tables only along a declared edge (the feed joins Video with VideoState in one query; a # along a declared edge (the feed joins Video with VideoState in one query; a boundary that split
# boundary that split it would cost the speed the app is for), and writes another unit's tables # it would cost the speed the app is for), and writes another unit's tables never — that goes
# never — that goes through the owner's service. Whose a row is, it says through `userscope`, and # through the owner's service. It commits only where a job owns its session. Whose a row is, it
# it commits only where a job owns its session. # says through `userscope`, and so does core code: the owner filter is checked in every module.
# Each unit's code outside the HTTP layer: its package, and the entry point that runs its jobs. # Each unit's code outside the HTTP layer: its package, and the entry point that runs its jobs.
UNIT_PACKAGES = { UNIT_PACKAGES = {
@@ -341,9 +328,7 @@ UNIT_PACKAGES = {
# pages' reads and writes. # pages' reads and writes.
"plex": ("plex/",), "plex": ("plex/",),
} }
# The units fenced so far; the rest join with their sprint. # Whose tables a unit's own code may READ. Core code reads a unit's tables through its queries.
FENCED = {"capture", "reader", "catalog", "plex"}
# Whose tables a unit's own code may READ. Core code reads a fenced unit's tables through its queries.
READS = { READS = {
"capture": {"catalog", "plex"}, "capture": {"catalog", "plex"},
"reader": {"catalog", "plex"}, "reader": {"catalog", "plex"},
@@ -352,7 +337,7 @@ READS = {
} }
# The columns that say whose a row is. # The columns that say whose a row is.
OWNER_COLUMNS = {"user_id", "to_user_id", "from_user_id", "created_by"} OWNER_COLUMNS = {"user_id", "to_user_id", "from_user_id", "created_by"}
# Where a fenced unit's code may commit: the jobs that own their session (the worker and what it # Where a unit's code may commit: the jobs that own their session (the worker and what it
# runs, the GC, the backfill — none of them inside a request), and the one write that must commit # runs, the GC, the backfill — none of them inside a request), and the one write that must commit
# where it stands: an upload's rows and the file it placed on disk land together or not at all, so # where it stands: an upload's rows and the file it placed on disk land together or not at all, so
# a failed commit can remove the file (`upload.ingest_upload`). Everything else flushes; the handler # a failed commit can remove the file (`upload.ingest_upload`). Everything else flushes; the handler
@@ -465,14 +450,14 @@ def _enclosing_functions(tree: ast.AST) -> dict[ast.AST, str]:
def package_violations(rel: str, source: str) -> list[str]: def package_violations(rel: str, source: str) -> list[str]:
"""What a module outside the HTTP layer does against a fenced unit's boundary, as `line: what`.""" """What a module outside the HTTP layer does against a unit's boundary, as `line: what`."""
tree = ast.parse(source) tree = ast.parse(source)
mine = _package_unit(rel) mine = _package_unit(rel)
found = [] found = []
models = _imported_models(tree) models = _imported_models(tree)
for name in models.values(): for name in models.values():
theirs = UNIT_MODEL_NAMES[name] theirs = UNIT_MODEL_NAMES[name]
if theirs == mine or (mine not in FENCED and theirs not in FENCED): if theirs == mine:
continue continue
if theirs not in READS.get(mine or "", set()): if theirs not in READS.get(mine or "", set()):
found.append(f"0: {mine or 'core'} imports {theirs}'s {name}") found.append(f"0: {mine or 'core'} imports {theirs}'s {name}")
@@ -487,10 +472,10 @@ def package_violations(rel: str, source: str) -> list[str]:
if written is None: if written is None:
continue continue
theirs = UNIT_MODEL_NAMES[written] theirs = UNIT_MODEL_NAMES[written]
if theirs != mine and (mine in FENCED or theirs in FENCED): if theirs != mine:
found.append(f"{node.lineno}: {mine or 'core'} writes {theirs}'s {written}") found.append(f"{node.lineno}: {mine or 'core'} writes {theirs}'s {written}")
if mine in FENCED:
found += _owner_filters(tree) found += _owner_filters(tree)
if mine is not None:
functions = _enclosing_functions(tree) functions = _enclosing_functions(tree)
allowed = UNIT_COMMITS.get(mine, set()) allowed = UNIT_COMMITS.get(mine, set())
for node in ast.walk(tree): for node in ast.walk(tree):
@@ -522,7 +507,7 @@ def _owner_filters(tree: ast.AST) -> list[str]:
return found return found
def test_a_fenced_units_code_keeps_to_its_edges(): def test_a_units_code_keeps_to_its_edges():
hits = {rel: found for rel, src in _app_files() if (found := package_violations(rel, src))} hits = {rel: found for rel, src in _app_files() if (found := package_violations(rel, src))}
assert hits == {}, ( assert hits == {}, (
"read another unit's tables only along READS, write them only through their service, say" "read another unit's tables only along READS, write them only through their service, say"
@@ -530,12 +515,8 @@ def test_a_fenced_units_code_keeps_to_its_edges():
) )
def test_a_fenced_units_routes_filter_owners_through_userscope(): def test_every_route_filters_owners_through_userscope():
hits = { hits = {rel: found for rel, src in _http_files() if (found := _owner_filters(ast.parse(src)))}
rel: found
for rel, src in _http_files()
if _unit_of(rel) in FENCED and (found := _owner_filters(ast.parse(src)))
}
assert hits == {} assert hits == {}
@@ -562,6 +543,7 @@ PACKAGE_CASES = {
("sync/videos.py", "from app.models import Playlist"): 1, # no catalog→reader edge ("sync/videos.py", "from app.models import Playlist"): 1, # no catalog→reader edge
("downloads/x.py", "q = DownloadJob.user_id == 1"): 1, ("downloads/x.py", "q = DownloadJob.user_id == 1"): 1,
("downloads/x.py", "q = owned_by(DownloadJob.user_id, 1)"): 0, ("downloads/x.py", "q = owned_by(DownloadJob.user_id, 1)"): 0,
("quota.py", "q = QuotaEvent.user_id == 1"): 1, # core too
("downloads/x.py", "def f(db):\n db.commit()"): 1, ("downloads/x.py", "def f(db):\n db.commit()"): 1,
("downloads/gc.py", "def f(db):\n db.commit()"): 0, ("downloads/gc.py", "def f(db):\n db.commit()"): 0,
("downloads/upload.py", "def ingest_upload(db):\n db.commit()"): 0, ("downloads/upload.py", "def ingest_upload(db):\n db.commit()"): 0,
-1
View File
@@ -113,7 +113,6 @@ project checks needs a row.
| CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) | | CVE gate | HIGH/CRITICAL with a fix available only | `--ignore-unfixed` + `--severity HIGH,CRITICAL`: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is `.trivyignore.yaml`. | 0 open (E32 S1) |
| npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 | | npm audit | `--omit=dev --audit-level=high` | What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). | 0 |
| knip | `entry` — one file added | Not a narrowing: knip's Vite plugin reads entries from index.html only, so the shared link's page (`watch.html` → `src/watchMain.tsx`) is named here, or the lane reports that whole entry unused. It adds files to the lane, never takes any out. | 1 entry | | knip | `entry` — one file added | Not a narrowing: knip's Vite plugin reads entries from index.html only, so the shared link's page (`watch.html` → `src/watchMain.tsx`) is named here, or the lane reports that whole entry unused. It adds files to the lane, never takes any out. | 1 entry |
| unit fence | the database work the HTTP layer still does, pinned | `backend/tests/test_unit_fences.py`: a unit's route file may not build SQL, call the session's data methods, set a model's attributes or import a unit's tables; a core route may not import a unit's tables. Switched on unit by unit: `ALLOWED` pins each file's count exactly, so a rise fails and a fall fails until the pin is lowered — the list may only shrink. End state: empty, and the pin mechanism goes. A unit in `FENCED` is held below its HTTP layer too: its code reads another unit's tables only along `READS`, writes them never, filters owners through `userscope`, and commits only where `UNIT_COMMITS` names a job's own session. | 826 in 23 files (E42 S0) → 667 in 19 (S1: Capture 0, fenced) → 525 in 14 (S2: Reader 0, fenced) → 354 in 9 (S3: Catalog 0, fenced; core routes 0) → 178 in 4 (S4a: Plex browse, detail, people, collections 0) → 0 (S4b: Plex 0, fenced — every unit fenced) |
| React Compiler | the functions it skips, pinned | `components/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) | | React Compiler | the functions it skips, pinned | `components/compilerCoverage.contract.test.ts` — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. | 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4) |
## Accepted findings ## Accepted findings