diff --git a/.github/ISSUE_TEMPLATE/related-work.yml b/.github/ISSUE_TEMPLATE/related-work.yml new file mode 100644 index 0000000..a13382c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/related-work.yml @@ -0,0 +1,90 @@ +name: Submit a related work +description: Propose a work (essay, paper, art, music, video, code, response, …) for the site's Related Works registry. +title: "[work] " +labels: ["related-work"] +body: + - type: markdown + attributes: + value: | + Thank you for engaging with The Interdependent Way. Submissions are reviewed by the + maintainer; approval lists the work on the public Related Works page with your + attribution. Listing is not endorsement, transfers no review or theorem status, and + never modifies canon or the research ledgers. Your GitHub username and everything you + write here are public. + + **The site displays, it does not host.** Your work stays wherever you host it. If you + provide a display source below, visitors' browsers load the work directly from your + hosting to show it inline; the site itself stores only the metadata you enter here. + Removal requests go to wayseer@interdependentway.org and take effect on the next + site build. + - type: input + id: title + attributes: + label: Work title + description: The title of the work being submitted. + validations: + required: true + - type: input + id: creator + attributes: + label: Creator + description: Who made it — the name that should be credited. + validations: + required: true + - type: input + id: link + attributes: + label: Link + description: An https URL where the work lives (page, repository, video, gallery, …). + validations: + required: true + - type: dropdown + id: type + attributes: + label: Work type + options: + - essay + - paper + - art + - music + - video + - code + - response + - other + validations: + required: true + - type: textarea + id: relation + attributes: + label: Relation to The Interdependent Way + description: Which Article, chapter, or concept does this work engage, and how? + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A short description for the listing (a few sentences). + validations: + required: true + - type: input + id: display + attributes: + label: Display source + description: Optional — an https URL for displaying the work inline on the page, loaded straight from your hosting. Either a direct image/audio/PDF file URL, or an embed URL from a supported platform (YouTube, Vimeo, Dailymotion, Twitch, Rumble, Odysee, Streamable, Loom, TED, SoundCloud, Bandcamp, Spotify, Apple Music/Podcasts, Mixcloud, Audiomack, Podbean, archive.org, Google Docs, OneDrive, Issuu, Scribd, SlideShare, CodePen, CodeSandbox, JSFiddle, itch.io, Sketchfab). Missing your platform? Mention it in the description and it can be added. Leave blank to list as a link only. + validations: + required: false + - type: input + id: license + attributes: + label: License note + description: Optional — the work's license or usage terms, if you want them shown. + validations: + required: false + - type: checkboxes + id: rights + attributes: + label: Rights and permission + options: + - label: I have the right to share this link, I consent to the details above being displayed publicly, and I accept responsibility for the accuracy and lawfulness of this submission. I understand the site displays this metadata only and does not host the work. + required: true diff --git a/docs/related-works.md b/docs/related-works.md new file mode 100644 index 0000000..eb275b9 --- /dev/null +++ b/docs/related-works.md @@ -0,0 +1,50 @@ +# Related Works registry + +Public submissions of works engaging The Interdependent Way, moderated by label. + +## Flow + +1. A visitor submits via the structured issue form (`.github/ISSUE_TEMPLATE/related-work.yml`), + which applies the `related-work` label — or emails the same fields to the maintainer, + who files the issue on their behalf. +2. The maintainer reviews. Adding the `approved` label publishes the listing on the next + site build; removing either label delists it. Closing the issue does not delist — + the labels are the single switch. +3. `scripts/fetch-works.mjs` (in `refresh:data`) fetches all issues carrying both labels, + parses the issue-form body, validates each submission (required fields, https link, + allowed type), and writes `src/_data/generated/works.json` plus a last-known-good + snapshot. Invalid-but-approved submissions surface on the page as visible gaps rather + than disappearing. `OFFLINE=1` uses the snapshot. +4. `/works/` renders the registry with attribution, per-work provenance (issue link), + and the boundary statement. + +## Boundary + +Listing is moderation, not review: it transfers no endorsement, no research or review +status, and no theorem/proof/empirical status, and it never modifies canon or the +Article Lab research ledgers. Works remain at their own links under their own licenses. + +## Display, not host (legal boundary) + +The registry **displays works without hosting them**. Hard rules for anyone touching +this feature: + +- The site stores no copies: the fetcher retrieves issue metadata only and never + downloads a submitted work. Inline display uses pointers — the visitor's browser loads + the image/audio/PDF/embed directly from the creator's own hosting at view time. +- Display sources are validated fail-closed: https only; direct image/audio/PDF file + URLs, or iframe embeds from the deliberate allowlist in `scripts/fetch-works.mjs` + (`IFRAME_EMBED_HOSTS` — grouped by medium: video, audio/music/podcasts, + documents/archives/slides, code/interactive/3D). Extending the allowlist is a + deliberate per-host decision — every entry means third-party script runs on the works + page for approved listings — but it is expected to grow: submitters can request a + missing platform in their submission, and adding it is a one-line change plus review. +- A work with no (or an invalid) display source lists as metadata plus an outbound link; + invalid display sources exclude the submission visibly so the submitter can fix it. +- The submitter affirms rights, consent, and responsibility for accuracy and lawfulness + in the required checkbox; the site's role is limited to moderated display. +- Takedown: removal requests go to wayseer@interdependentway.org; the maintainer removes + the `approved` label (or the listing's issue), and the listing disappears on the next + build. No copy remains because none was ever made. + +This is design-level mitigation, not legal advice. diff --git a/package.json b/package.json index 81b49a5..0680a92 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "npm run refresh:data && eleventy --serve", - "refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:textbook", + "refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:textbook && npm run refresh:works", "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", @@ -15,14 +15,15 @@ "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/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/site-contract.test.mjs", + "test": "node --test tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs", "test:generated": "node --test tests/generated-site.test.mjs tests/textbook-generated.test.mjs tests/math-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", "test:links": "node tests/links.test.mjs", "test:performance": "node scripts/performance-placeholder.mjs", - "check": "npm run audit:workflows && npm run build && npm test && npm run test:generated" + "check": "npm run audit:workflows && npm run build && npm test && npm run test:generated", + "refresh:works": "node scripts/fetch-works.mjs" }, "dependencies": { "@11ty/eleventy": "3.1.2", @@ -39,5 +40,7 @@ "axe-core": "4.10.3", "pagefind": "1.3.0" }, - "engines": { "node": "24.x" } + "engines": { + "node": "24.x" + } } diff --git a/scripts/fetch-works.mjs b/scripts/fetch-works.mjs new file mode 100644 index 0000000..95b236a --- /dev/null +++ b/scripts/fetch-works.mjs @@ -0,0 +1,271 @@ +import { execFileSync } from 'node:child_process'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; + +// === MODULE_BUILD === +// id: related_works_registry_fetch +// module_name: fetch-works +// module_kind: instrument +// summary: Resolves maintainer-approved related-work submissions from GitHub issues into one provenance-bearing works registry dataset. +// owner: Erin Spencer +// public_surface: npm run refresh:works, generated.works +// internal_surface: issue-form body parsing, per-submission validation with visible exclusions, snapshot fallback +// auth_boundary: optional read-only GITHUB_TOKEN +// storage_boundary: writes generated works data and a last-known-good snapshot +// network_boundary: allowlisted read-only HTTPS to api.github.com +// user_data_boundary: publishes only fields submitters place in a public GitHub issue +// admin_only: false +// tests: tests/works-registry.test.mjs +// rollout: included in refresh:data before validation and Eleventy generation +// rollback: remove refresh:works and the /works/ route; submissions remain as ordinary GitHub issues +// === END MODULE_BUILD === +// Usage: run `npm run refresh:works`; set OFFLINE=1 to use the retained snapshot or an empty registry. +// Limits: listing is maintainer moderation only — it transfers no endorsement, review status, or +// theorem/proof/empirical status, and it never touches the research ledgers or canon. + +// === BOUNDARIES === +// id: related_works_network_boundary +// summary: Reads only issues labeled related-work and approved from this repository via the allowlisted GitHub API. +// 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: public issue content only; submitter login retained as provenance +// admin_only: false +// pii: none beyond what submitters publish themselves +// 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 registryRepository = 'The-Interdependency/The-Interdependency.github.io'; +const submissionLabel = 'related-work'; +const approvalLabel = 'approved'; +const snapshotPath = 'src/_data/snapshots/works.last-known-good.json'; +const generatedPath = 'src/_data/generated/works.json'; + +export const WORK_TYPES = ['essay', 'paper', 'art', 'music', 'video', 'code', 'response', 'other']; + +export const REQUIRED_FIELDS = { + 'Work title': 'title', + 'Creator': 'creator', + 'Link': 'url', + 'Work type': 'type', + 'Relation to The Interdependent Way': 'relation', + 'Description': 'description' +}; + +const OPTIONAL_FIELDS = { 'License note': 'license', 'Display source': 'displayUrl' }; + +// Hosts whose player/embed URLs may be iframed. Extend deliberately; every +// entry means third-party script runs on the works page for approved listings. +// Grouped by medium; unlisted hosts (e.g. a PeerTube instance) can be added on +// request via an ordinary issue. +export const IFRAME_EMBED_HOSTS = new Set([ + // video + 'www.youtube.com', + 'www.youtube-nocookie.com', + 'player.vimeo.com', + 'www.dailymotion.com', + 'player.twitch.tv', + 'rumble.com', + 'odysee.com', + 'streamable.com', + 'www.loom.com', + 'embed.ted.com', + // audio, music, podcasts + 'w.soundcloud.com', + 'bandcamp.com', + 'open.spotify.com', + 'embed.music.apple.com', + 'embed.podcasts.apple.com', + 'www.mixcloud.com', + 'audiomack.com', + 'www.podbean.com', + // documents, archives, slides + 'archive.org', + 'docs.google.com', + 'onedrive.live.com', + 'e.issuu.com', + 'www.scribd.com', + 'www.slideshare.net', + // code, games, interactive, 3D + 'codepen.io', + 'codesandbox.io', + 'jsfiddle.net', + 'itch.io', + 'sketchfab.com' +]); + +const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.avif']; +const AUDIO_EXTENSIONS = ['.mp3', '.ogg', '.wav', '.m4a', '.flac', '.opus']; + +// Derive how a display source is shown. The site never fetches or stores the +// bytes — these are pointers the visitor's browser loads from the creator's +// own hosting at view time (display, not host). +export function deriveDisplay(displayUrl) { + const url = new URL(displayUrl); + const pathname = url.pathname.toLowerCase(); + if (IMAGE_EXTENSIONS.some(ext => pathname.endsWith(ext))) return { kind: 'image', url: url.href }; + if (AUDIO_EXTENSIONS.some(ext => pathname.endsWith(ext))) return { kind: 'audio', url: url.href }; + if (pathname.endsWith('.pdf')) return { kind: 'pdf', url: url.href }; + if (IFRAME_EMBED_HOSTS.has(url.hostname)) return { kind: 'iframe', url: url.href }; + return null; +} + +// GitHub issue forms render each field as "###