Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/bills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Bill gate

# The technical department's gate. ICC returns the approved artifact, the tech
# department opens a pull request, and this runs on it.
#
# Merging a bill file never amends the constitution. The constitution changes
# only through `act apply`, with its manifest and its tripwire.

on:
pull_request:
paths:
- 'bills/**'
- 'constitution/current.yaml'
- 'acts/register.yaml'
workflow_dispatch:

permissions:
contents: read
pull-requests: read

jobs:
gate:
name: Check bills
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The gate diffs against the base, so it needs the history to do it.
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm

- run: npm ci

# Schema, target resolution, staleness, evidence files and their
# checksums, and the threshold for any recorded approvals — plus the
# cross-bill checks no single-bill run can see.
#
# Warnings surface as annotations, not only in this log: a warning nobody
# sees is a warning that does not exist.
- name: Run the bill gate
id: gate
env:
BASE_REF: origin/${{ github.base_ref }}
run: npm run bill-gate

# What the approving bodies read, so the tech department reviews the same
# thing rather than a YAML diff.
- name: Upload the gate report
if: always()
uses: actions/upload-artifact@v4
with:
name: bill-gate-report
path: bill-gate-report.md
if-no-files-found: ignore

- name: Render the instruments this pull request touches
if: always()
env:
BASE_REF: origin/${{ github.base_ref }}
run: |
set -e
mkdir -p rendered
for f in $(git diff --name-only "$BASE_REF"...HEAD -- 'bills/**/*.yaml' 'bills/**/*.yml' || true); do
[ -f "$f" ] || continue
echo "rendering $f"
node src/cli.mjs bill render "$f" || true
node src/cli.mjs bill ballot "$f" || true
done
find bills -name '*-ballot.html' -o -name '*.html' -o -name '*.txt' 2>/dev/null \
| while read -r r; do cp "$r" rendered/ 2>/dev/null || true; done
ls -la rendered || true

- name: Upload the rendered instruments
if: always()
uses: actions/upload-artifact@v4
with:
name: rendered-instruments
path: rendered/
if-no-files-found: ignore

# The whole corpus still has to hold together.
- name: Validate the corpus
run: npm run validate

# The suite asserts against built output as well as source, so the build
# runs first — the deploy workflow does this and this one did not, which
# is how 29 tests failed on "dist/ missing" the first time this ran.
- name: Build
run: npm run build

- name: Check links
run: npm run linkcheck

- name: Test
run: npm test
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ env:
# other ships a site whose links point at a directory that is not there.
BASE_PATH: /
SITE_ORIGIN: https://constitution.stmorg.in
# /bills/ is record and always ships. /propose/ is action and ships when the
# desk behind it is staffed — see process/ADOPTION.md for the four conditions.
PROPOSE_ENABLED: 'false'

