• English
  • 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) has its own niche 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. cpython-137205). 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.

    01
    Pick a clone target.

    For an upstream PR, clone aletheia-works/vivarium directly. To self-publish, follow the fork guide and clone your fork.

    02
    Set up the toolchain via mise.

    mise install at the repo root pulls bun and the rest. For Layer 1 specifically, also run bun install from src/layer1_wasm/.

    03
    Copy the closest existing recipe.

    Pick one in the same language: Python → src/layer1_wasm/cpython-137205/, Ruby → ruby-21709/, PHP → <php-slug>/, Rust → <rust-slug>/. Copy the whole directory: cp -r src/layer1_wasm/cpython-137205 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-verdict and the schema for the VIVARIUM_RESULT envelope come from there.

    // 4 · EDIT REPRO.TS

    Swap the reproduction logic; keep the helpers.

    The copied repro.ts imports the verdict helpers from ../_shared/verdict.js and, for a Python recipe, startPyodideWorker from ../_shared/pyodide-worker-client.js (Ruby, PHP and Rust use ruby_loader.js, php_loader.js and rust_loader.js instead). Leave that frame alone and swap only the reproduction body.

    Two rules make the page readable, and both matter:

    • Write ordinary code and print what happened. The output pane shows the script's real stdout, so a visitor can see why the answer is what it is. Do not end the script in a bare expression for the page to pick up — nothing reads it.
    • Leave the machine-readable values in a global named result (or results for a list of rows). That is what the verdict and the Contract v1 envelope are built from.
    const REPRO_CODE = `
    import pandas as pd
    # ↑ minimum code that exercises the upstream bug
    
    left = ...   # value 1
    right = ...  # value 2
    mismatch = left != right
    
    result = {
      "left": left,
      "right": right,
      "mismatch": mismatch,
    }
    
    print("left ".ljust(10) + str(left))
    print("right".ljust(10) + str(right))
    print()
    if mismatch:
      print("The two disagree.")
    else:
      print("The two agree.")
    `.trim();
    

    The driver deletes result before every run, so a visitor who edits the script in the browser and removes the assignment gets unreproduced rather than the previous run's answer.

    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.

    Pyodide runs in a Web Worker shared by every Python recipe, so booting it never freezes the page — and there is no worker file to write. startPyodideWorker spawns it and returns run / evaluate / install.

    // 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.14"
    # dependencies = ["pandas==2.3.3"]
    # ///
    """… what the bug is, and how to run this file …"""
    
    # ← the body here is the same text as REPRO_CODE, character for character
    
    if mismatch:
      print("verdict=reproduced — …", file=sys.stderr)
      sys.exit(0)
    else:
      print("verdict=unreproduced — …", file=sys.stderr)
      sys.exit(1)
    

    Keep the body identical to REPRO_CODE. Only the PEP 723 header, the docstring, and the verdict= line plus exit code are extra — that is what makes the native run a check on the browser one rather than a second script that merely looks similar. Run the repo's formatter over the native file and mirror whatever it does back into the string literal, or the next autofix pass will drift them apart.

    mise exec uv -- uv run repro.py spins up an ephemeral venv and runs it. The other languages share the idea, not the mechanism:

    • Ruby — a plain repro.rb the interpreter runs; there is no PEP 723 equivalent. Run it with mise run repro:native:ruby rather than mise exec ruby, because the version pin lives on the task. See ruby-21709.
    • Rust — no companion file at all. The crate that compiles to repro.wasm is the native CLI, so mise run repro:native:rust runs the same src/repro.rs the page runs. See <rust-slug>.

    mise run repro:native runs all of them. No php-wasm recipe has been authored yet, so there is no repro.php to copy — _shared/php_loader.ts is in place for whoever writes the first one.

    // 7 · REGENERATE recipes.json

    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
    

    Check that docs/site/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/repro/

    // 8 · WHERE TO PUBLISH

    Upstream PR, fork, or consumer integration.

    01
    Contribute to the upstream catalogue.

    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.

    02
    Self-publish on your fork.

    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.

    03
    Track an existing recipe from your project repo.

    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.

    // NEXT

    Read the glossary →

    The terms used here — slug, verdict, contract, layer — are pinned to fixed meanings in the glossary.

    VIVARIUM IS PART OF ALETHEIA-WORKS · SEE SOURCE ON GITHUB →