The geocoder check failed a contract it should have been reading

rega: "0 search bars on both engines". The product is right and the check was
wrong, twice, and both mistakes are mine.

UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT. research-map.vue says
`hasGeocoder = mapProvider === 'mapbox'`, and research-map.instances.json says it
in words: "Under maplibre the search bar is deliberately ABSENT —
GeocoderComponent's maplibre path still geocodes through Mapbox, so shipping it
would bill the very key this instance exists to avoid." The previous revision had
just been taught that a bar-less leaf is a hollow pass, and applied that lesson
to the one engine where absence is the point. An absent bar is in fact the
STRONGEST form of this gate's own assertion: a search that cannot be typed into
cannot send anyone's IP to Mapbox.
The rule is conditional now rather than hardcoded, because a keyless geocoder
may ship one day and this gate should not need rewriting when it does: no bar
under maplibre is recorded and passes; a bar under maplibre must work and still
make zero Mapbox requests. Either way the zero is what is asserted.

THE MAPBOX RUN WAS NEVER GIVEN A TOKEN, so it could not have rendered a bar
either. The leaf bakes BAKED_TOKEN = "" and nothing else supplies one, so
mapbox-gl cannot construct a Map, `map` stays null, and the bar's
`v-if="appReady && map && hasGeocoder"` never fires. Note the mechanism, because
it is not the obvious one: an explicit ?provider=mapbox does NOT fall back to
maplibre — explicitMapProvider reads the URL first — so the engine is Mapbox, it
simply has no key. A gate that fails a product for a key the gate did not
provide is measuring its own environment.
It takes MAPBOX_TOKEN from the env, or `token` from the build config — the same
ladder check:experimental-shelf uses — and injects it as window.MAP_TOKEN the way
check:auto-engine and check:a11y-button-names do. Only for the mapbox run: a
token present during the maplibre run is the very variable this gate holds at
zero. With no token the mapbox half is NOT MEASURED, reported through the SKIP
line run-parallel reads, so the wave prints it as unmeasured rather than counting
it as a pass. The maplibre half is measured either way, keyless being the whole
point of that engine.

Also separates "the bar was removed" from "the map never constructed" by probing
for a canvas, since those are different bugs with one symptom — and the second is
how this check misread itself.

The token is never printed, only its presence.
This commit is contained in:
Doxa Maps 2026-09-26 13:12:48 +00:00
parent c377e19925
commit f42daf2802

View file

