check:composite-freshness found stale on main: gospel-internet. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
rega found ?card= throwing "Cannot read properties of null (reading 'country')" at the back
button. Card mode had no `selected` — the country — and the sheet reads selected.* in
FOURTEEN places, so guarding the one line rega pointed at would have left thirteen. The card
already knows its country, so `selected` is populated from it and every existing read is
satisfied; the curated-analysis block stays hidden on its own hasCurated test because a card
carries none of those fields. The back button is also hidden in card mode: it would have
opened a country sheet with no pins behind it.
The lesson is mine and worth the line. I shipped this having measured the shards, the byte
saving, the addressing and the card contents — 400 slugs resolving, a tier-0 and a tier-2
card correct — and reported it as done. Every one of those checks passed on a feature that
rendered nothing, because I cannot launch a browser and did not say that the render was
therefore unverified. Data being right is not the feature working.
So the gate grows the arm that would have caught it: a headless mount of ?card=<slug> from
the roster, asserting the page renders more than 400 characters and names the group, plus an
unknown slug that must still render. rega runs it (browser gates are rega's), which is
exactly why it belongs in the gate rather than in my own ad-hoc runs.
BUILD-FIELD-RESOURCE-CARD. For the catalyst standing with the group on a phone. Seeing ONE
group used to cost the engine, the world boundaries, the basemap tiles and the 2.3 MB pin
index before a single link appeared.
?card=<slug> renders the group's sheet alone — no engine, no boundaries, no tiles, no pin
index. Measured from the served files: the map transfers 7,942 KB plus a ~700 KB engine and
unbounded tiles; the card transfers 678 KB worst case, of which 661 KB is the bundle itself
and 17 KB the largest shard. 92% fewer bytes before tiles are counted at all. The bundle is
now the floor, and splitting it is a separate job I have not done.
SHARDED BY A STABLE HASH, after two measured failures: first two letters of the slug gave a
120 KB worst shard because every "Deaf <people>" lands in "de"; three letters left 110 KB
because those slugs all begin "dea". djb2 low byte gives 255 even shards, mean 9 KB, max
17 KB, and the leaf recomputes the key so there is no index and one request. The identical
function lives in both files: if either drifts the card 404s rather than serving somebody
else's group.
Flat filenames, card-<key>.json, because internal/generate-tree.mjs stages tables/*.json as
files and throws EISDIR on a subdirectory — shared build machinery, not mine to change for
one plugin.
Share uses navigator.share where it exists (it offers whatever the catalyst already has) and
falls back to the clipboard, with a cancelled share treated as the non-event it is. The link
carries doxa.life's own slug, so what arrives reopens this card rather than a search — the
slug now reaches the sheet from a tapped pin too, so sharing works from the map as well.
Honesty unchanged: 1,738 cards carry something in the group's own language, 369 carry
nothing and say so with the way in. Checked 400 roster rows resolve to a card, 0 missing;
a tier-0 and a tier-2 card both carry what they should.
For an AI or Bible-translation team choosing where a first New Testament
draft is feasible: per language, five named open signals, each cited and
licensed: Glottolog's most extensive description (CC BY 4.0), Scripture
books found (eBible), a language Wikipedia (CC BY-SA 4.0), Common Voice
validated speech hours (CC0) and FLORES+ (CC BY-SA 4.0). A dated snapshot
the build reads; no live fetch. The board shows 'Open data (of 5)', a
sortable column (with the signals in the CSV), a drawer section with each
signal and its source, and a line on the tree card. It is a count of named
facts, not a score. Codes that could not be mapped are listed (6 Wikipedia
codes), and the 31 languages Glottolog lacks read as unknown, never none.
Of the 736 languages with no New Testament found: 20 have 3+ signals.
Each row names only features on main: Gospel Resources' views and data tables, Cities with
Internet and the Gospel x Internet board (incl. internet-by-adm1), Big Picture / Simple Research
filters with Export CSV, Export this selection and Full Profile, UPG-100's Prayer List, Poster Map
and Card Deck. The agents block lists manifest.json, the public tables and list files, the two
PUBLIC pray.doxa.life endpoints the maps already call (no keyed endpoint), and the ?lang /
?provider / ?list / ?tk switches. check:public-hygiene clean.
check:composite-freshness found stale on main: cities-internet
gospel-resources
key-cities. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
unicode-bidi:plaintext (row 86) also resolves a cell's alignment from its own text, so English cells
left-aligned inside an rtl table, and at its left edge 'Mandarin' clipped and 'Spanish' rows looked blank
(rega, L20 at ee1cbc05). Cells in an rtl table now align right; the per-cell bidi isolation stays.
check:composite-freshness found stale on main: cities-internet
gospel-resources
key-cities. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
dx-shepherd (R500): with no rudder-engine process the dev proxy returned 500 ECONNREFUSED for
/api/rudder/{devices,events}, which showed as console errors. While a backend is down, backendDevPlugin now
answers its routes itself: /events streams one status event (engine offline, 'The desktop engine is not
running.'), which Rudder renders as its own state, and other routes return an empty JSON list. Running
engines are proxied as before.
rega row 86 (gospel tables at ?lang=ar; every table renders DataTable.vue): English and mixed cells flipped
under the page's rtl ('(Open Bible Stories (Door43-Catalog'), and the English hint tail and the meta caveats
rendered with their full stop leading. Cells take unicode-bidi:plaintext (Tabulator generates them, so CSS is
the lever); the English hint tail and each caveat are isolated in <bdi dir="ltr">. From gospel-plugin.
cap5-plugin-studio (Rudder's button now reads its label), prod-gospel-resources and its phone
shot. Derived from check:shots on this tree, tokenless; check:shots passes.
check:composite-freshness found stale on main: plugin-studio. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The Plugin Studio's corner button read "rudder-voice" (rega, repo: it showed in the README retake). A widget
button now reads studio catalogue key widget:<row key>, falling back to the row label; widget:rudder-voice/
rudder-voice is "Rudder — voice to text" in all 11 locales (Rudder kept as the product name).
rega (7a7ca67b): on the dev server the corner widget never mounted, because /__plugin-gallery/manifest.json
built its profiles without the .meta.json fields only generate-tree read. Both now read them through one
module, internal/profile-meta.mjs (stage, heavy, widget per profile; bundle stage = production if any
profile says so), so the dev Studio and the built Studio cannot disagree. Verified: the dev model lists the
same 7 production bundles and rudder-voice:bottom-right; the built manifest is unchanged.
rega measured it at ?lang=ar: ".of 5,067 languages have…" and ".both key-gated" — English
runs inside an rtl block, so the browser moves their leading and trailing punctuation to
the wrong end. Thirty of them across four leaves, found by walking the template and
flagging every static English run with no dir anywhere in its ancestry.
Each run is wrapped in <bdi dir="ltr"> rather than giving the block a direction: these
paragraphs mix English prose with translated labels and numbers, and a dir on the
container would left-align the Arabic beside it. bdi isolates exactly the run and nothing
else.
The first pass broke the build and the second caught it: my tag regex ended a tag at the
first ">", so v-if="matches.length > 8" looked like text and the wrapper landed INSIDE the
attribute — v-if="matches.length > <bdi dir="ltr">8". check:sfc-parse failed, I patched
four sites by hand, and that left orphaned </bdi> closes. Reverting all four files and
redoing it with a quote-aware tokenizer was the cheaper fix than repairing my own repair:
30 runs, every open matched, 0 wrappers inside an attribute, SFC-PARSE and gates:pre 11/11
green.
Needs eyes: this is a bidi rendering fix and only a screenshot can confirm the punctuation
sits right. rega has the case at l17-7a7ca67b.
A neighbor dot showed a pointer and did nothing on click. Clicking a dot now picks
and scrolls to its card; clicking a card (or Enter on it) flies to the dot and rings
it, so the map and the brief agree on which neighbor you are looking at.
check:composite-freshness found stale on main: country-posters
gospel-internet
gospel-resources
key-cities
my-upg-100-list
plugin-studio
regions-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
rega measured the Arabic packet (row 83) and found page 8 still English. dx-shepherd's rule, which this
follows exactly: NEVER machine-translate Scripture — use a published public-domain translation per locale,
cited with its name (END-DXS-MK5GO).
ar Arabic Van Dyck Bible ru Russian Synodal Bible es Reina Valera 1909
de German Luther Bible 1912 fr French Ostervald Bible
Fetched from bible.helloao.org, which ships licenceUrl + website + sha256 per translation and points at
ebible.org's own licence pages. Full provenance — translation id, name, licence URL, sha256 — is recorded in
data/people-groups/scripture-sources.json, because a credit line on the page can carry a NAME but not a
verifiable source, and the two are different claims.
★ VERSE NUMBERING VERIFIED AGAINST THE ENGLISH, not assumed. All five extract exactly verses 1-20 of 43 in
the chapter; verse 1 matches the WEB's "they came to the other side of the sea, into the country of the
Gadarenes" and verse 20 ends at "Decapolis ... everyone marveled". Lengths land within 1,689-2,399 chars
against the English 2,141, which is the spread you would expect across languages rather than a truncation.
The English passage carries no verse numbers, so these are joined as continuous prose the same way.
⚠ AND I DID NOT WRITE ANY OF IT FROM MEMORY. Scripture reproduced from recall is machine translation with
extra confidence — the same failure the rule forbids, one step removed. Every character came from the API
and was checked against the English before storing.
hi/zh/pt/it/ro STAY ENGLISH and stay in DECLARED_ABSENT, which is now narrowed rather than deleted: PD
candidates exist (Hindi 1851 Serampore-lineage, Chinese Union Version 1919, Almeida 1819, Diodati 1649,
Cornilescu 1924) but their licence status varies by jurisdiction and I could not confirm it. The entry
shrinks one locale at a time as each is cited.
VERIFIED: check:packet-strings-parity PASS, and the ar and de packets both render 9 sheets PASS with the
passage and its credit on page 8.
rega, row 82 (gospel-internet board at ?lang=ar; gospel-resources charts share the component): under an
inherited rtl the SVG's text-anchor start flipped, so category labels ran off the left edge and value
labels drew back over their bars ('869' cut to '9'). The chart's SVG is direction:ltr, since its geometry
is left-to-right by construction; each label still shapes as Arabic. In every locale the value column was a
fixed 74px, which clipped '10,160,000' in a 335px card; it now fits the longest value label (never under
74px). Needs eyes: row 82 in ar and en.
BUILD-CORNER-WIDGET-CLASS-RUDDER-IN-PLUGIN-STUDIO (design approved by dx-shepherd). A profile's .meta.json
declares "widget": { "corner": "bottom-right" | ... }; generate-tree carries it into manifest.json, so the
Studio keeps no list. The Studio mounts each widget once in a fixed corner host (z-index above everything),
outside the rail and the pane, collapsed to a button, with its open state remembered. Picking it in the rail
opens the corner instead of mounting a second copy in a pane. The host and the mounted element live in a
page-level singleton on document.body, so a Studio hot update or remount re-attaches the same element and
Rudder keeps its live stream. rudder-voice declares bottom-right. Needs eyes: open, HMR, collapse.
check:composite-freshness found stale on main: all-people-groups
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
pplr-data-maps
prayer-walk-planner
regions-dashboard
research-dashboard
template-bundle
upg-neighbor. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
With provider=mapbox explicit and no token, useMapInstance logged 'No Mapbox access token provided' as
console.error once per map (7x on pplr), while the UI already says "Map unavailable". It is now a single
console.warn per page, so production consoles stay at 0 errors (dx-shepherd, TKWARN).
Its nowrap placeholder overflowed the bounded selection side and ran under
'Try a combination' at 1280 and 1440 (rega r82). The side now clips and the
placeholder ellipsizes, with its full text as the tooltip.
check:composite-freshness found stale on main: rudder-voice. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
rudder-voice's connect() called new window.EventSource unconditionally from its mounted hook, so a headless
mount (and any host without EventSource) threw 'window.EventSource is not a constructor' and failed
check:profiles-mount in en and ar for every lane (go-int). It now shows the engine-not-running state.
profiles-mount: 140 mounted clean.
check:composite-freshness found stale on main: gospel-internet; also rebuilt: rudder-voice. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
Rudder is the first plugin that isn't a map: it shows that one plugin-designs/ folder, developed live
in the Plugin Studio, can ship as a desktop app with its own local backend.
- plugin-designs/rudder-voice: <rudder-voice> element. Words stream into the end of a text area while
you type/paste anywhere (typing holds the stream; nothing duplicated or lost). Mic picker with
hot-plug, 3-second mic test, plain-language errors, info panel, privacy-safe debug report.
meta.json declares "targets": ["desktop"].
- backend/rudder-engine (Go): supervises NVIDIA NeMo-Speech.cpp (ggml, CPU) with the Nemotron 3.5 ASR
Streaming GGUF over a loopback WebSocket; records with parec/pw-record only while recording; recovers
dead Bluetooth mic links (reopen -> CVSD voice profile -> optional RUDDER_BT_RESET) and starts
headsets on CVSD. No telemetry: traced with strace, zero sockets/DNS while transcribing.
- Privacy: log lines redact MAC addresses and the home folder, transcripts are never logged, Host check
against DNS rebinding, CORS only for the desktop webview, random API key for the speech server.
- internal/backend-dev-plugin.mjs + vite.config.js: `bun run dev` starts built plugin backends and
proxies /api/<name> same-origin, so a plugin calls the same API in the studio and on desktop.
- backend/rudder-engine/setup-runtime.sh: fetches the runtime and model pinned by SHA-256.
Not yet: the Tauri desktop build (sidecar) and the PocketBase word-correction table.
The family names were typed in English. They now come from the shared religion
catalogue (religionFamilyLabel), in the legend, its list and the ready line; the keys
and the map filter stay English.
The seven map views read the shared messages.mapUnavailable through usePplrT, which
prefixes every key with pplr., so they rendered the raw key. They now read it with
tStatic, which takes a qualified key and follows the page locale.
deployment/cloud/: a bootstrap Dockerfile (Bun, git, oauth2-proxy v7.15.3) that clones the repo
and runs deployment/cloud/entry.sh from it, so the recipe updates with git; the Vite overlay
(host, allowed hosts, HMR over wss); and the pull loop that follows the branch. With no OIDC env it
serves Vite directly (laptop default unchanged). With OIDC_ISSUER set, oauth2-proxy owns the port
and Vite listens on localhost behind it: Studio, maps and the HMR websocket all sit behind one
sign-in, with PKCE S256, a confidential client and a REQUIRED group allow-list (it refuses to
start without one). Secrets reach oauth2-proxy through its environment only. README gets a short
'Run it in the cloud' section linking the recipe.
upg100's 6b1f0ba4 gave the zoning gate a committed fixture (it now measures on a clean tree, exit 0) and
dropped the last reader of upg100WallIntro, so both land: the gate beside check:zone-census, and the
markup-carrying key deleted from all 11 catalogues (its three plain parts replace it).
The gate caught what I shipped: rendering the Tibet packet at PACKET_LANG=ar failed with "the funnel page
carries Latin-script prose in a ar packet: 'This packet is', 'UPG-', 'lists … How each number is reached'".
Exactly right — my new strings were English-only, and two of them PRINT on a page whose siblings
(funnel_title, funnel_intro, funnel_people) are fully translated. English text on an otherwise Arabic page
is not a graceful fallback, it is a half-translated page.
So all 17 new keys are now genuinely translated across es fr pt de it ro ru ar hi zh — 150 values — and
DECLARED_ABSENT is back to its original two entries (the Mark 5 passage and its credit). The declaration
mechanism was the right call while they were untranslated and the wrong place to leave them.
⚠ 'UPG-' IS ALLOWLISTED, AND THE ORDER MATTERS. It is the Latin run left by "UPG-100", the product name every
locale keeps verbatim — the same reason 'Project' is already in LATIN_OK. But it is allowed only AFTER the
real translations landed: an allowlist entry added when the failure first appeared would have silenced the
gate on fifteen genuinely untranslated strings. The allowlist is for names, not for work not yet done.
VERIFIED at the rendered layer in BOTH locales: en and ar each PASS with 9 sheets, map painted, content
intact. The Arabic packet carries its life-calendar title, christian-share title, both legend labels, the
three prayer columns and the "1 of 28" tip in Arabic, with 2 grids and 32 shaded cells — the same numbers as
the English one, which is the point: the arithmetic is language-independent and only the words change.
check:packet-strings-parity PASS.