Write your first reproduction
// GUIDE · WRITE A RECIPE
Author one Layer 1 recipe that runs in a browser tab.
Copy the nearest existing recipe, swap the slug and the reproduction script, and you have a Vivarium-compatible reproduction page running locally. From there, the choice is upstream PR or self-publish on a fork.
// 0 · WHAT THIS GUIDE COVERS
A minimal Layer 1 (in-browser WASM) recipe.
Layer 1 pulls the upstream code into the browser via
Pyodide,
Ruby.wasm, php-wasm, or the Rust
wasm32-wasip1 target, runs the reproduction script the
moment the page loads, and returns a verdict. No install, no server —
so a Layer 1 recipe is the lowest-cost first contribution to Vivarium.
Layer 2 (Docker) and Layer 3 (record-replay) have their own niches in architecture, but if you are writing your first recipe, start at Layer 1.
This guide ends at "I have a working recipe locally." Where to publish it is a separate two-way fork: Run on your own fork or a PR to the upstream repo.
// 1 · PICK A BUG
Slug shape: <project>-<issue>.
If the upstream bug has an issue tracker entry, the slug is
<project>-<issue> (e.g.
pandas-56679). Otherwise, a descriptive kebab-case
string (e.g. bash-local-shadows-exit) works. The slug is
the directory name and also the Manifest v1 slug field.
A Layer 1-friendly bug usually has these properties:
- No external I/O (no network, no filesystem, no subprocess).
- The verdict collapses to one boolean (e.g. "are these two values equal?").
- The required libraries ship inside the WASM build of the runtime (Pyodide / Ruby.wasm / php-wasm bundled package list).
// 2 · CLONE AND COPY A TEMPLATE
Copying the closest existing recipe is the shortest path.
For an upstream PR, clone aletheia-works/vivarium directly. To self-publish, follow the fork guide and clone your fork.
mise install at the repo root pulls bun and the rest. For Layer 1 specifically, also run bun install from src/layer1_wasm/.
Pick one in the same language: Python → src/layer1_wasm/pandas-56679/, Ruby → ruby-21709/, PHP → php-12167/, Rust → regex-779/. Copy the whole directory: cp -r src/layer1_wasm/pandas-56679 src/layer1_wasm/<your-slug>.
// 3 · EDIT INDEX.HTML
Three swaps: title, heading, lede.
Keep the structure of the copied index.html; replace
these three to match the bug:
<title>— tab title (e.g.Vivarium · Reproducing <project>#<issue>).<h1>— heading. Link to the upstream issue.<p class="lede">— one-to-two-sentence summary.
Do not touch:
<meta name="vivarium-contract" content="v1">, the
#verdict element, or the link to
../_shared/style.css. Those carry Contract v1, and
removing any of them breaks machine-readable verdict capture.
The authoritative spec lives at Contract v1. The allowed values for
data-verdictand the schema for theVIVARIUM_RESULTenvelope come from there.
// 4 · EDIT REPRO.TS
Swap the reproduction logic; keep the helpers.
The copied repro.ts imports from
../_shared/loader.js and
../_shared/verdict.js. Leave that frame alone and
swap only the reproduction body. For a Python
recipe:
const REPRO_CODE = `
import pandas as pd
# ↑ minimum code that exercises the upstream bug
result = {
"left": ..., # value 1
"right": ..., # value 2
"mismatch": <left> != <right>,
}
result
`.trim();
The helpers handle the verdict wiring: if the comparison is
true (bug reproduced), call
setVerdict("reproduced", "..."); otherwise
setVerdict("unreproduced", "...").
setResult({ contract: "v1", bug, runtime, result, timing })
writes the VIVARIUM_RESULT envelope.
// 5 · RUN IT LOCALLY
bun run build → static server → open in browser.
# from src/layer1_wasm/
bun install
bun run build # repro.ts → repro.js
# serve the recipe directory directly
python -m http.server -d src/layer1_wasm/<your-slug> 8765
# open http://localhost:8765/ in a browser
The page loads, the upstream library streams in from the CDN, and
the verdict badge moves from pending to either
reproduced or unreproduced. In DevTools,
check that VIVARIUM_VERDICT and
VIVARIUM_RESULT agree with the badge — that's
the Contract v1 surface.
Pyodide / Ruby.wasm / php-wasm are all configured to avoid
SharedArrayBuffer, so COOP/COEP headers are not required. A plain static server (python -m http.server,bunx serve) is enough.
// 6 · NATIVE RE-VERIFICATION (OPTIONAL)
Show the same bug reproduces outside the browser.
A reproduction observed via Pyodide / Ruby.wasm is more convincing when it also reproduces on a native CPython / MRI Ruby. For Python, uv + PEP 723 inline metadata gives you a single-file companion:
# /// script
# requires-python = ">=3.13"
# dependencies = ["pandas==2.3.3"]
# ///
import pandas as pd
# … same reproduction logic …
mise exec uv -- uv run repro.py spins up an
ephemeral venv and runs it. Ruby, PHP, and Rust use the same
pattern — existing recipes' repro.rb /
repro.php / Cargo.toml are good
starting points.
// 7 · REGENERATE recipes.json
Get your recipe into the gallery.
recipes.json, which the gallery (/repro/)
and the MCP server consume, is generated from
src/layer*/ at build time. To regenerate locally:
mise run recipes:index
# or
cd docs && bun run generate-index
Check that docs/public/api/recipes.json contains your
slug. With the docs site running locally, the new recipe shows up
as a card on the gallery:
cd docs && bun run dev
# http://localhost:3000/vivarium/en/repro/// 8 · WHERE TO PUBLISH
Upstream PR, fork, or consumer integration.
Open a PR on aletheia-works/vivarium with a Conventional Commit such as feat(layer1): add <slug> reproduction. The CI gate is essentially Contract v1 conformance — anything that turns green is in.
Push the recipe to your own vivarium fork's main, enable deploy via the fork guide, and the recipe appears at <you>.github.io/vivarium/. Sharing is just sharing the URL.
If you only want to watch a recipe (no new authoring), you don't need this guide — see Integrate with your own repo, which uses Manifest v1 plus the reusable consumer-workflow.
If a step in this guide fails, that's a bug in this guide — file an issue with the step number. Where readers stall sets the priority for the next docs revision.