コンテンツにスキップ

ADR-29 — ブラウザプレイグラウンド

ステータス: Accepted、2026-05-23; v0.1.10〜0.1.11で実装

ブラウザベースのRigorプレイグラウンド——リアルタイム診断とannotateスタイルの型コメントを表示するテキストエディタ——を構築し、どのようにホストすべきかという決定を記録する。2つのアプローチを評価した: 完全にブラウザ内のWASMランタイム(ruby.wasm)と、静的サイトをフロントエンドとするサーバーサイドAPI。サーバーサイドAPIが採用された短期パスである;具体的なゲート条件のセット(WD6)が、ブラウザ内WASM完全移行が実行可能になる時期を定義する。サーバーサイドAPI + 静的フロントエンドはplugins/rigor-playground/プラグインとrigor playgroundコマンド(ローカルサービング、WD4 / ADR-32 WD10に従いrigor-rbs-inlinerequire_magic_comment: falseでロード)として出荷された;それをCloudflare Pages / Fly.ioへデプロイするのはops作業であり、ruby.wasm移行はWD6にゲートされたままである。

修正2026-05-25: WD4がデフォルトのプラグインセットを空からrigor-rbs-inline有効(ADR-32のWD10に従い、require_magic_comment: falseで)へ変更した。これにより、# @rbs形コメントを含む貼り付けスニペットが最初のリクエストからインラインRBSとして解析され、ユーザー側の設定が不要になる。

修正2026-05-29: ADR-34のWD7により、リクエストごとのサンドボックスはseverity_profile: strict(または同等のルールごとのオーバーライド)を設定し、新しいcall.unresolved-toplevelルールがfoo 1のような貼り付けスニペットで発火するようにする。balancedデフォルトを継承するとルールが:warningにマッピングされ——表面化はするが、プレイグラウンドとのユーザーの最初のインタラクションになる可能性が最も高い例を過小評価することになる。

公開されたプレイグラウンドにより、ユーザーは何もインストールせずに任意のスニペットに対してRigorを試せる。目標とするエクスペリエンスは、左側にMonacoまたはCodeMirrorスタイルのエディタ;右側(またはインラインの型コメントとして)にrigor annotateの出力とrigor check --format jsonからの診断ストリームがあり、キーストロークまたはデバウンスティックごとに更新される。

ホスティング目標はCloudflare Workers / Pages: 静的フロントエンドで運用するオリジンサーバーがなく、リクエストごとのインフラコストがゼロで、グローバルな低遅延配信が可能。この目標により、ランタイムの選択が重要になる——Rigorエンジンがユーザーのブラウザ(WASM)で動作するかバックエンドサービスで動作するかが、完全な静的ホスティング目標が達成可能かどうかを決定する。

オプションA — 完全なruby.wasm(ブラウザ内WASM)

Section titled “オプションA — 完全なruby.wasm(ブラウザ内WASM)”

ruby.wasmはWebAssemblyにコンパイルされたRubyインタープリタを組み込む。フロントエンドページはRubyランタイム + すべてのgemソース + RigorのYAMLデータカタログを同梱し、解析は完全にブラウザ内で実行される——キーストロークごとのネットワーク往復なし、運用するバックエンドなし。

