Files
felhom.eu/scripts/render_stands.py
T
admin 6088afcbed
gates / gates (push) Successful in 21s
Verify the standing picture against source: 12 downgrades, and the decay ran both ways
55 claims verified. Twelve moved, all downwards: walked 32 -> 20, built 5 -> 17.
Register ceiling R-284 -> R-290.

THE RULE DID NOT FIRE THE WAY IT WAS EXPECTED TO. Not one downgrade came from
code moving under an old proof. All twelve came from step 1 of the same rule --
the cited evidence does not exist. Measured: of the 28 capability-map rows
behind the page's claims, 8 carry a tests/ or audits/ path and 20 carry prose
only. The green dots were drawn from rows that cite an argument, not a walk
(R-290). The map, not the dataset, is what needs fixing -- it still says
PROVEN-LIVE for all twelve.

And once it ran backwards: fault.operator-email looked contradicted by R-182,
but live source shows the backup_run_failures digest allowlisted, operator-only
and templated, with recovery_unit_capture_failed now record-only. The claim is
right and the REGISTER ROW is stale (R-289). The session went looking for stale
proofs and found a stale defect.

R-281 WITHDRAWN -- wrong in both directions, settled by the operator's mailbox.
The tripwire DID fire (escrow_blob_served 10:19:41Z = 12:19 CEST) and false
error-severity alarms fired too, for deliberate attended work (R-285). The
measurement's cause is ESTABLISHED: the P7 query copied hub.db without hub.db-wal,
and the signature is exact -- it reported "2 events all day, newest 00:30:07",
and the rows at or before 00:30:07 number exactly 2. Timezone and wrong-key were
tested and refuted. The control had been drawn from the same stale snapshot as
the measurement, which is why it agreed (R-286).

Part 4: NO WORKFLOW CHANGED, deliberately. The gate is not ref-sensitive -- it
enumerates from the Gitea tags API, and both previous tag pushes passed. The red
is TRUE: run 267 saw v0.120.0 downloadable, run 284 on the same commit saw 404.
Who deleted the package is NOT established and is not guessed (R-287).

The page is now generated from where-felhom-stands.yaml by scripts/render_stands.py:
static, zero script tags, every moved status carrying a visible "changed, was X"
chip. The React bundle -- whose content was gzip+base64 inside a JS module map --
is kept as a dated snapshot. scripts/check_stands.py gates the data and convicted
51 problems in my own first draft before the staged positive control ever ran.
2026-08-09 18:40:49 +02:00

