dragon_rollout_governance_check.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. from __future__ import annotations
  2. from datetime import datetime
  3. import json
  4. from pathlib import Path
  5. import pandas as pd
  6. from dragon_rollout_governance import (
  7. FORWARD_OK,
  8. HOLD_AND_REVIEW,
  9. ROLLBACK_REVIEW_REQUIRED,
  10. evaluate_rollout,
  11. gate_rows,
  12. )
  13. def _load_json(path: Path) -> dict[str, object]:
  14. if not path.exists():
  15. return {}
  16. return json.loads(path.read_text(encoding="utf-8"))
  17. def _load_csv(path: Path) -> pd.DataFrame:
  18. if not path.exists():
  19. return pd.DataFrame()
  20. return pd.read_csv(path, encoding="utf-8-sig")
  21. def main() -> None:
  22. base_dir = Path(__file__).resolve().parent
  23. manifest_path = base_dir / "dragon_daily_rc1_manifest.json"
  24. monitor_history_path = base_dir / "dragon_monitor_history.csv"
  25. divergence_log_path = base_dir / "dragon_branch_divergence_log.csv"
  26. monitor_health_path = base_dir / "dragon_monitor_health_report.md"
  27. manifest = _load_json(manifest_path)
  28. monitor_history = _load_csv(monitor_history_path)
  29. divergence_log = _load_csv(divergence_log_path)
  30. decision, gates, reasons, active_branch = evaluate_rollout(
  31. manifest=manifest,
  32. monitor_history=monitor_history,
  33. divergence_log=divergence_log,
  34. monitor_health_report_exists=monitor_health_path.exists(),
  35. )
  36. fallback_branch = "alpha_first_selective_veto"
  37. latest_bar_date = str(manifest.get("latest_bar_date", ""))
  38. request_date = str(manifest.get("as_of_request_date", ""))
  39. generated_at = datetime.now().isoformat(timespec="seconds")
  40. snapshot = pd.DataFrame(gate_rows(gates))
  41. snapshot.to_csv(base_dir / "dragon_rollout_governance_snapshot.csv", index=False, encoding="utf-8-sig")
  42. gate_status_counts = snapshot["status"].value_counts().to_dict() if not snapshot.empty else {}
  43. decision_payload = {
  44. "generated_at": generated_at,
  45. "request_date": request_date,
  46. "latest_bar_date": latest_bar_date,
  47. "release_version": manifest.get("release_version", "RC1"),
  48. "candidate_branch": manifest.get("branch", "alpha_first_glued_refined_hot_cap"),
  49. "fallback_branch": fallback_branch,
  50. "active_branch": active_branch,
  51. "decision": decision,
  52. "gate_status_counts": gate_status_counts,
  53. "reasons": reasons,
  54. }
  55. (base_dir / "dragon_rollout_state.json").write_text(
  56. json.dumps(decision_payload, indent=2, ensure_ascii=False) + "\n",
  57. encoding="utf-8",
  58. )
  59. lines = [
  60. "# Dragon Rollout Governance Report",
  61. "",
  62. f"- generated_at: `{generated_at}`",
  63. f"- request_date: `{request_date}`",
  64. f"- latest_bar_date: `{latest_bar_date}`",
  65. f"- candidate_branch: `{decision_payload['candidate_branch']}`",
  66. f"- fallback_branch: `{fallback_branch}`",
  67. f"- decision: `{decision}`",
  68. f"- active_branch: `{active_branch}`",
  69. "",
  70. "## Gate Summary",
  71. f"- ok: `{int(gate_status_counts.get('ok', 0))}`",
  72. f"- warning: `{int(gate_status_counts.get('warning', 0))}`",
  73. f"- hard_fail: `{int(gate_status_counts.get('hard_fail', 0))}`",
  74. "",
  75. "## Gate Details",
  76. ]
  77. if snapshot.empty:
  78. lines.append("- No gate rows were generated.")
  79. else:
  80. for _, row in snapshot.iterrows():
  81. lines.extend(
  82. [
  83. f"### {row['gate']}",
  84. f"- status: `{row['status']}`",
  85. f"- value: `{row['value']}`",
  86. f"- threshold: `{row['threshold']}`",
  87. f"- detail: {row['detail']}",
  88. f"- action: {row['action']}",
  89. "",
  90. ]
  91. )
  92. if reasons:
  93. lines.extend(
  94. [
  95. "## Decision Reasons",
  96. *(f"- `{item}`" for item in reasons),
  97. "",
  98. ]
  99. )
  100. lines.extend(
  101. [
  102. "## Artifacts",
  103. "- `dragon_rollout_state.json`",
  104. "- `dragon_rollout_governance_snapshot.csv`",
  105. "- `dragon_rollout_rollback_runbook.md`",
  106. ]
  107. )
  108. (base_dir / "dragon_rollout_governance_report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  109. runbook_lines = [
  110. "# Dragon Rollback Runbook",
  111. "",
  112. "## Trigger Conditions",
  113. "- Any gate in `dragon_rollout_governance_snapshot.csv` with `status = hard_fail`.",
  114. "- Decision equals `ROLLBACK_REVIEW_REQUIRED`.",
  115. "- Persistent warning state (`HOLD_AND_REVIEW`) for more than two bars.",
  116. "",
  117. "## Immediate Actions",
  118. "1. Freeze candidate branch decisions for live-facing guidance.",
  119. "2. Switch active branch to control branch `alpha_first_selective_veto`.",
  120. "3. Re-run forward chain and verify monitor status:",
  121. " `py -3 dragon_forward_observation_pipeline.py`",
  122. "4. Inspect latest artifacts:",
  123. " - `dragon_daily_monitor_snapshot.csv`",
  124. " - `dragon_monitor_health_report.md`",
  125. " - `dragon_branch_divergence_report.md`",
  126. "",
  127. "## Recovery Checklist",
  128. "1. Confirm all hard-fail gates are cleared.",
  129. "2. Confirm warning streaks return to threshold range.",
  130. "3. Confirm branch divergence drops to `none` or `mild`.",
  131. "4. Regenerate rollout report and ensure decision is `FORWARD_OK` before reactivating candidate branch.",
  132. "",
  133. "## Current Snapshot",
  134. f"- latest_bar_date: `{latest_bar_date}`",
  135. f"- decision: `{decision}`",
  136. f"- recommended_active_branch: `{active_branch}`",
  137. f"- fallback_branch: `{fallback_branch}`",
  138. ]
  139. (base_dir / "dragon_rollout_rollback_runbook.md").write_text("\n".join(runbook_lines) + "\n", encoding="utf-8")
  140. if decision == FORWARD_OK:
  141. return
  142. if decision == HOLD_AND_REVIEW:
  143. return
  144. if decision == ROLLBACK_REVIEW_REQUIRED:
  145. return
  146. if __name__ == "__main__":
  147. main()