Only the browser seat runs browsers, and only inside internal/browser-slot.sh. That rule was remembered, not enforced: an agent ran a gate it took for a headless source check and it launched Chromium outside the slot. internal/lib/playwright-guarded.mjs is now the one way a repo file reaches playwright: its launch(), launchPersistentContext(), launchServer(), connect() and connectOverCDP() exit 75 (NOT MEASURED) with a message naming browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it for the command it runs, so a bash -c grandchild inherits it. All 72 files that imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates; plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2, research-map-plus-grid 2, research-dashboard 1): a mechanical import swap. check:browser-gates-guarded (gates:pre) fails a direct import; bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate moves to internal/lib/browser-gates.mjs so the runner and the check share it.
165 lines
8.9 KiB
JavaScript
165 lines
8.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-upg-map-data-painted — the UPG-100 map must have OUR DATA on it, not just a basemap.
|
|
*
|
|
* THE DEFECT THIS CATCHES. Driver 2026-09-24: "the polygons for the map and map colors and pins
|
|
* are not showing up". Nothing in the gate suite could answer that. verify-map-paint asserts a live
|
|
* GL context, zero console errors and that Mapbox emitted attribution — every one of which is TRUE
|
|
* of a perfectly-painted basemap carrying none of our zones, polygons or pins. "The map engine
|
|
* booted and tiles are loading" and "my data is on it" are two different properties, and only the
|
|
* first one was ever measured.
|
|
*
|
|
* ★ WHY THIS READS A STATUS STRING INSTEAD OF INSPECTING THE MAP. Both obvious approaches are
|
|
* wrong, and I shipped one of them before measuring:
|
|
* - COUNTING DOM MARKERS returns 0 on a healthy map. Mapbox draws these pins as GL geometry
|
|
* inside the canvas; there is no .mapboxgl-marker element to count. I carried "markers: 0"
|
|
* across three probes as proof of breakage. It was proof of a wrong selector.
|
|
* - SAMPLING THE CANVAS returns an empty buffer. A WebGL drawing buffer is cleared after
|
|
* compositing, so drawImage/getImageData see alpha 0 everywhere. Measured: 0 of 13362 sampled
|
|
* pixels had any alpha at all, on a map visibly full of colour.
|
|
* The profile already computes the exact numbers, at the moment the layers are added, and prints
|
|
* them: upg-100-list.vue:1849 writes `N polys · R/T zones · P pins` into .upg-status. That is the
|
|
* product's own count of what it put on the map — authoritative in a way a proxy cannot be. The
|
|
* loop's standing rule is that a number printed but not asserted on is decoration; these three
|
|
* were printed by the product and asserted on by nothing.
|
|
*
|
|
* ⚠ IT ASSERTS THE ZONE RATIO, NOT JUST PRESENCE. "27/27 zones" and "1/27 zones" both contain
|
|
* polygons, and only one of them is the product. A threshold on the ratio is what separates
|
|
* "some data arrived" from "the data arrived".
|
|
*
|
|
* Usage: node internal/check-upg-map-data-painted.mjs [url ...]
|
|
* Defaults to the LOCAL tree. Pass live URLs explicitly to audit what is published — the two
|
|
* diverge, and conflating them cost a whole tick of wrong diagnoses.
|
|
*/
|
|
import { chromium } from './lib/playwright-guarded.mjs'
|
|
import { resolve, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { servesThisTree } from './_serves-this-tree.mjs'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
const BASE = process.env.BASE || `http://127.0.0.1:4173/${TREE_NAME}`
|
|
const DEFAULT = [
|
|
'my-upg-100-list/upg-100-list/index.html',
|
|
'my-upg-100-list/upg-100-mfe/index.html',
|
|
]
|
|
const urls = process.argv.slice(2).length
|
|
? process.argv.slice(2)
|
|
: DEFAULT.map((p) => BASE.replace(/\/$/, '') + '/' + p)
|
|
|
|
// Floors, not exact counts: the dataset grows, and a gate that hardcodes today's totals fails on
|
|
// every data refresh and teaches people to edit the expectation rather than read it. These are
|
|
// "clearly the real thing" thresholds — measured 2026-09-24 at 195 polys / 27 of 27 zones /
|
|
// 2107 pins, so each floor sits far below the real value and far above a broken one.
|
|
const MIN_POLYS = 50
|
|
const MIN_PINS = 500
|
|
const MIN_ZONE_RATIO = 0.9
|
|
|
|
let reachable = false
|
|
try { const r = await fetch(urls[0], { signal: AbortSignal.timeout(5000) }); reachable = r.ok } catch { /* precondition */ }
|
|
if (!reachable) {
|
|
// NOT MEASURED, exit 75 — never exit 0. Through browser-slot.sh this line once read
|
|
// "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" (the slot had overwritten BASE)
|
|
// and EXITED 0, so a wave counted a gate that measured nothing as green.
|
|
console.log(`UPG-MAP-DATA: NOT MEASURED — nothing served at ${urls[0]}.`)
|
|
console.log(` Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:<port>/${TREE_NAME}`)
|
|
process.exit(75)
|
|
}
|
|
|
|
// ★ "ANSWERS 200" AND "IS SERVING THIS BUILD" ARE DIFFERENT QUESTIONS. This gate reads the counter
|
|
// the profile writes when it adds its layers — a number that means nothing unless you know WHICH
|
|
// build produced it. Pointed at a stale server it would report someone else's polygons as proof
|
|
// that this checkout works.
|
|
// See _serves-this-tree.mjs (bundler-2, BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE):
|
|
// :4173 on this box was once held by a different worktree and a gate reported three confident runs
|
|
// about code nobody had changed.
|
|
// ⚠ NOT MEASURED, exit 75 — the repo's EX_TEMPFAIL for "not measured" (a9550858), which the wave
|
|
// runner reports as ⊘. It was exit 2 here, a code nothing else in the repo reads that way.
|
|
// This check is also what makes the :4173 default safe: another agent's tree there is refused.
|
|
const TREE_BASE = String(urls[0]).split(`/${TREE_NAME}/`)[0]
|
|
const treeCheck = await servesThisTree(TREE_BASE, ROOT)
|
|
if (!treeCheck.ok) {
|
|
console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.')
|
|
console.error(` ${treeCheck.why}`)
|
|
console.error(' The polygon and pin counts below would describe someone else\'s build.')
|
|
process.exit(75)
|
|
}
|
|
|
|
const WALK = `(root) => { const o = []; const rec = (r) => { for (const e of r.querySelectorAll('*')) { o.push(e); if (e.shadowRoot) rec(e.shadowRoot) } }; rec(root); return o }`
|
|
|
|
const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--enable-unsafe-swiftshader'] })
|
|
|
|
async function measure(url) {
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
|
const cssWarn = []
|
|
page.on('console', (m) => { if (/missing CSS declarations/i.test(m.text())) cssWarn.push(1) })
|
|
try {
|
|
await page.goto(url, { waitUntil: 'load', timeout: 60000 })
|
|
} catch (e) {
|
|
await page.close()
|
|
return { url, error: String(e.message).slice(0, 80) }
|
|
}
|
|
// The counts are written when the layers land, which is after tiles and after the data fetch.
|
|
await page.waitForTimeout(18000)
|
|
const out = await page.evaluate(`(() => {
|
|
const all = (${WALK})(document)
|
|
const el = all.find((e) => /\\bupg-status\\b/.test((e.className || '').toString()))
|
|
const text = el ? (el.textContent || '').trim() : null
|
|
if (!text) return { text: null }
|
|
// "195 polys · 27/27 zones · 2107 pins"
|
|
const m = text.match(/(\\d+)\\s*polys?\\s*·\\s*(\\d+)\\/(\\d+)\\s*zones?\\s*·\\s*(\\d+)\\s*pins?/i)
|
|
if (!m) return { text }
|
|
return { text, polys: +m[1], zonesResolved: +m[2], zonesTotal: +m[3], pins: +m[4] }
|
|
})()`)
|
|
await page.close()
|
|
return { url, cssWarn: cssWarn.length, ...out }
|
|
}
|
|
|
|
const results = []
|
|
try {
|
|
for (const u of urls) results.push(await measure(u))
|
|
} finally {
|
|
await browser.close()
|
|
}
|
|
|
|
console.log('UPG-MAP-DATA')
|
|
const fails = []
|
|
for (const r of results) {
|
|
const name = r.url.split(`/${TREE_NAME}/`)[1] || r.url
|
|
if (r.error) { fails.push(`${name}: navigation failed — ${r.error}`); console.log(` ${name} — ERROR`); continue }
|
|
if (!r.text) {
|
|
// A MISSING STATUS LINE IS A FAILURE, NOT A SKIP. If the element never rendered its own
|
|
// counter, nothing here can vouch for the map, and silently passing is the false green this
|
|
// gate exists to remove.
|
|
fails.push(`${name}: no .upg-status counter rendered — the element did not reach the point `
|
|
+ 'where it reports what it painted')
|
|
console.log(` ${name} — no status line`)
|
|
continue
|
|
}
|
|
if (r.polys === undefined) {
|
|
fails.push(`${name}: .upg-status reads "${r.text}" — not the expected "N polys · R/T zones · P pins"`)
|
|
console.log(` ${name} — unparsed: ${r.text}`)
|
|
continue
|
|
}
|
|
const ratio = r.zonesTotal ? r.zonesResolved / r.zonesTotal : 0
|
|
console.log(` ${name} — ${r.polys} polys · ${r.zonesResolved}/${r.zonesTotal} zones `
|
|
+ `· ${r.pins} pins${r.cssWarn ? ' · ⚠ engine CSS warning' : ''}`)
|
|
if (r.polys < MIN_POLYS) fails.push(`${name}: ${r.polys} polygons, expected at least ${MIN_POLYS} — the zone shapes are not on the map`)
|
|
if (r.pins < MIN_PINS) fails.push(`${name}: ${r.pins} pins, expected at least ${MIN_PINS} — the people-group dots are not on the map`)
|
|
if (ratio < MIN_ZONE_RATIO) {
|
|
fails.push(`${name}: only ${r.zonesResolved} of ${r.zonesTotal} zones resolved to geometry `
|
|
+ `(${Math.round(ratio * 100)}%) — polygons are present but most zones paint nothing, which `
|
|
+ 'looks like a working map with holes in it')
|
|
}
|
|
}
|
|
|
|
if (fails.length) {
|
|
console.error(`\nUPG-MAP-DATA: FAIL — ${fails.length} problem(s)`)
|
|
for (const f of fails) console.error(' - ' + f)
|
|
console.error('\n This gate reads the counter the profile itself writes when it adds its layers')
|
|
console.error(' (upg-100-list.vue:1849). A basemap paints perfectly with none of this on it, so')
|
|
console.error(' every other map gate can stay green while the reader sees an empty world.')
|
|
process.exit(1)
|
|
}
|
|
console.log(`\nUPG-MAP-DATA: PASS — ${results.length} leaf/leaves carry their polygons, zones and pins.`)
|