diff --git a/internal/check-board-map-options.mjs b/internal/check-board-map-options.mjs new file mode 100644 index 000000000..49c11fca6 --- /dev/null +++ b/internal/check-board-map-options.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * check-board-map-options.mjs — the GEOGRAPHIC caller must state its map options, never inherit them. + * + * WHY THIS EXISTS. gospel-internet had its own copy of the map helper (src/boardEngine.js) whose + * defaults were whiteboard:false / boundaries:true / centre [60,20] / zoom 1.4 — the values a real map + * needs. The library's createWhiteboardMap was extracted for the WHITEBOARD case, so its defaults are + * the exact inverse: whiteboard:true / boundaries:false / [0,0] / zoom 1. Adopting the library while + * GiMap still relied on defaults would have hidden every basemap layer and dropped boundary + * attribution — a blank map in production, from a change that reads like a pure de-duplication. + * + * ★ WHY A NEW GATE RATHER THAN check:gospel-whiteboard. I poisoned GiMap into whiteboard mode and + * check:gospel-whiteboard still exited 0. It asserts chart GEOMETRY (projection, radii, the bounded + * gap) and never imports a component, so it cannot see a call-site option. A pass from a check that + * cannot fail is not evidence, so this asserts the thing that actually regresses. + * + * The rule: every createWhiteboardMap call must pass `whiteboard` EXPLICITLY. A whiteboard caller + * passing true is inheriting nothing; the geographic caller passing false is stating its intent. Either + * way the call reads the same against either module, which is what makes the de-duplication safe. + */ +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' + +const DIR = 'plugin-designs/gospel-internet/src/components' +const CALL = /createWhiteboardMap\s*\(/g + +const pass = [] +const fail = [] +const ok = (label, cond, detail = '') => (cond ? pass : fail).push(detail ? `${label} — ${detail}` : label) + +/** The option object of a call, by brace matching from the call's opening paren. */ +function callOptions(src, at) { + let depth = 0 + let i = src.indexOf('(', at) + const start = i + for (; i < src.length; i++) { + const c = src[i] + if (c === '(') depth++ + else if (c === ')') { depth--; if (depth === 0) break } + } + return src.slice(start, i + 1) +} + +const files = readdirSync(DIR).filter((f) => f.endsWith('.vue')) +let calls = 0 +for (const f of files) { + const src = readFileSync(join(DIR, f), 'utf8') + for (const m of src.matchAll(CALL)) { + calls++ + const args = callOptions(src, m.index) + // `whiteboard:` must appear in the call itself, not be left to the module's default. + ok(`${f}: createWhiteboardMap states whiteboard: explicitly`, /\bwhiteboard\s*:/.test(args), + args.replace(/\s+/g, ' ').slice(0, 90)) + // A geographic caller (whiteboard:false) must also state boundaries, or it silently loses + // boundary attribution to the library default of false. + if (/\bwhiteboard\s*:\s*false/.test(args)) { + ok(`${f}: the geographic caller states boundaries: explicitly`, /\bboundaries\s*:/.test(args)) + } + } +} + +ok('every call site was found and checked', calls >= 4, `${calls} call(s) across ${files.length} components`) + +// NEG CONTROL: the matcher must actually be able to SEE a missing option, or every arm above is +// vacuous. Run it over a call that omits whiteboard and require it to be judged non-compliant. +const bad = "await createWhiteboardMap(el.value, cfg, { token: tk })" +ok('NEG CONTROL: a call omitting whiteboard: is detected', + !/\bwhiteboard\s*:/.test(callOptions(bad, bad.indexOf('createWhiteboardMap')))) +// And a compliant call must NOT trip it, or the gate would just ban the helper. +const good = "await createWhiteboardMap(el.value, cfg, { whiteboard: false, boundaries: true, token: tk })" +ok('NEG CONTROL: a compliant call passes', + /\bwhiteboard\s*:/.test(callOptions(good, good.indexOf('createWhiteboardMap')))) + +for (const p of pass) console.log(` ✓ ${p}`) +for (const f of fail) console.log(` ✗ ${f}`) +if (fail.length) { + console.log(`\nBOARD-MAP-OPTIONS: FAIL — ${fail.length} of ${pass.length + fail.length}`) + process.exit(1) +} +console.log(`\nBOARD-MAP-OPTIONS: PASS — ${pass.length} assertions over ${calls} call sites`) diff --git a/package.json b/package.json index 188d7b29d..8260da4c6 100644 --- a/package.json +++ b/package.json @@ -328,7 +328,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived 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:zoning-declared 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:auto-engine check:engine-contract 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:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:snippet-engine check:snippet-engine:self-test", - "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-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim", + "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-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:host-locale-fetch check:print-frame-reclaim", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", @@ -377,7 +377,8 @@ "check:people-group-records": "bun internal/check-people-group-records.mjs", "check:poster-browser-equiv": "bun internal/check-poster-browser-equiv.mjs", "check:poster-render-pinned": "bun internal/check-poster-render-pinned.mjs", - "pin:poster-render": "node library/scripts/posters/pin-poster-render.mjs" + "pin:poster-render": "node library/scripts/posters/pin-poster-render.mjs", + "check:board-map-options": "node internal/check-board-map-options.mjs" }, "//scripts": { "_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.", @@ -613,7 +614,8 @@ "check:people-group-records": "the records half of the print standard selects EXACTLY the set it is asked for, two-sided: a country selector returns that country and not another, an unknown selector returns nothing rather than every group on earth, one fetch per language serves every item, and a failed fetch is not cached so the next click retries.", "check:poster-browser-equiv": "a map sheet GENERATED IN THE BROWSER (library/packet/posterSheets.js, the print standard: generate at the click) is byte-identical to the one the Node generator writes, given the same records: it runs the real generate.mjs for one UPG-100 zone (the map-only packet sheet, an India zone so the ADM1 path runs) and one country, draws both through the browser feed (the SERVED boundaries, data/zone-sheet.js, the library name table) and compares bytes. The drawing code is one factory (poster-render.js); this guards the FEED, which is what can still drift. NOT MEASURED without the generator's offline records cache.", "check:poster-render-pinned": "the map-sheet generator a browser imports at print time (served at assets/lib/poster-render.js, not bundled) is integrity-pinned like every embed, because it executes on partner pages: the pin in library/packet/poster-render.integrity.js equals the sha384 of the source and of the served copy (a stale pin names `bun run pin:poster-render`), a TAMPERED copy is refused and its code never runs (a planted side effect stays unset), and the genuine copy loads and draws.", - "pin:poster-render": "rewrite library/packet/poster-render.integrity.js from library/packet/poster-render.js — run after any change to the generator; check:poster-render-pinned fails until it is." + "pin:poster-render": "rewrite library/packet/poster-render.integrity.js from library/packet/poster-render.js — run after any change to the generator; check:poster-render-pinned fails until it is.", + "check:board-map-options": "the GEOGRAPHIC map caller must state whiteboard/boundaries EXPLICITLY, never inherit them. library/maps/whiteboard-map.js defaults to whiteboard:true/boundaries:false (it was extracted for the whiteboard case), the inverse of what a real map needs, so a caller relying on defaults renders a blank basemap. check:gospel-whiteboard cannot see this — it asserts chart geometry and imports no component." }, "devDependencies": { "@capacitor/android": "^7.6.9",