Status view, part 3 (the gate): scripts/status_view_gate.py in repo_gates.py (fast: pre-push + CI) — size outside decide, one Updated paragraph, English outside quotes, cited rows open, no decided row in decide, the P1 and every operator-owned row on the page; WARN-FIRST until 2026-10-27, then it refuses; 39 decoy cases (red-proved per check and on both sides of the date); filed R-1174 (P4, Process & tooling) + its due check 2026-10-27; R-919 owner cell -> operator (the upload and the look); register 152 -> 153
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -1,3 +1,16 @@
|
||||
## 2026-10-20 — the status page gate, warn-first until 2026-10-27 (`status_view_gate.py`, R-1174)
|
||||
|
||||
New gate `scripts/status_view_gate.py`, registered in `repo_gates.py` (FAST set: pre-push hook and CI). It checks root
|
||||
`STATUS.md` against `documentation/backlog/OPEN-ITEMS.md`: at most 50 lines and 6,000 characters outside the decide
|
||||
table; exactly one „Updated" paragraph of at most 3 sentences, with no „Earlier" chain; no Hungarian letter outside a
|
||||
quote; every cited R-n an open row; no decided, closed or done row in the decide table; the P1 row under „What is
|
||||
broken" or „decide"; every open row whose Owner names the operator under „decide" or „do". **Warn-first:** before
|
||||
2026-10-27 (`SWITCH_DATE`) a conviction prints a WARNING and exits 0; from that date it exits 1 (a dated check in the
|
||||
register's DUE-CHECKS block makes the switch visible). No sections, or no register, is INCONCLUSIVE (exit 2) at once.
|
||||
A clean run prints what it looked at. Decoys: `scripts/test_status_view_gate.py` (39 cases on temp files, run by
|
||||
`test_gate_decoys.py` and `script_tests_gate.py`); red-proved per check (each check removed → its decoys FAIL) and on
|
||||
both sides of the date (switch moved later → 14 refusal cases FAIL; one day earlier → 14 warning cases FAIL).
|
||||
|
||||
## 2026-10-19 — a dependency and secret scanner on every push, warn-first (`security_scan_gate.py`, R-986)
|
||||
|
||||
New shared gate `scripts/security_scan_gate.py`, registered in all four runners (felhom.eu `repo_gates.py` gate 18,
|
||||
|
||||
@@ -36,6 +36,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
||||
18. security-scan govulncheck (called vulnerabilities) + gitleaks on the pushed commits — WARN-FIRST: it
|
||||
lists, it never refuses; it touches the network (vuln.go.dev) and says NOT CHECKED
|
||||
offline, which is why it may sit in the fast set (R-986; shared, felhom.eu/scripts)
|
||||
19. status-view root STATUS.md is one screen and a view of OPEN-ITEMS.md — WARN-FIRST until 2026-10-27,
|
||||
then it refuses (R-1174)
|
||||
|
||||
**THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already —
|
||||
it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so
|
||||
@@ -210,6 +212,11 @@ GATES = [
|
||||
# never a push. ~10 s here (govulncheck on hub/ + gitleaks on the unpushed commits). CI fetches the pinned
|
||||
# gitleaks (FELHOM_SCAN_FETCH=1 in gates.yml) and has no Go, so govulncheck says NOT CHECKED there, out loud.
|
||||
("security-scan", os.path.join(SCRIPTS, "security_scan_gate.py"), [ROOT], True, False),
|
||||
# R-1174 (2026-10-20) — root STATUS.md is one screen and a VIEW of OPEN-ITEMS.md: size outside the decide table,
|
||||
# one „Updated" paragraph, English, every cited row open, no decided row in the decide table, the P1 and every
|
||||
# operator-owned row on the page. WARN-FIRST until 2026-10-27 (exit 0 with a WARNING), a refusal from that date.
|
||||
# Fast: two file reads.
|
||||
("status-view", os.path.join(SCRIPTS, "status_view_gate.py"), [], True, False),
|
||||
]
|
||||
|
||||
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Status-view gate — root STATUS.md stays one screen, and stays a VIEW of OPEN-ITEMS.md.
|
||||
|
||||
Run from the repo root: python3 scripts/status_view_gate.py [STATUS.md] [OPEN-ITEMS.md]
|
||||
Exit 0 clean (or convicted before SWITCH_DATE: a loud WARNING) · 1 convicted · 2 inconclusive.
|
||||
|
||||
WHY THIS EXISTS. The operator's instruction for STATUS.md is: a plain-language view of
|
||||
OPEN-ITEMS.md, never a second source of truth, one screen except the section that asks them to
|
||||
decide. Nothing checked it, so every session prepended its news and nobody removed anything: on
|
||||
2026-10-20 the page was 107 lines and about 31,700 characters, the „Updated" paragraph held three
|
||||
weeks of news, eleven decided rows sat in the decide table, and the do list held finished items,
|
||||
statements and Hungarian text. That page was rebuilt the same day; this gate keeps it rebuilt.
|
||||
|
||||
THE CHECKS (each has a decoy in scripts/test_status_view_gate.py that has been seen to fail):
|
||||
size everything outside „Waiting on you — decide" is at most MAX_LINES lines and
|
||||
MAX_CHARS characters (blank lines count: they take screen space too).
|
||||
updated exactly one „**Updated" paragraph, at most MAX_UPDATED_SENTENCES sentences after its
|
||||
bold lead, and no „Earlier" chain inside it (older news goes to STATUS-HISTORY.md).
|
||||
english no Hungarian letter outside a quote („…", "…", “…”, `…`): a UI text or a page name
|
||||
may be quoted, the page itself is English (STE).
|
||||
open-ids every R-n the page cites is an OPEN row of OPEN-ITEMS.md.
|
||||
decide no row of the decide table is decided, closed or done.
|
||||
p1 every open P1 row appears under „What is broken" or „decide".
|
||||
operator every open row whose Owner cell names the operator appears under „decide" or „do"
|
||||
(a compact „Later:" line of ids counts).
|
||||
|
||||
WARN-FIRST until SWITCH_DATE (2026-10-27, register row R-1174, with a dated check in the
|
||||
DUE-CHECKS block): before that date a conviction prints a WARNING and exits 0; from that date it
|
||||
exits 1. INCONCLUSIVE (exit 2) has no warn period — a page with no sections or an unreadable
|
||||
register is never a pass (the due-checks rule: a gate that finds nothing to check is not coverage).
|
||||
|
||||
A clean run says what it looked at (lines, characters, rows checked) — an absent complaint is not
|
||||
evidence (workspace standing rule 3).
|
||||
|
||||
Stdlib only, no network, no subprocess: --fast, so it runs in the pre-push hook AND in CI.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except (AttributeError, ValueError, OSError): # pragma: no cover
|
||||
pass
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DEFAULT_STATUS = os.path.join(ROOT, "STATUS.md")
|
||||
DEFAULT_REGISTER = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
|
||||
|
||||
# The one date the warning becomes a refusal. One constant, named in R-1174 and in the DUE-CHECKS block.
|
||||
SWITCH_DATE = date(2026, 10, 27)
|
||||
|
||||
MAX_LINES = 50
|
||||
MAX_CHARS = 6000
|
||||
MAX_UPDATED_SENTENCES = 3
|
||||
|
||||
# Section headings, matched on the start of a `## ` line, case-insensitive.
|
||||
SECTIONS = {
|
||||
"works": "what works",
|
||||
"broken": "what is broken",
|
||||
"work_on": "what we work on",
|
||||
"decide": "waiting on you — decide",
|
||||
"do": "waiting on you — do",
|
||||
}
|
||||
|
||||
HU_LETTERS = re.compile(u"[áéíóöőúüűÁÉÍÓÖŐÚÜŰ]")
|
||||
# A quote is a span the page may carry in Hungarian: a UI text, a page name, a code span.
|
||||
QUOTE_SPANS = [
|
||||
re.compile(u"„[^\n„]*?[\"”]"),
|
||||
re.compile(u"“[^\n“]*?”"),
|
||||
re.compile(u"\"[^\"\n]*\""),
|
||||
re.compile(u"`[^`\n]*`"),
|
||||
]
|
||||
RID = re.compile(r"\bR-(\d+)\b")
|
||||
REG_ROW = re.compile(r"^\|\s*\*\*(R-\d+)\*\*\s*\|")
|
||||
OPERATOR = re.compile(r"\b(operator|viktor)\b", re.I)
|
||||
FINISHED = re.compile(r"\b(DECIDED|CLOSED)\b")
|
||||
FINISHED_CELL = ("done", "decided", "closed")
|
||||
SENTENCE_END = re.compile(u"[.!?](?=\\s+[A-Z„\"“(`*]|\\s*$)")
|
||||
|
||||
|
||||
def inconclusive(msg, *more):
|
||||
print("STATUS-VIEW GATE INCONCLUSIVE: " + msg)
|
||||
for m in more:
|
||||
print(" " + m)
|
||||
print(" INCONCLUSIVE is not a pass: a gate that finds nothing to check is not coverage.")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def today_utc():
|
||||
override = os.environ.get("FELHOM_GATE_TODAY")
|
||||
if override:
|
||||
try:
|
||||
d = datetime.strptime(override.strip(), "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
inconclusive("FELHOM_GATE_TODAY=%r is not YYYY-MM-DD" % override)
|
||||
print("!! FELHOM_GATE_TODAY=%s IS SET — this gate is evaluating against that date, NOT "
|
||||
"today's real UTC date. Unset it for a real run." % override.strip())
|
||||
return d
|
||||
return datetime.now(timezone.utc).date()
|
||||
|
||||
|
||||
def read(path, what):
|
||||
if not os.path.isfile(path):
|
||||
inconclusive("%s not found at %s" % (what, path))
|
||||
with open(path, encoding="utf-8-sig") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def split_cells(line):
|
||||
return [c.strip() for c in re.split(r"(?<!\\)\|", line.strip())[1:-1]]
|
||||
|
||||
|
||||
def open_rows(text):
|
||||
"""{id: (sev, owner)} for every register row. A literal `|` sits inside backticks (register rule)."""
|
||||
rows = {}
|
||||
for line in text.split("\n"):
|
||||
m = REG_ROW.match(line)
|
||||
if not m:
|
||||
continue
|
||||
# Split outside backticks, as register_shape_gate.py does.
|
||||
cells, cur, tick = [], "", False
|
||||
for ch in line.strip():
|
||||
if ch == "`":
|
||||
tick = not tick
|
||||
if ch == "|" and not tick:
|
||||
cells.append(cur)
|
||||
cur = ""
|
||||
else:
|
||||
cur += ch
|
||||
cells = [c.strip() for c in cells[1:]]
|
||||
if len(cells) < 8:
|
||||
continue
|
||||
rows[m.group(1)] = (cells[2], cells[7])
|
||||
return rows
|
||||
|
||||
|
||||
def sections(lines):
|
||||
"""{key: (start, end)} line ranges (heading line included), plus the list of all `## ` lines."""
|
||||
heads = [i for i, l in enumerate(lines) if l.startswith("## ")]
|
||||
found = {}
|
||||
for n, i in enumerate(heads):
|
||||
end = heads[n + 1] if n + 1 < len(heads) else len(lines)
|
||||
title = lines[i][3:].strip().lower()
|
||||
for key, want in SECTIONS.items():
|
||||
if title.startswith(want) and key not in found:
|
||||
found[key] = (i, end)
|
||||
return heads, found
|
||||
|
||||
|
||||
def unquote(s):
|
||||
for rx in QUOTE_SPANS:
|
||||
s = rx.sub(" ", s)
|
||||
return s
|
||||
|
||||
|
||||
def main(argv):
|
||||
status_path = argv[1] if len(argv) > 1 else DEFAULT_STATUS
|
||||
reg_path = argv[2] if len(argv) > 2 else DEFAULT_REGISTER
|
||||
today = today_utc()
|
||||
page = read(status_path, "STATUS page")
|
||||
reg = open_rows(read(reg_path, "register"))
|
||||
if not reg:
|
||||
inconclusive("no register rows (`| **R-n** |`) in %s" % reg_path)
|
||||
|
||||
lines = page.rstrip("\n").split("\n")
|
||||
heads, sec = sections(lines)
|
||||
if not heads:
|
||||
inconclusive("no `## ` sections in %s" % status_path)
|
||||
missing = [SECTIONS[k] for k in SECTIONS if k not in sec]
|
||||
if missing:
|
||||
inconclusive("%s lacks the section(s): %s" % (status_path, ", ".join(missing)),
|
||||
"The checks need every section; the page shape is in the repo CLAUDE.md.")
|
||||
|
||||
def text_of(*keys):
|
||||
out = []
|
||||
for k in keys:
|
||||
a, b = sec[k]
|
||||
out.extend(lines[a:b])
|
||||
return "\n".join(out)
|
||||
|
||||
convictions = []
|
||||
d0, d1 = sec["decide"]
|
||||
outside = lines[:d0] + lines[d1:]
|
||||
while outside and not outside[-1].strip():
|
||||
outside.pop()
|
||||
n_lines = len(outside)
|
||||
n_chars = len("\n".join(outside))
|
||||
|
||||
# size
|
||||
if n_lines > MAX_LINES:
|
||||
convictions.append(("size", "%d lines outside „decide” (limit %d)" % (n_lines, MAX_LINES)))
|
||||
if n_chars > MAX_CHARS:
|
||||
convictions.append(("size", "%d characters outside „decide” (limit %d)" % (n_chars, MAX_CHARS)))
|
||||
|
||||
# updated
|
||||
upd = [i for i, l in enumerate(lines) if "**Updated" in l]
|
||||
if len(upd) != 1:
|
||||
convictions.append(("updated", "%d „**Updated” paragraphs (exactly one is allowed)" % len(upd)))
|
||||
else:
|
||||
a = b = upd[0]
|
||||
while a > 0 and lines[a - 1].strip():
|
||||
a -= 1
|
||||
while b + 1 < len(lines) and lines[b + 1].strip():
|
||||
b += 1
|
||||
para = " ".join(l.strip() for l in lines[a:b + 1])
|
||||
body = re.sub(r"^.*?\*\*Updated[^*]*\*\*", "", para, count=1).strip()
|
||||
n_sent = len(SENTENCE_END.findall(body))
|
||||
if body and not SENTENCE_END.search(body[-1:] + " "):
|
||||
n_sent += 1 # a last sentence without its full stop still counts
|
||||
if n_sent > MAX_UPDATED_SENTENCES:
|
||||
convictions.append(("updated", "the „Updated” paragraph has %d sentences (limit %d)"
|
||||
% (n_sent, MAX_UPDATED_SENTENCES)))
|
||||
if re.search(r"\bEarlier\b", para):
|
||||
convictions.append(("updated", "the „Updated” paragraph carries an „Earlier” chain "
|
||||
"(older news goes to STATUS-HISTORY.md)"))
|
||||
|
||||
# english
|
||||
for no, l in enumerate(lines, 1):
|
||||
bare = unquote(l)
|
||||
m = HU_LETTERS.search(bare)
|
||||
if m:
|
||||
convictions.append(("english", "line %d: Hungarian outside a quote near %r"
|
||||
% (no, bare[max(0, m.start() - 20):m.start() + 20].strip())))
|
||||
|
||||
# open-ids
|
||||
cited = sorted(set("R-" + x for x in RID.findall(page)), key=lambda r: int(r[2:]))
|
||||
for r in cited:
|
||||
if r not in reg:
|
||||
convictions.append(("open-ids", "%s is cited but is not an OPEN row" % r))
|
||||
|
||||
# decide
|
||||
a, b = sec["decide"]
|
||||
n_decide = 0
|
||||
for no in range(a + 1, b):
|
||||
l = lines[no].strip()
|
||||
if not l.startswith("|") or re.match(r"^\|[\s:|-]+\|$", l) or re.match(r"^\|\s*#\s*\|", l):
|
||||
continue
|
||||
n_decide += 1
|
||||
cells = [c.lower().strip("* ") for c in split_cells(l)]
|
||||
if FINISHED.search(l) or any(c in FINISHED_CELL for c in cells):
|
||||
convictions.append(("decide", "line %d: a decided/closed/done row in the decide table" % (no + 1)))
|
||||
|
||||
# p1
|
||||
p1_text = text_of("broken", "decide")
|
||||
p1_rows = sorted(r for r, (sev, _) in reg.items() if sev == "P1")
|
||||
for r in p1_rows:
|
||||
if not re.search(r"\b%s\b" % re.escape(r), p1_text):
|
||||
convictions.append(("p1", "%s (P1) is not under „What is broken” or „decide”" % r))
|
||||
|
||||
# operator
|
||||
op_text = text_of("decide", "do")
|
||||
op_rows = sorted((r for r, (_, owner) in reg.items() if OPERATOR.search(owner)), key=lambda r: int(r[2:]))
|
||||
for r in op_rows:
|
||||
if not re.search(r"\b%s\b" % re.escape(r), op_text):
|
||||
convictions.append(("operator", "%s names the operator as owner and is not under „decide” or „do”" % r))
|
||||
|
||||
print("status-view: %s" % status_path)
|
||||
print("status-view: %d lines / %d characters outside „decide” (limits %d / %d); decide table %d row(s)"
|
||||
% (n_lines, n_chars, MAX_LINES, MAX_CHARS, n_decide))
|
||||
print("status-view: %d R-id(s) cited, checked against %d open register rows; %d P1 row(s); "
|
||||
"%d operator-owned row(s)" % (len(cited), len(reg), len(p1_rows), len(op_rows)))
|
||||
if not convictions:
|
||||
print("status-view: OK — one screen, one „Updated” paragraph, English, every cited row open, "
|
||||
"no finished decision, the P1 and every operator-owned row on the page")
|
||||
return 0
|
||||
refuse = today >= SWITCH_DATE
|
||||
word = "CONVICTED" if refuse else "WARNING"
|
||||
for check, msg in convictions:
|
||||
print("status-view %s [%s]: %s" % (word, check, msg))
|
||||
if refuse:
|
||||
print("status-view: %d conviction(s) — refused (the warn-first period ended on %s, R-1174)"
|
||||
% (len(convictions), SWITCH_DATE.isoformat()))
|
||||
return 1
|
||||
print("!! status-view: %d conviction(s) — WARNING ONLY until %s; from that date this gate REFUSES "
|
||||
"the push (R-1174). Fix STATUS.md now." % (len(convictions), SWITCH_DATE.isoformat()))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -116,6 +116,12 @@ COVERS = {
|
||||
"accepted line with no reason warns; a CALLED govulncheck finding is listed, an imported-only "
|
||||
"one is not; no tool anywhere is NOT CHECKED out loud, never a silent pass; the clean commit "
|
||||
"and the accepted line pass. Warn-first: every case exits 0, the LISTING is the fact"),
|
||||
"status-view": ("R-1174: scripts/test_status_view_gate.py, run from here (temp files, the real tree untouched): one "
|
||||
"decoy per check, each the shape a real session writes — 50 more lines, news prepended with the "
|
||||
"old paragraph left behind, an Earlier chain, a Hungarian do-line, a closed row cited, a DECIDED "
|
||||
"row left in the decide table, the P1 line deleted to meet the size, the Later line deleted — "
|
||||
"each convicting ON ITS OWN check, warning before 2026-10-27 and refusing from it; the genuine "
|
||||
"fixture and the REAL page must pass; no sections / no register are INCONCLUSIVE (exit 2)"),
|
||||
"hub-gofmt": ("R-1160: an unformatted .go file in a SCRATCH module dir is convicted by name; its "
|
||||
"formatted twin passes; with no gofmt reachable, CI passes OUT LOUD and a dev machine is "
|
||||
"INCONCLUSIVE, never a silent pass"),
|
||||
@@ -805,6 +811,19 @@ else:
|
||||
_n = _ss.stdout.strip().splitlines()[-1] if _ss.stdout.strip() else "?"
|
||||
print(" ok %-20s %s" % ("security-scan", _n))
|
||||
|
||||
# ── status-view (R-1174) ─────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Its decoys are temp STATUS/register pairs, so they live in their own file and are RUN from here (security-scan's shape).
|
||||
ran += 1
|
||||
_sv = subprocess.run([sys.executable, os.path.join("scripts", "test_status_view_gate.py")],
|
||||
cwd=ROOT, capture_output=True, text=True)
|
||||
if _sv.returncode != 0:
|
||||
fails.append("status-view: its decoy suite FAILED — a decoy did not convict\n%s"
|
||||
% (_sv.stdout + _sv.stderr)[-800:])
|
||||
else:
|
||||
_n = _sv.stdout.strip().splitlines()[-1] if _sv.stdout.strip() else "?"
|
||||
print(" ok %-20s %s" % ("status-view", _n))
|
||||
|
||||
# ── script-tests (R-885) ─────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Its decoys are whole fake repos, so they live in their own file and are RUN from here (guide-quote's shape).
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Decoy tests for status_view_gate.py (R-1174).
|
||||
|
||||
Run: python3 scripts/test_status_view_gate.py Exit 0 every case behaved · 1 a case did not.
|
||||
|
||||
A DECOY IS THE LABEL WITHOUT THE FACT (R-421). Each case below takes a GENUINE page — the fixture,
|
||||
shaped like the real STATUS.md — and breaks ONE check the way a real session would: the „Updated"
|
||||
paragraph with an „Earlier" chain behind it, a decided row left in the decide table, a Hungarian
|
||||
do-line, a page that meets the size by deleting the P1 or the „Later" line. Each must convict, and
|
||||
must convict ON THAT CHECK (its `[check]` tag in the output): a decoy caught by some other check
|
||||
proves nothing about the check it was built for.
|
||||
|
||||
Both directions: the genuine fixture and the REAL STATUS.md against the REAL register must pass.
|
||||
The date switch is tested on both sides (FELHOM_GATE_TODAY). Fixtures are temp files; nothing here
|
||||
touches the real tree.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
GATE = os.path.join(HERE, "status_view_gate.py")
|
||||
|
||||
BEFORE = "2026-10-26" # the last warn-only day
|
||||
AFTER = "2026-10-27" # SWITCH_DATE: convictions refuse
|
||||
|
||||
REGISTER = u"""# OPEN-ITEMS — fixture
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **R-10** | Security & access | P1 | **Reused logins.** | **NARROWED** | — | rotate | operator |
|
||||
| **R-11** | Backup & restore | P3 | **Immich off-site size.** | **READY** | D21 | decide | CC |
|
||||
| **R-12** | Process & tooling | P4 | **A failed disk.** | **OPEN** | — | pull it | operator |
|
||||
| **R-13** | Business & legal | P4 | **A lawyer.** | WAITING-ON-OPERATOR | — | send | Viktor |
|
||||
| **R-14** | Install & onboarding | P3 | **The minimum machine.** | WAITING-ON-OPERATOR | — | approve | operator |
|
||||
| **R-15** | Box system & updates | P3 | **The scratch box.** | OPEN | — | fix | CC |
|
||||
"""
|
||||
|
||||
PAGE = u"""# STATUS — what works, what's broken, what's next
|
||||
|
||||
**Ready for the first real tester? Yes.** The laptop is off.
|
||||
|
||||
**Updated 2026-10-20 (day).** Hub 0.162.0 is live (your yes). This page is one screen again. A rehearsal runs tonight.
|
||||
|
||||
## What works
|
||||
- A box installs from nothing (map A1).
|
||||
|
||||
## What is broken
|
||||
- **P1:** reused logins (R-10, yours).
|
||||
- The scratch box (R-15).
|
||||
|
||||
## What we work on
|
||||
- The rehearsal. CC.
|
||||
|
||||
## Waiting on you — decide
|
||||
|
||||
| # | Row | Question | Pick (why) | The other option costs | If you do nothing | Blocked |
|
||||
|---|---|---|---|---|---|---|
|
||||
| D21 | R-11 | Leave out the AI model files now? | **B now** | A: a new restore path | 12 times the data | — |
|
||||
| D5 | R-14 | Approve the minimum machine? | **Approve** | Change it: one edit | no hardware page | the hardware page |
|
||||
|
||||
## Waiting on you — do
|
||||
|
||||
**Still needed? (yes/no)**
|
||||
1. Look at the Calibre-Web page („Adatok és mentés"). Since 2026-10-16.
|
||||
|
||||
**The rest**
|
||||
- Pull the failed disk (R-12). Since 2026-10-12.
|
||||
- Rotate the reused logins (R-10, P1). Since 2026-10-09.
|
||||
- Later (yours, not urgent): R-13.
|
||||
"""
|
||||
|
||||
PASSED, FAILED = [], []
|
||||
|
||||
|
||||
def run(page, register=REGISTER, today=BEFORE, raw_page_path=None, raw_reg_path=None):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = raw_page_path or os.path.join(d, "STATUS.md")
|
||||
r = raw_reg_path or os.path.join(d, "OPEN-ITEMS.md")
|
||||
if raw_page_path is None and page is not None:
|
||||
open(p, "w", encoding="utf-8").write(page)
|
||||
if raw_reg_path is None and register is not None:
|
||||
open(r, "w", encoding="utf-8").write(register)
|
||||
env = dict(os.environ)
|
||||
env.pop("FELHOM_GATE_TODAY", None)
|
||||
if today:
|
||||
env["FELHOM_GATE_TODAY"] = today
|
||||
pr = subprocess.run([sys.executable, GATE, p, r], capture_output=True, text=True, env=env,
|
||||
encoding="utf-8", errors="replace")
|
||||
return pr.returncode, pr.stdout + pr.stderr
|
||||
|
||||
|
||||
def mutate(old, new, page=PAGE):
|
||||
assert page.count(old) == 1, "fixture anchor not unique: %r" % old
|
||||
return page.replace(old, new)
|
||||
|
||||
|
||||
def case(name, page, expect_rc, must=None, mustnot=None, **kw):
|
||||
rc, out = run(page, **kw)
|
||||
ok = rc == expect_rc and (must is None or must in out) and (mustnot is None or mustnot not in out)
|
||||
(PASSED if ok else FAILED).append(name)
|
||||
print("%s %s (rc=%d, want %d%s)" % ("PASS" if ok else "FAIL", name, rc, expect_rc,
|
||||
", must %r" % must if must else ""))
|
||||
if not ok:
|
||||
print(" " + out.replace("\n", "\n ")[:1500])
|
||||
|
||||
|
||||
def decoy(name, page, check):
|
||||
"""A decoy convicts on its own check: WARNING before the switch (exit 0), refusal from it (exit 1)."""
|
||||
case(name + " / before the switch: warns", page, 0, must="WARNING [%s]" % check, today=BEFORE)
|
||||
case(name + " / from the switch: refuses", page, 1, must="CONVICTED [%s]" % check, today=AFTER)
|
||||
|
||||
|
||||
# ── the genuine article, both sides of the switch ────────────────────────────────────────────────
|
||||
case("genuine fixture passes (before)", PAGE, 0, must="status-view: OK", mustnot="WARNING", today=BEFORE)
|
||||
case("genuine fixture passes (after)", PAGE, 0, must="status-view: OK", today=AFTER)
|
||||
case("clean run says what it looked at", PAGE, 0, must="lines /", today=BEFORE)
|
||||
|
||||
# ── size ─────────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("size: 50 more What-works lines", mutate("- A box installs from nothing (map A1).\n",
|
||||
"- A box installs from nothing (map A1).\n" + "- One more capability, in plain words (map B1).\n" * 50), "size")
|
||||
decoy("size: few lines, too many characters", mutate("- A box installs from nothing (map A1).\n",
|
||||
"- A box installs from nothing (map A1). " + "It works well and it is proven live. " * 170 + "\n"), "size")
|
||||
case("size: a long decide table is exempt", mutate(
|
||||
"| D5 | R-14 |", "| D5 | R-14 | Approve? | **Approve** | one edit | no page | — |\n" * 60 + "| D6 | R-14 |"),
|
||||
0, must="status-view: OK", today=AFTER)
|
||||
|
||||
# ── updated ──────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("updated: news prepended, the old paragraph left behind", mutate(
|
||||
"**Updated 2026-10-20 (day).**", "**Updated 2026-10-21 (day).** New news.\n\n**Updated 2026-10-20 (day).**"), "updated")
|
||||
decoy("updated: an Earlier chain inside the paragraph", mutate(
|
||||
"A rehearsal runs tonight.", "A rehearsal runs tonight. **Earlier:** a lot happened."), "updated")
|
||||
decoy("updated: four sentences", mutate(
|
||||
"A rehearsal runs tonight.", "A rehearsal runs tonight. The installer is published."), "updated")
|
||||
decoy("updated: no Updated paragraph at all", mutate(
|
||||
"**Updated 2026-10-20 (day).** ", ""), "updated")
|
||||
case("updated: version numbers are not sentence ends", mutate(
|
||||
"Hub 0.162.0 is live (your yes).", "Hub 0.162.0 and agent 0.161.0 are live (your yes)."),
|
||||
0, must="status-view: OK", today=AFTER)
|
||||
|
||||
# ── english ──────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("english: a Hungarian do-line", mutate(
|
||||
"**The rest**\n", "**The rest**\n- Nézd meg a Calibre-Web oldalát a demo-hp-n.\n"), "english")
|
||||
case("english: the same text inside a quote passes", mutate(
|
||||
"**The rest**\n", "**The rest**\n- Read the line „Nézd meg a Calibre-Web oldalát” on demo-hp.\n"),
|
||||
0, must="status-view: OK", today=AFTER)
|
||||
|
||||
# ── open-ids ─────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("open-ids: a closed row cited in What works", mutate(
|
||||
"(map A1).", "(map A1; R-99 closed it)."), "open-ids")
|
||||
|
||||
# ── decide ───────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("decide: a DECIDED row left in the table", mutate(
|
||||
"| D5 | R-14 |", "| D20 | R-11 | **DECIDED 2026-10-20: A** — built and live. | done | — | — | — |\n| D5 | R-14 |"),
|
||||
"decide")
|
||||
decoy("decide: a row whose Pick cell reads done", mutate(
|
||||
"| D5 | R-14 | Approve the minimum machine? | **Approve** |", "| D5 | R-14 | Approve the minimum machine? | done |"),
|
||||
"decide")
|
||||
|
||||
# ── p1 ───────────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("p1: the page meets its size by deleting the P1 line", mutate(
|
||||
"- **P1:** reused logins (R-10, yours).\n", ""), "p1")
|
||||
decoy("p1: the P1 moved to What works", mutate(
|
||||
"- **P1:** reused logins (R-10, yours).\n", "", page=mutate(
|
||||
"- A box installs from nothing (map A1).\n", "- A box installs from nothing (map A1). Logins: R-10.\n")), "p1")
|
||||
|
||||
# ── operator ─────────────────────────────────────────────────────────────────────────────────────
|
||||
decoy("operator: the Later line deleted", mutate("- Later (yours, not urgent): R-13.\n", ""), "operator")
|
||||
decoy("operator: a waiting row named only in What is broken", mutate(
|
||||
"- Pull the failed disk (R-12). Since 2026-10-12.\n", "", page=mutate(
|
||||
"- The scratch box (R-15).\n", "- The scratch box (R-15). The failed disk (R-12).\n")), "operator")
|
||||
|
||||
# ── inconclusive: never a pass, no warn period ───────────────────────────────────────────────────
|
||||
case("inconclusive: a page with no sections", u"# STATUS\n\nAll good.\n", 2, must="INCONCLUSIVE", today=BEFORE)
|
||||
case("inconclusive: a page missing the do section",
|
||||
PAGE.split("## Waiting on you — do")[0], 2, must="INCONCLUSIVE", today=BEFORE)
|
||||
case("inconclusive: an empty register", PAGE, 2, must="INCONCLUSIVE", register=u"# nothing here\n", today=BEFORE)
|
||||
case("inconclusive: no register file", PAGE, 2, must="INCONCLUSIVE",
|
||||
raw_reg_path=os.path.join(HERE, "no-such-register.md"), today=BEFORE)
|
||||
|
||||
# ── the real page against the real register ──────────────────────────────────────────────────────
|
||||
case("the real STATUS.md passes against the real register", None, 0, must="status-view: OK",
|
||||
raw_page_path=os.path.join(ROOT, "STATUS.md"),
|
||||
raw_reg_path=os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md"), today=None)
|
||||
|
||||
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
|
||||
if FAILED:
|
||||
for f in FAILED:
|
||||
print(" FAILED: " + f)
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user