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:
parent
2250ed4fe4
commit
4b0751998d
2 changed files with 105 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
72
internal/lib/effective-stage.mjs
Normal file
72
internal/lib/effective-stage.mjs
Normal 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'
|
||||
}
|
||||
Loading…
Reference in a new issue