# -*- coding: utf-8 -*-
"""Golden-currency gate (R-242) — a controller release is not DELIVERED until a golden carries it.

Run from the repo root:  python3 scripts/golden_currency_gate.py
Exit 0 clean · 1 convicted (a released controller has no golden) · 2 inconclusive.

WHY THIS EXISTS, and why a rule was not enough.

R-242 was filed on 2026-08-07 as a mechanism-less rule: *a controller release that changes
customer-visible behaviour is not finished until a golden carries it, and nothing enforces that.*
It was deliberately recorded and not built. **It recurred the next day** — controller v0.206.0 shipped
the R-241 fixes while the vouched golden still carried 0.205.0, so a machine installed that morning
would have received neither. That is the second occurrence in two days (the first, R-239, went
unnoticed until a walk measured it from the customer's side), and it is what a rule without a
mechanism does.

The failure mode is FORGETTING, not lying — nobody ever decided to ship a stale golden. So this gate
is built to catch a missed step, and it is not, and does not pretend to be, an adversarial control.

⚠ WHAT IT CHECKS, AND WHAT IT DELIBERATELY DOES NOT — read this before trusting a green.

It checks that a golden has been **BAKED** for the newest released controller, by looking for that
version's bake-evidence directory in this repo. It does **NOT** check that the golden was **VOUCHED**,
because the vouched version lives ONLY in the hub's `hub_settings` table — there is no copy in git.

That limit is forced, not chosen, and the reasoning is recorded so nobody re-derives it:

  * Both the pre-push hook AND CI run `repo_gates.py --fast`, which by contract selects only gates
    that touch **no network**. A hub-reading gate could therefore be registered as non-fast and would
    then run in NEITHER place — a check that does not run where it applies is precisely the R-29
    census failure this repo's runner was built to end. A gate nobody runs is worse than no gate,
    because it reads as coverage.
  * Recording the vouched version in a tracked file instead would create a second source of truth
    that can drift from the hub, and a green gate over a false claim is the worst outcome available.

**So a bake without a vouch still passes this gate.** The bake is the step that happens in this repo
and is therefore the step this repo can see; the vouch is an operator act against the hub and needs a
different mechanism. That gap is real and is recorded as R-242's remaining half, NOT papered over
here. In practice the two are minutes apart in the same session, and the recurrence this gate is
built for was a missing BAKE.

WHY VERSION AND NOT BEHAVIOUR. It compares version numbers, so a controller release that changed
nothing a customer can see also trips it. That is accepted deliberately: deciding "customer-visible"
mechanically is not possible, judging it by hand is what already failed twice, and the cost of a
false trip is one bake — which is the operation the project wants to be routine anyway. **A gate that
cries wolf is one people learn to bypass, and `--no-verify` exists**, so the tolerance is stated
rather than assumed: if this ever fires on a release nobody wants a golden for, the honest fix is a
recorded waiver in the register, never a habit of bypassing.

FAIL-CLOSED, BUT HONEST ABOUT NOT KNOWING. An absent controller clone, or a CHANGELOG whose top
header cannot be parsed, exits **2 (INCONCLUSIVE)** — never 0. The runner reports 2 distinctly for
exactly this reason: an undetermined result is not a pass, and it is not a conviction either.
"""
import os
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The controller clone sits beside this one. The same sibling assumption reuse_refs_check.py and
# instructions_gate.py already make — an absent sibling is INCONCLUSIVE, never a silent pass.
CONTROLLER_CHANGELOG = os.path.join(os.path.dirname(ROOT), "felhom-controller", "CHANGELOG.md")
EVIDENCE_DIR = os.path.join(ROOT, "documentation", "tests")

# `## v0.206.0 — …` on the FIRST such line: the CHANGELOG is newest-first by convention.
RELEASED_RE = re.compile(r"^##\s+v(\d+)\.(\d+)\.(\d+)\b")
# `golden-0.205.0-2026-08-07/` — the bake-evidence directory the runbook's §4.1 produces.
EVIDENCE_RE = re.compile(r"^golden-(\d+)\.(\d+)\.(\d+)-\d{4}-\d{2}-\d{2}$")


def newest_released():
    """(tuple, str) of the newest controller release, or (None, reason)."""
    if not os.path.isfile(CONTROLLER_CHANGELOG):
        return None, "controller clone not found at %s" % CONTROLLER_CHANGELOG
    with open(CONTROLLER_CHANGELOG, encoding="utf-8") as fh:
        for line in fh:
            m = RELEASED_RE.match(line)
            if m:
                return tuple(int(g) for g in m.groups()), line.strip()[:90]
    return None, "no '## vX.Y.Z' header found in %s" % CONTROLLER_CHANGELOG


def newest_baked():
    """(tuple, str) of the newest golden bake recorded here, or (None, reason)."""
    if not os.path.isdir(EVIDENCE_DIR):
        return None, "bake-evidence directory not found at %s" % EVIDENCE_DIR
    found = []
    for name in os.listdir(EVIDENCE_DIR):
        m = EVIDENCE_RE.match(name)
        if m:
            found.append((tuple(int(g) for g in m.groups()), name))
    if not found:
        return None, "no golden-<VER>-<DATE>/ evidence directory under %s" % EVIDENCE_DIR
    found.sort()
    return found[-1]


def vstr(v):
    return ".".join(str(p) for p in v)


def main():
    released, rel_note = newest_released()
    if released is None:
        print("GOLDEN CURRENCY GATE INCONCLUSIVE: %s" % rel_note)
        sys.exit(2)
    baked, bake_note = newest_baked()
    if baked is None:
        print("GOLDEN CURRENCY GATE INCONCLUSIVE: %s" % bake_note)
        sys.exit(2)

    # Print the evidence unconditionally — a gate that only speaks when it fails teaches nobody what
    # it is watching, and this one is watching the thing two releases already slipped through.
    print("  newest released controller : %s   (%s)" % (vstr(released), rel_note))
    print("  newest golden baked        : %s   (documentation/tests/%s)" % (vstr(baked), bake_note))

    if released > baked:
        print("")
        print("GOLDEN CURRENCY GATE FAILED: controller v%s is released and NO golden carries it "
              "(newest bake is %s)." % (vstr(released), vstr(baked)))
        print("A machine installed right now would receive v%s — the release is written, tested and "
              "pushed, and NOT delivered." % vstr(baked))
        print("Fix: bake a golden per documentation/runbooks/RUNBOOK-manual-build.md §4.1, then vouch "
              "it (a THREE-field change: golden_version + agent_version + min_agent).")
        print("If this release deliberately needs no golden, record a waiver in "
              "documentation/backlog/OPEN-ITEMS.md — never a bypass.")
        sys.exit(1)

    print("golden currency gate OK — the newest released controller has a golden "
          "(NOTE: this checks the BAKE, not the vouch — see the module docstring)")


if __name__ == "__main__":
    main()
