Register check:provider-for

The gate proves providerFor(map) answers the same as the raw window.maplibregl sniffs it replaced (C3), on
both engines and on the ambiguous inputs where the originals fell through to mapbox. It also proves the
obvious isMapboxMap() rewrite would have inverted that fall-through. Registered in scripts (alphabetical,
with its //scripts note) and gates:post; source-only. From regions-plugin 332dcf74; package.json edited by
hand, not taken whole.
This commit is contained in:
Doxa Maps 2026-09-26 17:24:01 +00:00
parent 8b859cf645
commit dcd25a0a4f
2 changed files with 114 additions and 1 deletions

View file

@ -0,0 +1,111 @@
#!/usr/bin/env node
/* check-provider-for.mjs — run as `bun run check:provider-for`. Source-only, headless, ~0.1s.
Placed in internal/ with studio's explicit authorisation for this one gate (END-STU-C3AB): it spans
library/ plus three separate plugins, so no single plugin's scripts/ folder owns it. */
// C3: do the library helpers behave like the raw-global code they replace, on BOTH engine shapes AND
// on the ambiguous case that is neither?
//
// Same condition the shepherd set for C2: an equivalence gate, old pattern vs new, both engines. The
// three migrated sites, verbatim in shape from main 1a761741:
// A1 pplrData.js:115 window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox'
// A2 research-map.vue:1322 !!(window.maplibregl && map.value instanceof window.maplibregl.Map)
// B all-people-groups:298 mapProvider === 'maplibre' ? window.maplibregl : window.mapboxgl
//
// ★ THE AMBIGUOUS CASE IS THE POINT OF THIS GATE, and it nearly cost a silent behaviour change.
// My first plan was to rewrite A1 and A2 in terms of the EXISTING isMapboxMap(). That is wrong in a
// way no per-engine test would have caught: both originals test for MAPLIBRE and fall through to
// mapbox/false, so a stub, a destroyed map, or a page where neither global loaded reads as 'mapbox'.
// An isMapboxMap() rewrite inverts the fall-through to 'maplibre'/true — changing what a diagnostic
// field reports and which attribution a poster PRINTS. Hence providerFor() returns undefined and each
// call site keeps its own default visible. Changing a default is not a curation migration.
//
// ★ AND THE PREMISE I HAD WRONG BEFORE THAT: I recorded in SPEC-LIBRARY-CURATION that "engineFor
// returns a NAME". It does not — it returns the engine NAMESPACE OBJECT (`return ml` / `return mb`).
// Two of my four bucket-D "must not migrate" conclusions rested on that false premise. Corrected in
// the spec; the lesson is that a helper's RETURN TYPE is a fact to read, not to infer from its name.
import { readFileSync } from 'node:fs'
import { providerFor, engineFor, isMapboxMap } from '../library/composables/useMapProvider.js'
// ★ engineGlobal CANNOT BE IMPORTED HERE, and the workaround matters. It lives in useMapInstance.js,
// whose import chain reaches library/i18n/index.js, which calls `import.meta.glob` -- a VITE
// transform, so plain Node throws "(intermediate value).glob is not a function". C2's gate never hit
// this because useMapProvider.js has no such chain.
// So this EXTRACTS AND EXECUTES engineGlobal's REAL SOURCE LINES rather than a paraphrase of them.
// Re-typing a three-line function into this gate would test my copy, and a copy silently drifts from
// the thing it stands for -- the same trap as a tool copied to a new filename that no-ops and exits 0.
// CDN is stubbed because engineGlobal reads only `.global` from the ENGINES table; the urls are
// irrelevant to it, which is checkable in the extracted text printed below on failure.
const umiSrc = readFileSync(new URL('../library/composables/useMapInstance.js', import.meta.url), 'utf8')
const enginesBlock = /const ENGINES = \{[\s\S]*?\n\}/.exec(umiSrc)
const engineGlobalSrc = /export function engineGlobal\(provider\) \{[\s\S]*?\n\}/.exec(umiSrc)
if (!enginesBlock || !engineGlobalSrc) {
console.error(' FAIL - could not extract ENGINES / engineGlobal from useMapInstance.js -- the gate is NOT MEASURED, not passing')
process.exit(2)
}
const engineGlobal = new Function(
'const CDN = { mapboxGlJs:{url:""}, mapboxGlCss:{url:""}, maplibreGlJs:{url:""}, maplibreGlCss:{url:""} };\n'
+ enginesBlock[0] + ';\n'
+ engineGlobalSrc[0].replace(/^export /, '') + ';\n'
+ 'return engineGlobal;'
)()
let fails = 0
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
class MapboxMap {}
class MaplibreMap {}
globalThis.window = { mapboxgl: { Map: MapboxMap }, maplibregl: { Map: MaplibreMap } }
// the THREE old patterns, as they stood
const oldA1 = (map) => (window.maplibregl && window.maplibregl.Map && map instanceof window.maplibregl.Map ? 'maplibre' : 'mapbox')
const oldA2 = (map) => !!(window.maplibregl && map instanceof window.maplibregl.Map)
const oldB = (provider) => (provider === 'maplibre' ? window.maplibregl : window.mapboxgl)
// the NEW expressions, exactly as they now read at the call sites
const newA1 = (map) => providerFor(map) || 'mapbox'
const newA2 = (map) => providerFor(map) === 'maplibre'
const newB = (provider) => engineGlobal(provider)
// ── 1. A1 and A2 across every map shape, INCLUDING neither ──────────────────────────────────────
const CASES = [
['a Mapbox map', new MapboxMap()],
['a MapLibre map', new MaplibreMap()],
['AMBIGUOUS: a plain object that is neither', {}],
['AMBIGUOUS: null', null],
['AMBIGUOUS: undefined', undefined]
]
for (const [label, map] of CASES) {
ok(oldA1(map) === newA1(map), `A1 ${label}: old "${oldA1(map)}" === new "${newA1(map)}"`)
ok(oldA2(map) === newA2(map), `A2 ${label}: old ${oldA2(map)} === new ${newA2(map)}`)
}
// ── 2. B across every provider name, including the unrecognised one ─────────────────────────────
for (const p of ['maplibre', 'mapbox', 'auto', '', undefined, 'nonsense']) {
ok(oldB(p) === newB(p), `B provider=${JSON.stringify(p)}: both resolve to the same namespace (${oldB(p) === window.maplibregl ? 'maplibregl' : 'mapboxgl'})`)
}
// ── 3. THE INVERSION THIS GATE EXISTS TO PREVENT ────────────────────────────────────────────────
// Documented as an executable claim, not as prose: isMapboxMap() is NOT a drop-in for these sites.
const wouldHaveBeen = (map) => (isMapboxMap(map) ? 'mapbox' : 'maplibre')
ok(wouldHaveBeen({}) !== oldA1({}),
`PROOF: the isMapboxMap() rewrite I nearly shipped disagrees with the original on the ambiguous case (it says "${wouldHaveBeen({})}", the original says "${oldA1({})}")`)
ok(wouldHaveBeen(new MapboxMap()) === oldA1(new MapboxMap()) && wouldHaveBeen(new MaplibreMap()) === oldA1(new MaplibreMap()),
'and it agrees on BOTH real engines -- which is exactly why a per-engine-only gate would have passed it')
// ── 4. return TYPES, since inferring one from a name is what went wrong ─────────────────────────
ok(typeof providerFor(new MaplibreMap()) === 'string', 'providerFor returns a STRING name')
ok(providerFor(new MaplibreMap()) === 'maplibre' && providerFor(new MapboxMap()) === 'mapbox', 'and the right one per engine')
ok(providerFor({}) === undefined, 'and undefined for neither -- never a guessed default')
ok(engineFor(new MaplibreMap()) === window.maplibregl, 'engineFor returns the NAMESPACE OBJECT, not a name (the premise I had backwards)')
ok(typeof engineGlobal('maplibre') === 'object', 'engineGlobal returns a namespace OBJECT from a name')
ok(/ENGINES\[provider\]\?\.global \|\| ENGINES\.mapbox\.global/.test(engineGlobalSrc[0]),
'CONTROL: the EXTRACTED source really is the mapbox-defaulting version -- this is what makes the bucket-C fallback finding true, and it is read from the file, not remembered')
// ── 5. NEG CONTROLS: the comparisons above must be capable of failing ───────────────────────────
ok(oldA1(new MaplibreMap()) !== oldA1(new MapboxMap()), 'NEG CONTROL: A1 discriminates the two engines at all')
ok(oldB('maplibre') !== oldB('mapbox'), 'NEG CONTROL: B discriminates the two namespaces at all')
const brokenNewA1 = () => 'mapbox' // a helper that always says mapbox
ok(!CASES.every(([, m]) => oldA1(m) === brokenNewA1(m)),
'NEG CONTROL: a helper that always answered "mapbox" would FAIL this gate -- so passing means something')
console.log(fails ? `\nC3-EQUIVALENCE: ${fails} FAIL(s)` : '\nC3-EQUIVALENCE: all three migrated sites match the raw-global code they replace, on both engines and on every ambiguous input')
process.exit(fails ? 1 : 0)

View file

@ -166,6 +166,7 @@
"check:print-geometry": "node internal/check-print-geometry.mjs",
"check:pin-dim-slider": "node plugin-designs/doxa-research-map/scripts/check-pin-dim-slider.mjs",
"check:profiles-mount": "node internal/check-composites-mount.mjs --all-profiles",
"check:provider-for": "node internal/check-provider-for.mjs",
"check:provider-switch": "node internal/check-provider-switch.mjs",
"check:public-hygiene": "node internal/public-hygiene-check.mjs",
"check:published": "node internal/check-published-matches-build.mjs",
@ -215,7 +216,7 @@
"dev": "bun x vite",
"fix:sri": "node internal/fix-sri.mjs",
"gallery": "node internal/generate-gallery.mjs",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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",
"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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",
"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",
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
"library-index": "node internal/generate-library-index.mjs",
@ -362,6 +363,7 @@
"check:poster-print-fallback": "clicks the REAL poster print control with every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; SKIPs otherwise.",
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
"check:profiles-mount": "check:composites-mount over EVERY profile of every bundle and every preset instance (with its props), in en and ar: any setup, watcher or mount error fails it. Headless, ~40 s. A source mount: a build-only error (a minified name hoisted out of order) needs the built bundle and is not covered.",
"check:provider-for": "providerFor(map) must answer the same as the raw `map instanceof window.maplibregl.Map` sniffs it replaced (C3) - on BOTH engines AND on the AMBIGUOUS input that is neither, where the originals fell through to mapbox/false. Carries an executable proof that the obvious isMapboxMap() rewrite INVERTS that fall-through while agreeing on both real engines, which is how it would have changed a poster attribution unnoticed. Headless, source-only. engineGlobal is extract-and-executed from source because useMapInstance.js cannot be imported in plain Node.",
"check:provider-switch": "the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
"check:published": "what readers are SERVED must be what we BUILT. Every other gate measures the local tree, so all of them stay green while a stale publish serves something else — and manufactures bug reports that cannot be reproduced. Compares the sha384 per bundle. SKIPs when the published host is unreachable (a network gate that goes red on flaky wifi gets ignored).",
"check:quoted-scripts": "documentation/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS — update the DOC.",