このADR時点(2026-05-23)のブロッカー:

  1. Ruby 4.0 WASMビルドがないruby.wasmはRuby 3.x系の本番ビルドを提供する。Rigorのgemspecはrequired_ruby_version = [">= 4.0.0", "< 4.1"]をピン留めする(ADR-27 WD7——ruby/rbsが最新Rubyを追跡し、ADR-15のRactorモデルが最近のランタイム機能を必要とするため、Rigorはlatest-Ruby専用を維持する)。このピン留めを緩めることはWD7の根拠と矛盾するため、実行可能な回避策ではない。Ruby 4.0 WASMターゲットはまだ存在しない;出荷される際は本番品質になる前に実験的ビルドとして届くと思われる。

  2. C拡張の依存関係。Rigorが依存するパーサprismと型環境レイヤーrbsの両方がC拡張を持つ。これらは標準のruby.wasmランタイムバンドルには含まれていない。WASMでのビルドにはEmscriptenツールチェーンとパッチ作業が必要だが、いずれの上流プロジェクトもruby.wasmターゲット向けに現在メンテナンスしていない。これは設定の選択ではなく、それ自体が独立したエンジニアリングプロジェクトである。

  3. キャッシュレイヤーのflocklib/rigor/cache/store.rbはアトミックなキャッシュ書き込みを守るためにFile::LOCK_EXflock(2))でアドバイザリーロックを取得する(ADR-6)。flockはWASMサンドボックスではサポートされていない(WASI/Emscriptenの仮想ファイルシステムはPOSIXファイルロックを実装していない)。プレイグラウンドはキャッシュをno-opにスタブできるが、これは専用のコードパスまたはビルド時抽象化レイヤーを必要とする。

  4. Cloudflare Workersのバンドルサイズ。ruby.wasmランタイムバイナリは約15 MB;gemソースとRigorの740 KBのYAMLビルトインカタログを追加すると、1 MBのWorkerスクリプト制限を大幅に超える(有料プランではWASMモジュール制限が25 MBに上がるが、スクリプト + WASM + アセットの全体バンドルは依然タイトなフィットで無料ティアオプションではない)。

  5. Ractor(ADR-15)。v0.1.8で出荷したフォークベースのワーカープールはWASMでは無効化される;単一スニペットを解析するプレイグラウンドは並行性を必要としないため、シングルスレッド実行パスで対処可能だが、メンテナンスする条件付きコードパスが増える。

長期的なアップサイド。オプションAは理想的な最終状態であり続ける: リクエストごとのサーバーコストがゼロ、ネットワーク往復の遅延なし、運用またはセキュリティ確保するバックエンドなし。ブロッカーはすべて時間依存——Ruby 4.0 WASMはいつか出荷される;prismはすでにJavaScript WASMビルドを持っており(ウェブベースのエディタで使用される)、公式のRuby WASMビルドは自然なフォローオン;Cloudflareのバンドル制限はruby.wasmチームがよりコンパクトなランタイムを出荷すれば重要性が下がる。WD6はゲート条件を記録する。

オプションB — サーバーサイドAPI + 静的フロントエンド

Section titled “オプションB — サーバーサイドAPI + 静的フロントエンド”

フロントエンドはエディタとレンダリングロジックを含む静的HTML/JSページ(Cloudflare Pages)。解析リクエストはrigor check --format jsonrigor annotateにシェルアウトしてJSONを返す小さなHTTP APIに送られる。バックエンドは別のホスティングサービス(Fly.io、Railway、または同等)にデプロイされ、静的フロントエンドから切り離されて保持される。

利点:

  • 既存のCLIとそのJSON出力契約(contract)で今日動作する。
  • Rigorエンジンへの変更なし——APIはユーザーがローカルで実行するのと同じバイナリの薄いシムである。
  • Cloudflare Pages上のフロントエンドは完全に静的;APIサーバーのみがコンピュートを実行する。
  • リクエストごとの分離は些細に強制される: 各リクエストがTempfileを書き込み、解析を実行し、それを破棄する。

課題:

  • 完全に静的ではない——小さなAPIサーバーを運用する必要がある。
  • 解析リクエストごとのネットワーク往復遅延(デバウンス駆動のプレイグラウンドでは許容可能;ウォームサーバーで100行スニペットに対して< 500 msを目標)。
  • セキュリティ強化が必要: 入力サイズ上限、リクエストタイムアウト、レート制限、悪意ある入力がホストに影響しないようにするサンドボックス化。

オプションC — プロキシとしてのCloudflare Workers

Section titled “オプションC — プロキシとしてのCloudflare Workers”

