Commit graph

620 commits

Author SHA1 Message Date
Doxa Maps
a08cf1e4d8 Gate: no PocketBase hooks in the repo
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.
2026-09-28 00:20:34 +00:00
Doxa Maps
a37a4e9c12 The LOCALE column clears for Big Picture: the held numerals decision is allowed at its one site
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.
2026-09-28 00:20:33 +00:00
Doxa Maps
82e05018ee composes speaks three vocabularies, and unresolved refs now fail
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.
2026-09-28 00:20:33 +00:00
Doxa Maps
b919371b26 Follow a remote: DX_SYNC_REMOTE also takes a repo URL, and checks every 5 s by default
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
7b49aef938 bun run dev runs PocketBase at /api/pb, or uses a shared one
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
de004b3703 bun run dev can follow a shared branch: fast-forward only, never over local edits
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.
2026-09-27 23:54:30 +00:00
Doxa Maps
bac8a1048c Rudder with RUDDER_ENGINE=0 never starts the engine, and a page on another machine never offers to record the server's mic
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.
2026-09-27 23:24:08 +00:00
Doxa Maps
9b150b902d Square panel: no child brings its own outline
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.
2026-09-27 23:23:42 +00:00
Doxa Maps
55573df1d2 Rudder learns whether this server should have an engine even when its event stream is buffered; the legend bar keeps its rounded ends
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.
2026-09-27 23:14:15 +00:00
Doxa Maps
8e267b0ce5 Rudder on a server that should run the engine says it is not running yet, not that it only runs on your computer
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.
2026-09-27 23:14:15 +00:00
Doxa Maps
8afea17c6b Capability is derived from what a plugin composes, and the preset-plugin standard is written down
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.
2026-09-27 23:14:15 +00:00
Doxa Maps
13dfe63a1f Clear the regions locale column without going blind
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.
2026-09-27 23:14:15 +00:00
Doxa Maps
7d3cfca897 Plugin Studio: every plugin sits under its capability 1-5; no Experimental section
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.
2026-09-27 23:14:14 +00:00
Doxa Maps
cf0b23a4d1 check:mjs-parses parses with node even when the gates run under bun
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.
2026-09-27 22:10:35 +00:00
Doxa Maps
533ef04a9f Rudder transcribes on a server: the page streams its own microphone to the engine
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.
2026-09-27 22:10:35 +00:00
Doxa Maps
30a02d2782 Rudder's corner shows a live mic when collapsed, and dev backends survive Vite restarts
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.
2026-09-27 20:09:03 +00:00
Doxa Maps
2044081798 Rebuild the served tree after merging main
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2026-09-27 19:26:55 +00:00
Doxa Maps
37aaf69c24 rega's four notes on the card, all four mine
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.
2026-09-27 19:26:29 +00:00
Doxa Maps
e79d6dc8f4 A shared link reads a list separated by , or |, so partner links already published keep working
Links on partner pages write band=yes|partial, and the reader split on ',' only,
so such a link read back as one value and matched no row. Reading now accepts
either separator; writing stays ','. A literal '|' in a value is percent-encoded
before any join, so every separator left in a URL is a real one.
2026-09-27 19:10:08 +00:00
Doxa Maps
f68cb24e78 2,001 groups had readable Scripture and were told they did not
go-int cross-checked 20 roster languages with "no New Testament found in the pools" against
open-data signals. Thirteen of the twenty had readable text all along — on a record the tier
could not see — and chasing that found one rule written four times and wrong in two of them.

THE BUG. tierOfCode tested `!!c.s`, one field, while this build's own hasReadableScripture
tested four sources. Tier 2 means "Scripture AND film", so a language whose text is on the
Faith Comes By Hearing record (au.m names the media it holds), on Scripture Earth, or in a
Door43 translation was scored 1 — "some resource found" — with its own text and the JESUS
Film both in hand. Measured: 602 codes and 2,001 of 12,379 groups move 1 -> 2. Tier 0 does
not move at all, 1,592 before and after, so nothing about who has NOTHING changes: the gap
lists, the 369 zero-access groups and the bridge-reach table are untouched.

