1#!/usr/bin/env python32"""PUBLIC self-check (arm A4 and controller): verifies deliverable presence and well-formedness only. It knows nothing about the3correct answers. Usage: python tools/selfcheck.py [--workspace .] [--json]"""4from __future__ import annotations5import argparse, json, os, sys, zipfile67def check(workspace: str) -> dict:8 task = json.load(open(os.path.join(workspace, "task.json"), encoding="utf-8"))9 out = os.path.join(workspace, "out"); problems = []; checks = []10 for d in task.get("required_deliverables", []):11 p = os.path.join(out, d["path"])12 if d["path"].endswith("/") or d.get("kind") in ("dir", "directory"):13 ok = os.path.isdir(p) and any(os.path.isfile(os.path.join(dp, f)) for dp, _, fs in os.walk(p) for f in fs)14 checks.append({"path": d["path"], "exists": ok})15 if not ok: problems.append(f"missing or empty directory deliverable: out/{d['path']}")16 continue17 ok = os.path.isfile(p) and os.path.getsize(p) > 018 checks.append({"path": d["path"], "exists": ok})19 if not ok: problems.append(f"missing or empty deliverable: out/{d['path']}"); continue20 kind = d.get("kind") or os.path.splitext(p)[1].lstrip(".")21 if kind in ("xlsx", "docx", "pptx"):22 try:23 with zipfile.ZipFile(p) as z: z.testzip()24 except Exception as e: # noqa: BLE00125 problems.append(f"out/{d['path']} is not a valid {kind} (zip) file: {e}"); continue26 if kind == "xlsx" and d.get("must_have_formulas"):27 try:28 import openpyxl29 wb = openpyxl.load_workbook(p); n = sum(1 for ws in wb.worksheets for row in ws.iter_rows() for c in row if isinstance(c.value, str) and c.value.startswith("="))30 if n == 0: problems.append(f"out/{d['path']} contains no formulas (the brief requires a working spreadsheet)")31 errs = 032 try:33 wb2 = openpyxl.load_workbook(p, data_only=True)34 errs = sum(1 for ws in wb2.worksheets for row in ws.iter_rows() for c in row if isinstance(c.value, str) and c.value.startswith("#"))35 except Exception: pass # noqa: BLE00136 if errs: problems.append(f"out/{d['path']} has {errs} cells with error values (#REF!, #DIV/0!, ...)")37 except ImportError: pass38 if kind == "docx":39 try:40 import docx41 n = sum(len(par.text.split()) for par in docx.Document(p).paragraphs)42 if n < 50: problems.append(f"out/{d['path']} has only {n} words")43 except ImportError: pass44 if kind == "json":45 try: json.load(open(p, encoding="utf-8"))46 except Exception as e: problems.append(f"out/{d['path']} is not valid JSON: {e}") # noqa: BLE00147 cp = os.path.join(out, "COMPLETION.json")48 if os.path.isfile(cp):49 try:50 c = json.load(open(cp, encoding="utf-8"))51 for k in ("status", "summary", "deliverables"):52 if k not in c: problems.append(f"COMPLETION.json lacks '{k}'")53 if c.get("status") not in ("completed", "needs_clarification", "abstained"): problems.append("COMPLETION.json status must be completed | needs_clarification | abstained")54 except Exception as e: problems.append(f"COMPLETION.json invalid: {e}") # noqa: BLE00155 else: problems.append("missing out/COMPLETION.json")56 return {"ok": not problems, "problems": problems, "checks": checks}5758if __name__ == "__main__":59 ap = argparse.ArgumentParser(); ap.add_argument("--workspace", default="."); ap.add_argument("--json", action="store_true")60 a = ap.parse_args(); r = check(a.workspace)61 if a.json: print(json.dumps(r, indent=1))62 else:63 print("SELF-CHECK:", "OK" if r["ok"] else f"{len(r['problems'])} problem(s)")64 for p in r["problems"]: print(" -", p)65 sys.exit(0 if r["ok"] else 1)66