Capture harness, fixture verification, CI, and the public README
The site does no live inference. Rollouts are captured once against spark-1 and replayed at their recorded wall-clock — a public demo with no auth cannot hold an API key, and a recorded run can be scrubbed, permalinked, blind-compared and verified in ways a live one cannot. What stops it being a video is that the browser re-derives every number from the recorded moves. verify_fixtures.py is the Python half of that: it replays every committed fixture through the engine and reproduces its own rewards. All 16 land at delta 0.0. A fixture that cannot be regenerated is a claim with no receipt. First real measurement, thinking off, 8 seeds: solved 0/8. The model repeats guesses it has already played, invents words (trape, slith, postt, boomy), and contradicts its own feedback — consistency 0.09 to 0.17. That is the published failure taxonomy showing up in our own data on the first run, and it is why `consistency` is a reward component rather than a footnote. A capture failure is recorded as a turn with a null reply, never dropped. A capture that silently discarded failed turns would be reporting a better model than the one that ran. CI gates both halves and four things that fail silently in production: the word lists must rebuild byte-identically, the prerendered routes must carry their own baked og tags (crawlers do not run JS, so without them every shared link previews as the homepage), no blob: URL may reach the bundle (the site's CSP has no worker-src, so it falls back to default-src 'self' and a blob worker is blocked with no error), and the conformance digest must match across languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user