#!/usr/bin/env python3
"""Adoption-posture gate — a staged obligation must say why nothing fails today.

The convention (``docs/recipes/adoption-posture.md``): a standards family that
enacts an obligation whose mechanism has not shipped carries a section headed

    ## N. Adoption posture (why nothing here fails today)

telling a reader which of its rules bind on enactment, which are staged, what
has to land before they don't, and whose act each landing is. Without it,
enacting a family reads as declaring 44 repos non-conformant on merge day.

**The staged set is derived, never restated (DP-14).** This repo's DOC-15
ledger already owns "which MUSTs are not in force here" — that is exactly what
a ``⏳`` row means (CFM-03). So this gate joins two machine surfaces:

  1. ``docs/inprogress/MGF_STANDARDS_CONFORMANCE.md`` — every ``| XX-NN | … |
     ⏳ |`` row, i.e. the staged rule ids;
  2. ``docs/standards/rules.json`` — id → owning doc (generated by
     ``gen_rules_index.py``, so it cannot drift from the Rules boxes);

and requires the posture section on each doc that owns at least one staged
rule. It follows that the requirement **self-expires per family**: the day a
family's last ``⏳`` flips to ``✅``, the section stops being required — the
same shape ``mgf-fed``'s ``_PENDING_CORPUS`` uses for staged doc aliases.

Not a MUST, deliberately: only one repo in the federation authors standards
families, so a DOC-* rule would make 43 members carry an ``N/A`` row for an
obligation they structurally cannot hold. The reasoning, and the promotion
trigger that would change it, are §5 of the recipe. **A gate is a citation.**

Usage::

    python scripts/check_adoption_posture.py     # exit 0 GREEN / 1 with reasons

Wiring: invoked by ``gen_rules_index.py --check`` (the same channel
``check_headline_coherence.py`` and ``check_testing_doc.py`` ride — the
``.woodpecker.yml`` is a WF-13 GENERATED artifact, so a corpus gate reaches CI
through pytest, never a hand-edited pipeline step) and pinned directly, with
its TS-29 seeded self-test, by ``tests/unit/test_check_adoption_posture.py``.
"""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path

#: The canonical heading. The parenthetical is part of the convention, not
#: decoration: it is what tells a reader the section is a conformance
#: statement rather than advocacy for the rules.
POSTURE_HEADING = "Adoption posture (why nothing here fails today)"

#: A DOC-15 ledger row: "| SHP-01 | SESSION_HOOKS | ⏳ | evidence… |". Column 2
#: (the family's doc) is deliberately NOT trusted — rules.json owns id → doc.
_LEDGER_ROW = re.compile(r"^\|\s*([A-Z]{2,5}-\d+)\s*\|[^|]*\|\s*([^|\s]+)\s*\|", re.MULTILINE)

#: The staged marker, per CFM-03's four-marker vocabulary.
_STAGED = "⏳"

#: Any heading level, so a family that nests the section is still conformant.
_HEADING = re.compile(r"^#{2,4}\s+.*$", re.MULTILINE)


def _rule_docs(root: Path) -> dict[str, str]:
    """rule id -> owning doc filename, from the generated machine index."""
    data = json.loads((root / "docs" / "standards" / "rules.json").read_text(encoding="utf-8"))
    return {str(r["id"]): str(r["doc"]) for r in data["rules"]}


def _ledger_rows(root: Path) -> list[tuple[str, str]]:
    """[(rule_id, marker)] for every per-MUST row in the DOC-15 ledger."""
    ledger = root / "docs" / "inprogress" / "MGF_STANDARDS_CONFORMANCE.md"
    return _LEDGER_ROW.findall(ledger.read_text(encoding="utf-8"))


def _has_posture(doc: Path) -> bool:
    text = doc.read_text(encoding="utf-8")
    return any(POSTURE_HEADING in line for line in _HEADING.findall(text))


def check(root: Path) -> list[str]:
    """Return the list of posture failures (empty == GREEN)."""
    errors: list[str] = []
    rows = _ledger_rows(root)

    # The non-empty pin (TS-29). A reformatted ledger would make every check
    # below vacuously green, which is the failure this whole gate exists to
    # refuse in others.
    if not rows:
        return [
            "docs/inprogress/MGF_STANDARDS_CONFORMANCE.md: parsed ZERO per-MUST "
            "rows — the gate has nothing to check, so it would pass vacuously. "
            "The row shape ('| XX-NN | FAMILY | marker | evidence |') changed; "
            "fix this parser before trusting a green run."
        ]

    rule_docs = _rule_docs(root)
    staged: dict[str, list[str]] = {}
    for rule_id, marker in rows:
        if marker != _STAGED:
            continue
        doc = rule_docs.get(rule_id)
        if doc is None:
            errors.append(
                f"MGF_STANDARDS_CONFORMANCE.md: staged row for '{rule_id}' but "
                f"rules.json anchors no such rule — the ledger and the corpus "
                f"disagree about what exists."
            )
            continue
        staged.setdefault(doc, []).append(rule_id)

    std = root / "docs" / "standards"
    for doc, ids in sorted(staged.items()):
        path = std / doc
        if not path.is_file():
            errors.append(f"docs/standards/{doc}: named by rules.json but not on disk.")
            continue
        if not _has_posture(path):
            errors.append(
                f"docs/standards/{doc}: {len(ids)} staged rule(s) "
                f"({', '.join(sorted(ids))}) carry a '{_STAGED}' row in the "
                f"DOC-15 ledger, but the doc has no '## N. {POSTURE_HEADING}' "
                f"section — a reader cannot tell which of its rules bind today. "
                f"See docs/recipes/adoption-posture.md §6 for the template."
            )
    return errors


def main() -> int:
    root = Path(__file__).resolve().parents[1]
    errors = check(root)
    if errors:
        print("ADOPTION-POSTURE GATE: FAILED")
        for e in errors:
            print(f"  ✗ {e}")
        return 1
    rows = _ledger_rows(root)
    staged = sum(1 for _, marker in rows if marker == _STAGED)
    rule_docs = _rule_docs(root)
    docs = {rule_docs[r] for r, m in rows if m == _STAGED and r in rule_docs}
    print(
        f"ADOPTION-POSTURE GATE: GREEN — {len(rows)} ledger rows read, "
        f"{staged} staged, {len(docs)} doc(s) required to carry the posture "
        f"section and all do."
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())
