691 lines
167 KiB
JSON
691 lines
167 KiB
JSON
{
|
||
"name": "doxa-maps",
|
||
"version": "0.1.0",
|
||
"license": "Apache-2.0",
|
||
"private": true,
|
||
"description": "A portable bundler that generates embeddable JAMstack maps — one library, many map bundles that embed on any site, compose into a lazy-loaded SPA, or wrap a Capacitor Android/iOS app.",
|
||
"type": "module",
|
||
"scripts": {
|
||
"apk:debug": "bun run build && bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug",
|
||
"apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
|
||
"app:start-page": "node internal/set-app-start-page.mjs",
|
||
"browser-gates": "node internal/check-browser-gates-guarded.mjs --list",
|
||
"build": "bash internal/build-lock.sh",
|
||
"build:bundle": "bun x vite build",
|
||
"build:bundles": "node internal/run-parallel.mjs --bundles",
|
||
"build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && node internal/sync-dropin.mjs --from-config --if-present",
|
||
"build:watch": "bun x vite build --watch",
|
||
"cap:sync": "./node_modules/.bin/cap sync android",
|
||
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
|
||
"capability-exemplars": "node internal/generate-capability-exemplars.mjs",
|
||
"check:a11y-button-names": "node internal/check-a11y-button-names.mjs",
|
||
"check:mobile-gating": "node internal/check-mobile-gating.mjs",
|
||
"check:app-start-page": "node internal/set-app-start-page.mjs --check",
|
||
"check:approx-pin-note": "node internal/check-approx-pin-note.mjs",
|
||
"check:auto-engine": "node internal/check-auto-engine.mjs",
|
||
"check:no-internal-role-nouns": "node internal/check-no-internal-role-nouns.mjs",
|
||
"check:plural-forms": "node internal/check-plural-forms.mjs",
|
||
"check:basemap-labels": "node internal/check-basemap-labels-localized.mjs",
|
||
"check:browser-gates-guarded": "node internal/check-browser-gates-guarded.mjs",
|
||
"check:capability-citations": "node internal/check-capability-citations.mjs",
|
||
"check:capability-derived": "node internal/check-capability-derived.mjs",
|
||
"check:capability-exemplars": "node internal/check-capability-exemplars.mjs",
|
||
"check:card-deck": "node internal/check-card-deck.mjs",
|
||
"check:catalog-currency": "node internal/check-catalog-currency.mjs",
|
||
"check:catalog-identity": "node internal/check-catalog-identity.mjs",
|
||
"check:cdn-cache": "bun internal/check-cdn-cache.mjs",
|
||
"check:cdn-pins": "node internal/check-cdn-pins.mjs",
|
||
"check:click-sheet": "node plugin-designs/gospel-resources/scripts/verify-gospel-click-sheet.mjs",
|
||
"check:composed-filters": "node internal/check-gospel-composed-filters.mjs",
|
||
"check:composite": "node internal/check-composite-freshness.mjs",
|
||
"check:clustering-modes": "node plugin-designs/doxa-research-map/scripts/check-clustering-modes.mjs",
|
||
"check:composite-embed-knobs": "node internal/check-composite-embed-knobs.mjs",
|
||
"check:composite-interop": "node internal/check-composite-interop.mjs",
|
||
"check:composite-locale-leaks": "node plugin-designs/research-map-plus-grid/scripts/check-composite-locale-leaks.mjs",
|
||
"check:composite-reg": "node internal/check-composite-registration.mjs",
|
||
"check:composite-srcs": "node internal/check-composite-srcs.mjs",
|
||
"check:composite-token-url": "node internal/check-composite-token-url.mjs",
|
||
"check:composites-mount": "node internal/check-composites-mount.mjs",
|
||
"check:country-names": "node internal/check-country-names.mjs",
|
||
"check:country-sheet-one-path": "node plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs",
|
||
"check:country-sheet-one-path:self-test": "node plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs --self-test",
|
||
"check:coverage-consistent": "node internal/check-gospel-coverage-consistent.mjs",
|
||
"check:coverage-consistent:chained": "VERIFY_MAX_AGE_DAYS=36500 node internal/check-gospel-coverage-consistent.mjs",
|
||
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
||
"check:dashboard-filter-a11y": "node plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs",
|
||
"check:dashboard-geocoder-provider": "node plugin-designs/research-dashboard/scripts/check-dashboard-geocoder-provider.mjs",
|
||
"check:deck-click": "node internal/check-deck-click.mjs",
|
||
"check:dep-names": "node internal/check-dependency-names.mjs",
|
||
"check:describe-group": "node internal/check-describe-group.mjs",
|
||
"check:descriptions": "node internal/check-profile-descriptions.mjs",
|
||
"check:dev-assets": "node internal/check-dev-assets.mjs",
|
||
"check:doc-scripts": "node internal/check-doc-quotes-scripts.mjs",
|
||
"check:readme-size": "node internal/check-readme-web-folder-size.mjs",
|
||
"check:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test",
|
||
"check:drop": "node internal/drop-portability-check.mjs",
|
||
"check:element-census-vs-live": "node internal/check-element-census-vs-live.mjs",
|
||
"check:embed-base": "node internal/check-embed-base-resolution.mjs",
|
||
"check:embed-basemap": "node internal/check-embed-basemap-paints.mjs",
|
||
"check:embed-targets": "node internal/check-embed-targets-exist.mjs",
|
||
"check:embed-targets:self-test": "node internal/check-embed-targets-exist.mjs --self-test",
|
||
"check:embedded-rtl": "node internal/check-embedded-rtl.mjs",
|
||
"check:embedder-keys": "node plugin-designs/regions-dashboard/scripts/check-embedder-keys.mjs",
|
||
"check:english-leaks": "node internal/check-gospel-english-leaks.mjs",
|
||
"check:experimental-shelf": "node internal/check-experimental-shelf.mjs",
|
||
"check:export-poster-prints": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs",
|
||
"check:export-poster-prints:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-poster-prints.mjs --provider maplibre",
|
||
"check:upg-zone-joins": "node internal/check-upg-zone-joins.mjs",
|
||
"check:gospel-set-charts": "node internal/check-gospel-set-charts.mjs",
|
||
"check:gospel-set-fit": "node internal/check-gospel-set-fit.mjs",
|
||
"check:facet-cascade": "node internal/check-facet-cascade.mjs",
|
||
"check:facet-cascade:offline": "node internal/check-facet-cascade.mjs --offline",
|
||
"check:facet-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test",
|
||
"check:facet-shape": "node internal/check-facet-shape.mjs",
|
||
"check:facet-changes-map": "node internal/check-facet-changes-map.mjs",
|
||
"check:facet-changes-map:built": "sh -c 'BASE_URL=${BASE_URL:-http://127.0.0.1:4173} PROBE_PATH=${PROBE_PATH:-/doxa-maps/research-dashboard/research-dashboard/index.html} exec node internal/check-facet-changes-map.mjs'",
|
||
"check:facet-changes-map:self-test": "node internal/check-facet-changes-map.mjs --self-test",
|
||
"check:facet-one-field": "node internal/check-facet-one-field.mjs",
|
||
"check:facet-one-field:self-test": "node internal/check-facet-one-field.mjs --self-test",
|
||
"check:filter-width": "node internal/check-filter-width-stable.mjs",
|
||
"check:filter-multivalue": "node plugin-designs/research-map-plus-grid/scripts/check-filter-multivalue.mjs",
|
||
"check:fits-screen": "node internal/check-fits-the-screen.mjs",
|
||
"check:fits-screen:self-test": "node internal/check-fits-the-screen.mjs --self-test",
|
||
"check:fits-screen:self-test:b": "node internal/check-fits-the-screen.mjs --self-test --self-test-clause=b",
|
||
"check:fits-screen:self-test:c": "node internal/check-fits-the-screen.mjs --self-test --self-test-clause=c",
|
||
"check:fits-screen:self-test:d": "node internal/check-fits-the-screen.mjs --self-test --self-test-clause=d",
|
||
"check:fits-screen:self-test:e": "node internal/check-fits-the-screen.mjs --self-test --self-test-clause=e",
|
||
"check:fixed-anchoring": "node internal/check-fixed-anchoring.mjs",
|
||
"check:frozen-labels": "node internal/check-frozen-labels.mjs",
|
||
"check:frozen-labels:self-test": "node internal/check-frozen-labels.mjs --self-test",
|
||
"check:gallery-embed": "node internal/check-research-gallery-embed.mjs",
|
||
"check:gallery-embed:iframe": "node internal/check-research-gallery-embed.mjs --mode iframe",
|
||
"check:gallery-embed:js": "node internal/check-research-gallery-embed.mjs --mode js",
|
||
"check:gallery-embed:live-cors": "node internal/check-research-gallery-embed.mjs --live-cors",
|
||
"check:gallery-embed:maplibre": "node internal/check-research-gallery-embed.mjs --provider maplibre",
|
||
"check:gated-not-in-tree": "node internal/check-gated-not-in-tree.mjs",
|
||
"check:gospel-all": "node internal/check-gospel-all.mjs",
|
||
"check:gospel-internet": "node plugin-designs/gospel-internet/scripts/check-gospel-internet-model.mjs && node plugin-designs/gospel-internet/scripts/check-gospel-internet-i18n.mjs && node plugin-designs/gospel-internet/scripts/check-language-tree.mjs && node plugin-designs/gospel-internet/scripts/check-gospel-internet-render.mjs",
|
||
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
|
||
"check:gospel-whiteboard": "node internal/check-gospel-whiteboard.mjs",
|
||
"check:hardcoded-english": "node internal/check-hardcoded-english.mjs",
|
||
"check:hardcoded-english:self-test": "node internal/check-hardcoded-english.mjs --self-test",
|
||
"check:embed-contract": "node internal/check-gospel-embed-contract.mjs",
|
||
"check:engine-contract": "node internal/check-gospel-engine-contract.mjs",
|
||
"check:link-coverage": "node internal/check-link-verification-coverage.mjs",
|
||
"dropin": "node internal/sync-dropin.mjs --from-config",
|
||
"gospel:bridge-reach": "node plugin-designs/gospel-resources/scripts/build-bridge-reach-table.mjs",
|
||
"gospel:field-cards": "node plugin-designs/gospel-resources/scripts/build-field-cards.mjs",
|
||
"check:link-coverage:poison": "node internal/check-link-verification-coverage.mjs --poison",
|
||
"check:engine-contract:poison": "node internal/check-gospel-engine-contract.mjs --poison",
|
||
"check:embed-contract:poison": "node internal/check-gospel-embed-contract.mjs --poison",
|
||
"check:map-attribution": "node internal/check-map-attribution.mjs",
|
||
"check:map-control-overlap": "node internal/check-map-control-overlap.mjs",
|
||
"check:studio-rail:self-test": "node internal/check-studio-rail.mjs --self-test",
|
||
"check:studio-rail": "node internal/check-studio-rail.mjs",
|
||
"check:translator-contacts": "node internal/check-translator-contacts.mjs",
|
||
"check:translator-contacts:poison": "node internal/check-translator-contacts.mjs --poison",
|
||
"check:heartbeat": "node internal/check-heartbeat-threshold.mjs",
|
||
"check:heartbeat:self-test": "node internal/check-heartbeat-threshold.mjs --self-test",
|
||
"check:help-reachable": "node internal/check-help-reachable.mjs",
|
||
"check:hover-channel": "node plugin-designs/research-map-plus-grid/scripts/check-hover-channel.mjs",
|
||
"check:host-locale-contract": "node internal/check-host-locale-contract.mjs",
|
||
"check:i18n-keys-exist": "node plugin-designs/research-map-plus-grid/scripts/check-i18n-keys-exist.mjs",
|
||
"check:i18n-never-translated": "node internal/check-i18n-never-translated.mjs",
|
||
"check:i18n-parity": "node internal/check-i18n-parity.mjs",
|
||
"check:sentence-slots": "node internal/check-sentence-slots.mjs",
|
||
"check:i18n-stopped-scope": "node internal/check-i18n-stopped-scope.mjs",
|
||
"check:i18n-treeshake": "node internal/check-i18n-treeshake.mjs",
|
||
"check:instance-config-keys": "node internal/check-instance-config-keys.mjs",
|
||
"check:kit-options-reachable": "node plugin-designs/research-dashboard/scripts/check-kit-options-reachable.mjs",
|
||
"check:kpi-unit": "node internal/check-kpi-unit-once.mjs",
|
||
"check:label-joins": "node internal/check-label-joins.mjs",
|
||
"check:language-code-link": "node plugin-designs/doxa-research-map/scripts/check-language-code-link.mjs",
|
||
"check:whiteboard-projection": "node internal/check-whiteboard-projection.mjs",
|
||
"check:frame-target": "node internal/check-frame-target.mjs",
|
||
"check:language-tree-model": "node internal/check-language-tree-model.mjs",
|
||
"check:tiered-points": "node internal/check-tiered-points.mjs",
|
||
"check:language-tree-layout": "node internal/check-language-tree-layout.mjs",
|
||
"check:layout-key-namespaced": "node plugin-designs/research-dashboard/scripts/check-layout-key-namespaced.mjs",
|
||
"check:leaf-engine-per-provider": "node internal/check-leaf-engine-per-provider.mjs",
|
||
"check:leaf-hints": "node internal/check-leaf-hints.mjs",
|
||
"check:leaf-english-leaks": "node internal/check-leaf-english-leaks.mjs",
|
||
"check:leaf-english-leaks:self-test": "node internal/check-leaf-english-leaks.mjs --self-test",
|
||
"check:leaves-mount": "node internal/check-gospel-leaves-mount.mjs",
|
||
"check:leaves-mount:poison": "node internal/check-gospel-leaves-mount.mjs --poison",
|
||
"check:legend-data-contract": "node internal/check-legend-data-contract.mjs",
|
||
"check:legend-export-reachable": "node internal/check-legend-export-reachable.mjs",
|
||
"check:legend-logical-insets": "node internal/check-legend-logical-insets.mjs",
|
||
"check:legend-logical-insets:self-test": "node internal/check-legend-logical-insets.mjs --self-test",
|
||
"check:legend-mirrors": "node plugin-designs/research-dashboard/scripts/check-legend-mirrors.mjs",
|
||
"check:legend-source": "node internal/check-legend-matches-source.mjs",
|
||
"check:library-index": "node internal/check-library-index.mjs",
|
||
"check:link-rot": "node internal/check-link-rot-sample.mjs",
|
||
"check:linked-vocabulary": "node plugin-designs/research-map-plus-grid/scripts/check-linked-vocabulary.mjs",
|
||
"check:list-prop-unit": "node internal/check-list-prop-unit.mjs",
|
||
"check:list-selection": "node internal/check-list-selection.mjs",
|
||
"check:locale-reachable": "node internal/check-locale-reachable.mjs",
|
||
"check:manifest-covers": "node internal/check-manifest-covers-discovery.mjs",
|
||
"check:map-data-painted": "node internal/check-map-data-painted.mjs",
|
||
"check:map-data-painted:poison": "node internal/check-map-data-painted.mjs --poison",
|
||
"check:map-embed-cells": "node internal/check-map-embed-cells.mjs",
|
||
"check:map-fly-reduced-motion": "node internal/check-map-fly-reduced-motion.mjs",
|
||
"check:map-paints": "node internal/check-gospel-map-paints.mjs",
|
||
"check:map-paints:poison": "node internal/check-gospel-map-paints.mjs --poison",
|
||
"check:map-props-unit": "node internal/check-map-props-unit.mjs",
|
||
"check:map-unmount-mid-init": "node internal/check-map-unmount-mid-init.mjs",
|
||
"check:map-unmount-race": "node internal/check-map-unmount-guard.mjs",
|
||
"check:mapbox-drew": "node internal/check-mapbox-drew.mjs",
|
||
"check:maplibre-drops": "node internal/check-maplibre-layer-drops.mjs",
|
||
"check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs",
|
||
"check:mjs-parse": "node internal/check-mjs-parses.mjs",
|
||
"check:mobile": "bun run cap:sync && bun run cap:verify",
|
||
"check:mst-clustering": "node internal/check-mst-clustering.mjs",
|
||
"check:nav-reachable": "node internal/check-nav-reachable-mobile.mjs",
|
||
"check:nested-token": "node internal/check-nested-token-delivery.mjs",
|
||
"check:no-abs-paths": "node internal/check-no-absolute-paths.mjs",
|
||
"check:no-codename": "node internal/check-no-internal-codename.mjs",
|
||
"check:no-conflict-markers": "node internal/check-no-conflict-markers.mjs",
|
||
"check:no-conflict-markers:poison": "node internal/check-no-conflict-markers.mjs --poison",
|
||
"check:no-credentials-omit": "node internal/check-no-credentials-omit.mjs",
|
||
"check:no-credentials-omit:self-test": "node internal/check-no-credentials-omit.mjs --self-test",
|
||
"check:no-cross-plugin-imports": "node internal/check-no-cross-plugin-imports.mjs",
|
||
"check:no-cross-plugin-imports:self-test": "node internal/check-no-cross-plugin-imports.mjs --self-test",
|
||
"check:no-dead-library": "node internal/check-no-dead-library.mjs",
|
||
"check:no-eval": "node internal/check-no-eval.mjs",
|
||
"check:no-free-identifiers": "node plugin-designs/research-map-plus-grid/scripts/check-no-free-identifiers.mjs",
|
||
"check:no-orphan-gates": "node internal/check-no-orphan-gates.mjs",
|
||
"check:no-public-binds": "node internal/check-no-public-binds.mjs",
|
||
"check:no-silent-engine": "node internal/check-no-silent-engine-substitution.mjs",
|
||
"check:no-silent-engine:self-test": "node internal/check-no-silent-engine-substitution.mjs --self-test",
|
||
"check:one-palette": "node internal/check-one-palette.mjs",
|
||
"check:one-palette:self-test": "node internal/check-one-palette.mjs --self-test",
|
||
"check:optional-t-equivalence": "node internal/check-optional-t-equivalence.mjs",
|
||
"check:package-json-unique-keys": "node internal/check-package-json-unique-keys.mjs",
|
||
"check:no-repo-pb-hooks": "node internal/check-no-repo-pb-hooks.mjs",
|
||
"check:packet-charts": "node internal/check-packet-charts.mjs",
|
||
"check:packet-browser": "node internal/check-packet-browser.mjs",
|
||
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
|
||
"check:packet-map-missing-loud": "node internal/check-packet-map-missing-loud.mjs",
|
||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||
"check:packet-render:expect-funnel": "node internal/check-packet-render.mjs --expect-funnel",
|
||
"check:series-isolation": "node internal/check-series-isolation.mjs",
|
||
"check:select-zone-race": "node internal/check-select-zone-race.mjs",
|
||
"check:selection-kinds": "node internal/check-selection-kinds.mjs",
|
||
"check:selector-fields": "node internal/check-selector-fields.mjs",
|
||
"check:selector-fields:self-test": "node internal/check-selector-fields.mjs --self-test",
|
||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
|
||
"check:packet-variants": "node internal/check-packet-variants.mjs",
|
||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||
"check:packet-a1-structure": "node internal/check-packet-a1-structure.mjs",
|
||
"check:india-states": "node internal/check-india-states.mjs",
|
||
"check:batch-selection-export": "node internal/check-batch-selection-export.mjs",
|
||
"check:zone-title-tier-a": "node internal/check-zone-title-tier-a.mjs",
|
||
"check:evangelical-share": "node internal/check-evangelical-share.mjs",
|
||
"check:map-labels": "node internal/check-map-labels.mjs",
|
||
"check:packet-offline": "node internal/build-packet-fixtures.mjs",
|
||
"check:packets-all": "node internal/check-packets-all.mjs",
|
||
"check:parse": "node internal/inline-parse-check.mjs",
|
||
"check:pinned-umd": "node internal/check-pinned-umd.mjs",
|
||
"check:popup-escaping": "node internal/check-popup-escaping.mjs",
|
||
"check:ported-globals-identical": "node internal/check-ported-globals-identical.mjs",
|
||
"check:one-scroll-container": "node internal/check-one-scroll-container.mjs",
|
||
"check:list-pattern-doc": "node internal/check-list-pattern-doc.mjs",
|
||
"check:pin-selection": "node internal/check-pin-selection.mjs",
|
||
"check:poster-action-print": "node internal/check-poster-action-print.mjs",
|
||
"check:popup-href-escaped": "node internal/check-popup-href-escaped.mjs",
|
||
"check:popup-pan-controls": "node internal/check-popup-pan-clears-controls.mjs",
|
||
"check:country-map-refetch": "node internal/check-country-map-refetch.mjs",
|
||
"check:pane-config-reforward": "node internal/check-pane-config-reforward.mjs",
|
||
"check:control-inset": "node internal/check-control-inset-measured.mjs",
|
||
"check:viewport-meta": "node internal/check-viewport-meta.mjs",
|
||
"check:viewport-meta:self-test": "VIEWPORT_META_SELFTEST=1 node internal/check-viewport-meta.mjs",
|
||
"check:poster-attribution": "node internal/check-poster-attribution.mjs",
|
||
"check:poster-layout-optin": "node internal/check-poster-layout-optin.mjs",
|
||
"check:window-globals-released": "node internal/check-window-globals-released.mjs",
|
||
"check:poster-card-fits": "node internal/check-poster-card-fits.mjs",
|
||
"check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs",
|
||
"check:poster-design-catalogue": "node plugin-designs/poster-design/scripts/check-catalogue.mjs",
|
||
"check:poster-fetch-guard": "node internal/check-poster-fetch-guard.mjs",
|
||
"check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs",
|
||
"check:posters-exportable": "node internal/check-posters-exportable.mjs",
|
||
"check:posterverse-shelf": "node plugin-designs/posterverse/scripts/check-posterverse-shelf.mjs",
|
||
"check:poster-prints": "node internal/check-poster-prints.mjs",
|
||
"check:poster-raster": "node internal/check-poster-raster.mjs",
|
||
"check:poster-wall-localised": "node internal/check-poster-wall-localised.mjs",
|
||
"check:prayer-walk-planner": "node internal/check-prayer-walk-planner.mjs",
|
||
"check:preset-config-merge": "node internal/check-preset-config-merge.mjs",
|
||
"check:print-geometry": "node internal/check-print-geometry.mjs",
|
||
"check:print-frame-reclaim": "node internal/check-print-frame-reclaim.mjs",
|
||
"check:pin-dim-slider": "node plugin-designs/doxa-research-map/scripts/check-pin-dim-slider.mjs",
|
||
"check:profile-table": "node internal/check-profile-table.mjs",
|
||
"check:profiles-mount": "node internal/check-composites-mount.mjs --all-profiles",
|
||
"check:provider-for": "node internal/check-provider-for.mjs",
|
||
"check:provider-switch": "node internal/check-provider-switch.mjs",
|
||
"check:public-hygiene": "node internal/public-hygiene-check.mjs",
|
||
"check:published": "node internal/check-published-matches-build.mjs",
|
||
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs",
|
||
"check:reduced-motion": "node internal/check-reduced-motion.mjs",
|
||
"check:region-fill-repaint": "node internal/check-region-fill-repaint.mjs",
|
||
"check:region-paint-single-source": "node internal/check-region-paint-single-source.mjs",
|
||
"check:rtl-shaping": "node internal/check-gospel-rtl-shaping.mjs",
|
||
"check:rtl-shaping:poison": "node internal/check-gospel-rtl-shaping.mjs --poison",
|
||
"check:ring-vocabulary": "node internal/check-ring-vocabulary.mjs",
|
||
"check:rtl-raw": "node internal/check-raw-map-rtl-shaping.mjs",
|
||
"check:universal-props": "node internal/check-universal-props.mjs --report",
|
||
"check:rtl-raw:poison": "node internal/check-raw-map-rtl-shaping.mjs --poison",
|
||
"check:scrim-viewport": "node internal/check-scrim-covers-viewport.mjs",
|
||
"check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test",
|
||
"check:script-flags": "node internal/check-script-flags-reachable.mjs",
|
||
"check:script-registry": "node internal/check-script-registry.mjs",
|
||
"check:settled": "node internal/settled-tree-gate.mjs",
|
||
"check:provide-shadowing": "node internal/check-provide-shadowing.mjs",
|
||
"check:element-bootstrap": "node internal/check-element-bootstrap.mjs",
|
||
"check:map-menu-anchor": "node internal/check-map-menu-anchor.mjs",
|
||
"check:legend-top-offset": "node internal/check-legend-top-offset.mjs",
|
||
"check:mercator-slice": "node internal/check-mercator-slice.mjs",
|
||
"check:embed-url-absolute": "node internal/check-embed-url-absolute.mjs",
|
||
"check:grid-image-loading": "node internal/check-grid-image-loading.mjs",
|
||
"check:retry-fetch": "node internal/check-retry-fetch.mjs",
|
||
"check:grid-merge-defaults": "node internal/check-grid-merge-defaults.mjs",
|
||
"check:setup-order": "node internal/check-setup-order.mjs",
|
||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||
"check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs",
|
||
"check:shadow-style-labels-unique": "node internal/check-shadow-style-labels-unique.mjs",
|
||
"check:shadow-reduced-motion": "node internal/check-shadow-reduced-motion.mjs",
|
||
"check:regions-uses-library": "node plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs",
|
||
"check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs",
|
||
"check:ring-bands": "node internal/check-ring-bands.mjs",
|
||
"check:share-policy": "node internal/check-share-policy.mjs",
|
||
"check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs",
|
||
"check:shell-scroll": "node internal/verify-shell-scroll.mjs",
|
||
"check:shot-distinctness": "node internal/check-shot-distinctness.mjs",
|
||
"check:shot-fidelity": "node internal/check-shot-fidelity.mjs",
|
||
"check:shot-fidelity:strict": "node internal/check-shot-fidelity.mjs --strict",
|
||
"check:shot-fonts": "node internal/check-shot-fonts.mjs",
|
||
"check:shots": "node internal/check-shot-freshness.mjs",
|
||
"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:snippet-engine": "node internal/check-snippet-engine.mjs",
|
||
"check:snippet-engine:self-test": "node internal/check-snippet-engine.mjs --self-test",
|
||
"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",
|
||
"check:surfaces-read-the-tier": "node internal/check-surfaces-read-the-tier.mjs",
|
||
"check:surfaces-read-the-tier:poison": "node internal/check-surfaces-read-the-tier.mjs --poison",
|
||
"check:synced-css": "node internal/check-synced-css-copies.mjs",
|
||
"check:synced-css:self-test": "node internal/check-synced-css-copies.mjs --self-test",
|
||
"check:tab-radius": "node internal/check-tab-radius-token.mjs",
|
||
"check:tab-radius:self-test": "node internal/check-tab-radius-token.mjs --self-test",
|
||
"check:text-highlight": "node plugin-designs/regions-dashboard/scripts/check-text-highlight.mjs",
|
||
"check:tabs-localized": "node internal/check-tabs-localized.mjs",
|
||
"check:targets": "node internal/check-targets.mjs",
|
||
"check:targets:self-test": "node internal/check-targets.mjs --self-test",
|
||
"check:tauri": "node internal/check-tauri-config.mjs",
|
||
"check:template-literal": "node internal/template-literal-check.mjs",
|
||
"check:tree-relative": "node internal/check-tree-relative.mjs",
|
||
"check:tree-weight": "node internal/check-published-tree-weight.mjs",
|
||
"check:upg-map-data": "node internal/check-upg-map-data-painted.mjs",
|
||
"check:url-resolver": "node internal/check-url-resolver.mjs",
|
||
"check:vision-card-numbers": "node internal/check-vision-card-numbers.mjs",
|
||
"check:wp-embed": "php deployment/wordpress/tests/wp-embed-contract-test.php",
|
||
"check:zone-census": "node internal/check-zone-census.mjs",
|
||
"check:zoning-declared": "node internal/check-zoning-declared.mjs",
|
||
"check:zoning-declared:self-test": "node internal/check-zoning-declared.mjs --self-test",
|
||
"clean": "node internal/clean.mjs",
|
||
"deploy:dev": "bun run build && bun run dropin",
|
||
"desktop:build": "node internal/desktop-build.mjs",
|
||
"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: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:script-flags 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: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:region-paint-single-source 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:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||
"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:india-states check:batch-selection-export 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-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
|
||
"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",
|
||
"lists": "node library/scripts/people-groups/build-lists.mjs",
|
||
"packet-fixture": "node internal/generate-packet-fixture.mjs",
|
||
"packet:ci": "node internal/build-packet-fixtures.mjs",
|
||
"packets": "cd library/scripts/posters && VITE_API_BASE_URL=https://pray.doxa.life node packet.mjs",
|
||
"phone": "bun run apk:debug && bun run apk:install",
|
||
"phone:deep-research": "APP_START=doxa-maps-spa/doxa-maps-spa/app bun run phone:from-tree",
|
||
"phone:from-tree": "bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk",
|
||
"posters": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs",
|
||
"posters:export": "cd library/scripts/posters && node export-public.mjs",
|
||
"posters:gallery": "cd library/scripts/posters && node build-gallery.mjs && node build-hd-gallery.mjs",
|
||
"posters:one": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs",
|
||
"posters:previews": "cd library/scripts/posters && python3 make-previews.py",
|
||
"posters:shoot": "cd library/scripts/posters && DSF=2 node shoot.mjs",
|
||
"preview": "bun x vite preview",
|
||
"probe:legend-columns": "node internal/legend-columns-probe.mjs",
|
||
"serve": "node internal/serve-strict.mjs",
|
||
"serve:permissive": "python3 -m http.server 4173 --bind 127.0.0.1 --directory public",
|
||
"serve:shell": "python3 -m http.server 4174 --bind 127.0.0.1 --directory library/layouts/app-wrapper-material",
|
||
"shell": "node internal/install-shell.mjs --no-token",
|
||
"shots": "node internal/readme-shots.mjs",
|
||
"smoke": "node internal/smoke.mjs",
|
||
"smoke:aria-labels": "sh -c 'B=${BASE_URL:-http://127.0.0.1:4173}; exec node internal/check-aria-labels-localized.mjs \"$B/doxa-maps/my-upg-100-list/upg-100-list/index.html\" es,ar,zh'",
|
||
"smoke:embeds": "node internal/embed-smoke.mjs",
|
||
"smoke:gallery": "node internal/plugin-gallery-smoke.mjs",
|
||
"smoke:legend-info": "sh -c 'B=${BASE_URL:-http://127.0.0.1:4173}; exec node internal/check-legend-info-localized.mjs \"$B/doxa-maps/my-upg-100-list/upg-100-list/index.html\" es,ar,zh'",
|
||
"smoke:playground": "bun run smoke:gallery",
|
||
"smoke:share": "node internal/share-roundtrip-smoke.mjs",
|
||
"smoke:share-all": "sh -c 'B=${BASE_URL:-http://127.0.0.1:4173}; exec node internal/share-roundtrip-smoke.mjs --url \"$B/doxa-maps/prayer-walk-planner/prayer-walk-planner/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/all-people-groups/all-people-groups/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/cities-internet/connectivity-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/country-posters/country-posters-mfe/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/demo-map/demo-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/doxa-research-map/research-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/doxa-simple-map/doxa-simple-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/gospel-resources/resources-map/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/research-dashboard/research-dashboard/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/dashboard-plus-grid/dashboard-plus-grid/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html?tk=$MAPBOX_TOKEN\" --url \"$B/doxa-maps/my-upg-100-list/upg-100-mfe/index.html?tk=$MAPBOX_TOKEN\" --browser chromium'",
|
||
"smoke:studio": "node internal/smoke-studio.mjs",
|
||
"sync:dropin": "node internal/sync-dropin.mjs",
|
||
"tauri:build": "./node_modules/.bin/tauri build",
|
||
"token": "node internal/write-token.mjs",
|
||
"tree": "node internal/generate-tree.mjs",
|
||
"verify": "node library/scripts/research/verify-all.mjs",
|
||
"verify:5-caps": "node internal/verify-5-capabilities.mjs",
|
||
"verify:browser-all": "node internal/verify-browser-all.mjs",
|
||
"verify:browser-all:plugin-gallery": "node internal/verify-browser-all.mjs --plugin-gallery",
|
||
"verify:full": "node library/scripts/research/verify-all.mjs --full",
|
||
"verify:gate1": "node internal/acceptance/big-picture-acceptance.mjs",
|
||
"verify:gospel-map": "node internal/verify-gospel-map.mjs",
|
||
"verify:interop": "node internal/acceptance/interop-harness.mjs",
|
||
"verify:interop:self-test": "node internal/acceptance/interop-harness.mjs --self-test",
|
||
"verify:interop:self-test:camera-freeze": "node internal/acceptance/interop-harness.mjs --self-test --self-test-mode=camera-freeze",
|
||
"verify:map-paint": "node internal/verify-map-paint.mjs",
|
||
"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",
|
||
"check:wall-wysiwyg": "node plugin-designs/wall-of-people-groups/scripts/check-wall-wysiwyg.mjs",
|
||
"check:wall-print-watch": "node plugin-designs/wall-of-people-groups/scripts/check-wall-print-watch.mjs",
|
||
"check:wall-no-bg-photos": "node plugin-designs/wall-of-people-groups/scripts/check-wall-no-bg-photos.mjs",
|
||
"check:wall-pan": "node plugin-designs/wall-of-people-groups/scripts/check-wall-pan.mjs",
|
||
"check:template-i18n": "node plugin-designs/template-bundle/scripts/check-template-i18n.mjs",
|
||
"check:snippets-draw": "node internal/check-snippets-draw.mjs",
|
||
"check:snippets-draw:mapbox": "node internal/check-snippets-draw.mjs --mapbox",
|
||
"pin:poster-render": "node library/scripts/posters/pin-poster-render.mjs",
|
||
"check:board-map-options": "node internal/check-board-map-options.mjs",
|
||
"check:panel-sheets": "node internal/check-panel-sheets.mjs",
|
||
"check:poster-wall-host-seams": "node internal/check-poster-wall-host-seams.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.",
|
||
"browser-gates": "lists every package.json script that launches a browser (the same detection run-parallel uses to route them through internal/browser-slot.sh). Run each as internal/browser-slot.sh bun run <name>.",
|
||
"cap:sync": "NOT `bunx` — bunx is not on PATH in this environment (command not found, exit 127), which broke cap:sync -> apk:debug -> phone at step one. The CLI IS installed locally (@capacitor/cli 7.6.9); invoke the local bin directly. The rest of package.json already uses the spaced `bun x` form.",
|
||
"capability-exemplars": "regenerate documentation/reference-capability-exemplars.md — the DERIVED answer to \"which bundle is the capability-N example?\", a question answered WRONGLY in three separate artifacts (a gate, the README, the /plugin-designer skill).",
|
||
"check:a11y-button-names": "every VISIBLE button must have an accessible name (aria-label / aria-labelledby / visible text — a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
|
||
"check:mobile-gating": "one phone flag per map (SPEC-MOBILE-FLAG): every <profile>.meta.json `mobile` is desktop-only or larger-screens; every flagged profile's leaf page carries its flag (and no leaf carries one its meta dropped); the Research Gallery derives its rules from the manifest, holds no id set, and mounts nothing on a phone before the flags land; dashboard-plus-grid stays desktop-only; the three notice sentences exist in all 11 locales and the desktop-only one names the map; ProfileLoader blocks, advises, and stays quiet inside the Gallery. --self-test plants one bad case per rule.",
|
||
"check:approx-pin-note": "people-group pins say they are localities, not towns or boundaries, in ONE shared wording (library/components/legends/ApproxPinNote.vue): the colocation index finds groups sharing a coordinate, the note names them only for a shared point, never prints a raw coordinate, and renders from each locale's catalogue. Headless.",
|
||
"check:auto-engine": "BROWSER. The default engine (auto) is MapLibre, token or not: on one map leaf, no token AND a host token alone both draw with MapLibre, paint, and make zero api.mapbox.com requests; ?provider=mapbox with that token draws Mapbox and carries it. In gates:post, NOT MEASURED outside the browser slot.",
|
||
"check:no-internal-role-nouns": "shipped text (library, plugins, internal, docs, skills, README) names no internal process role: a line that uses the role noun and is not in internal/fixtures/internal-role-nouns-baseline.json FAILS. Forward-only: the baseline, keyed by file and line text with counts (not line numbers), may only shrink as owners sweep their areas (--shrink tidies it; it never grows). Matches the role noun only, not a GPU or device driver; the word is assembled at runtime, so the gate does not carry it. Node only.",
|
||
"check:no-orphan-gates": "every gate-shaped internal/*.mjs is reachable or tagged @one-shot; and every check:/verify:/smoke: script is run by a standing chain, matched by EXACT name (gates:pre, gates:post, the browser suite GATES, the build, and what those bun run), or declared in internal/fixtures/on-demand-checks.json with its reason, or in the shrink-only baseline of the 2026-09-29 orphans. A chained :self-test does not chain its gate. --shrink / --init maintain the baseline. And every gate-shaped FILE under plugin-designs/ or library/ is run by PATH from a script or another file's code (comments stripped; a basename counts only when no other gate file shares it), tagged @one-shot, or in the baseline's `files` list.",
|
||
"check:snippets-draw": "BROWSER. Every production profile's published embed.script, pasted VERBATIM on a partner page on another origin (only YOUR_TREE_URL / YOUR_SOURCE filled), must DRAW: a hooked MapLibre instance with its style loaded, a sized canvas and tiles 2xx, no failure note, 0 page errors (a no-map profile: a rendered shadow tree with text). Composites that open on a list are pressed to their map view first (settle rule). Ported from the snipgate browser probe. NOT MEASURED outside the browser slot.",
|
||
"check:snippets-draw:mapbox": "BROWSER. The same over every embed.scriptMapbox, verbatim with ONLY YOUR_MAPBOX_TOKEN filled from env MAP_TOKEN, and the map must draw on Mapbox fetching no maplibre-gl. NOT MEASURED without a token, never a pass; the token is redacted from every logged line.",
|
||
"check:wall-wysiwyg": "the Wall's screen IS its poster: the Bless view's on-screen poster and its print come from one layout, so for every preset (3x4ft, 2x3ft, 5x8ft) the sheets, the 2,107 card rects, the card text, the verse reference and the chrome agree. Two-sided on planted layout defects. Headless.",
|
||
"check:wall-print-watch": "the Wall's HD poster print never prints silently incomplete: with fake timers and a fake frame, the watch counts photos as they load, counts the ones that could not, and reports both. Two-sided on planted printWatch copies. Headless.",
|
||
"check:wall-no-bg-photos": "the Wall's store downloads no photos on its own (it used to test all 932, 132 MB, on every visit): the REAL store runs through Vite against the committed fixtures/pray-list-sample.json with fetch stubbed and every Image request counted. Two-sided with the old background test planted. Headless, no network.",
|
||
"check:wall-pan": "the Wall pans and zooms with the pointer 1:1: the REAL WallCanvas methods (through Vite) with d3-zoom's transform arithmetic, over 12 fits and zooms: a pan moves every card exactly as far as the pointer, a zoom keeps the point under the pointer, toWorld inverts toScreen, and a fling never loses the wall. Two-sided: the old transform-inside-the-fit formulas are planted and caught. Headless.",
|
||
"check:template-i18n": "the starter template every new plugin copies renders its text from the catalogue: profile B, server-rendered with the REAL template.json in en, ar and zh, takes its heading, note and sentence from the page's language. Two-sided with the English catalogue served as Arabic. Headless (SSR).",
|
||
"check:coverage-consistent:chained": "every clause of check:coverage-consistent EXCEPT the link-verification age (VERIFY_MAX_AGE_DAYS set out of reach): the chains run what depends on the code and data, and leave what depends on the calendar to the on-demand run, so the gates cannot turn red on a date with no change.",
|
||
"check:viewport-meta": "every generated page declares <meta name=\"viewport\"> with width=device-width. Without it a browser lays the page out at the 980px desktop fallback even when the viewport is set to 390, so a mobile probe silently measures a viewport no phone has and still passes (measured: innerWidth 980 without the tag, 390 with it). width=device-width is required specifically, because a meta carrying only initial-scale still lays out at 980. Checked on the BUILT tree, not the generator template. Fixed-size print sheets are exempt by their stated shape (a .stage with a three-digit px width), and a page that must render at a fixed desktop width carries a viewport-meta-ok: reason. 8 self-tests. Source-only, headless.",
|
||
"check:viewport-meta:self-test": "plants each check:viewport-meta violation - no meta at all, and a meta with initial-scale but no width=device-width - and confirms both are caught, that correct tags in either quote style or uppercase pass, and that the print-sheet exemption applies to a fixed-px stage but NOT to a responsive one.",
|
||
"check:plural-forms": "the three things a CLDR pluralRules map can silently break. (1) FORM COUNT: a locale must author as many forms as it has CLDR categories or the pick clamps onto the wrong one (ru 4, ar 6). (2) THE tStatic PATH: seven bundles install no vue-i18n and read through tStatic, which does NO plural selection, so a pipe key there reaches the page as the literal \"a | b | c\". (3) ZERO: a locale with an explicit CLDR zero category must author it. Written as the precondition for installing the map, and kept as the guard after.",
|
||
"check:basemap-labels": "the BASEMAP's place names follow the reader's language on BOTH engines (library/maps/localize-labels.js): reads the RENDERED country labels at ?lang=en and ?lang=ru — Mapbox via the Standard basemap's place-labels featureset, MapLibre via each symbol's evaluated text — and requires them to flip. Serves this checkout on an OS-assigned port; --dir <tree> for a negative control. Negative-tested: a bundle built before 319064a1 FAILS on both engines.",
|
||
"check:browser-gates-guarded": "no repo file imports playwright except through internal/lib/playwright-guarded.mjs, whose launch refuses (exit 75, NOT MEASURED) unless internal/browser-slot.sh has exported DOXA_BROWSER_SLOT. Browsers run only on the browser seat, inside the slot; this makes that rule enforced, not remembered.",
|
||
"check:capability-citations": "a doc that names a bundle as \"capability N\" must match manifest.json, which is what the BUILD computed. README captioned doxa-vision-card as cap-1; it ships 2 instances so it is parameterized (cap-2). The SAME error was fixed once in verify-5-capabilities.mjs and never reached the README — two copies of one fact, nothing forcing agreement.",
|
||
"check:capability-derived": "every bundle lands on a real capability 1-5, never 0, and a DECLARED tier must agree with the composes edges it declares. Tiers 3-5 used to be authored only - discover.mjs infers single and parameterized and nothing else - so a composite whose author forgot a meta.json shipped as capability 1; research-dashboard did exactly that while nesting ten views, and a missing file is indistinguishable from a deliberate choice, so nothing failed. The declaration stays authoritative: it is CHECKED, not overridden, because a meta that contradicts the structure means one of the two is stale and picking silently is how a tier rots. Two traps found by running the rule BEFORE writing it: composes is per-PROFILE at source but hoisted to the BUNDLE in the manifest, so deriving from the bundle row gave a composite's edges to its plain sibling leaves (11 false failures on correct data); and refs come in two forms in one field, 'bundle/profile' and a bare 'bundle', so resolving only the first demoted plugin-studio from 5 to 4. With both handled: 10 composing profiles, 10 agree, 0 disagree. An unresolved ref is REPORTED, never counted as 'not a composite' - a typo and a genuine plain child look identical from here. Negative-tested on the research-dashboard shape (a composite declared single).",
|
||
"check:capability-exemplars": "REGENERATES and content-compares (not mtimes: a full build rewrites manifest.json every time, so an mtime rule would cry wolf after every build and get switched off).",
|
||
"check:catalog-identity": "proves the LIVE server serves the CATALOG’s exact bytes, by hashing each live bundle and comparing sha384 against the manifest’s own integrity field. An HTTP 200 cannot do this: /app/doxa-maps/ returns 200 with a 2872-byte HTML SIGN-IN PAGE. Network-dependent, so NOT in build:unlocked and NOT in CI; SKIPs cleanly when unreachable. CATALOG=<dir> also requires the live manifest to match a catalog checkout.",
|
||
"check:cdn-cache": "the container's /cdn cache headers, over real HTTP against a server it starts on 127.0.0.1: moving files (manifest, runtime config, html, bundle entries) max-age=300, hashed names a year + immutable, other assets an hour; every response has an ETag and answers If-None-Match with 304. By the repo owner. Headless; in gates:pre.",
|
||
"check:cdn-pins": "every runtime third-party script/stylesheet in shipped code is an exact version listed in library/constants/cdn.js with its SRI hash (no floating ranges). --online re-fetches each and compares the hash.",
|
||
"check:click-sheet": "the COUNTRY-CLICK SHEET without a GPU: every country in the roster resolves to >=1 pin, every link the sheet would render is absolute, and has_audio stays the STRING \"unknown\" where the pool was never checked (collapsing it to false would read as \"no audio exists\"). It had been CRASHING on a path deleted when data moved out of public/, and its two inputs were a private by-country copy and the curated 2,106-row subset the sheet stopped reading — so it was unrunnable AND aimed at files the surface no longer uses. Joins by ISO3 now, not by country-name substring. Carries its own 4-case self-test. Static, sub-second.",
|
||
"check:composed-filters": "CELL C — the gospel map composed above its own tables. Asserts the composite keeps the reader INSIDE it: \"Every resource in <language>\" must be a <button> that filters in place, not an <a> that navigates away and throws out the map the reader just positioned. Checks the tag, that the URL did not change, that the table NARROWED (row count down but not to zero), that the pin count survived, and that nothing 4xx'd. Picks a tier-2 pin from the live source — the first version opened pin 0, a group with nothing found, and failed a working composite on an empty table. Browser gate: run under both rendezvous locks, product lock first.",
|
||
"check:composite": "a COMPOSITE is a COPY taken at its own build time: fails if a nested child's source is newer than the parent's bundle. NOT in build:unlocked on purpose — there everything was just built, so it could never catch the real case (a PARTIAL build). Run it before PUBLISH.",
|
||
"check:composite-embed-knobs": "a composite must pass the HOST's tk and provider down to the map it nests, proven on a genuinely foreign origin (tree served on one port, partner page on another). Our own leaves bake a token and assign window.MAP_TOKEN, so this only ever breaks where there is no leaf - the partner page the embed snippet is written for, and the case nothing else tested. Keyless maplibre asserts ZERO requests to api.mapbox.com, because 'it rendered' would pass on a build that quietly fell back with a baked key.",
|
||
"check:clustering-modes": "every option in the research map's clustering picker must DRAW something different. Choosing 'Native' changed nothing: mapStore never declared clusteringMode, so the picker wrote to a property nothing watched, AND the Clusters checkbox force-set 'mst' regardless of the store, so even a fixed picker would have snapped back on the next tick. Asserts on what the GL style RENDERS (circles, count labels, lines), not on state - 'clusteringMode === mapbox' would pass on a build still drawing MST. Also asserts no orphan layers between modes, and that MST and Network differ, since two labels for one behaviour is the same defect as a dead option.",
|
||
"check:composite-interop": "every link between a capability-4 composite's children exists on BOTH ends, headless: the event, attribute or config-key name is sent in the sender file and listened for in the receiver (a rename on one side fails, naming both files), the same literal is in every built bundle that ships an end, every composite is mapped (or says why it has no links), and no host lookup calls getCurrentInstance() inside a plain function, where Vue returns null when it runs from a watcher. Exists because Simple Research map -> grid died that way while a synthetic-event proof passed. Both ends existing is necessary, not sufficient: the runtime half is check:gallery-embed's linked phase.",
|
||
"check:composite-locale-leaks": "a composite rendered in a non-English locale must not show a string the catalogue already translates. The criterion is a LOOKUP, not a category: English catalogue holds the string as a value, this locale holds a different value for that key, the page still renders English. The previous sweep split strings into chrome/data/third-party and scored single words like 'Region' and 'Prayer' as data, so 28 leaks on Big Picture and 41 on Simple Research counted as zero. Surfaces that a lookup cannot judge - vendor map chrome, table DATA cells, facet-rail values - are suppressed by NAME with a stated reason, never by word.",
|
||
"check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.",
|
||
"check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.",
|
||
"check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.",
|
||
"check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; country-posters/src/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.",
|
||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser.",
|
||
"check:dashboard-geocoder-provider": "the research dashboard leaf at ?provider=maplibre makes ZERO requests to api.mapbox.com, and its search bars still suggest (built tree; browser gate). It was kept out of gates:post while the generated leaf hard-linked the Mapbox engine; the leaf now loads only the engine it is asked for.",
|
||
"check:dep-names": "every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
|
||
"check:poster-attribution": "the poster footer is a LICENCE statement, so it must credit whoever actually served the tiles. posterDefaults ships '© Mapbox © OpenStreetMap', which under the fleet's MapLibre default credits a company that served nothing. Checked live 2026-09-28: the OpenFreeMap STYLE declares no attribution on either source — the credit is in the TileJSON, merged into the SOURCE object after load — so it is read from map.getSource(id), flattened from HTML, deduped, and an unreadable map falls back rather than printing a blank line. Headless; in gates:pre.",
|
||
"check:script-registry": "one bundle script executes ONCE PER PAGE. Every plugin is a self-contained IIFE, so library/utils/embedScript.js is compiled into each one and a module-scoped cache was one cache per BUNDLE: two shells on a page each loaded the same child, which re-executed a whole bundle (a second Vue, i18n and pinia) before its guarded customElements.define silently declined. The check imports the module twice under different URLs — two module instances, one globalThis, the same shape as two IIFEs — and asserts the second caller receives the FIRST ONE'S PROMISE, because counting script tags alone is satisfied by the DOM-adoption path and would not catch a regression. Headless; in gates:pre.",
|
||
"check:map-control-overlap": "two controls pinned to the same corner of the research-dashboard map must not land on each other. The zoom toolbar was pinned at a literal top:34px, tuned when the projection toggle measured 23px tall; a later WCAG 2.5.8 fix gave the toggle min-height:24px, which in a shadow root with no global box-sizing rendered 34px and ran 8px into a toolbar drawn over it at z-index 10. The check COMPUTES the toggle's rendered height from its own declarations, honouring box-sizing, and requires the toolbar to clear it and to DERIVE its offset rather than restate a number. Headless; in gates:pre.",
|
||
"check:legend-data-contract": "nobody may read a key off useLegendData that it does not return. It returns items, never rows; two plugins read legend.rows anyway and JavaScript answered undefined, so the same mistake surfaced as a THROWN Export poster in countries-map (exp NE-17) and as a silently BLANK poster legend in research-map. The returned key set is read from the composable rather than restated. NOT in gates:pre yet: it still reports countries-map, which is another owner's file (exp's d6916e05e covers .rows but not the dead .build/.refresh calls). Run it directly.",
|
||
"check:window-globals-released": "a component that puts something on window must take it back. Heap snapshots (leak-722ac7789) measured SR+APG mounted twice at 54.2 -> 65.1 MB with live Vue apps 6 -> 8: every unmounted app stayed in memory, each pinned by one window key holding a closure, which keeps the component, its shadow root and the whole app. Flags only values that RETAIN (closures and object references), not warn-once latches or page-wide caches keyed off themselves. NOT in gates:pre: 19 sites remain across other owners' plugins. Run it directly.",
|
||
"check:describe-group": "the prayer packet's generated profile sentence (library/packets/describeGroup.js): each GSEC level states its own fact, and an index-ordered language name (\"Arabic, Moroccan\") is un-inverted by library/utils/languageLabel.js without shredding a comma inside parentheses, the 13 names the old split printed reversed. A catalogue arm renders every GSEC level from the REAL packet-strings.json in en and ar and fails on a blank sentence. Headless; in gates:pre.",
|
||
"check:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
|
||
"check:element-census-vs-live": "what is on ONE side and not the other, ours vs the live doxa.life page we clone: harvests every visible TEXT NODE through every shadow root on both sides and diffs the SETS. OURS-ONLY unaccounted = a scope violation (FAIL), THEIRS-ONLY = a clone omission (FAIL), SANCTIONED = an allowlist entry carrying the ruling + date + card (refused without them), DATA DRIFT = same label, different digits (reported). The negative control is MANDATORY: a known ours-only string must land in the ours-only set or it prints CENSUS IS BLIND and exits 75; an unreachable live page is also 75. Presets: internal/element-census/<name>.json (--preset <name>, default all); ad hoc: --leaf <bundle/profile/index.html[#hash]> --live <url> --neg <text> [--sanctioned <file>]. Promoted from regions' probe; regions passes (list 229 vs 228, the only difference its sanctioned poster toggle). Through browser-slot.sh.",
|
||
"check:embed-base": "the shared URL resolver, pinned by ARITHMETIC — no browser, no network, no served tree, so it cannot SKIP itself into irrelevance. It computes what a leaf's own \"../../assets\" means and asserts the resolver agrees. Caught nothing for fifteen ticks because it did not exist: resolveAssetsBase computed one segment too high, every local gate passed (the dev web-root has assets BOTH inside and beside the tree), and the byte-level publish gates went green on the broken publish.",
|
||
"check:embed-basemap": "the gospel map's BASEMAP must paint inside a genuinely cross-origin iframe on the token the page BAKES, not one a probe forces on it. Every other browser gate here forces a restricted token (u=doxalife) that 403s on the basemap, then counts PINS — which come from our own GeoJSON and paint with no token at all — so 'the map renders' had never been measured. Runs a negative control every time by rewriting the token on the wire; if the control passes it reports PROVED NOTHING rather than a pass. ~60s, needs a browser.",
|
||
"check:embed-targets": "every embed.iframe src in manifest.json must resolve to a file that EXISTS. Those snippets ARE deployment-method 1 (iframe) — a partner pastes them verbatim — and NOTHING else asserts their targets are still emitted, because every other gate walks only files that exist and is blind to ABSENCE. `the `check:embed-targets:self-test` script` poisons a copy of the manifest and asserts the gate NOTICES: a gate that cannot fail is worse than no gate.",
|
||
"check:embedder-keys": "regions-mfe forwards page-scoped config to its nested panes by an explicit key list (INHERITED_PAGE_KEYS). Asserts the documented embedder keys travel (dataSource, tokenUrl among them) AND that profile and instanceId do NOT: the host's would overwrite each pane's own and collapse both tabs into one view.",
|
||
"check:english-leaks": "A translated key must not render in English. The method: a string is a LEAK when the catalogue has a DIFFERENT value for the locale and the page still shows the English one - a render/wiring fault, not a missing translation. Third-party attributions, data names, placeholder-only strings and the prose this bundle deliberately keeps in English are all excluded, the last by simply not being in the catalogue. Scoped to the LEAF on purpose: six keys were reported leaking in the gallery and the leaf is clean in ar/zh/ru/hi - the gallery builds its iframe src with no lang parameter, so the embed never learns the locale. Embedder defect, not a bundle one.",
|
||
"check:experimental-shelf": "Every Experimental-folder bundle, leaf + Gallery tab, mapbox + maplibre: mounts with content, 0 errors / failed requests, maps load and DRAW their own data. Self-tests on a deliberately broken bundle first. Run through internal/browser-slot.sh.",
|
||
"check:export-poster-prints": "the Simple Research 'Export poster' button opens the PRINT DIALOG on the poster in ONE click and downloads NOTHING (the rule since 2026-09-28): leaf AND Research Gallery, en AND ar; the printing document is the 1700x2200 poster on an 8.5in x 11in sheet; no download arrives; 0 page errors. window.print is stubbed, so this measures everything up to the dialog. Replaces check:export-one-click, which asserted the download this rule forbids. Run inside the shared browser slot. Exit 2 = SKIP, 75 = NOT MEASURED.",
|
||
"check:export-poster-prints:maplibre": "the same four one-click prints on the keyless MapLibre engine (OpenFreeMap tiles): the hidden print map must be built with the engine that drew the screen, and the footer must credit the tiles actually printed.",
|
||
"check:facet-cascade": "a child facet selection must not outlive its parent: clearing a parent facet must cascade-clear its descendants on EVERY clear path. Asserts on RESULT COUNTS, not DOM — an orphaned child silently ANDs the map to empty. run check:facet-cascade:self-test for the NEGATIVE test (reverts the cascade and requires this gate to FAIL).",
|
||
"check:facet-changes-map": "a facet selection must CHANGE THE DOTS — and the dots must SAY what they mean. Asserts (1) the rd-bubbles feature set changes by count AND identity, (2) every surviving dot satisfies the selected level, (3) while a prayer filter is active circle-color is a DATA-DRIVEN EXPRESSION reading peoplePraying, not a flat literal. Assertion 3 is the one that matters: the prayer filter ALREADY narrowed 2107->689 correctly while it was reported broken, because every dot painted the same orange. Browser gate (needs `bun run dev` on 5173, or BASE_URL/PROBE_PATH at a served tree), so NOT in build:unlocked. run the `check:facet-changes-map:self-test` script for the NEGATIVE test.",
|
||
"check:facet-changes-map:built": "the SAME gate against the BUILT tree instead of the dev server. Measured 2026-09-24: the dev default is 1 PASS / 3 FAIL in four runs on a busy box (88 unbundled module requests vs the built leaf's 3), while the built tree passes deterministically. Use this when you need a trustworthy answer; use the dev default when checking uncommitted source.",
|
||
"check:facet-changes-map:self-test": "reverts the fix IN THE LIVE PAGE and requires the gate to FAIL. SELF_TEST_MODE=colour (default, isolates the colour assertion — the reported bug) | props | both | frozen (blocks the source update, isolating the set assertion). All four modes verified caught.",
|
||
"check:facet-one-field": "ONE AXIS = ONE CONTROL-REACHABLE FACET. Asserts on the PARTITION each facet induces over the rows, NOT on which r.<field> it reads: three fields (religion, primary_language, rop1) are legitimately read by two facets each as tier-1/tier-2 pairs, so a gate on duplicate field READS would fail correct code. Scoped to facets a user can actually select (rail + dropdowns + charts), because a search-only projection sharing a partition is fine until it becomes a control. run check:facet-one-field:self-test for the NEGATIVE test.",
|
||
"check:filter-multivalue": "SPEC-DASHBOARD-KIT D1: filterSet[key] may hold an array meaning OR-within-key, keys still AND together — and exactly ONE function answers 'does this row match'. The rule `a(r) === filterSet[k]` was written out SEVEN times (shell, facet rail x2, map panel, table, charts rail, CSV exporter); the moment it grew an array case, six copies would have kept comparing an array with === , matched nothing, and left the panels disagreeing about what is on screen — the facet rail printing counts for a scope the map and table no longer render. Runs the real bus (OR within, AND across, one-element array collapses to a scalar, EMPTY array clears rather than matching nothing, toggleFilterValue adds/removes/clears, cascade intact), greps the panels for surviving copies, round-trips the URL hash including a value containing a comma, and lifts the grid's valueMatches to prove both ends OR the same way. The list separator is `|` and not `,` because 120 of the 864 live language labels contain a comma. No browser.",
|
||
"check:fits-screen": "THE FITNESS-FOR-USE GATE (LL-the-user-found-three-bugs-my-gates-could-not-see): a green STATE suite (mounts/fetches/counts/paints) said nothing about whether the product is USABLE, and one sitting with the product found five real defects that no gate saw. Asserts 5 clauses on the built dashboard-plus-grid composite + research-dashboard leaf, real browser, live data: (A) the dashboard band fits ONE 1440x900 viewport and is shorter than its own tree-legend's scrollHeight; (B) the people-groups grid is still reachable by scrolling underneath it (a fix for A must not re-break this — AP-fixing-reachability-by-making-the-thing-taller); (C) a wheel over the map changes ZOOM and does not scroll the pane, a wheel below it scrolls and does not zoom — instrumented on the DISPATCHED EVENT (composedPath), not geometry, per LL-four-wrong-diagnoses-instrument-the-event-not-the-geometry; (D) the grid's live count ([data-table-count], the composite's documented single source of truth — show-total=false on the grid child is deliberate) is visible and CHANGES when a facet narrows it; (E) a 1440/1280/768/390 responsive sweep has no horizontal document overflow and no element clipped to zero height (.rd-td/.rd-th ellipsis excluded — expected). Spawns its own serve-strict on PORT (default 4391); SKIPs (exit 0) if the tree is not settled at START OR re-settled at END, or pray.doxa.life is unreachable — never reports a defect from a torn tree. Also best-effort probes any cap-5 shell the manifest declares (type nested-mfe-spa) by CONTENT-MATCHING a tab, never by hardcoded structure, so it survives the shell's own in-flight restructure without depending on it. Needs a built tree (`bun run build`). run `check:fits-screen:self-test` for the NEGATIVE test.",
|
||
"check:fits-screen:self-test": "FIVE regression modes, one per clause, each required to fire ITS OWN [A]/[B]/[C]/[D]/[E]-named failure: run clause a via THIS entry and b-e via `check:fits-screen:self-test:<b|c|d|e>`. a (default — forces the dashboard section past the viewport, the AP-fixing-reachability shape), b (n/a here — B is proven by A's own poison already trapping the grid below the fold; kept as a mode for a future independent poison), c (freezes map.scrollZoom enabled, the BUG-MAP-EATS-THE-WHEEL regression), d (blanks the live count element), e (forces the shell wider than the viewport at 1440). A gate that has only ever printed PASS is unproven.",
|
||
"check:fits-screen:self-test:b": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
|
||
"check:fits-screen:self-test:c": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
|
||
"check:fits-screen:self-test:d": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
|
||
"check:fits-screen:self-test:e": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
|
||
"check:frozen-labels": "a label resolved ONCE that never re-resolves on a mid-session locale switch: t() or tStatic() stored at module/setup scope, or tStatic() inside a computed (it reads a plain global the computed cannot track). Parsed, not regexed. Scoped by PROFILE stage: a production profile or library/ FAILS, an experimental profile is REPORTED only. In gates:post. The :self-test plants 4 bad shapes and 6 good ones (14 assertions).",
|
||
"check:gallery-embed": "the cap-5 Research Gallery must embed on a FOREIGN ORIGIN (a stand-in host page on localhost, the built tree on 127.0.0.1) in BOTH modes, bare <script>+<doxa-maps-spa> and <iframe>, with the layout inside the host box and every curated tab painting a map. The summary line prints the tab count PER MODE, and a mode that checked zero tabs is a FAIL. One mode only: check:gallery-embed:js or check:gallery-embed:iframe. Starts its own servers on OS-assigned ports; needs only a built tree. Exit 2 = SKIP (no built tree / no playwright).",
|
||
"check:gallery-embed:live-cors": "the Research Gallery on a foreign page under the LIVE static layer's CORS rule (Access-Control-Allow-Origin on files inside the tree's folders, NOT on the root manifest.json, measured on the live test host): it must still boot every tab, by reading the CORS-readable manifest copy generate-tree writes beside its bundle.",
|
||
"check:gated-not-in-tree": "a GATED profile (the build config's gatedProfiles, today key-cities) never reaches the public tree: no public/<tree>/<name>/ folder, no manifest.json entry, no page referencing <name>/<name>.js. vite.config.js builds a gated bundle into .gated/<tree>/ (ignored by git). Data tables are out of scope on purpose: key-cities.json also feeds the production Cities with Internet map. Two-sided: three planted leaks in a temporary tree, each caught. Node only.",
|
||
"check:gospel-all": "EVERYTHING gospel-resources owns, in one command. Its ten gates were all hand-run, which is how a dead bundle shipped: a duplicate module killed all five leaves and the one gate run that day was pointed at the map. check:no-orphan-gates was right that they were reachable — reachable is not run. Deliberately NOT in gates:pre/post: three need a browser, the shared wave holds browser gates under load, and a bundle owner should not be able to slow everyone else's build by adding to a list they do not own. Static gates run first and stop the run on failure, so a scarce browser slot is never spent confirming a table is inconsistent. Exit 75 = NOT MEASURED, never folded into a pass.",
|
||
"check:gospel-internet": "the Gospel x Internet board, headless, against the REAL table and the REAL kit bus: the join table is what its builder makes from main (2,107 / 2,107, province placement agreeing with gospel-by-province), every facet count equals the rows the bus then keeps (the five-numbers-agree invariant), D1 OR-within-key, the country->province cascade, the 369 zero-access headline by band 220/45/45/59, URL state round-trip, CSV header-first/provenance-last with injection neutralised, no engagement column; then every catalogue key the bundle names exists in all 11 locales and is translated. Each half has a --poison arm that must fail. Plugin-local because check:i18n-keys-exist cannot see a useOptionalT-based alias.",
|
||
"check:gospel-whiteboard": "the gospel set charts' geometry once drawn on a map: the bounded set's gap survives projection, the drawing stays inside the Mercator clamp, and three more properties, each with the arm that would catch its own defect. By the gospel owner. Headless.",
|
||
"check:hardcoded-english": "English a reader sees that no locale can reach: template text, static title=, script-block sentences, template literals, and quoted literals inside BOUND attributes, across the production plugins. Quiet on translator calls (including discovered aliases and proxy-t misses), te()-guarded fallbacks, css, urls and developer error text. Deliberately NOT in gates:post yet: it reports 179 today (gospel-resources 148, my-upg-100-list 25, cities-internet 6 incl. one cross-area key), with English inside dir=ltr islands listed as deliberate and not counted; it joins when that reaches 0 or every hit is carded. The :self-test (28 assertions, fixtures validated first so a malformed sample cannot pass as silence) runs in gates:pre.",
|
||
"check:heartbeat": "the HEARTBEAT ZONE threshold (the choropleth's top bin) must come from config AND the shipped config must still render today's ramp. Two sides: (B) a configured threshold must MOVE the edge — catches re-hard-coding; (A2) dashboard.config.js's own map.* must still resolve to 5/20/60/150/Infinity — catches the shipped default silently changing. A2 was MISSING at first and heartbeatThreshold:999 shipped past this gate printing 'default ramp unchanged'. A grep for '150' cannot do either job: it false-positives on the legitimate default and false-negatives on a re-hard-coding at 200. run check:heartbeat:self-test for the NEGATIVE tests (3 cases).",
|
||
"check:heartbeat:self-test": "THREE regression cases, each required to fire ITS OWN assertion: [literal] resolver replaced by the old hard-coded array -> B; [shipped-default] dashboard.config.js heartbeatThreshold changed -> A2; [shipped-mode] heartbeatMode flipped to quantile -> A2. Poisons IN MEMORY, never the file. Testing that the FEATURE works is not testing that the REGRESSION is caught — this gate shipped with only the first and missed the second.",
|
||
"check:help-reachable": "every Experimental map with explainer text in doxa-maps-spa/src/experimentalHelp.js reaches it from its OWN \"?\" (a hosted HelpButton or useHostedHelp in its bar), or through a named control of its own (upg-neighbor's About); the app listens for doxa:help, marks its panes with data-doxa-help and has no top-centre button. Executes the composable on a fake DOM: doxa:help must bubble, be composed, and find the host across a shadow root. Six planted defects each caught. Node only, no browser.",
|
||
"check:hover-channel": "SPEC-DASHBOARD-KIT D3: the bus's third coordinated channel points at a record without committing to one. Hover looks like a weaker selection, and the cheap implementation is to put the key in selectionSet 'just while hovering' - every consequence of which is bad and none obvious at the call site: useSelectionCamera watches selectionVersion, so a hover would fly the camera at every mousemove (the resize-strobe class, and a reader may be photosensitive); the CSV exporter reads selection > filter > all, so a hover would change what an export contains; the table's selection pill and the shell's live region would both narrate the pointer. Runs the real bus: hover holds its own ref, never enters the selection, never bumps the version, never touches the filters, a repeat emits once (hover fires on every mousemove across 2,107 rows), and the three channels clear independently. Does NOT check which panels light up - D3 says panels opt in, and the first opt-in belongs with a consumer that has a reason. No browser.",
|
||
"check:host-locale-contract": "a HOST's profile-config.locale reaches every leaf: each leaf page is loaded with ONLY \"locale\":\"ar\" added to its profile-config (no ?lang=, the page keeps lang=\"en\") and must draw at least half the Arabic text the same page draws at ?lang=ar. regions-dashboard/regions-list is the positive control (NOT MEASURED if it does not enter Arabic). Guards ProfileLoader's inject('appI18n'): a bundle that never provides appI18n silently ignores the host. --dir <tree> for a before/after, --only a/b,c/d to narrow. Through browser-slot.sh.",
|
||
"check:i18n-keys-exist": "every literal catalogue key named in code exists in library/i18n/locales/en/. A localized CALL SITE is not a localized STRING: the dashboard's five resize handles all read T('aria.resizeFiltersRail', ...) and look correct in review, while aria.json held eleven keys and not one was a resize key - so every handle announced itself in English in all eleven locales, and the fallback that makes a missing key degrade to readable English is exactly what kept it silent. No other gate asks this: i18n-parity is satisfied because the key is missing from ALL locales equally, and english-leaks and composite-locale-leaks both need the catalogue to HOLD the string before an appearance counts as a leak. Matches tStatic/$t/t and the house-pattern T( alias, all of which take a full dotted key; prefix helpers like L( and any concatenated or template-literal key are COUNTED as unresolved and never guessed at. Found 22 missing keys on its first run. ROUND 2, after a test planted T('poison_key') behind `const T = useGiT()` and this gate reported the tree CLEAN -- the one failure a check is not forgiven, because a red gate gets fixed and a green one that should be red gets believed. THREE independent blindnesses stacked, and any one alone hid the key: the file never writes the word tStatic so the precondition skipped it before anything ran; useGiT is not tStatic so no alias was found; and the alias is PREFIXED (useOptionalT carries a namespace) while its call sites pass BARE keys, which the full-key pattern cannot match because it requires a dot. Now a translator that arrives from a COMPOSABLE is resolved like any other, with the namespace READ from the factory's one-line body rather than listed here -- useOptionalT/useOptionalI18n directly, plus any `export function useXxxT() { return useOptionalT('ns') }`, which is how useNeighborT and usePplrT are written. Because qualify() prefixes a bare key and leaves an already-qualified one alone, one binding registers as BOTH shapes. MEASURED before and after with the same poison on disk: the old version PASSES it, this one fails it as 'neighbor.zzz_not_a_real_key (via nt())'; coverage went 683 -> 743 full-key and 49 -> 56 prefixed call sites, so 67 sites were being scored clean without being read. A self-test now asserts all three fixes separately and exits 3 if any goes blind, because this gate guards goal (d) and had already been trusted once while lying. Empty-namespace bare keys stay outside the model and are COUNTED rather than assumed away; measured at zero today, since all five such sites pass dotted keys.",
|
||
"check:i18n-never-translated": "a key PRESENT in all 11 catalogs but IDENTICAL in all of them is copied, not translated. check:i18n-parity asserts presence; presence and translatedness are different properties. Carries an allowlist for keys correctly identical everywhere (pure format strings, brand names, ISO designations) — each with a reason, so the list records decisions rather than silencing findings.",
|
||
"check:i18n-stopped-scope": "createAppI18n() must succeed while a STOPPED effect scope is current: a P0 seen live (Plugin Studio gallery, every tab after the first threw 'SyntaxError: Unexpected error' from createI18n, because Vue 3.5 makes a new scope inactive under a stopped one). A bare createI18n() in the same scope must throw first, or the harness proves nothing. Also asserts a bundle's second provide('appI18n') raises no warning.",
|
||
"check:i18n-treeshake": "each built bundle carries every translation area its code asks for: every area-prefixed key literal in <bundle>.js belongs to an area the build included (the build narrows library/i18n's catalogue glob per bundle, internal/i18n-areas.mjs). Headless.",
|
||
"check:instance-config-keys": "a plugin reads its instance config keys (show-search, show-filters) from profile-config in both spellings, with the prop winning. By the regions owner; mutation-tested. Headless; in gates:post.",
|
||
"check:kit-options-reachable": "every option a shared kit composable ACCEPTS must be honoured somewhere in its body. Written because I nearly shipped one: kit landing 3 gave useSelectionCamera a `coords` option so a dashboard whose rows spell their coordinate differently could say so, and it was accepted in the signature and never passed to selectionBounds - a caller could set it, type-check clean, build clean and watch it do nothing. That is the worst shape a missing feature takes, because it READS as supported and nobody files a bug against a knob they assume they misused; this tree already paid for it once with a defineProps on a component the custom element never binds. It matters more for the kit than for one dashboard: the whole promise of SPEC-DASHBOARD-KIT is that a config produces a working dashboard, and a parameter that is merely present turns that promise into a guess. Strips comments from BOTH the signature and the body before searching - the first version was satisfied by the comment beside the fixed call site, and the second read comment prose as option names. Not a type checker: it catches a completely unused option, which is the failure invisible from outside.",
|
||
"check:language-code-link": "the map's language selection reaches the grid as a locale-stable CODE, or narrows nothing at all. Measured: Languages > Language > \"Arabic\" taking the grid 2,107 -> 0 in BOTH locales: the emitter looked for a row whose language LABEL equalled the selected name, but a language tier's label is derived by splitting the API label at the comma, and no row reads \"Arabic\" - they read \"Arabic, Sudanese\", \"Arabic, Levantine\", 22 codes in all. Lifts the emitter's match rule and the composite's gridLanguage() from the shipping source (never re-typed) and runs them against a committed fixture of the 864 distinct language label/value pairs: the emitter's rule must agree with the legend's own three-arm filter on every (label, tier) pair, every tier must select at least one row, a tier covering one code must emit it, a tier covering several must emit NONE and leave the axis unfiltered. No network, no browser; --refresh-fixture re-takes the fixture.",
|
||
"check:language-tree-layout": "the library language-tree layout builder (library/scripts): families packed without overlap, every roster language reachable. Headless; in gates:pre.",
|
||
"check:layout-key-namespaced": "two dashboards on one page must not share a saved layout. useDashboardLayout persists the four divider positions to localStorage, and until kit landing 2 the key was a single constant — invisible while there was exactly one dashboard, and SHARING THE ENGINE IS WHAT MAKES A SECOND ONE POSSIBLE, so SPEC-DASHBOARD-KIT section 8 named it as that landing's risk. The failure is quiet rather than loud: drag a divider in one dashboard, reload, and the other has moved. Asserts the shell passes no bare literal, EVALUATES the real expression with two different dashboard ids to prove the key varies (never a retyped copy of the rule), and that the pre-namespacing key is READ and never written — namespacing must not silently discard a reader's layout, and a migration that also deletes cannot be rolled back. Static; no DOM, no browser.",
|
||
"check:leaf-engine-per-provider": "every leaf in the manifest at ?provider=maplibre makes ZERO requests to any *.mapbox.com host and a map leaf still draws; at its default every map leaf draws with the engine it drew with before (read from the canvas that drew). --baseline <tree> compares against another tree (e.g. main). Browser gate, @browser-slot-max-hold 2700.",
|
||
"check:leaf-hints": "a map leaf gives the browser its origins early; a non-map leaf carries no map hint. generate-tree emits, per map leaf, a preconnect for the engine + style + data origins and (on the auto leaf) an engine preload from library/constants/cdn.js, so DNS/TLS and the engine start with the bundle instead of after it (A9). Two-sided: a map leaf without hints fails, a non-map leaf with an engine hint fails; a hint origin must be one the code uses. Reads the built tree; no server.",
|
||
"check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test.",
|
||
"check:legend-export-reachable": "the Export affordance must be reachable at BOTH widths. Selects a leaf row first (the control is gated on selectedNode) and opens the mobile sheet first (it starts closed). Asserts a REAL BOX, not CSS visibility — on mobile the elements report visibility:visible at 0x0. PASSES at both widths since the mobile sheet was given :export-enabled and :tabs. SKIPs when nothing is served.",
|
||
"check:legend-logical-insets": "the legend components position themselves with LOGICAL insets (inset-inline-start/end, padding-/margin-inline-*). A physical left/right does not mirror under dir=\"rtl\" while the map controls (inset-inline-end) do, so in Arabic the controls overlapped a legend that stayed left, on Simple Research, Prayer Progress and UPG-100 at once. Symmetric rules (left:50%, left:0 with right:0), auto resets and a short ALLOWED list with reasons pass.",
|
||
"check:legend-logical-insets:self-test": "poisons .stl-panel back to left:8px and asserts check:legend-logical-insets notices.",
|
||
"check:legend-mirrors": "the research-dashboard legends must MIRROR under RTL, and their labels must never become their values. An Arabic survey found them on the wrong edge with 70 of 85 letters Latin. Two faults that look like one and fail in opposite directions if fixed carelessly. (1) A physical `right:8px` pins a box to the physical edge in every locale; the logical property plus the auto on the other side is the fix, the pair being what cancels a physical inset inherited from a control container. Asserted on the map legend and on research-map's geocoder column, which exists only to share an edge with the legend beside it. (2) The English was NOT chrome: the religion donut's legend renders a family name DERIVED in English by library/colors/religion.js, the same table people-groups-grid re-derives per row so both ends byte-match. Translating those VALUES would work in English and match nothing in Arabic, silently, because an empty intersection reads as 'no results'. So it asserts the visible text is projected AND that no projection reached a wire: every :data-key/:data-facet binding in the file, not just the legend's, after a mutation test aimed at one silently hit another. No browser.",
|
||
"check:legend-source": "the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.",
|
||
"check:library-index": "the DERIVED library menu must not DRIFT from library/. REGENERATES the index into a throwaway tree (the generator, unmodified, over a symlinked library/) and diffs the BYTES — it used to compare mtimes, so `touch` or a rebase turned it red on a byte-identical doc. Fix: bun run library-index",
|
||
"check:link-rot": "re-tests a stratified sample of links we already VERIFIED, because nothing else ever re-tests one: a publisher can retire a page and the map keeps offering it, still labelled 'link checked'. THREE BANDS, each measured rather than assumed, each with its controls in band every run: plain STATUS where a status is evidence (jesusfilm.org among them — it does 404 on the /watch/jesus/<slug> shape the map ships, and the old exclusion was inherited from the retired ?languageId= URL); API-MAPPED for live.bible.is, whose page answers 200 for any fileset while /api/bibles/<id> answers 404; and REDIRECT-LANDING for fivefish.org, where a published code lands on its numbered language page and an unknown one is swept to /regions. If a control misbehaves it reports PROVED NOTHING rather than a pass. Only knowgod.com is left out — byte-identical page for any URL; the verifier proves those through Cru's API. ~3 minutes, network.",
|
||
"check:linked-vocabulary": "the two halves of research-map-plus-grid must mean the SAME THING by a religion family. The map filters by the legend's FIRST-LETTER key ('B'), the grid by a DERIVED family name ('Buddhism'), and both read ONE shared table (library/colors/religion.js) in opposite directions - so a wrong link stays invisible: both halves look filtered and show different peoples. Compares the total reached over the composite's wire against the total the grid gives for the same facet on its own, and runs the MUTANT first (the legend key instead of the name), failing as NO-TEETH if the wrong mapping scores the same as the right one.",
|
||
"check:list-selection": "what a vertical-legend row emits and the zones a region tree forgets, in Node with no mount: a leaf emits its zone id, a region emits its own, a cleared selection emits null, and every value is a serialisable string so it survives ?zone= and a deep link. Then the part that earns it: the four joinable UPG-100 zones missing from zone-regions.json (xinjiang among them, 100 groups) get an explicit selectable bucket, a NEGATIVE CONTROL proves the unaugmented tree really does drop them, ticking every region is shown to reach FEWER zones than exist (so a select-all must sweep zones, not regions), and a zone whose members do not resolve stays selectable and empty instead of vanishing.",
|
||
"check:list-prop-unit": "the people-group LIST prop, measured in Node with no browser: the ladder (profile-config list > ?list= > window.MAP_LIST > doxa) and aliases, the default list untouched and unfetched, a list keeping exactly its membership, an unloadable list failing closed (no rows, never DOXA's whole list), and every imb list having a membership file that matches its registry filter.",
|
||
"check:manifest-covers": "every DISCOVERED (non-gated) bundle must be in the built manifest",
|
||
"check:map-attribution": "the map credit line names the basemap of the engine that drew the map: under MapLibre OpenFreeMap + OpenMapTiles + OpenStreetMap contributors (never Mapbox), under Mapbox Mapbox + OpenStreetMap; geoBoundaries on both. Headless.",
|
||
"check:map-data-painted": "Every DECOUPLED map leaf must have OUR DATA on it, not just a basemap. verify-map-paint asserts a live GL context, attribution and zero console errors — all TRUE of a flawless basemap carrying none of our zones, polygons or pins, so every data-layer regression lands green. Measured 2026-09-26 by starve-probe (abort /assets/{boundaries,tables,people-groups}/ and the people-groups API, leave the basemap alone): 9 of the 13 leaves still report canvas, GL, attribution and 0 errors, so the hole is the MAJORITY of the suite; the other 4 lose their canvas entirely and the existing gate already fails them, so they are deliberately NOT listed — a leaf list is a claim about where a failure would otherwise go unseen. Each leaf is compared AGAINST ITSELF (healthy vs starved) because healthy leaves span 459..2821 colours and any global floor is vacuous or flaky: check:map-paints' 40-colour floor passes a starved upg-100-list at 306. Two measures, pass on EITHER — palette ratio <=0.55 OR >=3% of pixels changed — because each alone false-reds a real leaf (dashboard-plus-grid's 686x340 pane repaints 26.4% of the canvas at palette ratio 0.788; demo-map transforms the palette at ratio 0.196 while repainting 5.0%). A data-free map moves NEITHER. Uses page.screenshot({clip}), not elementHandle.screenshot(), whose stability wait never settles on an animating dashboard canvas (that cost 3 leaves a 20s timeout). Refuses to judge (exit 75) if either run has no canvas, if the starve aborted 0 requests, if the images cannot be compared, or if the gate itself throws. Run check:map-data-painted:poison to prove it can still fail. Browser gate, @browser-slot-max-hold 2700.",
|
||
"check:map-data-painted:poison": "The mutation arm for check:map-data-painted, and MANDATORY because that gate's assertion is a DISJUNCTION (palette moved OR pixels moved) — a weakened assertion that still passes everything reads as coverage while proving nothing. Starves BOTH runs of our data, so neither measure can move, and requires every leaf to go RED. A green poison run means the bars have drifted into vacuity. Browser gate, @browser-slot-max-hold 2700.",
|
||
"check:map-embed-cells": "PRODUCTION-MATRIX cells B (iframe) and C (script embed) for the seven Research Gallery maps on a genuinely foreign origin (tree on 127.0.0.1, partner page on localhost). C passes the HOST a distinct token and fails if any Mapbox request uses another. --only bundle/profile,… --cell B|C.",
|
||
"check:map-fly-reduced-motion": "every camera move in useMapFly is an instant jump when the reader asks for reduced motion (prefers-reduced-motion), and flyToCoords honours caller options. Headless: the real composable against a recording map.",
|
||
"check:map-paints": "The map must put PIXELS on screen, not just features. Exists because injecting mapbox-gl.css collapsed this profile's canvas from 1440x900 to 1440x300 - a white rectangle - and EVERY assertion went green over it: verify-gospel-map counted 2,107 of 2,107 pins rendered, leaves-mount found a full shadow root, the i18n gates found every label translated and nothing clipped. queryRenderedFeatures does not care whether anything is visible. This screenshots the element and counts distinct colours (a basemap has hundreds, a blank rectangle one or two) and checks the canvas fills its container. --poison hides the canvas and requires a red verdict.",
|
||
"check:map-props-unit": "the engine and token props decided in the library, measured in Node with no browser: auto picks the engine from the token ladder, an explicit provider wins, Clear clears the page-wide token, and tokenUrl is fetched and honoured (26 cases, one per defect U1/U3/U7).",
|
||
"check:map-unmount-mid-init": "end-to-end, deterministic: holds mapbox-gl.js, opens a map plugin in the studio, switches away (destroying the pane) mid-initialise, then releases the script. Fails on any page error. REQUIRES --url. Exit 2 if the race was not staged.",
|
||
"check:map-unmount-race": "browser-free, sub-second: runs the shipped ensureEngineCSS with a null element (what an unmounted pane hands it) against a copy with its guard stripped; the guarded arm must resolve and the stripped arm must throw \"reading getRootNode\". By gospel-plugin, adopted by studio.",
|
||
"check:mapbox-drew": "the Mapbox engine actually DREW, on every curated production map tab. The hole it fills: check-leaf-engine-per-provider proves MapLibre purity, and check-provider-switch loads ?provider=mapbox with a FAKE token — so the engine LOADING was measured and a tile RENDERING never was. Reads the engine off the canvas that drew (.mapboxgl-canvas vs .maplibregl-canvas), because a silent fallback to MapLibre looks identical to success; asserts queryRenderedFeatures() > 0, because a canvas at the right size is not a map. THREE exit states: 0 drew, 1 a real failure, 75 NOT MEASURED when there is no real token or only the known sandbox entitlement 403s (/v4/ composite, country-boundaries) blocked it — counting those as failures would manufacture work, counting them as passes would be false. Carries a negative arm: ?provider=maplibre must draw with MapLibre, else the engine check cannot tell the engines apart and every pass is unfalsifiable. Browser seat.",
|
||
"check:mobile": "deploy-2 CONTRACT check: needs NO JDK/SDK/device. cap sync is a file copy + plugin wiring, so it runs anywhere; cap:verify then asserts assets/public/index.html exists. Guards the SOFT FAILURE capacitor.config.json warns about: a wrong webDir yields an EMPTY assets/public and a BLANK WHITE SCREEN at launch, not an error.",
|
||
"check:mst-clustering": "the MST clustering mode draws a true minimum spanning tree/forest (same edge count and length as a reference Kruskal) and 2,107 pins take under 3 s on the main thread. Headless.",
|
||
"check:nav-reachable": "a cap-5 shell must have ONE on-screen control that opens its tab rail at phone width (needs `bun run serve` on 4173; browser gate, not in build:unlocked)",
|
||
"check:grid-merge-defaults": "the people-group grid's merge props (filtersToggle, backdrop, backdropFloor) must DEFAULT to today's grid, read from the SFC's defineProps by parser. ON DEMAND: those props are not on development yet (the grid merge, b1e839a33, is not landed), so it would be red in any chain; it chains with that merge.",
|
||
"check:no-abs-paths": "no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)",
|
||
"check:no-codename": "the development-era INTERNAL CODE NAME may not ship in any TRACKED file. Same hand-off family as check:no-abs-paths: meaningless to the Doxa team who inherit this, and invisible to every other gate (none read for vocabulary). Deliberately does NOT fail on the same letters appearing as the unix USERNAME inside a /home/… path — that is a portability defect check:no-abs-paths already reports better, and two gates disagreeing about one string teaches everyone to ignore both.",
|
||
"check:no-conflict-markers": "Nothing tracked may carry an unresolved merge. Exists because a merge from this worktree committed a BUILT bundle with 21 conflict markers - both sides concatenated, 1.13MB - and every gospel leaf rendered dead while the SRI hash matched and every request returned 200. The cause was a landing script that resolved one expected file then ran \"git add -A public\", staging every other file including the ones still holding markers. For a built file: take either side, REBUILD, commit the rebuild, then run this before pushing. --poison for the negative test.",
|
||
"check:no-credentials-omit": "no data fetch in library/ or plugin-designs/ uses credentials:'omit': behind a login gate a same-origin request then drops the session and is refused (the Research Gallery's 'Could not load Big Picture'). 'same-origin' never sends credentials cross-origin. A deliberate omit says so on its line: // credentials-omit: <reason>.",
|
||
"check:no-credentials-omit:self-test": "a planted omit is caught; prose and an annotated site are not.",
|
||
"check:no-cross-plugin-imports": "no plugin imports from another plugin's folder (src/, utils, data) by a relative path; shared code goes through library/ (@map/…). A composite may mount another bundle's PUBLIC profile .vue. --self-test plants a crossing import.",
|
||
"check:no-dead-library": "a deleted library module must stay unreferenced, and the scanner must prove it still works. Scans for a filename as a STRING across internal/, library/ and plugin-designs/ — not just imports — because check-gospel-set-charts.mjs read two components by filename off disk with no import anywhere, so a census and a build would both have missed it. Distinguishes a READ (fatal) from a comment MENTION (reported, harmless). TWO-SIDED: also asserts a known dynamic reference IS found (LegendDesktop lazy-loads LegendMobile), so green means the scanner works AND found nothing, rather than merely found nothing.",
|
||
"check:no-eval": "no built bundle contains new Function or eval (a strict CSP without 'unsafe-eval' must work), and vue-i18n in JIT mode renders real catalogue messages in en and ar with eval blocked. Headless.",
|
||
"check:no-free-identifiers": "every name a .vue script uses is imported, declared, a parameter or a known global (scope-aware, across plugin-designs and library). A free identifier compiles and then throws at runtime in exactly the path nobody clicked.",
|
||
"check:no-silent-engine": "A profile must NEVER fall back to a different map engine than its provider asked for. `engineGlobal(provider) || window.mapboxgl` looks defensive and is not: engineGlobal already returns the right global for the requested provider (useMapInstance.js:115 falls back to mapbox's NAME only for an UNKNOWN provider, never a missing engine), so the || fires in exactly one case — the page asked for maplibre and maplibre failed to load — and hands back the TOKEN-GATED engine to a page that asked for the KEYLESS one. Worse, accessToken is gated on !isMaplibre, so the substitute arrives WITHOUT a token: 401s on every tile instead of a legible failure. Found by regions' C3 read 2026-09-26; 2 offenders remained (cities-internet, gospel-resources) after my-upg-100-list was fixed. NOT a duplicate of check:leaf-engine-per-provider / check:provider-switch / check:auto-engine — all three assert the HAPPY path (what loads) and this bug only appears when something does NOT load, so all three were green while it existed. A SOURCE check on purpose: reproducing it live means serving a leaf with MapLibre deliberately blocked, spending a browser slot on what a grep decides. Exits 75 when no file calls engineGlobal — 'found nothing' is not 'found nothing wrong'. Run :self-test to prove the pattern still matches.",
|
||
"check:optional-t-equivalence": "C4's shared guarded translator (library/composables/useOptionalT.js) must behave like the twenty hand-written try{useI18n()}catch blocks it replaces: all three host states, and the per-site register asserts each migrated file still passes ITS OWN fallback rather than inheriting the catalogue, because routing them through tStatic would change what seven bare-host bundles render. EVERY LATER C4 WAVE APPENDS A ROW. Extract-and-executes the module source, since useOptionalT imports vue-i18n and an i18n index that calls import.meta.glob.",
|
||
"check:package-json-unique-keys": "no object in package.json may carry the same key twice: JSON.parse keeps the last silently, so a duplicate kills the first entry while every gate that reads the PARSED object passes (a //scripts note sat twice after a merge). A small scanner tracks keys per object; its self-test runs every time.",
|
||
"check:no-repo-pb-hooks": "no PocketBase hooks (pb_hooks/, *.pb.js) in the repo: the cloud container pulls it live, so a pushed hook would be server-side JS from any writer; the one team sign-in hook is baked into the image",
|
||
"check:packet-browser": "the packet the READER gets — the legend's Export > Prayer packet path, proving the shared renderer runs in a BROWSER. Asserts the same 12 points / 12 wheel labels / page-1 geography / print layer as check:packet-render, which it did not until 2026-09-24. CONDITIONAL: SKIPs without a server over the repo root (python3 -m http.server 4898) and a reachable records API.",
|
||
"check:packet-fixture": "the committed fixture must still match clusters.json and must never carry image_url (CI would fetch 43 photos from someone else's CDN).",
|
||
"check:packet-map-missing-loud": "page 1 of a prayer packet SAYS when its zone map is missing, instead of printing a blank cover: two-sided (the notice appears when mapSvg is empty and is absent when the map is present, SVG intact), in en/es/ar and both variants, and a locale without the key prints the English sentence, never the raw key. Node only, no browser.",
|
||
"check:packet-render:expect-funnel": "check:packet-render with the page-2 FUNNEL declared REQUIRED. Exists because of a real near-miss: a merge resolved with `git checkout --ours` deleted the printPacket call site that supplies the funnel data (60 lines) and EVERY GATE STAYED GREEN — it had to, because the funnel page is OPTIONAL by design (buildPacket omits it when the arg is null), so a packet without page 2 is not malformed, just smaller. Always requiring it would be the wrong fix: the 11 pre-funnel laos packets and any host with no funnel data would go red, and a false red costs as much as a false green. So the CALLER, which knows whether this packet was built with funnel data, declares it — an absence is then either declared (no flag) or a failure (flag passed, page missing), never a silent nothing. Pass it for any packet from a profile that wires packetFunnel(). Browser gate.",
|
||
"check:packet-strings-parity": "the PACKET catalog (packet-strings.json) must be whole in every language: all string keys, all 12 prayer points with every field, all 12 wheel rhythms. check:i18n-parity covers library/i18n/locales only — six gates READ this file and none checked it. Asserts PRESENCE, not translatedness, with point4_passage declared-absent on record.",
|
||
"check:packet-variants": "the poster wall's Prayer List prints the PACKET (not the columnar list), and every variant it offers exists in packet-render's VARIANTS. A variant may drop fixed pages but never people: lightweight keeps every group. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
||
"check:india-states": "India's states-and-union-territories table (data/people-groups/india-states.json) stays true to the stamping it is derived from: all 36 ISO 3166-2:IN divisions (28 state + 8 union_territory, Nagaland + Mizoram listed even when empty), every India people group of admin-membership.json in exactly ONE division, every shapeName a feature of the served geometry, NO DRIFT (re-derived from today's inputs it is byte-identical and meta's input sha384s are the files on disk), and pin == table == served copy under a size ceiling. Mutation-tested. Headless; in gates:pre. By the UPG-100 owner.",
|
||
"check:batch-selection-export": "UPG-100's batch selection EXPORTS its selection (restored 2026-09-30): Copy's first line parses in data/apply-grouping.mjs (names, UUPG total, colour) with the people-group CSV after it; the CSV header, quoting and formula-neutralising hold under a real CSV parse; the HUD wires the colour swatches, hex input, Copy, Export CSV and the records prefetch; the 9 new strings are in all 11 locales, translated. Mutation-tested (CSV first, no formula guard, Copy unwired: each FAILS). Headless; in gates:pre. By the UPG-100 owner.",
|
||
"check:tree-relative": "the committed tree names no host of its own. With embedBaseUrl empty in the build config, the manifest must say embedBase \".\" and every scriptUrl, iframeUrl and embed snippet src must be relative. check:drop cannot see this: it looks for root-relative \"/...\" references and skips https:// ones, so a tree built with EMBED_BASE_URL (a deploy build) would pass it and pin every snippet to one host, the way the retired CDN route broke. Data addresses (runtime.apiBaseUrl) are not checked. SKIPs when the config sets embedBaseUrl. Static; takes an optional manifest path for poison tests.",
|
||
"check:universal-props": "every plugin reads the props the Plugin Studio header sets for all of them through the library: map engine, locale, Mapbox token and token URL, and the people-group list. It reads each bundle's source AND its shipped bundle and fails on a bypass (a mapbox:// style literal, a private token global, a list fetched around the library); a by-design exception is ALLOWed by name with its reason. --report also prints the matrix. Headless.",
|
||
"check:zone-title-tier-a": "generated zone titles ship for ONE-country zones only: the 3 such zones produce a localized, non-code title in en/es/ar that differs across languages, AND every other zone declines so its hand-written label survives unchanged (a multi-country zone has a name that is not a list of its members). Two-sided, and NOT MEASURED when the country-name supplier cannot resolve a known code.",
|
||
"check:evangelical-share": "the packet's % evangelical page states THIS list's share: summed over every DOXA group the per-group table reproduces christian-share.json exactly; different lists give different figures; a list with no published levels prints a dash, never 0% (Number(null) is 0); the coverage clause appears only when partial.",
|
||
"check:map-labels": "the packet's page-1 map names its countries in the packet's language, on a real tracked poster: English is byte-identical with and without the relabeller; in ar and zh no known English country name survives, halos match fills and the privacy line is localized; the width guard shrinks wider translations and grows nothing; Arabic drops letter tracking; an unknown label is left as drawn.",
|
||
"check:packet-offline": "the offline packet generator RUNS: it builds the committed fixture packets (no network) and fails naming the error if any build fails. node --check cannot see an evaluation-order fault - reading a const before its declaration stopped every offline run while the syntax check passed.",
|
||
"check:packets-all": "runs check:packet-render over EVERY generated packet, not one hand-named file. The single-file gate SKIPs with no argument, so it was in package.json, passing in all 11 locales, and in neither gates:pre nor gates:post — it only ran when someone typed a filename. SKIPs when no packets are generated.",
|
||
"check:pin-dim-slider": "the side menu's pin-dim slider and the legend's dimming must COMPOSE, not compete. Both want the pin layer's circle-opacity, so there is ONE writer and the value is the PRODUCT: slider x (1 matched, 0 dimmed). Asserts the LIVE paint property, not the store: flat 1 with clustering off, flat 0.5 with clustering on, ['case', expr, 0.5, 0] once something is dimmed, and ['case', expr, 1, 0] when the slider returns to 1. Dims by RELIGION deliberately - a region selection sets a layer FILTER rather than an opacity expression, so testing that path would report a failure about code that never writes opacity.",
|
||
"check:pinned-umd": "library/utils/loadPinnedUmd.js (d3 for PPLR and the Wall) loads a pinned UMD with SRI and never touches the host page's global of the same name: a host's own d3 gains no keys, a failed load leaves it in place. Headless.",
|
||
"check:popup-escaping": "the library's pin and region popups render hostile data (an <img onerror>, a <script>, a javascript: URL) as text, never markup: every field goes through escapeHtml/safeUrl (library/utils/html.js). Headless.",
|
||
"check:ported-globals-identical": "the DX Life base/_globals.scss rules that shadow-root components used to copy now come from one module (library/styles/portedGlobals.js), and the CSS each component actually INJECTS must be byte-identical to the pre-refactor baseline: it runs each component's setup with stubbed imports and hashes the real useShadowStyles argument, not the source text. Deduplication with a proof that no pixel can change. Node only, no browser.",
|
||
"check:pin-selection": "the shared source reader understands BOTH engines: mapbox-gl stores a GeoJSON source _data as the FeatureCollection, maplibre-gl wraps it as { geojson } (or { url } when remote). Reading only the Mapbox shape returned an EMPTY list on MapLibre — the keyless engine public builds use — which silently killed zoomToLegendRow camera fits on three maps and the research packet row selection, with no error because every caller no-ops on no features. Asserts both shapes, that a remote source returns [] AND warns, the serialize fallback, and that selectedPinIds agrees with the reader. Headless, no map. Exit 0 pass, 1 fail.",
|
||
"check:popup-href-escaped": "a URL interpolated into an HTML string is escaped before it lands in href= or src=. Written for RegionCountryMap's popup, which escaped every value except its two link URLs -- both built from joinBase(hostAttribute, apiSlug), so a quote in either escaped the attribute. Data URLs, blob URLs, encodeURIComponent and safeUrl() are recognised as safe; anything else needs an href-escaped-ok: reason. 6 self-tests. Source-only, headless.",
|
||
"check:poster-action-print": "a poster button opens the print dialog (the rule since 2026-09-28: never a direct download). Two arms: PosterWall's posterAction defaults to 'print' and any consumer opting into 'download' carries a posterAction-download: comment saying why; and no shipped file hand-rolls an <a download> for a poster IMAGE (svg/png/webp/pdf) without first attempting to print, which the prop arm cannot see. Data exports (csv/txt/json) are out of scope. 6 self-tests. Source-only, headless.",
|
||
"check:print-frame-reclaim": "a print frame is kept until it has printed: openPrintFrame reclaims it 60 s after its load (not after its creation), at afterprint, or at a 10-minute ceiling, and printImageBlob revokes its object URL on the same clock. Fake timers; two-sided (the pre-fix copy fails). The Wall poster loads 132 MB of photos, and the old 60 s-from-creation clock removed the frame before it could print.",
|
||
"check:poster-card-fits": "a poster card's print-options row must stay inside its own card. Reported by the repo agent off a 1440px screenshot: 27/27 cards overflowing by 110px, 36 neighbour intrusions. .pfs already had width:82%, so SOURCE READING SAID IT WAS FINE — flex items default to min-width:auto and refuse to shrink. Asserts BOTH no-overlap AND that the controls keep a usable box, because a row whose buttons collapsed to nothing would also \"not overflow\". Sweeps 390/768/1024/1440/1920px and asserts the row sits BELOW the preview too — measuring only left/right edges is how it passed a driver-visible regression. CARD_FIT_WIDTH overrides the list. SKIPs when nothing is served.",
|
||
"check:poster-degrades": "with NO stored poster, NO source and NO generator (forced in the browser: every posters/*.webp and *.html answers 404, lib/poster-render.js is aborted), nothing must print and the reader must be told why. Measures any served tree; NOT MEASURED without a wall url.",
|
||
"check:poster-design-catalogue": "Poster Design's configuration catalogue (plugin-designs/poster-design/src/templates.js) says only true things about the library: its sheet formats are exactly poster-render.js FORMATS at the same pixel sizes (the generator is a pinned asset, so the table cannot be imported); every configuration name is unique and well formed; the card entries are exactly card-render STOCK; every catalogue key it names exists in en/posterDesign.json; the sample country can be drawn. Six mutants (px drift, a missing format, a duplicate name, a bad key, an undrawable sample, a dropped stock) each FAIL.",
|
||
"check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",
|
||
"check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.",
|
||
"check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
|
||
"check:posters-exportable": "every poster the tree offers can be printed. With posterSources on, the tree once shipped only the 28 zone sources, so 350 of 378 posters said \"poster not found (404)\". Each id, from data/posters and the wall's previews, must ship a real WebP (RIFF..WEBP) or a real poster source (its class=\"stage\" element), so a placeholder or a host's fallback page cannot pass. SKIPs when posters are off by config, which check:poster-degrades covers. Reads the built tree; no server.",
|
||
"check:posterverse-shelf": "Posterverse (the capability-5 poster shell) shows exactly its allow-list and never a gated plugin. Asserts the rail rows per section (Posters: upg-100, country-posters, poster-design; Experimental: wall-of-people-groups, bible-posters), that no row names key-cities or a shell (a key-cities bundle is planted into the built manifest, because the real one withholds gated profiles), that every present row opens the profile it names (no silent fallback to a leaf), that posterverse.meta.json composes equals the rows whose bundle exists, and 11-locale parity of the shell strings. --self-test plants one defect per arm. Headless. Exit 0 pass, 1 fail, 75 no built manifest.",
|
||
"check:prayer-walk-planner": "the Prayer Walk Planner is a full-page tool that prints a real walking map: the map canvas fills the page below the toolbar in its own leaf AND inside the Research Gallery at 1280x800 and 390x844 (the gallery once collapsed it to 76px / 0px); a real zone (Balat, Istanbul) renders a >=2000px print map that is not blank, saved to OUT=<dir> so a human can read it; ?lang=es gives a Spanish toolbar; 0 page errors. Run inside the shared browser locks. Exit 2 = SKIP.",
|
||
"check:preset-config-merge": "a preset page (<bundle>/<profile>/<instance>/index.html) keeps every default in its profile's .meta.json config, with the instance's props merged over them (upg-neighbor's presets lost pinHint:false when props replaced the config). Reads the built tree; SKIP without one.",
|
||
"check:profile-table": "the prayer packet's profile TABLE (20 full-width rows: picture, generated description, three boxes; PER_PAGE stays 20): a row stays tall enough for a name plus a two-line description at the smallest fit size, and truncation happens only at a whole word. Character-count, not DOM, so Node and the browser agree. Cannot see the RENDERED row height: that is check:packet-render, a browser gate. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||
"check:profiles-mount": "check:composites-mount over EVERY profile of every bundle and every preset instance (with its props), in en and ar: any setup, watcher or mount error fails it. Headless, ~40 s. A source mount: a build-only error (a minified name hoisted out of order) needs the built bundle and is not covered.",
|
||
"check:provider-for": "providerFor(map) must answer the same as the raw `map instanceof window.maplibregl.Map` sniffs it replaced (C3) - on BOTH engines AND on the AMBIGUOUS input that is neither, where the originals fell through to mapbox/false. Carries an executable proof that the obvious isMapboxMap() rewrite INVERTS that fall-through while agreeing on both real engines, which is how it would have changed a poster attribution unnoticed. Headless, source-only. engineGlobal is extract-and-executed from source because useMapInstance.js cannot be imported in plain Node.",
|
||
"check:provider-switch": "the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
|
||
"check:published": "what readers are SERVED must be what we BUILT. Every other gate measures the local tree, so all of them stay green while a stale publish serves something else — and manufactures bug reports that cannot be reproduced. Compares the sha384 per bundle. SKIPs when the published host is unreachable (a network gate that goes red on flaky wifi gets ignored).",
|
||
"check:quoted-scripts": "documentation/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS — update the DOC.",
|
||
"check:readme-size": "the README's web-folder row (N files, M MiB) against the built public/<tree>: fails when either number is more than 10% off and prints the row to paste. The folder grows as maps are added, so a batch of posters or data trips it on purpose: the row had gone stale twice.",
|
||
"check:reduced-motion": "a library component that animates must carry its OWN @media (prefers-reduced-motion) rule. A page-level rule does not cross a shadow boundary, and no generated leaf ships one anyway (measured: 0 across the built tree), so there is nothing to inherit. Also asserts the rule lives where the styles do: a component injecting through useShadowStyles whose rule sits only in a <style> block has written a rule that never reaches the shadow root.",
|
||
"check:region-fill-repaint": "the region fill must be repainted after its data arrives, in EVERY arrival order. ★ WHAT THIS NOW GUARDS, narrowed by measurement after it landed: the ordering is not merely uncaused-by-the-grey-tab, it is UNCONSTRUCTIBLE today — region-dashboards.vue:1091 gates the whole map subtree behind v-if=\"loading\", and loadRegions assigns points inside the try while loading=false is in the finally, so the map can never mount with empty points (measured: 0 grey in 36 mounts, including the API held back 20s). So this is a guard against a FUTURE refactor that drops that v-if data gate, not a live path. The grey Regions tab stays OPEN and is not this. fill-color is SNAPSHOTTED into the layer when addLayers creates it, from a match-expression built out of props.points — with no points that expression is a flat no-data grey, and applySubject guards every write on getLayer() so a change arriving before the layer exists is dropped and never retried. Models the arrival orders (asserting at least one DOES grey without the repaint, else the model proves nothing) and asserts the source carries the repaint INSIDE the .then() block that received the boundaries, brace-matched rather than within a character distance — a 1600-char window passed on an unrelated applySubject 1200 chars downstream.",
|
||
"check:religion-label-not-value": "the library's two RELIGION_FAMILIES sites (useLegendData, useSearch) show religionFamilyLabel() to the reader, while the value a filter or search matches on stays the canonical English family name: a translated label must never become a wire value.",
|
||
"check:ring-bands": "library/components/charts/ringBands.js keeps the four promises the ring target rests on: every pin inside its own ring band, no two pins closer than the pin diameter, no pin inside a ring label's keep-out arc, and byte-identical output for the same input (input ORDER included, so a caller that re-sorts its rows does not move every dot). Each arm carries a negative control. One arm exists because this gate PASSED the bug it now catches: uncapped, a long label on ring 1 claimed 96% of that small circle, capacity went to zero and every group on the innermost ring was reported unplaced — all other arms stayed green, because 'reported rather than silently dropped' is not the same promise as 'the ring draws'. Labels now truncate, and say how many characters fit.",
|
||
"check:ring-vocabulary": "the ring names the printed packet uses (packet-strings.json) agree with the ones the screen uses (gospelInternet.json) in every locale, so a packet and the map cannot name the same ring two ways. By the UPG-100 owner. Headless; in gates:pre.",
|
||
"check:scrim-viewport": "a full-screen SCRIM must cover the VIEWPORT, not just the content pane. A position:fixed;inset:0 scrim is PANE-relative wherever an ancestor sets contain/transform — VerticalTabsLayout sets contain:layout on .vtl-content — so a scrim dimmed 1024x793 @ (256,107) at a 1280x900 viewport and left the 256px rail CLICKABLE behind a modal. A source grep cannot see it: the declaration is correct, the ANCESTOR is the variable. Needs `bun run dev`; browser gate, not in build:unlocked.",
|
||
"check:scrim-viewport:self-test": "reverts each scrim to plain inset:0 and requires EVERY case to FAIL — a gate that cannot fail is worse than no gate, and this one measures a property that looks fine in a screenshot either way.",
|
||
"check:select-zone-race": "each layer of the UPG-100 first-click select-zone fix, asserted separately so a regression names the layer it broke; four mutants fail by name. By the UPG-100 owner. Headless; in gates:pre.",
|
||
"check:selection-kinds": "the UPG-100 selection kinds (by the UPG-100 owner, with d3ec8af7): each kind of selection the list offers resolves to the groups it names. Headless; in gates:pre.",
|
||
"check:selector-fields": "every UPG-100 selector's fields, read FROM the evaluator (not a table in this file), must be requested by every fetch that evaluates it. A missing field matches nothing and prints an EMPTY packet with no error: eight zones did, until the fetch carried wagf_region/wagf_block. Headless; in gates:pre. :self-test has 6 cases.",
|
||
"check:snippet-engine": "every map profile's DEFAULT copy-paste snippet preloads MapLibre (the engine it draws with) and names no api.mapbox.com URL, and its embed.scriptMapbox variant preloads Mapbox AND sets provider:mapbox; a non-map profile has neither. The default used to preload Mapbox, so an unedited paste fetched an engine it never used and made a Mapbox request from a keyless page. Reads the built manifest; --self-test plants the old snippet and a provider-less variant.",
|
||
"check:setup-order": "a line's POSITION in a setup block is not the MOMENT it runs, and two P0 bugs came from that gap. ARM A: `watch(..., { immediate: true })` runs during setup, so anything it reaches must be declared ABOVE it - research-map.vue threw `Cannot access '_lastFitKey' before initialization` and blanked the whole map, because the watcher called a HOISTED function 350 lines down whose own constants were still in their temporal dead zone. A hoisted function promises that its NAME resolves early, not that its BODY can run early, and that asymmetry is invisible from both ends. ARM B: a lifecycle registrar closes over Vue's current instance, which exists only while setup is on the stack, so `onMounted(async () => { await init(); onBeforeUnmount(clean) })` drops the registration silently - DashboardMapPanel.vue leaked a ResizeObserver on document.body plus a window listener, warning only, feature working, and research-map.vue had a second copy this gate found. The fix is never 'move the line': declare the handle at setup scope and tear it down in a hook registered before the first await. Its own false positives are the interesting part - it went 6 hits -> 1 over three rounds, and every one it dropped was prose or shadowing read as code: `onBeforeUnmount` inside a console.error STRING, `table` inside `new Error(`table not found (${r.status})`)` in four files, and a `const [pins, byCountry]` destructured INSIDE the function, shadowing a computed 220 lines away. So it blanks comments, string text and template text (keeping ${...}, which is code) and subtracts every name a span declares for itself. Self-tests on both shipped defects, both fixes, and all three decoys; exits 3 if any arm goes blind.",
|
||
"check:sfc-parse": "every .vue script block must parse as the BUILD parses it — catches the LIVE-CSS backtick that check:template-literal cannot see (it reads CSS comments only). 0 false positives on 83 files, where 3 lookahead heuristics scored 98 and 136.",
|
||
"check:shadow-css-parses": "the CSS we assemble as JS STRINGS must parse as CSS — brace balance and declarations stranded outside any rule. check:sfc-parse proves only that the JS parses, so it passed a stylesheet with 9 orphaned declarations and braces at 12 open / 21 close (REGLIB batch 1); nothing else in the tree validates CSS syntax. Reads every useShadowStyles() argument off the @babel/parser AST the build already uses, because three hand-rolled lexers each produced a confident FALSE RED: a /* CSS comment */ read as a declaration, an apostrophe in a .vue prose header opening a phantom string literal, and a regex literal containing a quote. 9 self-tests run inline on every invocation, including the exact batch-1 damage, so it cannot silently stop being able to fail.",
|
||
"check:shadow-style-labels-unique": "every useShadowStyles call has its OWN label: the label keys the <style>, and a second component with the same label overwrites the first one's CSS in a shared shadow root (the regions list lost its sheet this way). Global, not per-root, so a copied component is caught when it is copied. AST walk cross-checked by a token count.",
|
||
"check:shadow-reduced-motion": "every animating library component honours prefers-reduced-motion: a page's own reset never crosses a shadow boundary, so useShadowStyles injects one per shadow root, and a component styled any other way must carry its own @media (prefers-reduced-motion: reduce) block. Self-test runs every time.",
|
||
"check:shared-reached": "a shared library fix is only as deployed as the last rebuild of EVERY bundle that consumes it. Each bundle ships its own COMPILED copy, so a one-line fix in library/ is live in whichever bundles happened to be rebuilt after it and silently absent from the rest — and every other gate measures ONE leaf, which renders the old behaviour perfectly. Asserts a distinctive marker string per tracked change; consumers are DERIVED from imports, never hardcoded.",
|
||
"check:shot-distinctness": "two README slots must not show the SAME view. README shipped \"List — page 1\" and \"page 2\" as the same picture: the shot chased a Next control and a scroll that DO NOT EXIST, every attempt inside try/catch so the miss was silent, and the PNGs differed by 0.16% render noise so no checksum caught them. Keys on url+VIEWPORT — same url at a different vp (deploy2 mobile) is intended.",
|
||
"check:shot-fidelity": "warns when THIS machine cannot RENDER what the shots must SHOW. `bun run shots` printed 13x OK while every emoji rendered as a tofu box — a glyph fallback loads perfectly (0 console errors, 0 HTTP>=400), so the generator cannot see it. Reports by default; the `check:shot-fidelity:strict` script to block once an emoji font is installed.",
|
||
"check:shot-fonts": "can THIS machine render the emoji the UI uses? Run BEFORE `bun run shots` — no font = tofu boxes in every committed PNG, which every other shot gate passes. Deliberately NOT in build:unlocked: it reports an ENVIRONMENT condition (this box has no emoji font), and a gate that always fails in the build makes the build useless.",
|
||
"check:shots": "a README screenshot that predates the bundle it shows is a picture telling a lie — and it looks IDENTICAL to a current one. Compares each shot's PNG against the BUILT BUNDLE it renders (not raw sources: an unbuilt edit is not yet in any leaf). Fix: node internal/readme-shots.mjs",
|
||
"check:studio-rail": "the Plugin Studio rail is laid out like plugin-designs/: one folder per bundle with its elements inside, bundles in the section of their highest capability (5 → 1) and in name order, one canonical node per plugin. Runs the studio's real rail code on the built manifest, headless.",
|
||
"check:translator-contacts": "the credit line LINKS the translating organisation now, and a contact URL is a link this map stands behind. Reads ALLOWED_CONTACT out of internal/verify-gospel-map.mjs rather than restating it (two copies of an allowlist drift, and the drifting copy is the one nobody reads), then asserts every cr.u is on that list, was recorded `verified` by a real fetch, is never also serving a RESOURCE (wycliffe.org is somewhere to write to, not a publisher this map reads text from), and is actually RENDERED as an anchor — the data can carry 838 URLs while the template shows none. Static, sub-second. Self-test: `bun run check:translator-contacts:poison`.",
|
||
"check:embed-contract": "the map's URL is a PUBLIC INTERFACE: a partner embeds ?codes=<iso3 list>, ?country=<ISO3> or ?view=zero in an iframe and never touches it again. A refactor breaks those links SILENTLY — the leaf still mounts, renders and logs no errors, it just shows the whole world to a church that asked for its forty groups. Asserts through the reader's eyes (the partner-list notice's two numbers), with both numbers and the probe codes derived from the roster the leaf itself reads, never typed in. Browser gate; needs no server of its own. Self-test: `bun run check:embed-contract:poison`.",
|
||
"check:engine-contract": "the map must use the engine it was ASKED for or fail VISIBLY — never a substitute. resources-map read `engineGlobal(provider) || window.mapboxgl`; engineGlobal already defaults to mapbox, so that could only fire for provider 'maplibre' and then returned MAPBOX — a keyless page silently handed the engine that needs a token, which the profile then does not give it, so its tiles 401. Invisible to every other gate: the leaf mounts, renders and logs nothing. Six runtime measurements (2 providers x engine present / blocked / both blocked, CDN blocked by request routing the way a proxy or an offline kiosk does) The static arm was DELETED once check:no-silent-engine went into gates:post (main 23976fb0): two gates owning one source rule meant this leaf was checked twice and every other leaf once, and the narrower copy is the one that drifts. Mapbox arms inject a dummy MAP_TOKEN, because without one the element never mounts on a static tree and that is the environment, not the product. Engine attribution is read off the canvas class, not a debug handle. Browser gate. --poison names where the falsifiable source arm went rather than pretending to have one; the tree-wide gate self-tests with --self-test, not --poison.",
|
||
"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 published Deep Research order (Big Picture, Simple Research, Regions, Prayer Progress, UPG-100 List, Cities with Internet, 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 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.",
|
||
"check:targets": "where each plugin runs: a <profile>.meta.json \"targets\" is a list of known values (web, mobile, desktop, widget; absent = web), a widget target has a widget corner, a backend key names an existing folder, and a plugin that is not for the web (Rudder) is in neither the built tree nor its manifest, so the /cdn export and the gallery cannot carry it.",
|
||
"check:targets:self-test": "plants each check:targets violation and confirms it is caught, and that a valid meta passes.",
|
||
"check:text-highlight": "useTextHighlight (library/composables) matches the inline search-highlight construction it replaced in regions-dashboard UupgsList and people-groups-grid across six range shapes, and pins the two deliberate escaping deltas (backtick escaped; null returns '' instead of throwing) with controls proving each is real.",
|
||
"check:tree-weight": "the tree we publish has a weight and nothing was watching it. A publish was ALREADY BLOCKED once at a 74MB asset tree (Forgejo rejected the pack); it has since grown to 132MB unnoticed. Reports the SPLIT, because a bare total invites deleting the wrong half: the 176 city posters are the heavy half AND the half that cannot be regenerated (they need OSM tiles). Raw bytes, not packed — see publish-html.mjs for why a raw saving is not a shipped one.",
|
||
"check:upg-map-data": "the UPG-100 map must carry OUR polygons/zones/pins, not just a basemap — reads the counter the profile itself writes. Pass live URLs to audit what is published.",
|
||
"check:url-resolver": "cap-5 resolve() must not prefix a rooted URL (the dev-surface 404 class)",
|
||
"check:vision-card-numbers": "the DOXA Vision Card must show the numbers the DATA gives: independently recomputes all four funnel steps (and DOXA-in-frontier) from people_groups.csv + the saved DOXA list, compares with vision-funnel.json, and with --url also with the numbers painted in the card. A typed-in number, a stale funnel file or a rendering bug fails it.",
|
||
"check:zone-census": "every zone a USER CAN SELECT must have a poster (asks the LEGEND, not clusters.json — two collapsed deaf lists are unreachable and not a gap). CONDITIONAL: SKIPs without out/posters/, which is gitignored — run `bun run posters` first. It cannot be fixtured like check:packets-all, because it asserts COVERAGE of real generated output and a fixture would only assert the fixture is complete.",
|
||
"check:zoning-declared": "A project list's ZONING matches what its author DECLARED (data/zoning-declarations.json). Zoning is human judgement — geography first, balanced with language — so a gate cannot decide whether a zoning is GOOD; it can decide whether it still matches what was stated, which is what makes UPG-100 reusable as a PATTERN for any project list (7000/4700/3000/2000/700) zoned by an agent who did not write this code. ★ EVERY OBVIOUS RULE FAILED THE REFERENCE IMPLEMENTATION when measured on the live 28 zones / 2107 records: 'every group in exactly one zone' is wrong (112 double-claims, 110 of them the design-approved deaf bloc double-count, legend-tree.js:151) and 'no group unzoned' is wrong (11 JPN/CHN/MNG groups, plausibly deliberate). So it asserts A1 no zone resolves to zero groups, A2 at most one GEOGRAPHIC zone per group with bloc zones declared as a second partition, A3 the orphan set EQUALS the declared set (a RESOLVED orphan fails too — a stale declaration stops being evidence), A4 every selector type is one selector-evaluator supports, A5 no zone over the declared page budget. A1 and A4 would both have caught this cycle's real defects (deaf + 5 wagf zones resolving to nothing) before a browser did, and A2 caught a third overlap my own hand analysis had missed. Exit 75 without records or declarations. Run :self-test to prove the five still fire.",
|
||
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
|
||
"desktop:build": "bun run desktop:build <plugin>: packages ONE plugin whose meta targets include desktop as a desktop app (Tauri): builds it into .desktop/<plugin>/, builds its sidecar engine for this machine, and runs tauri build with an override pointing the window at it. See documentation/HOWTO-build-the-desktop-app.md.",
|
||
"dropin": "copy the built tree (dx-maps/) into your site's public/ (vite.doxa-maps-build-config.json dropin.dest, default ../doxa-life/public), so ONE server serves the site and every map. Only the drop-in is touched: the site's own files stay, and stale files inside dx-maps/ are removed. It never prunes: --prune into a folder named public/ is refused, because it would delete the site.",
|
||
"gallery": "writes public/__gallery/index.html — the ARCHIVED bundle-index gallery, from public/doxa-maps/manifest.json. It is NO LONGER the root page: as of 2026-09-22 `/` mounts <plugin-studio> in BOTH trees (the built root page is written by `bun run tree`), and the gallery is the opt-in index at /__gallery. Archived, not deleted — the dev server imports renderGallery()/readBuildConfig() from this module.",
|
||
"gen:iso3-to-alpha2": "derive library/data/iso3-to-alpha2.json from vendored CLDR (48.2.2, Unicode-3.0) joined against the 242 codes in library/data/countries.json, so Intl.DisplayNames can produce a LOCALIZED country name — it takes alpha-2 and our data speaks alpha-3. Offline, no dependency. GRADES ITS OWN OUTPUT and writes nothing on failure: 242 coverage, injectivity, and KOS -> XK as the one documented exception (Kosovo has no ISO assignment). Cross-checks the 176-country research spine map and REPORTS disagreements without rewriting it — that file drives fetches and is not this generator's to edit.",
|
||
"internet:adm1": "regenerate data/tables/internet-by-adm1.json (internet use by first-level area; India from NFHS-5 2019-21, CC BY 4.0) from data/internet/ through a strict name crosswalk to the shipped gb-IND-ADM1 boundaries. Fails on an unmatched name, never guesses. Generic rows (iso3, adm1, measure, sex, area, value, source, year) so other plugins can reuse it.",
|
||
"library-index": "regenerate documentation/reference-library-index.md — the DERIVED menu of the 84 reusable library pieces an author can import instead of rebuilding. Was UNREACHABLE, and the index did not exist at all: its header promised 'can't drift from the code' while nothing ever ran it.",
|
||
"lists": "regenerate data/people-groups/lists/<id>.json, the DOXA slugs in each people-group list of library/lists/lists.json, from the IMB CSV and the DOXA snapshot. Re-run after either changes.",
|
||
"packet-fixture": "regenerate the committed records subset (smallest zone, image_url stripped) that lets CI build a packet at all — out/ is gitignored, so a clean checkout has neither the records cache nor any packet.",
|
||
"packet:ci": "build EVERY committed fixture, no network, each renamed to its own output so they coexist. Was one fixture (tibet); packet.mjs names its output for the ZONE not the fixture, so sequential builds overwrote each other and check:packets-all saw a single SHAPE while reporting success. The fixtures are chosen at the PER_PAGE=20 page boundary — exactly 20, 21, and 1 group — because laos (95) and tibet (43) are both mid-range remainder cases and the boundary itself had never rendered.",
|
||
"packets": "the prayer packets. READS out/posters/<zone>.html for page 1's map, which `bun run posters` writes — without it the packet still builds and page 1 is simply BLANK, so the script now warns per-zone AND in its summary. PACKET_LANG=<loc> for one language, ONLY=<zoneKey> for one zone, NOPHOTOS=1 to skip the photo fetch. ENV: ONLY=<zone> one zone · PACKET_LANG=<locale> · PACKET_RECORDS=<file> a committed fixture instead of the live API · PACKET_LIST=<list-id> the universal list prop for this generator (doxa default; unreached/frontier/frontier-500k/unengaged read data/people-groups/lists/<id>.json from disk, since there is no server to fetch the asset pool from). PACKET_LIST FAILS CLOSED on an unknown or empty list rather than silently printing every group — a packet that showed the wrong list is printed and handed to someone. It exists because the browser path honours ?list= while this generator did not, so the six-row funnel (a list's own total inserted before the reader's row) was unreachable by the only pipeline that can render it.",
|
||
"posters:one": "ONLY=<zoneKey> to build one",
|
||
"serve": "serves public/ the way PRODUCTION does: a DIRECTORY request returns 404, no index fallback. This was python3 -m http.server, which DID resolve a directory to index.html while the live static layer does not — so gates passed on URLs that 404 live (measured 2026-09-23; it cost a false \"publish failed\" reading and hid a bad URL in verify-shell-scroll for a whole flight). Loopback only, same as before. Override port/host with PORT/HOST.",
|
||
"serve:permissive": "LOOPBACK ONLY. This was --bind 0.0.0.0, which published the built tree on every interface of whatever machine ran a gate; measured on the build VPS it answered HTTP 200 on the box's PUBLIC IP while a browser gate was running. Every consumer (verify-browser-all, check:a11y-button-names, check:nav-reachable, smoke:share-all) already defaults to 127.0.0.1, so nothing needed the wider bind. Override with BASE_URL/BASE if you deliberately want to reach it from another host.",
|
||
"serve:shell": "LOOPBACK ONLY, same reason as `serve` above — a local preview server has no business answering on a public interface.",
|
||
"shell": "OPTIONAL, not part of build: overlays a layouts/ wrapper onto public/ for local use (the served root is the generated index.html); --no-token so the local credential is never copied into the served root",
|
||
"shots": "regenerate documentation/readme-images/*.png (fetches its own Mapbox token SERVER-SIDE, so the browser CORS block does not apply). Was UNREACHABLE — present but invoked by no script, which is how the 12 PNGs silently rotted a day behind the UI.",
|
||
"smoke:studio": "5s DEV-surface check: / mounts the studio, tabs>0, 0 4xx (exit 2 = dev server down)",
|
||
"verify:browser-all": "runs EVERY browser gate in the GATES array over the built tree in one command (needs `bun run serve` on 4173 + MAPBOX_TOKEN). Deliberately does NOT state a count: GATES lives in the .mjs and grows, so a number here rots on the next append — it said \"ALL 7\" while the array held 9. The script prints its own GATES.length at runtime; trust that. Every gate listed was RUN before being listed. The `verify:browser-all:plugin-gallery` mode is separate because it needs the vite dev server.",
|
||
"verify:gate1": "the Gate-1 chain end-to-end against LIVE data: header emits research-filter -> composite passes props down -> the REAL cap-2 grid re-filters -> count reflected back up. Asserts the NEGATIVE (a filter that must return 0 does) and RESET. Was an ORPHAN — present in internal/acceptance/ but invoked by NO script, so it could never report anything.",
|
||
"verify:gospel-map": "the gospel-resources MAP: pins render (not merely load), a tap opens a named group, EVERY link in the index is on a screened publisher, the table deep-link carries the group's ISO, and the gap filter leaves only tier 0. Needs a server (bun run serve); SKIPS loudly without one.",
|
||
"verify:interop": "the CAP-4 INTEROPERABILITY GATE (real browser, real shadow DOM, real live data) — closes the gap verify:gate1 leaves open. Drives the ACTUAL compiled composites: (1) DESKTOP dashboard-plus-grid map->grid (a facet-rail click must narrow the grid's live `Showing N` count line — card count is a proven FALSE-NEGATIVE trap, always 24, never asserted on), (2) DESKTOP grid->map (selecting table rows must MOVE THE CAMERA, read back off window.__rdashMap.getCenter()/getZoom() — useSelectionCamera.js's contract), (3) MOBILE research-map-plus-grid, where source-reading FIRST established there is NO cross-wire between the map and grid children by design (StackLayout composes two independent elements, no provide/inject/prop crosses the boundary) — so this pair asserts each half narrows on its OWN control, not a link that does not exist. Prints ONE line per combination, pass/fail only, plus a combination table. SKIPs cleanly (exit 0) when the tree is mid-build (internal/settled-tree-gate.mjs) or pray.doxa.life is unreachable — never reports a false defect from a torn tree. Spawns its own internal/serve-strict.mjs on PORT (default 4180) so it does not depend on another process's server. run `verify:interop:self-test` for the NEGATIVE test.",
|
||
"verify:interop:self-test": "TWO regression modes, each required to fire ITS OWN combination-named failure: grid-prop-drop (default — freezes the grid child's filter props/attrs on the live dashboard-plus-grid page, reproducing the exact historical bug \"selecting anything in the dashboard above did not filter the grid below\") and `verify:interop:self-test:camera-freeze` (pins fitBounds/flyTo to no-ops on window.__rdashMap, reproducing \"the grid->map link stops moving the camera\"). A harness that has only ever reported PASS is unproven.",
|
||
"verify:interop:self-test:camera-freeze": "★ A DEDICATED ENTRY, not a mode appended at the call site. The parent script value ends in an inline # comment, so a mode appended by the caller lands INSIDE that comment and the shell drops it — the run then exits 0 in DEFAULT mode, indistinguishable from the mode being selected. A self-test is the proof a gate can fail, so a self-test whose mode cannot be chosen is a gate certifying itself. This entry hardcodes its mode, so nothing has to be appended. (Flag names are deliberately not spelled here: check:script-flags scans script VALUES, and naming one in prose reads as advertising it.)",
|
||
"check:gospel-set-fit": "the set charts FIT the canvas they are given. renderWorldCopies:false puts a hard floor under the zoom at log2(canvasWidth/512) — one world must fill the canvas WIDTH — and on a wide, short figure that floor is too close for the drawing's own height, so the outermost groups fall outside the frame (measured at 1440: 54 outside in ar, 97 in en, which is what proved it was the width and not RTL). Pure arithmetic against the components' own projection, so no browser and no drift. Negative control: the old 0.17 extentFraction still fails 796x249 at 271px of 169, so a pass cannot be vacuous. Also asserts both charts share one footprint, or the same population reads as a different size when a reader switches tabs.",
|
||
"check:sentence-slots": "a translated SENTENCE that carries values stays ONE key, through library/composables/useSentence.js. Four properties that all fail silently: a translator can REORDER the clauses (the slots follow the message, not the call); a missing slot leaves its placeholder VISIBLE rather than rendering null or an empty gap, while 0 and '' are real values; slotted markup (a bold count, a dir=ltr island) goes in as a VALUE and is never translated around; and English renders as it did before the migration. Exists because the pattern it replaces built sentences by concatenating translated fragments around untranslated text — 'languages have', 'people.' — which in Arabic gives Arabic words in English clause order: grammatically broken, and worse than leaving it English because it looks translated. Sentinels are private-use (U+E000+n) so nothing in the i18n pipeline may rewrite them, and any sentinel already in a message is stripped before ours are placed — without that a stray swallowed a slot and the arm still passed, which is how this gate found its own weakness.",
|
||
"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. Measures on a fresh checkout: it reads the API-written cache (out/_records.json) when present and otherwise the committed fixture (library/scripts/posters/fixtures/equiv-records-npl-ind.json, the 124 public records the two arms consume), feeds BOTH sides from that same file via RECORDS_FILE, and says which input it measured.",
|
||
"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.",
|
||
"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.",
|
||
"check:popup-pan-controls": "the popup auto-pan reserves the map's bottom control band, so a popup that clears the CLIP is also clear of the things that take the click. Measured at 390 on a 366x732 map: the pan fitted the popup inside the container, cut 0, and a real click on Full Profile still did nothing because the top element at the button's centre was div.maplibregl-ctrl-attrib-inner (and span.pin-hint-pill on the sibling pin) - a cut-height check cannot see this, because the cut really was zero. Attribution is a licence term of the basemap, so the popup may not be raised over it; the usable frame is shorter than the container and the fit test says so. Runs the SHIPPED panPlanFor, read out of the SFC with new Function (it is not exported), never a re-typed copy, and asserts where the popup ENDS UP rather than re-deriving the expected number from the same formula. Sweeps band heights 0-120px and every width the component renders at. Source-only, headless.",
|
||
"check:control-inset": "bottomControlInset MEASURES the map's real bottom control band, so the pan reserves the band the controls actually occupy. check:popup-pan-controls proves the ARITHMETIC; this proves the band is real. If the inset silently returned 0 -- a selector typo, an engine renaming its control classes, a control moving outside the container -- panPlanFor would get bottomInset 0, behave exactly as it did before the fix, and check:popup-pan-controls would still pass every case, because its arithmetic is still correct: the bug back with both gates green. 8 cases against a real DOM, running the SHIPPED bottomControlInset read out of the SFC (it is not exported), never a copy: the band equals what the DOM independently says (not merely non-zero); no controls reserves 0; a zero-sized control reserves nothing; the taller of two corners wins; a wrapped attribution reserves more than one line; a control outside the container is ignored; a null container returns 0; and the MAPBOX corners count too, since the component takes a provider prop and every other case renders MapLibre names. Needs a browser: every assertion rests on getBoundingClientRect. 75 = no slot, never a pass.",
|
||
"check:panel-sheets": "the shared panel geometry (library/components/poster/panelSheets.js) for posters printed on several sheets: 60x96 = 3 × 60×36 stacked and 66x66 = 2 × 36×66 side by side, 6 in joins, and a single sheet; the sheets tile the finished poster, glue slices tile and bleed slices repeat exactly the join, a bleed join prints a guide not a solid band, a screen preview always shows the red band, and the Bless 5x8ft takes its bands and marks from the module. Two-sided (4 planted defects). Headless.",
|
||
"check:poster-wall-host-seams": "PosterWall's two host seams, and that with neither set every wall behaves as before. headerAction: default null is the Show-on-map TOGGLE (aria-pressed, one window event per click, is-last, no emit); { labelKey, ariaKey } is a plain button that emits header-action with the item, no window event, no aria-pressed, and reads the label alone for an item with no count. previewSrc: default null is the stored previews/<id>.webp and no IntersectionObserver; set, a card renders only once in view of the wall's own scroller, one render at a time, cached per page, and null / a throw / a timeout ends on the preview-unavailable placeholder. Image errors: a default wall retries the tree's own copy before the placeholder; a generated preview goes straight to it. Section heads: a section with people reads \"<n> people groups · <items>\" exactly as before; one with none reads its items alone. sectionItemCountKey lets a wall count one section in its own words ({ key, n }); unnamed sections keep the default. Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 17 mutants each FAIL."
|
||
},
|
||
"devDependencies": {
|
||
"@capacitor/android": "^7.6.9",
|
||
"@capacitor/cli": "^7.6.9",
|
||
"@capacitor/core": "^7.6.9",
|
||
"@tauri-apps/cli": "^2.11.5",
|
||
"@vitejs/plugin-vue": "^5.0.4",
|
||
"playwright": "1.61.1",
|
||
"terser": "^5.46.0",
|
||
"vite": "^5.4.21",
|
||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||
},
|
||
"dependencies": {
|
||
"@maplibre/maplibre-gl-geocoder": "^1.7.0",
|
||
"jspdf": "^4.2.1",
|
||
"maplibre-gl": "^5.6.0",
|
||
"pinia": "^2.3.1",
|
||
"topojson-client": "^3.1.0",
|
||
"vue": "^3.5.20",
|
||
"vue-i18n": "^9.14.5",
|
||
"world-atlas": "^2.0.2"
|
||
},
|
||
"engines": {
|
||
"node": ">=18.0.0"
|
||
}
|
||
}
|