diff --git a/.eleventy.js b/.eleventy.js index e0446bb..b9bcf6d 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -2,9 +2,9 @@ import markdownIt from 'markdown-it'; // === MODULE_BUILD === // id: eleventy_site_configuration -// purpose: Build the static-first public knowledge system and copy deliberate fallback artifacts. +// purpose: Build the static-first public knowledge system, render exact distributed-textbook Markdown, and copy deliberate fallback artifacts. // entrypoint: npm run build -// tests: tests/site-contract.test.mjs +// tests: tests/site-contract.test.mjs, tests/generated-site.test.mjs // === END MODULE_BUILD === export default function configureEleventy(eleventyConfig) { @@ -20,6 +20,7 @@ export default function configureEleventy(eleventyConfig) { eleventyConfig.addFilter('dateOnly', value => value ? String(value).slice(0, 10) : 'hmmm'); eleventyConfig.addFilter('where', (items, key, value) => (items || []).filter(item => item?.[key] === value)); eleventyConfig.addFilter('statusClass', value => `status-${String(value || 'hmmm').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`); + eleventyConfig.addFilter('markdown', value => md.render(String(value || ''))); return { dir: { input: 'src', output: '_site', includes: '_includes', data: '_data' }, diff --git a/.gitignore b/.gitignore index ba3421c..8d3e13d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ _site/ # Build-time generated data /src/_data/generated/canon.json +/src/_data/generated/textbook.json # Python __pycache__/ diff --git a/README.md b/README.md index 97fdc61..d03dcc3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,36 @@ # The Interdependent Way public knowledge system -This repository builds `interdependentway.org`: a static-first, progressively layered entrance to The Interdependent Way, its deliberate tensions, surrounding research, public artifacts, and the repositories attempting implementation. +This repository builds `interdependentway.org`: a static-first, progressively layered entrance to The Interdependent Way, its deliberate tensions, the distributed Interdependency textbook, surrounding research, public artifacts, and the repositories attempting implementation. ## What is authoritative -The canonical text lives in `wayseer00/main:canon/INTERDEPENDENT_WAY.txt`, and nowhere in this repository supersedes it. The repository copy at `canon/the_interdependent_way.md` is a recovery mirror only. Build output records whether the remote source or recovery mirror supplied the current snapshot, together with SHA-256 provenance; successful remote retrieval also records the resolved source commit and blob SHA. +The canonical text of **The Interdependent Way** lives in `wayseer00/main:canon/INTERDEPENDENT_WAY.txt`, and nowhere in this repository supersedes it. The repository copy at `canon/the_interdependent_way.md` is a recovery mirror only. Build output records whether the remote source or recovery mirror supplied the current snapshot, together with SHA-256 provenance; successful remote retrieval also records the resolved source commit and blob SHA. + +The **distributed Interdependency textbook** is a separate technical reading sequence. Chapters Zero through Seven remain owned by their source repositories: + +| Chapter | Title | Source | +|---:|---|---| +| 0 | Meta Energy Theory — Axioms, Postulates, and Theorems | `The-Interdependency/metapat:CHAPTER_ZERO.md` | +| 1 | The Subtractive Foundations of the Unit Carrier | `The-Interdependency/ucns:docs/chapter-1.md` | +| 2 | Measurement Without Transfer | `The-Interdependency/edcm:docs/chapter-2.md` | +| 3 | Modules That Speak for Themselves | `The-Interdependency/skill-lib:docs/chapter-3.md` | +| 4 | Canon Without Inversion | `The-Interdependency/interdependent-lib:docs/chapter-4.md` | +| 5 | One Architecture, Four Layers | `The-Interdependency/ptcna:docs/chapter-5.md` | +| 6 | The Instrument | `The-Interdependency/a0:docs/chapter-6.md` | +| 7 | The Echo | `The-Interdependency/zfae:docs/chapter-7.md` | + +This site displays exact, provenance-bearing chapter snapshots. It does not merge licenses or transfer theorem, proof, empirical, frontier, or canonical status between repositories. ## Architecture - `/` is the canon-derived **Awakening** splash page and public threshold. - `/home/` is the complete living knowledge-system entrance. - `/preamble/` remains one click from Awakening and from the primary navigation. +- `/chapters/` is the unified index for the eight-repository textbook. +- `/chapters/chapter-zero/` through `/chapters/chapter-seven/` render exact source Markdown with commit, blob, digest, and repository links. - Eleventy generates complete HTML into `_site`. - Pagefind supplies static search. -- GitHub organization and canon data are retrieved at build time, never in a visitor’s browser. +- GitHub organization, canon, and textbook source data are retrieved at build time, never in a visitor’s browser. - Every public organization repository receives a generated project page. - `.interdependency/project.yml` supplies reviewed project purpose, maturity, relationships, and links. - `fallback/` is a dependency-free emergency edition. @@ -28,18 +45,30 @@ npm run dev npm run check ``` -Use the recovery snapshots without network access: +Refresh only the textbook source set: + +```bash +npm run refresh:textbook +``` + +Use recovery snapshots without network access: ```bash OFFLINE=1 npm run build ``` +An offline build uses the retained last-known-good textbook snapshot when one exists. If a clean checkout has no retained chapter content, it renders metadata-only `hmmm` records rather than inventing or silently copying text. + Route checks after a build: ```text -/ Awakening threshold -/home/ knowledge-system entrance -/preamble/ direct canonical Preamble +/ Awakening threshold +/home/ knowledge-system entrance +/preamble/ direct canonical Preamble +/chapters/ textbook index +/chapters/chapter-zero/ Chapter Zero +... +/chapters/chapter-seven/ Chapter Seven ``` Add reviewed project metadata to a repository: @@ -60,6 +89,7 @@ Place that file at `.interdependency/project.yml`. Until it exists, the public p ## Status language - **canon** — exact or mechanically derived from the canonical source +- **distributed textbook** — exact source material whose status remains local to its owning repository - **interpretation** — explanatory material subject to review - **research** — externally sourced support, dissent, or context - **implemented** — a working public surface exists @@ -68,6 +98,6 @@ Place that file at `.interdependency/project.yml`. Until it exists, the public p ## Release discipline -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. +GitHub Actions runs the workflow action audit, canon and textbook provenance refresh, article-to-canon exactness gate, build, validation, tests, static search generation, browser checks, accessibility checks, deployment, and live build-identity verification. The textbook gate requires all eight current source files during an online production build and rejects missing content, reordered chapters, changed source locations, missing source identities, or silent fallback. The workflow action 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/package.json b/package.json index 73ebae8..1a14a1f 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,18 @@ "type": "module", "scripts": { "dev": "npm run refresh:data && eleventy --serve", - "refresh:data": "npm run refresh:canon && npm run refresh:github", + "refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:textbook", "refresh:canon": "node scripts/fetch-canon.mjs && node scripts/parse-canon.mjs", "refresh:github": "node scripts/fetch-github-org.mjs", + "refresh:textbook": "node scripts/fetch-textbook.mjs", "research:enrich": "node scripts/enrich-citations.mjs", "audit:workflows": "node scripts/audit-workflows.mjs", "prevalidate": "npm run refresh:data", "validate": "node scripts/validate-content.mjs && node scripts/verify-generated-routes.mjs && node scripts/verify-article-canon.mjs", "build": "npm run validate && eleventy && pagefind --site _site && node scripts/write-build-info.mjs", "pretest": "node scripts/prepare-tests.mjs", - "test": "node --test tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/site-contract.test.mjs", - "test:generated": "node --test tests/generated-site.test.mjs && node tests/links.test.mjs", + "test": "node --test tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/site-contract.test.mjs", + "test:generated": "node --test tests/generated-site.test.mjs tests/textbook-generated.test.mjs && node tests/links.test.mjs", "test:browser": "playwright test", "test:e2e": "playwright test tests/site.spec.mjs", "test:a11y": "playwright test tests/accessibility.spec.mjs", diff --git a/scripts/fetch-textbook.mjs b/scripts/fetch-textbook.mjs new file mode 100644 index 0000000..c1659a7 --- /dev/null +++ b/scripts/fetch-textbook.mjs @@ -0,0 +1,173 @@ +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; + +// === MODULE_BUILD === +// id: distributed_textbook_fetch +// module_name: fetch-textbook +// module_kind: service +// summary: Resolves chapters zero through seven from their owning repositories and emits one provenance-bearing reading dataset. +// owner: Erin Spencer +// public_surface: npm run refresh:textbook, generated.textbook +// internal_surface: allowlisted GitHub API and raw-file retrieval, chapter-source validation, snapshot fallback +// auth_boundary: optional read-only GITHUB_TOKEN +// storage_boundary: writes generated textbook data and a last-known-good snapshot +// network_boundary: allowlisted read-only HTTPS to api.github.com and raw.githubusercontent.com +// user_data_boundary: none +// admin_only: false +// tests: tests/textbook-integrity.test.mjs, tests/generated-site.test.mjs, tests/site.spec.mjs +// rollout: required by refresh:data before validation and Eleventy generation +// rollback: remove refresh:textbook and the generated chapter routes; source repositories remain unchanged +// === END MODULE_BUILD === +// Usage: run `npm run refresh:textbook`; set OFFLINE=1 to use a retained snapshot or metadata-only hmmm records. +// Limits: displays exact source Markdown but does not transfer license, theorem, proof, empirical, or canonical status between repositories. + +// === BOUNDARIES === +// id: distributed_textbook_network_boundary +// summary: Reads only the eight declared chapter files from allowlisted GitHub endpoints. +// auth_boundary: optional GitHub read token +// storage_boundary: write beneath src/_data/generated and src/_data/snapshots +// network_boundary: external read-only +// user_data_boundary: none +// admin_only: false +// pii: none +// secrets: GITHUB_TOKEN is passed only as an HTTPS authorization header and never written +// side_effects: generated dataset and snapshot writes +// owner: Erin Spencer +// === END BOUNDARIES === + +const apiOrigin = 'https://api.github.com'; +const rawOrigin = 'https://raw.githubusercontent.com'; +const allowedOrigins = new Set([apiOrigin, rawOrigin]); +const manifest = JSON.parse(await readFile('src/_data/textbook_sources.json', 'utf8')); +const snapshotPath = 'src/_data/snapshots/textbook.last-known-good.json'; +const generatedPath = 'src/_data/generated/textbook.json'; +const githubHeaders = ['-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28']; +if (process.env.GITHUB_TOKEN) githubHeaders.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`); + +function curlText(target, extraHeaders = []) { + const url = target instanceof URL ? target : new URL(target); + if (url.protocol !== 'https:' || !allowedOrigins.has(url.origin)) throw new Error(`refusing non-allowlisted textbook target: ${url.origin}`); + return execFileSync('curl', ['-fsSL', '--retry', '2', '--max-time', '30', ...extraHeaders, url.href], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); +} + +function getJson(target) { + return JSON.parse(curlText(target, githubHeaders)); +} + +function apiUrl(pathname, search = {}) { + const url = new URL(pathname, apiOrigin); + for (const [key, value] of Object.entries(search)) url.searchParams.set(key, String(value)); + return url; +} + +function encodedRepository(repository) { + const [owner, repo] = repository.split('/'); + if (!owner || !repo) throw new Error(`invalid chapter repository ${repository}`); + return { owner: encodeURIComponent(owner), repo: encodeURIComponent(repo) }; +} + +function encodedPath(path) { + return path.split('/').map(segment => encodeURIComponent(segment)).join('/'); +} + +function validateManifest() { + if (!Array.isArray(manifest) || manifest.length !== 8) throw new Error('textbook source manifest must contain chapters zero through seven'); + const numbers = manifest.map(source => source.number); + if (numbers.some((number, index) => number !== index)) throw new Error('textbook source manifest must be ordered 0 through 7'); + if (new Set(manifest.map(source => source.slug)).size !== 8) throw new Error('textbook chapter slugs must be unique'); + if (new Set(manifest.map(source => `${source.repository}:${source.path}`)).size !== 8) throw new Error('textbook chapter source locations must be unique'); +} + +function metadataOnly(source, error) { + return { + ...source, + content: null, + contentSha256: null, + commit: null, + blob: null, + resolvedUrl: null, + sourceUrl: `https://github.com/${source.repository}/blob/${source.branch}/${source.path}`, + retrievedAt: null, + fallback: true, + retrievalError: error + }; +} + +function fetchOne(source) { + const { owner, repo } = encodedRepository(source.repository); + const path = encodedPath(source.path); + const base = `/repos/${owner}/${repo}`; + const commitInfo = getJson(apiUrl(`${base}/commits/${encodeURIComponent(source.branch)}`)); + const commit = commitInfo.sha; + if (!/^[a-f0-9]{40}$/i.test(commit)) throw new Error(`${source.repository} branch did not resolve to a commit SHA`); + const fileInfo = getJson(apiUrl(`${base}/contents/${path}`, { ref: commit })); + if (fileInfo.type !== 'file' || !fileInfo.sha) throw new Error(`${source.repository}/${source.path} did not resolve to a file blob`); + const resolvedUrl = new URL(`/${owner}/${repo}/${commit}/${path}`, rawOrigin); + const content = curlText(resolvedUrl); + if (!content.trim()) throw new Error(`${source.repository}/${source.path} is empty`); + if (!content.includes(source.title)) throw new Error(`${source.repository}/${source.path} no longer contains expected title: ${source.title}`); + const chapterHeading = source.number === 0 ? '# Chapter Zero' : `# Chapter ${source.number}:`; + if (!content.includes(chapterHeading)) throw new Error(`${source.repository}/${source.path} no longer declares ${chapterHeading}`); + return { + ...source, + content, + contentSha256: createHash('sha256').update(content).digest('hex'), + commit, + blob: fileInfo.sha, + resolvedUrl: resolvedUrl.href, + sourceUrl: `https://github.com/${source.repository}/blob/${commit}/${source.path}`, + retrievedAt: new Date().toISOString(), + fallback: false, + retrievalError: null + }; +} + +async function readSnapshot() { + try { + const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8')); + return Array.isArray(snapshot?.chapters) ? snapshot : null; + } catch { + return null; + } +} + +validateManifest(); +await mkdir('src/_data/generated', { recursive: true }); +await mkdir('src/_data/snapshots', { recursive: true }); +const previous = await readSnapshot(); +const previousByNumber = new Map((previous?.chapters || []).map(chapter => [chapter.number, chapter])); +const offline = process.env.OFFLINE === '1'; +const chapters = []; + +for (const source of manifest) { + if (offline) { + const retained = previousByNumber.get(source.number); + chapters.push(retained ? { ...retained, ...source, fallback: true, retrievalError: 'offline requested; retained last-known-good chapter content' } : metadataOnly(source, 'offline requested; no retained chapter snapshot')); + continue; + } + try { + chapters.push(fetchOne(source)); + } catch (error) { + const retained = previousByNumber.get(source.number); + const message = String(error?.message || error); + chapters.push(retained ? { ...retained, ...source, fallback: true, retrievalError: message } : metadataOnly(source, message)); + } +} + +const dataset = { + schema: 'interdependency.distributed-textbook/1.0.0', + title: 'The Interdependency — Chapters Zero Through Seven', + generatedAt: new Date().toISOString(), + fallback: chapters.some(chapter => chapter.fallback), + complete: chapters.every(chapter => Boolean(chapter.content)), + chapterCount: chapters.length, + chapters +}; + +await writeFile(generatedPath, JSON.stringify(dataset, null, 2)); +if (!offline && chapters.every(chapter => !chapter.fallback && chapter.content)) await writeFile(snapshotPath, JSON.stringify(dataset, null, 2)); +console.log(`textbook chapters=${chapters.length} complete=${dataset.complete} fallback=${dataset.fallback}`); diff --git a/scripts/prepare-tests.mjs b/scripts/prepare-tests.mjs index f55694a..25faf86 100644 --- a/scripts/prepare-tests.mjs +++ b/scripts/prepare-tests.mjs @@ -2,9 +2,9 @@ import { execFileSync } from 'node:child_process'; // === MODULE_BUILD === // id: offline_test_data_preparation -// purpose: Regenerate canon and project data deterministically before npm test without requiring network access or a full site build. +// purpose: Regenerate canon, project, and distributed-textbook data deterministically before npm test without requiring network access or a full site build. // entrypoint: npm test via the pretest lifecycle -// tests: tests/canon-integrity.test.mjs, tests/repo-coverage.test.mjs +// tests: tests/canon-integrity.test.mjs, tests/textbook-integrity.test.mjs, tests/repo-coverage.test.mjs // === END MODULE_BUILD === // === BOUNDARIES === // id: offline_test_preparation @@ -17,7 +17,8 @@ const env = { ...process.env, OFFLINE: '1', GITHUB_TOKEN: '' }; for (const script of [ 'scripts/fetch-canon.mjs', 'scripts/parse-canon.mjs', - 'scripts/fetch-github-org.mjs' + 'scripts/fetch-github-org.mjs', + 'scripts/fetch-textbook.mjs' ]) { execFileSync(process.execPath, [script], { env, stdio: 'inherit' }); } diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index fc796bd..bcd85fe 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -5,18 +5,18 @@ import { access, readFile } from 'node:fs/promises'; // id: generated_content_gate // module_name: validate-content // module_kind: instrument -// summary: Refuses deployment when canon identity, heading hierarchy, snapshot integrity, generated route coverage, or recovery artifacts drift. +// summary: Refuses deployment when canon identity, heading hierarchy, textbook coverage, snapshot integrity, generated route coverage, or recovery artifacts drift. // owner: Erin Spencer // public_surface: npm run validate -// internal_surface: canon snapshot digest, heading hierarchy, and repository-route assertions +// internal_surface: canon snapshot digest, heading hierarchy, distributed-textbook provenance, and repository-route assertions // auth_boundary: none // storage_boundary: read // network_boundary: none // user_data_boundary: none // admin_only: false -// tests: tests/canon-parser.test.mjs, tests/canon-integrity.test.mjs, tests/repo-coverage.test.mjs, tests/site-contract.test.mjs +// tests: tests/canon-parser.test.mjs, tests/canon-integrity.test.mjs, tests/textbook-integrity.test.mjs, tests/repo-coverage.test.mjs, tests/site-contract.test.mjs // rollout: required by npm run build and npm run check -// rollback: remove the gate only with an explicit replacement preserving provenance, hierarchy, and route checks +// rollback: remove the gate only with an explicit replacement preserving provenance, hierarchy, textbook, and route checks // === END MODULE_BUILD === // Usage: run `npm run validate`; it refreshes data first and exits nonzero on any integrity mismatch. // Limits: validates repository artifacts, not GitHub Pages settings or public DNS. @@ -37,6 +37,8 @@ import { access, readFile } from 'node:fs/promises'; const canon = JSON.parse(await readFile('src/_data/generated/canon.json', 'utf8')); const repos = JSON.parse(await readFile('src/_data/generated/repos.json', 'utf8')); +const textbook = JSON.parse(await readFile('src/_data/generated/textbook.json', 'utf8')); +const textbookSources = JSON.parse(await readFile('src/_data/textbook_sources.json', 'utf8')); const snapshotRaw = await readFile('src/_data/snapshots/canon.last-known-good.md', 'utf8'); const snapshotText = snapshotRaw.replace(/^---\n[\s\S]*?\n---\n/, ''); const snapshotHash = createHash('sha256').update(snapshotText).digest('hex'); @@ -60,8 +62,29 @@ if (preamble.level !== 2 || preamble.section !== 'preamble') throw new Error('Pr if (!canon.source.fallback) { const interdefinablesIndex = canon.sections.findIndex(section => section.title === 'The Interdefinables'); const preambleIndex = canon.sections.findIndex(section => section.title === 'Preamble'); - if (interdefinablesIndex < 0 || preambleIndex !== interdefinablesIndex + 1) { - throw new Error('Preamble must be the next major section after The Interdefinables'); + if (interdefinablesIndex < 0 || preambleIndex !== interdefinablesIndex + 1) throw new Error('Preamble must be the next major section after The Interdefinables'); +} + +if (textbook.schema !== 'interdependency.distributed-textbook/1.0.0') throw new Error(`unexpected textbook schema: ${textbook.schema}`); +if (!Array.isArray(textbookSources) || textbookSources.length !== 8) throw new Error('textbook source manifest must contain exactly eight chapters'); +if (!Array.isArray(textbook.chapters) || textbook.chapters.length !== 8 || textbook.chapterCount !== 8) throw new Error('generated textbook must contain chapters zero through seven'); +for (let number = 0; number < 8; number += 1) { + const source = textbookSources[number]; + const chapter = textbook.chapters[number]; + if (source.number !== number || chapter.number !== number) throw new Error(`textbook chapter order drift at ${number}`); + for (const key of ['slug', 'title', 'repository', 'path', 'branch']) { + if (chapter[key] !== source[key]) throw new Error(`textbook chapter ${number} ${key} drift`); + } +} +if (new Set(textbook.chapters.map(chapter => chapter.slug)).size !== 8) throw new Error('duplicate textbook chapter slug'); +if (new Set(textbook.chapters.map(chapter => `${chapter.repository}:${chapter.path}`)).size !== 8) throw new Error('duplicate textbook source location'); +if (process.env.OFFLINE !== '1') { + if (!textbook.complete || textbook.fallback) throw new Error('production textbook refresh is incomplete or using fallback content'); + for (const chapter of textbook.chapters) { + if (!chapter.content?.trim()) throw new Error(`chapter ${chapter.number} content missing`); + if (!/^[a-f0-9]{64}$/.test(chapter.contentSha256 || '')) throw new Error(`chapter ${chapter.number} digest missing`); + if (!/^[a-f0-9]{40}$/.test(chapter.commit || '') || !/^[a-f0-9]{40}$/.test(chapter.blob || '')) throw new Error(`chapter ${chapter.number} source identity missing`); + if (!chapter.sourceUrl?.startsWith(`https://github.com/${chapter.repository}/blob/${chapter.commit}/`)) throw new Error(`chapter ${chapter.number} exact source URL missing`); } } @@ -69,4 +92,4 @@ if (repos.publicRepoCount !== repos.generatedRouteCount) throw new Error('repo r if (new Set(repos.repositories.map(repo => repo.slug)).size !== repos.repositories.length) throw new Error('duplicate project slug'); await access('fallback/index.html'); await access('artifacts/four-cuts-1.html'); -console.log(`validated ${canon.units.length} canon units, ${canon.notes.length} notes, canonical heading hierarchy, and ${repos.publicRepoCount} repositories`); +console.log(`validated ${canon.units.length} canon units, ${canon.notes.length} notes, ${textbook.chapterCount} textbook chapters, canonical hierarchy, and ${repos.publicRepoCount} repositories`); diff --git a/scripts/write-build-info.mjs b/scripts/write-build-info.mjs index 785b037..5586eec 100644 --- a/scripts/write-build-info.mjs +++ b/scripts/write-build-info.mjs @@ -5,16 +5,16 @@ import { readFile, writeFile } from 'node:fs/promises'; // id: public_build_identity // module_name: write-build-info // module_kind: worker -// summary: Publishes machine-readable site and canon identities for post-deployment verification. +// summary: Publishes machine-readable site, canon, and distributed-textbook identities for post-deployment verification. // owner: Erin Spencer // public_surface: _site/build.json -// internal_surface: git commit resolution and canonical provenance projection +// internal_surface: git commit resolution, canonical provenance projection, chapter-source identity projection // auth_boundary: none // storage_boundary: write // network_boundary: none // user_data_boundary: none // admin_only: false -// tests: tests/generated-site.test.mjs +// tests: tests/generated-site.test.mjs, tests/textbook-integrity.test.mjs // rollout: runs at the end of npm run build // rollback: remove build.json and both live identity checks together // === END MODULE_BUILD === @@ -23,7 +23,7 @@ import { readFile, writeFile } from 'node:fs/promises'; // === BOUNDARIES === // id: public_build_identity_boundary -// summary: Reads canon provenance and writes public build identity into the generated site. +// summary: Reads canon and textbook provenance and writes public build identity into the generated site. // auth_boundary: none // storage_boundary: write // network_boundary: none @@ -44,6 +44,7 @@ function localCommit() { } const canon = JSON.parse(await readFile('src/_data/generated/canon.json', 'utf8')); +const textbook = JSON.parse(await readFile('src/_data/generated/textbook.json', 'utf8')); const commit = process.env.GITHUB_SHA || localCommit(); const info = { repository: process.env.GITHUB_REPOSITORY || 'The-Interdependency/The-Interdependency.github.io', @@ -56,8 +57,26 @@ const info = { blob: canon.source.blob, contentSha256: canon.source.contentSha256, fallback: Boolean(canon.source.fallback) + }, + distributedTextbook: { + schema: textbook.schema, + chapterCount: textbook.chapterCount, + complete: Boolean(textbook.complete), + fallback: Boolean(textbook.fallback), + chapters: textbook.chapters.map(chapter => ({ + number: chapter.number, + slug: chapter.slug, + title: chapter.title, + repository: chapter.repository, + path: chapter.path, + commit: chapter.commit, + blob: chapter.blob, + contentSha256: chapter.contentSha256, + status: chapter.status, + fallback: Boolean(chapter.fallback) + })) } }; await writeFile('_site/build.json', `${JSON.stringify(info, null, 2)}\n`); -console.log(`build identity ${commit}`); +console.log(`build identity ${commit} textbook-chapters=${textbook.chapterCount}`); diff --git a/src/_data/textbook_sources.json b/src/_data/textbook_sources.json new file mode 100644 index 0000000..d48e29d --- /dev/null +++ b/src/_data/textbook_sources.json @@ -0,0 +1,90 @@ +[ + { + "number": 0, + "display_number": "Zero", + "slug": "chapter-zero", + "title": "Meta Energy Theory — Axioms, Postulates, and Theorems", + "repository": "The-Interdependency/metapat", + "path": "CHAPTER_ZERO.md", + "branch": "main", + "status": "root ontology and theory", + "summary": "The invariant vocabulary of distinction, boundary, simplex, tensor, energy-state, registration, and question." + }, + { + "number": 1, + "display_number": "One", + "slug": "chapter-one", + "title": "The Subtractive Foundations of the Unit Carrier", + "repository": "The-Interdependency/ucns", + "path": "docs/chapter-1.md", + "branch": "main", + "status": "carrier foundations", + "summary": "The directed 720° carrier, Structural Null, separated zeros, valuation triad, pairing, typed dispatch, and retained memory." + }, + { + "number": 2, + "display_number": "Two", + "slug": "chapter-two", + "title": "Measurement Without Transfer", + "repository": "The-Interdependency/edcm", + "path": "docs/chapter-2.md", + "branch": "main", + "status": "measurement discipline", + "summary": "Typed absence, frozen canon, evidence compartments, proof-transfer firewalls, identity epochs, and an honest measurement frontier." + }, + { + "number": 3, + "display_number": "Three", + "slug": "chapter-three", + "title": "Modules That Speak for Themselves", + "repository": "The-Interdependency/skill-lib", + "path": "docs/chapter-3.md", + "branch": "main", + "status": "self-declaration method", + "summary": "Module Self-Declared Metadata in Markdown: declarations beside implementation, machine reconciliation, and drift-resistant evidence." + }, + { + "number": 4, + "display_number": "Four", + "slug": "chapter-four", + "title": "Canon Without Inversion", + "repository": "The-Interdependency/interdependent-lib", + "path": "docs/chapter-4.md", + "branch": "main", + "status": "cross-repository canon placement", + "summary": "How a meta-package carries family-level canon without inverting runtime dependencies or duplicating authority." + }, + { + "number": 5, + "display_number": "Five", + "slug": "chapter-five", + "title": "One Architecture, Four Layers", + "repository": "The-Interdependency/ptcna", + "path": "docs/chapter-5.md", + "branch": "main", + "status": "architecture", + "summary": "The Prime Tensor Circled Neural Architecture as one package with neural, circle, seed, and core layers." + }, + { + "number": 6, + "display_number": "Six", + "slug": "chapter-six", + "title": "The Instrument", + "repository": "The-Interdependency/a0", + "path": "docs/chapter-6.md", + "branch": "main", + "status": "research instrument", + "summary": "a0 as the codebase, a0p as the public instrument, and persistent agent structure distinct from interchangeable model providers." + }, + { + "number": 7, + "display_number": "Seven", + "slug": "chapter-seven", + "title": "The Echo", + "repository": "The-Interdependency/zfae", + "path": "docs/chapter-7.md", + "branch": "main", + "status": "theory under development", + "summary": "Zeta Function Alpha Echo as an emergent event claim, explicitly bounded as theory with a falsifiable prediction." + } +] diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index 3b4125b..7a306e0 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -24,6 +24,7 @@ Start Preamble The Way + Textbook Article Lab Projects Artifacts @@ -36,12 +37,13 @@ diff --git a/src/assets/css/site.css b/src/assets/css/site.css index 58b52ac..d9995be 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -84,6 +84,28 @@ h3 { font-size: clamp(1.2rem, 2vw, 1.55rem); } .canon-outline .unit-level-4 { margin-left: clamp(2rem, 6vw, 5rem); } .canon-outline .unit-level-3 a { border-left: .25rem solid var(--violet); } .canon-outline .unit-level-4 a { border-left: .25rem solid var(--cyan); background: rgba(9, 13, 24, .76); } +.chapter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; padding: 0; margin: 1.5rem 0 3rem; list-style: none; } +.chapter-grid li { min-width: 0; } +.chapter-card { height: 100%; } +.chapter-number { display: block; margin-bottom: .4rem; color: rgba(174, 187, 210, .42); font: 800 clamp(2.8rem, 7vw, 5rem)/1 Georgia, "Times New Roman", serif; letter-spacing: -.04em; } +.chapter-card small { display: block; margin-top: 1rem; color: var(--silver); overflow-wrap: anywhere; } +.chapter-head { margin-bottom: 1.5rem; } +.chapter-provenance { margin: 0 0 2rem; } +.textbook-chapter { max-width: 55rem; margin: 3rem auto; padding: clamp(1.1rem, 4vw, 3.2rem); border: 1px solid var(--line); border-radius: var(--radius); background: linear-gradient(180deg, rgba(16,24,42,.96), rgba(7,10,18,.96)); box-shadow: var(--shadow); } +.textbook-chapter > :first-child { margin-top: 0; } +.textbook-chapter h1 { font-size: clamp(2.2rem, 5vw, 4.6rem); } +.textbook-chapter h1 + h1 { margin-top: .6rem; color: var(--cyan); font-size: clamp(1.55rem, 3.5vw, 2.9rem); letter-spacing: -.025em; } +.textbook-chapter h2 { margin-top: 3.5rem; padding-top: 1rem; border-top: 1px solid var(--line); } +.textbook-chapter h3 { margin-top: 2.2rem; } +.textbook-chapter p, .textbook-chapter li { max-width: 76ch; } +.textbook-chapter blockquote { margin: 1.5rem 0; padding: .2rem 1.2rem; border-left: .3rem solid var(--violet); color: var(--silver); } +.textbook-chapter pre { max-width: 100%; padding: 1rem; overflow: auto; border: 1px solid var(--line); border-radius: .7rem; background: #05070c; } +.textbook-chapter code { overflow-wrap: anywhere; } +.textbook-chapter table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; } +.textbook-chapter th, .textbook-chapter td { padding: .65rem .8rem; border: 1px solid var(--line); text-align: left; vertical-align: top; } +.textbook-chapter th { background: var(--night-soft); } +.textbook-chapter hr { margin: 3rem 0; border: 0; border-top: 1px solid var(--line); } +.chapter-pagination { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; margin: 3rem 0; } .source-block { white-space: pre-wrap; overflow-wrap: anywhere; padding: 1rem; border: 1px solid var(--line); border-radius: .8rem; background: #070a12; color: #dce6fa; font: .92rem/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; } dl.meta { display: grid; grid-template-columns: minmax(9rem, .35fr) 1fr; gap: .45rem 1rem; } dl.meta dt { color: var(--silver); } @@ -110,8 +132,10 @@ summary { cursor: pointer; font-weight: 800; } .index-list a { grid-template-columns: 1fr; gap: .15rem; } .canon-outline .unit-level-3 { margin-left: .65rem; } .canon-outline .unit-level-4 { margin-left: 1.3rem; } + .textbook-chapter { padding: 1rem; } + .chapter-pagination .button { flex: 1 1 100%; } .site-footer { grid-template-columns: 1fr; } .hmmm-boundary { grid-column: auto; grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } } -@media print { body { background: white; color: black; } body::before, .site-header, .actions, .noscript { display: none; } .site-main { max-width: none; padding: 0; } .card, .panel, .provenance, .hmmm, details { background: white; color: black; box-shadow: none; } a { color: black; } } +@media print { body { background: white; color: black; } body::before, .site-header, .actions, .noscript { display: none; } .site-main { max-width: none; padding: 0; } .card, .panel, .provenance, .hmmm, details, .textbook-chapter { background: white; color: black; box-shadow: none; } a { color: black; } } diff --git a/src/chapters/chapter.njk b/src/chapters/chapter.njk new file mode 100644 index 0000000..088fc64 --- /dev/null +++ b/src/chapters/chapter.njk @@ -0,0 +1,45 @@ +--- +layout: layouts/base.njk +pagination: + data: generated.textbook.chapters + size: 1 + alias: chapter +permalink: "/chapters/{{ chapter.slug }}/" +eleventyComputed: + title: "Chapter {{ chapter.display_number }} · {{ chapter.title }}" +--- + +
+

