{ "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": { "dev": "bun x vite", "build": "bash internal/build-lock.sh", "build:watch": "bun x vite build --watch", "preview": "bun x vite preview", "clean": "node internal/clean.mjs", "build:bundle": "bun x vite build", "tree": "node internal/generate-tree.mjs", "gallery": "node internal/generate-gallery.mjs # 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 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.", "check:drop": "node internal/drop-portability-check.mjs", "check:parse": "node internal/inline-parse-check.mjs", "verify": "node library/scripts/research/verify-all.mjs", "verify:full": "node library/scripts/research/verify-all.mjs --full", "serve": "node internal/serve-strict.mjs # 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:shell": "python3 -m http.server 4174 --bind 127.0.0.1 --directory library/layouts/app-wrapper-material # LOOPBACK ONLY, same reason as `serve` above — a local preview server has no business answering on a public interface.", "cap:sync": "./node_modules/.bin/cap sync android # 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.", "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)", "check:tauri": "node internal/check-tauri-config.mjs", "tauri:build": "./node_modules/.bin/tauri build", "apk:debug": "bun run build && bun run cap:sync && 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", "phone": "bun run apk:debug && bun run apk:install", "shell": "node internal/install-shell.mjs --no-token # 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", "posters": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs", "posters:one": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs # ONLY= to build one", "posters:shoot": "cd library/scripts/posters && DSF=2 node shoot.mjs", "posters:gallery": "cd library/scripts/posters && node build-gallery.mjs && node build-hd-gallery.mjs", "posters:export": "cd library/scripts/posters && node export-public.mjs", "posters:previews": "cd library/scripts/posters && python3 make-previews.py", "packets": "cd library/scripts/posters && VITE_API_BASE_URL=https://pray.doxa.life node packet.mjs # the prayer packets. READS out/posters/.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= for one language, ONLY= for one zone, NOPHOTOS=1 to skip the photo fetch.", "token": "node internal/write-token.mjs", "smoke": "node internal/smoke.mjs", "smoke:embeds": "node internal/embed-smoke.mjs", "smoke:gallery": "node internal/plugin-gallery-smoke.mjs", "smoke:playground": "bun run smoke:gallery", "smoke:share": "node internal/share-roundtrip-smoke.mjs", "probe:legend-columns": "node internal/legend-columns-probe.mjs", "sync:dropin": "node internal/sync-dropin.mjs", "deploy:dev": "bun run build && node internal/sync-dropin.mjs --from-config --prune # destination + allowlist come from vite.doxa-maps-build-config.json (dropin.dest / dropin.include); DOXA_DROPIN_DEST= overrides the destination", "check:public-hygiene": "node internal/public-hygiene-check.mjs", "check:label-joins": "node internal/check-label-joins.mjs", "check:dxlife": "node internal/check-dxlife-untouched.mjs", "check:facet-cascade": "node internal/check-facet-cascade.mjs # 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-cascade:self-test": "node internal/check-facet-cascade.mjs --self-test", "check:facet-one-field": "node internal/check-facet-one-field.mjs # ONE AXIS = ONE CONTROL-REACHABLE FACET. Asserts on the PARTITION each facet induces over the rows, NOT on which r. 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:facet-one-field:self-test": "node internal/check-facet-one-field.mjs --self-test", "check:facet-changes-map": "node internal/check-facet-changes-map.mjs # 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:self-test": "node internal/check-facet-changes-map.mjs --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:heartbeat": "node internal/check-heartbeat-threshold.mjs # 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": "node internal/check-heartbeat-threshold.mjs --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.", "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 && bun run gallery", "check:card-deck": "node internal/check-card-deck.mjs", "check:poster-raster": "node internal/check-poster-raster.mjs", "check:deck-click": "node internal/check-deck-click.mjs", "check:i18n-parity": "node internal/check-i18n-parity.mjs", "check:locale-reachable": "node internal/check-locale-reachable.mjs", "check:poster-prints": "node internal/check-poster-prints.mjs", "check:provider-switch": "node internal/check-provider-switch.mjs", "check:mfe-two-tab": "node internal/check-mfe-two-tab.mjs", "check:share-policy": "node internal/check-share-policy.mjs", "check:zone-census": "node internal/check-zone-census.mjs", "check:print-geometry": "node internal/check-print-geometry.mjs", "check:packet-browser": "node internal/check-packet-browser.mjs", "check:packet-render": "node internal/check-packet-render.mjs", "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", "verify:gospel-map": "node internal/verify-gospel-map.mjs # 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.", "check:sri": "node internal/sri-consistency-check.mjs", "fix:sri": "node internal/fix-sri.mjs", "check:composite": "node internal/check-composite-freshness.mjs # 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:shots": "node internal/check-shot-freshness.mjs # 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:library-index": "node internal/check-library-index.mjs # 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:quoted-scripts": "node internal/check-quoted-scripts.mjs # 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.", "capability-exemplars": "node internal/generate-capability-exemplars.mjs # 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": "node internal/check-a11y-button-names.mjs # 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 `