Files

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())