Files
felhom.eu/documentation/audits/app-guides-2026-10-15/tools/validate_draft.py
T

106 lines
5.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""validate_draft.py — would this draft pass the catalog's copy gate? Checked on a THROWAWAY copy, never the repo.
python3 validate_draft.py <catalog-root> <draft.json>
Copies `templates/<app>/`, `scripts/` and the freeze into a temp directory, writes the guide there with
apply_guide.py, admits the app to the temp freeze, and runs check-copy-i18n.py --root <temp> <app>. Also checks the
night's format: 3-5 sections, each with a title, a one-sentence intro and 2-5 steps; 4-6 FAQ entries; no URL, no
markdown, no bare DOMAIN outside `<label>.DOMAIN`. Prints PASS or the findings; exit 0 / 1. The repo is only read.
"""
import io
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
def fmt_problems(d):
p = []
for lang in ("hu", "en"):
g = d.get(lang) or {}
secs, faq = g.get("sections") or [], g.get("faq") or []
if not 3 <= len(secs) <= 5:
p.append("%s: %d sections (format: 3-5)" % (lang, len(secs)))
if not 4 <= len(faq) <= 6:
p.append("%s: %d FAQ entries (format: 4-6)" % (lang, len(faq)))
for i, se in enumerate(secs):
if not se.get("intro"):
p.append("%s: sections[%d] has no intro (format: one sentence each)" % (lang, i))
if not 2 <= len(se.get("steps") or []) <= 5:
p.append("%s: sections[%d] has %d steps (format: 2-5)" % (lang, i, len(se.get("steps") or [])))
strings = []
for se in secs:
strings += [se.get("title", ""), se.get("intro", "")] + list(se.get("steps") or [])
for qa in faq:
strings += [qa.get("q", ""), qa.get("a", "")]
for s in strings:
if re.search(r"https?://|www\.", s):
p.append("%s: a URL in guide text (not allowed): %r" % (lang, s))
if re.search(r"\*\*|__|`|<[a-zA-Z/][^>]*>|^#|\[[^\]]+\]\(", s):
p.append("%s: markdown or HTML in guide text (rendered as plain text): %r" % (lang, s))
for m in re.finditer(r"\bDOMAIN\b", s):
if not re.search(r"[a-z0-9-]+\.DOMAIN\b", s[max(0, m.start() - 40):m.end()]):
p.append("%s: bare DOMAIN (the box fills it with the household's domain): %r" % (lang, s))
if "—" in s and lang == "hu":
p.append("hu: em dash (R-932): %r" % s)
if re.search(r"[\U0001F300-\U0001FAFF☀-➿]", s):
p.append("%s: emoji: %r" % (lang, s))
return p
def main(argv):
root, draft = os.path.abspath(argv[1]), os.path.abspath(argv[2])
d = json.load(io.open(draft, encoding="utf-8"))
app = d["app"]
probs = fmt_problems(d)
tmp = tempfile.mkdtemp(prefix="guide-validate-")
try:
shutil.copytree(os.path.join(root, "scripts"), os.path.join(tmp, "scripts"),
ignore=shutil.ignore_patterns("__pycache__"))
os.makedirs(os.path.join(tmp, "templates"))
for a in os.listdir(os.path.join(root, "templates")):
src = os.path.join(root, "templates", a)
if os.path.isdir(src):
shutil.copytree(src, os.path.join(tmp, "templates", a))
r = subprocess.run([sys.executable, os.path.join(HERE, "apply_guide.py"), tmp, draft], capture_output=True,
text=True)
if r.returncode != 0:
probs.append("apply: " + (r.stdout + r.stderr).strip())
else:
subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--add-app", app, "--reason", "validate_draft"], capture_output=True, text=True)
# The ratchet is about the whole catalog; measure it on the temp copy and hand it back.
probe = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--expect-missing", "-1"], capture_output=True, text=True)
m = re.search(r"(\d+) strings have no English", probe.stdout)
miss = m.group(1) if m else "0"
r = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "check-copy-i18n.py"), "--root", tmp,
"--expect-missing", miss, app], capture_output=True, text=True)
if r.returncode != 0:
probs += ["copy gate: " + l.strip() for l in r.stdout.splitlines() if l.strip().startswith("- ")
and app in l]
if not any(app in l for l in r.stdout.splitlines() if l.strip().startswith("- ")):
probs.append("copy gate failed:\n" + r.stdout[-1500:])
if miss != "0":
probs.append("English coverage: %s copy string(s) of the catalog would have no English" % miss)
finally:
shutil.rmtree(tmp, ignore_errors=True)
if probs:
print("FAIL %s" % app)
for x in probs:
print(" - " + x)
return 1
print("PASS %s — format and copy gate clean" % app)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))