jobs:
# A schema violation, a failing test or a dead internal link must block
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ css/style.css.map
*.log
.DS_Store
assets/og/
bill-gate-report.md
8 changes: 3 additions & 5 deletions .night-run/provisions.sha256
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
{
"note": "Baseline for the unattended run. Any change outside PERMITTED in tripwire.mjs aborts the run.",
"file": "constitution/current.yaml",
"taken_at_commit": "f36a02c",
"permitted": [
"art-11.content"
],
"taken_at_commit": "48af740",
"permitted": [],
"count": 82,
"provisions": {
"preamble.title": "59e371c5cac498cc5ae8361dafb8f60b4023665a8fb606860a289dc3d399a8dc",
Expand Down Expand Up @@ -63,7 +61,7 @@
"art-11-s-1.content": "17612d9aca69f815ac99b45a8ac2c0e29108b3ea901880d03d95a709a1b00dde",
"art-11-s-2.title": "d456583904a7dd394696064df1106bc90fc44eefc1e884d5420a4b0ebf7794db",
"art-11-s-2.content": "544267149f490925bfeda78b4129a95ae7f65a58f62dd8986c630c72a56de380",
"art-12.title": "3303026655c83910b9a0ae51382813956e58271c276fd5a27d05f95b847b139a",
"art-12.title": "d10ccefc6d3a23b2fa9a1822dcddab633ccf745a419283eed73b321317088201",
"art-12.content": "8c5ac0ac874e1d0b65dbf0a7c081db4bb42c4886967e1288982734bffc8704ad",
"art-13.title": "8f672276e8b9971baef1e26d73b6733f436575230b342e58e5722c11536263fc",
"art-13.content": "3124807d0cfcf79deaa8683b46627fd1859abaf233627aa69ddeb8b9e7ed75ef",
Expand Down
Binary file modified .night-run/tripwire.mjs
Binary file not shown.
12 changes: 12 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ tests/ run against both the source and the built output
- Accessibility is not optional: landmarks, visible focus, full keyboard operation,
4.5:1 contrast in both themes, `prefers-reduced-motion` honoured.

## Testing a guard

Guards are tested on **throwaway branches with the synthetic edit committed** — never by stashing
around uncommitted work. Commit first; destructive git operations near an uncommitted tree are how a
careful process loses something.

```bash
git commit -am "wip" # your real work is safe
# make the synthetic bad edit, commit it, run the guard, then:
git reset --hard <the commit before> # the synthetic edit is gone
```

## Pull requests

- Branch from `main`. Reference the issue number.
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,38 @@ the workflow with `include_cname: true`. The CNAME is excluded by default and CI
fails if it appears without being asked for, so a build cannot silently repoint a
live domain.

## Amending the constitution

Amendments are not edits. A change to provision text requires an instrument, and this repository
carries the pipeline that produces one.

**The bill is the source of truth; the signed PDF is a rendering of it.** An amendment is drafted as
YAML, validated, rendered into the house style of the existing Acts, printed, signed and archived —
and then applied mechanically, because the instrument and the patch are the same object. The three
Acts of 2024 were authored the other way round, as prose applied to the text by hand, which is what
produced a half-applied constitution and fourteen reconciliation questions.

```bash
npx opencodelaw bill new --name my-amendment
npx opencodelaw bill validate bills/2026/my-amendment.yaml # prints the before/after diff
npx opencodelaw bill render bills/2026/my-amendment.yaml # the instrument, for signature
```

Approval is governed by **Article 16(3)**: two thirds of those present and voting in the board, the
intermediate board and the units. All three bodies are required and the pipeline will not enact on
fewer.

| | |
|---|---|
| [process/PROPOSING.md](process/PROPOSING.md) | How to write a bill. Start here. |
| [process/AMENDMENT-PROCESS.md](process/AMENDMENT-PROCESS.md) | Roles, lifecycle, thresholds, versioning. |
| [process/MINUTES-TEMPLATE.md](process/MINUTES-TEMPLATE.md) | What each body's record of resolution must contain. |
| [process/ADOPTION.md](process/ADOPTION.md) | What must be true before the in-browser bill builder goes live. |

The in-browser builder at `/propose/` is built but not yet published: it is an action surface, and
it opens when the ICC is ready to receive drafts. Authoring a bill by hand produces exactly the same
file — copy `bills/TEMPLATE.yaml` and run `npx opencodelaw bill validate`.

## Honest limitations

- **Reconciliation is a human process.** The tools compare texts, classify provisions
Expand Down
5 changes: 5 additions & 0 deletions acts/register.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@


acts:
- id: act-1-2024
origin: external-pdf
number: 1
year: 2024
title: First Constitution Amendment Act, 2024
Expand Down Expand Up @@ -76,6 +79,7 @@ acts:
source_lines: 158-181
note: Act restates the title as "Suspension/Termination".
- id: act-2-2024
origin: external-pdf
number: 2
year: 2024
title: Second Constitution Amendment Act, 2024
Expand Down Expand Up @@ -182,6 +186,7 @@ acts:
source_lines: 105-113
note: Act restates the title as "Exit Process".
- id: act-3-2024
origin: external-pdf
number: 3
year: 2024
title: Third Constitution Amendment Act, 2024
Expand Down
165 changes: 165 additions & 0 deletions bills/TEMPLATE.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# ---------------------------------------------------------------------------
# A BILL — a proposed amendment to the constitution.
#
# Copy this file to bills/<year>/ and edit it. You do not need to know YAML
# beyond "keep the indentation as you found it".
#
# npx opencodelaw bill validate bills/2026/my-bill.yaml
#
# Run that as often as you like. It checks your drafting and prints exactly
# what your bill would change, before and after. Read that diff — it is what
# the approval meetings will read.
#
# Author's guide: process/PROPOSING.md
# The rules this follows: process/AMENDMENT-PROCESS.md
# ---------------------------------------------------------------------------

opencodelaw_bill: "1.0"

bill:
short_title: An Act to ... # how the Act will be titled
# also_known_as: Membership Act, 2026 # optional familiar name
year: 2026
number: ~ # leave null — the ICC assigns this at submission
type: amendment # amendment | corrigendum | revision

moved_by:
name: Your Name # recorded permanently, from the moment you draft
role: Member # e.g. Unit Head, IBM-Technical Coordinator
contact: you@stmorg.in

drafted: 2026-01-15

# The constitution version you wrote this against. If the constitution moves
# on before your bill is approved, validation will tell you to rebase — so
# nobody ever votes on a change written against text that no longer exists.
base_version: "3.0.0"

version_bump: minor # minor for an amendment; major only for a revision

status: draft # you leave this alone; the pipeline moves it

history: [] # every status change is recorded here automatically

# ---------------------------------------------------------------------------
# Why you are proposing this. Explanatory ONLY.
#
# This becomes the STATEMENT OF OBJECTS AND REASONS at the end of the printed
# Act. It is never operative and can never be cited as the authority for
# anything — a rule this project learned the hard way, when Act 2 of 2024 set
# one amendment threshold in its operative text and a different one here.
# ---------------------------------------------------------------------------
objects_and_reasons: |
1. Article N is amended to ... .

# ---------------------------------------------------------------------------
# WHAT THE BILL ACTUALLY DOES. One entry per provision you are changing.
#
# The single rule that matters: `text` is the COMPLETE resulting text of that
# provision — the whole thing, as it should read after your amendment.
#
# Not a diff. Not "insert after the words". Not "delete the third sentence".
# Write out the provision as you want it to end up, in full.
#
# That is what makes an Act safe to apply: the tool compares what the
# provision says now with what you wrote, so applying the same Act twice
# changes nothing the second time, and an Act that no longer matches the text
# it was written against refuses to apply rather than corrupting it.
# ---------------------------------------------------------------------------
operations:
- id: op-1
operation: substitute # substitute | insert | omit | retitle | reserve
target: art-3 # the provision id, e.g. art-3 or art-6-s-2
scope: article # article (the whole provision) | clause
# title: Aims and Objectives # set this only if the Act states a heading
text: |
The complete new text of Article 3, written out in full, exactly as it
should read once this Act is applied.

# A second example — delete to remove it.
# - id: op-2
# operation: insert
# target: art-22
# scope: article
# title: Data Protection
# text: |
# The complete text of the new Article 22.

# ---------------------------------------------------------------------------
# APPROVALS — Article 16(3).
#
# All three bodies must approve. This is settled constitutional policy, not a
# default: a bill approved by fewer than three bodies cannot be enacted, and
# no checklist or shortcut can lower it.
#
# The ICC fills this in from the minutes. Leave the numbers null.
#
# `abstain` is excluded from the threshold — 16(3) says "present and voting".
# ---------------------------------------------------------------------------
approvals:
# One entry per body. The ICC fills these in from the signed record.
#
# `bill_sha256` is the substantive hash of the bill AS VOTED. `bill validate`
# prints it; the presiding officer reads it into the minutes. If the bill is
# edited afterwards the hash moves, the approval is void, and that body must
# resolve again — including when your own operations were untouched, because
# a provision can become contradictory purely because other articles moved.
#
# Get the text right before you circulate.
#
# `evidence.path` is a file in this repository. A live link is never evidence:
# a URL is mutable, unattributable, and dies with the platform.
- body: board
meeting:
date: ~
mode: in-person # in-person | online | hybrid
place: ""
presiding: ""
present: ~
for: ~
against: ~
abstain: ~ # excluded from the threshold
bill_sha256: ~
# Added by the ICC once the record exists. Left out of a draft entirely:
# an empty path and an empty checksum are not an unfilled form, they are a
# claim that a record exists at "" — which is why the validator rejects it.
#
# evidence:
# kind: minutes # minutes | ballot-tally | poll-export
# path: bills/2026/evidence/bill-1-2026-board-minutes.pdf
# sha256: <64 hex characters — `bill validate` will tell you if it is wrong>
recorded_by: ""
- body: intermediate-board
meeting: {date: ~, mode: in-person, place: "", presiding: ""}
present: ~
for: ~
against: ~
abstain: ~
bill_sha256: ~
# evidence: {kind: minutes, path: ..., sha256: ...}
recorded_by: ""
- body: units
meeting: {date: ~, mode: online, place: "", presiding: ""}
present: ~
for: ~
against: ~
abstain: ~
bill_sha256: ~
# Units vote online where the by-laws permit it. A poll is a voting
# MECHANISM, not a record: the evidence is a static export of the result,
# attested by the ICC coordinator, archived and hashed like minutes. The
# poll's URL proves nothing.
# evidence: {kind: poll-export, path: ..., sha256: ...}
recorded_by: ""

# ---------------------------------------------------------------------------
# ENACTMENT — filled in by the ICC when the Act is signed. Leave it alone.
# ---------------------------------------------------------------------------
enactment:
act_number: ~
act_year: ~
assent_date: ~
assented_by: ~
signed_by: ~
signed_pdf: ~
signed_pdf_sha256: ~
Loading
Loading