doxa-maps/internal/check-maplibre-layer-drops.mjs
doxa-maps-88655c70 44ef31b2c6 Tooling reads the tree's name from the build config, in one place
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.

- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
  historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
  for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
  backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
  vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
  Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
  fallback, the private-repo-name pattern in public-hygiene-check).

This renames nothing visible; the eventual rename becomes a one-line config change.
2026-09-25 16:25:22 +00:00

154 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
/**
* check-maplibre-layer-drops.mjs — MapLibre must not SILENTLY DROP a layer.
*
* THE DEFECT CLASS, found twice in one flight and invisible to every other gate:
* MapLibre validates a layer spec and, if any property is unknown or misplaced, REJECTS THE WHOLE
* LAYER. addLayer() does NOT throw — it accepts the call, emits an `error` EVENT on the map, and
* silently omits the layer. getLayer() then returns undefined and the map renders without it. So
* the symptom is "a thing is missing", with no exception, no failed request, and no console error
* from the app's own code. Three ticks of instrumenting addLayer() found nothing, because the
* failure never travels through addLayer's return path.
*
* MEASURED instances, both real and both shipped:
* layers.regions-fill.paint.fill-emissive-strength: unknown property
* -> *-emissive-strength is a Mapbox GL v3 LIGHTING property. The WAGF region fills were
* absent on every MapLibre map while Mapbox showed them.
* layers.apg-pins.paint.circle-sort-key: unknown property "circle-sort-key"
* -> circle-sort-key is a LAYOUT property, written into paint. Mapbox tolerated the
* misplacement; MapLibre dropped the layer, so ALL 12,379 pins on all-people-groups
* vanished under the keyless engine.
*
* WHAT IT ASSERTS: load every isMap leaf in the built manifest with ?provider=maplibre, listen to
* the map's own `error` event, and fail on any message in the layer/source-dropping family
* (unknown property / does not exist / is required / Expected / invalid). Other errors are
* REPORTED but not failed — a tile 404 is a different problem and should not be smuggled in here.
*
* ★ IT IS A REAL CONTROL, NOT DECORATION: run against the tree before the fixes it reports
* 1 of 17 leaves dropping, and 0 of 17 after. It has been seen both red and green on the same
* codebase, which is the only thing that makes a green meaningful.
*
* SKIPS CLEANLY (exit 0) when the server is unreachable. An UNMEASURED check is not a FAILED one —
* check:filter-width prints "FAIL" when it cannot reach its server, and that is how a gate teaches
* people to ignore it.
*
* Usage: bun run serve (or any server on BASE_URL), then:
* node internal/check-maplibre-layer-drops.mjs
* BASE_URL=http://127.0.0.1:4399 node internal/check-maplibre-layer-drops.mjs
*/
import { chromium } from 'playwright'
import { readFileSync, existsSync } from 'node:fs'
import { resolve, join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { servesThisTree } from './_serves-this-tree.mjs'
import { TREE_NAME } from './_tree-name.mjs'
// Portable by construction — see check-no-absolute-paths.mjs. Never hardcode a worktree.
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const BASE = String(process.env.BASE_URL || 'http://127.0.0.1:4173').replace(/\/+$/, '')
const MANIFEST = join(ROOT, 'public', TREE_NAME, 'manifest.json')
const DROP_RE = /unknown property|does not exist|is required|Expected|invalid/i
if (!existsSync(MANIFEST)) {
console.log('MAPLIBRE-LAYER-DROPS: SKIP — no built manifest (run `bun run tree` first).')
process.exit(0)
}
// ⚠ REACHABLE AND MINE ARE DIFFERENT QUESTIONS. This machine runs several worktrees of one
// repo and the default port belongs to whichever server started first — measured 2026-09-24:
// :4173 was another worktree's, and this gate reported PASS 16/16 about a build that was not
// on disk here. The layer-drop family is silent by nature (MapLibre omits a layer instead of
// throwing), so a verdict taken from the wrong tree is exactly the kind nobody can catch by
// looking. A mismatch is a precondition, not a product failure: SKIP, but name it.
{
const tree = await servesThisTree(BASE, ROOT)
if (/DIFFERENT build/.test(tree.why)) {
console.log('MAPLIBRE-LAYER-DROPS: SKIP — the server is not serving THIS build.')
console.log(' ' + tree.why)
console.log(' Start `bun run serve` in this checkout and pass its port as BASE_URL.')
process.exit(0)
}
}
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'))
const leaves = []
for (const b of manifest.bundles || []) {
for (const p of b.profiles || []) if (p.isMap) leaves.push([b.bundle, p.profile])
}
if (!leaves.length) {
console.log('MAPLIBRE-LAYER-DROPS: SKIP — the manifest declares no isMap leaf.')
process.exit(0)
}
// Capture the map's error events by wrapping whichever engine global the bundle loads at runtime.
const INIT = () => {
window.__mapErrs = []
const wrap = (ns) => {
if (!ns || ns.__wrappedForDrops) return ns
const Orig = ns.Map
function Patched(...a) {
const m = new Orig(...a)
try {
m.on('error', (ev) => {
const t = String((ev && ev.error && ev.error.message) || (ev && ev.message) || '')
if (t) window.__mapErrs.push(t.slice(0, 160))
})
} catch (_) { /* an engine without .on must not break the page */ }
return m
}
Patched.prototype = Orig.prototype
ns.Map = Patched
ns.__wrappedForDrops = true
return ns
}
for (const name of ['maplibregl', 'mapboxgl']) {
let v
Object.defineProperty(window, name, {
configurable: true, get() { return v }, set(nv) { v = wrap(nv) },
})
}
}
console.log(`MAPLIBRE-LAYER-DROPS: ${leaves.length} isMap leaf/leaves at ${BASE}`)
const browser = await chromium.launch({
args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist'],
})
const offenders = []
let unreachable = 0
for (const [bundle, profile] of leaves) {
const page = await browser.newPage({ viewport: { width: 1100, height: 760 } })
await page.addInitScript(INIT)
const url = `${BASE}/${TREE_NAME}/${bundle}/${profile}/index.html?provider=maplibre`
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
await page.waitForTimeout(9000)
const errs = await page.evaluate(() => [...new Set(window.__mapErrs || [])])
const drops = errs.filter((e) => DROP_RE.test(e))
if (drops.length) {
offenders.push({ leaf: `${bundle}/${profile}`, drops })
console.log(` ✗ ${bundle}/${profile} — ${drops[0]}`)
} else {
console.log(` ok ${bundle}/${profile}`)
}
} catch (e) {
unreachable++
console.log(` · ${bundle}/${profile} — UNMEASURED (${String(e.message).slice(0, 60)})`)
}
await page.close()
}
await browser.close()
if (unreachable === leaves.length) {
console.log('MAPLIBRE-LAYER-DROPS: SKIP — nothing was reachable at ' + BASE + '. Start `bun run serve`.')
process.exit(0)
}
if (offenders.length) {
console.error(`\nMAPLIBRE-LAYER-DROPS: FAIL — ${offenders.length} leaf/leaves silently drop a layer under MapLibre.`)
for (const o of offenders) for (const d of o.drops) console.error(` ${o.leaf}: ${d}`)
console.error('\n MapLibre rejects a whole layer for ONE unknown or misplaced property, without throwing.')
console.error(' Engine-only paint (e.g. *-emissive-strength) belongs behind emissivePaint(map, kind);')
console.error(' layout properties (e.g. circle-sort-key) belong in `layout`, not `paint`.')
process.exit(1)
}
console.log(`\nMAPLIBRE-LAYER-DROPS: PASS — ${leaves.length - unreachable} leaf/leaves, no layer dropped by MapLibre.`)