check:composite-freshness found stale on main: country-posters
doxa-maps-spa
key-cities
my-upg-100-list
plugin-studio
pplr-data-maps
regions-dashboard. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
doxa-maps-spa.js carried "internal role noun (Driver)" twice. It was not the
SPA's own file: the text is a CSS comment inside VerticalTabsLayout's style
literal, so it is string content and ships to every bundle using that layout.
Rewritten in BOTH synced copies (lines 355 and 844) to state the decision
rather than who made it. check:synced-css passes, so the copies stayed
identical. Verified against output: doxa-maps-spa.js goes 2 occurrences to 0.
Located by matching the SHIPPED text back to source, after two wrong guesses
worth recording. A backtick-parity test over a whole .vue file said all 25 of
that file's "Driver" mentions were outside a template literal, while 2
demonstrably shipped -- the crude lexer again. And the leak was not in
doxa-maps-spa.vue at all, which is where the bundle name pointed.
check:public-hygiene flags "internal role noun (Driver) — process vocabulary"
inside shipped bundles. Three of those reach regions-dashboard.js, and they
are not from the regions components: they come from the shared poster and
packet code, which every bundle using PosterWall carries.
The worst is not a comment at all. packet-strings.json's note_on_scripture is
a shipped STRING, so it is data a reader can read. The other two are a JS
comment and a CSS comment that both live INSIDE template literals, which
makes them string content rather than comments -- the minifier cannot strip
what is not a comment, and the shadow-CSS stripper does not reach a print
stylesheet.
Rewritten to say what the decision was rather than who made it. Three lines,
no behaviour, no reformatting. regions-dashboard.js goes from 3 occurrences
to 0 after a rebuild.
This is the white-label requirement, and it matters most for the artifact we
intend to hand over: a recipient reading our bundles should not meet our
internal process vocabulary.
The cloud container pulls the repo live; a pb_hooks/ folder or *.pb.js pushed by any writer would be
server-side JS on the team backend. The container never runs repo hooks (the one sign-in hook is baked
into the image), and this gate keeps a hooks folder from appearing at all. Self-tests on every run.
Two status lines formatted their counts with an inline en-US; they now call
formatCount, the one place counts are formatted (fixed en-US while numerals are
held for the Driver), so the only remaining LOC-ENUS site is the library's own.
check:universal-props showed LOCALE 'warn' on research-dashboard and people-groups-grid, both inside Big
Picture. Neither bundle has an en-US of its own any more — the fold put every count through
formatCount(), so both warns cited the SAME line, library/utils/formatCount.js:26, COUNT_LOCALE = 'en-US'.
That line is not a bundle ignoring the reader; it is the held decision written down. NUMERALS ARE HELD —
counts do not follow the locale — so 'ok' is not available here without breaking the policy and the gate
that asserts it. This allows it the way regions-dashboard's popup is allowed, at the SITE.
The site is shared, which is the one thing regions' precedent did not cover. A `bundle:RULE:file` key
would need one entry per importer, so the LOCALE column would warn afresh every time a bundle adopted the
shared helper — the column punishing the fold that made the policy auditable in one line instead of eight
call sites. A `*:RULE:file` key is honoured for every bundle that compiles the file, and ONLY for a path
under library/. That restriction is the whole guard: a bundle's own source can never be wildcarded, so
this cannot be used to clear a column broadly.
Measured on main: 4 LOC-ENUS warns -> 2, both remaining pplr-data-maps' and not mine. research-dashboard
and people-groups-grid read 'allowed', regions-dashboard unchanged.
Negative-tested in both directions, because an exemption that is wider than it reads is the failure here:
a planted toLocaleString('en-US') inside research-dashboard's own composable warns again, and a planted
one in library/composables/useDashboardData.js — shared, imported, but not the allowed file — warns too.
A self-test now also refuses a `*:` entry outside library/, which the lookup would silently never honour.
The capability gate reported five composes refs as "resolve to no known bundle/profile" —
bp-research-header, cities-internet-map, gospel-resources-map, my-upg-100-list-map,
research-dashboard-map. All five were correct data. `composes` accepts a third form besides
'<bundle>/<profile>' and a bare '<bundle>': a registered ELEMENT TAG. Nothing needed renaming and no
author's meta.json was wrong; the resolver was short. 5 unresolved -> 0.
Two things the element form forced, both measured:
· A SELF-REFERENCE is an internal child, not a nested plugin. big-picture composes
bp-research-header, its own element, which resolved back to big-picture (cap-4) and promoted the
bundle to cap-5. Excluding self keeps "nests other plugins" from meaning "has parts".
· A name covering several profiles resolves to the HIGHEST tier among them, and both non-slashed
forms now resolve the same way. A bare bundle name and that bundle's element tag both mean "this
bundle", so taking the first profile for one and the highest for the other made the answer depend
on discovery order. gospel-resources shows it: four single leaves and one cap-4 composite in one
bundle, and losing the 4 silently demotes whoever composes it.
The rule now lives in internal/_composed-tiers.mjs, imported by the generator that WRITES the tier and
the gate that CHECKS it. It was duplicated, and a rule this fiddly does not stay identical in two files;
the drift would have shown up as a gate passing a manifest the generator would refuse to write. The
data stays independent — the gate rebuilds the edges from the <profile>.meta.json files on disk and
checks the published manifest against them.
With the count at 0, an unresolved ref FAILS in both. While five were outstanding a warning was right:
they were correct data the resolver could not read, and failing would have blamed five authors for a
gap in the tooling. At zero, a typo and a genuine plain child still look identical from here, so the
one thing that must not happen is quietly reading one as the other.
Verified on the tree as it stands: 37 profiles, 25 bundles, 10 declaring composes edges — 10 agree,
0 disagree, 0 unresolved. Each fail arm negative-tested: a one-letter typo in a ref fails; a published
tier that disagrees with its own edges fails; the same composes shape pointing at another bundle's
cap-4 instead of its own element fails, which is what proves self-exclusion is load-bearing rather
than incidental.
One switch makes a laptop hot-reload what someone pushes elsewhere (e.g. cloud agents): a URL needs no
`git remote add`. Credentials in a https://user:token@ URL are never shown in the log or /api/dev-sync.
The first run downloads the pinned PocketBase (the cloud container's version) for this platform, checks its
SHA-256 and extracts it in-process into backend/bin (gitignored). It serves .pb/dev (gitignored) with the
migrations in git and hooks off, as in the cloud. DX_PB_URL proxies /api/pb to a shared PocketBase instead;
DX_PB_PORT moves the local one, and a port already in use is reported once instead of crash-looping.
Opt-in with DX_SYNC_REMOTE (plus DX_SYNC_BRANCH, default main, and DX_SYNC_INTERVAL, default 15 s) in
.env.local. Every interval the dev server fetches the branch and fast-forwards when this checkout is on it
with no modified tracked files and no local commits, so Vite hot-reloads what teammates landed. Otherwise
it never merges, stashes or resets: it logs once and reports "main is N commits ahead, pull when ready" at
/api/dev-sync for a Studio badge.
When the data arrived while the map was still loading tiles, the paint was skipped
and never retried, so Mobilize opened on an empty map (and Campaigns could miss its
dots the same way). A skipped paint now retries once the map is idle.
/admin/pb-signin-check (behind the site sign-in) signs in to pb-dev through the team account, with the
same realtime flow as the PocketBase SDK, and shows the email and groups it sees, or the exact refusal.
The token is never stored.
repo END-REPO-PBDOC: the Driver's decision went live and REVERSED their earlier instruction to me. When I
first wrote this section repo said to leave DX_PB_URL out entirely, because it sat behind the team sign-in
and failed from a laptop. It is now the TEAM DEFAULT, and the local database is the fallback.
So the section is rewritten rather than appended to: a joiner is now told to set
DX_PB_URL=https://<your team's dev server>/pb, that they sign in with the same team account and there is no
separate password, and that leaving it unset still gives them a private local one — which is a fine way to
start. Placeholder host in the public text, not ours.
⚠ THE OPENING LINE WAS QUIETLY WRONG AFTER THE CHANGE and I nearly left it. "There are three databases and
you only ever touch one" was true when the local one was the only option a joiner had; now they touch the
shared one OR a local one depending on one env line. The claim that actually matters — and is still exactly
true — is that they never touch the third. Now reads "You work against one of the first two — never the
third." A sentence can survive an edit around it and stop being true without looking any different.
Added the detail repo gave that a reader needs and would not guess: new collections start LOCKED until a
migration opens them, deliberately, so nothing is public by accident. That reinforces the schema-as-commits
point the section already makes rather than sitting beside it.
Names unchanged and re-verified: DX_PB_URL points at the PocketBase root (.../pb), DX_PB_PORT for the
port-in-use case. No internal hostnames anywhere in the file.
Laptops reach pb-dev's API without the site sign-in; PocketBase signs people in itself through the same
OIDC provider (its own client), members of OIDC_ALLOWED_GROUPS only, password sign-in off. The group check
is ONE hook baked into the image and pinned by SHA-256; hooks from the pulled repo still never run. The
dashboard, _superusers and system collections stay behind the site sign-in for admins. It fails closed:
without the baked gate or a working OIDC setup, /pb/ stays behind the site sign-in as before.
repo af37aa52 changed two things I had written emphatically, and studio confirmed both independently
(END-REPO-NAMES2 / END-STU-DOCFIX).
- DX_SYNC_REMOTE now takes a remote NAME **or** a full repo URL. My callout said "the remote's NAME, not a
URL" — a sentence whose whole point is now backwards, so it is rewritten rather than softened. A URL saves
a `git remote add`, and a private repo uses the laptop's existing git credentials.
- DX_SYNC_INTERVAL default is 5 seconds, minimum 2. It was 15/5.
- Added the reassurance studio named: a token in an `https://user:token@…` URL is never written to the log
or shown at /api/dev-sync. That is exactly the question someone pasting a private URL will have, and the
answer is good — worth stating rather than leaving them to wonder.
⚠ AND I DID **NOT** TOUCH LINES 38 AND 167, which also say "about every 15 seconds". studio flagged that
those describe the SHARED SERVER's pull loop — a different thing from DX_SYNC_INTERVAL that happens to have
carried the same number. Changing them because the number matched would have introduced a fresh error while
fixing a real one; checked both after the edit and they are intact.
Every other name still matches repo's branch. repo has also warned that a shared team database may later
become the default, which would make the PocketBase section go stale; they will tell me before it does.
repo answered with the names as they will ship (END-REPO-ENVNAMES) and dx-shepherd caught a path I had
wrongly removed. Both corrected; the doc now states nothing I have not verified.
⚠ MY "THE PATH DOES NOT EXIST" CHECK WAS WRONG, AND WRONG IN AN INSTRUCTIVE WAY. I searched the LAST
COMMIT's file list for pb_migrations and found nothing, then reported the path as unbuilt. `git ls-tree -r
origin/main` shows backend/pocketbase/pb_migrations/.gitkeep — it has been there all along. A file list from
one commit answers "was this touched recently", never "does this exist"; I asked the cheap question and read
it as the expensive one. The real path is restored.
THREE CORRECTIONS FROM repo, each of which would have cost a reader time:
- DX_SYNC_REMOTE is a remote NAME (`origin`), not a URL. My draft said "<the repo URL>", which would have
failed silently for every joiner who copied it.
- DX_PB_URL IS OUT ENTIRELY. repo was explicit: it points at the shared database, which sits behind the team
sign-in and does not work from a laptop. Telling a newcomer to set it would have sent them to debug an
auth wall on their first afternoon. What replaced it is the truth — `bun run dev` downloads and starts a
local database itself, and there is nothing to configure.
- VITE_MAPBOX_TOKEN is the one credential they must ask for, and my draft never mentioned it. A joiner
following the old page would have got a working repo and a blank map.
ALSO ADDED, because it is the behaviour that makes the feature safe to recommend: the auto-pull only runs
when you are on that branch, with no unsaved edits and no local commits. Otherwise it does not touch your
files, it says "main is N commits ahead … pull when ready" in the log. That is the difference between a
feature a newcomer trusts and one they turn off after it eats an afternoon's work.
Required set is now stated plainly: DX_SYNC_REMOTE=origin if you want teammates' work to arrive, plus the
Mapbox token. Everything else optional, with its default in a table.
No internal names, re-checked after the rewrite: the only product name left is PocketBase, which a joiner
sees in its own admin UI.
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
my-upg-100-list
research-dashboard
rudder-voice. Rebuilt, tree + SRI
regenerated; the landing precondition (fresh, one folder, SRI) passed in main-capture scan.
dx-shepherd ENDTOKEN-UPG-DOC, from the Driver's laptop-first decision. Written for someone staring at a
repo, a dev server and a database, unsure which one they are allowed to touch — so it answers that first
and explains second.
THE SHAPE, in the Driver's own three places: laptop = your workshop · main = the truth · the shared server
= the table, look but do not edit. Plus the rule the whole thing rests on, one plugin folder per person.
★ THE 95% NUMBER IS THE POINT OF THE PUSH SECTION, not decoration. A newcomer whose first push is rejected
assumes they broke something. The measured result — 95% of pushes rejected, 0 conflicts, 1.1 retries on
average — tells them the rejection IS the normal case and explains why it is cheap: you collide on who goes
first, never on the same lines, because you are never in the same file. That reframes a scary error into one
extra command, and it is the strongest argument for the one-folder rule.
⚠ TWO THINGS I CORRECTED RATHER THAN SHIPPED:
- I first cited `backend/pocketbase/pb_migrations` as the migrations path. It does NOT exist in this repo —
it is from the architecture doc's PLAN. A path a reader cannot find is worse than no path, because they
assume they cloned wrong. Now says "committed alongside the code" until it is real.
- "reloads every few seconds" is now "about every 15 seconds", which is what the source actually states.
NO INTERNAL NAMES, checked rather than intended: zero hits for the server hostnames, the source-control
product, the agent tabs, the dictation tool, the auth stack, "tenant" or the project's own code names.
Cross-linked to develop-a-plugin-and-see-it-live.md, which answers the different question of how to BUILD a
plugin — this page is about working alongside other people once you have one.
⚠ THE ENV SECTION IS PROVISIONAL. DX_SYNC_REMOTE / DX_SYNC_BRANCH / DX_PB_URL are taken from the
architecture doc; repo is building them now and I have asked for the exact names, which are required vs
optional, and what each points at. A doc with a wrong env name is worse than no doc — the reader blames
themselves — so this gets corrected the moment repo answers, before anyone links it.
A server where Rudder is laptop-only (RUDDER_ENGINE=0) no longer starts a
leftover engine binary. If an engine that records its own machine's microphone
is reached from another machine anyway, Rudder shows the laptop notice and
disables Record instead of recording the server.
ShareButton is not a .mcb. It wraps its button in a .share-wrap div carrying
border-radius:50% and a conic-gradient ring on ::before at inset:-2px, so the
button rules missed it and it kept a round rainbow halo inside the square
panel -- breaking the single-card illusion at the corner the eye lands on.
rega measured it at 390 and asked whether square should flatten it. It
should: in this variant the panel owns the outline, so a child may not bring
its own. The round default is untouched.
Gated, with a control asserting ShareButton really does carry a 50% radius,
so the two rules are not decorative.
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.