@ -25,13 +25,45 @@
* look identical: the bars appear, suggestions work, nothing errors. Only the request log tells the
* truth, which is why this counts requests rather than looking at the page.
*
* ASSERTIONS, on the built tree:
* maplibre ZERO requests to api.mapbox.com.
* both whatever search bars this leaf renders still produce suggestions — a zero bought by
* breaking search is not a fix. A leaf with NO bar now FAILS (it used to pass hollow);
* the default leaf is doxa-research-map/research-map, which mounts one (--leaf to change).
* ★ CORRECTED 2026-09-26, after rega reported "0 search bars on both engines" and it was the CHECK
* that was wrong, not the product. Two separate mistakes, both of them mine:
*
* Exit: 0 pass or SKIP (no built tree, no browser, no data) · 1 fail.
* 1. UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT — not a hollow pass and not a failure.
* research-map.vue: `hasGeocoder = mapProvider === 'mapbox'`, and research-map.instances.json
* says it in words: "Under maplibre the search bar is deliberately ABSENT — GeocoderComponent's
* maplibre path still geocodes through Mapbox, so shipping it would bill the very key this
* instance exists to avoid". The previous revision had just been taught that a bar-less leaf is
* a hollow pass, and applied that lesson to the one engine where ABSENCE IS THE POINT. An
* absent bar is in fact the STRONGEST form of this gate's own assertion: a search that cannot
* be typed into cannot send anyone's IP to Mapbox.
* So the rule is now conditional rather than hardcoded, because a keyless geocoder may ship
* one day and this gate should not have to be rewritten when it does: if maplibre renders NO
* bar, that is recorded and passes; if it renders one, it must WORK and still make zero
* Mapbox requests. Either way the zero is what is asserted.
*
* 2. THE MAPBOX RUN WAS NEVER GIVEN A TOKEN, so it could not have rendered a bar either. The leaf
* bakes BAKED_TOKEN = "" and nothing else supplies one, so mapbox-gl cannot construct a Map,
* `map` stays null, and the bar's `v-if="appReady && map && hasGeocoder"` never fires. Note the
* mechanism: an explicit ?provider=mapbox does NOT fall back to maplibre (explicitMapProvider
* reads the URL first) — the engine is Mapbox, it simply has no key. A gate that fails a
* product for a key the gate did not provide is measuring its own environment.
* It now takes MAPBOX_TOKEN from the env (or vite.doxa-maps-build-config.json `token`, the
* same ladder check:experimental-shelf uses) and injects it as window.MAP_TOKEN before
* navigation, the way check:auto-engine and check:a11y-button-names do. With no token the
* mapbox half is NOT MEASURED — reported as a SKIP line so run-parallel prints ⊘ rather than
* counting it as a pass, because an always-taken skip is indistinguishable from an unwired
* gate unless the runner says so.
*
* ASSERTIONS, on the built tree:
* maplibre ZERO requests to api.mapbox.com. (Measured with or without a token.)
* If a bar is present it must produce suggestions; if it is absent, that is the
* documented contract and is recorded as such.
* mapbox the bar is PRESENT and produces suggestions. Requires a token; without one this
* half is skipped, never failed. Mapbox traffic is expected here and not asserted on.
*
* NEVER PRINTS THE TOKEN. Only its presence is reported.
*
* Exit: 0 pass or SKIP (no built tree, no browser, no data, no token) · 1 fail.
*/
import { createServer } from 'node:http'
import { spawn } from 'node:child_process'
@ -50,6 +82,12 @@ let chromium
try { ({ chromium } = await import('playwright')) } catch { skip('playwright is not installed.') }
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
// The token ladder every other browser gate here uses: env first, then the build config. Presence
// is reported; the value never is.
const TOKEN = (() => {
if (process.env.MAPBOX_TOKEN) return process.env.MAPBOX_TOKEN
try { return JSON.parse(readFileSync(join(ROOT, 'vite.doxa-maps-build-config.json'), 'utf8')).token || '' } catch { return '' }
})()
// ★ A LEAF THAT MOUNTS THE SEARCH BAR, or the suggestion half proves nothing (2026-09-26: the
// research-dashboard leaf renders no bar, so every run passed with "search bars: 0" — a hollow PASS).
// Default: the research map, which mounts the library GeocoderComponent. --leaf <bundle>/<profile>.
@ -78,10 +116,17 @@ const browser = await chromium.launch({ args: ['--enable-unsafe-swiftshader', '-
const fails = []
const lines = []
try {
for (const engine of ['maplibre', 'mapbox']) {
// The mapbox half needs a key. Measure maplibre either way — it is the headline claim and it is
// fully measurable keyless, which is the whole point of the engine.
const engines = TOKEN ? ['maplibre', 'mapbox'] : ['maplibre']
for (const engine of engines) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
const mapboxCalls = []
page.on('request', (r) => { const u = r.url(); if (u.includes('api.mapbox.com')) mapboxCalls.push(u) })
// ★ ONLY under mapbox. Injecting a token into the maplibre run would change what is being
// measured: resolveMapProvider's `auto` rung reads a token as "use Mapbox", and even with an
// explicit ?provider=maplibre a present token is a variable this gate exists to hold at zero.
if (engine === 'mapbox') await page.addInitScript((tk) => { window.MAP_TOKEN = tk }, TOKEN)
await page.goto(`${LEAF}?provider=${engine}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
await page.waitForTimeout(20000)
@ -96,8 +141,11 @@ try {
if (el.shadowRoot) walk(el.shadowRoot)
}
}
walk(document)
if (!hits.length) return { bars: 0, suggestions: 0 }
// Did an engine actually construct a map? A canvas is the one artefact both engines share,
// and it tells "the bar was removed" apart from "there was never a map to hang it on".
let mapReady = false
for (const root of seen) { if (root.querySelector && root.querySelector('canvas.mapboxgl-canvas, canvas.maplibregl-canvas, .mapboxgl-map canvas, .maplibregl-map canvas')) { mapReady = true; break } }
if (!hits.length) return { bars: 0, suggestions: 0, mapReady }
const input = hits[0]
input.focus()
input.value = 'Afar'
@ -106,23 +154,40 @@ try {
await sleep(500)
const root = input.getRootNode()
const n = root.querySelectorAll('.mapboxgl-ctrl-geocoder .suggestions li, .maplibregl-ctrl-geocoder .suggestions li, .suggestions li').length
if (n) return { bars: hits.length, suggestions: n }
if (n) return { bars: hits.length, suggestions: n, mapReady }
}
return { bars: hits.length, suggestions: 0 }
return { bars: hits.length, suggestions: 0, mapReady }
})
await page.close()
lines.push(` · ${engine.padEnd(9)} api.mapbox.com requests: ${mapboxCalls.length} · search bars: ${search.bars} · suggestions for "Afar": ${search.suggestions}`)
lines.push(` · ${engine.padEnd(9)} api.mapbox.com requests: ${mapboxCalls.length} · search bars: ${search.bars} · suggestions for "Afar": ${search.suggestions}${engine === 'mapbox' ? ' · token: supplied' : ''}`)
if (engine === 'maplibre' && mapboxCalls.length) {
fails.push(`under maplibre the dashboard made ${mapboxCalls.length} request(s) to api.mapbox.com:`)
for (const u of [...new Set(mapboxCalls)].slice(0, 8)) fails.push(` ${u.slice(0, 150)}`)
}
// NO BAR IS A FAILURE, not a pass: this gate exists to prove search still works with zero Mapbox calls.
if (!search.bars) {
fails.push(`under ${engine} the leaf rendered NO search bar, so search was never exercised — point --leaf at a leaf that mounts one.`)
} else if (!search.suggestions) {
fails.push(`under ${engine} the search bar rendered but typing produced no suggestions — zero Mapbox traffic bought by a broken search is not a fix.`)
if (engine === 'maplibre') {
// THE HEADLINE. One request sends the visitor's IP to Mapbox and breaks the keyless promise,
// whatever the page looks like afterwards.
if (mapboxCalls.length) {
fails.push(`under maplibre the leaf made ${mapboxCalls.length} request(s) to api.mapbox.com:`)
for (const u of [...new Set(mapboxCalls)].slice(0, 8)) fails.push(` ${u.slice(0, 150)}`)
}
// A bar is OPTIONAL here and its absence is the documented contract (see the header). What is
// NOT optional is that a bar, if present, works — otherwise a future keyless geocoder could
// ship broken and this gate would read it as a clean zero.
if (!search.bars) {
lines.push(' (no search bar under maplibre — the documented contract: research-map.vue hasGeocoder = provider === \'mapbox\', because GeocoderComponent\'s maplibre path still geocodes through Mapbox. An absent bar is the strongest form of the zero above.)')
} else if (!search.suggestions) {
fails.push('under maplibre a search bar rendered but typing produced no suggestions — if a keyless geocoder now ships it has to work, not merely be quiet.')
}
} else {
// MAPBOX: the bar is the feature this profile was built around, and it must still work.
// `mapReady` separates "the bar was removed" from "the map never constructed", which are
// different bugs with the same symptom — and the second one is how this gate misread itself.
if (!search.bars) {
fails.push(search.mapReady
? 'under mapbox the map constructed but rendered NO search bar — hasGeocoder or the GeocoderComponent mount is broken.'
: 'under mapbox NO map constructed (so the bar\'s v-if could never fire). The token was supplied, so this is the product or the leaf, not a missing key.')
} else if (!search.suggestions) {
fails.push('under mapbox the search bar rendered but typing produced no suggestions.')
}
}
}
} catch (err) {
@ -138,4 +203,11 @@ if (fails.length) {
for (const f of fails) console.log(` ✗ ${f}`)
process.exit(1)
}
console.log(`${TAG}: PASS — MapLibre calls Mapbox zero times and still searches; Mapbox is unchanged.`)
if (!TOKEN) {
// Half-measured is not a pass. run-parallel reads a `TAG: SKIP` line and prints ⊘, so the wave
// cannot report this as one of N gates that graded something. Exit 0: nothing FAILED.
console.log(`${TAG}: SKIP — maplibre measured and clean; the mapbox half needs a key and none was supplied.`)
console.log(' Set MAPBOX_TOKEN in the environment, or `token` in vite.doxa-maps-build-config.json, to measure it.')
process.exit(0)
}
console.log(`${TAG}: PASS — MapLibre calls Mapbox zero times, and Mapbox still searches.`)