145 lines
4.0 KiB
Python
145 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
draft_verifier_v1.py — Verify draft artifacts against naming + evidence rules.
|
|
|
|
Measured checks:
|
|
1. Filename matches the convention: <slug>-<type>-<date>.(md|txt)
|
|
2. File contains a VERDICT: line.
|
|
3. File contains a CHECK: line with a non-empty command.
|
|
4. File is non-empty (> 50 bytes).
|
|
5. File mentions at least one measured/claimed tag.
|
|
|
|
Usage:
|
|
python3 draft_verifier_v1.py <path> [<path> ...]
|
|
python3 draft_verifier_v1.py /root/troupe-standup/wizard-work/quill/
|
|
|
|
Exit codes:
|
|
0 all files pass
|
|
1 one or more files fail
|
|
2 no files found
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
APPROVED_TYPES = {
|
|
"brief", "template", "runbook", "rfc", "sop", "checklist"
|
|
}
|
|
|
|
FAILED: list[str] = []
|
|
PASSED: list[str] = []
|
|
|
|
|
|
def check_name(path: Path) -> list[str]:
|
|
errors = []
|
|
stem = path.stem # no extension
|
|
parts = stem.split("-")
|
|
if len(parts) < 3:
|
|
errors.append("filename must have at least <slug>-<type>-<date>")
|
|
return errors
|
|
*slug_parts, typ, date_str = parts
|
|
slug = "-".join(slug_parts)
|
|
if not slug:
|
|
errors.append("slug is empty")
|
|
if typ not in APPROVED_TYPES:
|
|
errors.append(f"type '{typ}' not in approved list {sorted(APPROVED_TYPES)}")
|
|
if not re.fullmatch(r"\d{8}", date_str):
|
|
errors.append(f"date '{date_str}' is not YYYYMMDD")
|
|
return errors
|
|
|
|
|
|
def check_verdict(content: str) -> list[str]:
|
|
errors = []
|
|
if not re.search(r"(?m)^VERDICT:", content):
|
|
errors.append("missing VERDICT: line")
|
|
if not re.search(r"(?m)^CHECK:", content):
|
|
errors.append("missing CHECK: line")
|
|
# Ensure CHECK has a real command, not just a tautology
|
|
check_match = re.search(r"(?m)^CHECK:\s*(.+)", content)
|
|
if check_match:
|
|
cmd = check_match.group(1).strip()
|
|
if not cmd:
|
|
errors.append("CHECK: command is empty")
|
|
elif cmd in {"true", "false", ":", "exit 0", "exit 1"}:
|
|
errors.append(f"CHECK: command '{cmd}' is a tautology")
|
|
return errors
|
|
|
|
|
|
def check_size(path: Path) -> list[str]:
|
|
errors = []
|
|
if path.stat().st_size <= 50:
|
|
errors.append("file is too short (<= 50 bytes)")
|
|
return errors
|
|
|
|
|
|
def check_evidence(content: str) -> list[str]:
|
|
errors = []
|
|
has_measured = bool(re.search(r"(?i)\bMEASURED\b", content))
|
|
has_claimed = bool(re.search(r"(?i)\bCLAIMED\b", content))
|
|
if not (has_measured or has_claimed):
|
|
errors.append("no MEASURED or CLAIMED tags found")
|
|
return errors
|
|
|
|
|
|
def verify_file(path: Path) -> bool:
|
|
try:
|
|
content = path.read_text(encoding="utf-8", errors="replace")
|
|
except Exception as e:
|
|
FAILED.append(f"{path}: unreadable ({e})")
|
|
return False
|
|
|
|
checks = [
|
|
("naming", check_name(path)),
|
|
("verdict", check_verdict(content)),
|
|
("size", check_size(path)),
|
|
("evidence", check_evidence(content)),
|
|
]
|
|
|
|
ok = True
|
|
for label, errs in checks:
|
|
for e in errs:
|
|
FAILED.append(f"{path}: [{label}] {e}")
|
|
ok = False
|
|
if ok:
|
|
PASSED.append(str(path))
|
|
return ok
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 draft_verifier_v1.py <path> [<path> ...]", file=sys.stderr)
|
|
return 2
|
|
|
|
targets = [Path(p) for p in sys.argv[1:]]
|
|
files: list[Path] = []
|
|
for t in targets:
|
|
if t.is_file():
|
|
files.append(t)
|
|
elif t.is_dir():
|
|
files.extend(sorted(t.rglob("*.md")))
|
|
files.extend(sorted(t.rglob("*.txt")))
|
|
else:
|
|
FAILED.append(f"{t}: not found")
|
|
|
|
if not files:
|
|
print("draft_verifier_v1: no files to verify", file=sys.stderr)
|
|
return 2
|
|
|
|
all_ok = True
|
|
for f in files:
|
|
if not verify_file(f):
|
|
all_ok = False
|
|
|
|
print(f"Checked: {len(files)} Passed: {len(PASSED)} Failed: {len(FAILED)}")
|
|
if FAILED:
|
|
print("-- FAILURES --")
|
|
for line in FAILED:
|
|
print(line)
|
|
return 0 if all_ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|