check:composite-freshness found stale on main: cities-internet
country-posters
doxa-research-map
doxa-simple-map
my-upg-100-list
plugin-studio
rudder-voice. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
A zero-width segment button was still rendered: an invisible tab stop, and the
first child that took the bar's rounded left end. Segments with no width are
now left out.
The dev backend answers /status with the same offline status as /events, and
Rudder reads it on mount and whenever the event stream drops, so a proxy that
buffers the stream no longer leaves it saying "Runs on your computer" on a
server that should run the engine. The local-microphone notice no longer shows
off this computer. The legend progress bar paints its grey band as a rounded
strip and rounds the first and last segment, which the 24px touch targets had
squared off.
The dev backend reports expected=true while an engine the server is set up to
run is down (RUDDER_ENGINE=1 or RUDDER_AUDIO=browser). Rudder then reads "Not
running yet" and says the engine is starting or failed to set up; "Runs on
your computer" remains for a server with no engine at all.
Each segment was a 10px-tall button, under the 24px minimum (WCAG 2.5.8). The
track is now 24px with a centred 10px painted band, the segment buttons clip
their colour to that band, and negative margins keep the legend's layout
unchanged.
generate-tree derives each profile's tier from its composes edges (composing
anything is capability 4; composing a capability-4 plugin is capability 5). An
explicit type is checked against the derivation and a contradiction fails the
build naming both. check:capability-derived asserts every profile is 1-5 and
agrees. documentation/PRESET-PLUGIN-STANDARD.md describes the one
*.instances.json file a preset plugin declares.
check:universal-props showed regions-dashboard LOCALE = warn on a single
site: the map popup's fixed en-US population. That is a HELD decision, not
an oversight -- their own site renders en-US numerals, it is carded, and
_chk-numerals-held-format-does-not-vary-by-locale ASSERTS it does not vary
by locale, so making it locale-derived would break a gate that exists to
keep it fixed.
Clearing it had only two mechanisms and both were wrong. Changing the code
reverses a Driver-backed ruling. A bundle-wide ALLOW silences every FUTURE
en-US anywhere in regions-dashboard -- clearing a column by going blind.
So ALLOW is now site-scopable: a `bundle:RULE:file` key exempts the one file
that earned it, falling back to the old `bundle:RULE` form. The entry records
the card and the reason.
Verified in both directions: the column reads `allowed` instead of `warn`,
and planting a new operative en-US in regions-list.vue puts it straight back
to `warn` naming that file. The exemption is one file wide, not one bundle.
`allowed` is the honest terminal state here, not `ok`. `ok` would mean the
numerals follow the locale, and they deliberately do not.
Every plugin is capability 1 to 5, so the rail files each bundle under its
highest capability again. A bundle not yet in production says Experimental on
its own rows. check:studio-rail now asserts there is no section outside 1-5.
The Experimental section is a stage, not a tier, so its heading showed CAP 0
and the Research Gallery (capability 5) looked like a capability-0 plugin. The
heading now reads EXP and each of its rows names its own capability. Clicking a
plugin opens it without unfolding its folder or its parts; only the twisty
folds and unfolds, and a closed folder that stands for the plugin stays closed.
With two domains on one container, the engine answered the second one 403 "forbidden host", so Rudder
showed offline there. The allowed list now defaults to the hosts Coolify gives the container
(COOLIFY_FQDN), then the OIDC redirect host; always an explicit list, never a wildcard.
The pull loop rebuilt the engine when its source changed, but the old process kept running until the
next container start. Now, when the engine reports it is not in use, the old process is stopped by its
own PID and the dev server starts the new binary.
The streaming model holds back the last fraction of a second until more audio
or a pause arrives, and the commit alone did not flush it, so a Stop
mid-sentence lost its final words. The engine now sends 1.2 s of silence before
the commit, and keeps reading after the commit until the final text or 6 s of
quiet. Measured: a stream cut at 6 s of the test clip now ends "...ask not what
your" instead of "...ask not".
Rudder's setup ran before Vite and the sign-in proxy, so the whole site answered 502 for minutes on every
start while the engine built and ~742 MB downloaded; on the first start the download was reset mid-file and
Rudder stayed offline. Now the runtime and pinned model are baked into an early image layer (SHA-256
checked, resumed and retried on a stall), setup runs in the background, /api/rudder answers "starting"
meanwhile, and the dev server restarts once when setup ends so the engine starts.
check:composite-freshness found stale on main: none; also rebuilt: rudder-voice plugin-studio. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The engine's local device list does not apply when the page's own microphone is
used, so a server without pactl no longer shows its error above the draft.
On a loaded server the last words can land seconds after Stop, and the widget
read "Stop" until they did. Stop now releases the mic and returns to idle at
once while the session's socket stays open for the tail. Each engine session
sends its events to its own reader, so a session still draining never writes
into the next one, and Record again simply lets go of the old tail.
Under bun, process.execPath is bun, and bun --check runs the script instead of
parsing it; a cloud build hung inside check-link-rot-sample.mjs. The gate now
uses node under bun and fails plainly if node is missing.
Collapsed, the pill reads "Rudder · voice to text". While recording it drops the
label for a red dot, a mic that lights only while speech is detected, and the
last few words on one truncated line, so a closed panel still shows the reader
is being heard. Rudder reports this through an `activity` event. Open, the
panel no longer scrolls: the draft fills it and scrolls on its own, stays
editable while recording, and notices sit above it; with settings or info
open the panel scrolls so they stay reachable.
With RUDDER_AUDIO=browser the engine records no local microphone. The page
captures the reader's mic, converts it to 16 kHz mono PCM in an AudioWorklet,
and streams it over a WebSocket at /api/rudder/stream into the same speech
session a local recorder feeds. The words come back on that socket to that
reader only, never on the shared event stream. One session at a time; a second
reader is told the engine is busy. The engine honours RUDDER_HOME, accepts the
public host named in RUDDER_ALLOW_HOST, and refuses a socket from another
origin. The dev proxy now carries WebSockets to backends.
The first build in the container hung for 18+ minutes: the image had only bun's node shim, and
`node --check` under that shim runs each script instead of parsing it. The gates also already
passed before a commit reaches main, so the container now runs clean, token, bundles, tree and
check:sri only. The public exclusions and the refuse-the-swap check still run on every build.
runtime-config.js was written with python3, which the image does not have; bun writes it now.
The build's PID goes to /tmp/build-cdn.pid so it can be stopped without touching the container.
The engine now refuses non-loopback Host headers and Vite's proxy passes the public host through, so
without this every /api/rudder call is 403 (studio, RUDSRV-STU2). entry.sh derives it from
OIDC_REDIRECT_URL when unset; the README documents it and the CPU need (it ran ~1.5x behind real time
on a loaded box).
Go 1.22 in the image; on start, backend/rudder-engine/setup-runtime.sh fetches its pinned runtime and
model (SHA-256 checked, skipped when present) into RUDDER_HOME (default /data/rudder, meant for the
persistent volume) and builds the engine; RUDDER_AUDIO=browser. The pull loop rebuilds the untracked
engine binary when backend/rudder-engine changes. /api/rudder stays behind the sign-in: it is served
by Vite on the gated / route, and the only public routes remain /cdn/ and /api-prod/api/. README:
the flag, and 3.5 GB with it on.
Bundle integrity: all 25 bundles carry bundles[].integrity (sha384) and every profiles[].embed.script
already includes integrity + crossorigin=anonymous (checked on main); the section shows the snippet,
what the browser does with it, the CORS header it needs, and check:sri. The container's PocketBase:
locked-by-default collections, the prod dashboard blocked and the dev one admin-only, and hooks OFF,
now true in code: both instances get --hooksDir pointing at a path that never exists, so a pb_hooks/
folder added to the repo can't run. Per-plugin locks and isolated edge functions are stated as not
built.
check:composite-freshness found stale on main: none; also rebuilt: rudder-voice. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The engine records the microphone of the machine it runs on, so a cloud dev
server has none by design. Off loopback (and not the desktop app), an absent
engine now reads "Runs on your computer" with where to open Rudder instead,
rather than build instructions for a missing binary.
deployment/cloud: PocketBase (pinned, sha256-checked) as dev (/pb/, signed in; its /_/ dashboard for
OIDC_ADMIN_GROUPS only) and prod (/api-prod/api/ public, dashboard never exposed), with the schema
as migrations in backend/pocketbase/pb_migrations and data on the /data volume; superuser from env.
admin.mjs routes /cdn/, /api-prod/, /pb/ and /admin/ behind oauth2-proxy (public: /cdn/ and
/api-prod/api/ only). build-cdn.sh: flock (409 on a second build), a snapshot of the pulled commit,
the public exclusions applied BEFORE the build, a heap-capped build, a check of the output that
REFUSES the swap if anything excluded remains (proved with planted fixtures: a field-access file,
'Visa Requirements' content, and key-cities one level down each fail; clean passes), then an atomic
swap; runtime-config.js carries the public API URL. public-exclusions.txt is the ONE list the
giveaway export and the CDN build share. Any git host via REPO_URL/BRANCH/GIT_TOKEN.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-internet
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups; also rebuilt: rudder-voice plugin-studio. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
The Scripture chip reads gospel.charts.newTestament and fullBible, so the two
gospel.res copies had no reader. In Arabic the count now follows a label
("number of parts: {n}"), which is correct for every n.
Collapsed, the corner is a round bubble that turns red, pulses and shows a dot
while Rudder listens. Open, it is a 360px panel (a 60vh bottom sheet on a phone)
with a one-line header, the draft as the main element, and record and copy under
it; language, microphone, clear and debug sit behind a settings button.
Collapsing only hides the panel, so a recording keeps going and the draft stays.
c18d4ffb says "the built bundle no longer contains the inline English". One of those eleven
strings IS still in the bundle: "The {engine} map engine did not load." is the en CATALOGUE's
value for messages.engineMissing, which every bundle compiles in. What changed is that the
SOURCE no longer carries a copy of it — that was the point of the commit, and the sentence
overstated it.
An empty commit rather than an amend, because the commit is already on the remote branch and
--force is not available here. A wrong sentence in the record is what the next reader trusts, so
it is corrected in the record rather than left for them to disprove.
tOr() rendered English while a key was in flight. All eleven keys it guarded are on main, so it
is gone and every call site reads the catalogue: nParts, engineMissing, sheet.loading,
guide.wayInAll/wayInSome/wayInWhat, legend.hnrTip, guide.howNumbersMade, guide.isSignLanguage,
sheet.linkCopied, sheet.share.
I compared each default against the catalogue before deleting it, rather than assuming they
matched — deleting a fallback silently changes what a reader sees. Ten were identical. The
eleventh was not: gospel.sheet.share held my "Share this card" against the catalogue's "Share".
The catalogue wins, because it is translated in eleven locales where my phrase was not and a
button inside a card need not restate its container, so THE SHARE BUTTON NOW READS "Share" —
a visible change, deliberate, and the shorter label suits the 390px screen where those tap
targets were just fixed.
The four parameterised calls needed doing by hand: my regex for tOr(key, english, params)
stopped at the first ")" inside fmt(...), so it silently converted seven of eleven and left
four. Counted the remainder instead of trusting the count it printed.
gates:pre 11/11, ENGLISH-LEAKS PASS, and the built bundle no longer contains the inline English.
check:composite-freshness found stale on main: all-people-groups
cities-internet
country-posters
dashboard-plus-grid
demo-map
doxa-research-map
doxa-simple-map
gospel-internet
gospel-resources
key-cities
my-upg-100-list
pplr-data-maps
regions-dashboard
research-dashboard
template-bundle
upg-neighbor
wall-of-people-groups. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
check:composite-freshness found stale on main: none; also rebuilt: rudder-voice plugin-studio. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
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.
rega: the Scripture scope stayed English under ar, because gospel.res.newTestament and
gospel.res.fullBible exist in NO locale — while gospel.charts.newTestament and
gospel.charts.fullBible have existed, translated, in all eleven all along. Every locale was
falling back to my English defaults, and even en read my lowercase "full Bible" against the
catalogue's "Full Bible".
I asked studio for three new keys without checking whether the words were already owned. Two of
them were. The chip now reads the existing pair — verified in the built bundle: the Arabic
"العهد الجديد" and "الكتاب المقدس كاملاً" are both in it — and only the part count still needs a
key of its own, gospel.res.nParts with {n}, which I have narrowed studio's request to.
Two keys for one word is exactly the drift I have spent this flight fixing in other people's
code. The namespace reads oddly for a map chip, and that is the smaller problem.
Vibe coders get their own subsection at the top: one repo with many plugins (own src/, one
shared library), the Plugin Studio live with bun run dev, Rudder as the offline voice-to-text
corner widget in the Studio (Linux today), one plugin to embed or mobile with Rudder as the
desktop-target example (packaging next, as its own README says), and 'Run it in the cloud'. Only
what is on main. Acknowledgements thank the open-source projects it stands on (Vue, Vite, MapLibre,
Pinia, Vue I18n, Capacitor, Tauri, oauth2-proxy, NeMo-Speech.cpp + Nemotron, OpenStreetMap via
OpenFreeMap); no personal names. public-hygiene clean.
dx-shepherd caught what I had just shipped: "full Bible", "New Testament" and "N parts" were
being generated in the BUILDER, so English was baked into data that no locale can translate and
no catalogue owns. The same mistake I have been fixing in other people's surfaces all day, made
one commit earlier by me.
The builder now ships FCBH's part count and nothing else — 27 on 400 codes, 66 on 41, another
count on 82, none on 367 — and the leaf names it through tOr(), so it localises the moment
studio adds three keys: gospel.res.newTestament, gospel.res.fullBible and gospel.res.nParts
with {n}. English inline until then, as always.
A count that is neither 27 nor 66 is still reported as itself and never rounded up to a
Testament, and no count still names only the source.
The Scripture chip named its source ("Faith Comes By Hearing") and stopped there, for 887
codes. FCBH publishes its own verse scope in the same record — au.vsc, "N parts, FIRST to
LAST" — and 567 of those 766 carry one: 400 New Testaments, 41 full Bibles, the rest partial.
A catalyst standing with a group needs to know whether that link is a whole Bible or two
books, and the data said so all along.
The chip now reads "Scripture · checked (New Testament — Faith Comes By Hearing)". 27 and 66
are named as Testament and Bible; anything else reports the part count FCBH claims and is never
rounded up. 367 sx codes still carry no scope — Scripture Earth's 104 and Door43's 17 publish
none in this record, and 47 FCBH rows omit it — and those say only the source, which is the
honest thing to say when the scope is unknown.
Found because go-int asked whether the FCBH text is a New Testament. It is a fair question to
ask of a map that had the answer in a field it never rendered. Their tree now reads the same
field and their green count went 128 to 195 across the roster.
Still FCBH's claim, cited, not counted book by book here — the same standard as the rest of
this corpus, and it is on the chip rather than buried in a builder.
A ?tree= link applied its node as soon as the tree file loaded; if that was
before the rows, the node's on-board languages were the empty set and the
grid restored to 0 (rega F1). The node now applies once both have loaded.
The New Testament rule also reads FCBH's own verse scope (people-group-pins
codes[iso].au.vsc, 27 or 66 parts), gospel-plugin's pointer: green 128 ->
195. Kyrgyz (33 parts) and three languages with no scope stay yellow.
A family click wrote ?tree=<id> AND its whole language list (hundreds of
codes for a big family), a link that breaks in chat apps and SMS. The
board's writer now runs after the kit's and removes 'language' while a tree
node is active; ?tree= alone restores the node and its languages.
The access sub-ladder follows the corrected pools (read and watch 869 ->
1,129, watch or hear cannot read 806 -> 518, read 57 -> 85). Nothing found
(369), own/bridge/zero and the band headline are unchanged.
1 THE CHIPS SAID "unchecked" ON VERIFIED LINKS. Card mode never fetches the verification table,
so every chip understated the evidence behind it — caught on central-sama, whose Scripture and
film are both verified. Each shard now carries the states for its OWN urls, keyed by url exactly
as linkVerify is, so vchip() and vtitle() work unchanged. Shards grew 8.0 KB to 14.0 KB mean,
29 KB worst; against 6,051 KB for the map that is a price worth paying for not lying about
evidence.
2 MY GATE'S FALSE NEGATIVE, which cost rega a round: it asserted
text.includes(slug.split('-')[0]) — "abai" against a rendered "Abai Sungai". A slug fragment is
not a name. It compares the roster's name, case-folded, anywhere in the card.
3 THE EMPTY GREY BAND. The sheet is a bottom panel capped at 62% of the map; in card mode there
is no map under it, so a phone showed 285px of grey above it. Full height in card mode, and the
border radius goes with it.
4 THE CLOSE BUTTON with nothing behind it — hidden in card mode, both instances of it.
gates:pre 11/11, LINK-COVERAGE 12,292 unchanged, SFC-PARSE PASS. rega spot-checks after landing
per dx-shepherd; the render claim is still not mine to make.