217 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""render_stands.py — generate where-felhom-stands.html from where-felhom-stands.yaml.
Run by hand, deliberately not wired into CI:
python3 scripts/check_stands.py && python3 scripts/render_stands.py
WHY THIS EXISTS. The first version of this page was a React bundle: one 154 KB artefact whose
content sat gzip+base64 inside a JS module map. It rendered, and that was all it could do — it
could not be edited, a diff of it showed nothing a person could read, and it could not be
regenerated when a status moved. So it began going stale the moment it was committed, which is the
one thing a picture of "where we stand" must not do.
This emits plain static HTML with inline CSS and NO JavaScript. It is bigger on disk than it needs
to be and that is the trade: the page is a build product of a file a person can read and diff.
The palette and layout deliberately match the 2026-08-09 snapshot — the operator should recognise
the page, not learn a new one.
"""
import html
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
OUT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.html")
DOT = {"walked": "#34d399", "built": "#60a5fa", "partial": "#fbbf24", "missing": "#64748b"}
LEGEND = [
("walked", "Walked", "done end to end on real hardware, evidence on file"),
("built", "Built", "shipped and tested, the real path never walked"),
("partial", "Partial", "some walked, some not — the note says which"),
("missing", "Missing", "does not exist"),
]
STAGES = {1: "Getting a box", 2: "Making it theirs", 3: "Using it", 4: "Drives",
5: "Backing up", 6: "When something goes wrong", 7: "Getting everything back"}
def load(path):
claims, cur, sect = [], None, None
meta = {}
for raw in open(path, encoding="utf-8"):
line = raw.rstrip("\n")
if line.startswith("verified_on:"):
meta["date"] = line.split(":", 1)[1].strip()
if line.startswith(" - id:"):
cur = {"id": line.split(":", 1)[1].strip(), "sources": [], "changed": None}
claims.append(cur)
sect = None
continue
if cur is None:
continue
if line.strip() == "sources:":
sect = "sources"
continue
if line.strip() == "changed:":
sect = "changed"
cur["changed"] = {}
continue
if line.strip() == "verified:":
sect = "verified"
continue
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
if sect == "sources" and m:
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
continue
m = re.match(r'\s+(\w+): (.*)$', line)
if not m:
continue
k, v = m.group(1), m.group(2).strip().strip('"')
if sect == "changed":
cur["changed"][k] = v
elif sect == "verified":
cur.setdefault("v_" + k, v)
else:
cur.setdefault(k, v)
return meta, claims
def esc(t):
return html.escape(t, quote=False)
def dot(status):
return ('<span style="width:8px;height:8px;border-radius:50%%;background:%s;'
'flex:0 0 8px;margin-top:5px"></span>' % DOT.get(status, "#64748b"))
def claim_html(c):
bits = ['<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:9px">', dot(c.get("status"))]
body = ['<span style="font-size:12px;line-height:1.45;color:#b8c4d6;text-wrap:pretty">',
esc(c.get("title", ""))]
if c.get("changed"):
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
'background:#3a2a12;color:#fbbf24;font-size:10px;white-space:nowrap">changed 2026-08-09, was %s</span>'
% esc(c["changed"].get("from", "?")))
if c.get("v_verdict") in ("needs-hardware", "contested"):
col = "#a78bfa" if c["v_verdict"] == "contested" else "#7dd3fc"
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
'background:#1b2740;color:%s;font-size:10px;white-space:nowrap">%s</span>'
% (col, esc(c["v_verdict"])))
if c.get("note"):
body.append('<span style="display:block;color:#7c8aa3;font-size:11px;margin-top:3px">%s</span>'
% esc(c["note"]))
if c.get("changed", {}) and c["changed"].get("reason"):
body.append('<span style="display:block;color:#9a7b3a;font-size:11px;margin-top:2px">why it moved: %s</span>'
% esc(c["changed"]["reason"]))
srcs = " · ".join("%s %s" % (k, esc(v if len(v) < 62 else v[:59] + "")) for k, v in c["sources"])
body.append('<span style="display:block;color:#4b5b76;font-size:10px;margin-top:3px;'
'font-family:ui-monospace,SFMono-Regular,Menlo,monospace">%s</span>' % srcs)
body.append("</span>")
bits.append("".join(body))
bits.append("</div>")
return "".join(bits)
def panel(title, inner, sub=None):
h = ['<div style="background:#121b2c;border:1px solid #1f2c44;border-radius:6px;padding:14px 13px">']
h.append('<div style="font-size:14px;font-weight:500;padding-bottom:9px;margin-bottom:10px;'
'border-bottom:1px solid #1f2c44">%s</div>' % title)
if sub:
h.append('<div style="color:#6b7a91;font-size:11px;margin:-6px 0 10px">%s</div>' % esc(sub))
h.append(inner)
h.append("</div>")
return "".join(h)
def main():
meta, claims = load(SRC)
counts = {}
for c in claims:
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
changed = [c for c in claims if c.get("changed")]
o = []
o.append("<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">")
o.append('<meta name="viewport" content="width=device-width, initial-scale=1">')
o.append("<title>Where Felhom stands</title><style>")
o.append("@page{size:A3 landscape;margin:8mm}"
"*{margin:0;padding:0;box-sizing:border-box}"
"body{background:#0b1220;color:#e6edf7;font-family:-apple-system,BlinkMacSystemFont,"
"'Segoe UI',Roboto,sans-serif;-webkit-font-smoothing:antialiased;padding:26px 20px}"
".wrap{max-width:1600px;margin:0 auto}"
".grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:12px;align-items:start}"
".grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;align-items:start}"
".grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;align-items:start}"
"h1{font-size:22px;font-weight:600;letter-spacing:-.2px}"
"a{color:#60a5fa}"
"@media print{body{padding:0}.grid7{grid-template-columns:repeat(7,1fr)}}")
o.append("</style></head><body><div class=\"wrap\">")
o.append("<h1>Where Felhom stands</h1>")
o.append('<div style="color:#8fa0b8;font-size:12.5px;margin:7px 0 4px;max-width:1100px;line-height:1.5">'
'What we built, what happens when things go wrong, and what is still missing. '
'<b style="color:#cbd7e8">Generated from '
'<code>where-felhom-stands.yaml</code></b>, which cites the capability map, register row or '
'evidence document behind every claim — and which is checked by '
'<code>scripts/check_stands.py</code>.</div>')
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:16px">Verified %s against '
'felhom-agent <code>28ba8593b8</code>, felhom-controller <code>c732fe1283</code>, hub '
'<code>56f8aa611c</code>. <b style="color:#fbbf24">%d status(es) moved in that pass</b> — '
'each is marked on the page.</div>' % (meta.get("date", "?"), len(changed)))
leg = ['<div style="display:flex;gap:22px;flex-wrap:wrap;background:#121b2c;border:1px solid #1f2c44;'
'border-radius:6px;padding:11px 14px;margin-bottom:18px">']
for k, name, desc in LEGEND:
leg.append('<div style="display:flex;gap:8px;align-items:flex-start">%s'
'<span style="font-size:11.5px"><b>%s</b> <span style="color:#7c8aa3">(%d)</span>'
'<span style="display:block;color:#6b7a91;font-size:10.5px">%s</span></span></div>'
% (dot(k), name, counts.get(k, 0), esc(desc)))
leg.append('<div style="display:flex;gap:8px;align-items:flex-start"><span style="font-size:11.5px">'
'<b style="color:#7dd3fc">needs-hardware</b><span style="display:block;color:#6b7a91;'
'font-size:10.5px">only a running box could settle it</span></span></div>')
leg.append("</div>")
o.append("".join(leg))
o.append('<div style="font-size:15px;font-weight:600;margin:0 0 9px">The journey</div>')
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">Seven stages, left to '
'right, as they happen to a person.</div>')
o.append('<div class="grid7">')
for n in range(1, 8):
inner = "".join(claim_html(c) for c in claims
if c.get("band") == "journey" and c.get("stage") == str(n))
o.append(panel('<span style="color:#4b5b76;font-family:ui-monospace,monospace">%d</span> &nbsp;%s'
% (n, esc(STAGES[n])), inner))
o.append("</div>")
fails = [c for c in claims if c.get("band") == "failures"]
o.append('<div style="font-size:15px;font-weight:600;margin:22px 0 9px">When things go wrong</div>')
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">%d situations, and what '
'actually happens in each.</div>' % len(fails))
o.append('<div class="grid3">')
for c in fails:
o.append(panel(esc(c.get("title", "").split("")[0])[:74], claim_html(c)))
o.append("</div>")
o.append('<div style="margin-top:26px;padding-top:12px;border-top:1px solid #1f2c44;color:#4b5b76;'
'font-size:10.5px;line-height:1.6">'
'Generated by <code>scripts/render_stands.py</code> from <code>where-felhom-stands.yaml</code>. '
'Do not hand-edit this file — edit the data and regenerate. '
'The 2026-08-09 React bundle is kept as '
'<code>where-felhom-stands-2026-08-09-snapshot.html</code> and is not maintained.'
'</div>')
o.append("</div></body></html>")
open(OUT, "w", encoding="utf-8").write("\n".join(o))
print("wrote %s (%d bytes) — %d claims, %d changed"
% (os.path.relpath(OUT, ROOT), os.path.getsize(OUT), len(claims), len(changed)))
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
return 0
if __name__ == "__main__":
sys.exit(main())