Distributed textbook · Chapter {{ chapter.display_number }}

+
+ exact repository source + {{ chapter.status }} + {% if chapter.fallback %}fallback{% endif %} +
+

This chapter is displayed from {{ chapter.repository }}:{{ chapter.path }}. Its repository retains its license, status vocabulary, and claim authority; this reader does not transfer those statuses to any other chapter or to The Interdependent Way canon.

+
+
+

Source evidence

+
+
Repository
{{ chapter.repository }}
+
Path
{{ chapter.path }}
+
Commit
{% if chapter.commit %}{{ chapter.commit }}{% else %}hmmm{% endif %}
+
Blob
{% if chapter.blob %}{{ chapter.blob }}{% else %}hmmm{% endif %}
+
Digest
{% if chapter.contentSha256 %}{{ chapter.contentSha256 }}{% else %}hmmm{% endif %}
+
Retrieved
{{ chapter.retrievedAt or "hmmm" }}
+
+
Open exact source in GitHub
+
+{% if chapter.content %} +
+ {{ chapter.content | markdown | safe }} +
+{% else %} +

Chapter {{ chapter.display_number }} source unavailable

The offline build has no retained content snapshot for this chapter. Its declared source remains available through the repository link above; no substitute text has been invented.

{{ chapter.retrievalError }}

