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
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:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user