THE SAME BUG DOWNSTREAM, and this is the one a catalyst would have felt: resourceRows typed
an FCBH record "audio, text, video" as Audio Scripture only, so the access table's
byType.Scripture never saw it. 288 people groups were published as "watch or hear, cannot
read" while their own readable Scripture sat behind that link. Access now reads 1,129 "read
and watch" (was 869), 518 "cannot read" (was 806), 85 "read" (was 57), 369 nothing found and
6 stories only — unchanged where it should be, and 2,107 in total either way. Typing the row
by its text would have dropped "can hear", so the access builder now counts a Scripture row
whose scope names audio as hearable too.

ONE OWNER NOW. hasReadableScripture is exported from gospel-resource-rows.js and imported by
the pin builder and by check-gospel-coverage-consistent, whose arm F had its own narrow copy
and therefore reported 2,001 disagreements against the corrected tier — a gate defending the
bug it exists to catch. A rule with four copies has no owner; this one has.

`sx` ships alongside: the readable text with its source named, for the 887 codes whose text is
not at c.s, so a surface can LINK what the tier now counts instead of claiming Scripture and
showing a film. Wiring it into the sheet and the field card is the next commit; this one is
data and rules only, and public/ is deliberately NOT rebuilt because rega is rendering this
worktree.
2026-09-27 18:53:52 +00:00
Doxa Maps
e2cebd9130 Shared links keep partner param names, and a CSV export matches multi-value filters
A shared table link can now be written as one query param per facet, and a
param name may map to a different bus key, so links already published on
partner pages keep working. The CSV export matched no rows once a reader picked
a second value in a filter; the default row match now reads a multi-value filter
as OR, and the gate no longer exempts that line.
2026-09-27 18:40:47 +00:00
Doxa Maps
655bbdc561 A filtered table becomes a link anyone can open, and the link now carries the typed search
Card BUILD-SHAREABLE-FILTERED-TABLES, for researchers and media-to-movements teams: narrow the table to
the peoples you are working on, then give that exact view to a teammate or reopen it on a phone.

TWO GAPS, both measured on main before building.

1. IT WAS NOT REUSABLE. The share hash was a PRIVATE function inside
   plugin-designs/research-dashboard/src/composables/useDashboardShell.js, so exactly one dashboard in
   the fleet could produce a link. go-int's board had none — grepped, zero matches — and every future
   kit dashboard would have had none either. Nothing in it was research-specific except its address.
   It is now library/composables/useShareableView.js, parameterized: the bus and labelFor are passed
   in, the filter and search param names are options, and no dashboard is named inside it. The
   research dashboard supplies the one genuinely research thing — labelFor, for the display-only slug.

2. THE LINK WAS INCOMPLETE, AND SILENTLY SO. The hash serialized filterSet ONLY. Typing in the search
   box is the commonest way this persona narrows a table, so a shared link reopened with MORE ROWS than
   the sender saw, with nothing to tell the recipient their set was wider. For population data passed
   between teams that is worse than a link that plainly fails. `q` is appended only when a term is
   present, so a dashboard with an empty search box writes byte-identically the hash it wrote before.

`&` joins params now, so it is reserved in values alongside `;` `:` `*` `,` `%` — otherwise a search for
"rice & beans" would truncate the link at the ampersand.

