New-app checklist: reviewed, a gate (onboarding), the wger pilot record, the existing apps' gap page
gates / gates (push) Successful in 2s

NEW-APP-CHECKLIST.md: the reviewer's draft reviewed - 60 rows in 10 groups, each with how/why and a since date;
7 rows added, 16 sharpened, 9 wrong claims fixed. onboarding/_TEMPLATE.md (one line per id), onboarding/wger.md
(the pilot, exempt app, 11 open rows each a register row), onboarding/EXISTING-APPS-GAPS.md (read only, from
scripts/onboarding_gaps.py). Gate onboarding (scripts/check-onboarding.py) in --fast: a template directory not
among the 53 published before 2026-10-01 needs a complete record; decoys in test_gate_decoys.py (16 cases, 5 gate
mutants seen red). CLAUDE.md, REUSE.md 5, README point to it. No template changed.

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-10-01 15:20:52 +02:00
parent 6d72c091e0
commit dc0ab8b2a8
15 changed files with 1068 additions and 96 deletions
+135
View File
@@ -55,6 +55,7 @@ COVERS = {
"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)",
"onboarding": "a NEW template with no record; a record missing an id, or carrying it only inside an HTML comment; a `done` whose path does not exist, is an EMPTY directory (the mkdir shape, R-410) or names an absent sibling-repo file; an `n/a` with an empty or two-word reason; an `open` row; `opened:` backdated before the checklist; the template a new app copies lacking a new id - vs a complete record, an id added after `opened:`, and an exempt app's record with open rows (NEW-APP-CHECKLIST.md)",
"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)",
}
@@ -485,6 +486,138 @@ def test_record_cases(clone):
case_tr_move("FACT: a re-test with no new digest reaches the move gate too", clone,
[(NF, tr_append(rt(TR_D1, TR_D1, box_evidence="x")))] + with_steps, 1, ("no new digest",), {old: TR_D1})
def onboarding_cases():
"""The onboarding gate reads FILES — the checklist, the template, the records, and evidence paths that may live
in a SIBLING repository. So each case runs in its own scratch WORKSPACE: <ws>/app-catalog-felhom.eu (a clone,
with this working tree's checklist + template copied in — the files under test are the ones being edited) and
<ws>/felhom.eu (a stand-in sibling holding one evidence file). The real tree is never touched."""
global ran
ws = tempfile.mkdtemp(prefix="catalog-onboarding-")
cat = os.path.join(ws, "app-catalog-felhom.eu")
sh(["git", "clone", "-q", "file://" + ROOT, cat], cwd=ROOT)
for rel in ("NEW-APP-CHECKLIST.md", os.path.join("onboarding", "_TEMPLATE.md")):
os.makedirs(os.path.dirname(os.path.join(cat, rel)) or cat, exist_ok=True)
shutil.copy(os.path.join(ROOT, rel), os.path.join(cat, rel))
sib = os.path.join(ws, "felhom.eu", "documentation", "audits", "onb")
os.makedirs(sib)
with io.open(os.path.join(sib, "proof.txt"), "w", encoding="utf-8") as fh:
fh.write("measured\n")
shutil.copytree(os.path.join(cat, "templates", "vaultwarden"), os.path.join(cat, "templates", "newapp"))
ev = os.path.join(cat, "onboarding", "evidence", "newapp")
os.makedirs(ev)
with io.open(os.path.join(ev, "e.txt"), "w", encoding="utf-8") as fh:
fh.write("bench output\n")
ids = [m.group(1) for m in (re.match(r"^(\d+\.\d+) \|", l) for l in
io.open(os.path.join(cat, "onboarding", "_TEMPLATE.md"), encoding="utf-8")) if m]
if len(ids) < 50:
raise SystemExit("the template carries %d ids — the fixture drifted, not the gate" % len(ids))
def record(rows=None, opened="2026-10-01", app="newapp"):
rows = rows if rows is not None else ["%s | done | app-catalog-felhom.eu/onboarding/evidence/newapp/e.txt" % i
for i in ids]
return "# Onboarding record\n\napp: %s\nopened: %s\n\n%s\n" % (app, opened, "\n".join(rows))
def full(**over):
out = []
for i in ids:
out.append(over.get(i, "%s | done | app-catalog-felhom.eu/onboarding/evidence/newapp/e.txt" % i))
return [r for r in out if r is not None]
REC = os.path.join(cat, "onboarding", "newapp.md")
def case_onb(name, setup, expect_rc, must=()):
global ran
ran += 1
try:
setup()
r = sh([sys.executable, os.path.join(ROOT, "scripts", "check-onboarding.py"), "--root=" + cat,
"--today=2026-10-02"], cwd=cat)
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; missing %s\n%s" % (
name, r.returncode, expect_rc, [m for m in must if m not in out], out[-900:]))
finally:
for f in (REC, os.path.join(cat, "onboarding", "wger.md")):
if os.path.exists(f):
os.remove(f)
shutil.copy(os.path.join(ROOT, "NEW-APP-CHECKLIST.md"), os.path.join(cat, "NEW-APP-CHECKLIST.md"))
shutil.copy(os.path.join(ROOT, "onboarding", "_TEMPLATE.md"), os.path.join(cat, "onboarding", "_TEMPLATE.md"))
empty = os.path.join(cat, "onboarding", "evidence", "hollow")
if os.path.isdir(empty):
shutil.rmtree(empty)
def put(text, path=REC):
def f():
with io.open(path, "w", encoding="utf-8") as fh:
fh.write(text)
return f
try:
print("\n-- onboarding: the facts (each MUST be refused)")
case_onb("FACT: a new template with NO record", lambda: None, 1, ("newapp: NEW app with no onboarding record",))
case_onb("FACT: a record missing id 1.4", put(record(full(**{"1.4": None}))), 1, ("missing id(s): 1.4",))
case_onb("FACT: 1.4 answered only inside an HTML comment",
put(record(full(**{"1.4": "<!--\n1.4 | done | app-catalog-felhom.eu/onboarding/evidence/newapp/e.txt\n-->"}))),
1, ("missing id(s): 1.4",))
case_onb("FACT: done with a path that does not exist",
put(record(full(**{"2.5": "2.5 | done | app-catalog-felhom.eu/onboarding/evidence/newapp/restore.txt"}))),
1, ("id 2.5 is done but its evidence", "restore.txt"))
def hollow():
os.makedirs(os.path.join(cat, "onboarding", "evidence", "hollow"))
put(record(full(**{"5.1": "5.1 | done | app-catalog-felhom.eu/onboarding/evidence/hollow"})))()
case_onb("FACT: done with an EMPTY directory (the mkdir shape)", hollow, 1, ("id 5.1 is done but its evidence",))
case_onb("FACT: done naming an absent file in the sibling repo",
put(record(full(**{"3.6": "3.6 | done | felhom.eu/documentation/audits/onb/lockout.txt"}))),
1, ("id 3.6 is done but its evidence",))
case_onb("FACT: n/a with an EMPTY reason", put(record(full(**{"7.1": "7.1 | n/a | "}))), 1, ("id 7.1 is n/a",))
case_onb("FACT: n/a with a two-word reason", put(record(full(**{"7.1": "7.1 | n/a | not needed"}))), 1, ("id 7.1 is n/a",))
case_onb("FACT: an OPEN row", put(record(full(**{"6.3": "6.3 | open | the forced-fail case is not run yet"}))),
1, ("id 6.3 is OPEN",))
case_onb("FACT: opened: backdated before the checklist", put(record(full(), opened="2026-09-01")),
1, ("before the checklist existed",))
def new_id_template_lacks():
t = io.open(os.path.join(cat, "NEW-APP-CHECKLIST.md"), encoding="utf-8").read()
t = t.replace("\n## 7. Mail", "\n| 6.9 | 2026-10-01 | a new check | how | why |\n\n## 7. Mail", 1)
io.open(os.path.join(cat, "NEW-APP-CHECKLIST.md"), "w", encoding="utf-8").write(t)
put(record(full() + ["6.9 | done | app-catalog-felhom.eu/onboarding/evidence/newapp/e.txt"]))()
case_onb("FACT: a checklist id the template a new app copies lacks", new_id_template_lacks, 1,
("_TEMPLATE.md lacks checklist id(s): 6.9",))
case_onb("FACT: an exempt app's record with a done that points nowhere",
lambda: (put(record(full()))(), put(record(["1.5 | done | app-catalog-felhom.eu/nowhere.txt"],
app="wger"), os.path.join(cat, "onboarding", "wger.md"))()),
1, ("wger: id 1.5 is done but its evidence",))
print("-- onboarding: the genuine articles (each MUST pass)")
case_onb("GENUINE: a complete record (catalog + sibling evidence)",
put(record(full(**{"3.6": "3.6 | done | felhom.eu/documentation/audits/onb/proof.txt — measured on 9202",
"7.1": "7.1 | n/a | the app sends no mail at all"}))), 0, ("onboarding gate OK",))
def later_id():
t = io.open(os.path.join(cat, "NEW-APP-CHECKLIST.md"), encoding="utf-8").read()
t = t.replace("\n## 7. Mail", "\n| 6.9 | 2026-11-01 | a later check | how | why |\n\n## 7. Mail", 1)
io.open(os.path.join(cat, "NEW-APP-CHECKLIST.md"), "w", encoding="utf-8").write(t)
tp = os.path.join(cat, "onboarding", "_TEMPLATE.md")
io.open(tp, "a", encoding="utf-8").write("6.9 | open | not started: a later check\n")
put(record(full()))()
case_onb("GENUINE: an id added AFTER opened: does not bind", later_id, 0, ("onboarding gate OK",))
case_onb("GENUINE: an exempt app's record may say open",
lambda: (put(record(full()))(), put(record(["1.5 | open | the dev server runs (R-755)"], app="wger"),
os.path.join(cat, "onboarding", "wger.md"))()),
0, ("exempt app(s) with a record (shape-checked): wger",))
def no_sibling():
shutil.move(os.path.join(ws, "felhom.eu"), os.path.join(ws, "felhom.eu.away"))
put(record(full(**{"3.6": "3.6 | done | felhom.eu/documentation/audits/onb/lockout.txt"})))()
try:
case_onb("STATED SKIP: sibling repo absent (the CI shape) - printed, not checked", no_sibling, 0,
("NOT CHECKED here", "felhom.eu/documentation/audits/onb/lockout.txt"))
finally:
if os.path.isdir(os.path.join(ws, "felhom.eu.away")):
shutil.move(os.path.join(ws, "felhom.eu.away"), os.path.join(ws, "felhom.eu"))
finally:
shutil.rmtree(ws, ignore_errors=True)
def main():
gate = os.path.join(ROOT, "scripts", "check-engine-major.py")
if not os.path.isfile(gate):
@@ -961,6 +1094,8 @@ i18n:
finally:
shutil.rmtree(clone, ignore_errors=True)
onboarding_cases()
if fails:
print()
for f in fails: