Files
felhom.eu/scripts/check_stands.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

145 lines
6.8 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_stands.py — the gate on documentation/architecture/where-felhom-stands.yaml.
The page that dataset renders is the operator's picture of where the product stands. Its whole
value is that every claim on it is traceable, so this checks the traceability rather than the
claims: a claim nobody can trace is not a weaker claim, it is not a claim at all.
WHAT IT CONVICTS ON (each is a FAIL, exit 1):
1. an entry with NO source — §4 rule 1: that is a defect, not a claim
2. an `evidence:` path that does not resolve — a citation nobody opened
3. a `register:` id in neither OPEN-ITEMS.md nor CLOSED-ITEMS.md — a dangling register
reference. A CLOSED row is a valid citation (R-819): a stand that is walked or built is often
walked BECAUSE its row closed, and the 2026-10-03 triage moved most cited rows there. Reading
only the open register convicted every finished row the page leaned on.
4. a `capability-map:` anchor not found — the row it derives from has moved or gone
5. status `walked` with no `evidence:` source — THE LOAD-BEARING ONE. "Walked" means done end
to end on real hardware with evidence on file. If no evidence document is cited, the page is
drawing a green dot from an opinion. This is the rule the positive control exercises: take a
claim the page marks `missing`, mark it `walked`, and this fires.
WHAT IT DOES NOT DO, said plainly so the green is not over-read: it does not read the evidence and
judge whether it supports the claim, and it cannot. A human verdict lives in each entry's
`verified:` block; this gate checks that the paperwork exists, not that the paperwork is right.
Run: python3 scripts/check_stands.py [path-to-yaml]
"""
import os
import re
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
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
REGISTER = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
CLOSED = os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md")
CAPMAP = os.path.join(ROOT, "documentation", "architecture", "00-capability-map.md")
DOCS = os.path.join(ROOT, "documentation")
def load(path):
"""Minimal parser for the shape this file is written in.
Deliberately not PyYAML: the runner image and the gate hosts carry python3 and nothing else
(the same constraint that keeps `uses:` out of the CI workflow), and a gate that needs a pip
install is a gate that silently stops running.
"""
claims, cur, in_sources = [], None, False
for raw in open(path, encoding="utf-8"):
line = raw.rstrip("\n")
if line.startswith(" - id:"):
cur = {"id": line.split(":", 1)[1].strip(), "sources": []}
claims.append(cur)
in_sources = False
continue
if cur is None:
continue
if line.strip() == "sources:":
in_sources = True
continue
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
if in_sources and m:
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
continue
m = re.match(r'\s+(status|title|verdict|depth): (.*)$', line)
if m:
in_sources = False
cur[m.group(1)] = m.group(2).strip().strip('"')
return claims
def main():
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
claims = load(path)
register = open(REGISTER, encoding="utf-8").read()
closed = open(CLOSED, encoding="utf-8").read()
# Normalise the map before matching: its row labels carry ** and ` markup, so a literal
# substring probe against the raw file fails on text that is plainly there. Matching the
# rendered words is what the citation means.
capmap = open(CAPMAP, encoding="utf-8").read()
capmap = re.sub(r"[`*]", "", capmap)
capmap = re.sub(r"\s+", " ", capmap).lower()
print("check_stands — %d claim(s) in %s" % (len(claims), os.path.relpath(path, ROOT)))
fails = []
closed_cites = []
for c in claims:
cid = c["id"]
if not c["sources"]:
fails.append("%s: NO SOURCE — an entry with no source is a defect, not a claim" % cid)
for kind, ref in c["sources"]:
if kind == "evidence":
if not os.path.exists(os.path.join(DOCS, ref)):
fails.append("%s: evidence path does not resolve: documentation/%s" % (cid, ref))
elif kind == "register":
# A row's id cell is `| **R-NNN** |`; the closing ** keeps R-27 from matching R-273.
row = r"\*\*%s\*\*" % re.escape(ref)
if re.search(row, register):
pass
elif re.search(row, closed):
closed_cites.append("%s → %s" % (cid, ref))
else:
fails.append("%s: register id %s is in neither OPEN-ITEMS.md nor CLOSED-ITEMS.md"
% (cid, ref))
elif kind == "capability-map":
probe = " ".join(ref.split()[:4])
probe = re.sub(r"[`*]", "", probe)
if probe and probe.lower() not in capmap:
fails.append("%s: capability-map anchor not found: %r" % (cid, probe))
if c.get("status") == "walked" and not any(k == "evidence" for k, _ in c["sources"]):
fails.append("%s: status 'walked' but NO evidence document cited — a green dot "
"drawn from an opinion" % cid)
counts = {}
for c in claims:
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
print(" register citations resolved in CLOSED-ITEMS.md: %d" % len(closed_cites))
if fails:
print("\nCONVICTED — %d problem(s):" % len(fails))
for f in fails:
print(" " + f)
return 1
print("\ncheck_stands: OK — every claim cites a source, every citation resolves, and every "
"'walked' cites a walk.")
return 0
if __name__ == "__main__":
sys.exit(main())