doxa-maps/internal/check-provider-for.mjs
Doxa Maps 28d4c83d96
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 52eeb0db
2026-09-26 18:08:09 +00:00

111 lines
8 KiB
JavaScript

#!/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)