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"]); ok = os.path.isfile(p) and os.path.getsize(p) > 012 checks.append({"path": d["path"], "exists": ok})13 if not ok: problems.append(f"missing or empty deliverable: out/{d['path']}"); continue14 kind = d.get("kind") or os.path.splitext(p)[1].lstrip(".")15 if kind in ("xlsx", "docx", "pptx"):16 try:17 with zipfile.ZipFile(p) as z: z.testzip()18 except Exception as e: # noqa: BLE00119 problems.append(f"out/{d['path']} is not a valid {kind} (zip) file: {e}"); continue20 if kind == "xlsx" and d.get("must_have_formulas"):21 try:22 import openpyxl23 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("="))24 if n == 0: problems.append(f"out/{d['path']} contains no formulas (the brief requires a working spreadsheet)")25 errs = 026 try:27 wb2 = openpyxl.load_workbook(p, data_only=True)28 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("#"))29 except Exception: pass # noqa: BLE00130 if errs: problems.append(f"out/{d['path']} has {errs} cells with error values (#REF!, #DIV/0!, ...)")31 except ImportError: pass32 if kind == "docx":33 try:34 import docx35 n = sum(len(par.text.split()) for par in docx.Document(p).paragraphs)36 if n < 50: problems.append(f"out/{d['path']} has only {n} words")37 except ImportError: pass38 if kind == "json":39 try: json.load(open(p, encoding="utf-8"))40 except Exception as e: problems.append(f"out/{d['path']} is not valid JSON: {e}") # noqa: BLE00141 cp = os.path.join(out, "COMPLETION.json")42 if os.path.isfile(cp):43 try:44 c = json.load(open(cp, encoding="utf-8"))45 for k in ("status", "summary", "deliverables"):46 if k not in c: problems.append(f"COMPLETION.json lacks '{k}'")47 if c.get("status") not in ("completed", "needs_clarification", "abstained"): problems.append("COMPLETION.json status must be completed | needs_clarification | abstained")48 except Exception as e: problems.append(f"COMPLETION.json invalid: {e}") # noqa: BLE00149 else: problems.append("missing out/COMPLETION.json")50 return {"ok": not problems, "problems": problems, "checks": checks}5152if __name__ == "__main__":53 ap = argparse.ArgumentParser(); ap.add_argument("--workspace", default="."); ap.add_argument("--json", action="store_true")54 a = ap.parse_args(); r = check(a.workspace)55 if a.json: print(json.dumps(r, indent=1))56 else:57 print("SELF-CHECK:", "OK" if r["ok"] else f"{len(r['problems'])} problem(s)")58 for p in r["problems"]: print(" -", p)59 sys.exit(0 if r["ok"] else 1)60