diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 9b4849d..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Deploy GitHub Pages - -on: - push: - branches: - - main # Set a branch to deploy from - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout your repository - uses: actions/checkout@v4 - - - name: Setup Node.js (optional, remove if not needed) - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: './' # Adjust this path if your build output is in a subfolder, e.g., './_site' - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index aa0cf90..b19d104 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -28,9 +28,9 @@ jobs: 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/wayseer.github.io'||c.source.path!=='canon/the_interdependent_way.md') process.exit(1)" + 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.units.length||c.units.some(u=>!u.id||!u.hash)) process.exit(1)" + 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 diff --git a/README.md b/README.md index cf3b4c8..5aaffa9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository builds `interdependentway.org`: a static-first, progressively la ## What is authoritative -The canonical identity remains `wayseer00/wayseer.github.io:canon/the_interdependent_way.md`. The repository copy at `canon/the_interdependent_way.md` is a recovery mirror. Build output records whether the remote source or recovery mirror supplied the current snapshot, together with SHA-256 provenance. +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. ## Architecture diff --git a/docs/architecture.md b/docs/architecture.md index b94495f..6c13150 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,8 +4,8 @@ The production site is generated into `_site` by Eleventy. Pages are complete HT ## Data paths -- `scripts/fetch-canon.mjs` retrieves the Wayseer canonical alias `wayseer00/wayseer.github.io:canon/the_interdependent_way.md`. The transferred repository copy at `canon/the_interdependent_way.md` is recovery-only and is recorded as a fallback when used. -- `scripts/parse-canon.mjs` generates units, line ranges, note text, relationships, and SHA-256 digests. +- `scripts/fetch-canon.mjs` retrieves the Wayseer text canon from `wayseer00/main:canon/INTERDEPENDENT_WAY.txt`. The transferred repository copy at `canon/the_interdependent_way.md` is recovery-only and is recorded as a fallback when used. +- `scripts/parse-canon.mjs` generates units, line ranges, note text, relationships, and SHA-256 digests from either Markdown-style recovery mirrors or the plain-text canonical file. - `scripts/fetch-github-org.mjs` discovers every public organization repository and merges GitHub facts with `.interdependency/project.yml` or reviewed central overrides. - Pagefind indexes the generated site after Eleventy finishes. diff --git a/scripts/fetch-canon.mjs b/scripts/fetch-canon.mjs index 174dd53..eddf3ab 100644 --- a/scripts/fetch-canon.mjs +++ b/scripts/fetch-canon.mjs @@ -10,33 +10,70 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; // === END MODULE_BUILD === // === BOUNDARIES === // id: canon_network_boundary -// network: read-only HTTPS request to raw.githubusercontent.com +// network: read-only HTTPS request to allowlisted GitHub API and raw content endpoints // storage: writes generated snapshots beneath src/_data/snapshots // failure: falls back to the repository mirror and records fallback=true // === END BOUNDARIES === const canonical = { - repository: 'wayseer00/wayseer.github.io', - path: 'canon/the_interdependent_way.md', + repository: 'wayseer00/main', + path: 'canon/INTERDEPENDENT_WAY.txt', branch: 'main', - url: 'https://raw.githubusercontent.com/wayseer00/wayseer.github.io/main/canon/the_interdependent_way.md' + webUrl: 'https://github.com/wayseer00/main/blob/main/canon/INTERDEPENDENT_WAY.txt' }; const localMirror = 'canon/the_interdependent_way.md'; +const githubApiOrigin = 'https://api.github.com'; +const rawOrigin = 'https://raw.githubusercontent.com'; +const allowedOrigins = new Set([githubApiOrigin, rawOrigin]); +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 fetchRemote() { - if (process.env.OFFLINE === '1') throw new Error('offline requested'); - return execFileSync('curl', ['-fsSL', '--retry', '2', '--max-time', '30', canonical.url], { +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 canon target: ${url.origin}`); + } + return execFileSync('curl', ['-fsSL', '--retry', '2', '--max-time', '30', ...extraHeaders, url.href], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); } +function githubApiUrl(pathname, search = {}) { + const url = new URL(pathname, githubApiOrigin); + for (const [key, value] of Object.entries(search)) url.searchParams.set(key, String(value)); + return url; +} + +function getJson(target) { + return JSON.parse(curlText(target, githubHeaders)); +} + +function fetchRemote() { + if (process.env.OFFLINE === '1') throw new Error('offline requested'); + const [owner, repo] = canonical.repository.split('/'); + const encodedOwner = encodeURIComponent(owner); + const encodedRepo = encodeURIComponent(repo); + const encodedPath = canonical.path.split('/').map(segment => encodeURIComponent(segment)).join('/'); + const apiBase = `/repos/${encodedOwner}/${encodedRepo}`; + const commitInfo = getJson(githubApiUrl(`${apiBase}/commits/${encodeURIComponent(canonical.branch)}`)); + const commit = commitInfo.sha; + if (!/^[a-f0-9]{40}$/i.test(commit)) throw new Error('canon branch did not resolve to a commit SHA'); + const fileInfo = getJson(githubApiUrl(`${apiBase}/contents/${encodedPath}`, { ref: commit })); + if (fileInfo.type !== 'file' || !fileInfo.sha) throw new Error('canon path did not resolve to a file blob'); + const resolvedUrl = new URL(`/${encodedOwner}/${encodedRepo}/${commit}/${encodedPath}`, rawOrigin); + const text = curlText(resolvedUrl); + return { text, commit, blob: fileInfo.sha, resolvedUrl: resolvedUrl.href }; +} + await mkdir('src/_data/snapshots', { recursive: true }); let text; let fallback = false; let retrievalError = null; +let remote = { commit: null, blob: null, resolvedUrl: null }; try { - text = fetchRemote(); + remote = fetchRemote(); + text = remote.text; } catch (error) { fallback = true; retrievalError = String(error?.message || error); @@ -51,6 +88,9 @@ try { const contentSha256 = createHash('sha256').update(text).digest('hex'); const metadata = { ...canonical, + commit: remote.commit, + blob: remote.blob, + resolvedUrl: remote.resolvedUrl, retrievedAt: new Date().toISOString(), contentSha256, fallback, diff --git a/scripts/parse-canon.mjs b/scripts/parse-canon.mjs index 720325e..4ede66f 100644 --- a/scripts/parse-canon.mjs +++ b/scripts/parse-canon.mjs @@ -9,7 +9,7 @@ import slugify from 'slugify'; // tests: tests/canon-integrity.test.mjs // === END MODULE_BUILD === -const parserVersion = '0.3.0'; +const parserVersion = '0.4.1'; const provenance = JSON.parse(await readFile('src/_data/snapshots/canon.provenance.json', 'utf8')); const raw = await readFile('src/_data/snapshots/canon.last-known-good.md', 'utf8'); const text = raw.replace(/^---\n[\s\S]*?\n---\n/, ''); @@ -19,7 +19,6 @@ const units = []; const sections = []; let current = null; let sectionId = 'source'; -const articleBySection = new Map(); function slug(value) { return slugify(value, { lower: true, strict: true }) || 'unit'; @@ -30,36 +29,69 @@ function boundedRouteSlug(id) { const suffix = createHash('sha256').update(id).digest('hex').slice(0, 10); return `${candidate.slice(0, 84).replace(/-+$/, '')}-${suffix}`; } +function detectHeading(line) { + const markdown = /^(#{1,6})\s+(.+?)\s*$/.exec(line); + if (markdown) return { level: markdown[1].length, title: markdown[2].replace(/#+$/, '').trim() }; + const title = line.trim(); + if (!title) return null; + if (title === 'The Interdependent Way') return { level: 1, title }; + if (/^(Awakening|The Interdefinables|Human consciousness emerges from|Preamble|Etiquette of the Body Politic)$/i.test(title)) { + return { level: 2, title }; + } + if (/^Rights[\w\s’'&\-⁰¹²³⁴⁵⁶⁷⁸⁹]+of The Way[⁰¹²³⁴⁵⁶⁷⁸⁹]*$/i.test(title)) return { level: 2, title }; + if (/^Addendum:\s+.+$/i.test(title)) return { level: 2, title }; + if (/^Article\s+(One|Two|Three|Four|Five|Six|Seven|Eight)(?:\s+\([^)]+\))?$/i.test(title)) return { level: 3, title }; + if (/^(Binary essences meaningfully, divided; then, rooted\.|Trinary perceptual focal states of complex system spirals:.+|Trinary states of social perception:|Archetype passions of possession\..+|Summary|One-sentence takeaway \(exactly as previously given\))$/i.test(title)) { + return { level: 3, title }; + } + return null; +} +function extractNotes(content) { + const notes = []; + for (const line of content.split(/\r?\n/)) { + const bracket = /^\s*\[([^\]]+)\]\s+(.+)$/.exec(line); + if (bracket) { + notes.push({ marker: `[${bracket[1]}]`, text: bracket[2].trim() }); + continue; + } + const superscript = /^\s*>?\s*([⁰¹²³⁴⁵⁶⁷⁸⁹]+)\s*(.+)$/.exec(line); + if (superscript) { + notes.push({ marker: superscript[1], text: superscript[2].trim() }); + continue; + } + const digit = /^\s*>?\s*(\d+)\s+(.+)$/.exec(line); + if (digit) notes.push({ marker: digit[1], text: digit[2].trim() }); + } + return notes; +} +function extractNoteMarkers(content) { + const markers = []; + for (const match of content.matchAll(/\[[^\]]+\]|[⁰¹²³⁴⁵⁶⁷⁸⁹]+/g)) markers.push(match[0]); + return [...new Set(markers)]; +} function finish(endLine) { if (!current) return; current.endLine = endLine; current.content = current.lines.join('\n').trim(); current.hash = createHash('sha256').update(current.content).digest('hex'); - const notePattern = /^\s*\[([^\]]+)\]\s+(.+)$/gm; - current.notes = [...current.content.matchAll(notePattern)].map(match => ({ marker: `[${match[1]}]`, text: match[2].trim() })); - current.noteMarkers = [...new Set([...current.content.matchAll(/\[([^\]]+)\]/g)].map(match => `[${match[1]}]`))]; + current.notes = extractNotes(current.content); + current.noteMarkers = extractNoteMarkers(current.content); units.push(current); } for (let index = 0; index < lines.length; index += 1) { - const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]); + const heading = detectHeading(lines[index]); if (!heading) { if (current) current.lines.push(lines[index]); continue; } finish(index); - const level = heading[1].length; - const title = heading[2].replace(/#+$/, '').trim(); - if (level <= 3) { + const { level, title } = heading; + if (level <= 2) { sectionId = slug(title).replace(/^the-/, ''); if (!sections.some(section => section.id === sectionId)) sections.push({ id: sectionId, title, level, line: index + 1 }); } - let localId = slug(title); - if (/^article\s+/i.test(title)) { - const count = (articleBySection.get(sectionId) || 0) + 1; - articleBySection.set(sectionId, count); - localId = `article-${count}`; - } + const localId = slug(title); current = { id: `${sectionId}.${localId}`, title, diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 8da4827..65c30b6 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -9,9 +9,10 @@ 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')); -if (canon.source.repository !== 'wayseer00/wayseer.github.io') throw new Error(`unexpected canon repository: ${canon.source.repository}`); -if (canon.source.path !== 'canon/the_interdependent_way.md') throw new Error(`unexpected canon path: ${canon.source.path}`); +if (canon.source.repository !== 'wayseer00/main') throw new Error(`unexpected canon repository: ${canon.source.repository}`); +if (canon.source.path !== 'canon/INTERDEPENDENT_WAY.txt') throw new Error(`unexpected canon path: ${canon.source.path}`); if (!canon.source.contentSha256 || canon.source.contentSha256.length !== 64) throw new Error('canon missing SHA-256 digest'); +if (!canon.source.fallback && (!canon.source.commit || !canon.source.blob)) throw new Error('remote canon provenance missing commit or blob SHA'); if (!canon.units.length || canon.units.some(unit => !unit.hash || !unit.id)) throw new Error('canon units missing identity or hash'); if (repos.publicRepoCount !== repos.generatedRouteCount) throw new Error('repo route mismatch'); if (new Set(repos.repositories.map(repo => repo.slug)).size !== repos.repositories.length) throw new Error('duplicate project slug'); diff --git a/src/source/unit.njk b/src/source/unit.njk index a35372d..7f256e4 100644 --- a/src/source/unit.njk +++ b/src/source/unit.njk @@ -9,5 +9,5 @@ title: "Source: {{ unit.title }}" ---

