R-469: lift the MariaDB half of the engine-major rule; add R-450's own-edge clause
gates / gates (push) Successful in 1s

Slice 4 shipped 2026-09-13, so the rule's own expiry condition is met — for MariaDB.
PostgreSQL and MySQL stay refused (R-463: no pg_upgrade, refuses to start on an
older major's datadir across eleven templates).

A MariaDB major is now allowed ONLY as its own edge: never in the same commit as
another image move in that template (R-450, the bookstack 0b73e5e shape).

Two new decoy cases; two red-proofs, each seen to fail. 40 cases green.
No template moved.

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 12:49:18 +02:00
parent 18a6d2d824
commit 5ff36d098c
4 changed files with 130 additions and 39 deletions
+15 -5
View File
@@ -225,16 +225,21 @@ def main():
DOCMOST = "templates/docmost/docker-compose.yml"
# ── THE FACTS: these must be refused ─────────────────────────────────────────────────
out = case("FACT: kimai-db mariadb:11.6 -> 12.3 (cross-major)", clone,
[(KIMAI, swap_image("kimai-db", "mariadb:11.6", "mariadb:12.3"))],
out = case("FACT: docmost-postgres 16-alpine -> 17-alpine bundled with the app bump", clone,
[(DOCMOST, lambda t: swap_image("docmost-postgres", "postgres:16-alpine", "postgres:17-alpine")(t))],
expect_rc=1,
must_contain=("ENGINE-MAJOR GATE FAILED", "kimai-db", "mariadb 11 -> 12",
"R-448", "EXPIRY"))
must_contain=("ENGINE-MAJOR GATE FAILED", "docmost-postgres", "postgres 16 -> 17"))
if "REFUSAL_TEXT" in os.environ:
print(out)
case("FACT: docmost-postgres postgres:16-alpine -> 17-alpine", clone,
[(DOCMOST, swap_image("docmost-postgres", "postgres:16-alpine", "postgres:17-alpine"))],
expect_rc=1, must_contain=("docmost-postgres", "postgres 16 -> 17"))
expect_rc=1, must_contain=("docmost-postgres", "postgres 16 -> 17", "R-463"))
# R-469 + R-450 (2026-09-21): a MariaDB major bundled with the app's own bump is the
# bookstack 0b73e5e shape — two migrations behind one edge — and stays refused.
case("FACT: kimai-db 11.6 -> 12.3 BUNDLED with the kimai app bump", clone,
[(KIMAI, lambda t: swap_image("kimai", "kimai/kimai2:apache-2.57.0", "kimai/kimai2:apache-2.58.0")(
swap_image("kimai-db", "mariadb:11.6", "mariadb:12.3")(t)))],
expect_rc=1, must_contain=("IN THE SAME COMMIT as kimai", "OWN EDGE", "R-450"))
case("FACT: kimai-db mariadb:11.6 -> mariadb:lts (major unreadable)", clone,
[(KIMAI, swap_image("kimai-db", "mariadb:11.6", "mariadb:lts"))],
expect_rc=2, must_contain=("INCONCLUSIVE",))
@@ -243,6 +248,11 @@ def main():
case("GENUINE: kimai-db mariadb:11.6 -> 11.8 (within major)", clone,
[(KIMAI, swap_image("kimai-db", "mariadb:11.6", "mariadb:11.8"))],
expect_rc=0, must_contain=("engine-major gate OK",))
# R-469: the LIFT itself. A MariaDB major ALONE in its template is now permitted, and the
# gate says so by name rather than passing in silence.
case("GENUINE: kimai-db mariadb:11.6 -> 12.3 ALONE (the R-469 lift)", clone,
[(KIMAI, swap_image("kimai-db", "mariadb:11.6", "mariadb:12.3"))],
expect_rc=0, must_contain=("ALLOWED", "kimai-db", "mariadb 11 -> 12", "R-469"))
# ── THE DECOYS: the label moves, the fact does not — these must pass ─────────────────
def comment_and_env(text):