bfd9b2a7ea
gates / gates (push) Successful in 6m38s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
106 lines
5.1 KiB
Python
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))
|