Exact source · deliberate depth

{{ unit.title }}

Verbatim source content and machine-verifiable provenance. Commentary elsewhere on this site is subordinate to this layer.

-
Canonical repository alias
{{ generated.canon.source.repository }}
Path
{{ generated.canon.source.path }}
Branch
{{ generated.canon.source.branch }}
Retrieved
{{ generated.canon.source.retrievedAt }}
Recovery mirror used
{{ generated.canon.source.fallback }}
Canonical unit ID
{{ unit.id }}
Stable route
{{ unit.routeSlug }}
Source lines
{{ unit.startLine }}–{{ unit.endLine }}
Unit digest
{{ unit.hash }}
Document digest
{{ generated.canon.source.contentSha256 }}
+
Canonical repository
{{ generated.canon.source.repository }}
Path
{{ generated.canon.source.path }}
Branch
{{ generated.canon.source.branch }}
Resolved commit
{{ generated.canon.source.commit or 'fallback mirror' }}
Blob
{{ generated.canon.source.blob or 'fallback mirror' }}
Retrieved
{{ generated.canon.source.retrievedAt }}
Recovery mirror used
{{ generated.canon.source.fallback }}
Canonical unit ID
{{ unit.id }}
Stable route
{{ unit.routeSlug }}
Source lines
{{ unit.startLine }}–{{ unit.endLine }}
Unit digest
{{ unit.hash }}
Document digest
{{ generated.canon.source.contentSha256 }}
{{ unit.content }}
diff --git a/tests/canon-integrity.test.mjs b/tests/canon-integrity.test.mjs index 4f1b2cd..3165fa3 100644 --- a/tests/canon-integrity.test.mjs +++ b/tests/canon-integrity.test.mjs @@ -4,9 +4,13 @@ import { readFile } from 'node:fs/promises'; test('canon data preserves Wayseer identity, provenance, and stable unit evidence', async () => { const canon = JSON.parse(await readFile('src/_data/generated/canon.json', 'utf8')); - assert.equal(canon.source.repository, 'wayseer00/wayseer.github.io'); - assert.equal(canon.source.path, 'canon/the_interdependent_way.md'); + assert.equal(canon.source.repository, 'wayseer00/main'); + assert.equal(canon.source.path, 'canon/INTERDEPENDENT_WAY.txt'); assert.match(canon.source.contentSha256, /^[a-f0-9]{64}$/); + if (!canon.source.fallback) { + assert.match(canon.source.commit, /^[a-f0-9]{40}$/); + assert.match(canon.source.blob, /^[a-f0-9]{40}$/); + } assert.ok(canon.units.length > 0); const routes = new Set(); for (const unit of canon.units) {