#!/usr/bin/env node /** * sri-consistency-check.mjs — assert every leaf's baked SRI MATCHES the bundle it loads. * * ★ WHY THIS EXISTS — it caught a REAL live breakage on 2026-09-20 (after the fact). * The publish gate already asserted that every manifest bundle RESOLVES (the .js exists). * That is NOT enough. A tree can have every file present and still be internally * INCONSISTENT, and publishing it breaks the element in production. * * THE MECHANISM (know this before you "just rebuild one bundle"): * `BUNDLE=x bun x vite build` rewrites public/doxa-maps/x/x.js * but the leaf page public/doxa-maps/x/x/index.html carries a BAKED * integrity="sha384-..." that the per-bundle build does not touch. * Two things re-bake it. `bun run fix:sri` (internal/fix-sri.mjs) rewrites the hashes and * NOTHING else. `bun run tree` (internal/generate-tree.mjs) re-bakes them as a side effect of * regenerating every page from source. NEITHER needs the full build — an earlier version of * this comment said generate-tree "runs ONLY in the full build", which is false (it is its own * script) and is exactly why agents kept reaching for the destructive rebuild. * So a single-bundle build leaves the bundle NEWER than its leaf. Publish that pair and * the browser refuses to execute the script: * "Failed to find a valid digest in the 'integrity' attribute for resource ..." * The element then never registers — customElements.get() is false, no shadow root, * blank element. It looks exactly like a catastrophic bundle failure. It is not: the * bundle is fine, the HASHES simply disagree. * * PAINFUL IRONY worth remembering: the instruction "use BUNDLE=x vite build instead of * bun run build, to avoid clobbering the shared output tree" is what CAUSED this. The * safety advice created a different hazard. The fix is not to ban single-bundle builds — * it is to make sure an inconsistent tree can never SHIP. * * ★ WHAT THIS CHECK CANNOT SEE — a DELETED leaf (so never run it alone before a publish). * This script walks the leaves that EXIST and asserts each one's baked hash matches its * bundle. It asserts NOTHING about how many leaves there should be. A leaf that no longer * exists is simply not checked — the gate passes because there is nothing left to disagree. * So a tree gutted by a per-bundle `BUNDLE=x vite build` (which empties OTHER bundles' leaf * index.html) returns a confident PASS here while being unpublishable. * The leaf COUNT is settled-tree-gate.mjs's signal 4 (leaves >= MIN_LEAVES). The two checks * are complementary and the publish runbook runs BOTH: * bun run check:settled && bun run check:sri * Named by the grid-palette agent, 2026-09-20, after a five-sample poll watched leaves climb * 0 -> 58 mid-build; a single sample of a moving tree cannot tell "destroyed" from "rebuilding". * * NOT the same thing as the 5-minute SRI CACHE trap documented in the publish runbook. * That one is TRANSIENT (republishing the same bundle inside the 300s max-age serves a * stale .js against a fresh integrity) and resolves itself. THIS one is PERMANENT — * the bytes on disk genuinely disagree, and waiting changes nothing. * * USAGE * node internal/sri-consistency-check.mjs [treeDir] # default public/doxa-maps * Exits 1 listing every mismatch, so the publish gate aborts under `set -e`. */ import { readFileSync, existsSync, readdirSync } from 'node:fs' import { join, dirname, resolve } from 'node:path' import { createHash } from 'node:crypto' import { TREE_NAME } from './_tree-name.mjs' const TREE = process.argv[2] || `public/${TREE_NAME}` if (!existsSync(TREE)) { console.error('SRI-CONSISTENCY: tree not found: ' + TREE) process.exit(2) } function sri(file) { return 'sha384-' + createHash('sha384').update(readFileSync(file)).digest('base64') } // Walk every generated leaf page and check the