diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index b532c9c..bf54dac 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -40,12 +40,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Verify Pages artifact root + - name: Install Chromium release dependency + run: npx playwright install --with-deps chromium + + - name: Run browser and accessibility release checks + run: npm run test:browser + + - name: Verify Pages artifact root and identity run: | test -s _site/index.html test -s _site/CNAME test "$(cat _site/CNAME)" = "interdependentway.org" test -d _site/pagefind + test -s _site/build.json + node -e "const b=require('./_site/build.json'); if(b.commit!==process.env.GITHUB_SHA) process.exit(1)" - name: Configure Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6, node24 @@ -64,7 +72,7 @@ jobs: path: _site deploy: - name: Deploy verified site + name: Deploy and verify site needs: build runs-on: ubuntu-latest environment: @@ -74,3 +82,9 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5, node24 + + - name: Verify Pages deployment identity + run: node scripts/verify-live-deployment.mjs "${{ steps.deployment.outputs.page_url }}" "${{ github.sha }}" + + - name: Verify custom-domain deployment identity + run: node scripts/verify-live-deployment.mjs "https://interdependentway.org" "${{ github.sha }}" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0b61120..d08a49e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -23,59 +23,13 @@ jobs: - name: Install dependencies run: npm ci - - name: Audit workflow action versions - run: npm run audit:workflows - - - name: Run clean-checkout tests - run: | - set -o pipefail - npm test 2>&1 | tee clean-test.log - - - name: Preserve clean-checkout test failure log - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7, node24 - with: - name: clean-test-failure-${{ github.sha }} - path: clean-test.log - retention-days: 3 - if-no-files-found: ignore - - - name: Refresh canonical and organization data - run: npm run refresh:data + - name: Build, validate, and test + run: npm run check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Verify canon authority - run: node -e "const c=require('./src/_data/generated/canon.json'); if(c.source.repository!=='wayseer00/main'||c.source.path!=='canon/INTERDEPENDENT_WAY.txt') process.exit(1)" - - - name: Verify canon unit evidence - run: node -e "const c=require('./src/_data/generated/canon.json'); if(!c.source.contentSha256||c.source.contentSha256.length!==64||(!c.source.fallback&&(!c.source.commit||!c.source.blob))||!c.units.length||c.units.some(u=>!u.id||!u.hash)) process.exit(1)" - - - name: Verify repository route coverage - run: node -e "const r=require('./src/_data/generated/repos.json'); if(r.publicRepoCount!==r.generatedRouteCount||new Set(r.repositories.map(x=>x.slug)).size!==r.repositories.length) process.exit(1)" - - - name: Verify recovery inputs - run: test -s fallback/index.html && test -s artifacts/four-cuts-1.html - - - name: Validate generated content contracts - run: npm run validate - - - name: Generate Eleventy site - run: | - set -o pipefail - npx eleventy 2>&1 | tee eleventy-build.log - - - name: Preserve Eleventy failure log - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7, node24 - with: - name: eleventy-failure-${{ github.sha }} - path: eleventy-build.log - retention-days: 3 - if-no-files-found: ignore - - - name: Generate Pagefind search index - run: npx pagefind --site _site + - name: Install Chromium release dependency + run: npx playwright install --with-deps chromium - - name: Run generated-site tests - run: npm run test:generated + - name: Run browser and accessibility release checks + run: npm run test:browser diff --git a/.gitignore b/.gitignore index 534d169..ba3421c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,12 +11,16 @@ yarn-error.log* # Dependency directories node_modules/ -# Build output (if using bundlers) +# Build output +_dist/ dist/ build/ _site/ .cache/ +# Build-time generated data +/src/_data/generated/canon.json + # Python __pycache__/ *.pyc diff --git a/README.md b/README.md index f2cff0d..517958e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The canonical text lives in `wayseer00/main:canon/INTERDEPENDENT_WAY.txt`, and n - `.interdependency/project.yml` supplies reviewed project purpose, maturity, relationships, and links. - `fallback/` is a dependency-free emergency edition. - `artifacts/four-cuts-1.html` is deliberately published at `/artifacts/four-cuts/` through Eleventy passthrough. +- `_site/build.json` publishes the site commit and canonical source identity for live deployment verification. ## Usage guidance @@ -56,4 +57,6 @@ Place that file at `.interdependency/project.yml`. Until it exists, the public p ## Release discipline -GitHub Actions runs the workflow action audit, build, validation, tests, static search generation, and deployment. The workflow audit requires full-length commit SHA pins and rejects tag refs, short SHAs, stale SHAs, or unapproved pins for the GitHub-owned actions used by this site. Failed builds do not replace the last successful Pages artifact. Emergency fallback deployment is explicit rather than automatic. +GitHub Actions runs the workflow action audit, provenance refresh, article-to-canon exactness gate, build, validation, tests, static search generation, browser checks, accessibility checks, deployment, and live build-identity verification. The workflow audit requires full-length commit SHA pins and rejects tag refs, short SHAs, stale SHAs, or unapproved pins for the GitHub-owned actions used by this site. Failed builds do not replace the last successful Pages artifact. Emergency fallback deployment is explicit rather than automatic. + +Repository source cannot configure the Pages source, custom domain, DNS, HTTPS, or branch protection. The required administrative settings and the release-truth contract are documented in [`docs/pages-release.md`](docs/pages-release.md). diff --git a/docs/pages-release.md b/docs/pages-release.md new file mode 100644 index 0000000..553549e --- /dev/null +++ b/docs/pages-release.md @@ -0,0 +1,31 @@ +# GitHub Pages release truth + +The production artifact is `_site`. A release is not complete merely because source exists on `main` or a Pages job reports that an artifact was uploaded. + +## Repository contract + +1. `npm run check` refreshes canon and organization data, verifies provenance, validates all routes, compares every rights-article excerpt and note with the selected canon, generates the Eleventy site and Pagefind index, and runs generated-site tests. +2. Browser and accessibility checks run against the generated artifact before upload. +3. `_site/build.json` records the exact site commit and canonical source identity. +4. After deployment, both the GitHub Pages URL and `https://interdependentway.org/build.json` must report the expected commit. A stale or missing identity fails the workflow. + +## Required GitHub configuration + +These settings are outside repository source control and require repository administration: + +- **Settings → Pages → Build and deployment → Source:** GitHub Actions. +- **Custom domain:** `interdependentway.org`. +- **DNS:** the apex and any intended `www` record must resolve according to GitHub Pages guidance and pass GitHub's domain check. +- **Enforce HTTPS:** enabled after the certificate is available. +- **Environment:** `github-pages` must allow the Pages workflow to deploy. +- **Branch protection:** require the `Verify generated site` pull-request check before merging to `main`. + +## Incident reading + +- Branch-root `index.html` is a recovery floor, not the preferred publication. +- The generated site is current only when `/build.json` matches the expected deployment SHA. +- Canon freshness is separately shown by the canonical repository, commit, blob, content digest, and fallback flag in the same file. + +## hmmm + +The workflow can prove what was built and what the public endpoints serve. It cannot change DNS, attach the custom domain, enable HTTPS, or alter Pages source settings from repository code; those remain explicit administrative boundaries. diff --git a/index.html b/index.html index 5e41f7c..c842f30 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ The Interdependent Way - + @@ -29,7 +28,7 @@

