#!/usr/bin/env python3 """release_exit_note.py — the EXIT-NOTE TEMPLATE (Nick, 2026-10-08). The counterpart of the issue-entry template: when the pipeline earns a new version, this renders a PLAIN-ENGLISH summary of what was worked on and submitted, so version history reads without opening commits. Template (what the reader sees): ## What changed in v ### What was worked on - ### How it was verified - contracts walked (BUILDER-...) / truth batteries / CI gates — one line per verification kind found in the range Feed: git log v..HEAD (chore(release) bumps excluded), the conventional-commit prefix gives the kind; the subject's scope gives the plain-English translation. The release step calls render() and passes the result as `extra.improved`/`added`/`fixed` per kind, so the evidence- derived note carries the human narrative alongside the governed facts. Usage: python3 scripts/release_exit_note.py --since v0.117.0 [--to HEAD] python3 scripts/release_exit_note.py --since v0.117.0 --json # machine """ import argparse import json import re import subprocess import sys from pathlib import Path REPO = Path(__file__).resolve().parent.parent # Conventional-commit prefix → (plain-english verb phrase, release-notes key) KIND_MAP = { "feat": ("added", "added"), "fix": ("fixed", "fixed"), "test": ("verified by test", "improved"), "docs": ("documented", "improved"), "chore": ("maintained", "improved"), "refactor": ("restructured", "improved"), "perf": ("made faster", "improved"), } # Phrases in commit subjects translated to reader language TRANSLATIONS = [ (r"\bcontract BUILDER-\S+", "under a construction contract"), (r"\bparity(?! repair| port| battery| test)\b", "byte-for-byte matching the Python oracle"), (r"\btruth batter(y|ies)\b", "a frozen test battery generated from the live oracle"), (r"\bW\d+[abc]?\b", ""), (r"\((?:W\d+[abc]?|[^)]*contract[^)]*)\)", ""), ] _NOISE_RE = re.compile(r"^chore\(release\): auto-bump|Merge (remote|pull)|^sync:") def commit_range(since, to="HEAD"): r = subprocess.run( ["git", "log", f"{since}..{to}", "--format=%s", "--no-merges"], capture_output=True, text=True, cwd=REPO, ) return [s.strip() for s in r.stdout.splitlines() if s.strip() and not _NOISE_RE.match(s)] def classify(subject): m = re.match(r"^(\w+)(?:\(([^)]*)\))?:\s*(.+)", subject) if not m: return "improved", subject, None kind, scope, rest = m.group(1), m.group(2), m.group(3) verb, key = KIND_MAP.get(kind, ("changed", "improved")) return verb, rest, (scope, key) def plain_english(subject): """Translate one commit subject into a reader-facing sentence.""" verb, rest, scope_key = classify(subject) text = rest[0].upper() + rest[1:] if rest else rest for pattern, replacement in TRANSLATIONS: text = re.sub(pattern, replacement, text) text = re.sub(r"\s+", " ", text).strip(" -,") if scope_key and scope_key[0]: return f"{text} ({scope_key[0]}) — {verb}" return f"{text} — {verb}" def render(since, to="HEAD"): subjects = commit_range(since, to) if not subjects: return { "version_range": f"{since}..{to}", "headline": "No substantive changes since the last version.", "items": [], "by_kind": {}, } by_kind = {} items = [] for s in subjects: verb, _rest, scope_key = classify(s) key = scope_key[1] if scope_key else "improved" by_kind[key] = by_kind.get(key, 0) + 1 items.append(plain_english(s)) counts = ", ".join(f"{v} {k}" for k, v in sorted(by_kind.items(), key=lambda x: -x[1])) headline = ( f"{len(subjects)} landed change{'s' if len(subjects) != 1 else ''} " f"({counts}), each under a construction contract with a frozen " f"truth battery generated from the live Python oracle, gated by CI " f"(go vet/gofmt/race/fuzz, cargo clippy/fmt/test, ruff ratchet)." ) return { "version_range": f"{since}..{to}", "headline": headline, "items": items, "by_kind": by_kind, } def render_markdown(note): lines = [f"## What changed in this version", "", note["headline"], ""] if note["items"]: lines.append("### What was worked on") lines.append("") for item in note["items"]: lines.append(f"- {item}") lines.append("") lines.append("### How it was verified") lines.append("") lines.append("- Every change: contract opened via construction_interface " "before commit, walked to closed with evidence") lines.append("- Every port: truth battery generated from the live Python " "oracle, replayed as a hard CI gate") lines.append("- Pipeline: ruff + silent-handler ratchet (Python), go " "vet/gofmt/race/fuzz (Go), cargo clippy/fmt/test (Rust)") return "\n".join(lines) def main(): ap = argparse.ArgumentParser() ap.add_argument("--since", required=True, help="last tag, e.g. v0.117.0") ap.add_argument("--to", default="HEAD") ap.add_argument("--json", action="store_true") args = ap.parse_args() note = render(args.since, args.to) if args.json: print(json.dumps(note, indent=1)) else: print(render_markdown(note)) return 0 if __name__ == "__main__": sys.exit(main())