+{% endif %} + +

hmmm

Questions, corrections, and status changes belong to {{ chapter.repository }}. This page will follow the source on the next verified build.

diff --git a/src/chapters/index.njk b/src/chapters/index.njk new file mode 100644 index 0000000..eaa8d92 --- /dev/null +++ b/src/chapters/index.njk @@ -0,0 +1,43 @@ +--- +layout: layouts/base.njk +title: The Interdependency Textbook +description: Chapters zero through seven of the distributed Interdependency textbook, displayed together with exact repository provenance and status boundaries. +permalink: /chapters/ +--- +
+

Zero through Seven · eight repositories · one reading sequence

+

The Interdependency Textbook

+

The textbook is distributed by design. Each chapter lives with the repository whose subject it teaches, while this surface restores the complete reading order. The source repository retains its own license, status vocabulary, and authority; citation does not transfer theorem, proof, empirical, or canonical status between chapters.

+
+
+

Reading boundary

+

This textbook is distinct from the canonical text of The Interdependent Way, which remains in wayseer00/main:canon/INTERDEPENDENT_WAY.txt. These chapters explain the wider Interdependency technical architecture and preserve their repository-local claims.

+
+
+

Complete sequence

+

Chapters Zero through Seven

+
    + {% for chapter in generated.textbook.chapters %} +
  1. + + {{ chapter.display_number }} + {{ chapter.status }} +

    {{ chapter.title }}

    +

    {{ chapter.summary }}

    + {{ chapter.repository }} · {{ chapter.path }} +
    +
  2. + {% endfor %} +
