check:frozen-labels holds a composed bundle to the stage of the page that shows it

Written by the UPG-100 plugin owner. It now takes the stricter of a file's own profile stage and
its bundle's effective stage (internal/lib/effective-stage.mjs), so an experimental bundle that a
production page composes is checked as production, and the output says why.
This commit is contained in:
Doxa Maps 2026-09-28 14:16:20 +00:00
parent 76fe9ddf28
commit f45624915f

View file

@ -38,6 +38,10 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { join, relative, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
// ★ SHARED WITH check:hardcoded-english (regions-plugin). Two gates must agree about WHICH files are
// in scope even when they disagree about what they find in them; a second copy would drift on exactly
// the pair-form / transitive / cycle details below, and the drift would be silent.
import { effectiveStages, stageReason } from './lib/effective-stage.mjs'
import { parse as parseSfc } from '@vue/compiler-sfc'
import * as babel from '@babel/parser'
@ -185,7 +189,9 @@ function stageIndex() {
for (const b of m.bundles || []) for (const p of b.profiles || []) {
byFile.set(join('plugin-designs', b.bundle, p.profile + '.vue'), p.stage || b.stage || 'experimental')
}
return byFile
// The manifest itself and the bundle-level effective stage travel with it: the file-level answer
// needs both, and re-reading the manifest twice would let the two drift apart.
return { byFile, manifest: m, effStages: effectiveStages(m) }
}
const walk = (dir, acc = []) => {
@ -199,12 +205,13 @@ const walk = (dir, acc = []) => {
return acc
}
const stages = stageIndex()
if (!stages) {
const idx = stageIndex()
if (!idx) {
console.log(`FROZEN-LABELS: SKIP — no built tree at public/${TREE_NAME}. Run a build first.`)
console.log(' Reported as a SKIP, not a pass: a gate that reports success when it never ran is worse than no gate.')
process.exit(0)
}
const { byFile: stages, manifest, effStages } = idx
const files = ['library', 'plugin-designs'].flatMap((r) => walk(join(ROOT, r)))
const failures = [], reported = []
@ -215,11 +222,25 @@ for (const f of files) {
catch (e) { console.error(`FROZEN-LABELS: parse error in ${relative(ROOT, f)} — ${e.message}`); process.exit(2) }
if (!hits.length) continue
const rel = relative(ROOT, f)
// library/ is held to the production bar; a plugin file takes its own PROFILE stage.
const stage = rel.startsWith('library/') ? 'production' : (stages.get(rel) || 'experimental')
// ★ TWO QUESTIONS, BOTH ASKED. A .vue file IS a profile, so its own profile.stage is the precise
// answer for the FILE — bundle.stage reads 'production' if ANY profile is, and four bundles are
// mixed (cities-internet, gospel-resources, my-upg-100-list, regions-dashboard), so scoping by
// bundle alone would FAIL on experimental files inside a production bundle. But profile stage alone
// UNDER-reports: three experimental bundles are COMPOSED BY production ones (research-dashboard by
// dashboard-plus-grid, doxa-research-map by research-map-plus-grid, people-groups-grid by both), so
// a reader sees them on a production page while the file says experimental. Take the stricter of the
// two: FAIL if the file's own profile is production OR its bundle is production by effective stage.
const bundleOf = rel.startsWith('plugin-designs/') ? rel.split('/')[1] : null
const byProfile = rel.startsWith('library/') ? 'production' : (stages.get(rel) || 'experimental')
const byBundle = bundleOf ? (effStages.get(bundleOf) || 'experimental') : 'production'
const stage = (byProfile === 'production' || byBundle === 'production') ? 'production' : 'experimental'
// A widened scope must be able to EXPLAIN itself, or it reads as a bug to whoever it newly fails.
const why = (stage === 'production' && byProfile !== 'production' && bundleOf)
? ' [in scope: ' + stageReason(manifest, bundleOf) + ']'
: ''
for (const h of hits) {
const refs = h.binding === '(expression)' ? -1 : (src.match(new RegExp(`\\b${h.binding}\\b`, 'g')) || []).length - 1
const row = `${rel}:${h.line} ${h.binding} (${h.call}) refs=${refs < 0 ? '?' : refs}`
const row = `${rel}:${h.line} ${h.binding} (${h.call}) refs=${refs < 0 ? '?' : refs}${why}`
;(stage === 'production' ? failures : reported).push(row)
}
}