GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

PUBLIC_EXHIBIT_BUILD_PLAN.md



RELEASE

planning/2.8.5/plans/PUBLIC_EXHIBIT_BUILD_PLAN.md

sha256 0b04311a2d2a8dce · 9541 bytes · original held in the private archive

# Public Exhibit Build — boundary design + `--public` variant plan **Status:** BUILT (2026-07-23). The unified exhibit-asset scheme AND the `--public` variant build are now implemented (`museum/generate.mjs --public` → `museum/publish.mjs` → the self-contained `museum/public/` staging tree). Owner decided the three open sub-decisions (§4): prototypes PUBLIC, rooms PUBLIC minus their image grids, output to a `museum/public/` staging subtree. What remains is the EXPOSE layer (name-substitution + credential scan + the separate public repo + Cloudflare) — the NEXT P2 step, deliberately NOT done here (§6). **Home:** `planning/2.8.5/plans/`, next to `CAPTURE_PIPELINE_PLAN.md` — its natural successor. Archive-only; nothing here is ever pushed public (root `CLAUDE.md`, the one hard rule). Note the irony this plan is about: THIS repo can never be public; the *public exhibit* is a **separate** tree built from generated output only. --- ## 0. The ruling (owner, 2026-07-23) — Option B > Curated exhibits are PUBLIC and self-contained. The raw "capture everything" room galleries > (429 images / 172 MB) stay a PRIVATE-archive affordance and are NOT in the public build. This is the curation law made concrete ("capture everything, exhibit a curated subset"). The public tree carries **what is exhibited**, not all 172 MB. Bundling the galleries wholesale is explicitly **rejected** — it would double the archive's image storage and dump the raw index onto the public wall. --- ## 1. THE EXHIBIT-ASSET SCHEME (built — the foundation) One convention for every image a curated exhibit RENDERS. Implemented in `museum/generate.mjs` (`bundleExhibitAsset()` header + `EXHIBIT_ASSET_KINDS`): ``` museum/site/assets/<kind>/<name> kind ∈ { intent, reality, bugs } (+ future: gallery) ``` - Referenced with an **IN-SITE relative path** (`assets/<kind>/<name>`), never a `href(TMP_DIR, …archive-original)` that escapes the served root. - `<name>` is **PATH-DERIVED and STABLE** — an address only moves if the SOURCE is renamed, never on a content edit (the "addresses must not move" ruling): - **intent** — basename of the source mockup (`operations-412.png`) - **reality** — the capture identity `<game>-<panel>-<width>` (`fnv-operator-412.png`) - **bugs** — `sha8(sourceRel)-basename` (collision-safe across bugs sharing a basename) - `bundleExhibitAsset(kind, name, srcAbs)` is the shared direct-copy bundler for kinds whose source is an archive original (**intent, bugs**). **reality** keeps its own ledger+sha256 cache staging (a normal build must never launch a browser) but writes to the same `assets/reality/` convention. - A future **gallery** kind (owner-curated individual gallery picks promoted to the public wall) drops straight into this scheme — `assets/gallery/<name>` — no new mechanism. **Why this scheme and not a flat `assets/exhibit/`:** intent and reality already ship at `assets/intent/` and `assets/reality/`; a flat relocation would MOVE those published addresses. The scheme formalises the existing per-kind layout instead of breaking it. --- ## 2. THE PUBLIC / PRIVATE BOUNDARY ### PUBLIC (self-contained, served-render-guarded) - The curated exhibit pages: **`intent-vs-reality.html`, `bugs.html`** — the set named by `PUBLIC_EXHIBIT_PAGES` in the generator. - Their in-bundle assets: `assets/intent/`, `assets/reality/`, `assets/bugs/`. - Shared, self-contained chrome: `assets/robco.css`, the search shards, the lobby, growth, the room ACCOUNTS (prose), the doc pages (raw bytes, in-site). ### PRIVATE-archive-only (excluded from the public build) - **Room galleries** — `gallery-*.html` (429 imgs / 172 MB). Each cell is a `href(TMP_DIR, …planning/…)` embed that resolves only against the raw archive on disk. **Never bundled.** - **Every out-of-site "OPEN ORIGINAL / OPEN PROTOTYPE" anchor link** — these point at raw archive originals (`../../planning/…`, `../../library/…`) that do not exist in a public tree. Present at: `generate.mjs` prototype cards (`OPEN PROTOTYPE →`), sparse-room holdings (`OPEN →`), and the standing-tools file list. They are private "open the raw source" affordances. ### What is already TRUE today The escape breaks **nothing** right now: the private archive is opened as local files, where every `../../` ref resolves. It is strictly a *future* publication concern — which is exactly why the fix is designed with the public build, not retrofitted onto the private one. --- ## 3. What is IMPLEMENTED now (bounded, shipped this pass) 1. The unified exhibit-asset scheme (§1) — `bundleExhibitAsset()`, `EXHIBIT_ASSET_KINDS`, `bugPhotoName()`; intent bundling refactored onto it. 2. **Bug Museum photos bundled** — `bugs/*/record.md` provenance photos now copy into `assets/bugs/` with path-derived names and render from in-site paths (was `../../planning/…`, which 404'd when served). 3. **Served-render assertion scoped to the public set** — `assertServedImages()` runs over `PUBLIC_EXHIBIT_PAGES` (`intent-vs-reality.html`, `bugs.html`), serving the tree over a real localhost origin and GET-ing every `<img>`. It **passes without the 172 MB** because the private galleries are not in the set. It deliberately does NOT guard the galleries — guarding them would (correctly) fail while they escape, which is the whole point of keeping them private. *Verified:* two consecutive regens byte-identical; internal links 0 broken; served images 8/0 broken (6 intent-vs-reality + 2 bug photos); no browser launched. --- ## 4. The `--public` variant — BUILT (`museum/publish.mjs`) `node museum/generate.mjs --public` (re)builds the private site first (so it is current — no stale race with the last sync), then DERIVES `museum/public/` from it via `derivePublicTree()`: 1. **Copies the private site EXCEPT `gallery-*.html`** (the 20 raw image-grid pages / 172 MB stay private). Rooms remain as public pages; their `OPEN GALLERY · N images` link is neutralised to a `.mz-private` span ("held in the private archive"), not a dead link. 2. **Ref-driven transform of every page** (non-script segments only): each `href`/`src` is classified — in-tree → kept; an archive prototype `.html` → **bundled** into `assets/prototypes/<sha8-basename>` and rewritten in-site; anything else out-of-tree/excluded → **neutralised** (anchor stripped to a `.mz-private` span). This is exhaustive by construction, so ZERO out-of-site refs can survive. 3. **Emits `public-manifest.json`** (pages, excluded galleries, asset counts — stable/sorted). 4. **THE GATE — `assertPublicSelfContained()`**: serves the whole `museum/public/` tree over a real localhost origin and HTTP-GETs EVERY href + src on EVERY page (deduped), asserting **zero** broken / out-of-site refs. The generalisation of `assertServedImages` to publish scope — the real self-containment guarantee. On any breakage it throws (public staging NOT swapped). 5. **Atomic swap** into `museum/public/`; **ritual-not-gate** (throws → main()'s catch → exit 0, prior public tree intact). **Owner sub-decisions (resolved 2026-07-23):** prototypes **PUBLIC** (28 `.html` / 1.2 MB bundled); rooms **PUBLIC minus their image grids**; output to a **`museum/public/` staging subtree** (committed; later populates the separate public repo). The future `assets/gallery/` kind (§1) remains the path for individually curating gallery images onto the public wall — never un-privating the raw galleries. *Verified:* excluded 20 galleries; bundled 28 prototypes; neutralised 297 out-of-site anchors; served-link check over the whole tree = 0 broken; an independent scan finds 0 escaping/broken refs across 4399 local refs; two `--public` runs byte-identical (reproducible). --- ## 5. NORMAL builds vs `--public` A normal `node museum/generate.mjs` NEVER touches `museum/public/` — it is a ritual like `--capture`, refreshed only on `--public`. So a normal build/regen leaves the committed public tree clean, and `--public` reproduces it byte-identically. --- ## 6. NEXT P2 step — the EXPOSE layer (NOT built here) The self-contained public TREE is done; exposing it is the next step and is deliberately out of scope this session. It runs AS A TRANSFORM OVER `museum/public/`, after it is verified self-contained and before it populates the public repo: - **Name-substitution** — owner's real name → `zerckzzy`, with a **fail-closed guard**: a post-scan that ABORTS the expose if the real name (or any configured secret) survives anywhere in the tree. *Plug-in point:* a text pass in `museum/publish.mjs` `transformPage()` (already isolates non-script segments) OR a separate pass over `museum/public/` right before the publish push. Marked in the `publish.mjs` header. - **Credential / token scanning** — a fail-closed scan of the whole tree for keys/tokens/emails before any push. - **The `Robco-Exhibit` public repo + Cloudflare + the verify-private-then-expose sequence** — stand up the separate public repo (this archive can NEVER be public), push the substituted+scanned tree, wire hosting. --- ## 7. What this build explicitly did NOT do - Did **not** bundle the 172 MB galleries (the curation law; Option B). - Did **not** run the EXPOSE layer (§6) — no name-substitution, no credential scan, no public repo, no publish. Self-contained tree only. - Did **not** move existing `assets/intent/` or `assets/reality/` addresses.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).