Wordle module, cross-language tests, and the deploy path

The browser engine is a port of the Python one and CI proves it: all 21.2M
(guess, answer) pairs hashed on both sides to the same SHA-256. Six TS tests,
including the duplicate-letter table and the twelve pinned seed vectors that
keep ?seed= permalinks pointing at the same word the recording used.

Word lists are split by how they are used. answers.json is inlined because the
board needs it before first paint to turn a seed into a word, and a fetch there
means a visibly empty board on a cold cache. guesses.json is fetched, because it
is three times larger and only needed the first time somebody presses Enter;
until it lands, validation falls back to the answer list, which accepts strictly
fewer words. The failure mode is 'your real word was briefly rejected', not 'a
non-word was accepted' — the right way round.

The solver runs in a worker constructed from a same-origin module URL, never
Vite's ?worker&inline: that yields a blob:, and production CSP has no
worker-src, so it falls back to default-src 'self' and the worker is blocked
with no console error. It would fail in production only.

deploy.sh smoke-tests the real public hostname from the deploying machine and
fails on a body under 1 kB, because the bind bug's signature is a valid
certificate over an empty 200 and a local --resolve check passes anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 15:53:25 -07:00
parent 2c2dcad9fd
commit b601511e7f
38 changed files with 3419 additions and 646 deletions
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env node
/**
* `dist/sitemap.xml` and `robots.txt`.
*
* PIG's own site is `noindex` — it is a private tool. This one is the exact
* inverse: it exists to be found, shared and quoted, so the robots file opens
* everything and points at a sitemap listing every route the router actually
* produces.
*
* The route list is the same one `prerender.mjs` bakes, from the same reader,
* so a sitemap entry cannot point at a URL that has no prerendered document.
* Those two drifting apart is how you end up submitting 14 URLs and having 13
* of them indexed as the homepage.
*
* `robots.txt` is written into `public/` (source, committed) AND into `dist/`
* (because this runs after the build that would have copied it). The sitemap is
* a build artifact only — it lists what this build contains.
*/
import fs from 'node:fs';
import path from 'node:path';
import { SITE_ORIGIN, abs, die, exists, expandRoutes, green, loadAllMetas, loadVerticals, rel } from './_lib.mjs';
const DIST = abs('dist');
const PUBLIC = abs('public');
const { metas } = loadAllMetas();
const verticals = loadVerticals();
if (verticals.error) die(`could not read the verticals: ${verticals.error}`);
const { routes, errors } = expandRoutes({ metas, verticals: verticals.list });
if (errors.length) die(`route enumeration failed:\n - ${errors.join('\n - ')}`);
/* ------------------------------------------------------------- robots.txt */
const robots = [
'# demo.primeintellectgrowth.com',
'#',
'# This site is meant to be found. Every page is public, static and safe to',
'# crawl; the source it documents is public too.',
'',
'User-agent: *',
'Allow: /',
'',
`Sitemap: ${SITE_ORIGIN}/sitemap.xml`,
'',
].join('\n');
fs.mkdirSync(PUBLIC, { recursive: true });
fs.writeFileSync(path.join(PUBLIC, 'robots.txt'), robots, 'utf8');
/* ------------------------------------------------------------ sitemap.xml */
if (!exists(path.join(DIST, 'index.html'))) {
die(
`${rel(DIST)}/index.html does not exist, so there is no build to describe. ` +
'Run `pnpm build` first. (public/robots.txt has been written.)',
);
}
// One date for the whole build. Per-file mtimes would claim the vertical pages
// changed whenever anything in the bundle did, which is true of a hash-named
// asset and useless to a crawler.
const lastmod = new Date().toISOString().slice(0, 10);
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...routes.map((route) =>
[
' <url>',
` <loc>${escapeXml(`${SITE_ORIGIN}${route.path === '/' ? '/' : route.path}`)}</loc>`,
` <lastmod>${lastmod}</lastmod>`,
' </url>',
].join('\n'),
),
'</urlset>',
'',
].join('\n');
fs.writeFileSync(path.join(DIST, 'sitemap.xml'), xml, 'utf8');
fs.writeFileSync(path.join(DIST, 'robots.txt'), robots, 'utf8');
console.log(green(`sitemap: ${routes.length} URLs -> ${rel(path.join(DIST, 'sitemap.xml'))}`));
console.log(green(`robots: ${rel(path.join(PUBLIC, 'robots.txt'))} and ${rel(path.join(DIST, 'robots.txt'))}`));
/** Sitemap URLs are XML text; a bare `&` in a query string invalidates the file. */
function escapeXml(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}