- check:no-orphan-gates stopped at internal/check-grid-merge-defaults.mjs, which had no script. It
asserts defaults of three grid props that are not on development (the grid merge has not landed),
so it is registered ON DEMAND with that reason, not chained red. Past that first arm, its chain arm
found check:grid-image-loading and check:retry-fetch run by no chain; both depend only on code and
pass in under 300 ms, so they join gates:pre.
- A tracked vite.config.js.timestamp-*.mjs (395 KB, eight absolute paths) failed check:no-abs-paths.
Removed, and ignored at the source: vite writes one beside its config while it bundles it.
- check:library-index: documentation/reference-library-index.md regenerated for retryFetch.js.
- run-parallel printed a failed step's output only with --verbose: the unbraced else bound to the
inner if. Braced; a failing step now prints why.
- The Pages workflow ran bun run build without BUILD_ALLOW=1, so build-lock refused it with exit 75
and the workflow ran no gate at all. A CI runner is the only builder on its machine; it opts in.
package.json's gate descriptions, vite.config.js, the build config's
comment keys, capacitor.config.json and .gitignore named or quoted the
Driver in 13 lines. They sit outside check:no-new-driver-references'
areas, but they ship in the giveaway all the same, so they are reworded
the same way as the sweep. The two left are the reference gate's own
description, which has to say what it checks.
key-cities is a gated profile: generate-tree leaves it out of the manifest and the pages. Its
bundle file was still in public/dx-maps/key-cities/key-cities.js, committed, and the landing
freshness pass rebuilt it into public/ every time it looked "stale", so any host that serves or
drops in the tree folder whole shipped it.
- vite.config.js builds a gated bundle into .gated/<tree>/ (ignored by git) instead of public/.
- check:composite-freshness skips gated bundles: they have no committed bytes to be fresh against.
- The committed public/dx-maps/key-cities/ is removed. History is not rewritten.
- generate-tree removes a gated folder it finds in the tree (an older build, or a landing that kept the
tree's side of a conflict), so the tree it writes is clean whatever came before. Measured: a planted
public/dx-maps/key-cities/key-cities.js fails the gate, and one regeneration removes it.
- The gated list is read in one place, internal/_tree-name.mjs, beside the tree name.
- check:gated-not-in-tree (gates:pre) fails if a gated name has a folder in the tree, a manifest
entry, or a page referencing its script, and proves each arm on a planted leak first. It FAILED
on the tree before this change (key-cities/ is in the tree) and PASSES after.
The data tables are left alone on purpose: key-cities.json also feeds the production Cities with
Internet map, and whether that dataset should be public is a separate decision.
A plugin's <profile>.meta.json targets (web, mobile, desktop, widget; absent = web)
now decide where it is built. internal/plugin-targets.mjs is the one reading.
desktop:build builds a desktop plugin into .desktop/<plugin>/ with a page that
names its engine, builds its backend as a Tauri sidecar for this machine, and runs
tauri build with a --config override; tauri.conf.json and public/ are untouched.
The shell starts the bundled rudder-engine (RUDDER_AUDIO=browser, RUDDER_HOME in the
app data folder) and stops it on exit. A plugin whose targets leave out web
(Rudder: desktop + widget) is no longer built for the web tree: run-parallel,
generate-tree and check-composite-freshness skip it, and its public/ folder is
removed; the dev server still gives the Studio its corner. check:targets
(gates:pre, with a self-test of five planted violations) validates the values,
widget corners and backend folders and fails if a non-web plugin reaches the built
tree. Measured: desktop:build rudder-voice on Linux produced Rudder_0.1.0_amd64.deb
(8.8 MB) with the app and the engine side by side; the app has not been launched.
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.
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.
Driver decisions (cards BUILD-OUTPUT-IS-ONE-FOLDER, BUILD-PLUGIN-STUDIO-IS-THE-OUTPUT-FRONT-PAGE).
- public/ holds only <tree>/ and a generated README.md. The build no longer writes public/index.html,
public/assets/ or public/__gallery/; `rm -rf public && bun run build` recreates exactly these two.
- The Plugin Studio is <tree>/index.html (tree-base "."), replacing the staging-forge chooser; nothing
is written above the tree. check:staging-lang, which tested that chooser, is retired with it.
- Assets are gathered in a temp directory and the fetched allowlist staged into <tree>/assets/ as
before. fonts/, images/, brand/ and legacy/ existed ONLY in public/assets: they are sources and move
to data/ (git mv). Poster previews keep their tracked sources in plugin-designs/*/assets/previews
(all 378 verified present).
- __gallery/ is archived out of public/ (a copy kept in the umbrella's _qa-cycle/archive) and out of
the build; generate-gallery renders only on request, into a directory you name, never public/.
- Native shells load the output folder: capacitor webDir public/<tree> (cap copy needs an index.html
there), Tauri frontendDist + window url relative to it. internal/sync-app-shells.mjs keeps both in
step with the tree's name on every build, so the coming rename stays one config line; the Capacitor
start page resolves its leaf tree-relative.
- dropin.include now lists only extras (README.md): the tree is always shipped and never named twice;
excludes are tree-relative ({tree}/...). clean.mjs removes the old root entries from existing
checkouts; the build lock and public/.gitignore's rules move out of public/.
- check:wp-embed read the BUILT manifest from gates:pre, before the build, so a from-scratch build
could never pass; it runs in gates:post and reads the tree name from config.
- Stale bundles refreshed by the clean build (key-cities, template-bundle, wall-of-people-groups);
the CLI doc's quoted scripts re-synced; capability exemplars regenerated; the '#' note leaves
check:stale-sweep-claim's command (it was already in //scripts).
Proof: rm -rf public && bun run build -> public/ = README.md + doxa-maps/; check:sri, check:drop,
check:parse, check:public-hygiene, check:quoted-scripts, check:capability-exemplars PASS; all 57 pages
(the front page + 56 leaves) mount under /doxa-maps/ on this tree. check:facet-cascade is red as on main.
data-private/ was a TRACKED folder, so field-access-visas.json would ship
with the repo the moment it goes public. A tracked folder is not private.
The table generator no longer writes inside the checkout: private tables go
to DOXA_PRIVATE_DIR (a folder outside the repo) or are skipped. data-private/
is gitignored so it cannot be re-created by accident.
The file is removed from HEAD only; it is still in history (5 commits).
Publish from fresh history or scrub it before the repo goes public.
public/assets/<name> is not a source. generate-tree.mjs:1353 rmSync's it and
copies data/<name> over it on EVERY `bun run tree`, so five directories were a
byte-identical copy of a tracked source and BOTH copies were committed:
posters 378 files 40.9 MB
people-groups 2 files 23.0 MB
tables 19 files 14.2 MB
legacy 4 files 2.0 MB
vendor 3 files 0.6 MB
Verified identical, not assumed: same file set and matching hashes in all five.
And proven regenerable rather than argued — deleting public/assets/posters and
running `bun run tree` restored all 378 files with laos.webp md5 ea65d2f1, the
same hash the committed copy carried.
This is the Driver's "80mb of images in a public folder" almost exactly, and it
is the cheapest possible version of it: a publish was already rejected once at a
74 MB asset tree, and half that weight was a duplicate of something already in
the same repo. No feature changes, no size tradeoff, nothing regenerates
differently.
★ previews/ IS DELIBERATELY STILL TRACKED. It has no data/ source — make-previews.py
derives it from the full posters — and it is what the poster WALL renders from.
bundlePosters defaults false, so a dropped folder ships previews and NOT posters;
untracking previews would blank every wall. Treating "public/assets" as one
homogeneous pile would have removed the one directory that must stay.
The root .gitignore said "what actually ships is the exported subset in
public/assets/ (see export-public.mjs)" and BOTH HALVES were stale — which is why
the duplication looked intentional. export-public.mjs writes a SIBLING OF THE REPO
(../../../../upg-100-posters or $DOXA_PUBLIC), stating in its own header that this
is "NOT inside public/, because capacitor packages public/ whole"; and
public/assets/ is now generate-tree's mirror of data/. Comment corrected in the
same change, because a stale rationale is what restores a deleted duplicate.
Build green with the mirrors untracked: 7/7 pre, 23/23 bundles, 21/21 post.
Standalone repo that builds embeddable JAMstack map bundles (web components),
composes them into a lazy-loaded SPA via layouts/, and optionally wraps a
Capacitor Android/iOS app — one build, choose your output/CDN. Diátaxis docs.
History reset to a single clean commit; no PII, IPs, or internal terms.