Cloudflare Workerはリクエストを受け取りバックエンドにフォワードできる。これはバックエンドを排除せずに間接レイヤーを追加する。これ以上評価されない;バックエンドが必要なら、Worker経由でルーティングすることは構造的なメリットなしに複雑さを加える。(WorkersはWASMビルドをホストできるが——Workers経由のオプションA——これはオプションAのブロッカーを継承する。)

オプションB(サーバーサイドAPI + 静的フロントエンド)を短期実装として採用する。オプションAが長期目標;WD6は移行が正当化される前にすべて満たされなければならない3つの条件を定義する。

プレイグラウンドは既存のCLIコマンドをラップする3つのエンドポイントを公開する:

エンドポイントCLI相当レスポンス
POST /checkrigor check --format jsonJSON診断配列
POST /annotaterigor annotate注釈付きソーステキスト
POST /annotate-linesrigor annotate(整形){ 行番号 → 型 }マップ
POST /type-ofrigor type-ofポジションの型文字列

フロントエンドは静的なCloudflare Pagesサイト。バックエンドはFly.io(または同等)上の同じrigortype Rubyプロセス内で実行される最小限のRackアプリケーションで、Pumaスレッドごとに1ワーカー。

WD1 — フロントエンドはCloudflare Pages;バックエンドは別サービス

Section titled “WD1 — フロントエンドはCloudflare Pages;バックエンドは別サービス”

静的フロントエンド(HTML + JS + エディタバンドル)はCloudflare Pagesにデプロイされる。サーバーサイドレンダリングなし、フロントエンド自体のWorkerコンピュートなし。

APIバックエンドは別のデプロイメント(Fly.ioの無料ティアまたはRailway)。Ruby 4.0 + rigortype + 薄いRack/Pumaレイヤーを実行する。バックエンドのCORSヘッダーがPagesドメインからのクロスオリジンリクエストを許可する。

フロントエンドとバックエンドはplugins/rigor-playground/に同居する——静的アセット用のfrontend/とRack/Pumaバックエンドのgemルート。独立してデプロイされる;フロントエンドのRIGOR_API_URLはビルド時に環境変数として注入される。

すべてのエンドポイントはリクエストボディにUTF-8ソースを持つapplication/jsonを受け付ける。すべてのレスポンスはapplication/json

POST /check

リクエスト:

{ "source": "...", "config": {} }