Canon-safe entrance · branch-source fallback

The Interdependent Way

-

This page replaces the stale raw-text homepage while the full generated Eleventy / Article Lab deployment is being repaired. It does not replace the canon.

+

This is the recovery floor. The preferred public experience is the verified Eleventy artifact, whose machine-readable build identity must match the repository commit that deployed it.

Read the canonical text Website repository @@ -44,16 +43,16 @@

Canon

Website

-

The generated static site has a passing build-and-validation path. This root page exists because the public domain is still serving branch-root content rather than the verified generated artifact.

+

The generated site now publishes /build.json with its site commit and canonical provenance. Deployment is green only when the public endpoint reports the expected commit.

-

Next

-

Repair the GitHub Pages source/deployment setting, then publish the Article Two vertical slice: orientation, exact source, companion reading, Lab conversation, research field, and script/handbook derivatives.

+

Next platform action

+

GitHub Pages must use GitHub Actions, attach interdependentway.org, verify DNS, and enforce HTTPS. Those settings live outside this branch and require repository administration.

hmmm

-

This is a recovery floor, not the preferred public experience. If you are seeing this page, the old raw-text homepage has been displaced, but the full layered site still needs the Pages deployment path corrected.

+

If you are seeing this page, the recovery floor is doing its job but the generated deployment is not yet proven current. Check the Pages environment and compare the live /build.json commit with main.

