#!/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)
}