check.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Post-merge land closeout probe (§PMHF.5) — never merges, never writes docs. |
| 2 | |
| 3 | Composes the GFG freshness probe (D1/D2/marker authority) with the handover |
| 4 | ``land-phase`` posture (§PMHF.4) to resolve one of the frozen states: |
| 5 | ``not_applicable`` | ``land_a_in_progress`` | ``post_merge_incomplete`` | |
| 6 | ``land_b_in_progress`` | ``complete`` | ``unreadable``. |
| 7 | """ |
| 8 | |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import json |
| 12 | from dataclasses import dataclass |
| 13 | from pathlib import Path |
| 14 | |
| 15 | from adapters.base import VcsAdapter |
| 16 | from adapters.config import OverseerConfig, resolve_lane_docs |
| 17 | from adapters.factory import create_adapter |
| 18 | from adapters.runner import CommandRunner, SubprocessRunner |
| 19 | from cli.docs_paths import lane_living_doc_abs |
| 20 | from cli.version_lock import LockError, lock_path, read_version_lock |
| 21 | from tools.governance_freshness import ( |
| 22 | GovernanceFreshnessReport, |
| 23 | check_governance_freshness, |
| 24 | ) |
| 25 | from tools.governance_hygiene.next_regen import ( |
| 26 | LAND_B_REMEDIATION, |
| 27 | LAND_PHASE_A, |
| 28 | LAND_PHASE_B, |
| 29 | LAND_PHASE_UNREADABLE, |
| 30 | extract_land_id, |
| 31 | extract_paste_pr_number, |
| 32 | land_queue_conflict, |
| 33 | resolve_land_phase, |
| 34 | ) |
| 35 | |
| 36 | STATE_NOT_APPLICABLE = "not_applicable" |
| 37 | STATE_LAND_A_IN_PROGRESS = "land_a_in_progress" |
| 38 | STATE_POST_MERGE_INCOMPLETE = "post_merge_incomplete" |
| 39 | STATE_LAND_B_IN_PROGRESS = "land_b_in_progress" |
| 40 | STATE_COMPLETE = "complete" |
| 41 | STATE_UNREADABLE = "unreadable" |
| 42 | |
| 43 | CONFLICT_TOKEN = "land_phase_conflicts_queue_done" |
| 44 | LAND_B_FINISH_REMEDIATION = ( |
| 45 | "finish land-b: ok governance-sync --dry-run then apply; " |
| 46 | "clear land-phase once NEXT matches merged main" |
| 47 | ) |
| 48 | |
| 49 | |
| 50 | @dataclass(frozen=True) |
| 51 | class LandCloseoutReport: |
| 52 | """Result of a land-closeout probe (§PMHF.5).""" |
| 53 | |
| 54 | state: str |
| 55 | message: str |
| 56 | remediation: str | None |
| 57 | land_phase: str | None # land-a | land-b | None |
| 58 | freshness_ok: bool |
| 59 | d1: str | None |
| 60 | optional_pr_merged: bool | None # None = not probed |
| 61 | |
| 62 | @property |
| 63 | def ok(self) -> bool: |
| 64 | """Status-floor semantics: mid-land wait must not fail the tree (§PMHF.5).""" |
| 65 | return self.state in { |
| 66 | STATE_NOT_APPLICABLE, |
| 67 | STATE_LAND_A_IN_PROGRESS, |
| 68 | STATE_COMPLETE, |
| 69 | } |
| 70 | |
| 71 | |
| 72 | def land_complete(report: LandCloseoutReport) -> bool: |
| 73 | """§PMHF.3.3 — stricter than ``report.ok``; ``land_a_in_progress`` is never complete.""" |
| 74 | return report.freshness_ok and ( |
| 75 | report.state == STATE_COMPLETE |
| 76 | or (report.state == STATE_NOT_APPLICABLE and report.freshness_ok) |
| 77 | ) |
| 78 | |
| 79 | |
| 80 | def check_land_closeout( |
| 81 | config: OverseerConfig, |
| 82 | repo_root: Path, |
| 83 | *, |
| 84 | adapter: VcsAdapter | None = None, |
| 85 | runner: CommandRunner | None = None, |
| 86 | probe_merged_pr: bool = False, |
| 87 | freshness: GovernanceFreshnessReport | None = None, |
| 88 | ) -> LandCloseoutReport: |
| 89 | """Resolve land closeout per §PMHF.5.2. Reuses the GFG probe; no re-derivation. |
| 90 | |
| 91 | ``freshness`` may be injected by callers (``ok status``) that already ran the |
| 92 | GFG probe. ``probe_merged_pr`` is the only path that may invoke ``gh`` |
| 93 | (§PMHF.5.3); it is skipped for ``muse-only`` regardless of the flag. |
| 94 | """ |
| 95 | try: |
| 96 | lock_file = lock_path(repo_root) |
| 97 | if not lock_file.is_file(): |
| 98 | return LandCloseoutReport( |
| 99 | state=STATE_NOT_APPLICABLE, |
| 100 | message="not initialized — no land closeout obligation yet", |
| 101 | remediation=None, |
| 102 | land_phase=None, |
| 103 | freshness_ok=True, |
| 104 | d1=None, |
| 105 | optional_pr_merged=None, |
| 106 | ) |
| 107 | read_version_lock(lock_file) |
| 108 | except LockError as exc: |
| 109 | return _unreadable(f"version.lock unreadable: {exc}", remediation="ok sync") |
| 110 | except OSError as exc: |
| 111 | return _unreadable(f"could not load install state: {exc}", remediation="ok sync") |
| 112 | |
| 113 | active_runner = runner or SubprocessRunner() |
| 114 | if freshness is None: |
| 115 | active_adapter = adapter or create_adapter(config, repo_root, runner=active_runner) |
| 116 | freshness = check_governance_freshness( |
| 117 | config, |
| 118 | repo_root, |
| 119 | adapter=active_adapter, |
| 120 | runner=active_runner, |
| 121 | ) |
| 122 | |
| 123 | if freshness.state == "unreadable": |
| 124 | # §PMHF.5.2 step 3 / R2-M2: never mask unreadable as post_merge_incomplete. |
| 125 | return _unreadable( |
| 126 | f"governance freshness unreadable — failing closed: {freshness.message}", |
| 127 | remediation=freshness.remediation, |
| 128 | freshness_ok=False, |
| 129 | d1=freshness.d1, |
| 130 | ) |
| 131 | |
| 132 | try: |
| 133 | handover_text, roadmap_text = _load_default_lane_docs(config, repo_root) |
| 134 | except OSError as exc: |
| 135 | return _unreadable( |
| 136 | f"governance docs unreadable: {exc}", |
| 137 | remediation="ok governance-sync --dry-run", |
| 138 | freshness_ok=freshness.ok, |
| 139 | d1=freshness.d1, |
| 140 | ) |
| 141 | |
| 142 | land_phase = resolve_land_phase(handover_text) |
| 143 | if land_phase == LAND_PHASE_UNREADABLE: |
| 144 | return _unreadable( |
| 145 | "land-phase unreadable — unknown marker value or conflicting " |
| 146 | "land-a/land-b vocabulary in the paste fence", |
| 147 | remediation="fix the handover NEXT marker land-phase attribute", |
| 148 | freshness_ok=freshness.ok, |
| 149 | d1=freshness.d1, |
| 150 | ) |
| 151 | |
| 152 | main_branch = config.vcs.git.main_branch or "main" |
| 153 | if land_phase == LAND_PHASE_A: |
| 154 | land_id = extract_land_id(handover_text) |
| 155 | if land_id and land_queue_conflict(roadmap_text, land_id, main_branch=main_branch): |
| 156 | # §PMHF.3.3 frozen check: queue row DONE/MERGED while handover says land-a. |
| 157 | return LandCloseoutReport( |
| 158 | state=STATE_UNREADABLE, |
| 159 | message=( |
| 160 | f"{CONFLICT_TOKEN} — matching land queue row is DONE/MERGED " |
| 161 | "while handover land-phase=land-a" |
| 162 | ), |
| 163 | remediation=LAND_B_REMEDIATION, |
| 164 | land_phase=LAND_PHASE_A, |
| 165 | freshness_ok=freshness.ok, |
| 166 | d1=freshness.d1, |
| 167 | optional_pr_merged=None, |
| 168 | ) |
| 169 | |
| 170 | if land_phase is None: |
| 171 | return LandCloseoutReport( |
| 172 | state=STATE_NOT_APPLICABLE, |
| 173 | message="no land posture on NEXT — closeout not applicable", |
| 174 | remediation=None, |
| 175 | land_phase=None, |
| 176 | freshness_ok=freshness.ok, |
| 177 | d1=freshness.d1, |
| 178 | optional_pr_merged=None, |
| 179 | ) |
| 180 | |
| 181 | optional_pr_merged: bool | None = None |
| 182 | if probe_merged_pr and config.vcs.regime != "muse-only": |
| 183 | optional_pr_merged = _probe_merged_pr(handover_text, active_runner) |
| 184 | |
| 185 | if land_phase == LAND_PHASE_A: |
| 186 | if ( |
| 187 | freshness.d1 == "drifted" |
| 188 | or freshness.state in {"drifted", "stale_marker"} |
| 189 | or optional_pr_merged is True |
| 190 | ): |
| 191 | return LandCloseoutReport( |
| 192 | state=STATE_POST_MERGE_INCOMPLETE, |
| 193 | message=( |
| 194 | "merge reflected on main but handover NEXT still says land-a " |
| 195 | "(wait-for-merge)" |
| 196 | ), |
| 197 | remediation=LAND_B_REMEDIATION, |
| 198 | land_phase=LAND_PHASE_A, |
| 199 | freshness_ok=freshness.ok, |
| 200 | d1=freshness.d1, |
| 201 | optional_pr_merged=optional_pr_merged, |
| 202 | ) |
| 203 | return LandCloseoutReport( |
| 204 | state=STATE_LAND_A_IN_PROGRESS, |
| 205 | message="land-a in progress — waiting for Tier 3 merge; main tip still aligned", |
| 206 | remediation=None, |
| 207 | land_phase=LAND_PHASE_A, |
| 208 | freshness_ok=freshness.ok, |
| 209 | d1=freshness.d1, |
| 210 | optional_pr_merged=optional_pr_merged, |
| 211 | ) |
| 212 | |
| 213 | # land-b |
| 214 | if not freshness.ok or freshness.d1 == "drifted": |
| 215 | return LandCloseoutReport( |
| 216 | state=STATE_LAND_B_IN_PROGRESS, |
| 217 | message="land-b in progress — living docs not yet synced to merged main", |
| 218 | remediation=LAND_B_FINISH_REMEDIATION, |
| 219 | land_phase=LAND_PHASE_B, |
| 220 | freshness_ok=freshness.ok, |
| 221 | d1=freshness.d1, |
| 222 | optional_pr_merged=optional_pr_merged, |
| 223 | ) |
| 224 | return LandCloseoutReport( |
| 225 | state=STATE_COMPLETE, |
| 226 | message="land closeout complete — freshness ok and land-b posture satisfied", |
| 227 | remediation=None, |
| 228 | land_phase=LAND_PHASE_B, |
| 229 | freshness_ok=freshness.ok, |
| 230 | d1=freshness.d1, |
| 231 | optional_pr_merged=optional_pr_merged, |
| 232 | ) |
| 233 | |
| 234 | |
| 235 | def _unreadable( |
| 236 | message: str, |
| 237 | *, |
| 238 | remediation: str | None, |
| 239 | freshness_ok: bool = False, |
| 240 | d1: str | None = None, |
| 241 | ) -> LandCloseoutReport: |
| 242 | return LandCloseoutReport( |
| 243 | state=STATE_UNREADABLE, |
| 244 | message=message, |
| 245 | remediation=remediation, |
| 246 | land_phase=None, |
| 247 | freshness_ok=freshness_ok, |
| 248 | d1=d1, |
| 249 | optional_pr_merged=None, |
| 250 | ) |
| 251 | |
| 252 | |
| 253 | def _probe_merged_pr(handover_text: str, runner: CommandRunner) -> bool | None: |
| 254 | """§PMHF.5.3 optional enrichment; gh missing/failed → None (never fail open).""" |
| 255 | number = extract_paste_pr_number(handover_text) |
| 256 | if number is None: |
| 257 | return None |
| 258 | command = f"gh pr view {number} --json state,mergedAt" |
| 259 | result = runner.run(command) |
| 260 | if not result.ok: |
| 261 | return None |
| 262 | try: |
| 263 | payload = json.loads(result.stdout) |
| 264 | except json.JSONDecodeError: |
| 265 | return None |
| 266 | if not isinstance(payload, dict): |
| 267 | return None |
| 268 | state = str(payload.get("state", "")).strip().upper() |
| 269 | if state == "MERGED" or payload.get("mergedAt"): |
| 270 | return True |
| 271 | return False |
| 272 | |
| 273 | |
| 274 | def _load_default_lane_docs(config: OverseerConfig, repo_root: Path) -> tuple[str, str]: |
| 275 | lane = config.docs.default_lane if config.docs.lanes is not None else None |
| 276 | lane_docs = resolve_lane_docs(config, lane) |
| 277 | handover_path = lane_living_doc_abs(repo_root, config, lane_docs, lane_docs.handover) |
| 278 | roadmap_path = lane_living_doc_abs(repo_root, config, lane_docs, lane_docs.roadmap) |
| 279 | handover = handover_path.read_text(encoding="utf-8") if handover_path.is_file() else "" |
| 280 | roadmap = roadmap_path.read_text(encoding="utf-8") if roadmap_path.is_file() else "" |
| 281 | return handover, roadmap |
| 282 | |
| 283 | |
| 284 | def land_closeout_payload(report: LandCloseoutReport) -> dict: |
| 285 | """JSON payload shape shared by ``ok status`` and ``ok land-closeout`` (§PMHF.6.1).""" |
| 286 | return { |
| 287 | "state": report.state, |
| 288 | "ok": report.ok, |
| 289 | "message": report.message, |
| 290 | "remediation": report.remediation, |
| 291 | "land_phase": report.land_phase, |
| 292 | "freshness_ok": report.freshness_ok, |
| 293 | "d1": report.d1, |
| 294 | "optional_pr_merged": report.optional_pr_merged, |
| 295 | } |