status.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """``overseer status`` command (§K4.4).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from argparse import Namespace |
| 6 | from pathlib import Path |
| 7 | |
| 8 | from adapters.config import load_config |
| 9 | from adapters.errors import ConfigError, ReadError |
| 10 | from cli.context import CliContext |
| 11 | from cli.digest import sha256_hex |
| 12 | from cli.drift import compute_drift |
| 13 | from cli.footprint import resolve_footprint |
| 14 | from cli.kit_root import kit_version |
| 15 | from cli.output import CommandReport |
| 16 | from cli.paths import is_within_repo, resolve_config_path, resolve_repo_root |
| 17 | from cli.sanitize import format_config_error, sanitize_text |
| 18 | from cli.vcs_status import read_vcs_status, vcs_report |
| 19 | from cli.version_lock import LockError, lock_path, read_version_lock |
| 20 | from cli.commands.route import routing_policy_status |
| 21 | from tools.cost_awareness.format import format_cost_awareness_lines |
| 22 | from tools.cost_awareness.surface import build_cost_awareness_report, cost_awareness_payload |
| 23 | from adapters.factory import create_adapter |
| 24 | from tools.footprint_coverage import check_footprint_coverage |
| 25 | from tools.footprint_integrity import check_footprint_integrity |
| 26 | from tools.governance_freshness import check_governance_freshness |
| 27 | from tools.governance_gates import scan_governance_gates |
| 28 | from tools.governance_gates.format import format_pending_gate_lines, pending_gates_payload |
| 29 | from tools.land_closeout import check_land_closeout, land_closeout_payload |
| 30 | from tools.muse_sync import check_muse_sync |
| 31 | from tools.substrate_health import check_substrate |
| 32 | from tools.verification_evidence_gate import ( |
| 33 | build_verification_evidence_gate, |
| 34 | format_verification_evidence_gate_line, |
| 35 | verification_evidence_gate_payload, |
| 36 | ) |
| 37 | from tools.independent_second_reviewer import ( |
| 38 | build_independent_second_reviewer_gate, |
| 39 | format_independent_second_reviewer_gate_line, |
| 40 | independent_second_reviewer_gate_payload, |
| 41 | ) |
| 42 | from tools.optional_feature_tips import ( |
| 43 | build_optional_feature_tips, |
| 44 | optional_feature_tips_payload, |
| 45 | ) |
| 46 | from tools.workspace import build_status_report |
| 47 | from tools.workspace.board_names import status_board_name_advisory |
| 48 | |
| 49 | |
| 50 | GOVERNANCE_SYNC_MARKER = "last_governance_sync" |
| 51 | IDE_WORKSPACE_HINT = ( |
| 52 | "ide: open the repo root (folder containing .overseer) so .cursor/rules load" |
| 53 | ) |
| 54 | |
| 55 | |
| 56 | def _read_governance_sync_marker(repo_root: Path) -> str | None: |
| 57 | """Return timestamp line only (backward-compatible; tip fields live under governance_freshness).""" |
| 58 | marker = repo_root / ".overseer" / GOVERNANCE_SYNC_MARKER |
| 59 | if not marker.is_file(): |
| 60 | return None |
| 61 | text = marker.read_text(encoding="utf-8") |
| 62 | line = text.splitlines()[0].strip() if text.strip() else "" |
| 63 | return line or None |
| 64 | |
| 65 | |
| 66 | def _governance_freshness_payload(report) -> dict: |
| 67 | return { |
| 68 | "state": report.state, |
| 69 | "ok": report.ok, |
| 70 | "message": report.message, |
| 71 | "remediation": report.remediation, |
| 72 | "d1": report.d1, |
| 73 | "d2": report.d2, |
| 74 | "marker_present": report.marker_present, |
| 75 | "marker_r1": report.marker_r1, |
| 76 | "actual_r1": report.actual_r1, |
| 77 | } |
| 78 | |
| 79 | |
| 80 | def _compute_footprint_integrity( |
| 81 | repo_root: Path, |
| 82 | lock, |
| 83 | rendered, |
| 84 | ) -> tuple[str, list[str]]: |
| 85 | """Recompute kit-only digest; report preserved-living paths separately (§K6.4).""" |
| 86 | from cli.version_lock import ORIGIN_PRESERVED, compute_lock_digest, entry_origin |
| 87 | |
| 88 | prior = {entry.path: entry for entry in lock.footprint} |
| 89 | preserved_living: list[str] = [] |
| 90 | kit_entries = [] |
| 91 | for item in rendered: |
| 92 | entry = prior.get(item.destination) |
| 93 | origin = entry_origin(entry) if entry is not None else "kit" |
| 94 | dest = repo_root / item.destination |
| 95 | if dest.is_file(): |
| 96 | content = dest.read_bytes() |
| 97 | else: |
| 98 | content = b"" |
| 99 | digest_hex = sha256_hex(content) |
| 100 | if origin == ORIGIN_PRESERVED: |
| 101 | preserved_living.append(item.destination) |
| 102 | continue |
| 103 | from cli.version_lock import FootprintEntry, ORIGIN_KIT |
| 104 | |
| 105 | kit_entries.append( |
| 106 | FootprintEntry( |
| 107 | path=item.destination, |
| 108 | source=item.source, |
| 109 | sha256=digest_hex, |
| 110 | origin=ORIGIN_KIT, |
| 111 | ) |
| 112 | ) |
| 113 | computed = compute_lock_digest(kit_entries) |
| 114 | integrity = "ok" if computed == lock.footprint_digest else "mismatch" |
| 115 | return integrity, preserved_living |
| 116 | |
| 117 | |
| 118 | def _lock_summary(lock) -> dict: |
| 119 | return { |
| 120 | "lock_version": lock.lock_version, |
| 121 | "kit_version": lock.kit_version, |
| 122 | "config_version": lock.config_version, |
| 123 | "footprint_digest": lock.footprint_digest, |
| 124 | "installed_at": lock.installed_at, |
| 125 | "synced_at": lock.synced_at, |
| 126 | } |
| 127 | |
| 128 | |
| 129 | def _exit_code_from_conditions( |
| 130 | *, |
| 131 | config_error: bool, |
| 132 | integrity: str | None, |
| 133 | drift_status: str | None, |
| 134 | use_exit_code: bool, |
| 135 | substrate_ok: bool = True, |
| 136 | muse_sync_ok: bool = True, |
| 137 | footprint_self_integrity_ok: bool = True, |
| 138 | footprint_coverage_ok: bool = True, |
| 139 | verification_evidence_gate_ok: bool = True, |
| 140 | independent_second_reviewer_gate_ok: bool = True, |
| 141 | governance_freshness_ok: bool = True, |
| 142 | land_closeout_ok: bool = True, |
| 143 | workspace_ok: bool = True, |
| 144 | ) -> int: |
| 145 | """Apply frozen precedence: 2 > 6 > 35 > 3 > 0 (§MR.7.2). |
| 146 | |
| 147 | §KH2.5: ``muse_sync_ok`` folds into the 2 tier. §KH3.5: ``footprint_self_integrity_ok`` |
| 148 | (declared-but-absent kit-owned files) also folds into the same 2 tier, distinct from and |
| 149 | independent of the opt-in ``--check-footprint`` content-digest ``integrity`` tier (6). |
| 150 | §GFG.6: ``governance_freshness_ok`` (D1/D2 drift or stale marker) folds into the same 2 tier. |
| 151 | §PMHF.6.1: ``land_closeout_ok`` folds into the same 2 tier; ``land_a_in_progress`` |
| 152 | has ``ok=True`` so waiting for merge never false-fails status. |
| 153 | §MR.7.2: workspace relay failure is ``35`` and overrides drift ``3`` / clean ``0`` without |
| 154 | overriding config/substrate/muse_sync/footprint-self-integrity/governance-freshness (``2``) |
| 155 | or lock integrity (``6``). |
| 156 | """ |
| 157 | if not use_exit_code: |
| 158 | return 0 |
| 159 | if ( |
| 160 | config_error |
| 161 | or not substrate_ok |
| 162 | or not muse_sync_ok |
| 163 | or not footprint_self_integrity_ok |
| 164 | or not footprint_coverage_ok |
| 165 | or not verification_evidence_gate_ok |
| 166 | or not independent_second_reviewer_gate_ok |
| 167 | or not governance_freshness_ok |
| 168 | or not land_closeout_ok |
| 169 | ): |
| 170 | return 2 |
| 171 | if integrity == "mismatch": |
| 172 | return 6 |
| 173 | if not workspace_ok: |
| 174 | return 35 |
| 175 | if drift_status in {"behind", "ahead"}: |
| 176 | return 3 |
| 177 | return 0 |
| 178 | |
| 179 | |
| 180 | def run_status(args: Namespace, ctx: CliContext) -> int: |
| 181 | """Execute ``overseer status``.""" |
| 182 | report = CommandReport() |
| 183 | repo_root = resolve_repo_root(cwd=ctx.cwd, repo_arg=args.repo, command="status") |
| 184 | overseer_dir = repo_root / ".overseer" |
| 185 | |
| 186 | if not overseer_dir.is_dir(): |
| 187 | payload = { |
| 188 | "initialized": False, |
| 189 | "warnings": [], |
| 190 | } |
| 191 | if ctx.output.json_mode: |
| 192 | ctx.output.emit_json(payload) |
| 193 | else: |
| 194 | ctx.output.emit("not initialized") |
| 195 | return 0 |
| 196 | |
| 197 | config_path = resolve_config_path(repo_root, args.config) |
| 198 | if not is_within_repo(repo_root, config_path): |
| 199 | ctx.output.error("refused: config path outside repo root") |
| 200 | return 4 |
| 201 | |
| 202 | config_error = False |
| 203 | lock_error = False |
| 204 | |
| 205 | try: |
| 206 | config = load_config(config_path) |
| 207 | except ConfigError as exc: |
| 208 | config_error = True |
| 209 | ctx.output.error(format_config_error(exc, repo_root)) |
| 210 | payload = { |
| 211 | "initialized": True, |
| 212 | "error": str(exc), |
| 213 | "warnings": report.warnings, |
| 214 | } |
| 215 | if ctx.output.json_mode: |
| 216 | ctx.output.emit_json(payload) |
| 217 | return 2 |
| 218 | |
| 219 | substrate = check_substrate(config, repo_root) |
| 220 | if not substrate.ok: |
| 221 | report.add_warning(f"substrate: {substrate.state} — {substrate.message}") |
| 222 | if substrate.remediation: |
| 223 | report.add_warning(f"substrate-remediation: {substrate.remediation}") |
| 224 | |
| 225 | gate_scan = None |
| 226 | if config.governance_gates.remind and "status" in config.governance_gates.surfaces: |
| 227 | gate_scan = scan_governance_gates(config, repo_root) |
| 228 | if gate_scan.pending: |
| 229 | for line in format_pending_gate_lines(gate_scan): |
| 230 | report.add_warning(line) |
| 231 | |
| 232 | lock_file = lock_path(repo_root) |
| 233 | lock = None |
| 234 | try: |
| 235 | lock = read_version_lock(lock_file) |
| 236 | except LockError as exc: |
| 237 | lock_error = True |
| 238 | report.add_warning(str(exc)) |
| 239 | |
| 240 | rendered: list = [] |
| 241 | integrity: str | None = None |
| 242 | preserved_living: list[str] = [] |
| 243 | if lock is not None: |
| 244 | try: |
| 245 | rendered = resolve_footprint(config, kit=ctx.kit) |
| 246 | except ConfigError as exc: |
| 247 | config_error = True |
| 248 | ctx.output.error(format_config_error(exc, repo_root)) |
| 249 | return 2 |
| 250 | |
| 251 | if args.check_footprint: |
| 252 | integrity, preserved_living = _compute_footprint_integrity(repo_root, lock, rendered) |
| 253 | if integrity == "mismatch": |
| 254 | report.add_warning("footprint_integrity: mismatch") |
| 255 | for path in preserved_living: |
| 256 | report.add_warning(f"preserved-living: {path}") |
| 257 | |
| 258 | footprint_self_integrity = check_footprint_integrity(repo_root, lock=lock) |
| 259 | if not footprint_self_integrity.ok: |
| 260 | report.add_warning( |
| 261 | f"footprint_self_integrity: {footprint_self_integrity.state} — " |
| 262 | f"{footprint_self_integrity.message}" |
| 263 | ) |
| 264 | if footprint_self_integrity.remediation: |
| 265 | report.add_warning( |
| 266 | f"footprint_self_integrity-remediation: {footprint_self_integrity.remediation}" |
| 267 | ) |
| 268 | |
| 269 | footprint_coverage = check_footprint_coverage( |
| 270 | repo_root, |
| 271 | config, |
| 272 | lock=lock, |
| 273 | rendered=rendered if rendered else None, |
| 274 | kit=ctx.kit, |
| 275 | ) |
| 276 | if not footprint_coverage.ok: |
| 277 | report.add_warning( |
| 278 | f"footprint_coverage: {footprint_coverage.state} — {footprint_coverage.message}" |
| 279 | ) |
| 280 | if footprint_coverage.remediation: |
| 281 | report.add_warning( |
| 282 | f"footprint_coverage-remediation: {footprint_coverage.remediation}" |
| 283 | ) |
| 284 | |
| 285 | drift = compute_drift( |
| 286 | cli_version=kit_version(), |
| 287 | lock=lock, |
| 288 | rendered=rendered, |
| 289 | repo_root=repo_root, |
| 290 | ) |
| 291 | if drift["status"] in {"behind", "ahead"}: |
| 292 | report.add_warning(f"drift: {drift['status']}") |
| 293 | |
| 294 | vcs_result = read_vcs_status(config, repo_root, ctx.runner) |
| 295 | if isinstance(vcs_result, ReadError): |
| 296 | ctx.output.error(sanitize_text(str(vcs_result), repo_root)) |
| 297 | payload = { |
| 298 | "initialized": True, |
| 299 | "substrate": _substrate_payload(substrate), |
| 300 | "vcs": { |
| 301 | "error": sanitize_text(str(vcs_result), repo_root), |
| 302 | "command": sanitize_text(vcs_result.command, repo_root), |
| 303 | }, |
| 304 | "warnings": report.warnings, |
| 305 | } |
| 306 | if ctx.output.json_mode: |
| 307 | ctx.output.emit_json(payload) |
| 308 | return 2 |
| 309 | |
| 310 | muse_sync = check_muse_sync(config, vcs_result) |
| 311 | if not muse_sync.ok: |
| 312 | report.add_warning(f"muse_sync: {muse_sync.state} — {muse_sync.message}") |
| 313 | if muse_sync.remediation: |
| 314 | report.add_warning(f"muse_sync-remediation: {muse_sync.remediation}") |
| 315 | |
| 316 | governance_freshness = check_governance_freshness( |
| 317 | config, |
| 318 | repo_root, |
| 319 | adapter=create_adapter(config, repo_root, runner=ctx.runner), |
| 320 | runner=ctx.runner, |
| 321 | ) |
| 322 | if not governance_freshness.ok: |
| 323 | report.add_warning( |
| 324 | f"governance_freshness: {governance_freshness.state} — {governance_freshness.message}" |
| 325 | ) |
| 326 | if governance_freshness.remediation: |
| 327 | report.add_warning( |
| 328 | f"governance_freshness-remediation: {governance_freshness.remediation}" |
| 329 | ) |
| 330 | |
| 331 | # §PMHF.6.1: always-on land closeout probe; never invokes gh from status. |
| 332 | land_closeout = check_land_closeout( |
| 333 | config, |
| 334 | repo_root, |
| 335 | runner=ctx.runner, |
| 336 | probe_merged_pr=False, |
| 337 | freshness=governance_freshness, |
| 338 | ) |
| 339 | if not land_closeout.ok: |
| 340 | report.add_warning(f"land_closeout: {land_closeout.state} — {land_closeout.message}") |
| 341 | if land_closeout.remediation: |
| 342 | report.add_warning(f"land_closeout-remediation: {land_closeout.remediation}") |
| 343 | |
| 344 | model_routing_status = routing_policy_status(config, repo_root, kit_root=ctx.kit) |
| 345 | if model_routing_status.get("enabled") and not model_routing_status.get("valid"): |
| 346 | violation = model_routing_status.get("violation") or "invalid" |
| 347 | report.add_warning(f"model_routing: invalid — {violation}") |
| 348 | |
| 349 | cost_report = None |
| 350 | if config.cost_awareness.enabled and "status" in config.cost_awareness.surfaces: |
| 351 | cost_report = build_cost_awareness_report(config, repo_root, kit_root=ctx.kit) |
| 352 | if cost_report.invalid: |
| 353 | violation = cost_report.violation or "invalid" |
| 354 | report.add_warning(f"cost_awareness: invalid — {violation}") |
| 355 | elif cost_report.exit_code == 31: |
| 356 | ctx.output.error(cost_report.violation or "routing policy file missing or unreadable") |
| 357 | payload = { |
| 358 | "initialized": True, |
| 359 | "cost_awareness": cost_awareness_payload(cost_report), |
| 360 | "warnings": report.warnings, |
| 361 | } |
| 362 | if ctx.output.json_mode: |
| 363 | ctx.output.emit_json(payload) |
| 364 | return 31 |
| 365 | else: |
| 366 | for line in format_cost_awareness_lines(cost_report): |
| 367 | report.add_warning(line) |
| 368 | |
| 369 | handover_path = repo_root / config.repo.root_relative_docs / config.docs.handover |
| 370 | roadmap_path = repo_root / config.repo.root_relative_docs / config.docs.roadmap |
| 371 | handover_text = handover_path.read_text(encoding="utf-8") if handover_path.is_file() else None |
| 372 | roadmap_text = roadmap_path.read_text(encoding="utf-8") if roadmap_path.is_file() else None |
| 373 | verification_evidence_gate = build_verification_evidence_gate( |
| 374 | config, |
| 375 | repo_root, |
| 376 | handover_text=handover_text, |
| 377 | roadmap_text=roadmap_text, |
| 378 | ) |
| 379 | ve_line = format_verification_evidence_gate_line(verification_evidence_gate) |
| 380 | if ve_line: |
| 381 | report.add_warning(ve_line) |
| 382 | |
| 383 | independent_second_reviewer_gate = build_independent_second_reviewer_gate( |
| 384 | config, |
| 385 | repo_root, |
| 386 | handover_text=handover_text, |
| 387 | roadmap_text=roadmap_text, |
| 388 | ) |
| 389 | isr_line = format_independent_second_reviewer_gate_line(independent_second_reviewer_gate) |
| 390 | if isr_line: |
| 391 | report.add_warning(isr_line) |
| 392 | |
| 393 | optional_feature_tips = build_optional_feature_tips(config) |
| 394 | |
| 395 | # §NXP.6 — warn only; must not fold into --exit-code. |
| 396 | board_name_advisory = status_board_name_advisory(config) |
| 397 | if board_name_advisory is not None: |
| 398 | report.add_warning(board_name_advisory) |
| 399 | |
| 400 | payload = { |
| 401 | "initialized": True, |
| 402 | "kit_version": kit_version(), |
| 403 | "substrate": _substrate_payload(substrate), |
| 404 | "muse_sync": _muse_sync_payload(muse_sync), |
| 405 | "footprint_self_integrity": _footprint_self_integrity_payload(footprint_self_integrity), |
| 406 | "footprint_coverage": _footprint_coverage_payload(footprint_coverage), |
| 407 | "ide_workspace_hint": IDE_WORKSPACE_HINT, |
| 408 | "optional_feature_tips": optional_feature_tips_payload(optional_feature_tips), |
| 409 | "governance_freshness": _governance_freshness_payload(governance_freshness), |
| 410 | "land_closeout": land_closeout_payload(land_closeout), |
| 411 | "lock": _lock_summary(lock) if lock else None, |
| 412 | "drift": drift, |
| 413 | "footprint_integrity": integrity, |
| 414 | "preserved_living": preserved_living if args.check_footprint else [], |
| 415 | "vcs": vcs_report(vcs_result, config), |
| 416 | "last_governance_sync": _read_governance_sync_marker(repo_root), |
| 417 | "governance_gates": pending_gates_payload(gate_scan) |
| 418 | if gate_scan is not None |
| 419 | else {"enabled": False, "suppressed": False, "active_phases": [], "pending": []}, |
| 420 | "model_routing": model_routing_status, |
| 421 | "warnings": report.warnings, |
| 422 | "board_name_advisory": board_name_advisory, |
| 423 | } |
| 424 | if cost_report is not None: |
| 425 | payload["cost_awareness"] = cost_awareness_payload(cost_report) |
| 426 | ve_payload = verification_evidence_gate_payload(verification_evidence_gate) |
| 427 | if ve_payload is not None: |
| 428 | payload["verification_evidence_gate"] = ve_payload |
| 429 | isr_payload = independent_second_reviewer_gate_payload(independent_second_reviewer_gate) |
| 430 | if isr_payload is not None: |
| 431 | payload["independent_second_reviewer_gate"] = isr_payload |
| 432 | if lock_error: |
| 433 | payload["lock_error"] = True |
| 434 | |
| 435 | workspace_report = None |
| 436 | workspace_ok = True |
| 437 | if getattr(args, "workspace", False): |
| 438 | workspace_report = build_status_report(config, repo_root) |
| 439 | payload["workspace"] = workspace_report.to_json() |
| 440 | # S9: single-repo green must not imply workspace.ok; workspace failure |
| 441 | # contributes exit 35 only when --exit-code is set. |
| 442 | if workspace_report.configured and not workspace_report.ok: |
| 443 | workspace_ok = False |
| 444 | report.add_warning(f"workspace: {workspace_report.state}") |
| 445 | |
| 446 | exit_code = _exit_code_from_conditions( |
| 447 | config_error=config_error, |
| 448 | integrity=integrity if args.check_footprint else None, |
| 449 | drift_status=drift["status"], |
| 450 | use_exit_code=args.exit_code, |
| 451 | substrate_ok=substrate.ok, |
| 452 | muse_sync_ok=muse_sync.ok, |
| 453 | footprint_self_integrity_ok=footprint_self_integrity.ok, |
| 454 | footprint_coverage_ok=footprint_coverage.ok, |
| 455 | verification_evidence_gate_ok=verification_evidence_gate.ok, |
| 456 | independent_second_reviewer_gate_ok=independent_second_reviewer_gate.ok, |
| 457 | governance_freshness_ok=governance_freshness.ok, |
| 458 | land_closeout_ok=land_closeout.ok, |
| 459 | workspace_ok=workspace_ok, |
| 460 | ) |
| 461 | if lock_error and args.exit_code: |
| 462 | exit_code = 6 if exit_code in {0, 3, 35} else max(exit_code, 6) |
| 463 | |
| 464 | if ctx.output.json_mode: |
| 465 | ctx.output.emit_json(payload) |
| 466 | else: |
| 467 | ctx.output.emit(f"kit_version: {payload['kit_version']}") |
| 468 | if not substrate.ok: |
| 469 | ctx.output.emit(f"substrate: {substrate.state} — {substrate.message}") |
| 470 | if substrate.remediation: |
| 471 | ctx.output.emit(f"substrate-remediation: {substrate.remediation}") |
| 472 | if not muse_sync.ok: |
| 473 | ctx.output.emit(f"muse_sync: {muse_sync.state} — {muse_sync.message}") |
| 474 | if muse_sync.remediation: |
| 475 | ctx.output.emit(f"muse_sync-remediation: {muse_sync.remediation}") |
| 476 | if not governance_freshness.ok: |
| 477 | ctx.output.emit( |
| 478 | f"governance_freshness: {governance_freshness.state} — " |
| 479 | f"{governance_freshness.message}" |
| 480 | ) |
| 481 | if governance_freshness.remediation: |
| 482 | ctx.output.emit( |
| 483 | f"governance_freshness-remediation: {governance_freshness.remediation}" |
| 484 | ) |
| 485 | if not land_closeout.ok: |
| 486 | ctx.output.emit( |
| 487 | f"land_closeout: {land_closeout.state} — {land_closeout.message}" |
| 488 | ) |
| 489 | if land_closeout.remediation: |
| 490 | ctx.output.emit(f"land_closeout-remediation: {land_closeout.remediation}") |
| 491 | if lock: |
| 492 | ctx.output.emit(f"lock kit_version: {lock.kit_version}") |
| 493 | ctx.output.emit(f"drift: {drift['status']}") |
| 494 | if integrity: |
| 495 | ctx.output.emit(f"footprint_integrity: {integrity}") |
| 496 | if model_routing_status.get("enabled"): |
| 497 | if model_routing_status.get("valid"): |
| 498 | ctx.output.emit("model_routing: valid") |
| 499 | else: |
| 500 | violation = model_routing_status.get("violation") or "invalid" |
| 501 | ctx.output.emit(f"model_routing: invalid — {violation}") |
| 502 | if gate_scan is not None and gate_scan.pending: |
| 503 | ctx.output.emit("") |
| 504 | for line in format_pending_gate_lines(gate_scan): |
| 505 | ctx.output.emit(line) |
| 506 | if cost_report is not None and config.cost_awareness.enabled: |
| 507 | if cost_report.invalid: |
| 508 | violation = cost_report.violation or "invalid" |
| 509 | ctx.output.emit(f"cost_awareness: invalid — {violation}") |
| 510 | else: |
| 511 | for line in format_cost_awareness_lines(cost_report): |
| 512 | ctx.output.emit(line) |
| 513 | if not footprint_coverage.ok: |
| 514 | ctx.output.emit( |
| 515 | f"footprint_coverage: {footprint_coverage.state} — {footprint_coverage.message}" |
| 516 | ) |
| 517 | if footprint_coverage.remediation: |
| 518 | ctx.output.emit( |
| 519 | f"footprint_coverage-remediation: {footprint_coverage.remediation}" |
| 520 | ) |
| 521 | ctx.output.emit(IDE_WORKSPACE_HINT) |
| 522 | for line in optional_feature_tips: |
| 523 | ctx.output.emit(line) |
| 524 | if board_name_advisory is not None: |
| 525 | ctx.output.emit(board_name_advisory) |
| 526 | if ve_line: |
| 527 | ctx.output.emit(ve_line) |
| 528 | ctx.output.emit(f"vcs.regime: {vcs_result.regime}") |
| 529 | ctx.output.emit(f"vcs.branch: {vcs_result.branch}") |
| 530 | ctx.output.emit(f"vcs.dirty: {vcs_result.dirty}") |
| 531 | if workspace_report is not None: |
| 532 | ctx.output.emit(f"workspace.configured: {str(workspace_report.configured).lower()}") |
| 533 | ctx.output.emit(f"workspace.ok: {str(workspace_report.ok).lower()}") |
| 534 | ctx.output.emit(f"workspace.state: {workspace_report.state}") |
| 535 | if workspace_report.authoritative_handover: |
| 536 | ctx.output.emit( |
| 537 | f"authoritative_handover: {workspace_report.authoritative_handover}" |
| 538 | ) |
| 539 | for member in workspace_report.members: |
| 540 | base = member.get("handover_basename") or "?" |
| 541 | ctx.output.emit(f"workspace.member {member['id']}: handover={base}") |
| 542 | |
| 543 | return exit_code |
| 544 | |
| 545 | |
| 546 | def _substrate_payload(substrate) -> dict: |
| 547 | return { |
| 548 | "state": substrate.state, |
| 549 | "ok": substrate.ok, |
| 550 | "missing": list(substrate.missing), |
| 551 | "remediation": substrate.remediation, |
| 552 | "message": substrate.message, |
| 553 | } |
| 554 | |
| 555 | |
| 556 | def _muse_sync_payload(muse_sync) -> dict: |
| 557 | return { |
| 558 | "state": muse_sync.state, |
| 559 | "ok": muse_sync.ok, |
| 560 | "remediation": muse_sync.remediation, |
| 561 | "message": muse_sync.message, |
| 562 | } |
| 563 | |
| 564 | |
| 565 | def _footprint_self_integrity_payload(report) -> dict: |
| 566 | return { |
| 567 | "state": report.state, |
| 568 | "ok": report.ok, |
| 569 | "missing": list(report.missing), |
| 570 | "remediation": report.remediation, |
| 571 | "message": report.message, |
| 572 | } |
| 573 | |
| 574 | |
| 575 | def _footprint_coverage_payload(report) -> dict: |
| 576 | return { |
| 577 | "state": report.state, |
| 578 | "ok": report.ok, |
| 579 | "missing": list(report.missing), |
| 580 | "remediation": report.remediation, |
| 581 | "message": report.message, |
| 582 | } |