レスポンス(rigor check --format jsonResult#to_hをミラー):

{
"diagnostics": [
{
"path": "<playground>",
"line": 3,
"column": 5,
"rule": "call.undefined-method",
"message": "...",
"severity": "error"
}
],
"error_count": 1,
"success": false
}

POST /annotate

リクエスト: { "source": "..." } レスポンス: { "annotated": "# 型コメント付き注釈済みソース..." }

POST /annotate-lines(修正2026-05-25)

/annotateと同じ入力。/annotateの出力をクライアント向けに整形したもの。#=> dump_type:コメントの文法を再パースせずに型注釈をインレイヒントスタイルのオーバーレイとしてレンダリングしたいクライアント(スライス(slice)3フロントエンドの「型を表示」トグル)向け:

リクエスト: { "source": "..." } レスポンス: { "annotations": { "1": "String", "5": ":asc | :desc" } }

マップは1ベースの行番号をキー(JSONオブジェクトのキーは文字列)とし、値は型コメントのペイロード(対応する/annotate出力の#=> dump_type:以降)。注釈のない行はマップに含まれない。両エンドポイントは同じrigor annotate起動を共有し、/annotate-linesは純粋に表示バリアントである。

POST /type-of

リクエスト: { "source": "...", "line": 5, "column": 12 } レスポンス: { "type": "String" }

/checkconfigフィールドは初期は無視される;プレイグラウンドUIに.rigor.ymlオプションのサブセット(例: severity_profile:)を公開する将来のスライス向けに予約されている。

フロントエンドエディタはMonacoではなくCodeMirror 6。どちらもRubyシンタックスハイライトをサポートするが、CodeMirror 6が選ばれた理由:

  • バンドルサイズ。Ruby言語サポートを含む最小限のCodeMirror 6バンドルはgzip圧縮で約100 KB。Monacoのフルバンドルはgzip圧縮で約2 MB;Rubyサポートにはコミュニティパッケージが必要で、言語サーバー統合はWorkerベースの言語サーバープロセスを期待する(ここでは不要——診断はRigor APIから来る)。
  • 埋め込み可能性。CodeMirror 6はライブラリとして設計されており、/checkレスポンス駆動の波線下線デコレーションと/annotateからのインライン型コメント注釈を追加するのが簡単。
  • エディタ自体のビルド時コンパイル不要。Monacoはアセットパイプラインにmonaco-editorの別コピーを必要とするが、CodeMirror 6パッケージは標準バンドラー(またはバンドルレスセットアップ用のCDNインポート)でクリーンに統合される。

/checkからの診断はCodeMirrorのlintマーカー(赤い下線 + ホバーツールチップ)としてレンダリングされる。/annotateからの型注釈は、ボタンでトグルされるdiffスタイルの「ゴーストテキスト」オーバーレイとしてレンダリングされる。

WD4 — バックエンドサンドボックスとリクエスト分離

Section titled “WD4 — バックエンドサンドボックスとリクエスト分離”

各HTTPリクエストは分離して処理される:

  1. ソースコードはリクエスト開始時に作成されたTempfile/tmp/rigor-playground-*.rb)に書き込まれ、ensureブロックで削除される。
  2. Rigor::Analysis::Runner(または同等のエントリーポイント)はそのファイルに対してインプロセスで直接呼び出される——シェルexecなし、つまりパスからのインジェクションリスクなし。
  3. 永続的なオンディスクキャッシュ(ADR-6)はプレイグラウンドバックエンドでは無効化される。各リクエストはRBS環境をゼロから構築する。これによりクロスリクエストのキャッシュ汚染が回避され、flock依存関係が除去される;遅延コスト(RBS環境ブート約100 ms)はウェブプレイグラウンドには許容可能。
  4. リクエストごとにハードな10秒タイムアウトで暴走する推論を終了させる(意図的に敵対的なスニペットが高コストな再帰をトリガーする可能性がある)。
  5. 入力はソーステキスト64 KBに上限がある。UIはクライアントサイドでこれを強制;バックエンドはサーバーサイドで413レスポンスで強制する。

バックエンドはデフォルトでrigor-rbs-inlineをロードする(ADR-32のWD10に従い)。これにより、# @rbs形コメントを含む貼り付けスニペットはページが読み込まれた瞬間からインラインRBSとして解析される——プラグイン設定の探索も、# rbs_inline: enabledマジックコメントの入力も不要。プラグインのrequire_magic_comment:設定キーはfalseに設定されている。プレイグラウンドは単一バッファの探索サーフェス(surface)であり、WD2が緩和しようとしている複数ファイルのプロジェクト上の摩擦が存在しないためである。

プレイグラウンドの.rigor.yml(バックエンドに埋め込まれる)は固定の設定:

plugins:
- id: rigor-rbs-inline
config:
require_magic_comment: false
severity_profile: strict

他のプラグインはデフォルトではロードされない。

WD5 — バックエンドデプロイメント: Fly.ioの無料ティア(シングルマシン)

Section titled “WD5 — バックエンドデプロイメント: Fly.ioの無料ティア(シングルマシン)”

バックエンドはシングルのFly.io Machine(shared-CPU-1x、256 MB RAM)としてデプロイされる。2スレッドのシングルPumaワーカーで、期待されるプレイグラウンドトラフィックには十分。Fly.ioの無料枠がこの構成をコストなしでカバーする。

トラフィックがスケーリングを要求する場合、Fly.ioのオートスケーリングまたは別ホストへの移行は簡単——バックエンドはホストへのステートフルな結合のないプレーンなRackアプリである。バックエンドのデプロイメントマニフェスト(plugins/rigor-playground/fly.toml)はリポジトリにコミットされる。

レート制限(IPごとに50リクエスト/分)はfly.tomlhttp_service.rate_limiting経由でFly.ioプロキシレイヤーで強制される。4インフライトリクエストのグローバル同時実行制限により、バーストがRBS環境ブート中にシングルマシンのCPUを独占するのを防ぐ。

WD6 — ruby.wasm移行ゲート(3つの条件)

Section titled “WD6 — ruby.wasm移行ゲート(3つの条件)”

オプションBからオプションA(完全にブラウザ内WASM)への移行には、以下の3つの条件がすべて同時に満たされる必要がある:

  1. Ruby 4.0の公式WASMビルドが本番品質である。「本番品質」とは: ruby/ruby.wasmの下で公開され、上流のテストスイートをパスし、CDNから利用可能またはstable npmパッケージとして利用可能であること。実験的またはナイトリービルドはこの条件を満たさない。

  2. prismrbsのWASMパッケージが利用可能である。両方のgemが公式WASMビルドを提供している必要がある(ruby.wasmランタイムにバンドルされているか、別途ロード可能な.wasmモジュールとして)、かつWASMターゲット下で自身のテストスイートをパスする。

  3. RigorのテストスイートがWASM下でパスする。ruby.wasmランタイム内で(flockとフォークベースのワーカープールをスタブして)make testを実行するCIジョブが、テスト数のリグレッションなしにパスする必要がある。このゲートは、WASMサンドボックスにないPOSIX意味論を暗黙に仮定するエンジンコードを捕捉する。

3つの条件がすべて満たされるまで、サーバーサイドAPI(オプションB)が本番バックエンドである。ruby.wasmプロジェクトがRuby 4.xのアナウンスを出荷するとき、またはprismがWASMディストリビューションを公開するときにWD6の再評価を行うべきである。

WD7 — プレイグラウンドでのrigor annotate出力

Section titled “WD7 — プレイグラウンドでのrigor annotate出力”

rigor annotateは今日、型コメントを# :: Type注釈として(式行ごとに1つ)付加したソーステキストを出力する。プレイグラウンドはこれをトグル可能な「注釈ビュー」としてレンダリングする——「型を表示」をクリックすると、エディタコンテンツが注釈付きソースに置き換えられる;「編集」をクリックすると元に戻る。注釈ビューは読み取り専用;編集しようとすると自動的に編集モードに戻る。

将来のスライスでは、型注釈をエディタコンテンツを置き換える代わりにCodeMirrorのインレイヒント(式の後にインラインで、ゴーストテキストとしてスタイリング)としてレンダリングするかもしれない——これは、CodeMirrorのインレイヒントAPIがエコシステムで安定し、トグルUXが十分かどうかについてプレイグラウンドが実際のユーザーフィードバックを得るまで先送りされる。

このADRではスライスはスケジュールされていない。プレイグラウンドは0.2.x評価ラインをブロックしない新しい並行トラックである。

スライススコープ
1plugins/rigor-playground/ — Rackアプリケーション、/checkエンドポイント、リクエストごとのTempfile分離、10秒タイムアウト、64 KB上限、固定.rigor.yml(WD4 / ADR-32 WD10に従いrequire_magic_comment: falserigor-rbs-inlineをロード)。Fly.ioにデプロイ。スライス1はADR-32スライス1(source_rbs_synthesizer:マニフェストフィールドとrigor-rbs-inlineプラグインの存在)にゲートされる。
2plugins/rigor-playground/frontend/ — CodeMirror 6、デバウンスされた/check呼び出し、lintマーカー、Cloudflare Pagesデプロイ設定。
3/annotateエンドポイント + フロントエンドトグルビュー。
4/type-ofエンドポイント + フロントエンドホバー統合。
5(需要駆動)WD6条件が満たされたときのruby.wasm移行。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.