check:share-policy grows a DRIVEN arm: the two pure functions are lifted from the kit source and RUN,
so nine guarantees are measured rather than asserted — the old format byte-for-byte on an empty search,
slug still display-only, multi-value still dropping the slug, the typed search reaching the link and
coming back, a literal comma in a people-group name surviving, an ampersand in the search surviving, and
both legacy shapes (JSON hashes, and old #f= links with no q) still restoring. No browser needed to
prove a string round-trips.

Source only. Rebuild: research-dashboard, dashboard-plus-grid.

check:filter-multivalue arm C follows the file: it lifted escapeValue/unescapeValue from the shell to
drive the hash round-trip, and said so plainly when they moved ("the hash format moved and this gate is
blind to it") rather than passing vacuously. Repointed to the kit module.
2026-09-27 18:25:01 +00:00
Doxa Maps
c3332e3d85 The field card rendered blank, because I never rendered it
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-27 18:14:38 +00:00
Doxa Maps
89ff223846 Rudder with no engine running is quiet in dev: the dev backend answers offline instead of 500
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.
2026-09-27 10:05:45 +00:00
Doxa Maps
e53495dfcf The dev server's manifest carries stage, heavy and widget, so Rudder's corner mounts under bun run dev
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
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.
2026-09-27 09:41:26 +00:00
Doxa Maps
b22df1381e Mark 5 in five more languages, each a cited public-domain translation
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.
2026-09-27 09:07:17 +00:00
Doxa Maps
1775f5acb5 Corner widgets: a plugin can declare a fixed corner of the Plugin Studio, and Rudder takes bottom-right
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.
2026-09-27 09:07:17 +00:00
Doxa Maps
a4e8b8c058 Add Rudder (rudder-voice): offline streaming voice-to-text, a desktop-target plugin
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.
2026-09-27 08:23:17 +00:00
Doxa Maps
524e5ebfae Translate the new packet pages into all eleven locales
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.
2026-09-27 07:37:26 +00:00
Doxa Maps
e214cc2b7b LOC-ENUS counts the named constant, so folding the literal is not mistaken for fixing it
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.
2026-09-27 07:37:26 +00:00
Doxa Maps
f3b7e25e49 check:experimental-shelf strips the Mapbox preload under MapLibre again
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.
2026-09-27 07:37:26 +00:00
Doxa Maps
de243597db The prayer packet the Driver asked for: life calendar, the same grid as a percentage, a triangle
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.
2026-09-27 07:37:25 +00:00
Doxa Maps
8d739edfcd Unblock studio's landing 13: drop the dead fallback, make the zoning gate measure on a clean tree
Both halves of studio's L13U refusal were correct and both are mine.

1. THE LEGACY upg100WallIntro FALLBACK IS GONE. It existed for exactly one window — the three
   plain-text keys were landing in studio's catalogue while this consumer was already written, so the
   branch rendered the old markup-bearing key until they arrived. They are on main in all eleven
   locales, so the condition can no longer be true: it is DEAD CODE that keeps a key alive, because
   studio cannot delete upg100WallIntro while a line still reads it. A fallback whose condition cannot
   fire is not safety, it is a reference.
   The three remaining mentions in this file are COMMENTS, not code — verified that no live call site
   anywhere in plugin-designs/ or library/ reads it, so it is now safe to delete.

2. THE GATE DEFAULTED TO A GITIGNORED FILE. check:zoning-declared read
   library/scripts/posters/out/_records.json, which .gitignore:94 excludes, so on a clean checkout it
   exited 75 — NOT MEASURED forever, indistinguishable from an unwired gate. studio measured exactly
   that and correctly refused to wire it into gates:post. This tree has been here before:
   check:packets-all SKIPped on every build for weeks for the same reason.
   ★ THE FIX IS A COMMITTED FIXTURE, NOT A LOOSER GATE. data/people-groups/zoning-records.json carries
   slug plus every field a selector actually reads — 2,107 records, ~300 KB. country_code alone would
   not do: the deaf zone reads rop1 and five African/European zones read wagf_block/wagf_region, which
   is the same omission that made six zones silently resolve to nothing in the packet generator.
   The live cache is still PREFERRED when present, so a local run measures the freshest data and CI
   measures the committed one.
   VERIFIED on a genuinely clean tree (git archive of the staged tree, no cache, no network):
   PASS — 28 zones, 2,107 records, 2,096 covered, all five assertions green, exit 0.

check:i18n-keys-exist PASS, check:sfc-parse PASS.
2026-09-27 06:57:00 +00:00
Doxa Maps
d87ee852fe check:composite-interop: two known-issue entries outlived the bug they described
The gate failed on main 12a64702 with two LATE_KNOWN entries that no longer match anything:
dashboard-plus-grid.vue#reflectDir and research-map-plus-grid.vue#reflectDir. Landing 11 fixed both —
reflectDir now captures the instance in setup, so a host switching lang to ar after mount does get
dir=rtl — and the exemptions stayed behind.

That is the gate working as designed: it refuses an exemption that has stopped describing anything,
because a stale entry is a defect it would quietly stop reporting. Both entries are deleted, and the
note left in their place says why rather than leaving a bare gap.

Gate only, no product code, no rebuild. composite-interop rc=0 after.
2026-09-27 06:42:55 +00:00
Doxa Maps
d024b855bd check:composite-embed-knobs: a composite with no map is N/A, not unmeasured
bp-big-picture could not be measured "because there's no canvas under a minimal config", and that was
the honest answer to a question it should never have been asked. Big Picture nests NO MAP: it composes
a research header and a people-groups grid, and its built bundle contains zero occurrences of
mapbox-gl or maplibre-gl. There is no canvas to draw and no Mapbox half to prove.

The target list was wrong, not the gate. A profile qualifies if `composes` names a map bundle OR the
type is nested-elements-mfe; that second arm exists because country-posters-mfe bakes its children and
names nothing in `composes`, but it is loose enough to admit composites that nest no map at all.

Now a non-`composes` candidate must also SHIP an engine, decided on the BUILT artefact rather than the
manifest, because whether a bundle compiled a map engine is a fact about what it built. Map-less
composites are printed as N/A so they stay visible instead of being silently dropped.

MEASURED: exactly 2 excluded — bp-big-picture (big-picture) and stack-demo — both genuinely map-less.
No other owner's map target is affected, so the set goes from 9 targets with 2 permanently unmeasured
to 7 real targets plus 2 declared N/A.

Gate only, no product code, no rebuild. Still needs a browser and a token to run: rega's, not mine.
2026-09-27 06:42:55 +00:00
Doxa Maps
c4bcb1407d check:composite-embed-knobs opens country-posters-mfe on its map view
country-posters-mfe now reads profile-config view (exp-shepherd, 66837586); it opens on posters, so the gate
passes view:"map" to have a canvas to measure. Browser gate: rega confirms the unmeasured cases drop.
2026-09-27 06:42:54 +00:00
Doxa Maps
57b0200bb6 The built tree ships every people-group list, so ?list= draws pins
generate-tree's asset allow-list staged people-groups-slim and -points but not people-groups/lists/*.json,
so on the built tree every non-default list (frontier-500k, frontier, unreached, unengaged) 404'd and the map
drew 0 pins (go-int / rega at a71007f1). The lists folder now ships whole, like tables/: the fetch name is the
list id. Verified on a regenerated tree: all four files staged; check:list-prop-unit PASS.
2026-09-27 06:04:09 +00:00
Doxa Maps
24b9ada48e Plugin Studio and the gallery split production from experimental, from each bundle's own stage
The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A
profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is
production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into
manifest.json, so the Studio and the gallery read the same field and neither keeps a list.

The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard,
my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability
sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental
section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board
joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split.
2026-09-27 06:04:08 +00:00
Doxa Maps
65faa2d971 Keep the square panel together on a phone
A media query adds no specificity. The mobile block's
`.map-toolbar { gap: 6px }` is (0,1,0) exactly like
`.map-toolbar--square { gap: 0 }`, and it comes later in the sheet, so it
won: on every phone a square toolbar fell back to separate buttons inside
one ringed panel. The shape broke precisely where the Driver is most likely
to look at it.

The --no-geocoder pair is (0,2,0) and outranked the variant whatever the
order, so it needs the two-class answer.

This is the same lesson as the goal cards' gap-vs-margin: a ported mechanism
is only ported if it still WINS, and specificity is the part that decides
whether it does. Found by following the phone thread rather than by looking
at the variant again.

check:square-map-controls now asserts the override is inside the mobile
block, that the two-class form answers --no-geocoder, and -- as a control --
that the base rule really does come first, which is why the override is
needed at all. Removing it turns the gate red.

The CSS comment explaining it cost one more mistake: I wrote the selectors
in backticks, inside the template literal that holds the stylesheet. A
backtick ends the literal, which is the trap check:sfc-parse names in its own
failure text, and it caught me with it.
2026-09-27 06:04:08 +00:00
Doxa Maps
ef9e1775d9 check:universal-props: a bundle's source is not only its own folder any more
THE MATRIX WAS UNDER-REPORTING, AND MY OWN KIT LANDING IS WHY. This gate scanned
plugin-designs/<bundle>/** and nothing else, which was correct while every panel lived inside its
plugin. The dashboard-kit extraction moves panels into library/components/dashboards/ and the bundle
still COMPILES them, so a source rule stopped firing the moment the file moved, with the code
unchanged. Measured: kit landing 4c-3 moved DashboardFacetRail.vue, which holds three
toLocaleString('en-US') calls, and the matrix went from 13 warns to 10. Nothing improved; the audit
stopped looking. LOC-ENUS has no `shipped` matcher, so the built-bundle pass could not catch them
either.

This is not three lines. Every kit extraction, and every future one, silently shrinks this audit —
the more code becomes shared, the less of it this gate sees. So each bundle is now audited over the
library files it actually IMPORTS, resolved from its own `@map/...` specifiers and followed
transitively. Attribution stays per-bundle, which is what keeps the matrix actionable: a library file
compiled into four bundles is reported against all four, because all four ship it.

★ MY FIRST VERSION OF THIS WOULD HAVE TURNED THE BUILD RED WITH FOUR FALSE POSITIVES, and finding
that is the reason I measure a gate's blast radius before handing it over:
- library/api/sources.json got swept in and LIST-FETCH reported it three times as "a file that
  fetches the list". It is the endpoint REGISTRY — a .json file declares endpoints and fetches
  nothing. The per-bundle walk has an extension filter; my sweep did not. Now it does.
- library/api/DataSourceManager.js:464 was reported for LIST-DSM, on the line
  `const dataSourceManager = new DataSourceManager()` — the singleton the module exports. That rule
  exists to catch a CALLER constructing one without profileConfig. A module is not a bypass of its
  own contract, and reporting it told three owners the library was broken because the gate read a
  definition as a use.

AFTER, on main: 13 warns (was 10), 0 bypasses, PASS, and 19 bundles audited instead of 18 — following
imports gave one more bundle a row. Every restored warn is LOC-ENUS, which is warn-only by the
Driver's held numerals decision, so nothing here asks anyone to change a number.

Gate only. No product code, no rebuild. check:universal-props is not in gates:pre or gates:post, so
this cannot turn a build red on its own; it is run directly and by check:studio-all.
2026-09-27 06:04:08 +00:00
Doxa Maps
833cc691a2 The locale warn was flagging the code that honours the locale
LOC-ENUS flagged any en-US format literal. Two of regions-dashboard's three
warns were the 'en-US' inside a CATCH, whose try formats with
`${locale}-u-nu-latn` -- a verbatim clone of DX-life's RegionDetail.vue:16,
Latin digits with the reader's grouping. The fallback exists because a bad
locale tag throws RangeError in toLocaleString and blanking the page is
worse than one wrong separator.

So the matrix's locale column said "ignores the locale" about code that
honours it, which is the kind of false warn that teaches people to stop
reading the column.

Measured before changing the rule: of the NINE LOC-ENUS sites in the tree,
exactly TWO are such fallbacks. The other seven are genuinely operative and
still flagged, so the tree goes 10 warns -> 8 and only those two move.

The exemption needs BOTH halves -- a catch AND a locale-derived primary --
because a lone catch must not excuse a format that never consulted the
locale. That is the one rule here that can widen by accident, so four
adversarial shapes are pinned as a self-test: the real pattern is skipped,
while a catch around a hardcoded primary, a catch whose primary never
consulted a locale, and a plain operative call are all still caught.
Loosening the exemption to a bare catch makes the self-test exit 3.

regions-dashboard keeps ONE warn: RegionCountryMap's popup, fixed en-US on
purpose, carded and evidenced by DX-life's own convention. I did not add an
ALLOW for it -- ALLOW is keyed per BUNDLE, so it would also blind regions to
a future operative en-US, and a visible warn on a decision already taken is
cheaper than a blind spot.
2026-09-27 06:04:08 +00:00
Doxa Maps
d888b20d8b Gate the square controls against the engine's own stylesheet
check:square-map-controls, registered with a gates:post token.

It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.

It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.

And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.

Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
2026-09-27 06:04:08 +00:00
Doxa Maps
56fd4fba79 check:universal-props: key-cities no longer needs its engine exemption
key-cities now resolves its engine through the library (exp-shepherd, 71af822a, on main at c2784f5a), so the
'key-cities:ENG-STYLE' allowance is stale. Removed; the gate passes against main's key-cities (18 bundles),
and it fails on the pre-change file, so this commit must land on or after c2784f5a.
2026-09-27 06:04:07 +00:00
Doxa Maps
e9e1adc0d6 check:shadow-css-parses reads the style id: it must be a plain identifier
gospel-plugin's trap (COMMON.md): a CSS rule moved past the first argument of useShadowStyles concatenates
onto the style id ('@media(...){...}doxa-gospel-map'). That is valid JS, so the build stays green and the rule
is silently lost; the brace-balance scan never sees it because the id is not CSS. The walker now folds the
second argument too, and a literal id must match /^[a-z0-9-]+$/ (a computed id is left alone). Measured on
the current tree first: 0 offenders, so this breaks no owner. Self-tests cover the trap and a plain id.
From dx-shepherd (END-DXS-CSSID).
2026-09-27 06:04:07 +00:00
Doxa Maps
d5a95e97b8 merge main (gospel a37ea4ff) into packet/vertical
Six public/ conflicts, zero in source. They take main's side here and are regenerated from the merged
source in the next commit — a bundle is an output, and a hand-reconciled middle version is wrong by
construction.
2026-09-27 05:56:25 +00:00
Doxa Maps
7b6d02a568 Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-3/work
# Conflicts:
#	package.json
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/gospel-resources/charts/index.html
#	public/dx-maps/gospel-resources/data-tables/index.html
#	public/dx-maps/gospel-resources/gospel-dashboard/index.html
#	public/dx-maps/gospel-resources/gospel-resources.js
#	public/dx-maps/gospel-resources/resources-list/index.html
#	public/dx-maps/gospel-resources/resources-map/index.html
#	public/dx-maps/manifest.json
2026-09-27 05:46:57 +00:00
Doxa Maps
4ee5c02c8b merge main (landing 10 + the web-sized boundaries) into packet/vertical 2026-09-27 05:07:45 +00:00
Doxa Maps
d4bd75f4ba Library index: a data file is described by its own _description or _meta, not its first line
generate-library-index took the first line of every file as its description, which for a .json file is data:
the Data section read '"_meta": {', '"value": "AFG",' and a run of ISO codes (flagged by simpleresearch).
A .json file now uses _description, then _meta.description, then _meta.purpose, and otherwise says
'(no description)'. iso3-to-alpha2 now reads as what it is.
2026-09-27 05:05:04 +00:00
Doxa Maps
8c7a52ed6f Every shadow root honours prefers-reduced-motion
A page's own reduced-motion reset never crosses a shadow boundary, so library components rendered inside
custom elements slid, faded and spun for readers who asked for less motion: thirteen of them, including the
legends and map controls (regions-plugin found it on ProgressBar and named the class).

useShadowStyles now injects one prefers-reduced-motion reset into each shadow root it styles, which covers
every component styled through it, including later ones. PosterDialog, FilterDropdown and SearchFilterHeader
style themselves with <style> blocks and carry their own guard.

check:shadow-reduced-motion (in gates:pre): useShadowStyles must inject the reset, and every animating
library component must be covered. It fails without the reset (negative-tested) and found the three
<style>-block components on its first run.
2026-09-27 05:05:04 +00:00
Doxa Maps
a267b89583 The Research Gallery loads behind a login gate: same-origin fetches keep their credentials
Behind any login gate (basic auth, a session cookie, OIDC) the gallery said "Could not load Big Picture":
doxa-maps-spa fetched its same-origin manifest, and plugin-studio its token URL, with credentials:'omit', so
the gate's cookie never went and the request was refused (found on the new dev container; dx-shepherd).
Both now use 'same-origin', which never sends credentials cross-origin, so CDN and CORS behaviour is
unchanged.

check:no-credentials-omit (+ :self-test, in gates:pre) fails any credentials:'omit' in library/ or
plugin-designs/ unless the line says why (// credentials-omit: <reason>). Measured before: exactly these two
sites; it fails on the old code and passes on the fix.
2026-09-27 05:05:03 +00:00