Freshness covers every bundle a landing can have staled; SRI repairs keep the gallery's manifest copy

Two gaps in the build-owner tooling, found by rega while landing 13f5f3a3:
- check:composite-freshness judged only COMPOSITES. A library change (bd75f609's Arabic catalogue:
  library/i18n eager-loads every locale into every bundle that uses it) left 12 of 25 bundles stale on main,
  most of them not composites; my consumer rebuild d1af039a saw none of it. The check takes --all and
  --bundles a,b; the landing precondition scopes it by what the landing changed (shared code -> every
  bundle; otherwise composites + the bundles whose own folder changed); the build-owner landing always
  uses --all. Proven: at d1af039a it names prayer-walk-planner STALE (0b695f70 vs 0042dda2, rega's numbers);
  on main 13f5f3a3 --all finds the 12.
- fix:sri repaired manifest.json but not the Research Gallery's byte-identical copy
  (doxa-maps-spa/manifest.json, read first on a foreign origin), so an embedded gallery refused a rebuilt
  bundle; it now rewrites the copy too (tested: a changed bundle -> both repaired, identical, check:sri
  PASS). And generate-tree now writes the front page's integrity itself, so `bun run tree` after fix:sri no
  longer strips it (tree alone: 115 agreed, 0 added).
This commit is contained in:
doxa-maps-88655c70 2026-09-25 21:54:48 +00:00 committed by Doxa Maps
parent 2ff0ecba35
commit dca7d83b53
4 changed files with 35 additions and 7 deletions

View file

@ -49,6 +49,12 @@ const TREE = join(ROOT, 'public', TREE_NAME)
const argv = process.argv.slice(2)
const oi = argv.indexOf('--only')
const ONLY = oi >= 0 && argv[oi + 1] ? new Set(argv[oi + 1].split(',')) : null
// NOT ONLY COMPOSITES (rega, 2026-09-25): a library change merged into main reached prayer-walk-planner,
// which composes nothing, so this gate could not see it was stale. --all checks EVERY bundle; --bundles a,b
// checks those (the landing precondition passes the ones a landing can have changed).
const ALL = argv.includes('--all')
const bi = argv.indexOf('--bundles')
const EXTRA = bi >= 0 && argv[bi + 1] ? new Set(argv[bi + 1].split(',').filter(Boolean)) : new Set()
if (!existsSync(join(TREE, 'manifest.json'))) {
console.log(`COMPOSITE-FRESHNESS: SKIP — no built tree (public/${TREE_NAME}/manifest.json).`)
@ -78,8 +84,8 @@ try {
for (const f of readdirSync(join(DESIGNS, bundle)).filter((f) => f.endsWith('.meta.json'))) {
try { const c = JSON.parse(readFileSync(join(DESIGNS, bundle, f), 'utf8')).composes; if (Array.isArray(c) && c.length) composes = (composes || []).concat(c) } catch { /* not JSON: not a declaration */ }
}
if (!composes) continue
for (const e of composes) if (!resolveEntry(e, bundle)) unresolved.push(`${bundle} composes "${e}" — resolves to no bundle`)
if (!composes && !ALL && !EXTRA.has(bundle)) continue
for (const e of composes || []) if (!resolveEntry(e, bundle)) unresolved.push(`${bundle} composes "${e}" — resolves to no bundle`)
composites++
const committed = join(TREE, bundle, `${bundle}.js`)
if (!existsSync(committed)) { failures.push(`${bundle}: no committed bundle at public/${TREE_NAME}/${bundle}/${bundle}.js`); continue }
@ -96,7 +102,7 @@ try {
}
} finally { rmSync(tmp, { recursive: true, force: true }) }
console.log(`COMPOSITE-FRESHNESS: ${composites} composite(s) rebuilt into a temp dir and compared byte for byte with the committed bundle.`)
console.log(`COMPOSITE-FRESHNESS: ${composites} bundle(s) rebuilt into a temp dir and compared byte for byte with the committed bundle.`)
if (fresh.length) console.log(` ✓ ${fresh.join(', ')}`)
for (const u of unresolved) console.log(` ! UNRESOLVED ${u}`)
if (failures.length) {

View file

@ -174,6 +174,16 @@ function fixTree(root) {
if (!DRY) writeFileSync(mf, after)
st.filesWritten.push(mf)
}
// The Research Gallery reads a BYTE-IDENTICAL copy of the manifest first when it runs on a foreign
// origin (generate-tree writes it; see doxa-maps-spa.vue). Repairing the root manifest alone left the
// copy with the OLD integrity, so an embedded gallery refused the rebuilt bundle (rega, 2026-09-25).
for (const rel of ['doxa-maps-spa/manifest.json']) {
const copy = join(root, rel)
if (existsSync(copy) && readFileSync(copy, 'utf8') !== after) {
if (!DRY) writeFileSync(copy, after)
st.filesWritten.push(copy)
}
}
}
return st
}

View file

@ -693,7 +693,7 @@ function rootHtml(bundles, manifest) {
* is precisely why this was nearly missed. "The custom element mounted" is a WEAKER claim than
* "the plugin works"; a mounted element with no data is the failure that looks like success.
*/
function studioRootHtml(bakedToken) {
function studioRootHtml(bakedToken, integrity = '') {
// THE TREE'S FRONT PAGE (<tree>/index.html) — Driver decision, card
// BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE: the output is ONE folder, and opening it opens the
// studio. It used to sit one level ABOVE the tree (public/index.html) with the staging-forge chooser
@ -714,7 +714,7 @@ ${bakedToken ? ` <script>window.MAP_TOKEN=${JSON.stringify(bakedToken)};</scrip
</head>
<body>
<plugin-studio tree-base="."></plugin-studio>
<script src="${src}"></script>
<script src="${src}"${integrity ? ` integrity="${integrity}" crossorigin="anonymous"` : ''}></script>
</body>
</html>
`
@ -1366,7 +1366,9 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) {
}
// THE FRONT PAGE IS THE STUDIO (see studioRootHtml). The staging-forge chooser that used to be
// written here (rootHtml) is retired with it; nothing is written above the tree any more.
writeFileSync(join(root, 'index.html'), studioRootHtml(bakedToken))
// …with the studio bundle's integrity, written HERE: it used to be added only by fix:sri, so running
// `bun run tree` after fix:sri stripped it again (rega, 2026-09-25).
writeFileSync(join(root, 'index.html'), studioRootHtml(bakedToken, (bundles.find((b) => b.bundle === 'plugin-studio') || {}).integrity || ''))
bundles.forEach((b) => {

View file

@ -63,7 +63,17 @@ console.log('LANDING-PRECONDITIONS: 3 sri — every leaf loads the bundle its in
if (process.env.DOXA_SKIP_FRESHNESS === '1') {
console.log('LANDING-PRECONDITIONS: 4 fresh — SKIPPED (DOXA_SKIP_FRESHNESS=1; dry check only, never set by main-capture scan)')
} else {
const fresh = run(process.execPath, ['internal/check-composite-freshness.mjs'])
// SCOPE: what can this landing have made stale? A change to shared code (library/, the build config, the
// dependencies) reaches ANY bundle — measured 2026-09-25: one Arabic catalogue edit in library/i18n left 12
// of 25 bundles stale, most of them not composites, and a composite-only check saw none of it. Otherwise:
// every composite, plus each bundle whose own folder the landing touched.
const base = run('git', ['merge-base', 'HEAD', `origin/${process.env.MAIN_CAPTURE_BRANCH || 'main'}`]).stdout.trim()
const changed = base ? run('git', ['diff', '--name-only', base, 'HEAD']).stdout.split('\n').filter(Boolean) : []
const shared = !base || changed.some((f) => /^(library\/|vite\.config\.js$|package\.json$|bun\.lockb?$|vite\.doxa-maps-build-config\.json$)/.test(f))
const own = [...new Set(changed.map((f) => (f.match(/^plugin-designs\/([^/]+)\//) || [])[1]).filter(Boolean))]
const scope = shared ? ['--all'] : (own.length ? ['--bundles', own.join(',')] : [])
console.log(`LANDING-PRECONDITIONS: 4 fresh — scope: ${shared ? 'EVERY bundle (shared code changed' + (base ? '' : '; no merge-base') + ')' : 'composites' + (own.length ? ' + ' + own.join(', ') : '')}`)
const fresh = run(process.execPath, ['internal/check-composite-freshness.mjs', ...scope])
if (fresh.status !== 0) refuse(
'a committed composite is not what its source builds (stale consumers).\n' +
'README rule 5: ONE agent owns the full build. If your change touches library/ or anything compiled into\n' +