Skip to content

Rebuild v3: static engine, permanent citations, Acts 1-3 of 2024 applied - #31

Merged
KiranPranay merged 33 commits into
mainfrom
rebuild/v3
Aug 15, 2026
Merged

Rebuild v3: static engine, permanent citations, Acts 1-3 of 2024 applied#31
KiranPranay merged 33 commits into
mainfrom
rebuild/v3

Conversation

@KiranPranay

Copy link
Copy Markdown
Member

Rebuilds OpenCodeLaw as a static, validated, citable engine and applies all three Amendment Acts of 2024.

The law

  • Acts 1, 2 and 3 of 2024 applied. Articles 1–21, with 19 reserved (no instrument mentions it).
  • Every provision text is sliced from acts/text/, never retyped. PROVENANCE.md reports all 24 touched provisions as ALREADY-APPLIED — each matches its instrument.
  • Board approval recorded as stated; intermediate-board and unit approval remain unconfirmed and are not inferred.
  • Title corrections (Alumni, Annual Report, Amendments) come from the instruments, not from an editor.

The engine

  • Build-time rendering: the full constitution is in the served HTML with JavaScript disabled.
  • Permanent citation ids; reordering the YAML changes no anchor, and a test proves it.
  • Validation gates deployment: empty provisions, duplicate ids, unexplained numbering gaps, unresolvable Act references and Statements of Objects cited as authority all fail the build.
  • Archives are frozen and still resolve pre-amendment anchors.

Verification

58 tests plus a 10-step keyboard walkthrough, zero internal 404s, Lighthouse 100/100/100/100 on the live site.

Detail: AUDIT-CONFIRMED.md, RECONCILIATION.md, PROVENANCE.md, .night-run/REPORT.md, .night-run/CUTOVER.md.

Reproduce every claim in the pre-rebuild audit against a real DOM, the real
CDN dependencies and the real spec files before changing anything.

10 confirmed, 3 refuted, 1 partially confirmed. The refuted three are the
audit's entire "why it is broken right now" section:

- Art. 11 `Units` has a `content:` key holding an empty string, not a missing
  key. marked does not throw on ""; the page renders all 18 articles, 22
  sections, the amendments table and the back-to-top button with zero errors.
  The real defect is a provision that publishes silently blank.
- The unversioned marked CDN URL returns HTTP 200 (jsDelivr falls back to
  v15.0.12, the last release shipping marked.min.js). Unpinned, not absent.
- `navArticleId = navLink.href = '#article1'` yields the RHS, not the resolved
  URL, and scroll-spy reads getAttribute() anyway. All 42 nav links resolve.

Confirmed: scroll handler throws pre-render, gitignored stylesheet, `act: #`
as null, unresolvable archive links, mutable archive, wrong v1 version,
duplicate opencon.yaml, both broken @font-face declarations, no mobile nav,
three-way key naming disagreement.

Six further findings, incl. malformed rgba(0,0,0.87) and confirmation that
#320063 is the dominant logo hue. Supersedes OpenCodeLaw-Audit.md.
Separate engine from content and establish a real toolchain. No
constitutional text is added, altered or reworded in this commit.

