# The Amendment Pipeline This document specifies the full lifecycle of a constitutional amendment, from draft to ratified release, and exactly what the CI vote gate enforces at each step. It is the developer-level companion to Articles 3 (Amendment Procedure), 4 (Ratification), and 5 (Voting) of the kernel. The pipeline is implemented across these modules: | Stage | Module | CI workflow | |---------------|-------------------------|------------------------------| | Proposal | `govtool.proposal` | `.github/workflows/pr-gate.yml` | | Classification| `govtool.classifier` | `pr-gate.yml` | | Voting | `govtool.ballots` | (off-CI: citizens sign locally) | | Eligibility | `govtool.eligibility` | `pr-gate.yml` | | Tally + gate | `govtool.tally`, `govtool.gate` | `pr-gate.yml` | | Ratification | `govtool.gate`, `govtool.ledger` | `.github/workflows/ratify.yml` | All persistent state lives in two places: **git** (the constitutional text and its history) and **the ledger** (the append-only record of governance events). There is no database. There is no admin panel. If it isn't in git or the ledger, it didn't happen. --- ## 1. Proposal An amendment is a pull request against `main` that modifies files under `constitution/` (and/or `citizens/registry.yaml`, which is constitutional text). The PR becomes a *proposal* when a citizen registers it: ```bash govtool propose create --pr --title "" --key <keyfile> ``` This produces a ledger entry of type `proposal`: - `proposal_id` — deterministic id derived from the PR number and diff digest - `author` — citizen id (must be `active` in the registry at registration time) - `diff_digest` — SHA-256 over the canonical serialization of the PR's diff against the merge base - `classification` — the label the classifier assigned at registration - `window` — voting window open/close timestamps, computed from the policy in `constitution/kernel/article-05-voting.yaml` - author's Ed25519 signature over the canonical payload **The diff digest is the anchor of the whole pipeline.** Ballots sign over it. The gate recomputes it. If the PR is amended after registration, the digest changes and the gate fails with `DIGEST_MISMATCH` — voters can never have a different text merged than the one they ratified. Re-registering after a change opens a fresh window with a fresh proposal id; the old proposal entry remains in the ledger as superseded. ### Proposal eligibility `govtool.eligibility` enforces, at registration time: - the author exists in `citizens/registry.yaml` - the author's status is `active` (not `suspended`, not `departed`) - the author's signature verifies against their registered public key - the registry snapshot used is the one at the proposal's merge base — a PR cannot add citizens and have those citizens vote on the same PR. The eligible voter set is frozen at proposal registration. That last rule closes the most obvious capture move: stuff the registry and ratify with your own sock puppets in one motion. It is covered by `tests/test_eligibility.py` and the end-to-end gate tests. --- ## 2. Classification The classifier (`govtool.classifier`, full spec in [semver.md](semver.md)) inspects the structured diff and emits one of: - `kernel-major` — breaking change to kernel meta-rules or invariants - `kernel-minor` — additive, non-breaking kernel change - `userland-minor` — new userland module - `userland-patch` — parameter/clarification change within existing userland The label is computed, recorded in the proposal entry, and posted as a PR label and check output. The label selects the **gate tier** — which quorum and threshold apply — by reading the policy table in `constitution/kernel/article-03-amendment-procedure.yaml`. Defaults at v0: | Label | Threshold | Quorum | |------------------|--------------------|--------| | `kernel-major` | ≥ 2/3 of votes cast (abstentions excluded from the ratio, counted for quorum) | ≥ 50% of eligible citizens | | `kernel-minor` | > 1/2 of votes cast | ≥ 33% | | `userland-minor` | > 1/2 of votes cast | ≥ 33% | | `userland-patch` | > 1/2 of votes cast | ≥ 25% | The numbers are policy, read from YAML at gate time — changing them is itself a `kernel-major` amendment (the classifier guarantees this: any diff touching `article-03` is kernel-major by definition). --- ## 3. Voting Citizens vote during the window with: ```bash govtool vote cast --proposal <id> --choice yes|no|abstain --key <keyfile> ``` A ballot is a ledger entry of type `ballot` whose canonical payload contains: - `proposal_id` and `diff_digest` (a ballot is bound to an exact text) - `voter` — citizen id - `choice` — `yes` | `no` | `abstain` - `cast_at` — timestamp - Ed25519 signature over the canonical payload Rules enforced by `govtool.ballots` and re-verified by the gate: - **One person, one vote.** No capital weighting, no delegation in v0. - **Supersession.** If a citizen casts multiple ballots, the latest one with a valid signature inside the window counts. Earlier ballots stay in the ledger, marked superseded in the tally output. Changing your mind is legal and public. - **Window enforcement.** Ballots timestamped outside `[open, close]` are rejected at tally with `BALLOT_OUT_OF_WINDOW`. They remain in the ledger — rejection is recorded, not erased. - **Abstentions count toward quorum, not toward the threshold ratio.** Showing up and abstaining is participation; it makes quorum honest without forcing a side. --- ## 4. The gate `govtool gate check --proposal <id>` is what CI runs when the window closes (and on every push to the PR, in dry-run mode for early feedback). It executes the following checks **in order**, and emits a verdict object whether it passes or fails: 1. **Schema validation** — every changed YAML file under `constitution/` parses and validates against `govtool.schemas`. Malformed constitutional text never reaches a vote. 2. **Digest check** — recompute the diff digest from the actual PR state; compare against the registered proposal. Mismatch → `DIGEST_MISMATCH`. 3. **Classification check** — recompute the label; it must equal the registered label (prevents registering as patch, then escalating the diff). 4. **Invariant checks** — apply the diff to a working copy and verify every rule in `constitution/invariants.yaml` still holds over the amended text (e.g.: the amendment procedure cannot be made amendable by simple majority; the right to fork cannot be removed; citizenship cannot be made revocable without due process; the ledger cannot be made mutable). A vote cannot override an invariant — that is the constitutional analogue of a type error, and it fails *before* tallying so a doomed proposal doesn't burn a voting window. Removing or weakening an invariant is possible only via the explicit invariant-amendment path in Article 6, which the classifier force-labels `kernel-major`. 5. **Ledger integrity** — `govtool ledger verify` over the full chain. A gate never trusts a ledger it hasn't verified. 6. **Ballot verification** — for every ballot referencing the proposal: signature valid, voter eligible (against the frozen registry snapshot), digest matches, inside window. Each rejected ballot appears in the verdict with its rejection reason. 7. **Quorum** — `participating / eligible ≥ quorum` for the proposal's tier. Failure → `QUORUM_NOT_MET` (with the exact numbers). 8. **Threshold** — `yes / (yes + no)` against the tier threshold. Failure → `THRESHOLD_NOT_MET`. The verdict is a JSON object written to the check output and (in non-dry runs) appended to the ledger as a `tally` entry — including for failures. The verdict format is stable and machine-readable; downstream tooling (the release cadence counter, the benchmark harness in later milestones) consumes it. The gate is **pure with respect to its inputs**: given the same repo state, registry snapshot, and ledger, it produces the same verdict on any machine. Anyone can re-run the gate locally and confirm CI's verdict — CI has no authority, only convenience. --- ## 5. Ratification On a passing verdict, the ratification workflow (`ratify.yml`): 1. Merges the PR (merge commit — amendment history must stay legible; no squashing constitutional history). 2. Bumps `constitution/version.yaml` according to the classification (major/minor/patch) and tags the commit `vX.Y.Z`. 3. Appends a `ratification` ledger entry binding together: the proposal id, the tally entry hash, the merge commit SHA, the new version, and the ratified-at timestamp. 4. Updates the release cadence record (proposal-opened → ratified latency) in the verdict, which feeds the public cadence counter. The ratification entry is the constitutional moment. The merge commit and the ledger entry reference each other by hash; falsifying either is detectable from the other. ### Failure path A failing verdict appends a `tally` entry with `passed: false` and the gate posts the verdict on the PR. The PR may be revised and **re-registered** (new proposal, new window) or closed. Either way, the attempt is permanent ledger history. Failed amendments are first-class data — the adversarial self-play milestone consumes them. --- ## Sequence diagram ``` Citizen Git/PR Ledger CI (gate) | draft branch | | | |----------------->| | | | propose create | | | |--------------------------> proposal entry | | | | dry-run gate | | |<--------------------------------- verdict | | vote cast (xN) | | | |--------------------------> ballot entries | | | window closes | | | | | full gate | | | |<----- verify chain | | | |------> tally entry | | | merge + tag | | | |<------------------------------ ratify | | | |<-- ratification entry | ``` --- ## Threat notes (what the order of checks buys you) - **Bait-and-switch text:** blocked by the digest binding (checks 2 and 6). - **Registry stuffing in the same PR:** blocked by freezing the voter set at the merge-base registry (check 6 / eligibility rules). - **Tier dodging** (register as patch, push kernel changes later): blocked by re-classification at gate time (check 3). - **Majority overriding meta-rules:** blocked by invariants running *before* tally (check 4) — there is no vote count that passes a gate the invariants fail. - **Ledger tampering to fake a tally:** blocked by chain verification (check 5) and by the ratification entry cross-referencing the merge commit. Each of these has a corresponding test in `tests/test_gate_e2e.py` or the module tests. New exploits get new tests; see CONTRIBUTING.md.