970616b72b
gates / gates (push) Failing after 11m41s
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.
145 lines
6.8 KiB
Python
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())
|