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 landing13f5f3a3: - 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 rebuildd1af039asaw 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: atd1af039ait names prayer-walk-planner STALE (0b695f70 vs 0042dda2, rega's numbers); on main13f5f3a3--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:
parent
2ff0ecba35
commit
dca7d83b53
4 changed files with 35 additions and 7 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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' +
|
||||
|
|
|
|||
Loading…
Reference in a new issue