#!/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 [] 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 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())