doxa-maps/internal/check-no-silent-engine-substitution.mjs
Doxa Maps 471832876c
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 d1795008
2026-09-26 22:12:35 +00:00

166 lines
8.2 KiB
JavaScript

#!/usr/bin/env node
/**
* check-no-silent-engine-substitution — a profile must never fall back to a DIFFERENT map engine than
* the one its provider asked for.
*
* THE DEFECT, found by regions' C3 read and routed by dx-shepherd (END-SHEP-C3C, 2026-09-26):
*
* const gl = engineGlobal(provider) || window.mapboxgl
*
* That `||` looks like ordinary defensive coding and is not. engineGlobal() ALREADY resolves mapbox's own
* global for the mapbox case — useMapInstance.js:115 is
* `ENGINES[provider]?.global || ENGINES.mapbox.global`, which falls back to mapbox's NAME only for an
* UNKNOWN provider name, never for a missing engine. So the expression can fire in exactly ONE situation:
* the page asked for 'maplibre' and MapLibre failed to load. It then hands back MAPBOX — the token-gated
* engine — to a page that explicitly asked for the keyless one, breaking the keyless contract
* research-map.instances.json documents.
*
* ★ AND THE SUBSTITUTE IS BROKEN BY CONSTRUCTION, which is what makes this worth a gate rather than a
* one-line fix. These profiles gate `gl.accessToken = token` on `if (!isMaplibre)`, so a maplibre request
* that fell through to Mapbox got Mapbox WITHOUT a token: 401s on every tile instead of a legible failure.
* A fallback that cannot produce a working map even in principle is strictly worse than no fallback,
* because it converts a diagnosable error into a mysterious one.
*
* ⚠ WHY THE THREE EXISTING ENGINE GATES DID NOT CATCH IT, and why this is not a duplicate of them:
* check-leaf-engine-per-provider — a leaf loads the engine its provider asks for
* check-provider-switch — the page-level switch reaches a raw-built map
* check-auto-engine — 'auto' resolves to Mapbox with a token, MapLibre without
* All three assert the HAPPY PATH: what happens when engines load. This bug only appears when one does
* NOT load, so all three were green for as long as it existed. The property here is the negative one —
* "when the requested engine is absent, no other engine is substituted" — and nothing asserted it.
*
* ⚠ IT IS A SOURCE CHECK, ON PURPOSE. Reproducing it at the rendered layer means serving a leaf with
* MapLibre deliberately blocked, which is a browser run for a defect whose whole signature is one
* expression. The source is the authoritative place: the fallback either appears in the file or it does
* not. A browser gate here would cost a slot to measure something a grep decides.
*
* Usage: node internal/check-no-silent-engine-substitution.mjs
* node internal/check-no-silent-engine-substitution.mjs --self-test
*/
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = dirname(fileURLToPath(import.meta.url))
const REPO = process.env.DOXA_REPO || resolve(HERE, '..')
const ROOT = join(REPO, 'plugin-designs')
// ⚠ FLAGS FILTERED FROM THE POSITIONAL READ. A bare `--self-test` was otherwise taken as a path, which
// has bitten this repo three times (--self-test read as a leaf, --emit read as a baseUrl, --poison read
// as a packet).
const SELF_TEST = process.argv.includes('--self-test')
// The substitution to refuse: engineGlobal(...) OR-ed with a specific engine's global. Any of these
// named globals on the right of a `||` after engineGlobal is a cross-engine fallback by definition —
// engineGlobal already returns the RIGHT global for the requested provider, so the only thing an `||`
// can add is a DIFFERENT engine.
const ENGINE_GLOBALS = ['mapboxgl', 'maplibregl']
const PATTERN = new RegExp(
String.raw`engineGlobal\s*\([^)]*\)\s*\|\|\s*(?:window\.)?(${ENGINE_GLOBALS.join('|')})\b`,
'g',
)
function scan(text) {
const hits = []
const lines = text.split('\n')
lines.forEach((line, i) => {
// Skip comment lines: this file's own documentation quotes the bad pattern, and so does every fix
// that explains what it removed. A gate that fails on its own explanation teaches people to delete
// the explanation.
const t = line.trim()
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return
PATTERN.lastIndex = 0
let m
while ((m = PATTERN.exec(line)) !== null) hits.push({ line: i + 1, engine: m[1], text: t.slice(0, 110) })
})
return hits
}
if (SELF_TEST) {
// ★ PROVE THE PATTERN FIRES BEFORE TRUSTING A GREEN RUN. A source gate that has never matched anything
// is indistinguishable from a broken regex, and this one's whole value is that it matches.
const should = [
'const gl = engineGlobal(upgProvider) || window.mapboxgl',
' const gl = engineGlobal(p)||maplibregl',
'let gl = engineGlobal( provider ) || window.maplibregl;',
]
const shouldNot = [
'const gl = engineGlobal(upgProvider)',
'// const gl = engineGlobal(p) || window.mapboxgl <- the removed fallback',
'const gl = engineGlobal(p) || null',
'const token = mapboxToken?.value || profileConfig?.value?.tk || \'\'',
]
let bad = 0
for (const s of should) {
const n = scan(s).length
console.log(` ${n ? 'MATCHED ' : 'MISSED '} ${s.trim().slice(0, 62)}`)
if (!n) bad++
}
for (const s of shouldNot) {
const n = scan(s).length
console.log(` ${n ? 'FALSE+ ' : 'ignored '} ${s.trim().slice(0, 62)}`)
if (n) bad++
}
if (bad) {
console.error(`\nNO-SILENT-ENGINE-SUBSTITUTION --self-test: FAIL — ${bad} case(s) wrong.`)
process.exit(1)
}
console.log('\nNO-SILENT-ENGINE-SUBSTITUTION --self-test: PASS — the pattern matches every real form '
+ 'and ignores comments, a null fallback and an unrelated token chain.')
process.exit(0)
}
if (!existsSync(ROOT)) {
console.log(`NO-SILENT-ENGINE-SUBSTITUTION: NOT MEASURED — no plugin-designs tree at ${ROOT}`)
process.exit(75)
}
const findings = []
let scanned = 0
const walk = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name)
if (e.isDirectory()) {
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue
walk(p)
} else if (/\.(vue|js|mjs)$/.test(e.name)) {
let text
try { text = readFileSync(p, 'utf8') } catch { continue }
if (!text.includes('engineGlobal')) continue
scanned++
for (const h of scan(text)) findings.push({ file: p.slice(REPO.length + 1), ...h })
}
}
}
walk(ROOT)
console.log('NO-SILENT-ENGINE-SUBSTITUTION')
console.log(` ${scanned} file(s) call engineGlobal()`)
// ⚠ REFUSE TO JUDGE rather than pass vacuously. If nothing in the tree calls engineGlobal at all, the
// pattern had nothing to match and a green result would mean "I found no files", not "the code is clean".
if (!scanned) {
console.log('NO-SILENT-ENGINE-SUBSTITUTION: NOT MEASURED — no file calls engineGlobal(), so there was '
+ 'nothing to check. Exit 75 rather than 0: "found nothing" is not "found nothing wrong".')
process.exit(75)
}
if (findings.length) {
console.error(`\nNO-SILENT-ENGINE-SUBSTITUTION: FAIL — ${findings.length} silent engine substitution(s).`)
for (const f of findings) {
console.error(` ✗ ${f.file}:${f.line} falls back to ${f.engine}`)
console.error(` ${f.text}`)
}
console.error('\n engineGlobal() ALREADY returns the right global for the requested provider, so an `||`')
console.error(' after it can only supply a DIFFERENT engine — and it fires only when the page asked for')
console.error(' maplibre and maplibre failed to load. A page that asked for the KEYLESS engine then')
console.error(' receives the TOKEN-GATED one, and because accessToken is gated on !isMaplibre it')
console.error(' receives it WITHOUT a token: 401s instead of a legible error.')
console.error('\n FIX: delete the fallback. Keep the requested engine, and let the profile\'s existing')
console.error(' !gl branch fail visibly — console.warn plus the map\'s error/empty state, never a blank,')
console.error(' and never another engine. Name the engine in the message so a partner can tell an')
console.error(' honoured request from a silent downgrade.')
process.exit(1)
}
console.log('NO-SILENT-ENGINE-SUBSTITUTION: PASS — every engineGlobal() call keeps the engine its provider '
+ 'asked for.')