land_check.py
python
sha256:0e9549ec7b463911bc08b7d586dc320b1ac9b1f5c943ee7e3865dcc6cb0f6f83
chore(governance): sync handover+roadmap to 84db8c8 (drift:…
Human
9 hours ago
| 1 | """Close-ritual land check — compare require_paths to origin/main (never merge).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import hashlib |
| 6 | import subprocess |
| 7 | from dataclasses import dataclass |
| 8 | from pathlib import Path |
| 9 | from typing import Any, Callable |
| 10 | |
| 11 | from adapters.config import CloseRitualConfig, OverseerConfig |
| 12 | from adapters.factory import create_adapter |
| 13 | from adapters.runner import CommandRunner, SubprocessRunner |
| 14 | from tools.governance_freshness import check_governance_freshness |
| 15 | from tools.land_closeout import check_land_closeout |
| 16 | |
| 17 | |
| 18 | @dataclass(frozen=True) |
| 19 | class LandCheckResult: |
| 20 | """Outcome of ``ok land-check``.""" |
| 21 | |
| 22 | exit_code: int |
| 23 | landed: bool |
| 24 | mode: str |
| 25 | ref: str |
| 26 | paths: tuple[dict[str, Any], ...] |
| 27 | dirty_paths: tuple[str, ...] |
| 28 | messages: tuple[str, ...] |
| 29 | auto_merge: bool = False # always False — Tier 3 |
| 30 | |
| 31 | |
| 32 | def _sha256_bytes(data: bytes) -> str: |
| 33 | return hashlib.sha256(data).hexdigest() |
| 34 | |
| 35 | |
| 36 | def _git(repo_root: Path, *args: str) -> subprocess.CompletedProcess[str]: |
| 37 | return subprocess.run( |
| 38 | ["git", *args], |
| 39 | cwd=str(repo_root), |
| 40 | capture_output=True, |
| 41 | text=True, |
| 42 | ) |
| 43 | |
| 44 | |
| 45 | def compare_paths_to_main( |
| 46 | repo_root: Path, |
| 47 | paths: tuple[str, ...], |
| 48 | *, |
| 49 | remote: str, |
| 50 | main_branch: str, |
| 51 | ) -> tuple[str, list[dict[str, Any]], list[str]]: |
| 52 | """Return (ref, path_reports, dirty_paths).""" |
| 53 | ref = f"{remote}/{main_branch}" |
| 54 | if _git(repo_root, "rev-parse", "--verify", ref).returncode != 0: |
| 55 | if _git(repo_root, "rev-parse", "--verify", main_branch).returncode == 0: |
| 56 | ref = main_branch |
| 57 | else: |
| 58 | return ref, [], list(paths) |
| 59 | |
| 60 | reports: list[dict[str, Any]] = [] |
| 61 | for rel in paths: |
| 62 | wt_path = repo_root / rel |
| 63 | wt = _sha256_bytes(wt_path.read_bytes()) if wt_path.is_file() else None |
| 64 | shown = _git(repo_root, "show", f"{ref}:{rel}") |
| 65 | main_sha = _sha256_bytes(shown.stdout.encode("utf-8")) if shown.returncode == 0 else None |
| 66 | reports.append( |
| 67 | { |
| 68 | "path": rel, |
| 69 | "workingTreeSha256": wt, |
| 70 | "mainSha256": main_sha, |
| 71 | "match": wt is not None and main_sha is not None and wt == main_sha, |
| 72 | } |
| 73 | ) |
| 74 | |
| 75 | dirty = _git(repo_root, "status", "--porcelain", "--", *paths) |
| 76 | dirty_paths = [line[3:].strip() for line in dirty.stdout.splitlines() if line.strip()] |
| 77 | return ref, reports, dirty_paths |
| 78 | |
| 79 | |
| 80 | def run_land_check( |
| 81 | config: OverseerConfig, |
| 82 | repo_root: Path, |
| 83 | *, |
| 84 | mode: str | None = None, |
| 85 | emit: Callable[[str], None] | None = None, |
| 86 | runner: CommandRunner | None = None, |
| 87 | ) -> LandCheckResult: |
| 88 | """Run close_ritual land check. Never merges to main.""" |
| 89 | ritual: CloseRitualConfig = config.close_ritual |
| 90 | messages: list[str] = [] |
| 91 | |
| 92 | def _emit(line: str) -> None: |
| 93 | messages.append(line) |
| 94 | if emit: |
| 95 | emit(line) |
| 96 | |
| 97 | if not ritual.enabled: |
| 98 | _emit("close_ritual.enabled is false — land-check is a no-op (exit 0)") |
| 99 | return LandCheckResult( |
| 100 | exit_code=0, |
| 101 | landed=True, |
| 102 | mode=mode or ritual.mode, |
| 103 | ref="", |
| 104 | paths=(), |
| 105 | dirty_paths=(), |
| 106 | messages=tuple(messages), |
| 107 | ) |
| 108 | |
| 109 | effective_mode = mode or ritual.mode |
| 110 | if effective_mode not in {"verify_landed", "prepare_pr"}: |
| 111 | _emit(f"unsupported land-check mode: {effective_mode}") |
| 112 | return LandCheckResult( |
| 113 | exit_code=2, |
| 114 | landed=False, |
| 115 | mode=effective_mode, |
| 116 | ref="", |
| 117 | paths=(), |
| 118 | dirty_paths=(), |
| 119 | messages=tuple(messages), |
| 120 | ) |
| 121 | |
| 122 | if ritual.consumer_verify_script: |
| 123 | script = repo_root / ritual.consumer_verify_script |
| 124 | if not script.is_file(): |
| 125 | _emit(f"consumer_verify_script missing: {ritual.consumer_verify_script}") |
| 126 | return LandCheckResult( |
| 127 | exit_code=1, |
| 128 | landed=False, |
| 129 | mode=effective_mode, |
| 130 | ref="", |
| 131 | paths=(), |
| 132 | dirty_paths=(), |
| 133 | messages=tuple(messages), |
| 134 | ) |
| 135 | completed = subprocess.run( |
| 136 | ["python3", str(script)], |
| 137 | cwd=str(repo_root), |
| 138 | capture_output=True, |
| 139 | text=True, |
| 140 | ) |
| 141 | out = (completed.stdout or completed.stderr or "").strip() |
| 142 | if out: |
| 143 | _emit(out) |
| 144 | if completed.returncode != 0: |
| 145 | _emit( |
| 146 | "note: ok land-check never merges; use ok pr-land --authorized for wait-for-green land" |
| 147 | ) |
| 148 | return LandCheckResult( |
| 149 | exit_code=completed.returncode, |
| 150 | landed=False, |
| 151 | mode=effective_mode, |
| 152 | ref="consumer_script", |
| 153 | paths=(), |
| 154 | dirty_paths=(), |
| 155 | messages=tuple(messages), |
| 156 | ) |
| 157 | freshness_fail = _freshness_gate( |
| 158 | config, |
| 159 | repo_root, |
| 160 | mode=effective_mode, |
| 161 | runner=runner, |
| 162 | emit=_emit, |
| 163 | messages=messages, |
| 164 | ) |
| 165 | if freshness_fail is not None: |
| 166 | return freshness_fail |
| 167 | closeout_fail = _closeout_gate( |
| 168 | config, |
| 169 | repo_root, |
| 170 | mode=effective_mode, |
| 171 | runner=runner, |
| 172 | emit=_emit, |
| 173 | messages=messages, |
| 174 | ) |
| 175 | if closeout_fail is not None: |
| 176 | return closeout_fail |
| 177 | _emit("note: ok land-check never merges; use ok pr-land --authorized for wait-for-green land") |
| 178 | return LandCheckResult( |
| 179 | exit_code=0, |
| 180 | landed=True, |
| 181 | mode=effective_mode, |
| 182 | ref="consumer_script", |
| 183 | paths=(), |
| 184 | dirty_paths=(), |
| 185 | messages=tuple(messages), |
| 186 | ) |
| 187 | |
| 188 | paths = ritual.require_paths |
| 189 | if not paths: |
| 190 | _emit("close_ritual.require_paths is empty — configure paths or consumer_verify_script") |
| 191 | return LandCheckResult( |
| 192 | exit_code=2, |
| 193 | landed=False, |
| 194 | mode=effective_mode, |
| 195 | ref="", |
| 196 | paths=(), |
| 197 | dirty_paths=(), |
| 198 | messages=tuple(messages), |
| 199 | ) |
| 200 | |
| 201 | remote = config.vcs.git.remote |
| 202 | main_branch = config.vcs.git.main_branch |
| 203 | ref, reports, dirty_paths = compare_paths_to_main( |
| 204 | repo_root, |
| 205 | paths, |
| 206 | remote=remote, |
| 207 | main_branch=main_branch, |
| 208 | ) |
| 209 | all_match = all(r.get("match") for r in reports) and not dirty_paths |
| 210 | |
| 211 | if effective_mode == "prepare_pr": |
| 212 | if dirty_paths: |
| 213 | _emit("prepare_pr: dirty require_paths — commit before opening PR") |
| 214 | for d in dirty_paths: |
| 215 | _emit(f" dirty: {d}") |
| 216 | _emit( |
| 217 | 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' |
| 218 | ) |
| 219 | return LandCheckResult( |
| 220 | exit_code=1, |
| 221 | landed=False, |
| 222 | mode=effective_mode, |
| 223 | ref=ref, |
| 224 | paths=tuple(reports), |
| 225 | dirty_paths=tuple(dirty_paths), |
| 226 | messages=tuple(messages), |
| 227 | ) |
| 228 | freshness_fail = _freshness_gate( |
| 229 | config, |
| 230 | repo_root, |
| 231 | mode=effective_mode, |
| 232 | runner=runner, |
| 233 | emit=_emit, |
| 234 | messages=messages, |
| 235 | ) |
| 236 | if freshness_fail is not None: |
| 237 | return freshness_fail |
| 238 | _emit("prepare_pr: require_paths clean — push feature branch and open PR") |
| 239 | _emit( |
| 240 | 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' |
| 241 | ) |
| 242 | return LandCheckResult( |
| 243 | exit_code=0, |
| 244 | landed=False, |
| 245 | mode=effective_mode, |
| 246 | ref=ref, |
| 247 | paths=tuple(reports), |
| 248 | dirty_paths=(), |
| 249 | messages=tuple(messages), |
| 250 | ) |
| 251 | |
| 252 | # verify_landed |
| 253 | if not all_match: |
| 254 | _emit(f"verify_landed: FAIL — paths do not match {ref}") |
| 255 | for r in reports: |
| 256 | if not r.get("match"): |
| 257 | _emit(f" mismatch: {r['path']}") |
| 258 | for d in dirty_paths: |
| 259 | _emit(f" dirty: {d}") |
| 260 | _emit( |
| 261 | 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' |
| 262 | ) |
| 263 | return LandCheckResult( |
| 264 | exit_code=1, |
| 265 | landed=False, |
| 266 | mode=effective_mode, |
| 267 | ref=ref, |
| 268 | paths=tuple(reports), |
| 269 | dirty_paths=tuple(dirty_paths), |
| 270 | messages=tuple(messages), |
| 271 | ) |
| 272 | |
| 273 | freshness_fail = _freshness_gate( |
| 274 | config, |
| 275 | repo_root, |
| 276 | mode=effective_mode, |
| 277 | runner=runner, |
| 278 | emit=_emit, |
| 279 | messages=messages, |
| 280 | ) |
| 281 | if freshness_fail is not None: |
| 282 | return freshness_fail |
| 283 | |
| 284 | closeout_fail = _closeout_gate( |
| 285 | config, |
| 286 | repo_root, |
| 287 | mode=effective_mode, |
| 288 | runner=runner, |
| 289 | emit=_emit, |
| 290 | messages=messages, |
| 291 | ) |
| 292 | if closeout_fail is not None: |
| 293 | return closeout_fail |
| 294 | |
| 295 | _emit(f"verify_landed: PASS — require_paths match {ref}") |
| 296 | _emit("Tier 3 land complete once paths match; further merges use ok pr-land --authorized") |
| 297 | return LandCheckResult( |
| 298 | exit_code=0, |
| 299 | landed=True, |
| 300 | mode=effective_mode, |
| 301 | ref=ref, |
| 302 | paths=tuple(reports), |
| 303 | dirty_paths=(), |
| 304 | messages=tuple(messages), |
| 305 | ) |
| 306 | |
| 307 | |
| 308 | def _freshness_gate( |
| 309 | config: OverseerConfig, |
| 310 | repo_root: Path, |
| 311 | *, |
| 312 | mode: str, |
| 313 | runner: CommandRunner | None, |
| 314 | emit: Callable[[str], None], |
| 315 | messages: list[str], |
| 316 | ) -> LandCheckResult | None: |
| 317 | """§GFG.6: when close_ritual is enabled, fail land-check on stale freshness (exit 2).""" |
| 318 | active_runner = runner or SubprocessRunner() |
| 319 | adapter = create_adapter(config, repo_root, runner=active_runner) |
| 320 | report = check_governance_freshness( |
| 321 | config, |
| 322 | repo_root, |
| 323 | adapter=adapter, |
| 324 | runner=active_runner, |
| 325 | ) |
| 326 | if report.ok: |
| 327 | return None |
| 328 | emit(f"governance_freshness: {report.state} — {report.message}") |
| 329 | if report.remediation: |
| 330 | emit(f"governance_freshness-remediation: {report.remediation}") |
| 331 | emit("land-check refused — freshness gate (never merges)") |
| 332 | return LandCheckResult( |
| 333 | exit_code=2, |
| 334 | landed=False, |
| 335 | mode=mode, |
| 336 | ref="", |
| 337 | paths=(), |
| 338 | dirty_paths=(), |
| 339 | messages=tuple(messages), |
| 340 | ) |
| 341 | |
| 342 | |
| 343 | def _closeout_gate( |
| 344 | config: OverseerConfig, |
| 345 | repo_root: Path, |
| 346 | *, |
| 347 | mode: str, |
| 348 | runner: CommandRunner | None, |
| 349 | emit: Callable[[str], None], |
| 350 | messages: list[str], |
| 351 | ) -> LandCheckResult | None: |
| 352 | """§PMHF.6.2: ``landed=True`` only when closeout is ``complete``/``not_applicable``. |
| 353 | |
| 354 | ``land_a_in_progress``, ``post_merge_incomplete``, ``land_b_in_progress``, and |
| 355 | ``unreadable`` all refuse landed (exit 2) with remediation — never merges. |
| 356 | """ |
| 357 | active_runner = runner or SubprocessRunner() |
| 358 | report = check_land_closeout( |
| 359 | config, |
| 360 | repo_root, |
| 361 | runner=active_runner, |
| 362 | probe_merged_pr=config.vcs.regime != "muse-only", |
| 363 | ) |
| 364 | if report.state in {"complete", "not_applicable"}: |
| 365 | return None |
| 366 | emit(f"land_closeout: {report.state} — {report.message}") |
| 367 | if report.remediation: |
| 368 | emit(f"land_closeout-remediation: {report.remediation}") |
| 369 | emit("land-check refused — land closeout incomplete (never merges)") |
| 370 | return LandCheckResult( |
| 371 | exit_code=2, |
| 372 | landed=False, |
| 373 | mode=mode, |
| 374 | ref="", |
| 375 | paths=(), |
| 376 | dirty_paths=(), |
| 377 | messages=tuple(messages), |
| 378 | ) |
File History
1 commit
sha256:6abcf1fa82a7a621ccbc945f19acdba5bc0db54569599404a1452fb4a096a199
fix(ISR): default require_independent_second_reviewer to require
Human
minor
⚠
8 hours ago