One page loaded the same bundle twice and nothing said so

Every plugin ships as a self-contained IIFE, so library/utils/embedScript.js is
compiled separately into each one: eleven bundles on main each carry their own
copy. Its cache was a module-scoped const, which made "load exactly once" true
per BUNDLE rather than per page — the opposite of what the file's own header
claimed. Two shells nesting the same child (the Research Gallery and the Plugin
Studio both mounting doxa-research-map) each injected it.

Nothing failed, which is why it survived. The second request came from the HTTP
cache, so no failed request and no extra network row; the bundle then executed in
full a second time — a second Vue, a second vue-i18n, a second pinia, ~375 KiB
gzip of work — and its guarded customElements.define quietly declined at the very
end. Where the two URLs are different versions of one plugin, whichever executed
first owns the tag and the other is discarded in silence.

The cache now lives on globalThis, the only scope that matches the claim, under a
versioned key so a page mixing old and new bundles falls back to a cache each
rather than misreading a shape it does not expect. Entries are keyed by the
resolved href, because the callers do not pass the same string for one file.
A <script> already in the document is adopted, which is what covers bundles built
before this and host pages that write their own tag; that resolves immediately,
and it is safe only because every caller here follows with a wait on the element
itself — the note in the file says so, since a new caller must keep that pairing.

claimTag is a diagnostic, not a guard: by the time a second bundle arrives the
define is long since won, so the thing that was missing was anyone saying so.

The check imports the module twice under different URLs — two module instances
sharing one globalThis, the same shape as two IIFEs on one page. It asserts the
second caller receives the FIRST one's promise, not merely that one script tag
exists: mutation-testing showed the tag count alone is satisfied by the adoption
path, so it would have passed with the original bug restored.
This commit is contained in:
Doxa Maps 2026-09-28 19:15:51 +00:00
parent 16ff7e719b
commit 7efe106513
5 changed files with 294 additions and 13 deletions

View file