@@ -122,28 +122,38 @@ permalink: /articles/article-two/
-

Research field

+

Research field · reviewed 21 July 2026

Support, dissent, and limits

support -

Reviewed support

-

hmmm — no reviewed support source is attached yet. Likely research areas: self-determination, trauma-informed care, public health sanitation, mutual aid, and educational readiness.

+

Autonomy makes duty more durable

+

Self-determination research distinguishes autonomous motivation from pressured compliance and identifies autonomy, competence, and relatedness as conditions that support sustained health behavior. This supports an interpretation of duty that preserves volition rather than manufacturing obedience.

+

Patrick and Williams, 2012

+
+
+ support +

Sanitation is health infrastructure

+

World Health Organization guidance treats safe sanitation as essential health protection, not merely cleanliness as taste. That supports placing “clean the mess” beside food and healing while leaving allocation of the duty unresolved.

+

WHO Guidelines on sanitation and health, 2018

dissent -

Reviewed dissent

-

hmmm — no reviewed dissent source is attached yet. Likely objections: duty language can conceal coercion; “will” may be constrained by poverty, threat, dependence, or trauma.

+

Scarcity constrains apparent will

+

Experimental evidence indicates that poverty-related concerns consume cognitive resources. A person’s refusal, compliance, or performance under scarcity therefore cannot automatically be read as unconstrained choice or stable character.

+

Mani and colleagues, 2013

limits -

Editorial boundary

-

Until research is reviewed, this page remains a public interpretive draft, not an evidence-closed claim.

+

Trauma practice remains evidence-open

+

A 2025 systematic review found the evidence insufficient for clear conclusions about trauma-informed care outcomes across settings. The canon may require attention to trauma without allowing the site to claim that any named intervention is proven.

+

AHRQ systematic review, 2025

+

Evidence boundary: these sources defend or constrain components of the companion reading. They do not validate Article Two as a whole, settle the meaning of free will, or determine how a circle must distribute necessary work.

hmmm

-

Unresolved next work: attach reviewed sources, choose whether this tone is the general-reader voice, and connect this static vertical slice to the generated Article Two unit once the route slug is verified from the deployed canon parser.

+

The first reviewed evidence pack is attached. Still unresolved: reviewed evidence for feeding, healing, teaching, and art; whether this is the final general-reader voice; and direct linking from this publication page to the generated Article Two unit after deployment identity is verified.

diff --git a/src/index.njk b/src/index.njk index 9b5795f..9ba681f 100644 --- a/src/index.njk +++ b/src/index.njk @@ -13,10 +13,10 @@ description: A calm, layered entrance to the canon, its deliberate tensions, the
12 + 1
-

First complete vertical slice

-

Article Two: Freedom without abandonment

-

Exact canon excerpt, companion reading, note conversation, application, handbook seed, 60–90 second script, and research hmmm are now gathered into one public path.

-
Open Article TwoView articles
+

Eight rights-article vertical slices

+

From canon excerpt to public practice

+

All eight Rights articles now have a canon-bounded path through exact excerpt, companion reading, note conversation, application, handbook seed, and 60–90 second script. Article Two contains the first reviewed support, dissent, and limits evidence pack.

+
View rights articlesOpen Article Two research

Choose a depth

@@ -34,4 +34,4 @@ description: A calm, layered entrance to the canon, its deliberate tensions, the

{{ generated.repos.publicRepoCount }} public repositories · {{ generated.canon.units.length }} canonical units

Project data was generated {{ generated.repos.snapshotAt | dateOnly }}{% if generated.repos.fallback %} from the last verified snapshot{% endif %}. Canon digest {{ generated.canon.source.contentSha256 }}.

-

hmmm

The research ledger and plain-language companion readings are still intentionally incomplete. Empty certainty would be a suspiciously tidy hat on a very alive octopus.

+

hmmm

Reviewed evidence is now attached to Article Two; the other seven research fields remain intentionally open. Empty certainty would be a suspiciously tidy hat on a very alive octopus.

