Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1448452
Phase 0: verify audit findings empirically
KiranPranay Aug 14, 2026
5804c40
Phase 1: restructure to the target architecture
KiranPranay Aug 14, 2026
efa2202
Phase 1: add constitution/current.yaml as the working head
KiranPranay Aug 14, 2026
c3f57b8
Toolchain: CI matrix on Node 20 and 22, record deferred sanitize-html…
KiranPranay Aug 14, 2026
ddbffb4
Phase 3 prerequisite: commit extracted Act text
KiranPranay Aug 14, 2026
9aa00e7
Phase 2: schema, permanent citation identity, validator
KiranPranay Aug 14, 2026
846068f
Phase 3: provenance normaliser, with the <br> trap under test
KiranPranay Aug 14, 2026
736b44a
Phase 3: PROVENANCE.md — 24 comparisons, 1 DIVERGENT
KiranPranay Aug 14, 2026
6c97858
Phase 3: apply Q14 deletion, enforce SOR-authority rule
KiranPranay Aug 14, 2026
cd34b29
night-run: resumability scaffolding and provision tripwire
KiranPranay Aug 14, 2026
bec36ab
night-run L1: amendment register
KiranPranay Aug 14, 2026
107e7bb
night-run L2-L5: Act 1 recorded, honest versioning, validation green
KiranPranay Aug 14, 2026
a98446e
Phase 4-7: static build, SEO, UI assets, link checking
KiranPranay Aug 14, 2026
87db08e
Fix test discovery on Node 22, add CLI and starter template
KiranPranay Aug 14, 2026
8499690
Rewrite README and CONTRIBUTION for the rebuilt engine
KiranPranay Aug 14, 2026
fa881f8
Consolidate design tokens, remove a fourth hue, fix print colour leak
KiranPranay Aug 14, 2026
cae3709
Fix link contrast on the reconciliation banner
KiranPranay Aug 15, 2026
d2e7dfc
Accessibility 100 across both themes and both viewports
KiranPranay Aug 15, 2026
dab3d6d
Engine/content split, reusability tests, screenshots
KiranPranay Aug 15, 2026
0a034eb
night-run: morning report
KiranPranay Aug 15, 2026
92507e7
Cutover: build for the apex domain, not project Pages
KiranPranay Aug 15, 2026
3a5fd20
Redirect legacy Apache archive URLs instead of 404ing
KiranPranay Aug 15, 2026
0421262
title_source: mark which headings carry legal force
KiranPranay Aug 15, 2026
c642349
Add cutover record and remaining checklist
KiranPranay Aug 15, 2026
32d5d5d
Stop publishing a version the board never adopted; invert the heading…
KiranPranay Aug 15, 2026
0a4730e
Record direct evidence that no GitHub certificate covers the domain
KiranPranay Aug 15, 2026
7bdbf8a
Record board authorisation and build the expected-change manifest
KiranPranay Aug 15, 2026
38a9f70
Apply Act 1 of 2024 (Membership Act) to Articles 6 and 7
KiranPranay Aug 15, 2026
f36a02c
Apply Act 2 of 2024 to the preamble and Articles 3, 4, 5, 8, 14, 15, …
KiranPranay Aug 15, 2026
83e9c96
Apply Act 3 of 2024 (Finance Act); adopt version 3.0.0
KiranPranay Aug 15, 2026
3bce741
Record the six section anchors that changed meaning
KiranPranay Aug 15, 2026
7ecc899
Fix a containment stride that reported exact matches as divergent
KiranPranay Aug 15, 2026
b743b3e
Note Phase 8 requirements from applying three Acts; build nothing
KiranPranay Aug 15, 2026
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
124 changes: 124 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Deploy

