#!/usr/bin/env node /** * The attribution checker. * * This repo is public and Apache-2.0, and almost none of what makes the demo * work is ours: the primitives are shadcn/ui over Radix, the answer list is * Wordnik's filtered through SCOWL, the typeface is Manrope under the SIL Open * Font License, the icons are lucide, the charts are recharts. Every one of * those licences is permissive, and every one of them requires attribution. * * So `NOTICE` is not paperwork, it is a build artifact with a test. This script * is that test: it fails if NOTICE is missing any source we actually ship, and * it fails if a source is named without its licence beside it — because * "uses Manrope" without "OFL-1.1" is not attribution, it is a mention. * * The OFL additionally requires that the licence text travel WITH the font, so * `public/fonts/OFL.txt` is checked for separately. Shipping the .woff2 out of * node_modules and leaving the licence behind is the single easiest way to * violate the one licence on this list that has teeth. */ import fs from 'node:fs'; import { Report, abs, exists, read, rel } from './_lib.mjs'; const report = new Report('check-licenses'); const NOTICE = abs('NOTICE'); const OFL = abs('public', 'fonts', 'OFL.txt'); /** * Every third-party source this site ships, and the licence each one must be * named with. `used` is a cheap sanity check in the other direction: an entry * whose artifact has left the repo is stale attribution, and stale attribution * quietly becomes wrong attribution. */ const SOURCES = [ { label: 'shadcn/ui', match: /shadcn/i, licence: /\bMIT\b/, why: 'the UI primitives in src/components/ui are shadcn/ui components, hand-copied into this repo.', used: () => exists(abs('components.json')), }, { label: 'Radix UI', match: /radix/i, licence: /\bMIT\b/, why: 'every primitive with behaviour — dialog, tabs, tooltip, slider — is Radix underneath.', used: () => exists(abs('node_modules', '@radix-ui')), }, { label: 'the Wordnik word list', match: /wordnik/i, licence: /\bMIT\b/, why: 'the guess list is derived from Wordnik.', used: () => hasWordFile(/wordnik/i), }, { label: 'SCOWL', match: /\bSCOWL\b/i, licence: /permissive|attribution|BSD|Kevin\s+Atkinson/i, why: 'the answer list is filtered through SCOWL.', used: () => hasWordFile(/scowl/i), }, { label: 'Manrope', match: /manrope/i, // The OFL is version-specific and the version matters: OFL-1.1 is the one // Manrope ships under, and it is the one whose terms are quoted in OFL.txt. licence: /OFL[-\s]?1\.1|SIL\s+Open\s+Font\s+License/i, why: 'Manrope is the typeface, embedded as a variable woff2.', used: () => exists(abs('node_modules', '@fontsource-variable', 'manrope')), }, { label: 'lucide', match: /lucide/i, licence: /\bISC\b/, why: 'every icon on the site is a lucide icon.', used: () => exists(abs('node_modules', 'lucide-react')), }, { label: 'recharts', match: /recharts/i, licence: /\bMIT\b/, why: 'the reward and metric charts are recharts.', used: () => exists(abs('node_modules', 'recharts')), }, { label: "PIG's own token layer", match: /\bPIG\b|Prime\s+Intellect\s+Growth/i, licence: /Apache[-\s]?2\.0/i, why: 'the colour and motion tokens in src/index.css come from PIG and ship under Apache-2.0.', used: () => exists(abs('src', 'index.css')), }, ]; /** True when the words directory still carries a file from this source. */ function hasWordFile(pattern) { const dir = abs('envs', 'wordle_five', 'words'); return exists(dir) && fs.readdirSync(dir).some((name) => pattern.test(name)); } if (!exists(NOTICE)) { report.fail( 'NOTICE', 'NOTICE exists', 'there is no NOTICE file at the repo root. Every source below is shipped by this site and each of ' + `their licences requires attribution: ${SOURCES.map((s) => s.label).join(', ')}.`, ); report.finish(); } const notice = read(NOTICE); const lines = notice.split('\n'); for (const source of SOURCES) { // Every line that names the source, not just the first. "PIG-Demo" in the // copyright header matches the PIG entry three dozen lines before its actual // attribution block, and a first-match-wins reader fails on a NOTICE that is // completely correct. const hits = []; lines.forEach((line, i) => { if (source.match.test(line)) hits.push(i); }); if (hits.length === 0) { report.fail( 'NOTICE', 'attribution complete', `${source.label} is not named. It is shipped by this site — ${source.why} — and its licence requires ` + 'attribution. Add it with its licence identifier.', ); continue; } report.passed += 1; // The licence has to sit with the name, not merely somewhere in the file: // a NOTICE that says "MIT" once at the top and lists nine projects under it // is attributing all nine to whichever licence happens to be first. const near = (i) => lines.slice(Math.max(0, i - 2), i + 6).join('\n'); report.check( hits.some((i) => source.licence.test(near(i))), `NOTICE:${hits[0] + 1}`, 'attribution names the licence', `${source.label} is named on line(s) ${hits.map((i) => i + 1).join(', ')} but no matching licence appears ` + `beside any of them (looking for ${source.licence}). A name without a licence is a mention, not an ` + 'attribution.', ); if (!source.used()) { report.warn( `NOTICE names ${source.label}, but nothing in the repo appears to use it any more. ` + 'Stale attribution is how a NOTICE stops being trustworthy.', ); } } /* -------------------------------------------------------- the OFL's own rule */ report.check( exists(OFL), rel(OFL), 'OFL text ships with the font', 'Manrope is under SIL OFL-1.1, which requires the licence text to travel with the font files. ' + 'Copy node_modules/@fontsource-variable/manrope/LICENSE to public/fonts/OFL.txt. ' + 'Shipping the woff2 and leaving the licence behind is the one violation on this list with teeth.', ); if (exists(OFL)) { const text = read(OFL); report.check( /SIL OPEN FONT LICENSE/i.test(text) && /Version 1\.1/i.test(text), rel(OFL), 'OFL text ships with the font', 'the file exists but does not look like the SIL Open Font License 1.1. It must be the licence text ' + 'itself, not a pointer to it.', ); } report.finish();