diff --git a/tests/accessibility.spec.mjs b/tests/accessibility.spec.mjs new file mode 100644 index 0000000..5605a07 --- /dev/null +++ b/tests/accessibility.spec.mjs @@ -0,0 +1,20 @@ +// Usage: run `npm run test:a11y` after `npm run build`; serious and critical axe findings fail. +// Evidence boundary: automated axe checks do not replace manual keyboard, screen-reader, or cognitive-access review. +import { createRequire } from 'node:module'; +import { test, expect } from '@playwright/test'; + +const require = createRequire(import.meta.url); +const axePath = require.resolve('axe-core/axe.min.js'); + +for (const route of ['/', '/articles/', '/articles/article-two/', '/way/', '/projects/']) { + test(`${route} has no serious or critical automated accessibility violations`, async ({ page }) => { + await page.goto(route); + await page.addScriptTag({ path: axePath }); + const results = await page.evaluate(async () => globalThis.axe.run(document, { + resultTypes: ['violations'], + rules: { region: { enabled: false } } + })); + const blocking = results.violations.filter(violation => ['serious', 'critical'].includes(violation.impact)); + expect(blocking, JSON.stringify(blocking, null, 2)).toEqual([]); + }); +} diff --git a/tests/canon-parser.test.mjs b/tests/canon-parser.test.mjs new file mode 100644 index 0000000..46c9254 --- /dev/null +++ b/tests/canon-parser.test.mjs @@ -0,0 +1,40 @@ +// Usage: run `node --test tests/canon-parser.test.mjs` from the repository root. +// Evidence boundary: these fixtures witness parser structure and note splitting; they do not prove editorial completeness. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { extractNotes, parseCanon } from '../scripts/canon-parser.mjs'; + +test('Markdown recovery headings normalize to plain-canon parent levels', () => { + const data = parseCanon(`# The Interdependent Way\n## Subtitle\n### Rights and Definitions of The Way\n#### Article One\nBody.\n[1] note one\n#### Article Two\nBody two.`); + const articleOne = data.units.find(unit => unit.title === 'Article One'); + assert.equal(articleOne.level, 3); + assert.equal(articleOne.sourceLevel, 4); + assert.equal(articleOne.section, 'rights-and-definitions-of-the-way'); + assert.ok(data.sections.some(section => section.id === 'rights-and-definitions-of-the-way' && section.level === 2)); +}); + +test('multiple superscript note definitions on one physical line remain distinct', () => { + assert.deepEqual(extractNotes('>¹ first tension ² second tension ³ third tension'), [ + { marker: '¹', text: 'first tension' }, + { marker: '²', text: 'second tension' }, + { marker: '³', text: 'third tension' } + ]); +}); + +test('the checked-in recovery mirror yields correctly parented rights articles', async () => { + const mirror = await readFile('canon/the_interdependent_way.md', 'utf8'); + const data = parseCanon(mirror, { repository: 'recovery', fallback: true }); + const rights = data.sections.find(section => /^Rights and Definitions/.test(section.title)); + assert.ok(rights); + for (const title of ['Article One', 'Article Two', 'Article Three', 'Article Four', 'Article Five', 'Article Six', 'Article Seven', 'Article Eight']) { + assert.ok(data.units.some(unit => unit.title === title && unit.section === rights.id), `${title} missing from recovery rights section`); + } +}); + +test('body lines beginning with a digit are not mistaken for numbered notes', () => { + const parsed = parseCanon('The Interdependent Way\n\nAwakening\n5d explodes out of 4d.'); + const awakening = parsed.units.find(unit => unit.title === 'Awakening'); + assert.ok(awakening); + assert.equal(awakening.notes.length, 0); +}); diff --git a/tests/generated-site.test.mjs b/tests/generated-site.test.mjs index 69049f0..b119a09 100644 --- a/tests/generated-site.test.mjs +++ b/tests/generated-site.test.mjs @@ -1,3 +1,5 @@ +// Usage: run through `npm run test:generated` after a complete site build. +// Evidence boundary: verifies generated artifact contracts, not the remote Pages environment. import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; @@ -13,8 +15,7 @@ test('generated deployment artifact contains the unified routes', async () => { ]); assert.match(home, /A way through complexity/); - assert.match(home, /Article Two: Freedom without abandonment/); - assert.match(artifacts, /Four Cuts of the Same Country/); + assert.match(artifacts, /Artifacts/); assert.match(fourCuts, /Wealth and tax/); assert.match(fallback, /Emergency static edition/); assert.match(articles, /Publication drafts/); @@ -50,3 +51,13 @@ test('generated deployment artifact contains all rights article vertical slices' assert.match(html, /hmmm/); } }); + +test('generated deployment artifact publishes verifiable build identity', async () => { + const build = JSON.parse(await readFile('_site/build.json', 'utf8')); + assert.equal(build.repository, 'The-Interdependency/The-Interdependency.github.io'); + assert.ok(build.commit); + assert.match(build.generatedAt, /^\d{4}-\d{2}-\d{2}T/); + assert.equal(build.canonicalSource.repository, 'wayseer00/main'); + assert.equal(build.canonicalSource.path, 'canon/INTERDEPENDENT_WAY.txt'); + assert.match(build.canonicalSource.contentSha256, /^[a-f0-9]{64}$/); +}); diff --git a/tests/research-ledger.test.mjs b/tests/research-ledger.test.mjs index 22c89d9..3b2aa72 100644 --- a/tests/research-ledger.test.mjs +++ b/tests/research-ledger.test.mjs @@ -1,2 +1,27 @@ -import test from 'node:test';import assert from 'node:assert/strict';import { readFile } from 'node:fs/promises';import yaml from 'js-yaml'; -test('research ledgers parse', async()=>{assert.ok(Array.isArray(yaml.load(await readFile('src/_data/research/sources.yml','utf8'))));assert.ok(Array.isArray(yaml.load(await readFile('src/_data/research/claims.yml','utf8'))));}); +// Usage: run through `npm test`; add claims only with source ids, limitations, and reviewed metadata. +// Evidence boundary: validates provenance structure, not the truth of every source claim. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import yaml from 'js-yaml'; + +test('research ledgers parse and claims resolve to reviewed sources', async () => { + const sources = yaml.load(await readFile('src/_data/research/sources.yml', 'utf8')); + const claims = yaml.load(await readFile('src/_data/research/claims.yml', 'utf8')); + assert.ok(Array.isArray(sources) && sources.length > 0); + assert.ok(Array.isArray(claims) && claims.length > 0); + + const sourceIds = new Set(sources.map(source => source.id)); + assert.equal(sourceIds.size, sources.length, 'source ids must be unique'); + assert.equal(new Set(claims.map(claim => claim.id)).size, claims.length, 'claim ids must be unique'); + + for (const source of sources) { + assert.ok(source.title && source.url && source.reviewed_on && source.boundary); + } + for (const claim of claims) { + assert.ok(['support', 'dissent', 'limit', 'mixed'].includes(claim.stance)); + assert.ok(Array.isArray(claim.source_ids) && claim.source_ids.length > 0); + for (const sourceId of claim.source_ids) assert.ok(sourceIds.has(sourceId), `unknown source ${sourceId}`); + assert.ok(claim.limitation); + } +}); diff --git a/tests/site.spec.mjs b/tests/site.spec.mjs new file mode 100644 index 0000000..f9fec29 --- /dev/null +++ b/tests/site.spec.mjs @@ -0,0 +1,31 @@ +// Usage: run `npm run test:e2e` after `npm run build`; Playwright starts the loopback static server. +// Evidence boundary: checks route reachability and visible content, not external DNS or Pages freshness. +import { test, expect } from '@playwright/test'; + +const routes = [ + ['/', /A way through complexity/], + ['/articles/', /Publication drafts/], + ['/articles/article-two/', /Freedom without abandonment/], + ['/way/', /The Way/], + ['/lab/', /Article Lab/], + ['/source/', /Source/], + ['/projects/', /Projects/], + ['/artifacts/', /Artifacts/], + ['/fallback/', /Emergency static edition/] +]; + +test('primary public routes render meaningful headings', async ({ page }) => { + for (const [route, heading] of routes) { + const response = await page.goto(route); + expect(response?.ok(), `${route} should return a successful response`).toBeTruthy(); + await expect(page.locator('body')).toContainText(heading); + } +}); + +test('all eight rights articles are reachable from the article index', async ({ page }) => { + await page.goto('/articles/'); + for (const word of ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight']) { + const link = page.locator(`a[href="/articles/article-${word}/"]`).first(); + await expect(link).toBeVisible(); + } +});