# `proposals/` — Amendment Proposal Files Every constitutional change enters through a proposal file in this directory. The proposal file is the machine-readable half of an amendment PR: it carries the metadata the vote gate needs (id, proposer, change class, vote window, threshold), while the PR's diff against `constitution/` carries the actual text change. CI refuses any PR that modifies `constitution/` without exactly one new proposal file here. This README is a convention guide. The **authoritative format** is enforced by `src/govtool/schemas.py` and documented in [`docs/pipeline.md`](../docs/pipeline.md) — if this page and the schema ever disagree, the schema wins (Article 1: the repository is the source of truth, and the validators are part of it). ## Creating a proposal Never hand-write a proposal file from scratch. Scaffold it, so the id, timestamps, and signature envelope are generated correctly: ```bash python -m govtool proposal new \ --title "Lower funding-pool quorum to 40%" \ --proposer ``` Then: ```bash # 1. Make your edits under constitution/ # 2. Validate everything locally before pushing: python -m govtool proposal validate proposals/.yaml python -m govtool classify # confirms the change class CI will assign python -m govtool validate # full constitution source check pytest # the whole suite, including gate e2e ``` Open the PR using the amendment template — CI runs the same three checks and will block on any mismatch between your self-assessed class and the classifier's verdict. ## Lifecycle A proposal moves through these states (full state machine, including who may trigger each transition and the exact CI jobs involved, in [`docs/pipeline.md`](../docs/pipeline.md)): | State | Meaning | Where it lives | |-------------|----------------------------------------------------------------|--------------------------------------| | `draft` | Wording under discussion; not yet eligible for ballots | PR marked draft / discussion issue | | `open` | PR open, proposal validated, classifier verdict posted | open PR + this directory | | `voting` | Vote window open; signed ballots being collected | PR + ledger `ballot` entries | | `ratified` | Threshold met at window close; ratification workflow merges | merged to `main`, ledger `ratification` entry, `constitution/version.yaml` bumped | | `rejected` | Threshold not met, quorum failed, or window expired | PR closed, ledger `rejection` entry | | `withdrawn` | Proposer withdrew before window close | PR closed, ledger `withdrawal` entry | Two properties hold no matter what: - **Every terminal state lands in the ledger.** Rejected and withdrawn proposals are part of the constitutional record, not garbage-collected history. The proposal file of a non-ratified amendment stays in the PR (and thus in git history), while its outcome entry stays in the ledger forever. - **Nothing merges without the gate.** Branch protection requires the `pr-gate` and `ratify` workflows; there is no human override path that bypasses tally verification. ## Voting on an open proposal ```bash python -m govtool ballot cast \ --proposal \ --choice yes|no|abstain \ --key ``` Ballots are signed, bound to the proposal id and the exact content hash of the text being voted on (so a post-hoc edit invalidates every ballot already cast), and appended to the ledger. `govtool tally ` shows the live count; the count that matters is the one the ratification workflow computes independently at window close. ## Naming Files are named `.yaml`, where the id is assigned by the scaffolder (monotonic, zero-padded, prefixed by target: `K-` for kernel, `U-` for userland — e.g. `K-0007.yaml`). Do not rename a proposal file after ballots exist: ballots bind to the id.