+
+
+

Source state

+
+
Schema
{{ generated.textbook.schema }}
+
Generated
{{ generated.textbook.generatedAt }}
+
Chapter count
{{ generated.textbook.chapterCount }}
+
Complete
{{ generated.textbook.complete }}
+
Fallback used
{{ generated.textbook.fallback }}
+
+
+

hmmm

The chapters now share a public reading surface, not a merged authority. Corrections still belong first in the repository that owns the chapter; this site should then refresh and display the new exact source identity.

diff --git a/src/home/index.njk b/src/home/index.njk index 45d77d3..7d528be 100644 --- a/src/home/index.njk +++ b/src/home/index.njk @@ -1,18 +1,24 @@ --- layout: layouts/base.njk title: The Interdependent Way -description: The living public knowledge system for the canon, its deliberate tensions, research, applications, and implementation projects. +description: The living public knowledge system for the canon, distributed textbook, deliberate tensions, research, applications, and implementation projects. permalink: /home/ ---
-

Canon · interpretation · research · implementation · frontier

+

Canon · textbook · interpretation · research · implementation · frontier

A way through complexity without pretending the tension is gone.

-

The Interdependent Way is a dense living text and a constellation of attempts to test, explain, implement, challenge, and preserve it. The Preamble is directly available here; deeper orientation and exact source remain available without hiding the entrance.

