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.
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.
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.
deployment/cloud/: a bootstrap Dockerfile (Bun, git, oauth2-proxy v7.15.3) that clones the repo
and runs deployment/cloud/entry.sh from it, so the recipe updates with git; the Vite overlay
(host, allowed hosts, HMR over wss); and the pull loop that follows the branch. With no OIDC env it
serves Vite directly (laptop default unchanged). With OIDC_ISSUER set, oauth2-proxy owns the port
and Vite listens on localhost behind it: Studio, maps and the HMR websocket all sit behind one
sign-in, with PKCE S256, a confidential client and a REQUIRED group allow-list (it refuses to
start without one). Secrets reach oauth2-proxy through its environment only. README gets a short
'Run it in the cloud' section linking the recipe.
Prose: the Tauri README's token note says 'the test host', the WordPress how-to's hosting option says
'a Forgejo host you run'. The WordPress contract test's catalog base was the live host as a fixture;
it is now https://cdn.example.org/plugin-assets/ in all three places (the passed base, the expected
script src, and the negative test), which keeps the test self-consistent: check:wp-embed PASS before
and after.
Driver decision: no embeddable URL carries "doxa" in its folder. The tree's name is one line in
vite.doxa-maps-build-config.json ("name": "dx-maps"); everything else follows it.
- __DOXA_TREE_NAME__ (the vite define rega landed, from MFE_NAME = internal/_tree-name.mjs) bakes the name into the
bundles for the runtime code that must recognise the tree in a URL. StackLayout.vue's
resolveBundleSrc anchored on a literal /doxa-maps/ and would have broken every composite's nested
bundles on a renamed tree; it now anchors on the define. The Research Gallery's dev fallback uses
it (one reader: rega's define, not a second one).
- The native shells follow via internal/sync-app-shells.mjs: capacitor webDir public/dx-maps, Tauri
frontendDist ../../public/dx-maps (its window url is tree-relative since the one-folder landing).
- The WordPress contract test reads the name from config instead of carrying three literals.
- check:locale-reachable extracted detectLocale alone and died with "localeFromUrl is not defined"
after 09d03c60 factored a helper out; it now follows the calls it makes (already red on main).
- generate-tree removes its temp asset pool even when it crashes (a failed build leaked ~50 MB into
/tmp each time; measured on a full disk).
- Capability exemplars regenerated for the new paths.
Proof: rm -rf public && bun run build -> public/ = README.md + dx-maps/; the 25 built bundles carry
zero 'doxa-maps/' paths (scanned); check:sri, check:drop, check:parse, check:public-hygiene PASS.
Tag names (doxa-* -> dx-*) are the next, separate landing.
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.
deployment/src-tauri/.gitignore listed /target, /gen and Cargo.lock together
with reasoning that fits the first two. /target is 2GB of compiled output and
/gen is emitted by tauri-build on every compile -- both genuinely
regenerated. A lockfile is not build output: it is the RESOLUTION RECORD, the
only artifact that makes cargo build reproducible.
Cargo's rule splits on crate type -- commit for a binary, ignore for a
library -- and cargo metadata reports this crate as kinds=['bin']
(src/main.rs, no lib.rs). Checked with cargo rather than inferred from the
file layout.
The stakes are concrete: Cargo.toml pins nothing tighter than
tauri = { version = "2" }, while the lockfile pins 430 packages. Without it
an inheritor building next month resolves a different dependency tree than
the one that produced the shipped 64 MB binary, and method 5 is already
"built, not shot", so its build must at least be repeatable.
Verified before committing 109KB: cargo metadata --locked --offline succeeds,
so the lockfile is consistent with the manifest, and it contains zero local
paths or credentials.
check:tauri gains a fifth assertion -- Cargo.lock present AND TRACKED.
Tracked-ness is the point: the file sat on disk the whole time and was still
absent from every clone. Negative-tested against the real state, not a
simulation: the gate FAILED naming the file while it was untracked, and
passed once staged.
The Tauri README said the bundles 'self-host mapbox-gl from public/assets/vendor/
(the no-CDN rule)'. Measured: vendor/ contains only tabulator, and every leaf
carries src=https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js. MapLibre
likewise loads from cdn.jsdelivr.net. The ENGINES registry in
library/composables/useMapInstance.js is the authority.
This mattered because the sentence sat next to CSP guidance: a script-src
written from the old claim would have blocked the engine and produced a blank
map - the exact symptom being debugged elsewhere.
Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.