#!/usr/bin/env python3 """ build_calibration_decision_draft_v1.py ─────────────────────────────────────────────────────────────────────────────── calibration_review_report_v1.json / calibration_approval_list_v1.json을 바탕으로 운영 승인 초안(APPROVE / HOLD / REJECT)을 만든다. 목적: - 사람 검토 전 단계에서 결정 초안을 자동 생성 - source=PROVISIONAL은 원칙적으로 APPROVE - PROVISIONAL_CANDIDATE는 HOLD - 나머지는 REJECT 또는 HOLD로 사유를 명시 출력: Temp/calibration_decision_draft_v1.json Temp/calibration_decision_draft_v1.md 사용법: python tools/build_calibration_decision_draft_v1.py """ from __future__ import annotations import json import sys from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parent.parent REVIEW = ROOT / "Temp" / "calibration_review_report_v1.json" APPROVAL = ROOT / "Temp" / "calibration_approval_list_v1.json" OUT_JSON = ROOT / "Temp" / "calibration_decision_draft_v1.json" OUT_MD = ROOT / "Temp" / "calibration_decision_draft_v1.md" if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1) def _load_json(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return data if isinstance(data, dict) else {} def _table(rows: list[dict[str, Any]], keys: list[str], max_rows: int = 25) -> str: if not rows: return "_데이터 없음_" header = "| " + " | ".join(keys) + " |" sep = "| " + " | ".join(["---"] * len(keys)) + " |" body = [] for row in rows[:max_rows]: body.append("| " + " | ".join(str(row.get(k, "")).replace("|", "ㅣ") for k in keys) + " |") suffix = f"\n\n_...총 {len(rows)}행 중 {max_rows}행 표시_" if len(rows) > max_rows else "" return "\n".join([header, sep, *body]) + suffix def _decide(row: dict[str, Any]) -> tuple[str, str]: source = str(row.get("source") or "") readiness = str(row.get("readiness") or "") sample_n = int(row.get("sample_n") or 0) if source == "PROVISIONAL" and sample_n >= 30: return "APPROVE", "source=PROVISIONAL and sample_n>=30" if source == "PROVISIONAL": return "APPROVE", "source=PROVISIONAL" if readiness == "PROVISIONAL_CANDIDATE": return "HOLD", "Needs provisional review" if sample_n >= 10: return "HOLD", "Sample present but not provisional" return "REJECT", "Insufficient evidence" def main() -> int: review = _load_json(REVIEW) approval = _load_json(APPROVAL) review_rows = review.get("review_rows") if isinstance(review.get("review_rows"), list) else [] decisions: list[dict[str, Any]] = [] summary = {"APPROVE": 0, "HOLD": 0, "REJECT": 0} for row in review_rows: if not isinstance(row, dict): continue decision, reason = _decide(row) item = { "id": row.get("id", ""), "source": row.get("source", ""), "sample_n": int(row.get("sample_n") or 0), "value": row.get("value"), "unit": row.get("unit", ""), "owner_formula": row.get("owner_formula", ""), "readiness": row.get("readiness", ""), "decision": decision, "reason": reason, } decisions.append(item) summary[decision] += 1 decisions.sort(key=lambda item: ({"APPROVE": 0, "HOLD": 1, "REJECT": 2}.get(str(item.get("decision") or ""), 3), -int(item.get("sample_n") or 0), str(item.get("id") or ""))) report = { "formula_id": "CALIBRATION_DECISION_DRAFT_V1", "generated_at": datetime.now(timezone.utc).isoformat(), "review_report_path": str(REVIEW), "approval_list_path": str(APPROVAL), "summary": summary, "decision_count": len(decisions), "decisions": decisions, "approval_candidate_count": int(approval.get("approval_candidate_count") or 0), } OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") md_lines = [ "# Calibration Decision Draft", "", "## Summary", "", f"- APPROVE: {summary['APPROVE']}", f"- HOLD: {summary['HOLD']}", f"- REJECT: {summary['REJECT']}", f"- decision_count: {len(decisions)}", "", "## Decision Table", "", _table(decisions, ["id", "source", "sample_n", "decision", "reason", "owner_formula", "readiness"]), "", "## Evidence", "", f"- review report: {REVIEW}", f"- approval list: {APPROVAL}", ] OUT_MD.write_text("\n".join(md_lines), encoding="utf-8") print(json.dumps({ "formula_id": report["formula_id"], "gate": "PASS" if summary["APPROVE"] else "WARN", "approve_count": summary["APPROVE"], "hold_count": summary["HOLD"], "reject_count": summary["REJECT"], "json_path": str(OUT_JSON), "md_path": str(OUT_MD), }, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())