Every reference to the Driver in internal/ is reworded the same way as in library/: the reason stays, the speaker and the quote go, an open decision reads "provisional, pending a product decision", and agent names and decision- sheet rows are dropped. Where a gate printed the word, its message now names the reviewer or the ruling instead. Three spots were code, not comments, and changed with their callers: - the element census's allowlist field `driver` is now `ruling`, in the gate and both presets (same validation: match, ruling, date and card required); - check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in the entries and in the regex that counts them (its output is otherwise byte-identical); - the package.json description of the census says "ruling". public-hygiene-check keeps its two patterns for the word: they are what keep it out of the served folder.
145 lines
8 KiB
JavaScript
145 lines
8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-manifest-covers-discovery.mjs — every DISCOVERED bundle must appear in the built manifest.
|
|
*
|
|
* THE DEFECT THIS CATCHES (found 2026-09-22 by comparing the dev and built manifests, not by any gate):
|
|
* discovered bundles ........ 19
|
|
* bundles in manifest ....... 19 <- the counts MATCH
|
|
* discovered but MISSING .... 1 key-cities
|
|
* in manifest, not real ..... 1 template-bundle
|
|
* `key-cities` had its compiled key-cities.js on disk but ZERO profile leaf directories, so its four
|
|
* profiles vanished from manifest.json and from the studio's tab list. A tree under review
|
|
* silently omitted a whole plugin.
|
|
*
|
|
* ★ WHY EVERY EXISTING GATE PASSED — this is the interesting part, and the reason for a new file:
|
|
* - the settled-tree LEAF FLOOR counts index.html recursively (44, floor 40). A bundle that loses
|
|
* ALL its leaves while another keeps extras stays comfortably above the floor.
|
|
* - check:embed-targets asserts every target the manifest LISTS exists on disk. It cannot see a
|
|
* bundle the manifest never listed — an omission is invisible to a check that iterates the list.
|
|
* - a BUNDLE COUNT comparison would also have passed: 19 == 19, because one bundle was effectively
|
|
* swapped for another. Counting is not covering.
|
|
* The only way to catch an omission is to compare the manifest against the INDEPENDENT source of
|
|
* truth — the scanner — rather than against itself.
|
|
*
|
|
* ★ `template-bundle` IS EXCLUDED BY DESIGN, NOT BY ACCIDENT. discover.mjs:111 filters it by name:
|
|
* n !== 'template-bundle'
|
|
* It is the scaffold a new plugin is copied from, so it correctly is not a plugin. This gate therefore
|
|
* only fails on the DISCOVERED-BUT-MISSING direction. The reverse (a manifest entry the scanner does
|
|
* not return) is reported as a WARNING, because stale build output is real but is healed by the next
|
|
* full build, whereas a missing plugin is silently absent from the product.
|
|
*
|
|
* NEGATIVE-TESTED: removing any bundle from manifest.json makes this FAIL naming it; restoring passes.
|
|
* A run that discovers fewer than MIN_BUNDLES FAILS rather than passing vacuously.
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { discoverSpaRegistry } from './plugin-gallery/discover.mjs'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
import { isWebBuilt } from './plugin-targets.mjs'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
|
|
const MIN_BUNDLES = Number(process.env.MIN_BUNDLES || 10)
|
|
|
|
if (!existsSync(MANIFEST)) {
|
|
console.error(`MANIFEST-COVERS: FAIL — public/${TREE_NAME}/manifest.json does not exist.`)
|
|
console.error(' Run `bun run build` first; there is nothing to compare against.')
|
|
process.exit(1)
|
|
}
|
|
|
|
let registry
|
|
try { registry = discoverSpaRegistry() }
|
|
catch (e) {
|
|
console.error('MANIFEST-COVERS: FAIL — the scanner threw: ' + e.message)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Vacuous guard: "0 discovered" must never read as "0 problems".
|
|
if (registry.length < MIN_BUNDLES) {
|
|
console.error(`MANIFEST-COVERS: FAIL — only ${registry.length} bundle(s) discovered (expected >= ${MIN_BUNDLES}).`)
|
|
console.error(' The scanner is broken or plugin-designs/ is empty. Refusing to pass vacuously.')
|
|
process.exit(1)
|
|
}
|
|
|
|
// ★ THE BUILD HAS A THIRD INPUT, AND OMITTING IT MADE THIS GATE CRY WOLF ON ITS FIRST REAL RUN.
|
|
// The first version of this file compared the SCANNER against the MANIFEST and reported any
|
|
// difference as a defect. It immediately "found" key-cities — discovered with 4 profiles, absent from
|
|
// the manifest — which looked exactly like the silent-omission bug it was written for, and survived a
|
|
// full rebuild, which made it look persistent and real.
|
|
//
|
|
// It is neither. vite.doxa-maps-build-config.json sets `gatedProfiles: ["key-cities"]`, and
|
|
// generate-tree.mjs:154 filters on it:
|
|
// const isBundleDir = (name) => !name.startsWith('_') && !name.startsWith('.')
|
|
// && !DEST_CONFIG.gatedProfiles.includes(name)
|
|
// So a bundle can be legitimately DISCOVERED (it is on disk with an index.js) and legitimately
|
|
// EXCLUDED (the config says do not publish it). The scanner answers "what exists"; the config answers
|
|
// "what ships". Comparing the first against the manifest without consulting the second asserts an
|
|
// invariant that was never true.
|
|
//
|
|
// This matters more than the bug it fixes: a gate that reports a deliberate configuration as a defect
|
|
// gets switched off, and a switched-off gate protects nothing. Read the same config the build reads.
|
|
let gated = []
|
|
try {
|
|
const cfg = JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8'))
|
|
gated = Array.isArray(cfg.gatedProfiles) ? cfg.gatedProfiles.map(String) : []
|
|
} catch { gated = [] }
|
|
|
|
// ★ AND A FOURTH: a bundle whose every profile targets something other than the web (Rudder:
|
|
// desktop + widget) is built by desktop:build, never into the tree. generate-tree and run-parallel
|
|
// skip it through the same isWebBuilt(); read it here too, or its deliberate absence reads as the
|
|
// silent-omission bug this gate exists for.
|
|
const nonWeb = registry.filter(b => !isWebBuilt(join(ROOT, 'plugin-designs', b.bundle))).map(b => b.bundle)
|
|
gated = [...gated, ...nonWeb]
|
|
|
|
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
|
|
const listed = new Map((manifest.bundles || []).map(b => [b.bundle, (b.profiles || []).length]))
|
|
const found = new Map(
|
|
registry
|
|
.filter(b => !gated.includes(b.bundle))
|
|
.map(b => [b.bundle, new Set(b.items.map(i => i.profile)).size])
|
|
)
|
|
const gatedPresent = registry.filter(b => gated.includes(b.bundle)).map(b => b.bundle)
|
|
|
|
const missingBundles = [...found.keys()].filter(b => !listed.has(b))
|
|
const orphanBundles = [...listed.keys()].filter(b => !found.has(b))
|
|
|
|
// A bundle can be listed and still be short PROFILES — the same silent-omission shape one level down,
|
|
// so check it too rather than stopping at bundle identity.
|
|
const shortProfiles = []
|
|
for (const [bundle, n] of found) {
|
|
if (!listed.has(bundle)) continue
|
|
if (listed.get(bundle) < n) shortProfiles.push({ bundle, listed: listed.get(bundle), found: n })
|
|
}
|
|
|
|
console.log(`MANIFEST-COVERS: ${found.size} publishable bundle(s) discovered, ${listed.size} listed in the manifest.`)
|
|
if (gatedPresent.length) {
|
|
console.log(` gated by config (present on disk, deliberately NOT published): ${gatedPresent.join(', ')}`)
|
|
}
|
|
|
|
if (orphanBundles.length) {
|
|
console.log(` note: ${orphanBundles.length} manifest entr(y/ies) the scanner does not return: ${orphanBundles.join(', ')}`)
|
|
console.log(' Stale build output — healed by the next full build. Not a failure.')
|
|
}
|
|
|
|
if (missingBundles.length || shortProfiles.length) {
|
|
console.error('\nMANIFEST-COVERS: FAIL — the built manifest does not cover everything the scanner finds:')
|
|
for (const b of missingBundles) {
|
|
console.error(` ✗ ${b} — discovered with ${found.get(b)} profile(s), ENTIRELY ABSENT from manifest.json`)
|
|
}
|
|
for (const s of shortProfiles) {
|
|
console.error(` ✗ ${s.bundle} — discovered ${s.found} profile(s), manifest lists only ${s.listed}`)
|
|
}
|
|
console.error(
|
|
'\n A plugin missing from the manifest is missing from the STUDIO: the tab list is built from this\n' +
|
|
' file, so the plugin silently does not exist for anyone reviewing the product. Counting bundles\n' +
|
|
' would not have caught it — one bundle can be swapped for another and the totals still match.\n' +
|
|
' Usual cause: the bundle has its compiled .js but no profile leaf directories, so generate-tree\n' +
|
|
' emitted nothing for it. A full `bun run build` regenerates both; a per-bundle vite build does not.'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('MANIFEST-COVERS: PASS — every discovered bundle and profile appears in the built manifest.')
|
|
console.log(' This compares the manifest against the SCANNER, not against itself: a check that iterates')
|
|
console.log(' the manifest can never notice something the manifest forgot to list.')
|