Sign-up locked twice (after_setup + case-insensitive blocks); wanderer closable (decision 48); ghost/HA/gramps probes; probe-measured gate; decoy fixture fix
gates / gates (push) Successful in 2s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-29 17:00:18 +02:00
parent f7d91069fb
commit 6446197925
36 changed files with 340 additions and 52 deletions
+3
View File
@@ -97,6 +97,9 @@ GATES = [
# defect it catches does not merely mis-colour a badge, it makes a SUCCESSFUL update stop a
# working app (the `verifying` phase waits on this probe), so it must bite at push time.
("probe-matches-compose", "check-probe-matches-compose.py", True, True, False),
# R-715 (2026-09-29): a setup gate's `setup_done_probe:` must carry its measured before/after answers directly
# above it. A guessed probe that never flips keeps an app closed to all but the household AND blocks their press.
("probe-measured", "check-probe-measured.py", True, True, False),
# 2026-09-23 (`09` §3 decision 13, §6.4 part 4): THE TEST RECORD. The static half needs no
# history and no network, so it bites in CI too: a ladder must be well-formed and its newest step
# must BE the compose's images. The move half needs history (skipped out loud on CI's shallow
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check-probe-measured.py — a `setup_done_probe:` must say what it MEASURED, before and after (R-715).
WHY. The setup gate (`09` §3 decision 46) opens an app when its own status says the first admin exists. A probe
that never flips keeps the app closed to everyone but the household — and it also makes the household's "Done"
press refuse (the press asks the probe first). Measured 2026-09-29 on gramps-web: its status answered 405 after the
setup, the box read only 200s, and the press was refused until the catalog was fixed. And zipline's upstream status
route answered 403 after the setup. A probe copied from upstream docs is a guess; only a before/after measurement on
a real install is evidence.
THE RULE. The comment block directly above `setup_done_probe:` (the contiguous `#` lines) must carry:
* the word "measured",
* a date (YYYY-MM-DD), and
* both answers: "before … after …", or "<value> -> <value>".
A comment elsewhere in the file (the tagline, another block) does not count.
Run from the repo root: python3 scripts/check-probe-measured.py [--root=DIR] [app ...]
Exit 0 all probes carry a measurement · 1 a probe without one.
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATE = re.compile(r"\b20\d\d-\d\d-\d\d\b")
def comment_above(lines, i):
out = []
j = i - 1
while j >= 0 and lines[j].lstrip().startswith("#"):
out.append(lines[j].lstrip()[1:].strip())
j -= 1
return " ".join(reversed(out))
def check(path):
lines = open(path, encoding="utf-8").read().split("\n")
for i, l in enumerate(lines):
if l.startswith("setup_done_probe:"):
c = comment_above(lines, i)
low = c.lower()
has_pair = "->" in c or ("before" in low and "after" in low)
if "measured" not in low or not DATE.search(c) or not has_pair:
return "the comment above setup_done_probe: lacks %s (found: %r)" % (
", ".join(x for x, ok in (("the word 'measured'", "measured" in low), ("a date", bool(DATE.search(c))),
("the before/after answers", has_pair)) if not ok), c[:160])
return None
def main():
root = ROOT
apps = []
for a in sys.argv[1:]:
if a.startswith("--root="):
root = a.split("=", 1)[1]
else:
apps.append(a)
tdir = os.path.join(root, "templates")
names = apps or sorted(d for d in os.listdir(tdir) if os.path.isdir(os.path.join(tdir, d)))
bad, n = [], 0
for app in names:
for dp, _, files in os.walk(os.path.join(tdir, app)):
if ".felhom.yml" in files and os.path.basename(dp) == app:
p = os.path.join(dp, ".felhom.yml")
if "\nsetup_done_probe:" in open(p, encoding="utf-8").read():
n += 1
why = check(p)
if why:
bad.append("%s: %s" % (app, why))
if bad:
print("probe-measured: FAIL — %d of %d probes carry no measurement:" % (len(bad), n))
for b in bad:
print(" - " + b)
return 1
print("probe-measured: OK — %d probes, each with a measured before/after" % n)
return 0
if __name__ == "__main__":
sys.exit(main())
+2 -1
View File
@@ -1078,8 +1078,9 @@
"description": "Feladatkezelő listák és táblák (Todoist/Trello alternatíva)"
},
"wanderer": {
"app_info.add_people": "Nyisd meg a regisztrációt 15 percre, és a családtagod regisztrál.",
"app_info.first_steps[0]": "Nyisd meg a hike.DOMAIN címet a böngészőben",
"app_info.first_steps[1]": "Hozd létre a fiókodat azonnal a telepítés után. Figyelem: ebben az alkalmazásban bárki, aki megtalálja a címet, fiókot hozhat létre.",
"app_info.first_steps[1]": "Hozd létre a fiókodat azonnal a telepítés után, aztán az alkalmazás oldalán zárd le a regisztrációt. Addig bárki, aki megtalálja a címet, fiókot hozhat létre.",
"app_info.first_steps[2]": "Importálj egy GPX fájlt vagy tervezz új útvonalat",
"app_info.first_steps[3]": "Fedezd fel a térképes megjelenítést",
"app_info.tagline": "Túra tervező - útvonalak, GPX nyomok és domborzati térképek",
+51 -15
View File
@@ -54,6 +54,7 @@ COVERS = {
"the app listens; vs a real probe port/path that the app does not answer (R-618)",
"test-record": "a ladder whose newest step is not the compose's images (a move without a record), a gap, a line that is not one JSON entry, a failed verdict - vs a clean ladder (09 decision 13)",
"test-record-move": "an image move with NO entry, with the entry only in a COMMENT or in README, with a failed/backfilled entry, with a digest the registry no longer serves, memory_tight without a raised limit - vs a proven entry that matches; a ref moving in a compose COMMENT is not a move (09 decision 13)",
"probe-measured": "the measurement written in the TAGLINE or another comment block, not directly above setup_done_probe:; a date with no before/after; before/after with no date; 'read upstream' instead of 'measured' - vs a genuine measured comment (R-715)",
"copy-i18n": "Hungarian edited in a COMMENT/README/display_name (label, not copy) vs a real frozen string changed; an English block that is not English, is not matched to a Hungarian twin, or rewrites a credential (R-560). Also the DEGRADED mode CI actually runs — PyYAML shadowed out, freeze only (R-595)",
}
@@ -469,19 +470,24 @@ def main():
# 11.6 -> 11.8 through its own test record, and a literal here broke every case below
# ("fixture drifted") without a single gate changing. R-663.
KDB = cur_image(clone, KIMAI, "kimai-db")
# docmost-postgres too (2026-09-29): it moved 16 -> 18 through its own ladder, and the typed "16-alpine" here
# made every run fail "fixture drifted" before a single case ran — the same shape as R-663. Read it.
DMDB = cur_image(clone, DOCMOST, "docmost-postgres")
DM_MAJ = int(DMDB.split(":")[1].split("-")[0].split(".")[0])
DM_NEXT = DMDB.replace(":%d" % DM_MAJ, ":%d" % (DM_MAJ + 1), 1)
if not KDB.startswith("mariadb:11."):
raise SystemExit("kimai-db is %s — the cases below assume a MariaDB 11 line; fixture drifted" % KDB)
# ── THE FACTS: these must be refused ─────────────────────────────────────────────────
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))],
out = case("FACT: docmost-postgres major + 1 (read from the clone)", clone,
[(DOCMOST, lambda t: swap_image("docmost-postgres", DMDB, DM_NEXT)(t))],
expect_rc=1,
must_contain=("ENGINE-MAJOR GATE FAILED", "docmost-postgres", "postgres 16 -> 17"))
must_contain=("ENGINE-MAJOR GATE FAILED", "docmost-postgres", "postgres %d -> %d" % (DM_MAJ, DM_MAJ + 1)))
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", "R-463"))
case("FACT: docmost-postgres major + 1, alone", clone,
[(DOCMOST, swap_image("docmost-postgres", DMDB, DM_NEXT))],
expect_rc=1, must_contain=("docmost-postgres", "postgres %d -> %d" % (DM_MAJ, DM_MAJ + 1), "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,
@@ -508,22 +514,24 @@ def main():
DOCMOST_FY = "templates/docmost/.felhom.yml"
DM, DR = cur_image(clone, DOCMOST, "docmost"), cur_image(clone, DOCMOST, "docmost-redis")
DPG = cur_image(clone, DOCMOST, "docmost-postgres")
if not DPG.startswith("postgres:16"):
raise SystemExit("docmost-postgres is %s — the cases below assume 16; fixture drifted" % DPG)
def pg_entry(mark=True, box=True, to_pg="postgres:18-alpine", extra=""):
# Read, never typed (2026-09-29: docmost moved 16 -> 18 and the typed 16 stopped every run). The conversion
# case moves the CURRENT major one up.
PG_FROM = int(DPG.split(":")[1].split("-")[0].split(".")[0]); PG_TO = PG_FROM + 1
PG_TO_REF = DPG.replace(":%d" % PG_FROM, ":%d" % PG_TO, 1)
def pg_entry(mark=True, box=True, to_pg=None, extra=""):
e = {"from": {"docmost": DM, "docmost-postgres": DPG, "docmost-redis": DR},
"to": {"docmost": DM, "docmost-postgres": to_pg, "docmost-redis": DR},
"to": {"docmost": DM, "docmost-postgres": to_pg or PG_TO_REF, "docmost-redis": DR},
"digest": {"docmost": "sha256:" + "a" * 64, "docmost-postgres": "sha256:" + "b" * 64, "docmost-redis": "sha256:" + "c" * 64},
"verdict": "proven", "tested_at": "2026-09-25T20:00:00Z", "harness_version": 4,
"evidence": "x/bench.json", "box_evidence": "x/box.json" if box else None, "memory_peak_pct": 20.0,
"marks": {"files_may_change": False, "needs_person": None, "memory_tight": False}}
if mark:
e["engine_conversion"] = {"service": "docmost-postgres", "engine": "postgres", "from": 16, "to": 18}
e["engine_conversion"] = {"service": "docmost-postgres", "engine": "postgres", "from": PG_FROM, "to": PG_TO}
return lambda t: t.rstrip("\n") + "\n - " + json.dumps(e) + "\n" + extra
pg18 = swap_image("docmost-postgres", DPG, "postgres:18-alpine")
case("GENUINE: docmost-postgres 16 -> 18 ALONE with its proven two-venue MARKED entry", clone,
pg18 = swap_image("docmost-postgres", DPG, PG_TO_REF)
case("GENUINE: docmost-postgres major+1 ALONE with its proven two-venue MARKED entry", clone,
[(DOCMOST, pg18), (DOCMOST_FY, pg_entry())],
expect_rc=0, must_contain=("ALLOWED", "docmost-postgres", "postgres 16 -> 18", "decision 35"))
expect_rc=0, must_contain=("ALLOWED", "docmost-postgres", "postgres %d -> %d" % (PG_FROM, PG_TO), "decision 35"))
case("DECOY: the entry is proven on both venues but carries NO conversion mark", clone,
[(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False))],
expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP", "engine_conversion None"))
@@ -531,7 +539,7 @@ def main():
[(DOCMOST, pg18), (DOCMOST_FY, pg_entry(box=False))],
expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP", "BOTH venues"))
case("DECOY: the mark sits in a COMMENT, the entry has none", clone,
[(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False, extra='# engine_conversion: {"service": "docmost-postgres", "engine": "postgres", "from": 16, "to": 18}\n'))],
[(DOCMOST, pg18), (DOCMOST_FY, pg_entry(mark=False, extra='# engine_conversion: {"service": "docmost-postgres", "engine": "postgres", "from": PG_FROM, "to": PG_TO}\n'))],
expect_rc=1, must_contain=("NOT PROVEN FOR THIS APP",))
case("DECOY: the marked entry, but the move is BUNDLED with the app's own bump", clone,
[(DOCMOST, lambda t: swap_image("docmost", DM, DM + "-next")(pg18(t))), (DOCMOST_FY, pg_entry())],
@@ -882,6 +890,34 @@ i18n:
test_record_cases(clone)
# probe-measured (R-715): the measurement must sit DIRECTLY above setup_done_probe:, with a date and both answers.
KOMGA = "templates/komga/.felhom.yml"
def drop_note(t):
return "\n".join(l for l in t.split("\n") if not (l.startswith("# measured on 9202") and "isClaimed" in l))
def case_pm(name, edits, expect_rc, must=()):
global ran
ran += 1
try:
for relpath, fn in edits:
edit(clone, relpath, fn)
r = sh([sys.executable, os.path.join(ROOT, "scripts", "check-probe-measured.py"), "--root=" + clone, "komga"], cwd=clone)
out = r.stdout + r.stderr
if r.returncode == expect_rc and all(m in out for m in must):
print(" ok %-52s rc=%d (expected %d)" % (name, r.returncode, expect_rc))
else:
fails.append("%s: rc=%d expected %d\n%s" % (name, r.returncode, expect_rc, out[-600:]))
finally:
sh(["git", "checkout", "-q", "--", "."], cwd=clone)
case_pm("probe-measured: genuine komga note passes", [], 0, ("probe-measured: OK",))
case_pm("probe-measured: note moved into the TAGLINE", [(KOMGA, lambda t: drop_note(t).replace(
"\ntagline:", "\ntagline:", 1).replace("app_info:\n", "app_info:\n # measured on 9202 2026-09-29: isClaimed false -> true\n", 1))], 1, ("FAIL",))
case_pm("probe-measured: date but no before/after", [(KOMGA, lambda t: drop_note(t).replace(
"\nsetup_done_probe:", "\n# measured on 9202 2026-09-29\nsetup_done_probe:"))], 1, ("before/after",))
case_pm("probe-measured: before/after but no date", [(KOMGA, lambda t: drop_note(t).replace(
"\nsetup_done_probe:", "\n# measured: isClaimed false -> true\nsetup_done_probe:"))], 1, ("a date",))
case_pm("probe-measured: 'read upstream' is not a measurement", [(KOMGA, lambda t: drop_note(t).replace(
"\nsetup_done_probe:", "\n# read upstream 2026-09-29: isClaimed false -> true\nsetup_done_probe:"))], 1, ("measured",))
finally:
shutil.rmtree(clone, ignore_errors=True)