Hold composed bundles to the bar their readers are actually on

A gate scoped by a bundle's own declared stage misses what a production reader
sees. Two production leaves compose three bundles that declare themselves
experimental, so the code on those pages was held to the experimental bar --
not by anyone's decision, but because the scope was computed from the wrong end
of the composition.

Effective stage: production if it declares production, or if anything
production-for-gating composes it, transitively. The graph's edges come in two
shapes, a bare bundle name and a bundle/profile pair, and reading only one
silently halves it.

It lives in its own module because check:frozen-labels needs the same rule.
Two gates with their own idea of "production" would disagree by passing a file
the other fails, with neither obviously wrong.

The scan goes from seven plugins to ten, nineteen files to twenty-nine, and
from 178 to 274. The 96 are not new: they are English on production pages that
no gate was measuring. The report names which plugins are in scope only by
composition, and why, since that is the first question a reader will have.
This commit is contained in:
Doxa Maps 2026-09-28 13:23:23 +00:00
parent 2250ed4fe4
commit 4b0751998d
2 changed files with 105 additions and 3 deletions

View file

@ -44,6 +44,7 @@ import { pathToFileURL } from 'node:url'
import { execSync } from 'node:child_process'
import { parse } from '@vue/compiler-sfc'
import * as babel from '@babel/parser'
import { productionForGating, stageReason } from './lib/effective-stage.mjs'
// ── what counts as user-visible ─────────────────────────────────────────────────────────────────
const VISIBLE_ATTRS = new Set(['title', 'placeholder', 'aria-label', 'alt', 'label'])
@ -408,13 +409,19 @@ export function scanSfc(src, file) {
// ── production plugins: the scope this gate defends ─────────────────────────────────────────────
// Read from the built manifest so the list cannot drift from what actually ships.
// ★ EFFECTIVE stage, not declared stage. A production leaf composes other bundles at runtime, so a
// reader on a production page sees code from bundles that declare themselves experimental — and
// scoping by the declared stage held that code to the experimental bar. The rule and the graph live
// in lib/effective-stage.mjs, shared with check:frozen-labels so the two gates cannot disagree about
// what "production" means.
function productionPluginDirs(root = '.') {
const mf = join(root, 'public/dx-maps/manifest.json')
if (!existsSync(mf)) return []
const m = JSON.parse(readFileSync(mf, 'utf8'))
const names = m.bundles.filter((b) => b.stage === 'production').map((b) => b.bundle)
return names.map((n) => join(root, 'plugin-designs', n)).filter((d) => existsSync(d))
MANIFEST = m
return productionForGating(m).map((n) => join(root, 'plugin-designs', n)).filter((d) => existsSync(d))
}
let MANIFEST = null
const sfcsIn = (dir) => {
const out = []
const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) {
@ -485,6 +492,17 @@ const msg = t('engineMissing')
const t2 = useOptionalT('regions')
const ok2 = t2('Countries')`
// ★ EFFECTIVE STAGE: a production leaf pulls experimental bundles into the production bar.
const STAGE_FIXTURE = {
bundles: [
{ bundle: 'leaf-prod', stage: 'production', composes: ['mid-exp', 'pair/with-profile'] },
{ bundle: 'mid-exp', stage: 'experimental', composes: ['deep-exp'] },
{ bundle: 'deep-exp', stage: 'experimental' },
{ bundle: 'pair', stage: 'experimental' },
{ bundle: 'lonely-exp', stage: 'experimental' },
],
}
function selfTest() {
let fails = 0
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
@ -562,6 +580,12 @@ function selfTest() {
const xw = crossAreaKeys(XAREA_WRAPPED, 'planted-xarea-wrapped.vue')
ok(xw.length === 1 && /pplr\.messages\.mapUnavailable/.test(xw[0].text),
'SEES it through a WRAPPER factory with no literal namespace (usePplrT() → useOptionalT(\'pplr\'))')
const eff = productionForGating(STAGE_FIXTURE)
ok(eff.includes('mid-exp'), 'an experimental bundle COMPOSED by a production leaf is production-for-gating')
ok(eff.includes('deep-exp'), 'and TRANSITIVELY — composition nests, so one pass is not enough')
ok(eff.includes('pair'), "a composes entry of the form 'bundle/profile' resolves to its BUNDLE")
ok(!eff.includes('lonely-exp'), 'CONTROL: an experimental bundle nobody composes stays out of scope')
ok(stageReason(STAGE_FIXTURE, 'mid-exp').startsWith('composed by'), 'and the report can say WHY it is in scope')
console.log(fails ? `\nSELF-TEST: ${fails} FAIL(s)` : '\nSELF-TEST: the gate can see planted leaks and stays quiet on correct code')
return fails
}
@ -697,7 +721,13 @@ const leaks = findings.filter((f) => !f.deliberate && !f.suspect)
// WHICH BYTES, so two correct runs are indistinguishable in a report. The stamp is therefore not
// only for the writer who forgets to name a sha — it is what lets a READER tell two runs apart at
// all. When counts disagree, compare the tree line before anything else.
console.log(`\n ${dirs.length} production plugin(s), ${files} SFC(s) scanned · tree ${treeStamp()}`)
const composed = MANIFEST ? dirs.map((d) => d.split('/').pop()).filter((n) => stageReason(MANIFEST, n).startsWith('composed')) : []
console.log(`\n ${dirs.length} production-for-gating plugin(s), ${files} SFC(s) scanned · tree ${treeStamp()}`)
if (composed.length) {
// Named, because "why is this experimental bundle in a production scan?" is the first question a
// reader will have, and the answer is a fact about the composition graph rather than a judgement.
console.log(` including ${composed.length} reached only by composition: ${composed.map((n) => `${n} (${stageReason(MANIFEST, n)})`).join('; ')}`)
}
for (const f of leaks) console.log(` ${f.kind.padEnd(16)} ${f.file.replace(/^.*plugin-designs\//, '')} ${JSON.stringify(f.text).slice(0, 90)}`)
if (deliberate.length) {
// Listed so nobody wonders where they went, but NOT counted: dir="ltr" is the plugin's own marker

View file

@ -0,0 +1,72 @@
// EFFECTIVE stage: what bar a bundle should be held to, rather than what stage it declares.
//
// ★ THE GAP THIS CLOSES (sr, 2026-09-28). A gate that scopes by a bundle's OWN stage misses what a
// production reader actually sees. research-map-plus-grid is production and composes
// doxa-research-map and people-groups-grid, both declared experimental; dashboard-plus-grid is
// production and composes research-dashboard and people-groups-grid. So the code on a production
// page was being held to the experimental bar — not because anyone decided that, but because the
// scope was computed from the wrong end of the composition.
//
// THE RULE: a bundle is production-for-gating if it declares production, OR if any bundle that is
// itself production-for-gating composes it. Transitive, because composition nests.
//
// ★ THE GRAPH'S EDGES HAVE TWO SHAPES and taking one and not the other silently halves it:
// `composes` holds both bare bundle names ('doxa-research-map') and bundle/profile pairs
// ('regions-dashboard/regions-list'). The bundle is the part before the slash in either case.
//
// Shared by check:hardcoded-english and check:frozen-labels so the two cannot disagree about what
// "production" means — a disagreement that would show up as one gate passing a file the other fails,
// with neither obviously wrong.
/** The bundle a `composes` entry refers to: 'a/b' → 'a', 'a' → 'a'. */
export const composedBundle = (entry) => String(entry || '').split('/')[0]
/**
* @param {{bundles?: Array<{bundle?: string, stage?: string, composes?: string[]}>}} manifest
* @returns {Map<string, 'production'|'experimental'>} effective stage per bundle name
*/
export function effectiveStages(manifest) {
const bundles = (manifest && manifest.bundles) || []
const byName = new Map()
for (const b of bundles) if (b && b.bundle) byName.set(b.bundle, b)
const eff = new Map()
for (const b of bundles) if (b && b.bundle) eff.set(b.bundle, b.stage === 'production' ? 'production' : 'experimental')
// Walk down from every production root, marking what it composes. Repeat until nothing changes:
// a production bundle may compose one that composes another, and a single pass would stop at the
// first level. The visited set makes a cycle terminate rather than hang.
let changed = true
while (changed) {
changed = false
for (const [name, stage] of [...eff]) {
if (stage !== 'production') continue
const b = byName.get(name)
for (const entry of (b && b.composes) || []) {
const child = composedBundle(entry)
if (!eff.has(child)) continue // composes something not in the manifest
if (eff.get(child) === 'production') continue
eff.set(child, 'production')
changed = true
}
}
}
return eff
}
/** Bundle names a gate should treat as production. */
export function productionForGating(manifest) {
return [...effectiveStages(manifest)].filter(([, s]) => s === 'production').map(([n]) => n).sort()
}
/** Why a bundle is in scope — for a report that must explain itself. */
export function stageReason(manifest, name) {
const bundles = (manifest && manifest.bundles) || []
const self = bundles.find((b) => b && b.bundle === name)
if (self && self.stage === 'production') return 'declares production'
const parents = bundles
.filter((b) => (b.composes || []).some((e) => composedBundle(e) === name))
.filter((b) => effectiveStages(manifest).get(b.bundle) === 'production')
.map((b) => b.bundle)
return parents.length ? `composed by ${parents.join(', ')}` : 'experimental'
}