#!/usr/bin/env node /** * fix-sri.mjs — rewrite every baked SRI hash in an ALREADY-BUILT tree, and nothing else. * * ★ WHY THIS EXISTS, and the premise it CORRECTS. * The dispatch that asked for this said: "`bun run tree` regenerates manifest.json + leaf pages * but NOT the baked SRI hashes, so `bun run check:sri` fails." MEASURED 2026-09-23, that is * FALSE — generate-tree.mjs hashes each bundle (its `integrity` local, ~line 1054) and threads * the value into every page it writes. Proven by poisoning one leaf's integrity * (check:sri -> exit 1), running `bun run tree`, and re-checking (exit 0). * * So the cheap path was never missing. What was missing is the SIGNPOST and the SIZE: * * 1. THE SIGNPOST. check:sri's own failure text said "FIX: run a full `bun run build`". An agent * that trusts the gate it just ran is then sent straight at the destructive path — whose first * step deletes public/doxa-maps/ and tears the tree out from under every concurrent reader * (measured 3+ times: a 461-file phantom deletion, several false "element did not mount" * diagnoses). The gate was TEACHING the failure mode the build-lock guard exists to prevent. * That message now names this script. A gate that diagnoses correctly and prescribes the * dangerous remedy is worse than one that says nothing, because it is obeyed. * * 2. THE SIZE. `bun run tree` is cheap only next to a full build. MEASURED on this box: 2.7-4.5s, * and it WRITES 1234 files under public/ (450 under public/doxa-maps) every single run, * because it re-copies 401 runtime assets and re-emits all 48 pages. That churn is not free * to your neighbours: internal/settled-tree-gate.mjs fails a tree with ANY write in the last * 45s, so one `bun run tree` blinds every other agent's check:settled for three quarters of a * minute, and those agents are told "do NOT measure or publish this tree." * * Worse, on a SHARED BRANCH `tree` regenerates pages FROM SOURCE — so it silently bakes a * neighbour's in-flight .vue edits into leaf pages under YOUR change. Same family as the * measured hazard of committing a built bundle on a shared branch. * * This script writes ONLY the pages whose hash actually disagrees. In the normal case that is * one file, and when the tree is already consistent it is ZERO files — nothing to churn, * nothing to un-settle, no source re-read at all. * * ★ WHAT IT DOES NOT DO — and this limit is the point, not an omission. * It makes the HASHES AGREE. It does not make the BUNDLE CORRECT. If public/doxa-maps/x/x.js was * built from stale source, this bakes a matching hash for stale code and check:sri goes green. * That is equally true of `bun run tree`, and it is why this is a REPAIR verb, not a build verb: * build the bundle first (`BUNDLE=x bun x vite build`), then run this to re-agree the leaves. * Bundle FRESHNESS is a different question with its own gate — `bun run check:composite`. * It also asserts nothing about how many leaves SHOULD exist; a tree gutted by a partial build * is still unpublishable after this passes. That is settled-tree-gate.mjs's signal 4. * The publish runbook still runs BOTH: `bun run check:settled && bun run check:sri`. * * WHAT IT REWRITES * - every `\n') // (b) a leaf with NO integrity at all — invisible to check:sri, still a real downgrade writeFileSync(join(dir, 'b', 'q', 'index.html'), '\n') // (c) a manifest with a wrong bundle hash AND a wrong paste-verbatim embed snippet writeFileSync(join(dir, 'manifest.json'), JSON.stringify({ schema: 'self-test', bundles: [{ bundle: 'b', scriptUrl: './b/b.js', integrity: WRONG, profiles: [{ profile: 'p', embed: { script: '' } }], }], }, null, 2) + '\n') // 1. THE CONTROL MUST BE ABLE TO GO RED. A fixer validated by a check that has only ever // printed PASS proves nothing at all. const red = spawnSync(process.execPath, [CHECKER, dir], { encoding: 'utf8' }) ok(red.status === 1, 'check:sri goes RED on the poisoned tree (exit ' + red.status + ', expected 1)') ok(/b\/p\/index\.html/.test(red.stderr + red.stdout), 'it goes red for the RIGHT page (b/p/index.html named in the output)') // 2. repair const st = fixTree(dir) ok(st.rehashed >= 2, 'rehashed the stale leaf AND the stale manifest bundle (rehashed=' + st.rehashed + ', expected >=2)') ok(st.added === 1, 'added the ONE missing integrity (added=' + st.added + ')') ok(st.missing.length === 0, 'no unresolvable script refs (missing=' + st.missing.length + ')') // 3. GREEN AFTER — and not vacuously green: it must have had something to check. const green = spawnSync(process.execPath, [CHECKER, dir], { encoding: 'utf8' }) ok(green.status === 0, 'check:sri goes GREEN after the repair (exit ' + green.status + ')') const checked = Number((green.stdout.match(/checked (\d+) script reference/) || [])[1] || 0) ok(checked >= 2, 'the green run actually MEASURED both leaves (checked=' + checked + ', expected >=2) — a tree with nothing to compare also prints PASS') // 4. the manifest, which check:sri never reads, is repaired too const m = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8')) ok(m.bundles[0].integrity === REAL, 'manifest bundles[].integrity now matches the bytes') ok(m.bundles[0].profiles[0].embed.script.includes(REAL), 'the paste-verbatim embed snippet now matches the bytes') ok(readFileSync(join(dir, 'b', 'q', 'index.html'), 'utf8').includes('integrity="' + REAL + '" crossorigin="anonymous"'), 'the integrity-less leaf gained one, in generate-tree\'s emitted shape') // 5. IDEMPOTENT — the whole point is that a consistent tree costs ZERO writes, so running // this never un-settles the tree for a neighbouring agent. const again = fixTree(dir) ok(again.filesWritten.length === 0, 'a second run writes 0 file(s) (wrote ' + again.filesWritten.length + ')') } finally { rmSync(dir, { recursive: true, force: true }) } if (fails.length) { console.error('\nFIX-SRI SELF-TEST: FAIL — ' + fails.length + ' assertion(s):') for (const f of fails) console.error(' ✗ ' + f) process.exit(1) } console.log('\nFIX-SRI SELF-TEST: PASS — the repair is proven against a check that was first proven to fail.') } if (SELF_TEST) { selfTest() } else { let code = 0 for (const root of (TREES.length ? TREES : [`public/${TREE_NAME}`])) { code = report(root, fixTree(root)) || code } process.exit(code) }