はじめての再現を書く
// GUIDE · 再現を書く
ブラウザだけで動く Layer 1 レシピを 1 つ書く。
既存レシピをコピーしてスラッグと再現スクリプトを差し替えるだけで、Vivarium 互換の再現ページが手元で動く。完成したらアップストリームへ PR を送るか、自分のフォークに置き続けるかを選べる。
// 0 · このガイドが対象にするもの
Layer 1(ブラウザ内 WASM)の最小レシピ。
Layer 1 は Pyodide、 Ruby.wasm、php-wasm、 Rust の wasm32-wasip1 ターゲットなどで上流コードを ブラウザに持ち込み、ページが開いた瞬間に再現スクリプトを走らせて verdict を返す層です。インストール不要・サーバー不要なので、 Vivarium に最初に追加するレシピとして最もコストが低いです。
Layer 2(Docker)の役割は アーキテクチャ に役割が書かれていますが、書き始めるならまず Layer 1 一択です。
このガイドはレシピを書くところまでを対象にします。 その先「どこに公開するか」は 2 分岐で、 自分のフォークで動かす または上流リポジトリへの PR、のどちらかです。
// 1 · 再現したいバグを決める
スラッグは <project>-<issue> 形式。
上流 Issue がある場合は <project>-<issue> (例: cpython-137205)が標準。 ない場合は説明的なケバブケース(例: bash-local-shadows-exit)。 これがディレクトリ名でもあり、Manifest v1 の slug でもあります。
バグを選ぶときは、Layer 1 で完結するか確認するのがコツです:
- 外部 I/O(ネットワーク、ファイル、サブプロセス)を踏まない。
- verdict が 1 つの真偽値に落ちる(例: 「2 つの値が等しいか」)。
- 必要なライブラリが対象ランタイムの WASM ビルドに乗っている(Pyodide / Ruby.wasm / php-wasm の同梱パッケージ一覧)。
// 2 · クローンして雛形をコピーする
一番近い既存レシピを丸ごとコピーするのが最短。
アップストリームに PR したいなら aletheia-works/vivarium を直接クローン、自分のフォークに置きたいなら フォーク手順 でフォークしてからクローンします。
mise install でリポジトリ直下から bun など必要なツールが入ります。Layer 1 用には cd src/layer1_wasm 配下で bun install も走らせてください。
言語が同じレシピを丸ごとコピーするのが楽です。Python なら src/layer1_wasm/cpython-137205/、Ruby なら ruby-21709/、PHP なら <php-slug>/、Rust なら <rust-slug>/。cp -r src/layer1_wasm/cpython-137205 src/layer1_wasm/<あなたの slug> でディレクトリごと複製。
// 3 · index.html を書き換える
差し替えるのは title、見出し、lede の 3 か所。
コピーした index.html の構造はそのまま使い、以下を上流バグに合わせて差し替えます:
<title>— タブに出るタイトル(例:Vivarium · Reproducing <project>#<issue>)。<h1>— 見出し。上流 Issue へのリンクを含めます。<p class="lede">— 1〜2 文でバグを説明する lede。
触ってはいけないもの: <meta name="vivarium-contract" content="v1">、 #verdict 要素、../_shared/style.css へのリンク。 これらが Contract v1 を構成しているので、外すと verdict が機械的に読めなくなります。
Contract v1 の正典は Contract v1 にあります。
data-verdictの取りうる値、VIVARIUM_RESULTenvelope のスキーマは spec ページが正です。
// 4 · repro.ts を書き換える
再現ロジックだけ差し替える。helper はそのまま。
コピー元の repro.ts は ../_shared/verdict.js の verdict helper と、Python レシピなら ../_shared/pyodide-worker-client.js の startPyodideWorker を import している部分が共通骨格です (Ruby / PHP / Rust はそれぞれ ruby_loader.js / php_loader.js / rust_loader.js)。 ここはそのまま残して、再現コード本体だけ差し替えます。
ページを読めるものにするための規則が 2 つあり、どちらも重要です:
- 普通のコードとして書き、起きたことを
printする。 出力ペインにはスクリプトの実 stdout がそのまま出るので、訪問者はなぜその答えになるのかを読めます。ページに拾わせるための裸の式でスクリプトを終わらせないこと — 誰も読みません。 - 機械可読な値は
resultという名前のグローバルに残す(行の並びならresults)。verdict と Contract v1 envelope はここから組み立てられます。
const REPRO_CODE = `
import pandas as pd
# ↑ 上流バグを表す最小コード
left = ... # 比較する値 1
right = ... # 比較する値 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();
driver は毎回の実行前に result を削除します。訪問者がブラウザ上でスクリプトを編集して代入を消した場合、前回の答えではなく unreproduced になります。
verdict 設定は helper が面倒を見ます: 比較が true(バグが再現した)なら setVerdict("reproduced", "...")、そうでなければ setVerdict("unreproduced", "...")。 setResult({ contract: "v1", bug, runtime, result, timing }) で VIVARIUM_RESULT envelope も書き出されます。
Pyodide は全 Python レシピが共有する Web Worker で動くので、起動が ページを固めることはありません。worker ファイルを書く必要もありません。
startPyodideWorkerがそれを起動し、run/evaluate/installを返します。
// 5 · ローカルで動かす
bun run build → 静的サーバ → ブラウザで開く。
# src/layer1_wasm/ から
bun install
bun run build # repro.ts → repro.js を生成
# レシピディレクトリを直接サーブ
python -m http.server -d src/layer1_wasm/<あなたの slug> 8765
# ブラウザで http://localhost:8765/ を開く
ページが開き、上流ライブラリが CDN からロードされ、 verdict バッジが pending → reproduced / unreproduced のどちらかに落ちます。 DevTools コンソールで VIVARIUM_VERDICT と VIVARIUM_RESULT も同じ値が入っていれば、 Contract v1 上は OK です。
Pyodide / Ruby.wasm / php-wasm はいずれも
SharedArrayBufferを使わない構成にしてあるので、COOP/COEP ヘッダは不要です。 普通の静的サーバ(python -m http.serverやbunx serve)で動きます。
// 6 · ネイティブ再検証(任意)
ブラウザ外でも同じバグが出ることを 1 行で確かめる。
Pyodide / Ruby.wasm 経由で観測できたバグが、本物の CPython / MRI Ruby でも同じく再現することを示すと、レシピの説得力が上がります。 Python なら uv + PEP 723 inline metadata で 1 ファイル完結に書けます:
# /// script
# requires-python = ">=3.14"
# dependencies = ["pandas==2.3.3"]
# ///
"""… どんなバグで、このファイルをどう走らせるか …"""
# ← ここの本体は REPRO_CODE と 1 文字も違わない同じテキスト
if mismatch:
print("verdict=reproduced — …", file=sys.stderr)
sys.exit(0)
else:
print("verdict=unreproduced — …", file=sys.stderr)
sys.exit(1)
本体は REPRO_CODE と同一に保ちます。 追加してよいのは PEP 723 ヘッダ、docstring、verdict= 行と exit code だけです。 これがあって初めて native 実行はブラウザ側の検算になり、似ているだけの別スクリプトになりません。 native ファイルにリポジトリの formatter をかけ、その結果を文字列リテラル側にも反映してください。 さもないと次の autofix で 2 つが乖離します。
mise exec uv -- uv run repro.py で ephemeral venv が組まれて走ります。他の言語も考え方は同じですが、仕組みは共有していません:
- Ruby — インタプリタがそのまま走らせる
repro.rbで、PEP 723 に相当するものはありません。mise exec rubyではなくmise run repro:native:rubyで実行してください。 バージョンのピンがタスク側にあるためです。ruby-21709を参照。 - Rust — コンパニオンファイルはありません。
repro.wasmにコンパイルされる crate そのものが native CLI なので、mise run repro:native:rustはページが走らせるのと同じsrc/repro.rsを実行します。<rust-slug>を参照。
mise run repro:native で全部まとめて走ります。 php-wasm のレシピはまだ書かれていないので、雛形にする repro.php は存在しません。 _shared/php_loader.ts は最初の 1 本を書く人のために用意されています。
// 7 · recipes.json を再生成
ギャラリーに自分のレシピを並べる。
ギャラリー(/repro/)と MCP サーバが読む recipes.json は、ビルド時に src/layer*/ をスキャンして自動生成されます。 手元での確認用には:
mise run recipes:index
# あるいは
cd docs && bun run generate
生成された docs/site/public/api/recipes.json に 自分のスラッグが入っていれば成功です。 docs サイトをローカルで動かすと、ギャラリーにカードが現れます:
cd docs && bun run dev
# http://localhost:3000/vivarium/ja/repro/// 8 · どこに公開するか
上流 PR か、自分のフォークか。
aletheia-works/vivarium 上で feat(layer1): add <slug> reproduction のような Conventional Commits で PR を立てます。レビュー基準は既存レシピを見れば分かりますが、最低限 Contract v1 に従っていれば CI が緑になります。
レシピを自分の vivarium フォークの main に push し、フォーク手順 の deploy 設定を有効化すると、自分の <you>.github.io/vivarium/ に並びます。共有は URL を渡すだけ。
自分のプロジェクトリポジトリ側で「このレシピを CI で監視する」だけしたい場合は、レシピを書く必要はありません。自分のリポジトリに統合する に Manifest v1 と consumer-workflow.yml を使う最短手順があります。
手順のどこかで詰まったら、それは write-your-first-reproduction のバグです。詰まったステップ番号を Issue に残してください。