diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3a5d3d9 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.gitignore b/.gitignore index 18c426d..1cf1a43 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file + +*.log +.DS_Store +assets/og/ diff --git a/.htaccess b/.htaccess deleted file mode 100644 index 3910f13..0000000 --- a/.htaccess +++ /dev/null @@ -1,12 +0,0 @@ -RewriteEngine on - -DirectoryIndex index.html - -RewriteCond %{THE_REQUEST} /([^.]+)\.html [NC] -RewriteRule ^ /%1 [NC,L,R] - -RewriteCond %{REQUEST_FILENAME}.html -f -RewriteRule ^ %{REQUEST_URI}.html [NC,L] - -# Deny access to .git directory -RedirectMatch 404 /\.git \ No newline at end of file diff --git a/.night-run/CUTOVER.md b/.night-run/CUTOVER.md new file mode 100644 index 0000000..f70740b --- /dev/null +++ b/.night-run/CUTOVER.md @@ -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#`, 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 ']*>' +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. diff --git a/.night-run/LOG.md b/.night-run/LOG.md new file mode 100644 index 0000000..8b0f8c9 --- /dev/null +++ b/.night-run/LOG.md @@ -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 | diff --git a/.night-run/PHASE-8-AMENDMENT-WORKFLOW.md b/.night-run/PHASE-8-AMENDMENT-WORKFLOW.md new file mode 100644 index 0000000..d15a8e6 --- /dev/null +++ b/.night-run/PHASE-8-AMENDMENT-WORKFLOW.md @@ -0,0 +1,75 @@ +# Phase 8 — improved amendment workflow (deferred, nothing built) + +Requirements noted while applying Acts 1, 2 and 3. **Nothing here is implemented.** Recorded now +because this is when the gaps were visible; they will not be as obvious later. + +## What applying three Acts actually cost + +The engine records amendments well. It does not help you *make* one. Every step below was manual, +and each is a place a future amendment can go wrong quietly. + +### 1. Structure cannot be inferred from a PDF + +The single largest cost. Deriving article/section shape from the Act's layout failed in both +directions on the same heuristic: it read Article 9's sentences as section titles, collapsed Article +10's three clauses into one, and found none of Article 8's five. PDF indentation is not a structural +signal. + +Resolved by declaring structure explicitly in `src/act-application.mjs` as line ranges, with +`verifyCoverage` asserting the slices reconstruct the span exactly. That works, but it is a +hand-authored file per Act. + +**Requirement:** Acts should be authored in a machine-readable form — the same YAML schema, as a +patch — with the PDF as the signed rendering of it rather than the source of truth. An Act would +then declare `target`, `operation`, `scope` and its text directly, and applying it would need no +transcription at all. + +### 2. Clause-scope edits are surgery + +Act 1's Article 7 replaces sub-clause (a) *point 6* and sub-clause (b), inside a nested list whose +points (a)1–5 stay. That was declared by line range into the current text — which means the +declaration goes stale the moment the provision changes. + +**Requirement:** clauses need addressable ids of their own (`art-7-c-6-a-6`), so an Act can target a +clause rather than a line. Today only articles and sections are addressable. + +### 3. Nothing proposes, everything asserts + +`apply-act.mjs` writes the change and verifies it after. There is no state where an amendment is +drafted, reviewed and approved before it lands. + +**Requirement:** a proposed amendment should be a branch plus a rendered diff of the *provisions*, +not of the YAML — a reviewer should see "Article 13 becomes …", not a unified diff. + +### 4. The approval chain is recorded, never enforced + +`procedure` holds the bodies Article 16(3) requires, and it is prose. Nothing checks that an Act +with `complete: false` has not been applied — Acts 1–3 were applied with intermediate-board and unit +approval still unconfirmed, which was authorised, but the engine could not have stopped it. + +**Requirement:** the validator should refuse to apply an Act whose `procedure.complete` is false +unless an explicit override is recorded, with who authorised it and when. + +### 5. Section ids move when an Act restructures + +Act 2 changed what six section anchors mean. That is correct — a section id names a position — but +nothing warned before it happened; it was noticed by reading the diff. + +**Requirement:** the applier should detect and report anchor meaning changes as part of its +verification, the way it reports provisions changing outside the manifest. + +### 6. Numbering gaps are discovered late + +Inserting Article 20 created an unaccounted gap at 19, caught only when validation failed after the +write. + +**Requirement:** the manifest should predict the post-application numbering and require a `reserved` +entry up front, before anything is written. + +## What already works and should not be rebuilt + +- `verifyCoverage` — proving declared slices reconstruct the Act span exactly. +- The expected-change manifest, and aborting on any provision changing outside it. +- The tripwire, re-baselined per Act rather than disabled. +- `sor-as-authority` — a Statement of Objects can never be cited as authority. +- Text comparison that folds enumerators and typography but strips tags first. diff --git a/.night-run/QUEUE.md b/.night-run/QUEUE.md new file mode 100644 index 0000000..1c072d6 --- /dev/null +++ b/.night-run/QUEUE.md @@ -0,0 +1,84 @@ +# Night run queue + +**Protocol:** resume at the first unchecked item. Do not re-plan or re-audit. +One task → verify → commit → tick → next. Never batch. Max 3 attempts per gate, then `BLOCKED`. +Run `node .night-run/tripwire.mjs` before **every** commit. Red = stop everything. + +**Branch:** `rebuild/v3` · never push to `main`, never force-push. +**Deploy target:** `https://servicetomankind.github.io/OpenCodeLaw/` · base path `/OpenCodeLaw/` · **no CNAME in artifact**. + +--- + +## S — Scaffolding + +- [x] S1 Tripwire script + baseline (82 provision strings) +- [x] S2 QUEUE.md, LOG.md +- [x] S3 Commit scaffolding + +## L — Authorised legal metadata (no provision text changes except L2) + +- [x] L1 `acts/register.yaml` — all three Acts; Act 1 provisions for arts. 9,10,11,12,18 only +- [x] L2 **DEVIATED** — Act 1 supplies no article-level body for art-11; empty `content` key removed instead. See REPORT. +- [x] L3 `amended_by: [act-1-2024]` on arts. 9,10,11,12,18 only +- [x] L4 `version: 3.0.0-alpha.1` + `reconciliation_state` block +- [x] L5 Validation green on the two remaining known items only + +## 4 — Build and deploy + +- [x] 4a `src/build.mjs` skeleton + base-path helper + YAML load +- [x] 4b Templates: layout, provision rendering, sanitised Markdown +- [x] 4c `index.html` — full constitution, all provisions inline +- [x] 4d `articles//index.html` per article +- [x] 4e `amendments/index.html` from register +- [x] 4f `archive/` index + `archive/v1.0.0/` + `archive/v2.0.0/` w/ banner + canonical +- [x] 4g `404.html`, `.nojekyll`, static asset copy, Act PDFs +- [x] 4h Reconciliation banner from `reconciliation_state` (never hardcoded) +- [x] 4i Hash resolution after DOM ready + sticky offset + legacy `#articleN` map +- [x] 4j Link checker — zero internal 404s +- [x] 4k Workflow: ci → validate → test → build → linkcheck → deploy (CNAME gated off) +- [x] 4l **Gate 4** + enable Pages + deploy + log URL + +## 5 — SEO + +- [x] 5a Per-page title, description from provision text, canonical +- [x] 5b OG + Twitter tags, real `

` in source +- [x] 5c Build-time OG images (SVG→PNG, banner base plate) +- [x] 5d JSON-LD: Legislation, Article, BreadcrumbList, Organization +- [x] 5e `sitemap.xml` w/ per-provision lastmod + `robots.txt` +- [x] 5f **Gate 5** — raw curl checks, JSON-LD validates, sitemap URLs 200 + +## 6 — UI + +- [x] 6a `tokens.css` — palette + `data-theme` + no-flash head script +- [x] 6b Font fixes (truetype, Cuprum format, font-display, Open Sans decision) +- [x] 6c `layout.css` — 18px/1.65/68ch, ragged-right, margin article numbers +- [x] 6d Desktop sticky ToC w/ overflow; **mobile bottom-sheet ToC (#1)** +- [x] 6e IntersectionObserver scroll-spy + auto-expand (#4) +- [x] 6f ⌘K search over build-time index +- [x] 6g Progress bar, focus mode, amended-by chips +- [x] 6h Copy-link toast `aria-live` (#7) +- [x] 6i A11y pass: skip link, landmarks, focus-visible, keyboard, reduced-motion, `