- +

The Interdependent Way is a dense living text and a constellation of attempts to test, explain, implement, challenge, and preserve it. The Preamble is directly available here; the distributed Interdependency textbook now restores chapters Zero through Seven into one reading sequence.

+
12 + 1
+
+

Distributed Interdependency textbook

+

Chapters Zero through Seven

+

Eight chapters now form one continuous technical reading path across metapat, ucns, edcm, skill-lib, interdependent-lib, ptcna, a0, and zfae. Each chapter remains bound to the license, status vocabulary, and authority of the repository that carries it.

+
Open all eight chaptersBegin at Chapter Zero
+

Eight rights-article laboratories

From canon excerpt to public practice

@@ -26,6 +32,7 @@ permalink: /home/ Canon-derived

Return to Awakening

Re-enter the opening threshold and its reminder that you are not alone.

Canon-derived

Read the Preamble

Enter the opening civic claim directly through a stable route generated from the selected canon.

Canon-derived

Read the Way

Move through the preamble, articles, etiquette, addenda, and their internal structure without beginning at the raw wall of text.

+ Distributed textbook

Read Chapters Zero–Seven

Follow the technical architecture from root ontology through carrier, measurement, self-declaration, canon placement, architecture, instrument, and echo.

Interpretation

Enter the Article Lab