on:
push:
branches: [rebuild/v3, main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
skip_cname:
description: >-
Omit the CNAME file. Only for a deliberate rollback to the
servicetomankind.github.io URL — constitution.stmorg.in points here now
and a deploy without CNAME can drop the domain setting.
type: boolean
default: false
required: false

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: false

env:
# The custom domain serves from the root, so the base path is "/". Every
# internal link and asset derives from these two; changing one without the
# other ships a site whose links point at a directory that is not there.
BASE_PATH: /
SITE_ORIGIN: https://constitution.stmorg.in

jobs:
# A schema violation, a failing test or a dead internal link must block
# publication rather than ship a broken or legally incorrect constitution.
validate:
name: Validate and test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 22 is the target floor; 20 stays until the local floor moves. See README.
node: ['20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci

- name: Validate constitution
run: npm run validate

- name: Build
run: npm run build

- name: Check links
run: npm run linkcheck

- name: Test
run: npm test

build:
name: Build artifact
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- run: npm ci

- name: Build static site
run: npm run build

- name: Check links
run: npm run linkcheck

# The validate matrix already tested both runtimes, but it builds its own
# copy. Running the suite here means the exact artifact that ships was
# tested against the exact Node it was produced on.
- name: Test the artifact build
run: npm test

# constitution.stmorg.in is live. An Actions deploy whose artifact has no
# CNAME can drop the custom domain setting, so its absence is now the
# failure — the opposite of the pre-cutover rule.
- name: Guard the custom domain
run: |
if [ ! -f dist/CNAME ]; then
echo "::error::dist/CNAME missing — a deploy without it can drop the custom domain."
exit 1
fi
echo "CNAME present: $(cat dist/CNAME)"
if grep -rqE 'href="/OpenCodeLaw/|src="/OpenCodeLaw/' dist; then
echo "::error::Built pages still carry the /OpenCodeLaw/ prefix but the site serves from /."
exit 1
fi
echo "No stale project-Pages prefix in the output."

- name: Upload Pages artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v3
with:
path: ./dist

deploy:
name: Deploy to Pages
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
# Build output — published by GitHub Actions, never committed.
dist/

# Dependencies — restored with `npm ci` from the committed lockfile.
node_modules/

# Legacy SCSS source map. NOTE: css/style.css is deliberately NOT ignored;
# ignoring the site's only stylesheet is what made a clean checkout ship
# with no CSS. See AUDIT-CONFIRMED.md §6.
css/style.css.map
css/style.css

*.log
.DS_Store
assets/og/
12 changes: 0 additions & 12 deletions .htaccess

This file was deleted.

186 changes: 186 additions & 0 deletions .night-run/CUTOVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Cutover — constitution.stmorg.in

**Status: the domain is already live on this build.** It was pointed here on 15 Aug 2026, ahead of
this document. What follows is therefore part record, part the checklist that remains.

The old site was **not** on GitHub Pages — `has_pages` was `false` until this rebuild, and the
committed `.htaccess` shows it was served by Apache. Cutover was a DNS repoint, not a Pages setting,
which is why the old site kept serving until the moment DNS changed.

---

## What happened, and what broke

| | |
|---|---|
| Pages enabled, Source: GitHub Actions | done |
| `rebuild/v3` added to the `github-pages` branch policy | done — it was `main`-only and was silently failing the deploy job |
| Custom domain set to `constitution.stmorg.in` | done, by Pranay |
| **Base path switched `/OpenCodeLaw/` → `/`** | **done, after the domain went live** |
| CNAME shipped in the artifact | done |
| Legacy `/archives/v1` redirects | done |

**The breakage.** The artifact was compiled for `BASE_PATH=/OpenCodeLaw/` while the apex domain
serves from `/`. Every asset and internal link resolved to a directory that did not exist —
`/styles/tokens.css` returned 200 while the HTML asked for `/OpenCodeLaw/styles/tokens.css`.

Fixed by rebuilding at `BASE_PATH=/`. Three guards now make it unrepeatable:

- the base path and origin are defined once, in `src/lib/paths.mjs`, and read by the build, the link
checker and the tests — three copies of a default is how they drift;
- a test asserts that if a `CNAME` is present the build must be compiled for `/`;
- CI greps the built output for a stale `/OpenCodeLaw/` prefix and fails on it.

---

## Still outstanding

### 0. Cloudflare rewrites the contact link, breaking the no-JS guarantee

**Confirmed live.** The build ships `href="mailto:pranay@stmorg.in"`; the served page carries
`/cdn-cgi/l/email-protection#<hex>`, which only resolves once Cloudflare's script has run. On a site
whose premise is that it works with JavaScript disabled, the one outbound link a reader is most
likely to need is the only thing that requires it.

This is Cloudflare's **Email Address Obfuscation** (Scrape Shield). It is applied at the edge and
**cannot be fixed in the build** — `npm test` asserts the build output is clean, which it is.

Two routes, both Pranay's to choose:

1. **Turn it off.** Cloudflare dashboard → the zone → **Scrape Shield** → disable *Email Address
Obfuscation*. Narrower option: leave it on globally and add a Configuration Rule disabling it for
`constitution.stmorg.in`.
2. **Stop publishing a raw address.** Replace `info.contact.email` with `info.contact.url` pointing
at a contact page. Nothing to obfuscate, and the address stops being scraped — which is what the
feature is for.

Verify after either:

```bash
curl -sS "https://constitution.stmorg.in/?cb=$(date +%s)" | grep -c "cdn-cgi/l/email-protection" # 0
```

### 1. HTTPS is not enforced by GitHub

`gh api repos/ServiceToMankind/OpenCodeLaw/pages` reports `https_enforced: false`, and the Pages
`html_url` is `http://`. HTTPS does work for visitors — but it is terminated by **Cloudflare**, not
by GitHub. The served HTML contains `/cdn-cgi/l/email-protection`, which is Cloudflare rewriting the
`mailto:` in the footer, so the domain is proxied (orange cloud).

**Direct evidence that no GitHub certificate exists for this hostname.** Fetching GitHub's Pages
origin with the right Host header fails TLS verification:

```
$ curl --resolve constitution.stmorg.in:443:185.199.108.153 https://constitution.stmorg.in/
curl: (60) SSL: no alternative certificate subject name matches target host name
'constitution.stmorg.in'
```

Visitors are unaffected — Cloudflare terminates TLS with its own certificate — but it confirms the
GitHub-side certificate has never been issued, which is what `https_enforced: false` reflects. It
also means **Cloudflare's SSL mode cannot currently be Full (strict)** against this origin without
returning [error 526][cf-526]: strict mode validates the origin certificate, and there is not a
valid one for this hostname. Whatever the mode is today, it is not that. Check it.

**What the documentation actually says** — checked rather than recalled, because the usual advice
here is folklore:

- GitHub's own pages on [securing a Pages site with HTTPS][gh-https] and
[troubleshooting custom domains][gh-tsh] **do not mention CDNs, proxies or Cloudflare at all.**
They say certificate provisioning depends on the DNS records resolving to GitHub's infrastructure,
that stray `A`/`AAAA`/`ALIAS`/`ANAME`/`CNAME` records "may prevent the HTTPS certificate from
generating", and that the fix for a stuck certificate is to remove and re-add the custom domain.
So the DNS-only step is **inference from those requirements, not documented GitHub guidance.**
- Cloudflare's side is clearer. Its community guidance is that using GitHub Pages with Cloudflare
requires disabling the HTTP proxy, and that under **Full (strict)** Cloudflare blocks the HTTP
validation GitHub uses to issue a certificate — see [the Cloudflare community thread][cf-gh] and
[Full (strict) mode][cf-strict].

**Therefore the sequence, stated as a proposal to verify and not as fact:** set the record to
DNS-only (grey cloud), wait for GitHub to issue the certificate and for *Enforce HTTPS* to become
available, then re-enable the proxy if it is wanted. **Confirm before running it** — if GitHub's
certificate does not cover the hostname once the proxy is back on, Full (strict) returns
[error 526][cf-526].

- **SSL mode.** If Cloudflare is set to **Flexible**, the Cloudflare→GitHub leg is plaintext while
the padlock still shows for visitors. It should be **Full (strict)**, which
[requires a valid publicly-trusted certificate on the origin][cf-strict] — which GitHub provides
once provisioning has succeeded. Check this first: it is the item with a real security consequence,
and it is independent of whether *Enforce HTTPS* is ever turned on.
- **Bytes served are not bytes built.** See item 0.

**Nothing changed. This is DNS and a third-party dashboard, outside anything authorised here.**

[gh-https]: https://docs.github.com/en/pages/getting-started-with-github-pages/securing-your-github-pages-site-with-https
[gh-tsh]: https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/troubleshooting-custom-domains-and-github-pages
[cf-gh]: https://community.cloudflare.com/t/github-pages-require-disabling-cfs-http-proxy/147401
[cf-strict]: https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/full-strict/
[cf-526]: https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-526/

### 1b. Six section anchors changed meaning

Applying Act 2 restructured Articles 14 and 15. Anyone holding a bookmark to a *section* of either
will land on different text than before:

| Anchor | meant | now means |
|---|---|---|
| `art-14-s-1` | Eligibility | Sabbatical Leave |
| `art-15-s-1` | Eligibility | Voluntary |
| `art-15-s-2` | Application | Involuntary |
| `art-14-s-2`, `art-14-s-3`, `art-15-s-3` | Application / Duration | no longer exist |

Article-level anchors (`art-14`, `art-15`) are unaffected. `/archive/2.0.0/` still serves the old
structure, so a stale citation remains readable as it stood. No redirect is possible or appropriate:
the Act changed what the provision says, and silently forwarding would hide that.

### 2. Caching

Pages serves `cache-control: max-age=600`. After a deploy, expect up to ten minutes of stale HTML at
the edge. A cache-busting query (`?cb=…`) confirms the origin immediately. If Cloudflare caching is
enabled for HTML, purge after deploys or the window is longer.

### 3. Verify after any future deploy

```bash
curl -sS "https://constitution.stmorg.in/?cb=$(date +%s)" | grep -c "/OpenCodeLaw/" # must be 0
curl -sS -o /dev/null -w "%{http_code}\n" https://constitution.stmorg.in/styles/tokens.css
curl -sS "https://constitution.stmorg.in/?cb=$(date +%s)" | grep -oE '<link rel="canonical"[^>]*>'
curl -sS "https://constitution.stmorg.in/?cb=$(date +%s)" | grep -c "cdn-cgi/l/email-protection" # must be 0
gh api repos/ServiceToMankind/OpenCodeLaw/pages --jq '{cname, https_enforced}'
```

---

## Rollback

If the new site has to come down quickly, in increasing order of severity:

1. **Revert the content, keep the domain.** `git revert` the offending commit and push; CI
redeploys in about two minutes. Preferred — the domain and DNS stay untouched.
2. **Roll back to a known-good commit.** `git revert` back to it and push. Do not force-push; the
deploy is driven by the branch head, and history rewriting is what makes the archive
untrustworthy.
3. **Take the new site off the domain.** Remove the custom domain in Pages settings and point DNS
back at the old Apache host. The old content still exists on that host; nothing in this
repository deleted it.
4. **Deploy without the domain.** Run the workflow manually with `skip_cname: true`. Only for a
deliberate return to `servicetomankind.github.io/OpenCodeLaw/`, and note that the build would
then also need `BASE_PATH=/OpenCodeLaw/` to be coherent.

**Do not** delete the `rebuild/v3` branch or the Pages deployment while the domain points here —
that leaves the constitution domain serving a 404.

---

## The thing worth pausing on

The site is now the public face of a governing document that is **mid-reconciliation**. It says so
on every page, which is the right behaviour and the point of the whole exercise. But it also means
`constitution.stmorg.in` currently publishes a constitution that:

- reflects **one of three** enacted Amendment Acts, and only partly;
- holds Articles 6 and 7 pending Q2;
- carries 32 headings that no instrument enacted, now visibly marked.

None of that is hidden, and all of it is more honest than what the domain served before. It is still
worth the board knowing the domain went live in that state rather than after sign-off.
20 changes: 20 additions & 0 deletions .night-run/LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Night run log

| Time (IST) | Task | Result |
|---|---|---|
| 00:44 | S1 tripwire baseline | 82 provision strings, GREEN |
| 00:44 | S2 queue + log | 44 tasks queued |
| 00:44 | S3 commit scaffolding | done |
| 00:48 | L1 acts/register.yaml | 3 acts, 5 Act-1 provisions, validates clean |
| 00:51 | L2 art-11 | DEVIATED: Act1 has no article-level body; removed empty content key (tripwire-permitted). Sections carry 99.4% of Act1 art-11 |
| 00:51 | L3 amended_by | 5 articles (9,10,11,12,18); 6,7 untouched |
| 00:51 | L4 version | 3.0.0-alpha.1 + reconciliation_state |
| 00:51 | L5 validate | PASSED 0 errors, 3 warnings |
| 01:06 | Phase 4 build+deploy pipeline | 24 pages, 41 tests, 0 internal 404s, 21 OG images |
| 01:11 | GATE 4 PASS | live at https://servicetomankind.github.io/OpenCodeLaw/ — all pages 200, PDFs 200, CNAME 404 (correct), pages.cname=null |
| 01:11 | Pages enabled | build_type=workflow; rebuild/v3 added to github-pages branch policy (was main-only) |
| 01:11 | CI fix | node --test tests/ fails on Node 22 (treats dir as module); switched to shell glob |
| 06:56 | GATE 6 PASS | LIVE Lighthouse 100/100/100/100 mobile AND desktop; a11y 100 in all 4 theme/viewport combos |
| 06:56 | Keyboard walkthrough | 10/10 pass — skip link, focus trap, Escape return, legacy anchor, 0 console errors |
| 06:56 | GATE 7 PASS | starter builds standalone (79 files, base /), SPEC.md idempotent, CLI works |
| 06:58 | R1 report | REPORT.md written; all 50 queue items done |
Loading
Loading