Files
felhom.eu/scripts/poster_facts_gate.py
T
admin 970616b72b
gates / gates (push) Failing after 11m41s
New apps 2026-10-10: Grocy and LubeLogger on the website; Monica stopped
The catalogue side is app-catalog-felhom.eu b7f0f7c. Here: the evidence, the
website, the register and the operator's view.

Website
- Two cards in the Otthon & Eletmod / Home & Lifestyle section of BOTH apps
  pages, with assets: grocy-logo.svg is grocy's own icon with its single fill
  made white like the other logos, lubelogger-logo.png is the app's own icon
  with its dark background dropped and the mark made white (the rule
  SparkyFitness's PNG follows), and six screenshots of each app's own UI with a
  household's own data, taken headless on the bench from the published template.
- The app count moved 56 -> 58 in 15 places per language set, both languages,
  and the open-source tile 49 -> 51. Checked by asking the same patterns for the
  new number afterwards. marketing/facebook/COPY.md still says 56 and is NOT
  changed: the post it carries is already scheduled.

Evidence
- documentation/audits/new-apps-2026-10-10/ — FIT.md (checklist group 0 for all
  three, with the Hungarian-UI column), the bench and box transcripts, the
  memory samples, the screenshots and the gate runs.

Register: 137 -> 139 rows, 2 opened, 0 closed.
- R-926 after a remove-keeping-backups and restore, nobody has checked what the
  app page shows for an after_install app's generated password. Measured here:
  LubeLogger is safe (its login is derived from the environment at every start,
  the new password signs in, the data is back); grocy's install password still
  signs in from the restored database but the deployed environment no longer
  carries ADMIN_PASSWORD at all. Seven apps are in the class.
- R-927 Monica, stopped at checklist 0.2 with the measurements, waiting on the
  operator.

Gate scripts: the same console trap in nineteen of them and in repo_gates.py
itself, where it ABORTED THE WHOLE RUNNER at the first gate — printing a
non-ASCII character on this workstation's cp1250 console raised
UnicodeEncodeError before the gate had decided anything, and reuse_refs_check.py
died while printing a NOTE. All now reconfigure their own streams. Red-proof
that the remaining script-test failures are not mine: test_due_checks_gate.py
fails the same 5 of 42 with the change reverted.
2026-10-10 12:34:05 +02:00

116 lines
5.6 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""poster_facts_gate.py — warn when the system poster is older than the facts it was drawn from.
Usage: python3 scripts/poster_facts_gate.py [<repo-root>]
Exit: ALWAYS 0 when it can read git. 2 only when it cannot tell (no git, file missing).
WHY THIS EXISTS, AND WHY IT ONLY WARNS.
`documentation/architecture/felhom-system-poster.html` is a drawing made in Claude Design from
`felhom-system-poster.facts.md`. **No script in this repository renders it**, which makes it unlike
`where-felhom-stands.html` (that one has `render_stands.py`). Nothing mechanical keeps the drawing
and its facts together, and `render_stands.py`'s own docstring already names the failure mode this
project has lived with: a build product "began going stale the moment it was committed".
So the facts file is the source of truth and the poster trails it. When a session changes a fact,
the rule ("The system poster stays true", in the shared rule file) says to edit the facts file in
the same commit, and either fix the poster's text or ask the operator for a new drawing. This gate
is the instrument that makes a skipped refresh VISIBLE.
**It must never fail a push**, and that is a deliberate choice rather than timidity: regenerating
the poster needs Claude Design and the operator, so a failing gate would block every unrelated push
until a human with another tool was available. A gate nobody can clear is a gate people learn to
bypass — and `--no-verify` is forbidden here, so the only remaining move would be to delete the
gate. A warning that shows up in STATUS costs nothing and keeps the fact visible.
HOW IT DECIDES. Commit time of the last commit touching each file (`git log -1 --format=%ct`), not
mtime: a checkout rewrites mtimes and would make every fresh clone shout. A file not yet committed
is treated as "no commit", and the gate says so instead of guessing.
"""
import os
import subprocess
import sys
# A gate's own console must not decide its verdict. These scripts quote back Hungarian copy, file
# paths and arrow characters; a Windows console here is cp1250, and printing one of them raised
# UnicodeEncodeError *before the gate had decided anything* — reuse_refs_check.py died while printing
# a NOTE, which the runner then reported as a failure (2026-10-10). Same trap class as
# poster_facts_gate.py's, which was found by its own red-proof.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError, OSError): # pragma: no cover - old Python, or a pipe
pass
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_ROOT = os.path.dirname(HERE)
ARCH = os.path.join("documentation", "architecture")
FACTS = os.path.join(ARCH, "felhom-system-poster.facts.md")
POSTER = os.path.join(ARCH, "felhom-system-poster.html")
def last_commit_epoch(root, rel):
"""Epoch seconds of the last commit touching `rel`, or None if it has never been committed."""
try:
out = subprocess.run(["git", "-C", root, "log", "-1", "--format=%ct", "--", rel],
capture_output=True, text=True, timeout=30)
except (OSError, subprocess.SubprocessError) as e:
raise RuntimeError("git is not usable here: %s" % e)
if out.returncode != 0:
raise RuntimeError("git log failed for %s: %s" % (rel, (out.stderr or "").strip()[:200]))
s = (out.stdout or "").strip()
return int(s) if s else None
def check(root):
"""Return (code, lines). code 0 = said something or nothing to say; 2 = could not tell."""
lines = []
for rel in (FACTS, POSTER):
if not os.path.isfile(os.path.join(root, rel)):
return 2, ["poster-facts: %s is missing - not checked" % rel]
try:
f_at = last_commit_epoch(root, FACTS)
p_at = last_commit_epoch(root, POSTER)
except RuntimeError as e:
return 2, ["poster-facts: %s - not checked" % e]
if f_at is None or p_at is None:
which = " and ".join(n for n, v in ((FACTS, f_at), (POSTER, p_at)) if v is None)
lines.append("poster-facts: not committed yet (%s) - nothing to compare" % which)
return 0, lines
if f_at > p_at:
days = (f_at - p_at) / 86400.0
lines.append(" WARNING: System poster is older than its facts - the facts file was committed "
"%.1f day(s) after the poster." % days)
lines.append(" The poster is a drawing; regenerate it in Claude Design from %s," % FACTS)
lines.append(" or, if the change was text only, edit the matching text in the poster.")
lines.append(" This is a WARNING on purpose: it never fails a push.")
else:
lines.append(" poster is current: the drawing is as new as its facts "
"(poster %+d s relative to facts)" % (p_at - f_at))
return 0, lines
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
root = argv[0] if argv else DEFAULT_ROOT
code, lines = check(root)
out = ["poster-facts gate - %s" % ("could not tell" if code == 2 else
"advisory, never fails a push")] + lines
for l in out:
try:
print(l)
except UnicodeEncodeError:
# A gate that must never fail a push must not fail on its own console either. Windows
# consoles default to cp1250 here; this was found by the red-proof, where a single
# non-ASCII character in the warning turned exit 0 into exit 1.
print(l.encode("ascii", "replace").decode("ascii"))
return code
if __name__ == "__main__":
sys.exit(main())