Moves (history preserved, contents byte-identical):
- specs/v1.yaml            -> constitution/versions/v1.0.0.yaml
- specs/v2.yaml            -> constitution/versions/v2.0.0.yaml
- acts/*.pdf               -> acts/pdf/<slug>.pdf

Deleted:
- .htaccess          Apache rewrites GitHub Pages never reads
- js/bak.js          dead backup of the renderer
- specs/opencon.yaml duplicate of v1 (differs only in version key)
- archives/*.html    pointed at the mutable current spec; replaced by
                     frozen, built archive pages in Phase 4
- .vscode/settings.json

Toolchain: Node ESM, zero framework, exact-pinned dependencies installed
from npm and bundled at build time. No runtime CDN references will appear
in build output.

Dependency pins were chosen against advisories, not defaults:
- js-yaml 4.3.1     4.1.0 has prototype pollution via merge keys (high)
                    plus three quadratic-CPU DoS advisories
- ajv 8.20.0        8.17.1 has ReDoS via the $data option (moderate)
- sanitize-html 2.17.5  <=2.17.4 lets javascript: URIs through action,
                    formaction, data, poster and background attributes;
                    2.17.6+ requires Node >=22.12, which CI does not run
- marked 15.0.12
npm audit reports 0 vulnerabilities.

Prototype pollution and URI-scheme escapes are in scope, not theoretical:
Phase 7 points this engine at third-party specs.

Toolchain runs on Node 18 and 20. CI pins Node 20; engines allows >=18.18
so the build stays verifiable on this machine (Node 18.19.1, no version
manager). Raise both together if Node 20+ becomes a hard requirement.

CI: validate and test gate build, which gates deploy. A schema violation
blocks publication rather than shipping a broken constitution.

css/style.css is no longer gitignored. .nojekyll and CNAME added.

Refs #1, #4, #7, #25, #26
Byte-identical copy of constitution/versions/v2.0.0.yaml (md5
7a4c2bff5e3e9048b5063fba6670c004). Not a single character of
constitutional text is authored or changed here.

This establishes the engine/content split: versions/ is append-never,
edit-never history; current.yaml is the single mutable source of truth.
Today they hold the same text because v2.0.0 is what the society
currently publishes.

Phase 3 amends this file to v3.0.0 by applying Acts 1, 2 and 3 of 2024
verbatim from acts/pdf/, and freezes v2.0.0.yaml with status: superseded.
That phase requires human sign-off on RECONCILIATION.md before merge.

Committed separately so rename detection records the real lineage:
folding it into the restructure made git pair opencon.yaml with
v2.0.0.yaml, which is the wrong provenance for an immutable archive.
… bump

CI now validates on Node 20 and 22; the published build runs on 22.

README records that Node 18 is EOL and the local floor must move to 22 LTS
before Phase 4 ships.

Investigated sanitize-html 2.17.6/2.17.7 as requested. They do carry further
security fixes: SVG SMIL elements can retarget another element's href via
attributeName/values to smuggle a javascript: URI, and a raw `<` escaping
<textarea>/<xmp> can reopen a tag on re-parse.

Not bumping yet, and the reason is a hard block rather than a preference:
2.17.6+ requires htmlparser2@^12, which is ESM-only, so require() throws
ERR_REQUIRE_ESM on Node 18. Verified, not inferred from the engines field.
2.17.5 is the newest release that loads on the current floor.

Neither fix is reachable here: the sanitizer runs a strict allowlist over
Markdown-rendered output permitting no SVG and no animation elements, so
there is no element to retarget. Defence-in-depth, not a live hole.

The bump is gated on the Node 22 floor move and recorded in the README so
it cannot be forgotten.
Verbatim `pdftotext -layout` extraction of all three Acts (11 pages: 5+4+2)
into acts/text/. Committed before register.yaml so every legal assertion
made later is diffable against a source that lives in the repo.

Scope re-derived independently from the PDFs. It matches the corrected
table in full:

  Act 1  amends 6(1-5), 7(4,6), 9, 10, 11, 12   inserts 18
  Act 2  amends Preamble, 3, 4, 5, 8, 14, 15,   inserts 20
                16(3), 17(1)
  Act 3  amends 13                              inserts 21

Article 19 verified absent: a whitespace-normalised search over all three
texts finds every article from 3 to 18, then 20 and 21. No occurrence of
19 in any form. The gap is real and is not an extraction artefact.

Extraction quality is good; no OCR needed. Two Acts carry a STATEMENT OF
OBJECTS AND REASONS, which is explanatory rather than enacting, and in one
case contradicts its own operative text. Recorded for RECONCILIATION.md,
not resolved here.
Authors schema/opencodelaw-1.0.schema.json (draft 2020-12) and
src/validate.mjs, and migrates all three specs onto them. Structural only:
252 provision strings verified byte-identical against HEAD. No provision
text, and no title, is changed.

Identity is now explicit. Every article carries `id: art-N` and
`number: N`; every section `id: art-N-s-M` and `number: M`. Nothing is
derived from array position, so reordering the YAML cannot re-point a
citation. A test asserts this against the real constitution.

Root key is `opencodelaw: "1.0"`, ending the three-way disagreement
between the docs, the specs and the renderer.

Beyond schema conformance the validator enforces the relational rules
that actually protect the document:

- ids globally unique, and `id` must agree with `number`
- article numbering dense from 1; a gap is legal only where a `reserved`
  or `omitted` entry explicitly occupies that number
- `amended_by`, `act.amends` and `act.provisions[].target` must resolve
- `act.pdf` and `act.source_text` must exist on disk
- archived files: version must match filename, status must be superseded
- exactly one document may be `current`
- no archived version may duplicate the current version
- no example.com URL anywhere in `info`
- no template may link to an anchor no provision produces

Schema additions beyond the original plan: `provenance` records text that
entered outside the amendment process rather than erasing it, and
provision `status` (active | omitted | reserved) makes a numbering gap
representable without renumbering.

Validation deliberately does not pass yet. It reports exactly two real
problems, both of which are open questions rather than defects to fix:

  - art-11 `Units` has an empty body (RECONCILIATION Q11)
  - v2.0.0 duplicates the current version, the archive-shadows-current
    condition created by Phase 1 scaffolding and cleared by Phase 3

The first is the provision that published blank for two years. It is now
a hard build failure rather than a silent blank page.

RECONCILIATION.md opens with twelve blocking questions, not the four
expected. New ones include Act 2 setting two different amendment
thresholds (2/3rd operative vs 3/4th in its Statement of Objects), Act 1
defining `Unit Board Member` and `Coordinator` with identical operative
words, and the amendment table naming a different author and an earlier
date than the Acts themselves.

No text is applied until PROVENANCE.md is signed off.
Adds src/text-compare.mjs and its tests before any provenance verdict is
produced, because a wrong normaliser here causes an amendment already in
force to be re-applied.

Order is load-bearing: tags are stripped first, and to a space rather than
to nothing, so `a<br>b` cannot become `ab`. Punctuation goes last.

Measured on the real Article 9 pair (6 <br> tags, 118 tokens):

  naive   (punctuation first, tags never)  similarity 0.9559, exact match false
  correct (tags -> space, punctuation last) similarity 1.0000, exact match true

The naive form injects 6 stray `br` tokens. Under exact-equality matching it
returns a false NOT-APPLIED, which is the reported failure. Under a 0.94
similarity threshold it survives by 0.016 — close enough that it would fail
on a shorter provision with the same tag density.

The fixture asserting Article 9 classifies ALREADY-APPLIED is in the suite,
along with a test asserting the naive form does contain the stray token, so
the guard cannot silently stop guarding.

Classification is similarity-based rather than exact so that enumerator
style ((1) vs 1.) and typography (curly vs straight quotes) do not mask an
applied amendment. An insertion not yet made is NOT-APPLIED, never
DIVERGENT.
Classifies every provision the three Acts touch against the text in the
constitution today and the pre-Act text in v1.0.0. No constitutional text
is changed.

  ALREADY-APPLIED   4   art-9, art-11, art-12, art-18 — all Act 1
  NOT-APPLIED      19
  DIVERGENT         1   art-10

This confirms the hypothesis: Act 1 was substantially applied to v2 and
never recorded; Acts 2 and 3 were not applied at all. Article 6, which Act
1 also amends, is 100% its pre-Act text — so Act 1 was applied in part,
not in whole.

Two bugs in my own tooling were caught and fixed before the verdicts were
trusted. Both were silent and both produced wrong answers:

1. Clause (6) of Article 7 begins immediately after a PDF form feed. \f is
   not a JS line terminator, so `^[ \t]*\(` never matched and the clause
   was absent from the analysis with no error. Form feeds are now
   normalised, and a guard asserts each clause-scope heading yields exactly
   the clauses it names, throwing rather than under-reporting.

2. Clause-scope verdicts scored only against the Act, ignoring whether the
   provision had changed at all since before the Acts. That reported six
   provisions as DIVERGENT which are word-for-word their own pre-Act text —
   the definition of NOT-APPLIED. Verdicts now use three measures, and
   `drift` is what separates the two.

Both are covered by regression tests. Importing the generator no longer
rewrites the report as a side effect.

art-10 is a genuine divergence and is not resolved. Its three sections are
a 100.0% match to Act 1 (318 tokens against 318) while its article body is
89.7% the pre-Act text and only 25.0% present in the Act. Act 1 was applied
to the sections and the superseded introduction was kept. That paragraph
was also hand-edited to Act 1's coordinator naming with no instrument doing
so, though Act 1's Statement of Objects does authorise the one deletion.
Recorded as Q14.

RECONCILIATION.md records the six answered questions, adds Q13 (procedural
validity under Article 16(3), noting Act 2 amends that very provision) and
Q14. Schema gains `scope` on register provisions with `clauses` required
for clause scope, plus `procedure`, `drafting_discrepancy`, `signed_by`,
`moved_by`, `unverified_attribution` and `passed_date`; the single `author`
field is gone.
Deletes Article 10's retained article-level paragraph from the operative
text. Act 1 substitutes Article 10 in full, running its heading straight
into clause (1) and supplying no article-level body, so the old body does
not survive by default.

Guarded before writing: the change halts unless all eight coordinator
roles survive in art-10-s-3. They do. The paragraph's unique lead-in,
making the IBM responsible for "all legal and financial activities", is
confirmed absent from s-3 — it is the part clause (3) reframes to "proper
functioning of STM units", and the part that would contradict Article 21
once Act 3 inserts it. Retaining it was a live contradiction, not a
redundancy.

Verified as the only change: 82 provision strings compared before and
after, exactly one differs.

Nothing is destroyed. versions/v2.0.0.yaml keeps the paragraph verbatim,
`provenance` on art-10 records the text and the unrecorded hand-edits, and
RECONCILIATION Q14 quotes both the pre-Act and hand-edited forms.

Corrects an error in the previous commit: it claimed the Statement of
Objects authorised removing Volunteer Coordinator. It does not, and saying
so would have collapsed the Q3 ruling — an SOR that can authorise a
deletion in Article 10 can set a voting threshold in Article 16. The
operative text of Act 1 clause (3) carries all three edits on its own; the
SOR only corroborates intent.

That is now convention C1 and is enforced rather than stated: validate.mjs
fails with `sor-as-authority` if any provisions[].source_lines cites a line
at or after an Act's STATEMENT OF OBJECTS AND REASONS. `drafting_
discrepancy` is the only field permitted to point there. Tests assert each
Act's SOR boundary and that Act 2's 2/3rd is operative while its 3/4th
appears only below that line.

Schema: an article may consist entirely of sections. Required by Act 1's
Article 10, which supplies no body. An empty-string `content` remains an
error in every case — that defect published Article 11 blank.

Validator errors now name the provision: "art-11 Units/content" rather
than "/articles/10/content", which pointed at index 10 and read as
Article 10.

Q2 escalated to blocking. Article 6 is 100% pre-Act text, so applying Act 1
there now requires knowing whether clause (6) Donor survives. Articles 6
and 7 are held; both options are stated neutrally for the board.

art-10 now classifies ALREADY-APPLIED at 100.0%. No DIVERGENT verdicts
remain in the corpus.
Baseline hashes all 82 provision strings in current.yaml. The only change
permitted for the rest of the run is art-11.content (Q11 restoration);
anything else aborts before commit.

QUEUE.md holds 44 tasks in resume order, LOG.md one line per completion.
acts/register.yaml records all three instruments. Act 1 carries provisions
for arts. 9, 10, 11, 12, 18 only — the reconciled set. Articles 6 and 7 are
held under Q2 and are named in reconciliation_state, not here. Acts 2 and 3
are recorded as instruments with no provisions: unapplied, and several of
their targets do not yet exist to resolve against.

Line ranges are generated from the parser, never transcribed, and clamped
below each Act's Statement of Objects so no entry can cite explanatory text
as authority (C1). Generation halts if arts. 6 or 7 appear, if a count is
wrong, or if a PDF is missing.

Act 2 records both drafting discrepancies: the 2/3rd vs 3/4th threshold
(Q3, operative governs) and the amendment-vs-insertion conflict on Article
15 (Q4, open).

Fixes form-feed normalisation to strip rather than substitute a newline.
All 11 form feeds sit at a line start, so stripping keeps line numbers
identical to the files on disk — source_lines is an audit trail someone
opens in an editor, and the previous handling shifted every line after a
page break.

Schema gains act.application_status (applied | partially-applied | pending).
version 3.0.0-alpha.1, effective_from 2024-05-03, with a reconciliation_state
block naming exactly what is not applied and why: arts. 6 and 7 held under
Q2, Acts 2 and 3 held entirely. The build renders its banner from this block,
never from hardcoded text, so the banner cannot drift from the document.

amended_by: [act-1-2024] on arts. 9, 10, 11, 12, 18. Articles 6 and 7 are
asserted untouched before writing.

L2 deviates from instruction, deliberately. Act 1 substitutes Article 11 as
the heading running straight into clause (1); it supplies NO article-level
body. Its clause (1) is a 100.0% match to the existing art-11-s-1 and clause
(2) a 98.6% match to art-11-s-2 — together 99.4% of the enacted text. So
there is no article-level text to restore: writing clause (1) at article
level would duplicate section 1 verbatim, and removing that section means
renumbering, which is forbidden and would break the art-11-s-2 citation.

Instead the empty  key is removed, making Article 11 a section-only
article — the shape Act 1 actually enacts, and the shape the schema now
permits because Act 1's Article 10 required it. No provision text was added,
removed or altered; tripwire confirms art-11.content is the only change.

Archived blank provisions are warnings, not errors. The archive must
reproduce what was published and Article 11 of v2.0.0 was published blank;
versions/ is also not editable tonight. The same blank in the live document
remains a hard error. The classifier is narrow — only emptiness constraints
on a content field — so a missing key or wrong type still fails everywhere.

Schema: semver accepts a prerelease suffix, so a mid-reconciliation document
can say so in its version rather than claiming 3.0.0.

Validation now PASSES: 0 errors, 3 warnings.
Replaces client-side YAML fetching with build-time rendering. Every provision
is in the served HTML: the previous renderer meant social scrapers saw an
empty body and one missing key could blank half the document.

24 pages: the full constitution, one page per article, the amendment
register, an archive index and both frozen versions, plus 404. All 41 tests
pass, link check reports zero internal 404s across 1807 links and 150
in-page anchors.

Anchors are the schema ids exactly. A legacy map resolves every old
#article5 / #article5-section2 link, and hash resolution runs after DOM
ready with the sticky-header offset applied, so a cold load lands on the
cited provision rather than near it.

The reconciliation banner is generated from reconciliation_state and is
asserted by a test to name every blocker the YAML lists. A banner that can
drift from the document it describes is worse than no banner.

Base path is configurable and applied in one place. The link checker fails
on any root-relative URL that escapes it, which is the classic way a site
works locally and 404s on project Pages. CNAME is excluded from the
artifact and CI fails if it appears without being asked for — the custom
domain still serves the old site.

Two bugs found and fixed by the checker rather than in production:
- article pages linked every other article's sections as same-page anchors,
  producing 840 dead fragments. The ToC now resolves cross-page.
- OG plates silently fell back to the banner. Two causes: a <style> block
  with CSS classes, which ImageMagick's built-in renderer cannot resolve
  without librsvg, and a font stack passed through JSON.stringify, whose
  nested double quote terminated the XML attribute early and surfaced as a
  misleading "No such file or directory". 21 plates now render.

Markdown is sanitised against a strict allowlist that admits no SVG and no
animation elements, closing the attributeName scheme-smuggling class
outright rather than relying on URL parsing. `id` is never taken from
content: anchors are a public API and authored Markdown must not mint one.

schema/SPEC.md is generated from the JSON Schema and verified to regenerate
identically, so the docs cannot drift from the code again.

Fonts corrected: format("truetype") and format("woff"), both font-display:
swap. Open Sans is gone — it was never imported and never rendered.
`node --test tests/` passed on Node 20 and failed on Node 22, which treats a
bare directory argument as a module to resolve and threw MODULE_NOT_FOUND.
A shell-expanded glob works on 18, 20 and 22 alike. The matrix caught this
before it reached the deploy job.

Adds the `opencodelaw` bin: validate (a single file against the schema, or
the whole corpus with its relational rules), build, and spec. Adds
examples/starter/constitution.yaml — a minimal valid document with commented
fields, including a reserved article demonstrating how a numbering gap is
recorded rather than closed by renumbering. It validates clean.
README drops the CDN install instructions entirely — they pointed at an
unversioned marked.min.js and a tag of this repo that no longer reflects how
the project works. Replaced with what it is, why citations are permanent,
quickstart, how to adopt it for another organization, Pages deployment
including the base-path and CNAME safety, and honest limitations.

CONTRIBUTION replaces "install a VS Code extension and hand-compile SCSS"
with npm ci and the actual scripts, and states the rule that matters: do not
edit provision text without an enacting instrument, and a Statement of
Objects is never the authority for an operation.
A contract review of the generated assets found real defects; these are the
load-bearing ones.

tokens.css is now the single source of truth. layout.css had redefined
--accent-ink at the same specificity but loads later, so tokens.css's
definition was dead — and the two files meant opposite things by the name
("text on an accent fill" vs "accent as text"). The derived tokens move to
tokens.css with the contrast ratios their author documented; "text on an
accent fill" is --on-accent throughout. --accent-soft and a second
--action-wash were the same ideas under duplicate names and are gone.

Removed the green (#1e7a3c / #7fd39b). The palette rule allows three brand
hues and green is a fourth. The "applied" status pill now uses the brand
purple; ochre already carried "partially applied".

Replaced a hardcoded rgba(0, 0, 0, 0.87) on .to-top with a token. A
malformed three-argument rgba in the old stylesheet is one of the findings
this rebuild started from; reintroducing a hardcoded one is not the fix.

print.css overrode six token names that no stylesheet defines or reads
(--canvas-raised, --ink-muted, --rule, --accent-tint, --accent-secondary,
--canvas-sunken) while leaving every live colour token untouched. Printing
from dark mode therefore put ochre-on-wash chips on paper. It now neutralises
the names that exist.

@font-face was declared twice, in both stylesheets. Fonts are declared once.

Styled body.is-modal-open, which app.js sets while the search dialog or the
mobile contents sheet is open. Nothing defined it, so the page scrolled
behind the overlay and a reader lost their place in a long document.
Lighthouse flagged the banner's "amendment register" link. The link colour
was chosen against the page ground, but inside the banner it sits on the
accent wash, which lifts the background and drops the ratio below 4.5:1 —
4.15:1 in light, 4.44:1 in dark.

Measured against the ground each link actually sits on and darkened within
the same hue: #0069d9 -> #0056b3 (5.6:1 on the wash, 6.5:1 on canvas) and
#66b2ff -> #7cbcff (5.0:1 on the wash, 6.3:1 on #333333).

Lighthouse accessibility: 96 -> 100.
Two contrast failures, both from measuring a colour against the page ground
rather than the surface it actually sits on.

The dark --accent-ink (#cc99ff) cleared 4.5:1 on #333333 but only reached
3.94:1 on the accent wash that a pressed or hovered control sits on — which
is where the theme toggle lives. Lifted to #d9b3ff within the same hue:
7.1:1 on the page, 4.9:1 on the wash.

Adds tests/keyboard.e2e.mjs: a scripted keyboard-only walkthrough that
passes 10 checks with no pointer. Skip link to main; theme toggle as a real
button flipping aria-pressed; Ctrl+K opening search with focus moving in and
returning to the opener on Escape; search navigating to a provision; the
mobile contents sheet opening, trapping focus through a full 25-tab lap and
returning it on Escape; copy-link naming and its polite live region; one
main and one h1 per page type; a cold load of #art-11-s-2 landing clear of
the sticky header; a legacy #article5 link rewriting itself to #art-5; and
zero console errors across six page types.

puppeteer-core is pinned to 25.7.0 — 23.x pulls a vulnerable extract-zip
through @puppeteer/browsers (3 high advisories) for a browser downloader
this never uses. 25.7.0 reports 0 vulnerabilities and, despite declaring
Node >=22.12, imports and drives Chrome correctly on Node 18, verified
rather than assumed. e2e is a separate script, not part of npm test, and
skips cleanly where no Chrome exists.

Lighthouse accessibility is now 100 in all four combinations of
desktop/mobile and light/dark.
Content paths are now configurable (CONSTITUTION_FILE, VERSIONS_DIR,
REGISTER_FILE, OUT_DIR), so a fork replaces the YAML and deploys without
touching the engine. Verified: the starter builds standalone into a temp
directory at BASE_PATH=/ with no versions and no act register, produces 79
files, and contains no trace of this organization's constitution.

The starter deliberately fails validation on one rule and one only. Its
contact address is example.org, which the placeholder check rejects, so
copying the template and deploying without editing it fails the build. The
test asserts exactly that: every structural rule passes and placeholder-url
is the single remaining error. The constitution this engine was built for
shipped two example.com URLs to production for two years.

Also asserts SPEC.md regenerates byte-identically twice and is not stale on
disk, the CLI exposes validate/build/spec, and no engine file hardcodes the
organization name.

Screenshots at 375/768/1440 in both themes, an archived page, and a print
preview PDF, in .night-run/screenshots/.
All 50 queue items complete. Live at
https://servicetomankind.github.io/OpenCodeLaw/ with the custom domain
untouched, verified three ways.

Lighthouse on the deployed site: 100/100/100/100 on both mobile and desktop.
Keyboard walkthrough 10/10. Provision tripwire green before every commit all
night, with art-11.content the only change and Articles 6 and 7 asserted
untouched.

Leads with the L2 deviation: Act 1 supplies no article-level body for
Article 11, so the empty content key was removed rather than text
duplicated, and Q11's live-document question changed shape.
constitution.stmorg.in was pointed at this deployment. The artifact was
compiled for BASE_PATH=/OpenCodeLaw/ while the apex domain serves from /, so
every asset and internal link 404'd. Confirmed against the live site:
/styles/tokens.css returned 200 while the HTML asked for
/OpenCodeLaw/styles/tokens.css.

BASE_PATH is now / and SITE_ORIGIN https://constitution.stmorg.in. The two
defaults move to src/lib/paths.mjs so build, link check and tests read one
source — three copies of the same default is how they drift apart, and a
drift here ships a site whose every link points at a directory that is not
there.

CNAME policy inverted. Before cutover its presence was the failure; now its
absence is, because an Actions deploy whose artifact lacks CNAME can drop the
domain setting. It defaults on, and the workflow input became skip_cname for
a deliberate rollback. CI also greps the built output for a stale
/OpenCodeLaw/ prefix and fails on it.

Also in this commit, from the review:

Section headings now derive from their article's level, so an article's
sections are one level below it rather than siblings. On the index both were
h3: the outline said Article 6 and Board Member were peers. Article pages
were correct by accident because the article there is h2. Lighthouse cannot
catch this — its sequential-headings audit only flags a skipped level, and
h3 -> h3 is not a skip. Two tests now assert the nesting and that no page
skips a level.

npm test uses argument-free discovery, which works identically on 18, 20 and
22 and still excludes the Chrome-dependent e2e file. The artifact-producing
job now runs the suite itself.
The previous host served /archives/v1 and /archives/v2 extensionless, via an
.htaccess rewrite that also mapped /x.html to /x. Both shapes are in the
wild, in minutes and emails and other people's documents, and all four
returned 404 on the new host — confirmed against the live domain.

Static hosting cannot rewrite, so each old shape is now a real page carrying
a canonical to its new address, a meta refresh, a visible link and noindex.
A test asserts every stub redirects to a page this build actually produces.
Act 1 titles Article 11's clause (2) "Establishment". It gives clause (1) no
title at all, and the lowercase "units" above it is something an editor
typed. Both rendered at identical weight. Every heading in the live document
now declares `enacted` or `editorial`: 8 of 40 are enacted.

Populated mechanically, but with two narrowings that change the answer. The
rule as first written ("the title appears in the Act text") got it wrong in
both directions:

1. It credited unapplied Acts. Act 2 retitles Article 14 to "Leaves", so a
   naive check marked "Sabbatical Leave" enacted — claiming legal force for a
   heading not in effect. Only an Act already applied to that provision can
   enact its title.
2. It matched prose, not headings. It marked Article 11's "units" as enacted
   because clause (1) opens "The units of the NGO...", and Article 15's
   "Resignation" off "the resignation letter". A title now counts only where
   an instrument states it as a heading.

Under the corrected rule "units" is editorial and "Establishment" is enacted,
which is the distinction this field exists to draw.

Nothing renamed. `units`, `Alumini`, `Anual Reports` and `Ammendments` stand
as they are, marked and listed on the amendments page for the board to ratify
or replace. Recorded in RECONCILIATION.md that Act 2's operative text spells
Article 16 as "Amendments", so that typo corrects itself when Act 2 is
applied — by the instrument, not by an editor.

The field is optional in the schema so the frozen archives stay valid
unedited; validate.mjs requires it on the current document only.

Q11 signed off as section-only, with the resolution attributed to Act 1's
operative structure rather than editorial preference.
Part record, part checklist: the domain went live ahead of this document, so
it documents what happened, what broke and why, and what is still open.

Outstanding items are DNS and third-party dashboard changes, deliberately not
executed: GitHub reports https_enforced false because Cloudflare proxies the
domain and terminates TLS, so GitHub cannot provision its own certificate;
Cloudflare's SSL mode should be confirmed as Full (strict) rather than
Flexible; and Cloudflare is rewriting page content via email obfuscation,
which means the bytes served are not exactly the bytes built.

Also records the four-step rollback, and that the domain now publishes a
constitution reflecting one of three Acts and only partly — visible on every
page, but worth the board knowing it went live in that state.
… marker

The page led with "Version 3.0.0-alpha.1 · effective 2024-05-03". Both halves
were claims nobody made. No 3.0.0 exists, and the effective date contradicted
the banner immediately below it, which says the Acts that assented on that
date are unapplied.

info now separates the publication label from the legal position:

  version: 3.0.0-alpha.1     working label, build metadata
  legal_status: not_adopted  required, enumerated
  adopted_version: 2.0.0     what the board actually adopted
  text_as_of: 2024-05-02     what the displayed text reflects

effective_from is removed: an unadopted text has no effective date. The
headline, the footer, the JSON-LD legislationDate and the sitemap lastmod all
now state the adopted position — "last adopted version 2.0.0 … no later
version has been adopted" — and the working label moved to the amendments
page under a heading that says it is not a statement of what is in force.

The validator errors if legal_status is missing on the live document, if an
unadopted document also carries effective_from, or if adopted_version equals
the working version.

Heading marker inverted. 8 of 40 headings are enacted, so marking the
editorial ones marked the rule rather than the exception, and put the words
"not enacted" inside 32 headings where they read as a claim about the
provision rather than its heading. Now only the 8 enacted headings carry a
glyph with an accessible label, and the default is stated once on the
amendments page: headings are editorial aids unless marked. The YAML is
unchanged; only the presentation was wrong.

Banner notes are block-level. Inline, each ran straight on from its blocker:
"awaiting Q2Act 1 of 2024 also amends…", on every page.

A test asserts the build ships a real mailto: and no Cloudflare
email-obfuscation link. The build is clean; the rewrite is applied at the
edge and cannot be fixed here.

The keyboard harness now reads the shared base path instead of hardcoding
/OpenCodeLaw — it had kept serving the old prefix after the apex move, so
every asset 404'd inside the tests while the real site was fine. All 10 pass.
Fetching GitHub's Pages origin with the right Host header fails TLS
verification: no alternative certificate subject name matches
constitution.stmorg.in. Visitors are unaffected because 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 settles one of the open questions in item 1: Cloudflare's SSL mode
cannot currently be Full (strict) against this origin without returning a
526, because 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.
No provision text changes in this commit. Tripwire green, still one permitted
change.

Authorisation recorded as stated, not inferred. Pranay: "the amendments were
authored long ago and approved by every member of the board." Each Act's
procedure block now carries board approval with that statement as evidence,
and leaves intermediate-board and units unpopulated with complete: false —
Article 16(3) required all three bodies and only the board was named.
Resolves Q13; the circularity stands, since Act 2 amends 16(3) itself.

Resolved without new decisions:
- Q4 by convention C1. "Inserted" appears only under STATEMENT OF OBJECTS AND
  REASONS, which C1 forbids as authority. It is an amendment renaming Article
  15 to Exit Process.
- Q2 by literal application. A substitution of named clauses (1)-(5) does not
  reach clause (6), so Donor stands. The tension with Act 1's amended Article
  7(4) is recorded as a standing item, not resolved in the text.
- Q7: the Acts make every title correction themselves — Alumni, Annual Report,
  Amendments — plus two structural renames, Article 14 to Leaves and Article
  15 to Exit Process. No editorial rename is made. Holding these was right.
- Q5 confirmed: publish both identical definitions as enacted, note the defect.
- Q12 verified untriggerable: the three Acts perform 16 substitutes and 3
  inserts, and zero renumbers.

The manifest is the substance here. src/manifest.mjs extracts every provision
each Act touches; src/act-application.mjs declares how each Act span maps onto
article body and sections, by line range, so no provision text is ever
retyped — only sliced from acts/text/.

Structure is declared rather than inferred because inference 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 layout indentation is not a structural signal.

verifyCoverage asserts the declared slices reconstruct each Act span exactly
under the normaliser, so a dropped line, a duplicated one or an invented
phrase all fail. All ten article-scope structures pass.
Articles 9, 10, 11, 12 and 18 were already reconciled and are recorded, not
rewritten — the applier detects them and changes nothing.

Article 6: clauses (1)-(5) substituted into sections 1-5, with the Act's own
titles. Section 3 is renamed College Unit Board Member -> Unit Board Member.
Clause (6) Donor is untouched: the Act names clauses (1)-(5) and a
substitution of named clauses does not reach an unnamed one (Q2).

Article 7: clause (4) substituted, defining STM DONOR. Within clause (6),
sub-clause (a) point 6 is substituted (the performance-plan notice drops from
three months to two) and sub-clause (b) is substituted (terminations now
require written approval from the Internal Compliance Coordinator). Points
(a)1-5 are unnamed by the Act and stand unchanged — its own Statement of
Objects describes exactly this scope.

Every string is sliced from acts/text/, never retyped. The applier verifies
that the set of provisions that changed equals the set the manifest expects
for this Act, that each matches the Act's prescribed text under the
normaliser, and that nothing outside the manifest moved. art-6 and art-7 were
the only changes.

Tripwire re-baselined, not disabled. It now guards the post-Act-1 state.
…16, 17; insert Article 20

Ten provisions. The preamble and Articles 3, 4 and 5 are substituted in full.
Article 8 is substituted with its five clauses as sections, including the
internship duration dropping from three academic years to two.

Two structural renames the Act makes itself:
- Article 14 Sabbatical Leave -> Leaves, with Sabbatical Leave becoming
  clause (1). Three sections become one.
- Article 15 Resignation -> Exit Process, restructured into (1) Voluntary and
  (2) Involuntary. Three sections become two.

Article 16 clause 3 substituted: the amendment threshold becomes a 2/3rd
majority present and voting. The Act's own Statement of Objects says 3/4th;
convention C1 makes the operative text govern, and the discrepancy stays
recorded on the Act rather than normalised away.

Article 16 is also retitled Ammendments -> Amendments. That correction comes
from the instrument, which heads the clause "16. Amendments", not from an
editor — which is why holding the typo through Phase 3 was right. The applier
now takes the restated heading on clause-scope provisions too; it previously
only did so for article scope.

Article 17 clause (1) substituted. The Act numbers it (1) and the article
numbers it (a); the same clause, confirmed by its opening words. Clause (b)
is untouched.

Article 20 Meetings and Activities inserted.

Article 19 recorded as reserved. Inserting Article 20 opens a gap at 19, and
the validator refuses an unaccounted one. No instrument mentions 19 — a
whitespace-normalised search over all three Acts finds no occurrence in any
form — so it is left unallocated rather than closed by renumbering, which
would break every citation after it.

The preamble may now declare title_source; Act 2 enacts its heading.
Article 13 substituted and retitled Anual Reports -> Annual Report, by the
instrument. Article 21 Financial Management inserted. Only those two
provisions changed; the tripwire caught the pair before this commit, which is
what it is for.

Reconciliation is complete. version 3.0.0, legal_status adopted,
effective_from 2024-05-03. adopted_version and text_as_of are dropped: they
existed to describe a gap between the label and what was in force, and there
is no longer one. The validator inverts to match — an adopted document must
now state effective_from, and adopted_version may not disagree with version.

The reconciliation banner is gone, because it is no longer true. Its removal
follows the data: reconciliation_state is deleted and the template renders
nothing. Two items move to the amendments page as standing notes rather than
disappearing — Article 19 reserved with no instrument, and the Article 6(6)
Donor tension together with the identical clause (3)/(4) definitions.

The heading marker now follows the data instead of a fixed direction.
Applying the Acts flipped the ratio exactly, from 8 enacted of 40 to 32 of
40, so the marked set switched from enacted to editorial. The principle is
unchanged: mark the exception, state the default once. The accessible label
is reworded to name the heading and not its legal effect — "not enacted"
beside a provision reads as a claim about the provision.

All three Acts of 2024 are now applied. Articles 1-21 with 19 reserved.
Act 2 restructured Articles 14 and 15. Article-level ids are stable, but six
section ids changed what they denote — art-15-s-1 meant Eligibility under
Resignation and now means Voluntary under Exit Process.

Correct behaviour: a section id names a position within an article, and the
Act restructured those articles. Recorded because it is the first time an
anchor has moved rather than merely existed, with the before/after mapping in
RECONCILIATION.md and a bookmark-holder's version in CUTOVER.md.

/archive/2.0.0/ still resolves every old anchor, verified, so a citation made
before today remains readable as it stood. No redirect: the Act changed what
the provision says, and forwarding silently would hide that.
PROVENANCE.md flagged Article 6 clauses (2), (4) and (5) as DIVERGENT at
90-94%. Compared clause by clause against the Act they are 100% exact. The
fault was in containment(): a window stride of width/8 could straddle the
clause boundary and never land on the true alignment.

Stride is now token-by-token wherever the haystack is small enough to afford
it. All 24 provisions the three Acts touch now report ALREADY-APPLIED, which
is the correct state: every one of them matches its instrument.

A false DIVERGENT in a legal report is the same class of noise as the 98.6%
that turned out to be a comparison artefact — it sends a reviewer looking for
a problem that is not there.
Recorded while the gaps were visible. Six requirements, the largest being
that structure cannot be inferred from a PDF — Acts should be authored in the
same machine-readable schema as a patch, with the PDF as the signed rendering
rather than the source. Also: clauses need addressable ids so an Act can
target a clause instead of a line range; there is no propose-review-approve
state; procedure.complete is recorded but never enforced; anchor meaning
changes are not reported by the applier; and numbering gaps surface only
after the write.

Nothing implemented, per the deferral.
@KiranPranay
KiranPranay merged commit 32826b1 into main Aug 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant