#!/usr/bin/env python3 """PUBLIC self-check (arm A4 and controller): verifies deliverable presence and well-formedness only. It knows nothing about the correct answers. Usage: python tools/selfcheck.py [--workspace .] [--json]""" from __future__ import annotations import argparse, json, os, sys, zipfile def check(workspace: str) -> dict: task = json.load(open(os.path.join(workspace, "task.json"), encoding="utf-8")) out = os.path.join(workspace, "out"); problems = []; checks = [] for d in task.get("required_deliverables", []): p = os.path.join(out, d["path"]) if d["path"].endswith("/") or d.get("kind") in ("dir", "directory"): 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) checks.append({"path": d["path"], "exists": ok}) if not ok: problems.append(f"missing or empty directory deliverable: out/{d['path']}") continue ok = os.path.isfile(p) and os.path.getsize(p) > 0 checks.append({"path": d["path"], "exists": ok}) if not ok: problems.append(f"missing or empty deliverable: out/{d['path']}"); continue kind = d.get("kind") or os.path.splitext(p)[1].lstrip(".") if kind in ("xlsx", "docx", "pptx"): try: with zipfile.ZipFile(p) as z: z.testzip() except Exception as e: # noqa: BLE001 problems.append(f"out/{d['path']} is not a valid {kind} (zip) file: {e}"); continue if kind == "xlsx" and d.get("must_have_formulas"): try: import openpyxl 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("=")) if n == 0: problems.append(f"out/{d['path']} contains no formulas (the brief requires a working spreadsheet)") errs = 0 try: wb2 = openpyxl.load_workbook(p, data_only=True) 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("#")) except Exception: pass # noqa: BLE001 if errs: problems.append(f"out/{d['path']} has {errs} cells with error values (#REF!, #DIV/0!, ...)") except ImportError: pass if kind == "docx": try: import docx n = sum(len(par.text.split()) for par in docx.Document(p).paragraphs) if n < 50: problems.append(f"out/{d['path']} has only {n} words") except ImportError: pass if kind == "json": try: json.load(open(p, encoding="utf-8")) except Exception as e: problems.append(f"out/{d['path']} is not valid JSON: {e}") # noqa: BLE001 cp = os.path.join(out, "COMPLETION.json") if os.path.isfile(cp): try: c = json.load(open(cp, encoding="utf-8")) for k in ("status", "summary", "deliverables"): if k not in c: problems.append(f"COMPLETION.json lacks '{k}'") if c.get("status") not in ("completed", "needs_clarification", "abstained"): problems.append("COMPLETION.json status must be completed | needs_clarification | abstained") except Exception as e: problems.append(f"COMPLETION.json invalid: {e}") # noqa: BLE001 else: problems.append("missing out/COMPLETION.json") return {"ok": not problems, "problems": problems, "checks": checks} if __name__ == "__main__": ap = argparse.ArgumentParser(); ap.add_argument("--workspace", default="."); ap.add_argument("--json", action="store_true") a = ap.parse_args(); r = check(a.workspace) if a.json: print(json.dumps(r, indent=1)) else: print("SELF-CHECK:", "OK" if r["ok"] else f"{len(r['problems'])} problem(s)") for p in r["problems"]: print(" -", p) sys.exit(0 if r["ok"] else 1)