import json
import os
import subprocess
import unittest
from pathlib import Path
from unittest.mock import patch

from gzkit.cli import main
from gzkit.commands.status_render import TABLE_TITLE_FEATURE  # noqa: F401
from gzkit.config import GzkitConfig
from gzkit.event_evidence import EventAnchor
from gzkit.ledger import (
    Ledger,
    adr_created_event,
    attested_event,
    audit_receipt_emitted_event,
    gate_checked_event,
    lifecycle_transition_event,
    obpi_created_event,
    obpi_receipt_emitted_event,
    obpi_withdrawn_event,
)
from gzkit.traceability import covers  # noqa: F401
from tests.commands.common import CliRunner, _init_git_repo, _quick_init, _write_obpi


class TestStatusCommand(unittest.TestCase):
    """Tests for gz status command."""

    def test_status_shows_no_adrs(self) -> None:
        """status shows message when no ADRs."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            result = runner.invoke(main, ["status"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("No ADRs found", result.output)

    def test_status_shows_adr(self) -> None:
        """status shows ADR when present."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            result = runner.invoke(main, ["status"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR-0.1.0-f", result.output)

    def test_status_show_gates_shows_gate2_pass_from_ledger(self) -> None:
        """status --show-gates shows Gate 2 PASS when latest gate check passed."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))

            result = runner.invoke(main, ["status", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("Gate 2 (TDD):   PASS", result.output)

    def test_status_show_gates_shows_gate2_fail_from_ledger(self) -> None:
        """status --show-gates shows Gate 2 FAIL when latest gate check failed."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "fail", "test", 1))

            result = runner.invoke(main, ["status", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("Gate 2 (TDD):   FAIL", result.output)

    def test_status_default_hides_gate_breakdown(self) -> None:
        """status output is OBPI/QC centric by default without gate rows."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["status"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("QC Readiness:", result.output)
            self.assertNotIn("Gate 2 (TDD):", result.output)

    def test_status_table_shows_adr_status_columns(self) -> None:
        """status --table renders a stable ADR summary table."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["status", "--table"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn(TABLE_TITLE_FEATURE, result.output)
            self.assertIn("Life", result.output)
            self.assertIn("Lane", result.output)
            self.assertIn("Status", result.output)
            self.assertIn("Checks", result.output)
            self.assertIn("ADR-0.1.0-f", result.output)
            self.assertIn("0/0", result.output)
            self.assertIn("TDD", result.output)

    def test_status_table_wraps_long_adr_ids_instead_of_truncating(self) -> None:
        """status --table preserves long ADR ids instead of ellipsizing them."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            adr_dir = Path(config.paths.adrs)
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))

            adr_id = "ADR-0.10.0-obpi-runtime-surface"
            adr_path = adr_dir / f"{adr_id}.md"
            adr_path.parent.mkdir(parents=True, exist_ok=True)
            adr_path.write_text(f"---\nid: {adr_id}\n---\n\n# {adr_id}\n", encoding="utf-8")
            ledger.append(adr_created_event(adr_id, "", "heavy"))

            result = runner.invoke(main, ["status", "--table"])

            self.assertEqual(result.exit_code, 0)
            self.assertNotIn("…", result.output)
            self.assertIn("ADR-0.10.0-obpi-runtime-surface", result.output)

    def test_status_table_blocks_ready_on_incomplete_obpis(self) -> None:
        """status --table marks QC pending when linked OBPIs are incomplete."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Lite",
                        "status: Draft",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Draft",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Placeholder",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))

            result = runner.invoke(main, ["status", "--table"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR-0.1.0-f", result.output)
            self.assertIn("0/1", result.output)
            self.assertIn("PENDING", result.output)
            self.assertIn("OBPI completion", result.output)

    def test_status_json_orders_semver_ids_numerically(self) -> None:
        """status --json sorts SemVer ADR ids numerically, not lexicographically."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            adr_dir = Path(config.paths.adrs)
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))

            for adr_id in ("ADR-0.10.0-f", "ADR-0.2.0-f", "ADR-0.9.0-f"):
                adr_path = adr_dir / f"{adr_id}.md"
                adr_path.parent.mkdir(parents=True, exist_ok=True)
                adr_path.write_text(f"---\nid: {adr_id}\n---\n\n# {adr_id}\n", encoding="utf-8")
                ledger.append(adr_created_event(adr_id, "", "lite"))

            result = runner.invoke(main, ["status", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(
                list(payload["adrs"].keys()),
                ["ADR-0.2.0-f", "ADR-0.9.0-f", "ADR-0.10.0-f"],
            )

    def test_status_shows_obpi_completion_summary(self) -> None:
        """status renders OBPI completion as the primary unit."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Lite",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "",
                        "## Key Proof",
                        "uv run gz adr status ADR-0.1.0-f --json",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": (
                            "The demo OBPI completed with canonical receipt evidence."
                        ),
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                    },
                )
            )

            result = runner.invoke(main, ["status"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("OBPI Unit:       COMPLETED", result.output)
            self.assertIn("OBPI Completion: 1/1 complete", result.output)
            self.assertIn("all linked OBPIs completed with evidence", result.output)

    def test_status_excludes_withdrawn_obpi_files_from_adr_summary(self) -> None:
        """Withdrawn OBPIs stay on disk but do not count as active ADR work."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            active_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-active.md"
            withdrawn_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-02-old.md"
            active_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=active_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="src/module.py",
            )
            _write_obpi(
                path=withdrawn_path,
                status="Abandoned",
                brief_status="Abandoned",
                implementation_line="src/old.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-active", "ADR-0.1.0-f"))
            ledger.append(obpi_created_event("OBPI-0.1.0-02-old", "ADR-0.1.0-f"))
            ledger.append(obpi_withdrawn_event("OBPI-0.1.0-02-old", "ADR-0.1.0-f", "old"))

            result = runner.invoke(main, ["status"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("OBPI Completion: 0/1 complete", result.output)
            self.assertNotIn("OBPI-0.1.0-02-old", result.output)

    def test_obpi_status_json_includes_runtime_fields(self) -> None:
        """obpi status --json reports the focused OBPI runtime payload."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "The focused OBPI status is backed by receipt evidence.",
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                    },
                )
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["obpi"], "OBPI-0.1.0-01-demo")
            self.assertEqual(payload["parent_adr"], "ADR-0.1.0-f")
            self.assertTrue(payload["found_file"])
            self.assertTrue(payload["completed"])
            self.assertEqual(payload["runtime_state"], "completed")
            self.assertEqual(payload["proof_state"], "recorded")
            self.assertEqual(payload["attestation_requirement"], "optional")
            self.assertEqual(payload["issues"], [])

    def test_obpi_status_renders_withdrawn_obpi_as_withdrawn(self) -> None:
        """Focused OBPI status must not present withdrawn briefs as pending."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Abandoned",
                brief_status="Abandoned",
                implementation_line="",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(obpi_withdrawn_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f", "old"))

            json_result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])
            text_result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo"])

            self.assertEqual(json_result.exit_code, 0)
            self.assertEqual(json.loads(json_result.output)["runtime_state"], "withdrawn")
            self.assertEqual(text_result.exit_code, 0)
            self.assertIn("Runtime State: WITHDRAWN", text_result.output)
            self.assertIn("Completion: WITHDRAWN", text_result.output)
            self.assertNotIn("Runtime State: PENDING", text_result.output)

    def test_obpi_status_json_reports_missing_file(self) -> None:
        """obpi status reports missing brief files explicitly."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertFalse(payload["found_file"])
            self.assertIsNone(payload["file"])
            self.assertIn("linked in ledger but no OBPI file found", payload["issues"])

    def test_obpi_status_json_supports_file_backed_obpi_without_ledger_link(self) -> None:
        """obpi status still inspects standalone OBPI briefs."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="src/module.py",
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertFalse(payload["linked_in_ledger"])
            self.assertTrue(payload["found_file"])
            self.assertEqual(payload["parent_adr"], "ADR-0.1.0-f")
            self.assertEqual(payload["attestation_requirement"], "optional")

    def test_obpi_reconcile_json_passes_for_completed_obpi(self) -> None:
        """obpi reconcile passes when ledger, file, and proof are coherent."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "The reconcile path has canonical completion evidence.",
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                    },
                )
            )

            result = runner.invoke(main, ["obpi", "reconcile", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["passed"])
            self.assertEqual(payload["blockers"], [])
            self.assertEqual(payload["runtime_state"], "completed")

    def test_obpi_reconcile_json_reports_reflection_drift_without_blocking(self) -> None:
        """obpi reconcile stays green when only the markdown reflection is stale."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="",
                key_proof="",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": (
                            "The runtime engine is now the canonical execution path."
                        ),
                        "key_proof": "uv run gz obpi pipeline OBPI-0.1.0-01-demo --json",
                    },
                )
            )

            result = runner.invoke(main, ["obpi", "reconcile", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["passed"])
            self.assertEqual(payload["blockers"], [])
            self.assertEqual(payload["runtime_state"], "completed")
            self.assertIn("brief reflection is not marked Completed", payload["reflection_issues"])

    @covers("REQ-0.11.0-04-02")
    def test_obpi_reconcile_fails_closed_when_proof_missing(self) -> None:
        """obpi reconcile emits BLOCKERS and exits non-zero when proof is missing."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="src/module.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["obpi", "reconcile", "OBPI-0.1.0-01-demo"])

            self.assertEqual(result.exit_code, 1)
            self.assertIn("BLOCKERS:", result.output)
            self.assertIn("ledger proof of completion is missing", result.output)

    @covers("REQ-0.11.0-04-03")
    def test_obpi_reconcile_reports_anchor_drift_for_scope_changes(self) -> None:
        """Anchor-aware reconcile fails when tracked files changed since completion."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            module_path = Path("src/module.py")
            module_path.parent.mkdir(parents=True, exist_ok=True)
            module_path.write_text("value = 1\n", encoding="utf-8")
            anchor_commit = _init_git_repo(Path.cwd())

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "Anchor-aware reconciliation remains canonical.",
                        "key_proof": "uv run gz obpi reconcile OBPI-0.1.0-01-demo --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=anchor_commit, semver="0.1.0"),
                )
            )

            module_path.write_text("value = 2\n", encoding="utf-8")
            subprocess.run(
                ["git", "add", "src/module.py"],
                check=True,
                capture_output=True,
                text=True,
            )
            subprocess.run(
                ["git", "commit", "-m", "change module"],
                check=True,
                capture_output=True,
                text=True,
            )

            result = runner.invoke(main, ["obpi", "reconcile", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["passed"])
            self.assertEqual(payload["runtime_state"], "completed")
            self.assertEqual(payload["anchor_state"], "superseded")
            self.assertEqual(payload["anchor_commit"], anchor_commit)
            self.assertEqual(payload["anchor_drift_files"], ["src/module.py"])

    def test_obpi_status_json_surfaces_tracked_defects_for_anchor_drift(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
                tracked_defects=[
                    "GHI-11 (open): Ignore transient anchor drift noise",
                    "GHI-12 (closed): Preserve completion state when only anchor freshness drifts",
                ],
            )
            module_path = Path("src/module.py")
            module_path.parent.mkdir(parents=True, exist_ok=True)
            module_path.write_text("value = 1\n", encoding="utf-8")
            anchor_commit = _init_git_repo(Path.cwd())

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "Anchor drift is tracked with linked defects.",
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=anchor_commit, semver="0.1.0"),
                )
            )

            module_path.write_text("value = 2\n", encoding="utf-8")
            subprocess.run(
                ["git", "add", "src/module.py"],
                check=True,
                capture_output=True,
                text=True,
            )
            subprocess.run(
                ["git", "commit", "-m", "change module"],
                check=True,
                capture_output=True,
                text=True,
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["tracked_defects"][0]["id"], "GHI-11")
            self.assertEqual(payload["tracked_defects"][0]["state"], "open")
            self.assertEqual(payload["tracked_defects"][1]["id"], "GHI-12")
            self.assertEqual(payload["tracked_defects"][1]["state"], "closed")
            # Anchor is superseded (later commits on top), not stale
            self.assertEqual(payload["anchor_state"], "superseded")

    def test_obpi_reconcile_ignores_shared_file_changes_absorbed_by_later_sibling_completion(
        self,
    ) -> None:
        """Later completed sibling OBPIs should absorb shared-file drift for earlier siblings."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_dir = Path(config.paths.adrs) / "obpis"
            obpi_dir.mkdir(parents=True, exist_ok=True)
            obpi_one_path = obpi_dir / "OBPI-0.1.0-01-demo.md"
            _write_obpi(
                path=obpi_one_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            obpi_two_path = obpi_dir / "OBPI-0.1.0-02-demo.md"
            obpi_two_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-02-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 2",
                        "lane: Lite",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-02-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "- Validation commands run: uv run -m unittest discover tests",
                        "- Date completed: 2026-02-14",
                        "",
                        "## Key Proof",
                        "uv run gz adr status ADR-0.1.0-f --json",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            module_path = Path("src/module.py")
            module_path.parent.mkdir(parents=True, exist_ok=True)
            module_path.write_text("value = 1\n", encoding="utf-8")
            first_anchor = _init_git_repo(Path.cwd())

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(obpi_created_event("OBPI-0.1.0-02-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": (
                            "Earlier sibling receipts stay complete after later shared changes."
                        ),
                        "key_proof": "uv run gz obpi reconcile OBPI-0.1.0-01-demo --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=first_anchor, semver="0.1.0"),
                )
            )

            module_path.write_text("value = 2\n", encoding="utf-8")
            subprocess.run(
                ["git", "add", "src/module.py"],
                check=True,
                capture_output=True,
                text=True,
            )
            subprocess.run(
                ["git", "commit", "-m", "later sibling change"],
                check=True,
                capture_output=True,
                text=True,
            )
            second_anchor = subprocess.run(
                ["git", "rev-parse", "--short=7", "HEAD"],
                check=True,
                capture_output=True,
                text=True,
            ).stdout.strip()

            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-02-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": (
                            "Later sibling completion absorbs the shared-file change."
                        ),
                        "key_proof": "uv run gz obpi status OBPI-0.1.0-02-demo --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=second_anchor, semver="0.1.0"),
                )
            )

            result = runner.invoke(main, ["obpi", "reconcile", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["passed"])
            self.assertEqual(payload["runtime_state"], "completed")
            # Earlier OBPI's anchor is superseded by later sibling commit
            self.assertEqual(payload["anchor_state"], "superseded")
            self.assertEqual(payload["anchor_drift_files"], ["src/module.py"])

    def test_obpi_status_json_exposes_anchor_fields(self) -> None:
        """Focused OBPI status includes anchor reconciliation fields."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            module_path = Path("src/module.py")
            module_path.parent.mkdir(parents=True, exist_ok=True)
            module_path.write_text("value = 1\n", encoding="utf-8")
            anchor_commit = _init_git_repo(Path.cwd())

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": (
                            "Anchor fields come from the canonical completion receipt."
                        ),
                        "key_proof": "uv run gz obpi status OBPI-0.1.0-01-demo --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=anchor_commit, semver="0.1.0"),
                )
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["runtime_state"], "completed")
            self.assertEqual(payload["anchor_state"], "current")
            self.assertEqual(payload["anchor_commit"], anchor_commit)
            self.assertEqual(payload["current_head"], anchor_commit)
            self.assertEqual(payload["anchor_issues"], [])

    def test_obpi_status_uses_only_key_proof_section_for_file_reflection(self) -> None:
        """Key proof extraction still parses the file section without promoting canonical proof."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Lite",
                        "status: Draft",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Draft",
                        "",
                        "## Key Proof",
                        "uv run gz obpi status OBPI-0.1.0-01-demo --json",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["key_proof_ok"])
            self.assertEqual(payload["req_proof_inputs"], [])

    def test_obpi_status_accepts_legacy_verification_section_as_file_reflection(self) -> None:
        """Legacy Verification sections still parse as file proof without redefining lifecycle."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Heavy",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Verification",
                        "```bash",
                        "uv run gz status --table",
                        "uv run -m unittest discover tests",
                        "```",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "- Date completed: 2026-03-10",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                )
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["key_proof_ok"])
            self.assertEqual(payload["runtime_state"], "drift")
            self.assertEqual(payload["req_proof_state"], "missing")

    def test_status_json_accepts_legacy_gate_evidence_section_as_file_reflection(self) -> None:
        """Gate Evidence parses as file proof; ledger receipt is authoritative for completion."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Lite",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "## Gate Evidence",
                        "",
                        "| Gate | Evidence | Command/Path |",
                        "|------|----------|--------------|",
                        "| Gate 2 (TDD) | Tests pass | `uv run -m unittest discover tests` |",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "- Date completed: 2026-03-10",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                )
            )
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))

            result = runner.invoke(main, ["status", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-0.1.0-f"]
            self.assertEqual(adr_payload["obpi_summary"]["completed"], 1)
            self.assertEqual(adr_payload["obpi_summary"]["unit_status"], "completed")
            self.assertEqual(adr_payload["lifecycle_status"], "Completed")
            self.assertTrue(adr_payload["obpis"][0]["key_proof_ok"])

    def test_obpi_status_accepts_verification_heading_prefix_as_file_reflection(self) -> None:
        """Verification headings with legacy suffixes still parse as file proof only."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Heavy",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "- Date completed: 2026-03-10",
                        "",
                        "### Verification Commands Run (2026-03-10)",
                        "",
                        "```text",
                        "uv run gz lint",
                        "uv run gz test",
                        "```",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                )
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["key_proof_ok"])
            self.assertEqual(payload["req_proof_state"], "missing")

    def test_obpi_status_accepts_validation_commands_bullet_as_file_reflection(self) -> None:
        """Legacy validation-command bullets still parse as file proof only."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Heavy",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified: src/module.py",
                        "- Validation commands run:",
                        "  - uv run gz lint",
                        "  - uv run gz check",
                        "- Validation outcome: PASS",
                        "- Date completed: 2026-03-10",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                )
            )

            result = runner.invoke(main, ["obpi", "status", "OBPI-0.1.0-01-demo", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertTrue(payload["key_proof_ok"])
            self.assertEqual(payload["req_proof_inputs"], [])


class TestOrphanedAdrWarning(unittest.TestCase):
    """Tests for _warn_orphaned_adrs false-positive prevention (GHI-166)."""

    def test_no_false_positive_when_stem_has_slug_but_ledger_has_bare_id(self) -> None:
        """ADR file with slugged stem (ADR-0.1.0-my-feature) should not warn
        when the ledger has the bare ID (ADR-0.1.0-f).  GHI-166."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            project_root = Path.cwd()
            config = GzkitConfig.load(project_root / ".gzkit.json")

            # Create ADR file with slugged filename
            adr_dir = project_root / config.paths.design_root / "adr" / "pre-release"
            adr_dir.mkdir(parents=True, exist_ok=True)
            slug_dir = adr_dir / "ADR-0.1.0-my-feature"
            slug_dir.mkdir()
            adr_file = slug_dir / "ADR-0.1.0-my-feature.md"
            adr_file.write_text(
                "---\nid: ADR-0.1.0-f\nstatus: Draft\nlane: lite\n---\n# ADR-0.1.0-f\n",
                encoding="utf-8",
            )

            # Register with bare ID in ledger (as gz adr create does)
            ledger = Ledger(project_root / ".gzkit" / "ledger.jsonl")
            ledger.append(adr_created_event("ADR-0.1.0-f", "PRD-TEST-1.0.0", "lite"))

            # gz adr report should NOT warn about this ADR
            result = runner.invoke(main, ["adr", "report"])
            self.assertEqual(result.exit_code, 0)
            self.assertNotIn("exist on disk but are not registered", result.output)
            self.assertNotIn("WARNING", result.output)

    def test_no_false_positive_when_stem_is_bare_and_ledger_has_slugged_id(
        self,
    ) -> None:
        """Frontmatter id is authoritative: bare-stem file with slugged ledger
        entry is not an orphan. Filesystem-convention prefix match cannot
        resolve this case (bare stem is not a prefix of the slugged id).
        GHI-166 principled fix."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            project_root = Path.cwd()
            config = GzkitConfig.load(project_root / ".gzkit.json")

            adr_dir = project_root / config.paths.design_root / "adr" / "pre-release"
            adr_dir.mkdir(parents=True, exist_ok=True)
            adr_file = adr_dir / "ADR-0.5.0-f.md"
            adr_file.write_text(
                "---\nid: ADR-0.5.0-skill-lifecycle\nstatus: Draft\nlane: lite\n---\n"
                "# ADR-0.5.0-f: skill-lifecycle\n",
                encoding="utf-8",
            )

            ledger = Ledger(project_root / ".gzkit" / "ledger.jsonl")
            ledger.append(adr_created_event("ADR-0.5.0-skill-lifecycle", "PRD-TEST-1.0.0", "lite"))

            result = runner.invoke(main, ["adr", "report"])
            self.assertEqual(result.exit_code, 0)
            self.assertNotIn("exist on disk but are not registered", result.output)
            self.assertNotIn("WARNING", result.output)

    def test_genuine_orphan_still_warns(self) -> None:
        """An ADR file with no ledger entry at all should still produce a warning."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            project_root = Path.cwd()
            config = GzkitConfig.load(project_root / ".gzkit.json")

            # Create ADR file with no matching ledger entry
            adr_dir = project_root / config.paths.design_root / "adr" / "pre-release"
            adr_dir.mkdir(parents=True, exist_ok=True)
            slug_dir = adr_dir / "ADR-0.99.0-totally-orphaned"
            slug_dir.mkdir()
            adr_file = slug_dir / "ADR-0.99.0-totally-orphaned.md"
            adr_file.write_text(
                "---\nid: ADR-0.99.0-f\nstatus: Draft\nlane: lite\n---\n# ADR-0.99.0-f\n",
                encoding="utf-8",
            )

            # No ledger entry — should warn
            result = runner.invoke(main, ["adr", "report"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("exist on disk but are not registered", result.output)


class TestLifecycleStatusSemantics(unittest.TestCase):
    """Tests for derived lifecycle semantics on status/state surfaces."""

    def test_adr_status_default_hides_gate_breakdown(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR Overview", result.output)
            self.assertNotIn("Gate 2 (TDD):", result.output)

    def test_adr_status_renders_shared_table_via_deterministic_renderer(self) -> None:
        """gz adr status renders the same deterministic tables as gz adr report.

        Locks the skill Output Contract that says "consistent table format" and
        "do not replace the table with prose" for both modes. Before GHI #141's
        follow-up repair, this command produced ad-hoc indented prose and
        violated the skill design.
        """
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            status_result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f"])
            report_result = runner.invoke(main, ["adr", "report", "ADR-0.1.0-f"])

            self.assertEqual(status_result.exit_code, 0)
            self.assertEqual(report_result.exit_code, 0)
            self.assertIn("ADR Overview", status_result.output)
            self.assertIn("OBPIs", status_result.output)
            self.assertIn("ADR-0.1.0", status_result.output)
            self.assertIn("ADR Overview", report_result.output)

    def test_adr_status_discloses_withdrawn_obpis_hidden_from_table(self) -> None:
        """Withdrawn OBPIs are correctly excluded from the active OBPI table, but
        their existence must be disclosed so the resulting numbering gaps are not
        read as silently-dropped data (ledger history preserved). Drilldown only.
        """
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            active_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-active.md"
            withdrawn_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-02-old.md"
            active_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=active_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="src/module.py",
            )
            _write_obpi(
                path=withdrawn_path,
                status="Abandoned",
                brief_status="Abandoned",
                implementation_line="src/old.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-active", "ADR-0.1.0-f"))
            ledger.append(obpi_created_event("OBPI-0.1.0-02-old", "ADR-0.1.0-f"))
            ledger.append(obpi_withdrawn_event("OBPI-0.1.0-02-old", "ADR-0.1.0-f", "old"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            # The withdrawn OBPI stays out of the active table...
            self.assertNotIn("OBPI-0.1.0-02-old", result.output)
            # ...but its existence is disclosed so the gap is not read as dropped data.
            self.assertIn("Withdrawn (hidden): 1 OBPI", result.output)

            json_result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(json_result.exit_code, 0)
            payload = json.loads(json_result.output)
            self.assertIn("OBPI-0.1.0-02-old", payload["withdrawn_obpis"])

    def test_adr_status_accepts_semver_prefix_for_suffixed_adr(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            adr_path = Path(config.paths.adrs) / "ADR-0.5.0-skill-lifecycle-governance.md"
            adr_path.parent.mkdir(parents=True, exist_ok=True)
            adr_path.write_text(
                "---\n"
                "id: ADR-0.5.0-skill-lifecycle-governance\n"
                "---\n\n"
                "# ADR-0.5.0-f: skill-lifecycle-governance\n",
                encoding="utf-8",
            )

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-0.5.0-skill-lifecycle-governance", "", "heavy"))

            result = runner.invoke(main, ["adr", "status", "0.5.0"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR-0.5.0", result.output)

    def test_adr_status_show_gates_includes_gate_breakdown(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("Gate 2 (TDD):", result.output)

    def test_adr_status_heavy_features_missing_reports_gate4_pending(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init("heavy")
            runner.invoke(main, ["plan", "create", "f", "--lane", "heavy", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["gates"]["4"], "pending")
            self.assertNotIn("gate4_na_reason", payload)

    def test_adr_status_legacy_semver_id_still_resolves(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init("heavy")
            config = GzkitConfig.load(Path(".gzkit.json"))
            adr_path = Path(config.paths.adrs) / "ADR-0.2.0-gate-verification.md"
            adr_path.parent.mkdir(parents=True, exist_ok=True)
            adr_path.write_text(
                "---\nid: ADR-0.2.0-f\nlane: heavy\n---\n\n# ADR-0.2.0-f: Gate Verification\n",
                encoding="utf-8",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-0.2.0-f", "", "heavy"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.2.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["adr"], "ADR-0.2.0-f")

    def test_adr_status_json_completed(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Completed")
            self.assertEqual(payload["closeout_phase"], "attested")
            self.assertEqual(payload["attestation_term"], "Completed")

    def test_adr_status_json_obpi_incomplete_overrides_completed_lifecycle(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Pending")
            self.assertEqual(payload["closeout_phase"], "attested")
            self.assertEqual(payload["attestation_term"], "Completed")
            self.assertEqual(payload["obpi_summary"]["unit_status"], "pending")

    def test_adr_status_qc_readiness_includes_obpi_completion_blocker(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("QC Readiness: PENDING (pending: OBPI completion)", result.output)

    def test_adr_status_surfaces_closeout_blockers(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="src/module.py",
            )

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f"])

            self.assertEqual(result.exit_code, 0)
            self.assertIn("Closeout Readiness: BLOCKED", result.output)
            self.assertIn("Closeout Blockers:", result.output)
            self.assertIn(
                "OBPI-0.1.0-01-demo: ledger proof of completion is missing", result.output
            )

    def test_adr_status_closeout_blockers_include_tracked_defect_refs(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
                tracked_defects=[
                    "GHI-11 (open): Ignore transient anchor drift noise",
                    "GHI-12 (open): Reconcile shared-scope sibling completions correctly",
                ],
            )
            module_path = Path("src/module.py")
            module_path.parent.mkdir(parents=True, exist_ok=True)
            module_path.write_text("value = 1\n", encoding="utf-8")
            anchor_commit = _init_git_repo(Path.cwd())

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "Anchor drift is tracked with linked defects.",
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                        "scope_audit": {
                            "allowlist": ["src/module.py"],
                            "changed_files": ["src/module.py"],
                            "out_of_scope_files": [],
                        },
                        "git_sync_state": {
                            "dirty": False,
                            "ahead": 0,
                            "behind": 0,
                            "diverged": False,
                            "blockers": [],
                        },
                    },
                    anchor=EventAnchor(commit=anchor_commit, semver="0.1.0"),
                )
            )

            module_path.write_text("value = 2\n", encoding="utf-8")
            subprocess.run(
                ["git", "add", "src/module.py"],
                check=True,
                capture_output=True,
                text=True,
            )
            subprocess.run(
                ["git", "commit", "-m", "change module"],
                check=True,
                capture_output=True,
                text=True,
            )

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])

            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            # Anchor superseded by later commits — not a closeout blocker
            self.assertEqual(payload["closeout_blockers"], [])
            self.assertEqual(payload["obpis"][0]["anchor_state"], "superseded")
            self.assertEqual(payload["obpis"][0]["tracked_defects"][0]["id"], "GHI-11")

    def test_adr_status_json_validated(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))
            ledger.append(audit_receipt_emitted_event("ADR-0.1.0-f", "validated", "human"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Validated")
            self.assertEqual(payload["closeout_phase"], "validated")
            self.assertTrue(payload["validated"])

    def test_adr_status_json_abandoned(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "dropped", "human", "out of scope"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Abandoned")
            self.assertEqual(payload["attestation_term"], "Dropped")

    def test_obpi_scoped_validated_receipt_does_not_set_validated_lifecycle(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(
                audit_receipt_emitted_event(
                    "ADR-0.1.0-f",
                    "validated",
                    "human",
                    evidence={
                        "scope": "OBPI-0.1.0-01",
                        "adr_completion": "not_completed",
                        "obpi_completion": "attested_completed",
                    },
                )
            )

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Pending")
            self.assertEqual(payload["closeout_phase"], "pre_closeout")
            self.assertFalse(payload["validated"])

    def test_status_json_includes_lifecycle_fields(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "partial", "human", "staged rollout"))

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-0.1.0-f"]
            self.assertEqual(adr_payload["lifecycle_status"], "Completed")
            self.assertEqual(adr_payload["attestation_term"], "Completed - Partial")

    def test_status_json_obpi_incomplete_overrides_completed_lifecycle(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-0.1.0-f"]
            self.assertEqual(adr_payload["lifecycle_status"], "Pending")
            self.assertEqual(adr_payload["closeout_phase"], "attested")
            self.assertEqual(adr_payload["attestation_term"], "Completed")
            self.assertEqual(adr_payload["obpi_summary"]["unit_status"], "pending")

    def test_status_json_includes_obpi_summary_fields(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Draft",
                brief_status="Draft",
                implementation_line="",
            )

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-0.1.0-f"]
            self.assertIn("obpis", adr_payload)
            self.assertIn("obpi_summary", adr_payload)
            self.assertIn("closeout_ready", adr_payload)
            self.assertIn("closeout_blockers", adr_payload)
            self.assertEqual(adr_payload["obpi_summary"]["total"], 1)
            self.assertEqual(adr_payload["obpi_summary"]["completed"], 0)
            self.assertEqual(adr_payload["obpi_summary"]["unit_status"], "pending")
            self.assertFalse(adr_payload["closeout_ready"])
            self.assertIn("OBPI-0.1.0-01-demo", adr_payload["closeout_blockers"][0])

    def test_status_json_completed_status_with_empty_summary_stays_incomplete(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            obpi_path.write_text(
                "\n".join(
                    [
                        "---",
                        "id: OBPI-0.1.0-01-demo",
                        "parent: ADR-0.1.0-f",
                        "item: 1",
                        "lane: Lite",
                        "status: Completed",
                        "---",
                        "",
                        "# OBPI-0.1.0-01-demo: Demo",
                        "",
                        "**Brief Status:** Completed",
                        "",
                        "## Evidence",
                        "",
                        "### Implementation Summary",
                        "- Files created/modified:",
                        "- Tests added:",
                        "- Date completed:",
                        "",
                        "## Key Proof",
                        "uv run gz adr status ADR-0.1.0-f --json",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-0.1.0-f"]
            self.assertEqual(adr_payload["obpi_summary"]["completed"], 0)
            self.assertEqual(adr_payload["obpi_summary"]["unit_status"], "pending")
            self.assertIn(
                "ledger proof of completion is missing",
                adr_payload["obpis"][0]["issues"],
            )

    def test_adr_status_json_pool_adr_ignores_attestation_for_lifecycle(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            pool_dir = Path(config.paths.adrs) / "pool"
            pool_dir.mkdir(parents=True, exist_ok=True)
            pool_adr = pool_dir / "ADR-pool.sample.md"
            pool_adr.write_text(
                "---\nid: ADR-pool.sample\n---\n\n# ADR-pool.sample\n", encoding="utf-8"
            )

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-pool.sample", "", "heavy"))
            ledger.append(attested_event("ADR-pool.sample", "completed", "human"))

            result = runner.invoke(main, ["adr", "status", "ADR-pool.sample", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["lifecycle_status"], "Pending")
            self.assertEqual(payload["closeout_phase"], "pre_closeout")
            self.assertIsNone(payload["attestation_term"])
            self.assertFalse(payload["attested"])
            self.assertEqual(payload["gates"]["5"], "pending")
            self.assertEqual(payload["obpis"], [])

    def test_status_json_pool_adr_ignores_attestation_for_lifecycle(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            pool_dir = Path(config.paths.adrs) / "pool"
            pool_dir.mkdir(parents=True, exist_ok=True)
            pool_adr = pool_dir / "ADR-pool.sample.md"
            pool_adr.write_text(
                "---\nid: ADR-pool.sample\n---\n\n# ADR-pool.sample\n", encoding="utf-8"
            )

            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-pool.sample", "", "heavy"))
            ledger.append(attested_event("ADR-pool.sample", "completed", "human"))

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            adr_payload = payload["adrs"]["ADR-pool.sample"]
            self.assertEqual(adr_payload["lifecycle_status"], "Pending")
            self.assertEqual(adr_payload["closeout_phase"], "pre_closeout")
            self.assertIsNone(adr_payload["attestation_term"])
            self.assertFalse(adr_payload["attested"])
            self.assertEqual(adr_payload["gates"]["5"], "pending")

    def test_adr_status_json_includes_obpi_rows(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "obpis" / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(
                path=obpi_path,
                status="Completed",
                brief_status="Completed",
                implementation_line="src/module.py",
            )
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))
            ledger.append(
                obpi_receipt_emitted_event(
                    obpi_id="OBPI-0.1.0-01-demo",
                    parent_adr="ADR-0.1.0-f",
                    receipt_event="completed",
                    attestor="human:test",
                    obpi_completion="completed",
                    evidence={
                        "value_narrative": "ADR status rows reflect canonical receipt evidence.",
                        "key_proof": "uv run gz adr status ADR-0.1.0-f --json",
                    },
                )
            )

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertIn("obpis", payload)
            self.assertEqual(len(payload["obpis"]), 1)
            row = payload["obpis"][0]
            self.assertEqual(row["id"], "OBPI-0.1.0-01-demo")
            self.assertTrue(row["found_file"])
            self.assertTrue(row["completed"])
            self.assertTrue(row["evidence_ok"])
            self.assertEqual(row["runtime_state"], "completed")
            self.assertEqual(row["proof_state"], "recorded")
            self.assertEqual(row["attestation_requirement"], "optional")
            self.assertEqual(row["req_proof_state"], "recorded")
            self.assertEqual(row["req_proof_summary"]["present"], 1)
            self.assertEqual(row["issues"], [])

    def test_adr_status_json_reports_missing_linked_obpi_file(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-core-feature", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertIn("obpis", payload)
            self.assertEqual(len(payload["obpis"]), 1)
            row = payload["obpis"][0]
            self.assertEqual(row["id"], "OBPI-0.1.0-01-core-feature")
            self.assertFalse(row["found_file"])
            self.assertIn("linked in ledger but no OBPI file found", row["issues"])

    def test_adr_report_renders_overview_and_obpi_tables(self) -> None:
        """gz adr report renders deterministic ASCII tables."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "report", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR Overview", result.output)
            self.assertIn("OBPIs", result.output)
            self.assertIn("ADR-0.1.0", result.output)
            self.assertIn("Pending", result.output)

    def test_adr_report_shows_obpi_rows(self) -> None:
        """gz adr report includes OBPI rows when OBPIs are linked."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            config = GzkitConfig.load(Path(".gzkit.json"))
            obpi_path = Path(config.paths.adrs) / "OBPI-0.1.0-01-demo.md"
            obpi_path.parent.mkdir(parents=True, exist_ok=True)
            _write_obpi(obpi_path, "Draft", "draft", "Not started")
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-demo", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["adr", "report", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("OBPI-0.1.0-01-demo", result.output)
            self.assertIn("draft", result.output)

    def test_adr_report_shows_issues_section(self) -> None:
        """gz adr report prints issues when OBPIs have problems."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(obpi_created_event("OBPI-0.1.0-01-core-feature", "ADR-0.1.0-f"))

            result = runner.invoke(main, ["adr", "report", "ADR-0.1.0-f"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("Issues", result.output)
            self.assertIn("ledger proof of completion is missing", result.output)

    def test_adr_report_accepts_semver_prefix(self) -> None:
        """gz adr report resolves short semver prefixes."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "report", "0.1.0"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("ADR-0.1.0", result.output)

    def test_adr_report_no_arg_renders_summary_table(self) -> None:
        """gz adr report (no argument) renders the ADR summary table."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])

            result = runner.invoke(main, ["adr", "report"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn(TABLE_TITLE_FEATURE, result.output)
            self.assertIn("ADR-0.1.0-f", result.output)
            self.assertIn("Checks legend", result.output)

    def test_state_ready_json_only_includes_gate_ready_unattested_adrs(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "test", 0))

            result = runner.invoke(main, ["state", "--ready", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertIn("ADR-0.1.0-f", payload)


class TestStatusEpicFilter(unittest.TestCase):
    """Epic grouping filter for pool ADRs — OBPI-0.0.18-04."""

    @staticmethod
    def _write_pool_adr(config: GzkitConfig, slug: str, *, epic: str | None = None) -> None:
        pool_dir = Path(config.paths.adrs) / "pool"
        pool_dir.mkdir(parents=True, exist_ok=True)
        frontmatter = f"---\nid: ADR-pool.{slug}\n"
        if epic is not None:
            frontmatter += f"epic: {epic}\n"
        frontmatter += "---\n\n# ADR-pool." + slug + "\n"
        (pool_dir / f"ADR-pool.{slug}.md").write_text(frontmatter, encoding="utf-8")

    @covers("REQ-0.0.18-04-05")
    def test_status_epic_flag_documented_in_help(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            result = runner.invoke(main, ["status", "--help"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("--epic", result.output)
            self.assertIn("filename", result.output.lower())
            self.assertIn("frontmatter", result.output.lower())

    @covers("REQ-0.0.18-04-01")
    @covers("REQ-0.0.18-04-03")
    @covers("REQ-0.0.18-04-07")
    def test_status_epic_filter_matches_filename_prefix(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            self._write_pool_adr(config, "auth-login")
            self._write_pool_adr(config, "auth-oauth")
            self._write_pool_adr(config, "billing-invoice")
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            for adr_id in (
                "ADR-pool.auth-login",
                "ADR-pool.auth-oauth",
                "ADR-pool.billing-invoice",
            ):
                ledger.append(adr_created_event(adr_id, "", "lite"))

            result = runner.invoke(main, ["status", "--epic", "auth", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            ids = set(payload["adrs"].keys())
            self.assertEqual(ids, {"ADR-pool.auth-login", "ADR-pool.auth-oauth"})

    @covers("REQ-0.0.18-04-02")
    @covers("REQ-0.0.18-04-03")
    def test_status_epic_filter_matches_frontmatter_field(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            self._write_pool_adr(config, "claude-code", epic="vendor-alignment")
            self._write_pool_adr(config, "other-thing")
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-pool.claude-code", "", "lite"))
            ledger.append(adr_created_event("ADR-pool.other-thing", "", "lite"))

            result = runner.invoke(main, ["status", "--epic", "vendor-alignment", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            ids = set(payload["adrs"].keys())
            self.assertEqual(ids, {"ADR-pool.claude-code"})

    @covers("REQ-0.0.18-04-02")
    def test_status_epic_filter_warns_on_mismatch(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            self._write_pool_adr(config, "auth-login", epic="billing")
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(adr_created_event("ADR-pool.auth-login", "", "lite"))

            result = runner.invoke(main, ["status", "--epic", "auth", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertIn("ADR-pool.auth-login", payload["adrs"])
            warnings = payload.get("warnings", [])
            self.assertTrue(
                any("auth-login" in w and "billing" in w for w in warnings),
                f"Expected mismatch warning in {warnings!r}",
            )

    @covers("REQ-0.0.18-04-04")
    def test_status_epic_filter_empty_result_exits_zero(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            result = runner.invoke(main, ["status", "--epic", "nonexistent", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["adrs"], {})

    @covers("REQ-0.0.18-04-06")
    def test_status_default_behavior_unchanged_without_epic_flag(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertIn("ADR-0.1.0-f", payload["adrs"])
            self.assertNotIn("warnings", payload)
            self.assertNotIn("ADR-0.2.0-f", payload)


def _seed_adr_with_six_obpis(parent_adr: str = "ADR-0.1.0-f") -> None:
    """Seed a feature ADR with six pending OBPI briefs.

    Mirrors the RHEA reproduction case (ADR-0.0.0-f with six OBPIs) that
    triggered GHI #319 — used to exercise --full open-OBPI rendering.
    """
    runner_main = main
    config = GzkitConfig.load(Path(".gzkit.json"))
    obpi_dir = Path(config.paths.adrs) / "obpis"
    obpi_dir.mkdir(parents=True, exist_ok=True)
    for n in range(1, 7):
        slug = f"OBPI-0.1.0-{n:02d}-pending-item-{n}"
        path = obpi_dir / f"{slug}.md"
        path.write_text(
            "---\n"
            f"id: {slug}\n"
            f"parent: {parent_adr}\n"
            f"item: {n}\n"
            "lane: Lite\n"
            "status: Draft\n"
            "---\n\n"
            f"# {slug}: Pending\n\n**Brief Status:** Draft\n",
            encoding="utf-8",
        )
        ledger = Ledger(Path(".gzkit/ledger.jsonl"))
        ledger.append(obpi_created_event(slug, parent_adr))
    # silence the unused-import warning for `runner_main` — used by callers.
    _ = runner_main


class TestStatusFullFlag(unittest.TestCase):
    """gz status --full preserves identity-bearing fields per GHI #319."""

    def test_status_table_full_renders_long_adr_id_without_truncation(self) -> None:
        runner = CliRunner()
        with (
            patch.dict(os.environ, {"COLUMNS": "240"}, clear=False),
            runner.isolated_filesystem(),
        ):
            _quick_init()
            config = GzkitConfig.load(Path(".gzkit.json"))
            adr_dir = Path(config.paths.adrs)
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            long_id = "ADR-0.0.0-rhea-kernel-organizing-doctrine-extra-long-slug"
            adr_path = adr_dir / f"{long_id}.md"
            adr_path.parent.mkdir(parents=True, exist_ok=True)
            adr_path.write_text(
                f"---\nid: {long_id}\nlane: lite\nkind: feature\n---\n\n# {long_id}\n",
                encoding="utf-8",
            )
            ledger.append(adr_created_event(long_id, "", "lite"))

            result = runner.invoke(main, ["status", "--table", "--full"])
            self.assertEqual(result.exit_code, 0)
            self.assertNotIn("…", result.output)
            self.assertIn(long_id, result.output)

    def test_status_show_gates_full_renders_all_open_obpis(self) -> None:
        """`gz status --show-gates --full` lists every open OBPI (no "...and N more")."""
        runner = CliRunner()
        with (
            patch.dict(os.environ, {"COLUMNS": "240"}, clear=False),
            runner.isolated_filesystem(),
        ):
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_six_obpis()

            result = runner.invoke(main, ["status", "--show-gates", "--full"])
            self.assertEqual(result.exit_code, 0)
            self.assertNotIn("... and ", result.output)
            for n in range(1, 7):
                slug = f"OBPI-0.1.0-{n:02d}-pending-item-{n}"
                self.assertIn(slug, result.output)

    def test_status_show_gates_default_truncates_to_three(self) -> None:
        """Default (no --full) keeps the legacy "...and N more" preview."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_six_obpis()

            result = runner.invoke(main, ["status", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("... and ", result.output)


class TestStatusFullOutputForm(unittest.TestCase):
    """Output-form fixture for `gz status --show-gates --full` (Invariant 3)."""

    def test_show_gates_full_renders_obpi_rows_as_rich_table(self) -> None:
        runner = CliRunner()
        with (
            patch.dict(os.environ, {"COLUMNS": "240"}, clear=False),
            runner.isolated_filesystem(),
        ):
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_six_obpis()

            result = runner.invoke(main, ["status", "--show-gates", "--full"])
            self.assertEqual(result.exit_code, 0)
            # Rich box-drawing characters confirm a Rich Table rendered
            # the OBPI list (vs the legacy prose 3-row preview). Rounded
            # box uses │ for column separators.
            self.assertIn("│", result.output)
            self.assertIn("OBPI", result.output)
            self.assertIn("Attestation", result.output)
            self.assertIn("Anchor", result.output)


def _seed_adr_with_post_validation_fail(adr_id: str) -> None:
    """Seed ledger with a Validated ADR plus a stale post-validation gate fail.

    The shape mirrors GHI #411: a `Completed` lifecycle transition follows
    attestation, after which a `gate_checked: fail` event lands without any
    rollback. The effective gate view smooths it to `pass`; the sidecar must
    still surface the underlying observation.
    """
    ledger = Ledger(Path(".gzkit/ledger.jsonl"))
    ledger.append(gate_checked_event(adr_id, 2, "pass", "uv run gz test", 0))
    ledger.append(attested_event(adr_id, "completed", "human"))
    ledger.append(lifecycle_transition_event(adr_id, "adr", "Proposed", "Completed"))
    ledger.append(gate_checked_event(adr_id, 2, "fail", "uv run gz test", 1))


class TestPostValidationObservedGateFail(unittest.TestCase):
    """REQ-derived semantics for GHI #411 (observation laundering)."""

    def test_adr_status_json_includes_observed_post_validation_failures(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_post_validation_fail("ADR-0.1.0-f")

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(
                payload["observed_post_validation_gate_failures"],
                [2],
                "JSON consumers must see the laundered observations",
            )
            self.assertEqual(
                payload["gates"]["2"],
                "pass",
                "Lifecycle authority preserved on the gate cell",
            )

    def test_status_json_payload_field_is_jsonable(self) -> None:
        """Sidecar uses list[int] (not set[int]) so json.dumps succeeds."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_post_validation_fail("ADR-0.1.0-f")

            result = runner.invoke(main, ["status", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            entry = payload["adrs"]["ADR-0.1.0-f"]
            self.assertEqual(entry["observed_post_validation_gate_failures"], [2])

    def test_status_json_empty_when_no_laundering(self) -> None:
        """No false positives when raw and effective agree."""
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            ledger = Ledger(Path(".gzkit/ledger.jsonl"))
            ledger.append(gate_checked_event("ADR-0.1.0-f", 2, "pass", "uv run gz test", 0))
            ledger.append(attested_event("ADR-0.1.0-f", "completed", "human"))
            ledger.append(lifecycle_transition_event("ADR-0.1.0-f", "adr", "Proposed", "Completed"))

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--json"])
            self.assertEqual(result.exit_code, 0)
            payload = json.loads(result.output)
            self.assertEqual(payload["observed_post_validation_gate_failures"], [])

    def test_qc_readiness_blocked_by_observed_post_validation_failure(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_post_validation_fail("ADR-0.1.0-f")

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            # QC readiness must block on the laundered observation rather
            # than reporting READY against lifecycle alone.
            self.assertIn("Observed post-validation gate fail", result.output)


class TestPostValidationObservedGateFailOutputForm(unittest.TestCase):
    """Output-form fixture for GHI #411 surfaces (Invariant 3 carve-out)."""

    def test_show_gates_annotates_smoothed_pass_with_observation(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_post_validation_fail("ADR-0.1.0-f")

            result = runner.invoke(main, ["adr", "status", "ADR-0.1.0-f", "--show-gates"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("observed FAIL post-validation", result.output)

    def test_status_table_legend_advertises_observed_fail_code(self) -> None:
        runner = CliRunner()
        with runner.isolated_filesystem():
            _quick_init()
            runner.invoke(main, ["plan", "create", "f", "--kind", "feature"])
            _seed_adr_with_post_validation_fail("ADR-0.1.0-f")

            result = runner.invoke(main, ["status", "--table"])
            self.assertEqual(result.exit_code, 0)
            self.assertIn("X=Observed post-validation gate fail", result.output)
