architecture: the system poster committed, its facts given a home, and a rule to keep them together
gates / gates (push) Failing after 5m29s

PART A -- the poster. documentation/architecture/felhom-system-poster.html
(307 KB). Secret scan first: ZERO IPv4, zero PEM blocks, zero ssh keys, zero
Bearer. The one EAA... match is base64 inside an embedded "mime":"font/woff2"
blob, not a Facebook token. "token"/"secret"/"password" appear 11 times and
every one is a NAME ("6. ep0 read token", "the hub seal key"); the poster
itself says "Names only; no secret values". All five long base64 blobs are
declared assets: 1 image/png, 3 text/javascript, 1 font/woff2.

It renders with NO network: the source mentions cdn.jsdelivr.net and Google
Fonts, but the loaded requests are only the HTML plus blob:/data: URLs -- the
bundler inlined everything. Measured, not assumed, and it matters: this is a
disaster-recovery document, so needing the internet to draw would be a defect.
No console errors.

The operator's three Claude Design fixes are all present: (a) no "WG" badge,
WireGuard only for the tunnel, no badge on the ep0-copy tile; (b) the box ->
ep0 arrow reads "encrypted on the box, sent through WireGuard"; (c) "Known
gaps" holds two items and NOT the household-keys sentence, which is now a
neutral "By design" note under the ep0 household namespace.

ONE FACT ON IT WAS WRONG. The felhom.eu tile said "served from DooPlex through
Cloudflare". It is not: Cloudflare is DNS only and the traffic goes direct --
measured this morning for the privacy notice, which states exactly that. The
poster would have contradicted a published page. Fixed in place (a label):
"served from DooPlex, Cloudflare DNS only". The first wording overflowed the
fixed-size tile, so it was shortened to fit and the evidence lives in the
facts file instead -- checked by re-rendering, not by hoping.

PART B -- the facts and the rule. DESIGN-PROMPT-...md is renamed
felhom-system-poster.facts.md (one home per fact), with the three fixes folded
in as explicit instructions so a regeneration cannot undo them, plus a new
"Badges" section saying a "WG" chip must never come back.

New rule, section 6 "The system poster stays true", added IDENTICALLY to all
five copies of unprompted-work.md (the four repos and the workspace root on
DooPlex; verified identical by diff before and after) and to
PROMPT-TEMPLATE.md's end-of-session checklist as a FIFTH coupled artifact.

scripts/poster_facts_gate.py WARNS when the facts file has a newer commit than
the poster. It never fails a push, deliberately: a refresh needs Claude Design
and the operator, --no-verify is forbidden here, so a blocking gate would leave
deleting it as the only way out. It compares COMMIT times, not mtimes, because
a checkout rewrites mtimes and every fresh clone would shout.

RED-PROOF -- and it found a real bug in the gate. The first run warned
correctly but exited 1: a single non-ASCII character in its own warning raised
UnicodeEncodeError on this cp1250 console. A gate whose entire contract is
"never fails a push" was failing pushes. Fixed (ASCII output + an encode
guard), and the decoy now asserts BOTH the warning and exit 0. Three branches
proven: facts newer -> warns, rc 0; poster newer -> quiet, rc 0; poster
missing -> "could not tell", rc 2, not a false all-clear.

The decoy itself was seen to fail, twice, on Linux (the suite needs fcntl and
cannot run on Windows): breaking the warning gives STALE_WARNS=False, and
making it exit 1 gives RC_STALE=1. All 80 felhom.eu decoys behave.

PART C -- do box reports pass through Cloudflare? NO. Two channels. DNS from
PUBLIC resolvers (not DooPlex's own, which answers the LAN address):
hub.felhom.eu is a CNAME to dooplex.hopto.org -> 37.191.56.193, not a
Cloudflare address, and no cf-ray comes back. The manifest: an ordinary k3s
Ingress, Cloudflare named only in a DNS setup comment. THE CONTROL that makes
the negative mean something: iso.felhom.eu resolves to 172.67.x / 104.21.x,
real Cloudflare addresses -- so the method does detect proxying.

So nothing is added to the Cloudflare row: the hub path does not touch it.
06-offsite-connectivity.md section 1 claimed the public edge is a
Cloudflare-Tunnel and "DooPlex has no public IP" -- both untrue today. Kept
and marked STALE with the measurement rather than rewritten, because that
paragraph is the reason ep0 exists and the argument needs its premise visible.
total-loss-of-dooplex.md's "today a CNAME to dooplex.hopto.org" is confirmed
correct.

Register: 137 before, 137 after, 0 opened, 0 closed -- every finding here was
small and fixed in the session.
This commit is contained in:
2026-10-09 18:09:50 +02:00
parent e3741ae493
commit a08bd3cbd5
10 changed files with 826 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
#!/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
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())