#!/usr/bin/env python3 """ Pre-commit fence gate for docs/API.md. Checks: 1. Fence count is even (every ``` has a matching close). 2. No unpaired fences remain. Exits non-zero on failure, blocking the commit. Only runs when docs/API.md is staged for commit. Usage as pre-commit hook: ln -sf ../../deploy/check-fences.py .git/hooks/pre-commit Usage standalone: python3 deploy/check-fences.py [--file docs/API.md] """ import sys import os import subprocess def check_fences(path): """Return (n_fences, unpaired_lines) for the given file.""" try: lines = open(path, encoding="utf-8").read().split("\n") except FileNotFoundError: return (0, []) # file doesn't exist in this checkout — skip st = [] n = 0 for i, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith("```"): n += 1 if st: st.pop() else: st.append(i) return (n, st) def main(): # Default target target = "docs/API.md" # Allow --file override if "--file" in sys.argv: idx = sys.argv.index("--file") target = sys.argv[idx + 1] # When run as pre-commit hook, only check if the file is staged if not os.path.exists(target): sys.exit(0) # file not in this checkout — nothing to check n, unpaired = check_fences(target) ok = (n % 2 == 0) and (not unpaired) if ok: pairs = n // 2 print(f" gate: fences={n} (even) pairs={pairs} unpaired=none OK") sys.exit(0) else: status = "odd" if n % 2 == 1 else "even" print(f" gate: fences={n} ({status}) unpaired={unpaired} BLOCKED", file=sys.stderr) print(f" ⇒ fix the missing closing fence before committing", file=sys.stderr) if unpaired: print(f" unpaired opening fence at line(s): {unpaired}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()