The panel clipped its content, so at 390x844 the lower controls and the draft
could not be reached. The widget element now scrolls inside the panel, the
panel height follows the dynamic viewport minus the safe-area insets, the
corner host sits inside the safe area, and on a narrow screen Rudder uses less
padding and a shorter draft so the draft stays visible below the controls.
Two gaps found while checking the corner widget (1775f5ac: "Needs eyes: open, HMR, collapse" — all
three verified in Firefox against the running Plugin Studio: opens, keeps the SAME element across a
Studio hot update, collapses).
- LIVE MIC WHEN COLLAPSED: rudder-voice emits `recording` (true/false); the Studio's corner button
turns red with "● " while it listens, so closing the panel never hides that the mic is on.
- DEV BACKENDS SURVIVE VITE RESTARTS: Vite restarts itself when vite.config.js or anything it imports
changes. The backend was stopped on every restart and a new one started at once; the old process
still held the port, the new one died "address already in use", and Rudder showed "engine offline"
until `bun run dev` was restarted by hand (measured twice). Backends now live in a process-wide
registry: reused across restarts, stopped only when the dev process exits, restarted if they crash.
The "engine not running, quietly" answers (89ff2238) are unchanged and read the same registry.
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.
Folding eight en-US call sites onto library/utils/formatCount.js took the matrix from 13 warns to 3,
and that looked like progress. It was not: the rule matches a LITERAL 'en-US', the helper calls
toLocaleString(COUNT_LOCALE), so my eight sites went INVISIBLE while the behaviour was untouched — the
same false-green as a precondition that skips the file under test. The audit had stopped seeing a
decision that is still in force.
The rule now also matches the constant's DEFINITION. One site, attributed to every bundle that imports
it, which is what "held in one place" should read as: 5 warns —
library/utils/formatCount.js:26 against research-dashboard and people-groups-grid
RegionCountryMap.vue:394 regions' held Driver decision
pplr-coverage.vue:128, pplr-pins.vue:60
regions' catch-fallback discrimination is untouched and its self-test stays green (rc=0).
Gate only, no product code, no rebuild.
generate-tree's preload comment now ends "...the map draws with MapLibre and this line must go", but the
gate still looked for "remove this line", so under MapLibre it kept the preload and every arm reported a
Mapbox request that no real MapLibre host makes (rega, 1173a992). It matches either wording now.
Driver, 2026-09-27, in his own page order. Nine pages now, was seven.
1 map the zone's vertical one-pager (unchanged)
2 LIFE CALENDAR 100 years x 52 weeks = 5,200 boxes — a whole human life on one page
3 THE SAME GRID shaded by the evangelical share of these groups' POPULATION
4 funnel now an UPSIDE-DOWN TRIANGLE with an equation sidebar and a "1 of 28" tip
5..n groups 20 per page with photos AND three prayer tick columns
then the 12 prayer points and the prayer wheel (both unchanged)
★ THE 12 PRAYER POINTS WERE NOT LOST. The Driver believed they had gone; all 12 are intact in
packet-strings.json with his rhythms and sub-titles — praise/Two-Cross Gospel, wait/Holy Spirit &
deliverance, confess, read/the Father's love, ask/baptisms, intercede/laborers & shepherds,
pray_word/Persons & Houses of Peace, thank/rest, sing, meditate/communion, listen/accountability,
praise_end/great-greater-greatest — plus the Mark 5 passage and the Metacamp attribution.
★ PAGES 2 AND 3 ARE ONE GRID READ TWICE, which is the whole idea. 5,200 is the Driver's own arithmetic
(100 x 52), not a layout choice, so both pages derive from it and cannot drift into two grids that merely
look alike. A percentage drawn as a bar is an abstraction; drawn on the grid the reader has just
understood as their own life, 32 shaded boxes of 5,200 is the same fact made unavoidable.
★ AND PAGE 3's NUMBER IS POPULATION-WEIGHTED, because he asked for "% of Christian population vs lost".
That matters: by GROUP COUNT 69.1% of the 2,091 matched DOXA groups have no known evangelicals, but by
POPULATION the evangelical share is 0.61% of 209,079,159 people. The group-count version is a materially
milder picture of the same data. Computed in data/people-groups/christian-share.json, joined IMB
CSV ROP3Code+ISOAlpha3 to DOXA's snapshot — the same join the Vision Card uses — and READ here, never
re-derived, so one fact has one source.
⚠ THE BAND MIDPOINT IS AN ASSUMPTION AND THE PAGE SAYS SO. peoplegroups.org publishes EvangelicalLevel
as a BAND ("Less than 2%", "No Known Evangelicals"), never a number, so any percentage requires choosing
a value inside each band. The file records the method, the page prints it, and 0.61 + 99.39 = 100 exactly
— my first formatter rounded the large side to "99%", which loses the asymmetry that IS the point and
leaves a reader checking the arithmetic to find the page wrong.
⚠ GEOMETRY MEASURED, NOT EYEBALLED: square cells would need 26.5px x 100 rows = 2,654px of grid in
1,521px of printable height, overflowing the sheet 1.7x. 13px rows put 100 rows in 1,498px with 23px
spare. Caught by computing the height before rendering.
⚠ AND THE TIP SAID "1 of 27". listCount spread the three cluster keys and omitted `deaf`, whose two
deaf_lists entries buildLegendNodes collapses into one leaf — the same omission allZones() had before
3ef164c0, and this was its last copy. It prints on the page, so a 27 there is a visible wrong fact.
THE GATE IS TAUGHT, NOT TOLERANT: the two new sheets are DETECTED (.sheet-life/.sheet-christian) and
added as known terms, exactly as the funnel was, because that assertion exists to catch silently dropped
people groups and a ±1 tolerance is the slack a lost list page hides in. New assertions: 5,200 cells per
grid, the shaded count neither 0 nor 5,200 (either means the percentage never arrived), and exactly
3 x cards prayer boxes. Probes scoped to .lc-grid — the legend swatches share the cell class and made the
count 5,201 on the first run.
The 17 new strings are English-only and DECLARED_ABSENT with reasons rather than back-filled: English
copied into ten locale blocks would read as translated to this gate and to a reader, and the per-key
fallback already renders English without the lie.
VERIFIED: check-packet-render PASS (9 sheets, map painted, content intact) and
check:packet-strings-parity PASS on the Tibet fixture.
Out: the grid's 'Open in Gospel Resources' opens the selected peoples in
resources-map by its ?groups= contract (DOXA slugs: all 2,107 of the
board's slugs are Gospel Resources' pin slugs; capped at 300). And every
committed selection is dispatched as a bubbling, composed
gospel-internet:selection event with codes (slugs, ISO 639-3, ISO 3166),
the host captured in setup. In: ?groups= arrives selected, ?codes= and
?code= narrow by language, exactly as Gospel Resources reads them.