hub v0.137.0 source + burn-down round 2 in felhom.eu: R-277 R-581 R-600 R-544 R-855 R-134 R-92 R-292 R-599 R-725 R-728 (hub), R-819 R-857 R-555 R-364 R-587 (gates/tools), R-571 R-129 R-124-runbook (docs); 28 rows closed incl. catalog + agent v0.147.0 rows, R-350 merged into R-132, R-888 opened, R-887 mechanism (249 -> 222)
gates / gates (push) Successful in 2m3s
gates / gates (push) Successful in 2m3s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,20 @@
|
||||
## gates + tools — burn-down round 2 (2026-10-05)
|
||||
|
||||
- **R-819:** `check_stands.py` rule 3 also accepts an id found in `CLOSED-ITEMS.md`; registered as the `stands` gate in
|
||||
`repo_gates.py` with three decoys (the „check the stand's status agrees" half is not built — too vague to build).
|
||||
- **R-857:** `golden_currency_gate.py` reads a bake directory with a suffix after its date and, for two bakes of one
|
||||
version, the newest bake log; `RUNBOOK-manual-build.md`: re-vouch at once after a same-version re-bake (CASE 16, 17).
|
||||
- **R-555:** `wire_contract_gate.py` strips Go and template comments before matching receiver tokens (a tag named only in
|
||||
a comment no longer counts as decoded); 6 fields that this surfaced are allow-listed — 4 with recorded reasons, 2
|
||||
(`stacks.deployed`, `storage.decommissioned`) filed as R-888 for a decision. Decoy added; exemption removed.
|
||||
- **R-364:** `scripts/hu_grep.py` — search Hungarian text by bytes, refuse a zero unless an ASCII anchor hits and a
|
||||
negative control misses (`test_hu_grep.py`; `REUSE.md`).
|
||||
- **R-587:** `iso/build-felhom-iso.sh` refuses a RELEASE build while a `*.rootpw.txt` sits in its output dir; the
|
||||
build-deploy skill's publish block stops on one (`iso/test/test_rootpw_guard.py`, under a BusyBox PATH).
|
||||
- **R-725 (ISO half):** the bound-banner glyph (`iso/felhom-bootstrap.sh`, golden `bound.hu.txt`;
|
||||
`iso/test/test_console_glyphs.py`). Ships with the next ISO.
|
||||
- **R-208:** `test_dockerfile_arg_order.py` (hub + controller Dockerfiles).
|
||||
|
||||
## hub-db-backup — the snapshot time is read the BusyBox way too (2026-10-05, CI fix)
|
||||
|
||||
The new `script-tests` gate ran `test_hub_db_backup.py` on the CI runner (Alpine, BusyBox `date`) for the first time,
|
||||
|
||||
+17
-3
@@ -10,7 +10,10 @@ 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 absent from OPEN-ITEMS.md — a dangling register reference
|
||||
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
|
||||
@@ -30,6 +33,7 @@ import sys
|
||||
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")
|
||||
|
||||
@@ -69,6 +73,7 @@ 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.
|
||||
@@ -78,6 +83,7 @@ def main():
|
||||
|
||||
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"]
|
||||
@@ -88,8 +94,15 @@ def main():
|
||||
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":
|
||||
if not re.search(r"\*\*%s\*\*" % re.escape(ref), register):
|
||||
fails.append("%s: register id %s is not in OPEN-ITEMS.md" % (cid, ref))
|
||||
# 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)
|
||||
@@ -103,6 +116,7 @@ def main():
|
||||
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))
|
||||
|
||||
@@ -39,9 +39,6 @@ EXEMPT = {
|
||||
("felhom.eu", "hostinstall"):
|
||||
"R-426 — no plausible decoy constructed yet. It asserts installer invariants against a "
|
||||
"shell script; a legitimate decoy needs a shape a real edit would produce.",
|
||||
("felhom.eu", "wire-contract"):
|
||||
"R-426 — 607 lines comparing emitted fields to receiver structs across two repos; a decoy "
|
||||
"needs a Go edit, which this sweep was forbidden from making.",
|
||||
("felhom.eu", "due-checks"):
|
||||
"R-426 — no legitimate decoy constructed; the attempt in the sweep was a no-op and its "
|
||||
"verdict was withdrawn rather than reported.",
|
||||
|
||||
@@ -142,8 +142,10 @@ WAIVER_MAX_DAYS = 14
|
||||
|
||||
# `## v0.206.0 — …` on the FIRST such line: the CHANGELOG is newest-first by convention.
|
||||
RELEASED_RE = re.compile(r"^##\s+v(\d+)\.(\d+)\.(\d+)\b")
|
||||
# `golden-0.205.0-2026-08-07/` — the bake-evidence directory the runbook's §4.1 produces.
|
||||
EVIDENCE_RE = re.compile(r"^golden-(\d+)\.(\d+)\.(\d+)-\d{4}-\d{2}-\d{2}$")
|
||||
# `golden-0.205.0-2026-08-07/` — the bake-evidence directory the runbook's §4.1 produces — and a same-version
|
||||
# RE-BAKE, `golden-0.292.0-2026-10-04-rebake/` (R-857): an optional `-<suffix>` after the date. The re-bake
|
||||
# directory used to be invisible to this regex, so the gate kept reporting the FIRST bake's sha.
|
||||
EVIDENCE_RE = re.compile(r"^golden-(\d+)\.(\d+)\.(\d+)-(\d{4}-\d{2}-\d{2})(-[a-z0-9][a-z0-9-]*)?$")
|
||||
|
||||
|
||||
def released_versions():
|
||||
@@ -226,7 +228,12 @@ def newest_baked():
|
||||
if sha is None:
|
||||
rejected.append("%s (%s)" % (name, why))
|
||||
continue
|
||||
found.append((tuple(int(g) for g in m.groups()), "%s [sha %s…]" % (name, sha[:12])))
|
||||
# Order (R-857): version, then bake date, then a suffixed re-bake after the plain bake of the same
|
||||
# date (a re-bake directory is made after the first one), then the name. A tuple of the version
|
||||
# alone tied two bakes of one version and picked whichever sorted first.
|
||||
version = tuple(int(g) for g in m.groups()[:3])
|
||||
key = (version, m.group(4), 1 if m.group(5) else 0, name)
|
||||
found.append((key, "%s [sha %s…]" % (name, sha[:12])))
|
||||
if rejected:
|
||||
print(" NOT counted as bakes — a directory name is not a bake (R-410):")
|
||||
for r in sorted(rejected):
|
||||
@@ -235,7 +242,11 @@ def newest_baked():
|
||||
return None, ("no golden-<VER>-<DATE>/ directory under %s holds a bake log with a "
|
||||
"GOLDEN_SHA256 line" % EVIDENCE_DIR)
|
||||
found.sort()
|
||||
return found[-1]
|
||||
key, label = found[-1]
|
||||
same = [lbl for k, lbl in found if k[0] == key[0]]
|
||||
if len(same) > 1:
|
||||
print(" %d bakes of %s on file; the NEWEST is read: %s" % (len(same), ".".join(map(str, key[0])), label))
|
||||
return key[0], label
|
||||
|
||||
|
||||
WAIVER_KEY_RE = re.compile(r"^\s*([a-z_]+)\s*:\s*(.*?)\s*$")
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""hu_grep.py — count the lines holding a Hungarian (accented) string, and REFUSE to say "0" untested (R-364).
|
||||
|
||||
Usage: python3 scripts/hu_grep.py PATTERN --anchor ASCII_TEXT [--negative TEXT] PATH [PATH ...]
|
||||
Exit 0 found (prints the count and each file:line) · 1 a TESTED zero · 2 REFUSED (the zero is not evidence)
|
||||
|
||||
WHY. Accented-text search is an instrument that silently transforms its input. Three near-misses are on record:
|
||||
`ssh → pct exec → bash -c` mangled a pattern (2026-07-20); `kubectl exec … sh -c grep` returned 0 for three
|
||||
strings that WERE there (2026-08-13); `tar -tf` printed `őszibarack.md` as `\\305\\221szibarack.md` (2026-08-21).
|
||||
Each time the zero looked exactly like "absent". Discipline alone failed, so the judgement lives here:
|
||||
|
||||
* the file bytes are read by Python, never through a shell, and matched as UTF-8 bytes;
|
||||
* a pattern that ARRIVED transformed — octal escapes like `\\305\\221`, or U+FFFD — is refused outright;
|
||||
* for a pattern with any byte >= 0x80, a ZERO is reported only when
|
||||
- the ASCII ANCHOR (text you know is in these files) is found — the instrument can see the files; and
|
||||
- the NEGATIVE control (default: a string nobody writes) returns zero — the instrument can say no; and
|
||||
- the same pattern in the other Unicode normal form (NFC <-> NFD) is also absent — an "ő" typed as one
|
||||
code point does not match "o" + combining mark, and that zero would be a false one.
|
||||
Otherwise it prints REFUSED with the reason and exits 2.
|
||||
|
||||
An ASCII pattern is a plain count (exit 1 on zero) — the transformations above do not touch it.
|
||||
Pure Python, stdlib only: runs on the BusyBox CI runner and on any box with python3.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
|
||||
DEFAULT_NEGATIVE = "zz-hu-grep-negative-control-qx7"
|
||||
OCTAL_ESCAPE = re.compile(r"\\[0-3][0-7]{2}")
|
||||
|
||||
|
||||
def iter_files(paths):
|
||||
for p in paths:
|
||||
if os.path.isdir(p):
|
||||
for dp, dns, fns in os.walk(p):
|
||||
dns[:] = sorted(d for d in dns if d not in (".git", "node_modules", "__pycache__"))
|
||||
for f in sorted(fns):
|
||||
yield os.path.join(dp, f)
|
||||
else:
|
||||
yield p
|
||||
|
||||
|
||||
def hits(needle, paths):
|
||||
"""[(path, line_no)] for every line whose bytes contain needle (a str, matched as UTF-8 bytes)."""
|
||||
b = needle.encode("utf-8")
|
||||
out = []
|
||||
for f in iter_files(paths):
|
||||
try:
|
||||
with open(f, "rb") as fh:
|
||||
for n, line in enumerate(fh, 1):
|
||||
if b in line:
|
||||
out.append((f, n))
|
||||
except OSError as e:
|
||||
raise SystemExit("REFUSED: cannot read %s (%s) — an unreadable file is not an absent string" % (f, e))
|
||||
return out
|
||||
|
||||
|
||||
def judge(pattern, anchor, negative, paths):
|
||||
"""(exit_code, message, found_lines)."""
|
||||
if "�" in pattern or OCTAL_ESCAPE.search(pattern):
|
||||
return 2, ("REFUSED: the pattern arrived TRANSFORMED (%r) — octal escapes or U+FFFD mean a shell or a "
|
||||
"listing re-encoded it. Pass the real characters." % pattern), []
|
||||
found = hits(pattern, paths)
|
||||
if found:
|
||||
return 0, "%d line(s)" % len(found), found
|
||||
if all(ord(c) < 0x80 for c in pattern):
|
||||
return 1, "0 line(s)", []
|
||||
# A zero for an accented pattern: only with all three controls behaving.
|
||||
if not anchor or any(ord(c) >= 0x80 for c in anchor):
|
||||
return 2, "REFUSED: an accented pattern needs --anchor with ASCII text known to be in these files", []
|
||||
a = hits(anchor, paths)
|
||||
if not a:
|
||||
return 2, ("REFUSED: the anchor %r was not found either — the instrument cannot see these files, so "
|
||||
"its zero is not evidence" % anchor), []
|
||||
if hits(negative, paths):
|
||||
return 2, "REFUSED: the negative control %r was FOUND — this instrument cannot say no" % negative, []
|
||||
for form in ("NFC", "NFD"):
|
||||
other = unicodedata.normalize(form, pattern)
|
||||
if other != pattern:
|
||||
h = hits(other, paths)
|
||||
if h:
|
||||
return 2, ("REFUSED: 0 for the pattern as typed, but %d line(s) hold its %s form — the files and "
|
||||
"the pattern use different Unicode normal forms" % (len(h), form)), []
|
||||
return 1, ("0 line(s) — a TESTED zero: anchor %r found on %d line(s), negative control 0, other normal "
|
||||
"form 0" % (anchor, len(a))), []
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
||||
ap.add_argument("pattern")
|
||||
ap.add_argument("paths", nargs="+")
|
||||
ap.add_argument("--anchor", default="")
|
||||
ap.add_argument("--negative", default=DEFAULT_NEGATIVE)
|
||||
a = ap.parse_args(argv)
|
||||
rc, msg, found = judge(a.pattern, a.anchor, a.negative, a.paths)
|
||||
for f, n in found:
|
||||
print("%s:%d" % (f, n))
|
||||
print(msg)
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -145,6 +145,24 @@ while [[ $# -gt 0 ]]; do
|
||||
*) die "unknown argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
# ── Root-password guard (R-587, 2026-10-05) ──────────────────────────────────────────────────────────────
|
||||
# A non-release build writes <iso>.rootpw.txt (a plaintext root password) into the out directory, and a
|
||||
# RELEASE build's out directory is the public bucket's publish source. Only the publish command's
|
||||
# `--include "felhom-installer-<VER>*"` kept two such files off a world-readable bucket — by an accident
|
||||
# of naming. So a release build REFUSES to start while any *.rootpw.txt sits in its out directory.
|
||||
# POSIX sh on purpose (no [[ ]]): scripts/iso/test/test_rootpw_guard.py runs this function under the
|
||||
# BusyBox sh of the CI runner. No bypass flag: shred the file (`shred -u`), then build.
|
||||
rootpw_guard() {
|
||||
if $RELEASE && [ -d "$OUT_DIR" ]; then
|
||||
_rp="$(find "$OUT_DIR" -maxdepth 1 -name '*.rootpw.txt' 2>/dev/null | head -1)"
|
||||
if [ -n "$_rp" ]; then
|
||||
die "root-password guard: $_rp is in the out directory ($OUT_DIR), which a release is published FROM. Shred it (shred -u) before building a release — a root password must never sit beside a public upload (R-587)."
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
rootpw_guard
|
||||
|
||||
# ── Clean-tree gate (R-730, 2026-09-30) ────────────────────────────────────────────────────────────────
|
||||
# The manifest names the commit an image was built from. ISO 1.29.0's names 8d539f9, but the published image
|
||||
# carries a menu fix committed 35 minutes AFTER the build (31eeb36): it was built from an uncommitted tree, so
|
||||
|
||||
@@ -96,7 +96,7 @@ print_pairing_banner() {
|
||||
print_bound_banner() {
|
||||
set_console_font
|
||||
{ printf '\n================================================\n'
|
||||
printf ' Felhom — a doboz össze van kötve. ✔\n\n'
|
||||
printf ' Felhom — a doboz össze van kötve.\n\n'
|
||||
printf ' A beállítás magától folytatódik, ez néhány percig tart.\n'
|
||||
printf ' A vezérlőpult címét az e-mailben kapott levél tartalmazza.\n\n'
|
||||
printf ' Ezen a gépen nincs több teendőd.\n\n'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
================================================
|
||||
Felhom — a doboz össze van kötve. ✔
|
||||
Felhom — a doboz össze van kötve.
|
||||
|
||||
A beállítás magától folytatódik, ez néhány percig tart.
|
||||
A vezérlőpult címét az e-mailben kapott levél tartalmazza.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""R-725: every console banner felhom-bootstrap.sh paints uses only glyphs the console font draws.
|
||||
|
||||
The box console loads a Latin-2 font (Lat2-Terminus16 and friends, set_console_font). A glyph outside it is
|
||||
drawn as something else: the linked banner ended in "✔", and the household saw "a doboz össze van kötve. V".
|
||||
This test reads every printf literal in the script and the banner goldens the ISO harness compares against,
|
||||
and refuses any character outside ASCII + the Hungarian letters + the em dash (which the banners already use
|
||||
and which renders). Pure file reads: runs on the BusyBox CI runner.
|
||||
Run: python3 scripts/iso/test/test_console_glyphs.py"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ISO = os.path.dirname(HERE)
|
||||
ALLOWED_NON_ASCII = set("áéíóöőúüűÁÉÍÓÖŐÚÜŰ—")
|
||||
PRINTF = re.compile(r"""printf\s+(?:'([^']*)'|"([^"]*)")""")
|
||||
|
||||
|
||||
def bad_glyphs(text):
|
||||
return sorted({c for c in text if ord(c) > 127 and c not in ALLOWED_NON_ASCII})
|
||||
|
||||
|
||||
def main():
|
||||
failures, checked = [], 0
|
||||
script = os.path.join(ISO, "felhom-bootstrap.sh")
|
||||
with open(script, encoding="utf-8") as fh:
|
||||
for n, line in enumerate(fh, 1):
|
||||
for m in PRINTF.finditer(line):
|
||||
checked += 1
|
||||
bad = bad_glyphs(m.group(1) or m.group(2) or "")
|
||||
if bad:
|
||||
failures.append("felhom-bootstrap.sh:%d: %s" % (n, " ".join(bad)))
|
||||
gdir = os.path.join(HERE, "golden")
|
||||
goldens = sorted(f for f in os.listdir(gdir) if f.endswith(".txt"))
|
||||
for g in goldens:
|
||||
with open(os.path.join(gdir, g), encoding="utf-8") as fh:
|
||||
bad = bad_glyphs(fh.read())
|
||||
if bad:
|
||||
failures.append("golden/%s: %s" % (g, " ".join(bad)))
|
||||
# Scope is a fact: a reader that found no banners checked nothing.
|
||||
if checked < 20 or not goldens:
|
||||
print("FAIL: read only %d printf literals and %d goldens — the scope is wrong" % (checked, len(goldens)))
|
||||
return 1
|
||||
# Positive control: the checker convicts the glyph that started this row.
|
||||
if bad_glyphs("a doboz össze van kötve. ✔") != ["✔"]:
|
||||
print("FAIL: the checker does not convict the check-mark glyph")
|
||||
return 1
|
||||
if failures:
|
||||
print("FAIL: console glyphs the Latin-2 font cannot draw (R-725):\n " + "\n ".join(failures))
|
||||
return 1
|
||||
print("OK: %d printf literals + %d goldens use only console-safe glyphs" % (checked, len(goldens)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""R-587: a RELEASE ISO build refuses to start while a *.rootpw.txt sits in its out directory.
|
||||
|
||||
The out directory is the public bucket's publish source; a root-password file there was kept off the
|
||||
bucket only by the publish command's --include pattern, by an accident of naming.
|
||||
|
||||
HOW IT RUNS ANYWHERE. The build script is bash and needs docker; the CI runner has neither (Alpine +
|
||||
BusyBox + python3 + git). So this test lifts the `rootpw_guard` function out of build-felhom-iso.sh
|
||||
VERBATIM and runs it under `sh` with a stub `die` — the function is written in POSIX sh for exactly
|
||||
this. It also checks, statically, that the script CALLS the guard before the build does anything else
|
||||
of consequence (the clean-tree gate and the docker preflight), because a guard that is defined and not
|
||||
called is the seam-built-never-wired shape.
|
||||
Run: python3 scripts/iso/test/test_rootpw_guard.py"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
BUILD = os.path.join(os.path.dirname(HERE), "build-felhom-iso.sh")
|
||||
|
||||
|
||||
def guard_source():
|
||||
text = open(BUILD, encoding="utf-8").read()
|
||||
m = re.search(r"^rootpw_guard\(\) \{\n.*?^\}\n", text, re.S | re.M)
|
||||
if not m:
|
||||
raise SystemExit("FAIL: rootpw_guard() not found in build-felhom-iso.sh")
|
||||
return text, m.group(0)
|
||||
|
||||
|
||||
def run_guard(func, release, out_dir):
|
||||
script = ('die() { echo "DIE: $1"; exit 1; }\nRELEASE=%s\nOUT_DIR=%s\n%s\nrootpw_guard\necho PROCEEDED\n'
|
||||
% ("true" if release else "false", out_dir, func))
|
||||
p = subprocess.run(["sh", "-c", script], capture_output=True, text=True)
|
||||
return p.returncode, p.stdout + p.stderr
|
||||
|
||||
|
||||
def main():
|
||||
fails = []
|
||||
text, func = guard_source()
|
||||
tmp = tempfile.mkdtemp(prefix="rootpw-guard-")
|
||||
try:
|
||||
open(os.path.join(tmp, "felhom-installer-9.9.9-pve9.2-1.iso"), "w").close()
|
||||
# 1 — the genuine article: a release with a clean out dir proceeds.
|
||||
rc, out = run_guard(func, True, tmp)
|
||||
if rc != 0 or "PROCEEDED" not in out:
|
||||
fails.append("clean out dir: a release build was refused (rc=%d): %s" % (rc, out))
|
||||
# 2 — the decoy: a stray rootpw file (named so the publish --include would MISS it) refuses.
|
||||
stray = os.path.join(tmp, "felhom-pve-9.2-1-v1.24.0-nested.iso.rootpw.txt")
|
||||
open(stray, "w").close()
|
||||
rc, out = run_guard(func, True, tmp)
|
||||
if rc == 0 or "PROCEEDED" in out or "rootpw.txt" not in out:
|
||||
fails.append("stray rootpw file: the release build was NOT refused (rc=%d): %s" % (rc, out))
|
||||
# 3 — a non-release build in the same dir proceeds (it is the producer of these files, not a publish).
|
||||
rc, out = run_guard(func, False, tmp)
|
||||
if rc != 0 or "PROCEEDED" not in out:
|
||||
fails.append("non-release build was refused (rc=%d): %s" % (rc, out))
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
# 4 — wired: the guard is CALLED, and before the clean-tree gate and the docker preflight.
|
||||
call = re.search(r"^rootpw_guard\s*$", text, re.M)
|
||||
later = [re.search(r"^clean_tree_gate\s*$", text, re.M), re.search(r"^command -v docker", text, re.M)]
|
||||
if not call or any(m is None or m.start() < call.start() for m in later):
|
||||
fails.append("rootpw_guard is not called before clean_tree_gate and the docker preflight")
|
||||
if fails:
|
||||
print("FAIL (R-587):\n " + "\n ".join(fails))
|
||||
return 1
|
||||
print("OK: a release build refuses a *.rootpw.txt in its out dir; a clean dir and a non-release build proceed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -24,6 +24,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
|
||||
13. observations a REPORT.md observation with no register row behind it (R-389)
|
||||
14. register-shape a register row whose state cell was eaten, a duplicated id, or a blank line
|
||||
splitting the table — the register mis-stating how many findings exist (R-627)
|
||||
14b. stands where-felhom-stands.yaml: every claim cites a source that resolves, and every
|
||||
'walked' cites a walk (R-819; it was red and ran in no runner)
|
||||
15. script-tests every Python test suite under scripts/ (found by a walk), by exit code (R-885)
|
||||
16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421)
|
||||
|
||||
@@ -154,6 +156,9 @@ GATES = [
|
||||
# nothing noticed until a person read the file. R-254 had been missing its state cell since
|
||||
# 2026-08-08 — 45 days — for the same reason. Fast: one file read.
|
||||
("register-shape", os.path.join(SCRIPTS, "register_shape_gate.py"), [], True, False),
|
||||
# R-819 — check_stands.py guarded the operator's "where Felhom stands" page and was RED for weeks,
|
||||
# registered in no runner, so nobody saw it (the R-29 shape). Fast: three file reads.
|
||||
("stands", os.path.join(SCRIPTS, "check_stands.py"), [], True, False),
|
||||
# R-885 — the Python test suites under scripts/ (found by a walk) ran only by hand; a change that broke the
|
||||
# 15 tests of the DooPlex hub-DB scripts, or the 73 of the instructions gate, reached main green. ~20 s.
|
||||
("script-tests", os.path.join(SCRIPTS, "script_tests_gate.py"), [ROOT], True, False),
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R-208: in hub/Dockerfile no per-build ARG (VERSION, BUILD_TIME, GIT_COMMIT) is declared above the
|
||||
module-download RUN of the same stage.
|
||||
|
||||
WHY. An ARG in scope is part of every later RUN's cache key. Declared above `RUN go mod download`, a fresh
|
||||
--build-arg VERSION invalidates the download layer on every build — measured: 208 download records, each used
|
||||
once, ~440 MB of dead cache per build. Declared below it, the download is CACHED until go.mod/go.sum change.
|
||||
Pure text read; runs on the BusyBox CI runner. Run: python3 scripts/test_dockerfile_arg_order.py"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PER_BUILD = {"VERSION", "BUILD_TIME", "GIT_COMMIT"}
|
||||
|
||||
|
||||
def violations(text):
|
||||
"""Per stage: a per-build ARG that appears before that stage's `go mod download` RUN."""
|
||||
out, stage, seen_args, downloaded = [], 0, [], False
|
||||
for n, raw in enumerate(text.splitlines(), 1):
|
||||
line = raw.strip()
|
||||
if re.match(r"(?i)^FROM\s", line):
|
||||
stage, seen_args, downloaded = stage + 1, [], False
|
||||
continue
|
||||
m = re.match(r"(?i)^ARG\s+([A-Za-z_][A-Za-z0-9_]*)", line)
|
||||
if m and m.group(1) in PER_BUILD and not downloaded:
|
||||
seen_args.append((n, m.group(1)))
|
||||
if re.match(r"(?i)^RUN\s.*\bgo mod download\b", line):
|
||||
downloaded = True
|
||||
out.extend("line %d: ARG %s above the module download (stage %d)" % (n2, a, stage) for n2, a in seen_args)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
# Decoy first: the pre-fix shape must convict, or this test checks nothing.
|
||||
decoy = "FROM golang AS b\nARG VERSION=dev\nCOPY go.mod ./\nRUN go mod download || true\nRUN go build\n"
|
||||
if not violations(decoy):
|
||||
print("FAIL: the decoy (ARG VERSION above go mod download) was not convicted")
|
||||
return 1
|
||||
path = os.path.join(ROOT, "hub", "Dockerfile")
|
||||
text = open(path, encoding="utf-8").read()
|
||||
if not re.search(r"(?m)^RUN\s.*\bgo mod download\b", text):
|
||||
print("FAIL: hub/Dockerfile has no `go mod download` RUN — update this test with the new layout")
|
||||
return 1
|
||||
bad = violations(text)
|
||||
if bad:
|
||||
print("FAIL: hub/Dockerfile (R-208):\n " + "\n ".join(bad))
|
||||
return 1
|
||||
print("OK: hub/Dockerfile declares its per-build ARGs below the module download")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -53,6 +53,12 @@ COVERS = {
|
||||
"(2026-10-03) an old-shape row under the new header, a near-miss category, an "
|
||||
"old rank tag as Sev, an undefined state word, and a pipe outside backticks"),
|
||||
"decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)",
|
||||
"wire-contract": ("R-555: an emitted tag whose name the receiver carries ONLY in a // and a /* */ comment "
|
||||
"must convict (it passed for months as `language` did); the genuine article — the same "
|
||||
"name in a struct tag beside a `//` inside a string literal — must pass"),
|
||||
"stands": ("R-819: a stand citing a register id that exists in NEITHER register, and a stand "
|
||||
"marked 'walked' whose only source is a register row (a green dot from a label); "
|
||||
"plus the genuine article — a stand citing only a CLOSED row, which must PASS"),
|
||||
"site": "R-423: a NEW page under website/ that PAGES does not list (the gate used to scan 7 of 9)",
|
||||
"script-tests": ("R-885: FIVE cases in scripts/test_script_tests_gate.py, run from here: a suite that PRINTS "
|
||||
"OK and exits 1 (label without fact), a failing suite three levels deep (scope is a walk), "
|
||||
@@ -486,6 +492,52 @@ _rsg_case("pipe-outside-backticks",
|
||||
_HEALTHY.replace("**A finding.**", "**A finding.** owner: CC | operator"),
|
||||
must_convict=True, expect_rule="RULE 5")
|
||||
|
||||
# ── wire-contract (R-555) ────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# The real receiver (the hub tree) with ONE received tag taken out of its token set and put back only
|
||||
# through a file that names it in prose. The gate runs end to end against the real emitters, so the
|
||||
# decoy needs the controller and agent clones beside this one (CI fetches both; a missing clone is
|
||||
# INCONCLUSIVE and fails this suite, never a pass). `repo_size_bytes` is `offsite.repo_size_bytes`,
|
||||
# which the hub really decodes, so the genuine case passes for the right reason.
|
||||
_WC = r"""
|
||||
import os, sys, tempfile
|
||||
sys.path.insert(0, "scripts")
|
||||
import wire_contract_gate as g
|
||||
T, mode = "repo_size_bytes", sys.argv[1]
|
||||
tmp = tempfile.mkdtemp(prefix="r555-")
|
||||
body = {"comment": "package prose\n// %s is described here, in prose only.\n/* and %s again */\n" % (T, T),
|
||||
"genuine": "package prose\nvar u = \"http://x\" // not a comment start inside a string\n"
|
||||
"type X struct { A int `json:\"%s\"` }\n" % T}[mode]
|
||||
open(os.path.join(tmp, "prose.go"), "w").write(body)
|
||||
orig, hub = g.receiver_tokens, os.path.abspath(g.REPOS["hub"])
|
||||
g.receiver_tokens = lambda p: ((orig(p) - {T}) | orig(tmp)) if os.path.abspath(p) == hub else orig(p)
|
||||
rc, conv = g.run(quiet=True)
|
||||
print("rc=%d convicted_T=%s" % (rc, any(T in str(c) for c in conv)))
|
||||
sys.exit(0 if rc == 0 else (10 if any(T in str(c) for c in conv) else 11))
|
||||
"""
|
||||
for _mode, _want in (("comment", 10), ("genuine", 0)):
|
||||
ran += 1
|
||||
_p = subprocess.run([sys.executable, "-c", _WC, _mode], cwd=ROOT, capture_output=True, text=True)
|
||||
if _p.returncode != _want:
|
||||
fails.append("wire-contract/%s: rc=%d, want %d (10 = the comment-only tag convicted, 0 = passed)\n%s"
|
||||
% (_mode, _p.returncode, _want, (_p.stdout + _p.stderr)[-500:]))
|
||||
else:
|
||||
print(" ok %-20s %s" % ("wire-contract/" + _mode,
|
||||
"decoy rejected" if _want else "genuine accepted"))
|
||||
|
||||
# ── stands (R-819) ───────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# check_stands.py was red and in no runner. Its decoys are stand files written as a session would write
|
||||
# them, checked with the real registers. The genuine case is the one R-819 fixed: a citation of a row
|
||||
# that CLOSED (R-273, the never-tagged agent) is a valid source, not a dangling one.
|
||||
_STANDS = os.path.join(ROOT, "documentation", "architecture", "zz-r819-decoy-stands.yaml")
|
||||
_stand = lambda status, ref: (u"claims:\n - id: decoy.stand\n title: \"decoy\"\n status: %s\n"
|
||||
u" sources:\n - register: %s\n" % (status, ref))
|
||||
decoy("stands/dangling-id", "check_stands.py", plant_file(_STANDS, _stand("built", "R-99999")), args=(_STANDS,))
|
||||
decoy("stands/walked-no-walk", "check_stands.py", plant_file(_STANDS, _stand("walked", "R-273")), args=(_STANDS,))
|
||||
decoy("stands/closed-row-ok", "check_stands.py", plant_file(_STANDS, _stand("built", "R-273")), args=(_STANDS,),
|
||||
expect="pass")
|
||||
|
||||
print()
|
||||
if fails:
|
||||
for f in fails:
|
||||
|
||||
@@ -118,6 +118,7 @@ def main():
|
||||
print("CASE 4 ok: the tree is unchanged; the gate's verdict is the same as the baseline")
|
||||
|
||||
fails += waiver_cases()
|
||||
fails += rebake_cases()
|
||||
|
||||
if fails:
|
||||
print()
|
||||
@@ -245,4 +246,42 @@ def waiver_cases():
|
||||
return fails
|
||||
|
||||
|
||||
# ── R-857: a golden baked TWICE under one version ───────────────────────────────────────────────
|
||||
#
|
||||
# 2026-10-04: 0.292.0 was re-baked into `golden-0.292.0-2026-10-04-rebake/`. The gate's regex did not
|
||||
# match the suffix, so it kept reporting the FIRST bake's sha (d6cf8b33…) while the registry held the
|
||||
# second. The CONSEQUENCE asserted: the newer bake's sha is the one reported.
|
||||
def rebake_cases():
|
||||
fails = []
|
||||
OLD_SHA, NEW_SHA = "d6" * 32, "79" * 32
|
||||
env, tmp = synthetic_tree(["9.9.9"], "9.9.9", None)
|
||||
try:
|
||||
ev = env["GOLDEN_GATE_EVIDENCE_DIR"]
|
||||
shutil.rmtree(os.path.join(ev, "golden-9.9.9-2026-01-01"))
|
||||
for name, sha in (("golden-9.9.9-2026-01-04", OLD_SHA), ("golden-9.9.9-2026-01-04-rebake", NEW_SHA)):
|
||||
os.makedirs(os.path.join(ev, name))
|
||||
with io.open(os.path.join(ev, name, "bake.log"), "w", encoding="utf-8") as fh:
|
||||
fh.write("[golden] upload OK (HTTP 201)\nGOLDEN_SHA256=%s\n" % sha)
|
||||
rc, out = run_gate_env(env)
|
||||
if rc != 0:
|
||||
fails.append("CASE 16 (R-857): the gate failed on a current golden baked twice (rc=%d)\n%s" % (rc, out[-400:]))
|
||||
elif NEW_SHA[:12] not in out or OLD_SHA[:12] in out.split("newest golden baked")[-1].splitlines()[0]:
|
||||
fails.append("CASE 16 (R-857): two bakes of one version — the gate did not report the RE-BAKE's sha\n%s"
|
||||
% out[-400:])
|
||||
else:
|
||||
print("CASE 16 ok (R-857): of two bakes of one version, the re-bake's sha is the one reported")
|
||||
# A later DATE of the same version beats an earlier suffixed one.
|
||||
os.makedirs(os.path.join(ev, "golden-9.9.9-2026-01-05"))
|
||||
with io.open(os.path.join(ev, "golden-9.9.9-2026-01-05", "bake.log"), "w", encoding="utf-8") as fh:
|
||||
fh.write("GOLDEN_SHA256=%s\n" % ("ab" * 32))
|
||||
rc, out = run_gate_env(env)
|
||||
if ("ab" * 32)[:12] not in out:
|
||||
fails.append("CASE 17 (R-857): a later-dated bake of the same version was not preferred\n%s" % out[-400:])
|
||||
else:
|
||||
print("CASE 17 ok (R-857): a later-dated bake of the same version wins")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
return fails
|
||||
|
||||
|
||||
sys.exit(main())
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""R-364: hu_grep.py never reports an untested zero for an accented pattern.
|
||||
|
||||
Each case asserts the CONSEQUENCE an operator would act on: a present string is counted; a transformed
|
||||
pattern, a blind instrument, or a normal-form mismatch is REFUSED (exit 2), never a quiet 0; and a real
|
||||
absence, with every control behaving, is a 0 (exit 1). Pure Python — runs on the BusyBox CI runner.
|
||||
Run: python3 scripts/test_hu_grep.py"""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unicodedata
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import hu_grep # noqa: E402
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def case(label, want_rc, pattern, anchor, files, want_in=""):
|
||||
tmp = tempfile.mkdtemp(prefix="hu-grep-")
|
||||
try:
|
||||
for name, body in files.items():
|
||||
with open(os.path.join(tmp, name), "w", encoding="utf-8") as fh:
|
||||
fh.write(body)
|
||||
rc, msg, _ = hu_grep.judge(pattern, anchor, hu_grep.DEFAULT_NEGATIVE, [tmp])
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
if rc != want_rc or want_in not in msg:
|
||||
fails.append("%s: rc=%d msg=%r, want rc=%d containing %r" % (label, rc, msg, want_rc, want_in))
|
||||
else:
|
||||
print("ok %-34s rc=%d %s" % (label, rc, msg[:70]))
|
||||
|
||||
|
||||
PAGE = {"page.html": "<p>Ha lejárt, kérj újat az alábbi gombbal.</p>\n<p>Felhom</p>\n"}
|
||||
|
||||
case("present accented string counted", 0, "kérj újat", "Felhom", PAGE, "1 line")
|
||||
case("octal-escaped pattern REFUSED", 2, "k\\303\\251rj", "Felhom", PAGE, "TRANSFORMED")
|
||||
case("U+FFFD pattern REFUSED", 2, "k�rj", "Felhom", PAGE, "TRANSFORMED")
|
||||
case("blind instrument REFUSED", 2, "ügyfélszolgálat", "NotInTheFile", PAGE, "anchor")
|
||||
case("no anchor given REFUSED", 2, "ügyfélszolgálat", "", PAGE, "--anchor")
|
||||
case("NFD file, NFC pattern REFUSED", 2, "ügyfélszolgálat", "Felhom",
|
||||
{"nfd.txt": unicodedata.normalize("NFD", "Felhom ügyfélszolgálat\n")}, "NFD form")
|
||||
case("tested zero is a zero", 1, "ügyfélszolgálat", "Felhom", PAGE, "TESTED zero")
|
||||
case("ascii pattern plain zero", 1, "nothere", "", PAGE, "0 line")
|
||||
|
||||
if fails:
|
||||
print("\nFAIL:\n " + "\n ".join(fails))
|
||||
sys.exit(1)
|
||||
print("\nhu_grep: OK — no untested zero for an accented pattern")
|
||||
@@ -280,6 +280,30 @@ ALLOWLIST = {
|
||||
"and a fact displayed with nothing to compare it to is decoration."),
|
||||
(_CH, "backup.last_db_dump"): _R264,
|
||||
(_CH, "backup.last_integrity_check"): _R264,
|
||||
|
||||
# ---- R-555 (2026-10-05): surfaced when comments stopped counting as receivers ----
|
||||
#
|
||||
# Each of these passed for months because its NAME occurred hub-side only in a comment. No hub struct
|
||||
# decodes any of them. Listed so the drop is visible; none is known to be needed by a hub surface.
|
||||
# **Delete an entry when a hub struct starts decoding the field.**
|
||||
(_AH, "audit_tail"): (
|
||||
"R-555: ignored on purpose — hub/internal/api/handler.go's hostReportPayload comment names it "
|
||||
"among the fields the hub does not decode (backups/restore_tests/pbs_snapshots/audit_tail)."),
|
||||
(_AH, "guests.spec"): (
|
||||
"R-555: the parent of guests.spec.disk_bytes / memory_bytes above — sizing is hub-owned intent, "
|
||||
"not mirrored reality, so the object is not decoded either."),
|
||||
(_CH, "backup.integrity_ok"): (
|
||||
"R-555 / R-331: no producer since disk-tier restic moved to the agent (slice 8C); the Backup card "
|
||||
"dropped it rather than re-sourcing it (hub/internal/web/backup_card.go)."),
|
||||
(_CH, "backup.repo_size_mb"): (
|
||||
"R-555 / R-331: no producer since slice 8C; the Backup card reads offsite.repo_size_bytes "
|
||||
"instead (hub/internal/web/backup_card.go)."),
|
||||
(_CH, "stacks.deployed"): (
|
||||
"R-555: surfaced 2026-10-05 — the hub decodes no deployed-app list from the report; whether a hub "
|
||||
"surface needs it is not decided. Filed as R-888 for the operator to decide."),
|
||||
(_CH, "storage.decommissioned"): (
|
||||
"R-555: surfaced 2026-10-05 — the hub decodes no per-drive decommissioned marker from the report; "
|
||||
"whether a hub surface needs it is not decided. Filed as R-888 for the operator to decide."),
|
||||
}
|
||||
|
||||
# A node whose IMMEDIATE CHILDREN are still checked but whose DEEPER descendants are not, because the
|
||||
@@ -446,6 +470,48 @@ def walk(by_dir, by_name, start_dir, start_type, seen=None, prefix=""):
|
||||
|
||||
|
||||
TOKEN_RE = re.compile(r"[A-Za-z0-9_]+")
|
||||
TEMPLATE_COMMENT_RE = re.compile(r"\{\{-?\s*/\*.*?\*/\s*-?\}\}", re.S)
|
||||
|
||||
|
||||
def strip_go_comments(src):
|
||||
"""Go source with every // and /* */ comment removed; string, raw-string and rune literals kept.
|
||||
|
||||
R-555: a field whose name appeared hub-side only in a COMMENT counted as received — `language`
|
||||
passed because configs.go said "this page's existing language". A comment is prose, not a decoder.
|
||||
A small lexer rather than a regex, because a regex cannot tell `//` in "http://…" or a backtick
|
||||
struct tag from a comment, and the struct tags are exactly what this gate reads.
|
||||
"""
|
||||
out, i, n = [], 0, len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
||||
j = src.find("\n", i)
|
||||
i = n if j < 0 else j # keep the newline
|
||||
continue
|
||||
if c == "/" and i + 1 < n and src[i + 1] == "*":
|
||||
j = src.find("*/", i + 2)
|
||||
i = n if j < 0 else j + 2
|
||||
out.append(" ")
|
||||
continue
|
||||
if c in "\"'`":
|
||||
j = i + 1
|
||||
while j < n and src[j] != c:
|
||||
if c != "`" and src[j] == "\\":
|
||||
j += 1
|
||||
elif c != "`" and src[j] == "\n":
|
||||
break # an unterminated literal ends at the line
|
||||
j += 1
|
||||
out.append(src[i:j + 1])
|
||||
i = j + 1
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def strip_template_comments(src):
|
||||
"""An HTML template with every {{/* … */}} action removed (R-555)."""
|
||||
return TEMPLATE_COMMENT_RE.sub(" ", src)
|
||||
|
||||
|
||||
def receiver_tokens(repo_root):
|
||||
@@ -479,7 +545,10 @@ def receiver_tokens(repo_root):
|
||||
p = os.path.join(dp, f)
|
||||
try:
|
||||
with open(p, encoding="utf-8", errors="replace") as fh:
|
||||
toks.update(TOKEN_RE.findall(fh.read()))
|
||||
src = fh.read()
|
||||
# R-555: comments are stripped first — a name in prose is not a decoder.
|
||||
src = strip_go_comments(src) if f.endswith(".go") else strip_template_comments(src)
|
||||
toks.update(TOKEN_RE.findall(src))
|
||||
except OSError as e:
|
||||
# never swallowed: an unreadable source file makes the answer unknown, not "absent"
|
||||
die("wire-contract gate INCONCLUSIVE: cannot read %s: %s" % (p, e))
|
||||
|
||||
Reference in New Issue
Block a user