@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* check-script-registry — one bundle script executes ONCE PER PAGE, not once per bundle that asks.
*
* WHY THIS TEST EXISTS AND WHY IT IS SHAPED LIKE THIS. Every plugin ships as a self-contained IIFE,
* so library/utils/embedScript.js is compiled separately into each one: eleven bundles on
* origin/main each carry their own copy. A module-scoped cache in that file is therefore one cache
* PER BUNDLE, and two shells on one page each loaded the same child — fetched from the HTTP cache,
* so no failed request and no extra network row, then the whole bundle EXECUTED a second time and
* its guarded `customElements.define` quietly declined. Nothing threw. Nothing warned.
*
* The test reproduces the packaging rather than describing it: it imports the module TWICE under
* two different URLs, which in ESM yields two independent module instances sharing one globalThis —
* structurally the same thing as two IIFEs on one page.
*
* ★ THERE ARE TWO LAYERS AND THIS FILE TESTS THEM SEPARATELY, BECAUSE MUTATION-TESTING SHOWED THEY
* MASK EACH OTHER. Reverting the page registry to module scope did NOT change the number of
* <script> tags: the second bundle still found the first one's tag in the DOM and adopted it. So
* "one tag on the page" is evidence for the DOM layer, not for the registry, and asserting only
* that would have let the original bug back in wearing the fix's clothes. The registry is asserted
* by PROMISE IDENTITY instead — the second caller must receive the very promise the first is
* waiting on, which nothing but a page-scoped cache can hand it, and which is the difference
* between the second shell waiting for the real load and merely assuming it happened.
*
* Headless, no network, no browser: the DOM is a stub small enough to read, and it models the one
* browser behaviour the code depends on — assigning a relative `src` and reading back an absolute
* one. Exit 0 pass · 1 fail.
*/
import { resolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MODULE_URL = pathToFileURL(resolve(ROOT, 'library/utils/embedScript.js')).href
const REGISTRY_KEY = '__dxMapsScriptRegistry1'
const BASE = 'https://host.example/page/'
const fails = []
const ok = (what, cond) => { if (!cond) fails.push(what) }
const eq = (what, got, want) => { if (got !== want) fails.push(`${what}\n got: ${JSON.stringify(got)}\n want: ${JSON.stringify(want)}`) }
// ── the DOM stub ──────────────────────────────────────────────────────────────
let appended, planted, failNext, warnings
function makeScript() {
const o = { _src: '', type: '', onload: null, onerror: null, setAttribute(k, v) { o[k] = v } }
// A real <script> resolves a relative src against the document on assignment and reads back
// absolute. existingScriptFor() compares against that absolute form, so the stub must too —
// otherwise this test would pass while the browser found nothing.
Object.defineProperty(o, 'src', { get: () => o._src, set: (v) => { o._src = new URL(v, BASE).href } })
return o
}
function resetDom() {
appended = []; planted = []; failNext = false; warnings = []
globalThis.document = {
baseURI: BASE,
createElement: () => makeScript(),
querySelectorAll: () => planted.concat(appended),
head: {
appendChild(s) {
appended.push(s)
queueMicrotask(() => { if (failNext) { failNext = false; s.onerror && s.onerror() } else s.onload && s.onload() } )
},
},
}
globalThis.console = { ...console, warn: (m) => warnings.push(String(m)) }
}
function resetPage() { delete globalThis[REGISTRY_KEY]; resetDom() }
// Two module instances = two bundles on one page.
resetPage()
const A = await import(MODULE_URL + '?bundle=doxa-maps-spa')
const B = await import(MODULE_URL + '?bundle=plugin-studio')
ok('the two imports really are separate module instances', A !== B && A.loadScriptOnce !== B.loadScriptOnce)
const CHILD = '/dx-maps/doxa-research-map/doxa-research-map.js'
// 1. THE BUG: the second bundle must not execute the child again.
resetPage()
await A.loadScriptOnce(CHILD)
eq('first bundle injects the child', appended.length, 1)
await B.loadScriptOnce(CHILD)
eq('SECOND BUNDLE REUSES IT — one <script> on the page, not two', appended.length, 1)
// 1b. …and it reused the FIRST BUNDLE'S PROMISE, which is the part only the page registry can do.
// Same tick, no await between, so the DOM-adoption path cannot be what answers B.
resetPage()
const pA = A.loadScriptOnce(CHILD)
const pB = B.loadScriptOnce(CHILD)
ok('the second bundle waits on the first bundle\'s load, not its own guess', pA === pB)
await Promise.all([pA, pB])
eq('and still only one <script>', appended.length, 1)
// 2. the test can still see the old behaviour, so case 1 is not vacuous.
resetPage()
await A.loadScriptOnce(CHILD)
resetPage() // wipe the page registry AND the DOM: this is per-bundle caching
await B.loadScriptOnce(CHILD)
eq('with no shared page registry the second load DOES inject (what the bug looked like)', appended.length, 1)
// 3. the two shells do not pass the same string; the file is still one file.
resetPage()
await A.loadScriptOnce(CHILD)
await B.loadScriptOnce('https://host.example' + CHILD)
eq('relative and absolute spellings of one file share one entry', appended.length, 1)
resetPage()
await A.loadScriptOnce(CHILD)
await B.loadScriptOnce('/dx-maps/people-groups-grid/people-groups-grid.js')
eq('and a genuinely different bundle is NOT swallowed', appended.length, 2)
// 4. a tag from a bundle built before this registry, or written by the host page, is honoured.
resetPage()
const foreign = makeScript(); foreign.src = CHILD; planted.push(foreign)
await A.loadScriptOnce(CHILD)
eq('an existing <script> for the same file is adopted, not duplicated', appended.length, 0)
// 5. the dev-server ES-module rule (measured 2026-09-22) must survive the rewrite.
resetPage()
await A.loadScriptOnce('/plugin-designs/stack-demo/index.js')
eq('a dev entry is still type=module', appended.length === 1 && appended[0].type, 'module')
resetPage()
await A.loadScriptOnce(CHILD)
eq('a built IIFE is still a classic script', appended.length === 1 && appended[0].type, '')
// 6. an empty url stays a no-op (a <script src=""> refetches the page).
resetPage()
await A.loadScriptOnce('')
eq('empty url injects nothing', appended.length, 0)
// 7. a failed load must not poison the cache: a retry has to be able to try again.
resetPage()
failNext = true
let threw = false
await A.loadScriptOnce(CHILD).catch(() => { threw = true })
ok('a failed load rejects', threw)
planted = []; appended = [] // the failed tag is gone from the page
await A.loadScriptOnce(CHILD)
eq('and a retry injects again rather than resolving a dead promise', appended.length, 1)
// 8. SRI still reaches the element.
resetPage()
await A.loadScriptOnce(CHILD, { integrity: 'sha384-abc' })
eq('integrity is applied', appended.length === 1 && appended[0].integrity, 'sha384-abc')
// 9. claimTag: silent for the normal case, audible for the real collision.
resetPage()
A.claimTag('doxa-research-map', CHILD)
B.claimTag('doxa-research-map', CHILD)
eq('two shells nesting the SAME leaf is normal and silent', warnings.length, 0)
A.claimTag('people-groups-grid', '/dx-maps/people-groups-grid/people-groups-grid.js')
B.claimTag('people-groups-grid', '/dx-maps/big-picture/big-picture.js')
eq('two bundles shipping one tag warns once', warnings.length, 1)
ok('and the warning names both bundles', !!warnings[0] && warnings[0].includes('people-groups-grid.js') && warnings[0].includes('big-picture.js'))
A.claimTag('', CHILD); A.claimTag('x', '')
eq('a missing tag or url is ignored rather than recorded', warnings.length, 1)
// 10. the registry is shared across instances by identity, not by copy.
resetPage()
await A.loadScriptOnce(CHILD)
ok('both instances see one registry object', globalThis[REGISTRY_KEY] && Object.keys(globalThis[REGISTRY_KEY].loaded).length === 1)
if (fails.length) {
console.error('SCRIPT-REGISTRY: FAIL')
for (const f of fails) console.error(` ✗ ${f}`)
process.exit(1)
}
console.log('SCRIPT-REGISTRY: PASS — two bundles on one page load a child once, different spellings of one file share an entry, a foreign <script> is adopted, the dev-module rule and SRI survive, a failed load can be retried, and one tag shipped by two bundles is audible.')

View file

@ -80,7 +80,7 @@
import { defineComponent } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { bundleTreeRoot } from '@map/utils/embedBase.js'
import { loadScriptOnce } from '@map/utils/embedScript.js'
import { loadScriptOnce, claimTag } from '@map/utils/embedScript.js'
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
// THE BUG (Driver-reported 2026-09-23, verbatim console text):
@ -243,6 +243,9 @@ export default defineComponent({
injectScriptOnce(sec.src)
.then(() => waitForDefine(sec.element))
.then(() => {
// Name the bundle this tag actually came from, so a page carrying two bundles that both
// ship <sec.element> says so once instead of silently running whichever executed first.
claimTag(sec.element, resolveBundleSrc(sec.src))
if (!customElements.get(sec.element)) return
const el = document.createElement(sec.element)
const attrs = sec.attrs || {}

View file

@ -267,7 +267,7 @@ import { tStatic } from '../i18n/index.js'
const tr = (key, en) => tStatic(key) || en
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { icon as localIcon } from '@map/layouts/app-wrapper-material/icons.js'
import { loadScriptOnce } from '@map/utils/embedScript.js'
import { loadScriptOnce, claimTag } from '@map/utils/embedScript.js'
// Inject a bundle script exactly once per URL, through the ONE shared loader (library/utils/embedScript.js).
// This layout's private copy lacked the dev-server ES-module rule, so a bundle nested through it under
@ -735,6 +735,8 @@ export default defineComponent({
injectScriptOnce(row.src)
.then(() => waitForDefine(row.element))
.then(() => {
// See StackLayout: name the owning bundle so a duplicate-tag page is audible, not silent.
claimTag(row.element, row.src)
slot.innerHTML = ''
const el = document.createElement(row.element)
// props-in-advance: set each attr as BOTH a property (if upgraded) and an attribute, so the

View file

@ -10,13 +10,89 @@
*
* Moved verbatim; the comments below are the originals, because each records a measurement.
*
* ★ ONE BEHAVIOUR CHANGE, AND IT IS AN IMPROVEMENT: `_loaded` is now shared. Two shells on one page
* previously kept separate caches and would each load the same bundle. Now the second one reuses
* the first's promise. Same result, one network request instead of two.
* ★ "EXACTLY ONCE" WAS ONLY EVER TRUE INSIDE ONE BUNDLE, AND THAT IS THE BUG THIS FILE NOW FIXES.
* An earlier version of this header claimed that sharing `_loaded` here meant "two shells on one
* page ... now the second one reuses the first's promise". That was wrong, and the reason is the
* packaging, not the code: every plugin ships as a self-contained IIFE with `inlineDynamicImports`,
* so THIS MODULE IS COMPILED SEPARATELY INTO EACH BUNDLE and a module-scoped `const _loaded = {}`
* is one cache PER BUNDLE, not per page. MEASURED on origin/main's shipped tree 2026-09-28:
* 11 built bundles each carry their own copy.
*
* What that cost, and why it was invisible: every entry file guards its registration with
* `if (!customElements.get(tag)) customElements.define(tag, …)`, so the second load did NOT throw.
* It fetched from the HTTP cache (so the network panel looked innocent), then EXECUTED THE WHOLE
* BUNDLE a second time — a second Vue, a second vue-i18n, a second pinia, ~375 KiB gzip of work for
* doxa-research-map — and only at the very end declined to define the tag it had just built
* everything for. Nothing warned. The page simply ran twice as much script as it needed.
* Worse than the waste: if the two URLs are different VERSIONS of the same plugin, whichever
* executed first owns the tag and the other is silently discarded.
*
* So the cache now lives on the PAGE (`globalThis`), which is the only scope that matches the
* claim. See `registry()` for why the key is versioned.
*/
/**
* Load a bundle exactly once per URL; resolves after its script has executed.
* The page-wide registry. Bundles cannot share a module, but they all share one global object.
*
* ★ THE KEY CARRIES A VERSION ON PURPOSE. A page can mix bundles built weeks apart, so an old
* bundle and a new one must never read each other's registry through a shape either of them does
* not expect. A changed shape therefore gets a NEW key, and the two generations fall back to a
* cache each — today's behaviour, which merely wastes work — instead of one of them misreading the
* other's and skipping a load that never happened. Separate is recoverable; corrupt is not.
*
* `globalThis` rather than `window` so this file is loadable under node, where the gates run it.
*/
const REGISTRY_KEY = '__dxMapsScriptRegistry1'
function registry() {
const g = typeof globalThis !== 'undefined' ? globalThis : self
return g[REGISTRY_KEY] || (g[REGISTRY_KEY] = { loaded: Object.create(null), owners: Object.create(null) })
}
/**
* Resolve to the absolute URL the browser will actually request.
*
* ★ THIS IS WHAT MAKES TWO SHELLS AGREE. They do not pass the same string: StackLayout anchors a
* sibling bundle to the tree root first (resolveBundleSrc), VerticalTabsLayout passes the row's src
* untouched, and a host page may configure an absolute CDN URL. `/dx-maps/x/x.js` and
* `https://host/dx-maps/x/x.js` are one file and must be one cache entry, so the KEY is the
* resolved href — not the string the caller happened to hold.
*/
function absoluteHref(url) {
try {
if (typeof document !== 'undefined' && document.baseURI) return new URL(url, document.baseURI).href
if (typeof location !== 'undefined' && location.href) return new URL(url, location.href).href
} catch (_) { /* a non-URL string (a test stub, a bare name): key it as given */ }
return url
}
/**
* A <script> for this exact file that somebody else already put in the document.
*
* ★ WHY ADOPT ONE INSTEAD OF IGNORING IT. The registry only helps bundles that contain this
* version of this file. A page can also carry a bundle built BEFORE it, or a host page that writes
* its own <script> tag for a plugin — neither will ever touch `registry().loaded`. Reading the DOM
* is the one signal that is true regardless of who did the loading.
*
* We cannot know whether a foreign tag has finished executing (a script that already ran will never
* fire another onload), so adopting it resolves immediately. That is SAFE HERE FOR A CHECKED
* REASON, not by assumption: every caller in this repo follows loadScriptOnce with a wait on the
* custom element itself — doxa-maps-spa.vue:1405 and plugin-studio.vue:1138/1178 chain
* whenTagDefined(), StackLayout.vue:244 and VerticalTabsLayout.vue:736 chain their own
* waitForDefine() — and that wait, with its timeout, is the authoritative answer to "can I mount
* this yet". Resolving early hands the question to the component that can actually answer it.
* A NEW CALLER MUST KEEP THAT PAIRING; loadScriptOnce alone has never been proof the tag exists,
* which is the point the dev-server note below was written for.
*/
function existingScriptFor(href) {
if (typeof document === 'undefined' || !document.querySelectorAll) return null
const tags = document.querySelectorAll('script[src]')
for (let i = 0; i < tags.length; i++) if (tags[i].src === href) return tags[i]
return null
}
/**
* Load a bundle exactly once per URL PER PAGE; resolves after its script has executed.
*
* ★ A CLASSIC SCRIPT FIRES onload AND THEN FAILS, for a dev-server bundle. Measured 2026-09-22:
* pointing a plain <script> at a dev entry (/plugin-designs/<bundle>/index.js, a real ES MODULE)
@ -34,24 +110,57 @@
* with 0 rows rendered. VerticalTabsLayout's private copy LACKED this rule until it moved here, so a
* bundle nested through it on the dev server never defined its tag.
*
* ⚠ The module test runs against the URL THE CALLER GAVE, not the resolved href: `/plugin-designs/…`
* is root-relative and absolutising it first would destroy the very prefix being matched.
*
* An empty url is a no-op: a <script src=""> would re-fetch the page itself.
*
* SRI is optional (attrs.integrity).
* SRI is optional (attrs.integrity). An adopted foreign tag keeps whatever integrity IT was given —
* we do not restate ours on someone else's element, because the bytes it loaded were already
* checked, or already not, before we ever saw it.
*/
const _loaded = {}
export function loadScriptOnce(url, attrs) {
if (!url) return Promise.resolve()
if (_loaded[url]) return _loaded[url]
_loaded[url] = new Promise((ok, fail) => {
const reg = registry()
const href = absoluteHref(url)
if (reg.loaded[href]) return reg.loaded[href]
if (existingScriptFor(href)) return (reg.loaded[href] = Promise.resolve())
reg.loaded[href] = new Promise((ok, fail) => {
const s = document.createElement('script')
s.src = url
if (/^\/plugin-designs\//.test(url) || /\/index\.js(\?|$)/.test(url)) s.type = 'module'
if (attrs) for (const k in attrs) if (attrs[k]) s.setAttribute(k, attrs[k])
s.onload = () => ok()
s.onerror = () => { delete _loaded[url]; fail(new Error('failed to load ' + url)) }
s.onerror = () => { delete reg.loaded[href]; fail(new Error('failed to load ' + url)) }
document.head.appendChild(s)
})
return _loaded[url]
return reg.loaded[href]
}
/**
* Record which bundle a tag came from, and say so when two disagree.
*
* ★ THIS IS A DIAGNOSTIC, NOT A GUARD, and deliberately so. `customElements.define` is one-shot per
* page: by the time a second bundle gets here the first has already won, and there is nothing left
* to prevent — the damage was done at build time by two bundles shipping the same element. What was
* missing is that NOBODY SAID SO. The guarded define (`if (!customElements.get(tag))`) swallows the
* collision so completely that a page can render the wrong version of a plugin and look healthy.
*
* Call it after the tag is confirmed defined, with the URL it was loaded from. Same URL twice is the
* normal case (two shells nesting the same leaf) and is silent.
*/
export function claimTag(tag, url) {
if (!tag) return
const reg = registry()
const href = url ? absoluteHref(url) : ''
if (!href) return
const prev = reg.owners[tag]
if (!prev) { reg.owners[tag] = href; return }
if (prev === href) return
if (typeof console !== 'undefined' && console.warn) {
console.warn('[dx-maps] <' + tag + '> is shipped by two bundles on this page: ' + prev +
' defined it, ' + href + ' executed and was discarded. The page is running the first one.')
}
}
// Wait for a tag to ACTUALLY register. loadScriptOnce resolving is not proof the element exists (see

View file

@ -235,6 +235,7 @@
"check:settled": "node internal/settled-tree-gate.mjs",
"check:provide-shadowing": "node internal/check-provide-shadowing.mjs",
"check:element-bootstrap": "node internal/check-element-bootstrap.mjs",
"check:script-registry": "node internal/check-script-registry.mjs",
"check:map-menu-anchor": "node internal/check-map-menu-anchor.mjs",
"check:legend-top-offset": "node internal/check-legend-top-offset.mjs",
"check:mercator-slice": "node internal/check-mercator-slice.mjs",
@ -288,7 +289,7 @@
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:script-registry",
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -366,6 +367,7 @@
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
"check:dashboard-geocoder-provider": "the research dashboard leaf at ?provider=maplibre makes ZERO requests to api.mapbox.com, and its search bars still suggest (built tree; browser gate). It was kept out of gates:post while the generated leaf hard-linked the Mapbox engine; the leaf now loads only the engine it is asked for.",
"check:dep-names": "every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
"check:script-registry": "one bundle script executes ONCE PER PAGE. Every plugin is a self-contained IIFE, so library/utils/embedScript.js is compiled into each one and a module-scoped cache was one cache per BUNDLE: two shells on a page each loaded the same child, which re-executed a whole bundle (a second Vue, i18n and pinia) before its guarded customElements.define silently declined. The check imports the module twice under different URLs — two module instances, one globalThis, the same shape as two IIFEs — and asserts the second caller receives the FIRST ONE'S PROMISE, because counting script tags alone is satisfied by the DOM-adoption path and would not catch a regression. Headless; in gates:pre.",
"check:describe-group": "the prayer packet's generated profile sentence (library/packets/describeGroup.js): each GSEC level states its own fact, and an index-ordered language name (\"Arabic, Moroccan\") is un-inverted by library/utils/languageLabel.js without shredding a comma inside parentheses, the 13 names the old split printed reversed. A catalogue arm renders every GSEC level from the REAL packet-strings.json in en and ar and fails on a blank sentence. Headless; in gates:pre.",
"check:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
"check:done-evidence": "a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",