Gate the square controls against the engine's own stylesheet
check:square-map-controls, registered with a gates:post token. It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our ported values against THAT FILE rather than against a remembered number, which is possible only because the look the Driver asked for is the engine default: DX-life adds NavigationControl and overrides nothing, so the target is published. It asserts the three deliberate deviations as deviations -- the engine's 29px is not adopted, there is no overflow:hidden, and a forced-colors block ships -- so each stays a stated choice rather than decaying into a mistake nobody can see. And it asserts the blast radius: round is the default and none of the nine MapToolbar consumers passes controls=. Opting demo-map in turns it red, which is what makes the silence mean something. Its CSS assertions read the text passed to useShadowStyles, not the file. The first run went red on "we do not adopt 29px" because the prop's own doc comment quotes the engine's rule, 29px and all -- a checker that cannot tell a comment from a declaration keeps finding the documentation it stands on.
This commit is contained in:
parent
9c86d91c7f
commit
d888b20d8b
2 changed files with 158 additions and 1 deletions
155
internal/check-square-map-controls.mjs
Normal file
155
internal/check-square-map-controls.mjs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env node
|
||||
// check-square-map-controls.mjs — `bun run check:square-map-controls`. Source-only, headless, no network.
|
||||
//
|
||||
// The square map-control variant: is it the ENGINE'S card, and does nothing else move?
|
||||
//
|
||||
// The Driver asked for square controls as an OPTION beside the round ones. The key fact that shaped
|
||||
// the implementation is that the square look is not a design of DX-life's at all: their CountryMap.vue
|
||||
// adds mapboxgl.NavigationControl + FullscreenControl and overrides NOTHING, so what he admired is the
|
||||
// ENGINE'S DEFAULT grouped card. That makes the target published and exact rather than a matter of
|
||||
// taste -- so this check reads the engine's own stylesheet out of node_modules and compares our ported
|
||||
// values against it. A port checked against its SOURCE cannot drift with my memory of it.
|
||||
//
|
||||
// AND IT ASSERTS THE BLAST RADIUS, which is the half a visual review cannot see: round is the default
|
||||
// and no consumer opts in, so all nine maps that use MapToolbar render exactly as before.
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Each copy of this repo grades ITS OWN tree (check:no-abs-paths, 2026-09-24).
|
||||
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const rd = (p) => readFileSync(join(REPO, p), 'utf8')
|
||||
|
||||
let fails = 0
|
||||
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
|
||||
|
||||
const TOOLBAR_SRC = rd('library/components/map-controls/MapToolbar.vue')
|
||||
const BUTTON = rd('library/components/map-controls/MapControlButton.vue')
|
||||
|
||||
// ★ CSS ASSERTIONS READ THE STYLESHEET, NOT THE FILE. The first run of this check went red on
|
||||
// "we do not adopt 29px" -- because the prop's own doc comment QUOTES the engine's rule, 29px and all.
|
||||
// A comment is not a declaration, and a checker that cannot tell them apart will keep finding the
|
||||
// documentation it is standing on. So the CSS tests run against the text actually passed to
|
||||
// useShadowStyles; the prop and template tests still read the whole file, which is where they live.
|
||||
function shadowCss(src) {
|
||||
const at = src.indexOf('useShadowStyles(')
|
||||
const open = src.indexOf('`', at)
|
||||
const close = src.indexOf('`', open + 1)
|
||||
return open < 0 || close < 0 ? '' : src.slice(open + 1, close)
|
||||
}
|
||||
const TOOLBAR = shadowCss(TOOLBAR_SRC)
|
||||
if (!TOOLBAR.trim()) { console.log(' FAIL - could not extract MapToolbar\'s stylesheet; nothing below was measured'); process.exit(1) }
|
||||
|
||||
// ── 1. the ENGINE is the oracle, read from node_modules ─────────────────────────────────────────
|
||||
const ENGINE = 'node_modules/maplibre-gl/dist/maplibre-gl.css'
|
||||
if (existsSync(join(REPO, ENGINE))) {
|
||||
const css = rd(ENGINE).replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||
const rule = (sel) => {
|
||||
const m = css.match(new RegExp(sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\{([^}]*)\\}'))
|
||||
return m ? m[1].replace(/\s+/g, ' ').trim() : null
|
||||
}
|
||||
const group = rule('.maplibregl-ctrl-group')
|
||||
const filled = rule('.maplibregl-ctrl-group:not(:empty)')
|
||||
const divider = rule('.maplibregl-ctrl-group button+button')
|
||||
console.log(`\n the engine's own rules, read from ${ENGINE}:`)
|
||||
console.log(` group { ${group} }`)
|
||||
console.log(` filled { ${filled} }`)
|
||||
console.log(` divider { ${divider} }`)
|
||||
|
||||
const radius = (group || '').match(/border-radius:\s*([^;]+)/)
|
||||
ok(!!radius && radius[1].trim() === '4px', `the engine's group radius is 4px, and ours is too`)
|
||||
ok(/border-radius:\s*4px;/.test(TOOLBAR), 'our .map-toolbar--square carries border-radius:4px')
|
||||
|
||||
const ring = (filled || '').match(/box-shadow:\s*([^;]+)/)
|
||||
ok(!!ring, 'the engine states a ring on a non-empty group')
|
||||
ok(/box-shadow:\s*0 0 0 2px rgba\(0, 0, 0, 0\.1\)/.test(TOOLBAR),
|
||||
`our ring matches the engine's 0 0 0 2px rgba(0,0,0,.1) (engine: ${ring && ring[1]})`)
|
||||
|
||||
ok(/border-top:\s*1px solid #ddd/.test(TOOLBAR),
|
||||
`our divider matches the engine's 1px solid #ddd (engine: ${divider})`)
|
||||
|
||||
// ★ the ONE value deliberately NOT ported
|
||||
const btn = rule('.maplibregl-ctrl-group button')
|
||||
const eng = (btn || '').match(/height:\s*(\d+)px/)
|
||||
ok(!!eng && eng[1] === '29', `the engine sizes its buttons ${eng && eng[1]}px`)
|
||||
ok(!new RegExp('height:\\s*29px').test(TOOLBAR) && !new RegExp('width:\\s*29px').test(TOOLBAR),
|
||||
'and we DO NOT adopt 29px -- the variant takes the SHAPE and leaves the SIZE, so the touch target is not cut')
|
||||
ok(/--map-btn-size/.test(TOOLBAR), 'our size still comes from --map-btn-size')
|
||||
} else {
|
||||
console.log(`\n NOT MEASURED against the engine: ${ENGINE} is absent, so the ported values were not compared to their source.`)
|
||||
fails++ // a missing oracle is not a pass
|
||||
}
|
||||
|
||||
// ── 2. the grouped card is a CONTAINER, not a radius swap ───────────────────────────────────────
|
||||
ok(/\.map-toolbar--square\s*\{[^}]*gap:\s*0/.test(TOOLBAR), 'the square toolbar sets gap:0 -- one panel, not a spaced column')
|
||||
ok(/\.map-toolbar--square\s*\{[^}]*background:\s*#fff/.test(TOOLBAR), 'the PANEL owns the background')
|
||||
ok(/\.map-toolbar--square \.mcb[^{]*\{[^}]*background:\s*transparent/.test(TOOLBAR), 'and the buttons own none of it')
|
||||
ok(/\.map-toolbar--square \.mcb[^{]*\{[^}]*box-shadow:\s*none/.test(TOOLBAR), 'the buttons drop their own shadow, so there is one ring not N')
|
||||
ok(/\.map-toolbar--square > \* \+ \*\s*\{[^}]*border-top/.test(TOOLBAR),
|
||||
'the divider is on the toolbar CHILDREN, so it still lands where two different control types meet')
|
||||
ok(/\.map-toolbar--square \.hbg-ctrl\s*\{[^}]*margin:\s*0/.test(TOOLBAR),
|
||||
"the hamburger wrapper's 10px margin is zeroed -- it would otherwise tear the panel apart")
|
||||
|
||||
// ★ no overflow:hidden, and the reason is a focus ring
|
||||
ok(!/\.map-toolbar--square\s*\{[^}]*overflow:\s*hidden/.test(TOOLBAR),
|
||||
'the square panel does NOT use overflow:hidden -- it would clip a focus ring on any inner button')
|
||||
ok(/:first-child[^{]*\{[^}]*border-radius:\s*4px 4px 0 0/.test(TOOLBAR) &&
|
||||
/:last-child[^{]*\{[^}]*border-radius:\s*0 0 4px 4px/.test(TOOLBAR),
|
||||
'outer corners come from first/last child rounding instead, which is the engine\'s own mechanism')
|
||||
ok(/:only-child[^{]*\{[^}]*border-radius:\s*4px/.test(TOOLBAR), 'and a lone control is rounded on all four corners')
|
||||
|
||||
// ── 3. forced colours, which the round buttons never had ────────────────────────────────────────
|
||||
ok(/@media \(forced-colors: active\)/.test(TOOLBAR), 'the square variant ships a forced-colors block')
|
||||
ok(/ButtonText/.test(TOOLBAR), 'using ButtonText, so the ring and dividers survive a replaced palette')
|
||||
|
||||
// ── 4. the standalone per-button override ───────────────────────────────────────────────────────
|
||||
ok(/shape:\s*\{ type: String,\s*default: 'round' \}/.test(BUTTON), "MapControlButton's shape defaults to 'round'")
|
||||
ok(/\.mcb--square\{ border-radius:4px; \}/.test(BUTTON), 'a standalone square takes the engine\'s 4px corners')
|
||||
ok(/'mcb--square': shape === 'square'/.test(BUTTON), 'and the class is bound to the prop')
|
||||
|
||||
// ── 5. BLAST RADIUS: round is the default and nobody opted in ────────────────────────────────────
|
||||
ok(/controls:\s*\{ type: String, default: 'round' \}/.test(TOOLBAR_SRC), "MapToolbar's controls prop defaults to 'round'")
|
||||
const CONSUMERS = [
|
||||
['doxa-simple-map', 'plugin-designs/doxa-simple-map/doxa-simple-map.vue', 8],
|
||||
['resources-map', 'plugin-designs/gospel-resources/resources-map.vue', 5],
|
||||
['upg-100-list', 'plugin-designs/my-upg-100-list/upg-100-list.vue', 6],
|
||||
['countries-map', 'plugin-designs/country-posters/countries-map.vue', 5],
|
||||
['DashboardMapPanel', 'plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue', 2],
|
||||
['demo-map', 'plugin-designs/demo-map/demo-map.vue', 5],
|
||||
['connectivity-map', 'plugin-designs/cities-internet/connectivity-map.vue', 5],
|
||||
['research-map', 'plugin-designs/doxa-research-map/research-map.vue', 7],
|
||||
['all-people-groups', 'plugin-designs/all-people-groups/all-people-groups.vue', 1],
|
||||
]
|
||||
let optedIn = 0
|
||||
for (const [name, path] of CONSUMERS) {
|
||||
const src = rd(path)
|
||||
const m = src.match(/<MapToolbar[^>]*controls=/)
|
||||
if (m) { optedIn++; console.log(` ${name} opts in`) }
|
||||
}
|
||||
ok(optedIn === 0, `none of the ${CONSUMERS.length} MapToolbar consumers passes controls=, so every one renders exactly as before (${optedIn} opted in)`)
|
||||
|
||||
// ── 6. THE GEOMETRY, for rega to confirm with eyes ──────────────────────────────────────────────
|
||||
// A square toolbar is SHORTER, because the gaps become dividers. That is the one measurable knock-on:
|
||||
// MapControlMenu's matchToolbar limits a menu to the toolbar column's height.
|
||||
const BTN = 32, GAP_DESKTOP = 8, GAP_MOBILE = 6
|
||||
console.log('\n toolbar column height, round vs square (button 32px; ring is box-shadow, so outside layout):')
|
||||
for (const [name, , n] of CONSUMERS) {
|
||||
const round = n * BTN + (n - 1) * GAP_DESKTOP
|
||||
const square = n * BTN + (n - 1) * 1 // 1px dividers replace the 8px gaps
|
||||
console.log(` ${String(n).padStart(2)} control(s) ${name.padEnd(18)} round ${String(round).padStart(3)}px square ${String(square).padStart(3)}px shorter by ${round - square}px`)
|
||||
}
|
||||
const five = { round: 5 * BTN + 4 * GAP_DESKTOP, square: 5 * BTN + 4 }
|
||||
ok(five.square < five.round,
|
||||
`a 5-control toolbar goes ${five.round}px -> ${five.square}px, so a matchToolbar menu gets ${five.round - five.square}px shorter -- rega should confirm the menu still fits its content`)
|
||||
console.log(` at mobile gap ${GAP_MOBILE}px a 5-control toolbar is ${5 * BTN + 4 * GAP_MOBILE}px round vs ${five.square}px square`)
|
||||
|
||||
// ── 7. NEG CONTROLS ─────────────────────────────────────────────────────────────────────────────
|
||||
ok(!/\.map-toolbar\s*\{[^}]*border-radius/.test(TOOLBAR),
|
||||
'NEG CONTROL: the DEFAULT .map-toolbar has no border-radius, so the square rules are genuinely scoped to the variant')
|
||||
ok(/border-radius:\s*50%/.test(BUTTON),
|
||||
'NEG CONTROL: the round button is still a 50% circle -- the default was not quietly changed')
|
||||
ok(!/controls === 'round'/.test(TOOLBAR_SRC),
|
||||
'NEG CONTROL: nothing branches on round, so round is the ABSENCE of the square class rather than a second code path')
|
||||
|
||||
console.log(fails ? `\nSQUARE-CONTROLS: ${fails} FAIL(s)` : '\nSQUARE-CONTROLS: the variant matches the engine\'s own rules, keeps our touch target, and changes no consumer')
|
||||
process.exit(fails ? 1 : 0)
|
||||
|
|
@ -216,6 +216,7 @@
|
|||
"check:snapshot-guard": "node internal/check-snapshot-guard.mjs",
|
||||
"check:snapshot-guard:poison": "node internal/check-snapshot-guard.mjs --poison",
|
||||
"check:spa-tab-order": "node internal/check-spa-tab-order.mjs",
|
||||
"check:square-map-controls": "node internal/check-square-map-controls.mjs",
|
||||
"check:sri": "node internal/sri-consistency-check.mjs",
|
||||
"check:stale-sweep-claim": "node internal/check-no-stale-sweep-claim.mjs",
|
||||
"check:studio-all": "node internal/check-studio-every-plugin.mjs",
|
||||
|
|
@ -243,7 +244,7 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:regions-uses-library check:shadow-css-parses check:square-map-controls",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
|
|
@ -427,6 +428,7 @@
|
|||
"check:link-coverage": "rule 4 of the brief — 'verify a URL resolves before shipping it' — as a gate instead of a habit. verify-gospel-links.mjs does the verifying; nothing asserted the record COVERS what is shipped, so the honest state of the corpus was a number someone had to go and measure. DISCOVERS the URLs by recursing the pin index for any http string rather than naming fields: every field list in this bundle has gone stale (ga and se shipped unscreened, the SUN block and the access table were each added after the fact), and the recursion found 16 links my own hand-rolled measurement had missed. Resolves each against THREE records — the verification table, translator-org-links.json (contacts, fetched by their own script) and sun-deaf-index.json (proved by content against a catch-all control, which is stronger than a status code) — and fails on any unrecorded link, any shipped link recorded BROKEN, and any 'unknown' on a target a status code CAN prove. The status-blind list holds one URL SHAPE, not a host: jesusfilm.org/watch/jesus/<slug> discriminates and 2,049 links depend on that, while /watch/?languageId= does not. Static, sub-second. Self-test: `bun run check:link-coverage:poison`.",
|
||||
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||||
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||||
"check:square-map-controls": "MapToolbar controls=\"square\" must be the ENGINE'S grouped card, not a square of ours. Reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our ported values against THAT FILE -- 4px corners, 0 0 0 2px rgba(0,0,0,.1) ring, 1px solid #ddd dividers -- because the look the Driver asked for is the engine default (DX-life adds NavigationControl and overrides nothing), so the target is published rather than a matter of taste. Asserts the three deliberate deviations: the engine's 29px is NOT adopted (touch target over pixel parity), there is NO overflow:hidden (it clips a focus ring; outer corners come from first/last child rounding, the engine's own mechanism), and a forced-colors block ships. Also asserts the BLAST RADIUS: round is the default and none of the nine MapToolbar consumers passes controls=, so no map changes -- opting one in turns it red. CSS assertions read the text passed to useShadowStyles, not the file, because the prop's doc comment quotes the engine's 29px and a checker that cannot tell a comment from a declaration finds the documentation it stands on.",
|
||||
"check:stale-sweep-claim": "a caveat that has stopped being true is a wrong number. The gospel surfaces carry a paragraph saying which pools were searched; while the GRN sweep ran they truthfully said part of it was unread, the sweep finished, and the sentence stayed for four more commits. The DATA decides here: undecided gap codes mean the claim is allowed, all-decided means it must go, so it re-arms itself for the next slow pool. Does NOT flag a claim rendered under a v-if on the pending count — that form expires on its own. Self-test: `--poison`. Static, sub-second.",
|
||||
"check:surfaces-read-the-tier": "a surface may READ the tier the pin index computed, never re-derive 'this group has nothing' from the raw resource fields. That hand-rolled field list went stale every time a source was added: 7 codes drawn as having nothing while the map showed them a link, then 28 when Scripture Earth landed. Static, sub-second. Self-test: `bun run check:surfaces-read-the-tier:poison` re-injects the old formula into every surface and each must be caught.",
|
||||
"check:tabs-localized": "a chosen locale must CHANGE the rendered tab text. check:i18n-parity compares catalogs to each OTHER, so it cannot tell a shipped translation from a dead one; tabs.json is reached only via a DYNAMIC key (SemanticTreeLegend.vue:40), invisible to any grep. Renders it and reads the result.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue