Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person, named fleet agents or carried private host names now state the same technical fact in neutral words: poster-render (including the comments it writes into generated posters), packet-render, the poster and research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/), which nothing reads, are removed.
244 lines
13 KiB
JavaScript
244 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* check-shared-component-reached-bundles — a shared fix is only as deployed as the last rebuild
|
||
* of every bundle that consumes it.
|
||
*
|
||
* THE DEFECT THIS CATCHES. library/ is the single source of truth, but EVERY BUNDLE SHIPS ITS OWN
|
||
* COMPILED COPY of the components it imports. So a one-line fix in a shared component is live in
|
||
* whichever bundles happened to be rebuilt afterwards, and silently absent from the rest. Nothing
|
||
* detects it: every other gate measures the RENDERED BEHAVIOUR OF ONE LEAF, and a leaf whose
|
||
* bundle is stale renders the old behaviour perfectly.
|
||
*
|
||
* MEASURED 2026-09-24, two independent instances in one week:
|
||
* - PosterWall hover outline (landed a7f57c1): present in my-upg-100-list, ABSENT from
|
||
* key-cities, country-posters and regions-dashboard — 3 of 4 consumers stale.
|
||
* - SemanticTreeLegend legend.a11y keys: present in 4 bundles, ABSENT from research-dashboard —
|
||
* so a legend there still ships nameless carets while check:a11y-button-names passes 791/791,
|
||
* because that gate only sees the caret on leaves where an expandable branch happens to render.
|
||
* The second one was nobody's active work. It had simply been sitting there.
|
||
*
|
||
* ★ IT ASSERTS A MARKER STRING, NOT A TIMESTAMP OR A HASH. Comparing mtimes says "this bundle is
|
||
* older" and produces noise on every unrelated rebuild; comparing hashes cannot work at all,
|
||
* because a bundle legitimately differs from the library source it compiled. The question that
|
||
* actually matters is narrower and answerable: DOES THE BUILT BUNDLE CONTAIN THIS SPECIFIC CHANGE?
|
||
* A distinctive literal from the change answers exactly that, and it is the same technique used to
|
||
* prove a bundle is not stale after a rebuild.
|
||
*
|
||
* ⚠ THE MARKER MUST SURVIVE MINIFICATION. A CSS rule, a translation key or a user-visible string
|
||
* does; a variable name, a comment or whitespace does not. Markers are declared in MARKERS below
|
||
* with the commit that introduced them, so a reader can tell whether a red line means "not rebuilt"
|
||
* or "this marker was legitimately removed and the entry is stale".
|
||
*
|
||
* Usage: node internal/check-shared-component-reached-bundles.mjs
|
||
* SKIPs when the built tree is absent.
|
||
*/
|
||
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
||
import { resolve, dirname, join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { TREE_NAME } from './_tree-name.mjs'
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
|
||
const TREE = resolve(REPO, `public/${TREE_NAME}`)
|
||
|
||
// Each entry: a change that landed in a SHARED component, and a string that proves a built bundle
|
||
// contains it. `source` is where the marker must also still exist — if it does not, the marker is
|
||
// stale and the gate says so rather than failing every bundle.
|
||
const MARKERS = [
|
||
{
|
||
id: 'posterwall-hover-outline',
|
||
component: 'library/components/poster/PosterWall.vue',
|
||
marker: 'outline:2px solid #2f6fdb',
|
||
landed: 'a7f57c1',
|
||
why: 'the blue hover outline on poster cards',
|
||
},
|
||
{
|
||
id: 'legend-a11y-keys',
|
||
component: 'library/components/legends/SemanticTreeLegend.vue',
|
||
marker: 'legend.a11y',
|
||
landed: 'c4c60a4 and earlier',
|
||
why: 'accessible names + aria-expanded on the legend row caret and clear chip',
|
||
},
|
||
{
|
||
// A CONTROL-FLOW fix has no literal to grep, so this entry uses REGEX markers (see `has` below).
|
||
// The built shape keeps its structure through minification even though names change:
|
||
// unguarded const o=e.getRootNode();if(!(o instanceof ShadowRoot))
|
||
// guarded if(!e)return;const o=e.getRootNode();if(!(o instanceof ShadowRoot))
|
||
// The `instanceof ShadowRoot))return` tail pins it to ensureEngineCSS: useShadowStyles has its
|
||
// own `if(!n)return;const i=n.getRootNode();if(!(i instanceof ShadowRoot)||…` — which matched the
|
||
// first version of this regex and would have counted a rebuilt bundle as fixed WITHOUT the guard.
|
||
// The trailing `)return` is ensureEngineCSS's early exit; useShadowStyles continues with `||`.
|
||
// Proven both ways on a rebuilt doxa-simple-map: guarded source matches, guard stripped does not.
|
||
id: 'map-unmount-guard',
|
||
component: 'library/composables/useMapInstance.js',
|
||
sourceRe: /if \(!element\) return\n\s*const root = element\.getRootNode\(\)/,
|
||
builtRe: /if\(!([\w$]+)\)return;const ([\w$]+)=\1\.getRootNode\(\);if\(!\(\2 instanceof ShadowRoot\)\)return/,
|
||
// ★ NOT EVERY CONSUMER COMPILES THE GUARDED FUNCTION. ensureEngineCSS is only called by
|
||
// initializeMap; a bundle that imports pieces of useMapInstance (ensureEngineScript, engineGlobal…)
|
||
// but never initializeMap gets it TREE-SHAKEN AWAY, so the guard can never appear there — and the
|
||
// race cannot happen there either. Reported by upg100 (my-upg-100-list) 2026-09-25: 3 of 12
|
||
// bundles were false negatives. `staleRe` is the UNGUARDED shape: a bundle is MISSING the fix only
|
||
// if it still compiles ensureEngineCSS without the guard; with neither shape it is NOT AFFECTED.
|
||
staleRe: /const ([\w$]+)=([\w$]+)\.getRootNode\(\);if\(!\(\1 instanceof ShadowRoot\)\)return/g,
|
||
marker: 'if (!element) return (ensureEngineCSS)',
|
||
landed: '2026-09-25',
|
||
why: 'a map whose pane unmounts mid-initialise no longer throws "reading getRootNode" (check-map-unmount-mid-init)',
|
||
},
|
||
{
|
||
// The RTL text plugin now installs for MapLibre too, from useMapInstance at the moment an engine
|
||
// exists (a bundle entry runs before a lazily-injected MapLibre). The built marker is the keyless
|
||
// jsDelivr URL, a string literal that survives minification. staleRe is initializeMap's own error
|
||
// message: a bundle that compiles initializeMap WITHOUT the URL is stale; a bundle that compiles
|
||
// neither is not affected (it never boots a map).
|
||
id: 'rtl-text-maplibre',
|
||
component: 'library/composables/useMapInstance.js',
|
||
sourceRe: /installRtlTextPlugin\(gl\)/,
|
||
builtRe: /cdn\.jsdelivr\.net\/npm\/@mapbox\/mapbox-gl-rtl-text/,
|
||
staleRe: /GL JS script to your page/g,
|
||
marker: 'installRtlTextPlugin(gl) (MapLibre RTL shaping)',
|
||
landed: '2026-09-25',
|
||
why: 'Arabic, Hebrew and Persian map labels are shaped on MapLibre (keyless) maps, not drawn as disconnected glyphs',
|
||
},
|
||
{
|
||
// Basemap place names follow the reader's language (maps/localize-labels.js), applied from
|
||
// useMapInstance on every style.load. The built marker is the coalesce expression's literal
|
||
// prefix, which survives minification; staleRe is initializeMap's own error string (same logic
|
||
// as rtl-text-maplibre: a bundle that compiles initializeMap without the marker is stale).
|
||
id: 'basemap-labels-localized',
|
||
component: 'library/composables/useMapInstance.js',
|
||
sourceRe: /localizeBasemapLabels\(map\.value, readerLang\(\)\)/,
|
||
builtRe: /\["coalesce",\["get","name:"/,
|
||
staleRe: /GL JS script to your page/g,
|
||
marker: 'localizeBasemapLabels(map.value, readerLang()) (basemap labels localized)',
|
||
landed: '2026-09-25',
|
||
why: 'country, city and street names on the basemap render in the reader\'s language (Mapbox setLanguage, MapLibre name:<lang>)',
|
||
},
|
||
]
|
||
// A marker is a literal (`marker`, matched with includes) unless the entry supplies regexes.
|
||
const inSource = (m, src) => (m.sourceRe ? m.sourceRe.test(src) : src.includes(m.marker))
|
||
const inBuilt = (m, js) => (m.builtRe ? m.builtRe.test(js) : js.includes(m.marker))
|
||
// With a staleRe: does the bundle compile the OLD shape — an occurrence NOT immediately preceded by
|
||
// the guard? (The guarded form contains the old shape as a substring, hence the look-behind.)
|
||
const hasStale = (m, js) => {
|
||
if (!m.staleRe) return true
|
||
for (const x of js.matchAll(new RegExp(m.staleRe.source, 'g'))) if (!/return;$/.test(js.slice(Math.max(0, x.index - 24), x.index))) return true
|
||
return false
|
||
}
|
||
|
||
if (!existsSync(TREE)) {
|
||
console.log(`SHARED-REACHED-BUNDLES: SKIP — no built tree at ${TREE}.`)
|
||
console.log(' Build first (bun run tree). Absence is not a pass.')
|
||
process.exit(0)
|
||
}
|
||
|
||
// WHICH BUNDLES CONSUME A COMPONENT IS DERIVED, NOT LISTED. A hardcoded consumer list goes stale
|
||
// the moment someone adopts a shared component, and a gate that silently stops covering a new
|
||
// consumer is worse than no gate. Resolve it from the imports instead.
|
||
const consumersOf = (componentPath) => {
|
||
const name = componentPath.split('/').pop() // PosterWall.vue
|
||
const out = new Set()
|
||
const designs = resolve(REPO, 'plugin-designs')
|
||
if (!existsSync(designs)) return out
|
||
const walk = (dir, depth = 0) => {
|
||
if (depth > 4) return
|
||
for (const e of readdirSync(dir)) {
|
||
if (e === 'node_modules' || e.startsWith('.')) continue
|
||
const p = join(dir, e)
|
||
let st
|
||
try { st = statSync(p) } catch { continue }
|
||
if (st.isDirectory()) { walk(p, depth + 1); continue }
|
||
if (!/\.(vue|js|mjs)$/.test(e)) continue
|
||
let text
|
||
try { text = readFileSync(p, 'utf8') } catch { continue }
|
||
// ⚠ REQUIRE AN ACTUAL IMPORT, NOT A MENTION. This was a plain substring test, and a
|
||
// substring cannot tell an import from a comment ABOUT the component.
|
||
// MEASURED 2026-09-25: it reported research-dashboard as a stale consumer of
|
||
// SemanticTreeLegend for three ticks, and I routed that "drift" to its owner as work to do.
|
||
// It was a FALSE POSITIVE. research-dashboard imports the component ZERO times and mentions
|
||
// it four times in PROSE — DashboardFacetRail.vue:3 says "the floating @map
|
||
// SemanticTreeLegend is DROPPED", with the root cause it was dropped for. A bundle that
|
||
// deliberately removed a component is CORRECT to lack its translation keys; the gate was
|
||
// reading the comment explaining the removal as evidence of use.
|
||
// A gate that manufactures work for another agent is worse than one that misses drift.
|
||
const importPath = componentPath.replace(/^library\//, '@map/')
|
||
const stem = name.replace(/\.vue$/, '')
|
||
const isImport = new RegExp(
|
||
// import ... from '<path or .../Name.vue>' | import('<...>')
|
||
`(?:import[^;\\n]*?from\\s*|import\\s*\\(\\s*)['"\`][^'"\`]*(?:${
|
||
importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||
}|/${stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.vue)['"\`]`,
|
||
).test(text)
|
||
if (isImport) {
|
||
// the BUNDLE is the first directory under plugin-designs/
|
||
const rel = p.slice(designs.length + 1)
|
||
out.add(rel.split('/')[0])
|
||
}
|
||
}
|
||
}
|
||
walk(designs)
|
||
return out
|
||
}
|
||
|
||
console.log('SHARED-REACHED-BUNDLES')
|
||
const fails = []
|
||
const staleMarkers = []
|
||
|
||
for (const m of MARKERS) {
|
||
const srcPath = resolve(REPO, m.component)
|
||
if (!existsSync(srcPath)) {
|
||
staleMarkers.push(`${m.id}: source ${m.component} no longer exists`)
|
||
continue
|
||
}
|
||
const src = readFileSync(srcPath, 'utf8')
|
||
if (!inSource(m, src)) {
|
||
// THE MARKER IS GONE FROM SOURCE. That is a stale ENTRY, not a stale bundle — failing every
|
||
// consumer here would blame the bundles for a change that was legitimately reverted.
|
||
staleMarkers.push(`${m.id}: marker "${m.marker}" is no longer in ${m.component} — `
|
||
+ 'the change was reverted or reworded; update or drop this entry')
|
||
continue
|
||
}
|
||
|
||
const consumers = [...consumersOf(m.component)].sort()
|
||
const have = []
|
||
const missing = []
|
||
const unbuilt = []
|
||
const unaffected = []
|
||
for (const bundle of consumers) {
|
||
const built = join(TREE, bundle, `${bundle}.js`)
|
||
if (!existsSync(built)) { unbuilt.push(bundle); continue }
|
||
const js = readFileSync(built, 'utf8')
|
||
if (inBuilt(m, js)) have.push(bundle)
|
||
else if (hasStale(m, js)) missing.push(bundle)
|
||
else unaffected.push(bundle)
|
||
}
|
||
|
||
console.log(` ${m.id} — ${m.why}`)
|
||
console.log(` landed ${m.landed}; ${have.length} of ${have.length + missing.length} built `
|
||
+ `consumer(s) carry it${unaffected.length ? ` (${unaffected.length} not affected)` : ''}${unbuilt.length ? `, ${unbuilt.length} not built` : ''}`)
|
||
if (have.length) console.log(` ✓ ${have.join(', ')}`)
|
||
if (missing.length) console.log(` ✗ ${missing.join(', ')}`)
|
||
if (unbuilt.length) console.log(` · not built: ${unbuilt.join(', ')}`)
|
||
if (unaffected.length) console.log(` – not affected (the changed code is not compiled in): ${unaffected.join(', ')}`)
|
||
|
||
if (missing.length) {
|
||
fails.push(`${m.id}: ${missing.length} bundle(s) do NOT carry "${m.why}" — ${missing.join(', ')}`)
|
||
}
|
||
}
|
||
|
||
if (staleMarkers.length) {
|
||
console.log('\n marker entries needing attention (NOT bundle failures):')
|
||
for (const s of staleMarkers) console.log(` · ${s}`)
|
||
}
|
||
|
||
if (fails.length) {
|
||
console.error(`\nSHARED-REACHED-BUNDLES: FAIL — ${fails.length} shared change(s) have not reached every consumer.`)
|
||
for (const f of fails) console.error(' - ' + f)
|
||
console.error('\n THIS IS A REBUILD, NOT A CODE CHANGE. The fix is already in library/; each bundle')
|
||
console.error(' ships its own COMPILED copy, so a shared change is only as deployed as the last')
|
||
console.error(' rebuild of each consumer. Rebuild the named bundles:')
|
||
console.error(' BUNDLE=<name> bun x vite build && bun run tree')
|
||
console.error('\n Every other gate measures the rendered behaviour of ONE leaf, and a leaf whose')
|
||
console.error(' bundle is stale renders the OLD behaviour perfectly — so they all stay green.')
|
||
process.exit(1)
|
||
}
|
||
console.log('\nSHARED-REACHED-BUNDLES: PASS — every tracked shared change reached every built consumer.')
|