Upgrade harness: four fixtures and seven real upstream edges from the update night (R-462)
gates / gates (push) Successful in 1s

Test code only — no template changed and no image: line moved.

The update night walked real within-a-major upstream edges on scratch guest 9202 through the
product's own guarded Update, against a PRIVATE DRILL CATALOG; the live catalog was never
touched. This brings the expensive half of that work — the seed routes — back into the harness
so the same edges can be run here WITH their ABORT step, which the box deliberately does not
offer (09 6.1: whether the old image starts on migrated data is per-app and unpredictable).

- upgrade_fixtures.py: ActualBudget, Navidrome, AudiobookShelf, Vikunja. Each seeds through the
  app's OWN interface (R-156); each carries a negative control run on every verify(), so a
  readback that has broken into always succeeding fails instead of passing everything.
- upgrade-test.py: edges U1..U7, all real upstream moves existing 2026-09-21 that this catalog
  has NOT made, each holding its database engine constant.
- Limitations kept: Navidrome and AudiobookShelf seed the DATABASE half only, and say so.

OWED, stated so it is not mistaken for done: the U1..U7 harness RUNS, and with them the per-app
ABORT answers. The code is in; the runs are not.

Gates: catalog_gates.py --fast — image-pins, engine-major, catalog-since, copy-i18n all OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-21 21:00:50 +02:00
parent f5f6a152b5
commit 4463243f2e
4 changed files with 320 additions and 61 deletions
+30 -60
View File
@@ -1,70 +1,40 @@
# REPORT — the engine-major rule, half lifted (R-469 + R-450)
# REPORT — upgrade harness widened from the update night (2026-09-21)
**Base `18a6d2d8243e` → `5ff36d098cbc`.** No template moved. Architecture read first and named:
`felhom.eu/documentation/architecture/09-update-architecture.md` §3 decision 5 (the 2026-09-13 ruling
and its three precautions) and §6.1 (slice 4 as shipped).
**Test code only.** No template was changed; no `image:` line moved; `git diff` touches exactly
`scripts/upgrade_fixtures.py` and `scripts/upgrade-test.py`.
## Why now
## What was done
The rule of 2026-09-13 named its own expiry — *until the Update button takes a verified backup as its
precondition* — **precisely so it would be removed deliberately rather than forgotten.** That
condition was met the same day: update arc Slice 4 shipped (controller v0.237.0/v0.238.0; any backup
tier since v0.239.0). R-469 exists to make the removal a reviewed diff. This is it, and it removes
exactly half.
The update night (`felhom.eu/documentation/audits/DRILL-update-night-2026-09-21.md`) walked real
within-a-major upstream edges on scratch guest 9202 **through the product's own guarded Update**,
each app seeded and read back through its own front door, against a **private drill catalog** — the
live catalog was never touched. This commit brings the expensive half of that work (the seed routes)
back into the harness, so the same edges can also be run here **with their ABORT step**, which the
box deliberately does not offer.
## What changed
- Four new fixtures: `ActualBudget`, `Navidrome`, `AudiobookShelf`, `Vikunja`. Each seeds through the
app's OWN interface (R-156) and each carries a negative control that runs on every `verify()`.
- Seven new edges `U1`–`U7`, all real upstream moves that existed on 2026-09-21 and that this catalog
has NOT made. Every one holds its database engine constant.
- **LIFTED — MariaDB.** The four `mariadb:` sidecars (`bookstack-db`, `kimai-db`, `nextcloud-db`,
`romm-db`) may cross a major. They now have both halves: a verified backup in front of the Update,
and `MARIADB_AUTO_UPGRADE=1` (R-459) whose conversion the harness WATCHED run on the E3/E3b edges
with the seeded data read back after.
- **NOT LIFTED — PostgreSQL and MySQL.** Postgres performs no `pg_upgrade` and REFUSES to start on an
older major's datadir, across eleven templates (R-463). **A backup is a route BACK, not a
conversion** — the app would simply not come up. MySQL has nothing measured at all. The refusal
text now says this, instead of citing the shipped R-448.
- **NEW — one edge, one migration (R-450's second half, recorded 2026-09-02, enforced now).** A
MariaDB major must be the ONLY image move in its template in that commit. bookstack's `0b73e5e`
moved the application 25.02.2 → 26.05.2 **and** MariaDB 11.6 → 12.3 together: two migrations behind
one edge, and an unreadable failure when it breaks. The refusal names what it was bundled with.
**Within a major is unaffected** and may still ride with anything.
- **The gate PRINTS what it allowed**, by name and with the reason, rather than passing in silence. A
lifted rule that goes quiet is a lifted rule nobody can audit.
## What was proven, and where
## Red-proofs — two, each SEEN to fail
Box-side, through the guarded Update, with the data read back before and after — evidence per app in
`felhom.eu/documentation/audits/update-night-2026-09-21/apps/<app>/`. The harness-side runs of
U1–U7 are **owed**: the code is in, the runs are not.
| the mutation | what failed |
|---|---|
| drop the own-edge check (`others = []`) | *FACT: kimai-db 11.6 → 12.3 BUNDLED with the kimai app bump: rc=0 expected 1* — the bookstack shape passes |
| empty `LIFTED` | *GENUINE: kimai-db 11.6 → 12.3 ALONE (the R-469 lift): rc=1 expected 0* — the lift is undone |
## Gates
Both reverted; **40 decoy cases green**. The old *"a lone MariaDB major is REFUSED"* case is now the
*"…is ALLOWED"* case — that inversion is the lift itself. A third case pins the bundled PostgreSQL
shape.
`python3 scripts/catalog_gates.py --fast` — image-pins OK, engine-major OK, catalog-since OK,
copy-i18n OK. `all catalog gates OK`, exit 0. The two slow gates need a container runtime and were
not run; no template changed, which is what they inspect.
`catalog_gates.py --fast`: image-pins, engine-major, catalog-since, copy-i18n — **all OK**. The
pre-push hook ran and passed; no `--no-verify`.
## A catalog defect found by the night, filed and NOT fixed here
## Measured while here, and it belongs in this repo's record
A read-only sweep of all 66 unique pins against the public registries, from DooPlex, never from a box
(`felhom.eu/documentation/audits/UPDATE-ARC-STATE-2026-09-21.md` §3):
- **46 of 58 exact pins are behind upstream today**; 39 within a major, 7 across one. The seven were
all pinned 2026-07-18: nextcloud 34→35, paperless-ngx 2.20→3.2, claper 2.5→3.0, gokapi 1.9→2.2,
homepage 1.13→2.4, sparkyfitness 0.17→1.7 (two images).
- **6 of the 7 measurable floating pins have been repushed upstream since the catalog set them** —
`postgres:16-alpine`, `postgres:15-alpine`, `redis:7-alpine`, `mariadb:11.4`, `mariadb:12.3`,
`postgis:16-3.5-alpine`. Only `mariadb:11.6` has not. This is R-446 measured rather than theorised,
and it is the case for recording digests at push time (put to the operator as `09` §3b Q6).
- **`msdeluise/plant-it:0.10.0` is gone upstream.** The repo's own gate says INCONCLUSIVE (it refuses
to read a `denied` stderr as "dead", correctly); two independent signals say it really is gone —
Docker Hub's catalog API answers `object not found` for the whole repository, and the upstream
GitHub repo 404s. The app is already `lifecycle: abandoned`, so this is the expected end state, not
an incident. A deployed plant-it survives; it can never be redeployed.
## Rows
**R-469 PARTLY CLOSED** (MariaDB lifted; the PostgreSQL half stands until R-463).
**R-450 half SHIPPED** (the own-edge clause; the automatic-within-a-major half is Slice 6 and awaits
`09` §3b Q1–Q4). **R-605 filed** — a catalog gate that refused to run and one that ran and could not
decide print the same word.
Two apps are presented to the household as UNHEALTHY while working perfectly: `tandoor`'s
`.felhom.yml` probe names port 8080 where the container listens only on 80, and `zipline`'s names
`/api/health` where that app answers 404 — **while the compose healthcheck in the same file uses
`/api/healthcheck` and is correct**. A static sweep of all 53 templates comparing the two health
checks against each other finds both, plus `wger` and `home-assistant` as unmeasured candidates and
`adventurelog` as a false positive. Filed as **R-618** with the proposed gate; deliberately not
fixed in the same commit as the harness change.