c04f933d0b
gates / gates (push) Successful in 13s
CENSUS (read-only, hub store, tester's machine not contacted): no machine that is not ours can be in the state that cost demo-felhom its history. The hub holds escrow for three hosts; both demo boxes lost their pre-fix key in the same four hours on 2026-08-04; peti-felhom and david have no host row and no escrow at all. A control ran FIRST and had to pass -- the query returned "present (572 bytes)" for a host known to have material and "absent (NULL)" for one known not to. Corrected my own instrument on the way: a date-only comparison mislabelled both losses as after the fix, so the in-force moment is now pinned from the hub's first post-fix escrow row (11:11:37Z), which independently agrees with the register. PART 1 ESTABLISHED. The prune is recorded inside R-267 -- the row about the Configuration page being slow -- because pruning artifacts is what made that page fast. Arithmetic checks (23+7=30, plus three versions that only surfaced after the first thirty moved them onto page one = 33) and the PAGINATED listing shows both generics at exactly ten. R-291's blocking condition is released: the operator was being asked to establish something already written down. And my counter-argument yesterday was wrong in exactly the way R-267 warns about -- "containers hold 19" came from an unpaginated query; paginated they hold 270 and 169. RECEIPTS: three restored (drives.enrol, backup.tier1, fail.lost-recovery-code), each citing the document that walked it; the map already read PROVEN-LIVE for all three, so this follows the map rather than raising a status in the view. NINE HONEST GREYS. fault.selfheal's best hit argues against it -- an incident recording self-heal's absence through a 1h15m outage. THE DECAY RULE FIRED FOR THE FIRST TIME. backup.restore-proof has a receipt from 28 July and is superseded anyway: demo-hp's restore-test failed 5 August and the box has since been rebuilt. A claim about a continuing behaviour cannot rest on an old observation. The capability map still reads PROVEN-LIVE and is now the thing out of step -- recorded, not silently rewritten. PART 4 specified, not implemented. The orphan card promises restorability the box rendering it cannot evaluate: the discriminator is on the hub and no wire field carries it. A conditional promise the system cannot evaluate is the same defect as an unconditional false one, so the copy stops promising, says what happens, and names a route. Ships with the next controller change so one bake covers both.
228 lines
11 KiB
Python
228 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"):
|
|
ch = c["changed"]
|
|
if ch.get("also_moved"):
|
|
# Moved twice. Say so — a second move rendered as a first one hides the fact that a
|
|
# status is unsettled, which is exactly what a reader needs to see.
|
|
label = "moved twice: %s, then 2026-08-10 back to %s" % (
|
|
esc(ch["also_moved"]), esc(c.get("status", "?")))
|
|
else:
|
|
label = "changed 2026-08-09, was %s" % esc(ch.get("from", "?"))
|
|
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">%s</span>' % label)
|
|
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"]))
|
|
decay = c.get("decay") or (c.get("changed") or {}).get("decay")
|
|
if decay:
|
|
body.append('<span style="display:block;color:#c08a3e;font-size:11px;margin-top:2px">'
|
|
'proof decay: %s</span>' % esc(decay))
|
|
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> %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())
|