#!/usr/bin/env python3 """End-to-end demo of the FablePool amendment pipeline. Builds a scratch governance repository in ``demo/scratch/`` (a copy of this repo's constitution with a fresh five-citizen registry), then runs two full amendment lifecycles through the same CLI that CI uses: 1. A *minor* userland amendment that PASSES the vote gate (4 yes / 1 no, quorum and simple-majority threshold both met), is ratified, and bumps the constitution's minor version. 2. A *major* kernel amendment that FAILS the vote gate (3 yes / 2 no = 60%, below the two-thirds supermajority a breaking kernel change requires) and is recorded as rejected. Every step — enrollment, ballots, gate results, ratification — lands on the same append-only audit ledger, which is verified at the end. In the real pipeline the proposal rides a git pull request containing the actual text diff; this scratch demo declares the changed paths on the proposal document so the classifier and gate can grade it without a remote. Run with: python demo/run_demo.py Exits 0 only if both scenarios behave exactly as the constitution requires. """ from __future__ import annotations import sys import shutil from pathlib import Path import yaml HERE = Path(__file__).resolve().parent ROOT = HERE.parent SCRATCH = HERE / "scratch" # Allow running from a clean checkout without `pip install -e .` # (library dependencies must still be installed). try: from govtool.cli import main as govtool_main, RepoPaths, _read_version except ImportError: # pragma: no cover sys.path.insert(0, str(ROOT / "src")) from govtool.cli import main as govtool_main, RepoPaths, _read_version CITIZENS = ["alice", "bob", "carol", "dave", "erin"] PASS_ID = "0002-extend-disbursement-review" FAIL_ID = "0003-emergency-fast-track" def banner(title: str) -> None: print("\n" + "=" * 72) print(title) print("=" * 72) def gov(*args: str, expect: int | None = 0) -> int: """Invoke the govtool CLI in-process against the scratch repo.""" argv = [*args, "--repo", str(SCRATCH)] print(f"\n$ govtool {' '.join(args)}") rc = govtool_main(list(argv)) if expect is not None and rc != expect: print(f"\nFATAL: expected exit code {expect}, got {rc} " f"for: govtool {' '.join(args)}", file=sys.stderr) sys.exit(1) return rc def read_yaml(path: Path): return yaml.safe_load(path.read_text(encoding="utf-8")) def proposal_status(pid: str) -> str: for candidate in ( SCRATCH / "proposals" / pid / "proposal.yaml", SCRATCH / "proposals" / f"{pid}.yaml", SCRATCH / "proposals" / pid / f"{pid}.yaml", ): if candidate.exists(): doc = read_yaml(candidate) or {} return str(doc.get("status", doc.get("state", "?"))) return "?" def constitution_version() -> str: _, _, version = _read_version(RepoPaths(SCRATCH)) return version def build_scratch() -> None: if SCRATCH.exists(): shutil.rmtree(SCRATCH) SCRATCH.mkdir(parents=True) shutil.copytree(ROOT / "constitution", SCRATCH / "constitution") print(f"scratch governance repo created at {SCRATCH}") def main() -> None: banner("FablePool amendment-pipeline demo") build_scratch() # ------------------------------------------------------------------ banner("Step 1 — enroll five citizens (one person, one vote)") for name in CITIZENS: keyfile = SCRATCH / "keys" / f"{name}.key" gov("keygen", "--out", str(keyfile)) gov("citizen", "add", "--id", name, "--name", name.title(), "--key-file", str(keyfile)) gov("citizen", "list") # ------------------------------------------------------------------ banner("Step 2 — amendment that PASSES the vote gate (userland, minor)") gov("proposal", "new", "--id", PASS_ID, "--title", "Extend disbursement review window to 14 days", "--author", "alice", "--level", "minor", "--summary", "Gives every citizen two weeks to object to any " "funding-pool disbursement before it executes.", "--change", "constitution/userland/funding-pool.yaml") gov("proposal", "open", "--id", PASS_ID) for name, choice in [("alice", "yes"), ("bob", "yes"), ("carol", "yes"), ("dave", "yes"), ("erin", "no")]: gov("vote", "cast", "--proposal", PASS_ID, "--citizen", name, "--choice", choice, "--key", str(SCRATCH / "keys" / f"{name}.key")) gov("vote", "verify", "--proposal", PASS_ID) gov("proposal", "close", "--id", PASS_ID) version_before = constitution_version() gov("gate", "--proposal", PASS_ID, "--record", expect=0) gov("ratify", "--proposal", PASS_ID, expect=0) version_after = constitution_version() if version_after == version_before: print("FATAL: ratification did not bump the constitution version", file=sys.stderr) sys.exit(1) if proposal_status(PASS_ID) != "ratified": print(f"FATAL: expected {PASS_ID} to be ratified, " f"got {proposal_status(PASS_ID)!r}", file=sys.stderr) sys.exit(1) print(f"\n>>> {PASS_ID} ratified: constitution " f"{version_before} -> {version_after}") # ------------------------------------------------------------------ banner("Step 3 — amendment that FAILS the vote gate (kernel, major)") gov("proposal", "new", "--id", FAIL_ID, "--title", "Fast-track emergency powers without review", "--author", "bob", "--level", "major", "--summary", "Allows the emergency procedure to bypass post-hoc " "review. A breaking kernel change: requires a " "two-thirds supermajority.", "--change", "constitution/kernel/article-09-emergency.yaml") gov("proposal", "open", "--id", FAIL_ID) # 3 yes / 2 no = 60% support: full quorum, but under the 2/3 bar. for name, choice in [("alice", "yes"), ("bob", "yes"), ("carol", "yes"), ("dave", "no"), ("erin", "no")]: gov("vote", "cast", "--proposal", FAIL_ID, "--citizen", name, "--choice", choice, "--key", str(SCRATCH / "keys" / f"{name}.key")) gov("vote", "verify", "--proposal", FAIL_ID) gov("proposal", "close", "--id", FAIL_ID) gov("gate", "--proposal", FAIL_ID, "--record", expect=1) gov("proposal", "transition", "--id", FAIL_ID, "--to", "rejected") if proposal_status(FAIL_ID) != "rejected": print(f"FATAL: expected {FAIL_ID} to be rejected, " f"got {proposal_status(FAIL_ID)!r}", file=sys.stderr) sys.exit(1) version_final = constitution_version() if version_final != version_after: print("FATAL: a failed amendment must never change the constitution " f"version ({version_after} -> {version_final})", file=sys.stderr) sys.exit(1) print(f"\n>>> {FAIL_ID} correctly blocked: 60% support < 2/3 supermajority") # ------------------------------------------------------------------ banner("Step 4 — public audit trail") gov("ledger", "verify", expect=0) gov("ledger", "show", "--last", "40") # ------------------------------------------------------------------ banner("Demo summary") print(f" citizens enrolled .......... {len(CITIZENS)}") print(f" {PASS_ID}: PASSED gate, ratified, " f"version {version_before} -> {version_after}") print(f" {FAIL_ID}: FAILED gate (supermajority not met), rejected") print(f" audit ledger ............... verified, hash chain intact") print("\nDEMO COMPLETE — both scenarios behaved exactly as the " "constitution requires.") if __name__ == "__main__": main()