SPEC-DASHBOARD-KIT D3, and the last of the three decided-not-built items. The bus carried two coordinated channels — filterSet NARROWS scope, selectionSet MARKS individuals — and was missing the third: hoverKey POINTS at the one record under the pointer. It is how a reader sweeps a table and watches the map answer without choosing anything, and kepler.gl treats hover as a configured interaction beside filters for the same reason. IT IS A PLAIN ref AND NOT A SET, AND THAT IS THE WHOLE DESIGN. Hover fires on every mousemove across a 2,107-row table. selectionSet had to become a plain Set with a version counter precisely because a reactive collection fires per element; a single ref has none of that problem — one dependency, one write, no iteration. A repeat is a no-op, so an unchanged value never wakes a panel. HOVER IS NOT SELECTION, AND MUST NEVER BECOME IT. The cheap implementation is to put the key in selectionSet "just while hovering", and every consequence of that is bad while none is obvious at the call site: useSelectionCamera watches selectionVersion, so a hover would fly the camera at every mousemove — the resize-strobe bug wearing a new hat, on a Driver who is photosensitive. The CSV exporter reads selection > filter > all, so a hover would silently change what an export contains. The table's selection pill and the shell's live region both report selection counts, and both would narrate the pointer. So a panel may fade or outline on hover; it may not fly, select or filter. NO PANEL IS WIRED, deliberately, and I want that on the record rather than left to be noticed. D3 says panels opt in, and the first opt-in belongs with a consumer that has a reason: wiring one into Big Picture speculatively would be a visual change to the flagship that no reader asked for, landed while the kit stack is paused. That does leave a channel nothing uses — the same criticism I made of the unmounted filter chips — so the difference has to be earned rather than asserted: this is a documented, gated contract with its first consumer named, not a component that was quietly orphaned. check:hover-channel is what makes it a contract instead of dead code; the first panel to use it inherits the rules instead of inventing them. Three mutants, all caught: routing setHover through selectionSet, dropping the equality guard, and removing hoverKey from the returned surface. SOURCE ONLY. Stacked on landing 2. No rebuild needed — nothing renders it yet.
455 lines
96 KiB
JSON
455 lines
96 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",
|
||
"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",
|
||
"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: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:backlog-refs": "node internal/check-backlog-refs.mjs",
|
||
"check:backlog-status": "node internal/check-backlog-status-enum.mjs",
|
||
"check:basemap-labels": "node internal/check-basemap-labels-localized.mjs",
|
||
"check:capability-citations": "node internal/check-capability-citations.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-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:composites-mount": "node internal/check-composites-mount.mjs",
|
||
"check:coverage-consistent": "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: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:doc-scripts:self-test": "node internal/check-doc-quotes-scripts.mjs --self-test",
|
||
"check:done-evidence": "node internal/check-done-evidence.mjs",
|
||
"check:drop": "node internal/drop-portability-check.mjs",
|
||
"check:dxlife": "node internal/check-dxlife-untouched.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-one-click": "node plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs",
|
||
"check:export-one-click:maplibre": "node plugin-designs/doxa-research-map/scripts/check-export-one-click.mjs --provider maplibre",
|
||
"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-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: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:gospel-all": "node internal/check-gospel-all.mjs",
|
||
"check:gospel-tables-staged": "node internal/check-gospel-tables-staged.mjs",
|
||
"check:embed-contract": "node internal/check-gospel-embed-contract.mjs",
|
||
"check:engine-contract": "node internal/check-gospel-engine-contract.mjs",
|
||
"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: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: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:i18n-treeshake": "node internal/check-i18n-treeshake.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: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-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-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: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: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-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-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:packet-browser": "node internal/check-packet-browser.mjs",
|
||
"check:packet-fixture": "node internal/generate-packet-fixture.mjs --check",
|
||
"check:packet-render": "node internal/check-packet-render.mjs",
|
||
"check:packet-render:expect-funnel": "node internal/check-packet-render.mjs --expect-funnel",
|
||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.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:poster-card-fits": "node internal/check-poster-card-fits.mjs",
|
||
"check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs",
|
||
"check:poster-print-fallback": "node internal/check-poster-print-fallback.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:print-geometry": "node internal/check-print-geometry.mjs",
|
||
"check:pin-dim-slider": "node plugin-designs/doxa-research-map/scripts/check-pin-dim-slider.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:rtl-shaping": "node internal/check-gospel-rtl-shaping.mjs",
|
||
"check:rtl-shaping:poison": "node internal/check-gospel-rtl-shaping.mjs --poison",
|
||
"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:settled": "node internal/settled-tree-gate.mjs",
|
||
"check:sfc-parse": "node internal/check-sfc-script-parses.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: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:tabs-localized": "node internal/check-tabs-localized.mjs",
|
||
"check:tauri": "node internal/check-tauri-config.mjs",
|
||
"check:template-literal": "node internal/template-literal-check.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:verification-tags": "node internal/check-verification-tags.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",
|
||
"clean": "node internal/clean.mjs",
|
||
"deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune",
|
||
"dev": "bun x vite",
|
||
"fix:sri": "node internal/fix-sri.mjs",
|
||
"gallery": "node internal/generate-gallery.mjs",
|
||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence",
|
||
"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",
|
||
"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",
|
||
"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"
|
||
},
|
||
"//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.",
|
||
"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: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 is auto: on one map leaf, no token draws with MapLibre and makes zero api.mapbox.com requests, a token draws with Mapbox, and an explicit provider wins. Under the fleet browser policy it is a rega run, not part of gates:post.",
|
||
"check:backlog-refs": "a card may not depend on a card that does not exist. A dangling depends_on can never be done, so the card can never become ready and nothing says so — it reads as ordinary blocked work forever. FAILS only for depends_on on an OPEN card; a dangling parent or a stale link on a done card is REPORTED, because failing on filing debt teaches people to delete the reference rather than restore the card. Reads all three phase backlogs — references legitimately cross phases. SKIPs when _qa-cycle is absent.",
|
||
"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: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-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-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: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: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 (rega's alone).",
|
||
"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:descriptions": "every shipped profile must say what it is (reads the BUILT manifest)",
|
||
"check:done-evidence": "a card may not reach status:done with NO bluf AND NO fields — the Doxa team inherits this board and an evidence-less 'done' cannot be reconciled against the code. 10 pre-existing are baselined by id; the list should only ever SHRINK.",
|
||
"check:dxlife": "⚠ NOT WAVE-SAFE, deliberately unwired: exit codes 0/1/2 are a DOCUMENTED contract (0 unchanged · 1 drift · 2 clone not found), and run-parallel grades any non-zero as FAILURE — so a checkout without the sibling /root-dx-life clone goes red on a precondition. Changing the contract could break another consumer; hand-run it, or give it a recognised SKIP first.",
|
||
"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 Driver's words + 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. rega's 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-one-click": "the Simple Research 'Export poster' button gives a FILE on ONE click (no menu, no dialog, no second step): leaf AND Research Gallery, en AND ar; the file is a real print PNG (US Letter @200 DPI); 0 page errors. Run inside the shared browser slot. Exit 2 = SKIP, 75 = NOT MEASURED.",
|
||
"check:export-one-click:maplibre": "the same four one-click exports 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 the Driver reported it 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 the Driver found five real defects in one sitting 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: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: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: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: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, on a photosensitive Driver); 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.",
|
||
"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-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:language-code-link": "the map's language selection reaches the grid as a locale-stable CODE, or narrows nothing at all. rega 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: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: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. rega's 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-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: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: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-cross-plugin-imports": "no plugin imports from another plugin's folder (src/, utils, data) by a relative path; shared code goes through library/ (@map/\u2026). A composite may mount another bundle's PUBLIC profile .vue. --self-test plants a crossing import.",
|
||
"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: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-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: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: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 NEITHER poster nor source — the DEFAULT drop — nothing must print and the reader must be told why. check:poster-print-fallback SKIPs that case, so the behaviour a reader most often meets had no gate. MUST be served from the TREE ROOT, not public/, or assetsBase resolves outside the tree and the poster loads when it should 404.",
|
||
"check:poster-print-fallback": "clicks the REAL poster print control with 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; SKIPs otherwise.",
|
||
"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: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: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: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: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 \u2192 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 \u2014 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 \u2014 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 \u2014 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 \u2014 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) plus a static arm on the source, comments stripped so the fix's own quotation of the deleted line is not read as the defect. 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. Self-test: `bun run check:engine-contract:poison`.",
|
||
"check:snapshot-guard": "every gospel-resources catalogue fetcher must write through scripts/_snapshot-guard.mjs, which refuses a write that loses more than a tenth of the previous snapshot. Written after a rate-limited FCBH run silently replaced 1,666 video scopes with 1,103. Static, sub-second. Self-test: `bun run check:snapshot-guard:poison` injects a raw write and the check must fail on it.",
|
||
"check:spa-tab-order": "the cap-5 SPA must open on Big Picture and keep the Driver's order (Big Picture, UPG-100, Prayer Progress, Gospel Resources) — asserted as PRESENCE + tab-1 + RELATIVE order on the RENDERED rail, because the rail gained three sections and now renders 12 rows with others between them. ⚠ NOT WAVE-WIRED: it FAILS rather than skips when the SPA is unreachable, deliberately (\"a gate that cannot reach its subject has not passed, it has not run\") — correct standalone, but gates:post runs on checkouts with no server. Needs `bun run serve` on 4173.",
|
||
"check: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: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.",
|
||
"deploy:dev": "destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST=<dir> overrides the destination",
|
||
"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.",
|
||
"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 and go-int 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.)"
|
||
},
|
||
"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"
|
||
}
|
||
}
|