Test each Rights article through its notes, absurd limits, practice contrasts, domain applications, research support, dissent, and limits.

Publication

Read public articles

Follow selected canon units through companion readings, applications, handbook seeds, and script drafts without losing source boundaries.

Implementation

Map the Projects

Every public organization repository receives a generated space, grouped by function and refreshed at build time.

@@ -34,7 +41,7 @@ permalink: /home/

Current field snapshot

-

{{ 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 }}.

+

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

+

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

-

hmmm

Reviewed research now makes initial contact with all eight Rights articles, but coverage remains uneven and deliberately open to stronger dissent, replication, field studies, disability-led critique, and non-Western evidence.

+

hmmm

The textbook now has a complete public reading spine, while each chapter remains independently revisable in its source repository. The Article Lab research remains deliberately open to stronger dissent, replication, field studies, disability-led critique, and non-Western evidence.

diff --git a/tests/accessibility.spec.mjs b/tests/accessibility.spec.mjs index e648fdd..2704b8a 100644 --- a/tests/accessibility.spec.mjs +++ b/tests/accessibility.spec.mjs @@ -1,5 +1,5 @@ // 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. +// Evidence boundary: automated axe checks do not replace manual keyboard, screen-reader, mathematical-notation, or cognitive-access review. import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { test, expect } from '@playwright/test'; @@ -11,7 +11,10 @@ const articleLab = JSON.parse(readFileSync('src/_data/article_lab.json', 'utf8') const firstLabUnit = canon.units.find(unit => unit.id === articleLab[0].unit_id); if (!firstLabUnit) throw new Error(`missing Lab canon unit ${articleLab[0].unit_id}`); -for (const route of ['/', '/home/', '/preamble/', '/articles/', '/articles/article-two/', '/way/', '/lab/', `/lab/${firstLabUnit.routeSlug}/`, '/projects/']) { +for (const route of [ + '/', '/home/', '/preamble/', '/chapters/', '/chapters/chapter-zero/', '/chapters/chapter-seven/', + '/articles/', '/articles/article-two/', '/way/', '/lab/', `/lab/${firstLabUnit.routeSlug}/`, '/projects/' +]) { test(`${route} has no serious or critical automated accessibility violations`, async ({ page }) => { await page.goto(route); await page.addScriptTag({ path: axePath }); diff --git a/tests/generated-site.test.mjs b/tests/generated-site.test.mjs index 7aef9f0..f437d6c 100644 --- a/tests/generated-site.test.mjs +++ b/tests/generated-site.test.mjs @@ -6,10 +6,11 @@ import { readFile } from 'node:fs/promises'; // Usage: run only after Eleventy has generated _site, normally through npm run test:generated or npm run check. test('generated deployment artifact contains the unified routes', async () => { - const [splash, home, preamble, artifacts, fourCuts, fallback, articles] = await Promise.all([ + const [splash, home, preamble, chapters, artifacts, fourCuts, fallback, articles] = await Promise.all([ readFile('_site/index.html', 'utf8'), readFile('_site/home/index.html', 'utf8'), readFile('_site/preamble/index.html', 'utf8'), + readFile('_site/chapters/index.html', 'utf8'), readFile('_site/artifacts/index.html', 'utf8'), readFile('_site/artifacts/four-cuts/index.html', 'utf8'), readFile('_site/fallback/index.html', 'utf8'), @@ -25,11 +26,15 @@ test('generated deployment artifact contains the unified routes', async () => { assert.doesNotMatch(splash, /primary-nav/); assert.match(home, /A way through complexity/); assert.match(home, /href="\/preamble\/"[^>]*>Read the Preamble/); + assert.match(home, /href="\/chapters\/"/); + assert.match(home, /Chapters Zero through Seven/); assert.match(home, /href="\/"/); assert.match(home, /Return to Awakening/); assert.match(preamble, /One-click canon entrance/); assert.match(preamble, /Humanity faces extinction/); assert.match(preamble, /Canonical repository/); + assert.match(chapters, /The Interdependency Textbook/); + assert.match(chapters, /Chapters Zero through Seven/); assert.match(artifacts, /Artifacts/); assert.match(fourCuts, /Wealth and tax/); assert.match(fallback, /Emergency static edition/); @@ -46,6 +51,36 @@ test('generated deployment artifact contains the unified routes', async () => { ]) assert.match(articles, new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); }); +test('distributed textbook displays all eight exact chapter sources', async () => { + const textbook = JSON.parse(await readFile('src/_data/generated/textbook.json', 'utf8')); + const index = await readFile('_site/chapters/index.html', 'utf8'); + const contact = [ + /Zero is not nothing/, + /directed twofold branched angular cover/, + /NA != 0/, + /Modules That Speak for Themselves/, + /meta-package/, + /Prime Tensor Circled Neural Architecture/, + /research[\s\S]*instrument, not a product/, + /theory under development/ + ]; + + assert.equal(textbook.chapters.length, 8); + for (const chapter of textbook.chapters) { + assert.match(index, new RegExp(`href="/chapters/${chapter.slug}/"`)); + assert.match(index, new RegExp(chapter.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + const html = await readFile(`_site/chapters/${chapter.slug}/index.html`, 'utf8'); + assert.match(html, /class="textbook-chapter"/); + assert.match(html, new RegExp(`Chapter ${chapter.display_number} · ${chapter.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}<\\/title>`)); + assert.match(html, new RegExp(chapter.repository.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.match(html, new RegExp(chapter.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.match(html, /Open exact source in GitHub/); + assert.match(html, /Source evidence/); + assert.match(html, contact[chapter.number]); + } + assert.match(await readFile('_site/chapters/chapter-seven/index.html', 'utf8'), /theory under development/); +}); + test('Way map renders Human consciousness beneath Interdefinables and before Preamble', async () => { const way = await readFile('_site/way/index.html', 'utf8'); const interdefinablesSection = way.indexOf('<h2>The Interdefinables</h2>'); diff --git a/tests/site-contract.test.mjs b/tests/site-contract.test.mjs index 1850a89..c48f078 100644 --- a/tests/site-contract.test.mjs +++ b/tests/site-contract.test.mjs @@ -2,16 +2,19 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; -test('one static-first build owns public routes', async () => { +test('one static-first build owns public routes and exact Markdown rendering', async () => { const config = await readFile('.eleventy.js', 'utf8'); assert.match(config, /artifacts\/four-cuts-1\.html.*artifacts\/four-cuts\/index\.html/s); assert.match(config, /fallback/); + assert.match(config, /addFilter\('markdown'/); + assert.match(config, /html: false/); }); -test('base layout remains readable without javascript', async () => { +test('base layout remains readable without javascript and exposes the textbook', async () => { const layout = await readFile('src/_includes/layouts/base.njk', 'utf8'); assert.match(layout, /<noscript>/); assert.match(layout, /<main id="content"/); + assert.match(layout, /href="\/chapters\/"/); assert.doesNotMatch(layout, /fetch\(/); }); @@ -30,6 +33,27 @@ test('Awakening owns the static public threshold and routes inward', async () => assert.match(splash, /href="\/home\/"/); assert.match(home, /permalink: \/home\//); assert.match(home, /Return to Awakening/); + assert.match(home, /href="\/chapters\/"/); +}); + +test('distributed textbook routes are data-backed and source-bound', async () => { + const [manifest, index, chapter, fetcher] = await Promise.all([ + readFile('src/_data/textbook_sources.json', 'utf8'), + readFile('src/chapters/index.njk', 'utf8'), + readFile('src/chapters/chapter.njk', 'utf8'), + readFile('scripts/fetch-textbook.mjs', 'utf8') + ]); + const sources = JSON.parse(manifest); + assert.equal(sources.length, 8); + assert.deepEqual(sources.map(source => source.number), [0, 1, 2, 3, 4, 5, 6, 7]); + assert.match(index, /generated\.textbook\.chapters/); + assert.match(chapter, /pagination:[\s\S]*generated\.textbook\.chapters/); + assert.match(chapter, /chapter\.content \| markdown \| safe/); + assert.match(chapter, /Open exact source in GitHub/); + assert.ok(fetcher.includes("const apiOrigin = 'https://api.github.com';")); + assert.ok(fetcher.includes("const rawOrigin = 'https://raw.githubusercontent.com';")); + assert.match(fetcher, /refusing non-allowlisted textbook target/); + assert.match(fetcher, /contentSha256/); }); test('emergency static edition has no script or external dependency', async () => { diff --git a/tests/site.spec.mjs b/tests/site.spec.mjs index 0c5e3c7..cd7c703 100644 --- a/tests/site.spec.mjs +++ b/tests/site.spec.mjs @@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs'; import { test, expect } from '@playwright/test'; const canon = JSON.parse(readFileSync('src/_data/generated/canon.json', 'utf8')); +const textbook = JSON.parse(readFileSync('src/_data/generated/textbook.json', 'utf8')); const articleLab = JSON.parse(readFileSync('src/_data/article_lab.json', 'utf8')); const labRoutes = articleLab.map(record => { const unit = canon.units.find(candidate => candidate.id === record.unit_id); @@ -15,6 +16,9 @@ const routes = [ ['/', /You are not alone/], ['/home/', /A way through complexity/], ['/preamble/', /Humanity faces extinction/], + ['/chapters/', /The Interdependency Textbook/], + ['/chapters/chapter-zero/', /Zero is not nothing/], + ['/chapters/chapter-seven/', /theory under development/], ['/articles/', /Publication drafts/], ['/articles/article-two/', /Freedom without abandonment/], ['/way/', /The Way/], @@ -51,11 +55,30 @@ test('Awakening is the public splash and preserves one-click continuation', asyn await expect(page.locator('.source-block')).toContainText('Humanity faces extinction'); }); -test('the knowledge-system home links back through Awakening and keeps Preamble in primary navigation', async ({ page }) => { +test('the knowledge-system home links to Awakening, Preamble, and the distributed textbook', async ({ page }) => { await page.goto('/home/'); await expect(page.locator('a.brand')).toHaveAttribute('href', '/home/'); await expect(page.locator('nav[aria-label="Primary navigation"] a[href="/"]', { hasText: 'Awakening' })).toBeVisible(); await expect(page.locator('nav[aria-label="Primary navigation"] a[href="/preamble/"]', { hasText: 'Preamble' })).toBeVisible(); + await expect(page.locator('nav[aria-label="Primary navigation"] a[href="/chapters/"]', { hasText: 'Textbook' })).toBeVisible(); + await expect(page.locator('main a[href="/chapters/"]').first()).toBeVisible(); +}); + +test('chapters zero through seven are indexed, source-bound, and sequentially navigable', async ({ page }) => { + await page.goto('/chapters/'); + for (const chapter of textbook.chapters) { + await expect(page.locator(`a[href="/chapters/${chapter.slug}/"]`)).toBeVisible(); + } + + await page.goto('/chapters/chapter-zero/'); + await expect(page.locator('.textbook-chapter')).toContainText('Zero is not nothing'); + await expect(page.locator('.chapter-provenance')).toContainText('The-Interdependency/metapat'); + await expect(page.locator('.chapter-pagination a', { hasText: 'Next chapter' })).toBeVisible(); + + await page.goto('/chapters/chapter-seven/'); + await expect(page.locator('.textbook-chapter')).toContainText('theory under development'); + await expect(page.locator('.chapter-provenance')).toContainText('The-Interdependency/zfae'); + await expect(page.locator('.status-frontier')).toContainText('theory under development'); }); test('all eight rights articles are reachable from the article index', async ({ page }) => { diff --git a/tests/textbook-generated.test.mjs b/tests/textbook-generated.test.mjs new file mode 100644 index 0000000..477d82d --- /dev/null +++ b/tests/textbook-generated.test.mjs @@ -0,0 +1,28 @@ +// Usage: run after `npm run build`; verifies the public artifact binds every displayed chapter to its exact source identity. +// Evidence boundary: confirms build provenance fields, not the substantive truth or cross-repository status of chapter claims. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +test('public build identity binds chapters zero through seven to source commits', async () => { + const build = JSON.parse(await readFile('_site/build.json', 'utf8')); + const textbook = JSON.parse(await readFile('src/_data/generated/textbook.json', 'utf8')); + assert.equal(build.distributedTextbook.schema, 'interdependency.distributed-textbook/1.0.0'); + assert.equal(build.distributedTextbook.chapterCount, 8); + assert.equal(build.distributedTextbook.complete, true); + assert.equal(build.distributedTextbook.fallback, false); + assert.equal(build.distributedTextbook.chapters.length, 8); + + for (let number = 0; number < 8; number += 1) { + const publicChapter = build.distributedTextbook.chapters[number]; + const generatedChapter = textbook.chapters[number]; + assert.deepEqual( + [publicChapter.number, publicChapter.slug, publicChapter.repository, publicChapter.path], + [generatedChapter.number, generatedChapter.slug, generatedChapter.repository, generatedChapter.path] + ); + assert.match(publicChapter.commit, /^[a-f0-9]{40}$/); + assert.match(publicChapter.blob, /^[a-f0-9]{40}$/); + assert.match(publicChapter.contentSha256, /^[a-f0-9]{64}$/); + assert.equal(publicChapter.fallback, false); + } +}); diff --git a/tests/textbook-integrity.test.mjs b/tests/textbook-integrity.test.mjs new file mode 100644 index 0000000..7b8f5ca --- /dev/null +++ b/tests/textbook-integrity.test.mjs @@ -0,0 +1,61 @@ +// Usage: run through `npm test`; OFFLINE=1 may produce metadata-only hmmm records when no retained snapshot exists. +// Evidence boundary: verifies source identity, order, hashes, and status labels; it does not validate each chapter's substantive claims. +import { createHash } from 'node:crypto'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const expected = [ + [0, 'chapter-zero', 'Meta Energy Theory — Axioms, Postulates, and Theorems', 'The-Interdependency/metapat', 'CHAPTER_ZERO.md'], + [1, 'chapter-one', 'The Subtractive Foundations of the Unit Carrier', 'The-Interdependency/ucns', 'docs/chapter-1.md'], + [2, 'chapter-two', 'Measurement Without Transfer', 'The-Interdependency/edcm', 'docs/chapter-2.md'], + [3, 'chapter-three', 'Modules That Speak for Themselves', 'The-Interdependency/skill-lib', 'docs/chapter-3.md'], + [4, 'chapter-four', 'Canon Without Inversion', 'The-Interdependency/interdependent-lib', 'docs/chapter-4.md'], + [5, 'chapter-five', 'One Architecture, Four Layers', 'The-Interdependency/ptcna', 'docs/chapter-5.md'], + [6, 'chapter-six', 'The Instrument', 'The-Interdependency/a0', 'docs/chapter-6.md'], + [7, 'chapter-seven', 'The Echo', 'The-Interdependency/zfae', 'docs/chapter-7.md'] +]; + +test('distributed textbook manifest and generated data preserve chapters zero through seven', async () => { + const manifest = JSON.parse(await readFile('src/_data/textbook_sources.json', 'utf8')); + const textbook = JSON.parse(await readFile('src/_data/generated/textbook.json', 'utf8')); + + assert.equal(textbook.schema, 'interdependency.distributed-textbook/1.0.0'); + assert.equal(textbook.chapterCount, 8); + assert.equal(manifest.length, 8); + assert.equal(textbook.chapters.length, 8); + assert.equal(new Set(manifest.map(chapter => chapter.slug)).size, 8); + assert.equal(new Set(manifest.map(chapter => `${chapter.repository}:${chapter.path}`)).size, 8); + + for (const [number, slug, title, repository, path] of expected) { + const source = manifest[number]; + const chapter = textbook.chapters[number]; + assert.deepEqual( + [source.number, source.slug, source.title, source.repository, source.path], + [number, slug, title, repository, path] + ); + assert.deepEqual( + [chapter.number, chapter.slug, chapter.title, chapter.repository, chapter.path], + [number, slug, title, repository, path] + ); + assert.equal(chapter.branch, 'main'); + assert.ok(chapter.summary); + assert.ok(chapter.status); + assert.ok(chapter.sourceUrl?.startsWith(`https://github.com/${repository}/blob/`)); + + if (chapter.content) { + assert.ok(chapter.content.includes(title)); + assert.equal(createHash('sha256').update(chapter.content).digest('hex'), chapter.contentSha256); + assert.match(chapter.contentSha256, /^[a-f0-9]{64}$/); + if (!chapter.fallback) { + assert.match(chapter.commit, /^[a-f0-9]{40}$/); + assert.match(chapter.blob, /^[a-f0-9]{40}$/); + } + } else { + assert.equal(chapter.fallback, true); + assert.ok(chapter.retrievalError); + } + } + + assert.equal(textbook.chapters[7].status, 'theory under development'); +});