A public preview plugin for the whole-India poster: every India people
group on one printable sheet, generated live from the current data
(nothing stored) and shown as its printable sheets with the glue strips.
Each poster carries the blessing — header "Bless India in Jesus' name"
and the Aaronic blessing (Numbers 6:24-26), switchable.
Generates with the plugin's own copy of the renderer; shows the sheets
via the shared PanelSheets and prints as a document at true size.
Public and experimental; a phone sees the preview but Print is blocked
(desktop only) until a design is proven phone-safe. Registers the
blessIndia poster type for the catalogue; label translated in 11
locales. Replaces the earlier India Posters review draft (not landed).
Element <bless-india-posters>.
A review gallery for the whole-India poster challenge — fitting every
India people group on one printable sheet. It gathers each design
attempt with what was tried and the measured result, and lets a
reviewer label it, score it 1-10, and tag it a best practice or one to
avoid (saved locally, exportable as JSON).
Withheld from every public build: the attempts, their descriptions,
and the poster documents load only under the dev server, in a dev-only
gallery module that compiles away. A production build shows only the
"solution in progress" notice and emits zero whole-country-poster
bytes. Element <india-posters>; stage experimental, visibility team.
The lock guards the pilot, so it is the pilot lock, and the act is a merge onto the pilot: the
README's summary row, the harness paragraph, the kitchen list, the glossary and the alt text, and the
diagram's title, the lock ring, the coordinator line and the legend. "Publish to landing" and
"landing (main)" keep the word for the landing branch. Also: about 15 seconds to see a landed change;
a "merge" glossary row instead of "rebase", with pilot and landing beside development and main; the
cloud paragraph names BRANCH=development and PRODUCTION_BRANCH=main. The PNG is re-rendered at 2x.
The pilot branch (development) is the shared working version a hot-reload server follows; the
landing branch (main) is production, which its server rebuilds from; PR branches hold one feature
each, start from the landing branch and land on the pilot after the checks. The worktree, lock,
gates and build-owner steps stay. A short paragraph on cloud development with a swarm: a self-hosted
forge, a pilot and a landing server per app, and the pilot server's PR switching named as planned
only. The diagram's labels follow (pilot at the centre behind the lock, landing below); the PNG is
re-rendered at 2x from the SVG.
Copied unchanged from the bridge kit (727fb55): TUTORIAL.md (for the repository's GitHub admin,
about 10 minutes), pull-contributor.yml (the workflow it sets up; it refuses a contributor's
changes to .github/) and test-pull-contributor.sh (the TUTORIAL's "check the guard yourself",
which sits next to it). None of them is under .github/, so nothing runs by itself here.
README: one line in the agent section links the tutorial; nothing else in README changes.
.github/workflows/pages.yml is now documentation/deploy/github-pages.workflow.yml, with a
one-line README beside it: copy it into .github/workflows/ in your own repository to deploy to
GitHub Pages. Nothing referenced the old path. The repo carries no .github/ folder any more, so
a host never runs it automatically.
- Reword source comments in agent-written, neutral terms and drop internal task-file
paths, so shipped source cites no process documents or attributions (passes
check:no-new-driver-references).
- documentation/how-to.md: note that Bible Posters needs https://bible.helloao.org in
connect-src (text is fetched per book at render time; none is bundled), so a host
under a strict CSP does not silently block it.
- library/components/WARNING.md, library/dashboards/README.md and
documentation/contributing/README.md taught "graduate into the library at the second
consumer". They now teach the plugin-designer skill's rule (section 3): a component that
could serve more than one map is built once in library/, driven by props, and landed through
the build owner; one-off or experimental pieces stay in a map's folder; a new pattern is a
pilot, then approved, then rolled out. The contributing guide's "full reasoning" link pointed
to an index without it; it now points to the skill.
- The app shell doxa-maps-spa is labelled Deep Research. Ten docs still called it the Research
Gallery (15 mentions): DEPLOY.md, the desktop how-to, embeddability, develop-a-plugin,
reference-universal-props, reference.md, the shell's EMBED.md, and the plugin-designer skill
with two of its references. Identifiers (doxa-maps-spa, branch and route names) are unchanged;
README.md is not touched.
library/utils/peopleGroupName.js (pure) returns a group's name in the reader's
language only when the row's names are transliterations. When two or more of its
Latin-script names (fr pt es tl id de) are far from the English one (bigram Dice
< 0.5), the row was machine-translated as words (Balantak → "Balance",
Balo → "Sac à dos"), and every locale of that group shows English. That is
8,055 of 21,419 rows (37.6%). it and ro have no column and show English; zh
reads zh_CN. A fallback shows the caller's own English name.
check:pg-name-gate (gates:pre) proves it on the real table: the 4 known
translated rows show English in 7 locales, Balangao Bontoc keeps its names,
planted rows are two-sided, the whole-table fallback stays between 30% and 45%,
and no file outside the helper reads pg-names rows (pplr-data-maps is the
exception; the scanner is negative-controlled). Five mutants of the helper each
fail it. No consumer is wired yet.
staging-reference-v2.html's header states its design in plain words instead of a dated quote
(nothing reads the file; only file-map.md names it). THIRD-PARTY.md and
reference-universal-props.md no longer name an agent.
Comments, messages and docs only; no check logic changes.
- The role-word gate is renamed check-no-internal-role-nouns (and its baseline fixture). Same
logic: it builds the word from parts so the file does not carry it, and a planted line in
title case and in capitals still fails it while WebDriver and "GPU driver" do not.
- public-hygiene-check: the two role patterns are built from parts (the regexes are byte-
identical), the header loses the forge and date wording, and the prayer comment loses its
decision date. Its output on the tree is identical in normal and --strict modes.
- landing-preconditions, check-no-public-binds, check-published-tree-weight: no fleet tool
name, no "this box", no forge name in comments and messages.
- Removed: check-backlog-refs, check-backlog-status-enum, check-done-evidence,
check-verification-tags and check-dxlife-untouched (they police a private backlog and
workspace), with their scripts, docs, gate-list entries and orphan-baseline entry, and the
committed needs-eyes pages (check-embedded-rtl --emit writes them on demand).
- package.json docs: no agent names. Docs: the stale check mentions and the snapshot
references are gone (the snapshots themselves leave in the studio P commit).
gates:pre: PASS 115, FAIL 0, NOT MEASURED 1 (browser seat).
VerticalTabsLayout, header off: on a phone a 44px bar (a real grid row) replaces the floating corner
circle, with the rail toggle at the inline start and a #phone-end slot at the end; on a desktop a
#rail-end slot sits at the bottom of the rail (it receives `collapsed`). Neither renders with a header
on, so header-on consumers and desktop layouts without a filler are unchanged. The --map-fab-* footprint
is no longer published; its readers fall back to their plain insets.
ShellLanguageMenu fills either slot: the switcher's languages plus the current one, a native select
under a globe face. A pick dispatches one cancelable, composed, bubbling doxa-language-request on the
host element (library/utils/languageRequest.js). A host that cancels keeps the language; otherwise the
menu emits 'switch' and writes ?lang= only on a page the tree generated.
check:shell-header (gates:pre, Vite SSR) asserts where the bar and the rail slot render, the event's
shape and count, cancel and no-cancel, the ?lang= rule and the language list; its self-test (gates:post)
refuses 11 planted defects. check:nav-reachable now presses the bar's toggle.
deploy5-desktop-rudder.png is replaced with a screenshot of Rudder running (Ready, with a
transcript). It carries no names, emails, paths, hostnames or tokens, and its metadata is
stripped: the PNG holds only IHDR, IDAT and IEND. The alt text says what it shows.
The composites both shells now name (doxa-maps-spa's Prayer Mobilization
row, posterverse's composes) resolve once poster-design exists, so the
tree is generated here, once, on the merged tree.
delegation-model.svg, re-rendered to delegation-model.png at 2x (only IHDR, IDAT, IEND):
- development, not main, is at the centre, behind a landing lock that takes one landing at a
time; the plugin ring and the three outputs are kept (the desktop output working).
- A coordinator and a gate-keeper replace the single overseer; the legend gains them.
- "How a change lands" has four steps: own branch; a second reader; one at a time (in the
lock: merge development, rebuild, run every check, push only if nothing fails); promote
to main (only when the owner approves, after full checks on that version).
- The outputs band adds: the dev preview follows development, the published site follows
main.
README, the same section, so the text matches the picture: the summary table row, the
section's intro, the image's alt text, the kitchen list (now five points) and the glossary
("landing lock" replaces "merge queue"). No tool or personal names.
- The clone examples in README.md and documentation/DEPLOY.md name a private server; they
now read `git clone --depth 1 <your-repo-url>`.
- skills/plugin-designer/references/backend.md quoted a planning heading that names a
platform; it now says the tiers are recorded "as an open, unproven assumption".
- The desktop app (Tauri; Rudder runs as one) works: the capability table's Desktop column
is ✅, and the outputs table, the Output 3 heading and its screenshot's alt text say so.
The WordPress column keeps 🧪, so the table's legend is unchanged.
- delegation-model.svg: the desktop output's label reads WORKING, in the green of the other
two outputs. delegation-model.png is re-rendered from it at 2x in Chromium. Rendering the
previous SVG the same way reproduced the committed PNG with 0 pixels different, and the
new render differs only inside the desktop box. The PNG carries only IHDR, IDAT and IEND.
The shadow-styles id must be a plain lowercase identifier (check:shadow-css-parses
reads anything else as a CSS rule concatenated onto the id), so 'PanelSheets'
becomes 'panel-sheets'; nothing imports the component yet. The derived library
index gains panelSheets.js and PanelSheets.vue.
bun run capability-exemplars on the current tree. The posterverse bundle landed
without regenerating this table, so check:capability-exemplars (gates:post)
reported that the table disagrees with the manifest. Doc only; no bundle.
documentation/how-to-build-mobile-apps-from-plugins/README.md explains why Deep Research
(doxa-maps-spa, its app preset) is the plugin to wrap first: the seven production maps in one
phone-ready shell, token-free on MapLibre. It then walks the whole path: tools, the wiring check,
one command, what each step does, another plugin as the app, making it your own, and fixes.
The one command needed two small changes; the default app is unchanged:
- internal/set-app-start-page.mjs takes APP_START=<plugin>/<screen>[/<preset>] and opens that page
instead of the UPG-100 list. Unset, it behaves and marks the page exactly as before. A malformed
value, a page missing from the build, or links that would not resolve are refused, as before.
- package.json: phone:from-tree is sync, start page, verify, assemble, install from the committed
tree, with no full rebuild (bun run build refuses without BUILD_ALLOW=1).
phone:deep-research is phone:from-tree with Deep Research as the start page.
how-to.md links the guide, and two of its lines are corrected: webDir is public/dx-maps/, and
apk:debug's first step is the full build, which asks for BUILD_ALLOW=1.
Checked, with no Android tools: the start page written for doxa-maps-spa/doxa-maps-spa/app, then
the app root served and loaded in the shared browser at 390 and 1280. It showed the gallery
element, 15 tabs (the seven maps and the experimental ones) and a drawn map, with 0 console errors
and 0 failed requests. --check passed for APP_START and failed for the default until it was
restored; the default run still writes the UPG-100 page with its original marker. The Gradle and
adb steps are those bun run phone already uses; they were not run here.
The "what can this map do for you?" button can now live IN a map's control
column instead of floating over it. This is the bridge only: nothing changes
until a map adds the control and its gallery tab sets trigger: 'control'.
- map-controls/MapInfoButton.vue (new): a MapControlButton with the info
icon and the localized mapInfo.open label. Clicking it sends a bubbling,
composed 'dxmaps:mapinfo-open' event out of the plugin's shadow root to
the gallery pane. On mount it asks 'dxmaps:mapinfo-control' and shows
ONLY if a MapInfo on the same pane answers (ack), so a standalone leaf or
a partner page never shows a button that opens nothing. A MapInfo that
mounts later broadcasts 'dxmaps:mapinfo-host' and the button asks again.
- info/MapInfo.vue: new prop trigger, 'floating' (default, unchanged) |
'control'. In control mode it listens on its own pane: it acts only on
events whose composedPath contains the pane, and stops them there. Its own
floating button and pointer hide ONLY while the announced control is
MEASURED usable: connected, a non-zero on-screen rect, and in no
inert/aria-hidden subtree. It is re-measured by IntersectionObserver and
ResizeObserver plus a 1.5 s fallback timer, never inferred from an unmount
event. Escape and close return focus to the control when it is on screen,
else to the floating button. aria-expanded is kept on the control.
Proved on a UPG-100 pilot served locally (gallery page, 1440, MapLibre and
Mapbox, en and ar), exactly one info button on screen at every step:
wall 1 floating; Map 1 control (it wins its hit test and sits inside the
toolbar column); opened from the control, Escape closes it with focus back
on the control; toolbar hidden 1 floating, shown again 1 control; tab away
and back 1; back to the wall 1 floating. 0 page errors. That pilot's wiring
is not in this commit.
MapInfo (library/components/info) answers "what can this map do for
you?" with real output the map makes, pre-generated so it opens at once.
The examples are an ordered list of kinds rather than a fixed set,
because maps make different things: an image (a poster), pages (a packet
that shows its first page and expands to every page), a deck (cards that
expand from a stack to a grid) and a table (a real excerpt of the
spreadsheet a map exports, with translatable headers). An example whose
image is missing drops out quietly: no broken-image icon, no empty frame.
The pointer at the button goes for good once that map's popup has been
opened and closed; a checkbox in the popup turns it off on every map.
Both are kept in localStorage behind a guard, so private browsing cannot
break the page. On a phone the popup takes the whole screen; Escape
closes it and focus returns to the button. The pointer's motion stops
under prefers-reduced-motion.
All of its text is one new catalogue area, 'mapInfo', in all eleven
locales: the component's own words, and each map's lead, takeaway and
note as mapInfo.tab.<tabId>.<field>. One area because a bundle ships only
the areas its source names, and the gallery that mounts a popup per map
names none of the maps' own areas. This commit carries the UPG-100
map's two sentences; the other maps' follow.
- check:no-orphan-gates stopped at internal/check-grid-merge-defaults.mjs, which had no script. It
asserts defaults of three grid props that are not on development (the grid merge has not landed),
so it is registered ON DEMAND with that reason, not chained red. Past that first arm, its chain arm
found check:grid-image-loading and check:retry-fetch run by no chain; both depend only on code and
pass in under 300 ms, so they join gates:pre.
- A tracked vite.config.js.timestamp-*.mjs (395 KB, eight absolute paths) failed check:no-abs-paths.
Removed, and ignored at the source: vite writes one beside its config while it bundles it.
- check:library-index: documentation/reference-library-index.md regenerated for retryFetch.js.
- run-parallel printed a failed step's output only with --verbose: the unbraced else bound to the
inner if. Braced; a failing step now prints why.
- The Pages workflow ran bun run build without BUILD_ALLOW=1, so build-lock refused it with exit 75
and the workflow ran no gate at all. A CI runner is the only builder on its machine; it opts in.
A fresh reader given only the README and the HOWTO reached the Studio and a plugin's /__leaf page with
nothing else needed, but one step. After "port 8090 is already in use ... set DX_PB_PORT", neither
doc said the setting is read when bun run dev starts, so the new port only applies after a restart.
The README copies .env.example before starting the server, and the port message appears only once it
runs, so the steps looped with no stated way out. All three places that mention DX_PB_PORT now say to
pick a free port and restart.
A fresh reader given only the README and HOWTO-join-the-table got through install, bun run dev and
the Studio. One gap: no address for a single plugin's page. The Capability 1 fold says each plugin's
folder holds "an index.html for previewing it", which reads as /plugin-designs/<plugin>/index.html,
and that 404s under bun run dev. The page is /__leaf/<plugin>/<profile>, and the Studio's list is
/__plugin-gallery/manifest.json. Both are now named.
Three smaller notes from the same reader:
- the README copied .env.example to .env while the HOWTO uses .env.local. Both work and .env.local
wins; the README now uses .env.local too, which also keeps .env free for compose.yaml;
- the Mapbox token read as required; it is one of two ways to get a background;
- the 8090 port hint (DX_PB_PORT) was only in the HOWTO; it is now named where the README first
mentions PocketBase.
Also, the engine rule (was a follow-up commit, f0e965e4d):
Since 2026-09-28 useMapProvider.js resolves `auto` to MapLibre, and Mapbox is an explicit choice:
"provider": "mapbox" plus a token. The docs still said "Mapbox when a token is available":
- README's map-engines row;
- how-to.md's provider section;
- the plugin-designer skill's props table, which even says to confirm in useMapProvider.js.
All three now state the current rule. So do four lines that told readers to set provider maplibre to
avoid needing a key: README's Get it running and its "No token" fold, .env.example's token comment,
and reference.md's no-key row.
Case-insensitive git grep "Mapbox when a token" over README.md, documentation/ and skills/: 3 before, 0 after.
THIRD-PARTY.md's two open licensing items "need an owner's decision" instead of going to a named person.
needs-eyes describes the browser policy and a maintainer's laptop as the second pair of eyes. The
skill and its glossary say the Experimental section was replaced by capability tiers, with the same
pointers into plugin-studio.vue. grep -w Driver over documentation/ and skills/ is now 0;
check:no-new-driver-references passes.
rega measured dashboard-plus-grid's own leaf at 390px: the page carried
"mobile": "desktop-only" in its profile-config, and the dashboard, the grid
and a MapLibre canvas mounted anyway. The rule lived in ProfileLoader, and
this composite defines its element without ProfileLoader (it bakes its
children in), so nothing on that page read the flag.
- library/composables/useScreenGate.js holds the rule (narrow screen, not
inside the Gallery, dismissed for the session); ProfileLoader uses it,
unchanged in behaviour.
- dashboard-plus-grid calls it: on a narrow screen the desktop-only message
replaces the element, and the dashboard and grid never mount.
- check:mobile-gating (g): every flagged profile's element must mount
through ProfileLoader (an import in its index.js, not a mention in a
comment) or call useScreenGate itself. The pre-fix composite fails it.
A map says how it behaves on a narrow screen once, in its <profile>.meta.json:
"mobile": "desktop-only" (blocked on a phone) or "larger-screens" (opens,
under a dismissible notice). Absent means phone-ready. Until now the rule
lived only in the Research Gallery, as two hard-coded id sets, so a map
opened standalone on its own leaf page on a phone got neither the block nor
the notice.
- profile-meta.mjs knows the vocabulary; generate-tree and the dev manifest
carry the flag into each profile's manifest entry, and generate-tree into
the leaf page's BASE_CONFIG (with the map's name for the desktop-only
sentence).
- ProfileLoader mounts library/components/LargerScreenNotice.vue on a screen
under 640px: for desktop-only the profile is never mounted and a centred
message says why; for larger-screens a slim in-flow bar sits above the map
(it cannot cover the map's controls), dismissible per profile for the
session, in the element's own locale and direction. Inside the Gallery it
stays quiet: every Gallery pane now carries data-doxa-gallery.
- One text: the Gallery's two sentences moved into the library catalogue as
messages.largerScreenNotice / desktopOnlyNotice (+ noticeDismiss), all 11
locales moved as they were, and the Gallery reads them from there.
- The Gallery derives its rules from the manifest's flags. On a phone it
mounts nothing until the manifest has landed: its baked first tab (Big
Picture) would otherwise mount there before the flags say it is
desktop-only.
- dashboard-plus-grid is desktop-only and gospel-internet/board is
larger-screens, the two sets the Gallery held; nothing changes for either.
- check:mobile-gating now asserts the vocabulary, that each flagged leaf page
carries its flag, the Gallery's derivation and its closed-before-manifest
rule, that Big Picture stays desktop-only, the 11 locales, and the
notice's mounting. Its self-test plants one failure per rule.
dx-base measured two defects in the A8 draft for text that goes public:
- Links into the private DX Life repo answer 404 to anyone without an account. The live
example is now plain words (README, DEPLOY), and the clone line points at the public repo
(doxa-open/doxa-maps, as DEPLOY already did). DEPLOY's "three ways" becomes the two routes;
the CDN route is retired.
- The web folder row said 536 files, 76 MiB. The folder is 855 files, 119.2 MiB. New
check:readme-size re-measures public/dx-maps and fails when the row is more than 10% off,
printing the line to paste. It passes at 855/119; the old numbers fail.
The drop-in was proven on a laptop with one server: the three DX Life branches rendered their maps,
7 of 7 checks passed, with internal/sync-dropin.mjs --from-config --dest <doxa-life>/public. This
makes that the default path:
- dropin.dest is ../doxa-life/public. The old ../doxa-life/public/embed/doxa-maps-build was stale.
- bun run dropin = sync-dropin --from-config; deploy:dev (build + drop-in) now uses it.
- The README gains "The easiest path": the studio for the work, dropin.dest at your site's public/,
then build and drop in.
One hazard came with the new destination, and it is closed here. --prune deletes every file in the
destination outside the drop-in's allowlist, which was right while that was a folder of its own. Into
a site's public/, it would delete the site. deploy:dev passed --prune, so it no longer does, and
sync-dropin refuses --prune into any folder named public/. Measured in a scratch site: --prune into
site/public exits 1 with the reason; a plain drop-in copies dx-maps/ and leaves the site's own
file in place.
Railway, DigitalOcean and `docker build` use a root Dockerfile with no setting, and it is
Coolify's and Dokploy's default path. The file moves; nothing is copied.
- VOLUME /data is gone. Railway rejects the line, and a named volume is needed anyway
(VOLUME alone gives each new container a fresh anonymous one). The platforms mount /data.
- The baked PocketBase gate's comment named the old path. It now says the repo-root
Dockerfile, and the gate's SHA-256 pin moves with it (2987055b... -> 2eceef03...). The
hook's code is unchanged: the exact RUN passes `sha256sum -c`, and the old block still
passes its old pin.
- .dockerignore excludes everything: the Dockerfile copies nothing from the build context,
so no platform sends the whole repository to the builder.
- compose.yaml: the same image with its /data volume and memory limit; settings from .env.
- deployment/cloud/README.md gains "Where it runs": Coolify, Dokploy, Railway, a server with
Docker, Kubernetes, and why not DigitalOcean App Platform (no persistent volumes).
DEPLOY.md and two comments follow the move.
gates:post had gone red on development in three places that no landing runs, because landings run
gates:pre. The library index and the capability-exemplars table are derived documents: regenerated,
they now list the country-map bundle, BackToTopButton and useHostedHelp. check:no-free-identifiers
flagged the Wall's new Path2D (a canvas built-in every browser has) as never bound; it knows the
canvas built-ins now. No code changes.
Output 3 showed only the map app as a desktop app. Rudder is the desktop
example the README describes, and it has now been built with
`bun run desktop:build rudder-voice` and run on Linux: a screenshot of the
running app goes under the map app's, with one line on how to build it.
The PNG carries no text chunks (the original recorded its creation time
and timezone); the pixels are unchanged.
The joining page, DEPLOY and the laptop sync default moved to development in the
commit before this one. This finishes the move and closes what a fresh clone's
friend test found.
- landing-preconditions diffs against origin/development when no landing branch
is exported, instead of main.
- CLOUD-DEVELOPMENT-ENVIRONMENT, the plugin-designer skill (SKILL.md, workflow,
glossary) and the shared dev server's notes land on and follow development;
the skill says never to push to main.
- The sync fetches with the laptop's own credentials. git runs with prompts off,
so on a clone with no credential helper it failed every interval with a bare
"fetch-failed / remote: Unauthorized", which read as a broken server. That case
is now its own state, needs-credentials: the log (once, not every tick) and
/api/dev-sync say "dev-sync needs git credentials for <remote>". The joining
page and .env.example say so, and that a fresh clone starts on main and must
switch to development before the sync can fast-forward it.
Measured in scratch repos: the private remote with no helper gives
needs-credentials and one warning over two ticks; a local remote with no
DX_SYNC_BRANCH follows development and fast-forwards; a missing remote stays
fetch-failed.