


TypeScriptモノレポの双方向プロトコルSDKを、公開APIを壊さずに保守する
クライアントとサーバーのどちらからもリクエストを送る、型の厳しいSDKを複数パッケージで出している時に使う。外へ出すエクスポートを意図して選び、新しい操作をケイパビリティのチェック付きで両側に足し、破壊的変更を記録し、サンプルをテストとして動かす。
このAIルールを使うには
## 変更がどの層に入り、公開APIに触れるかを先に見る
本ガイドは、プロトコル SDK を公開するマルチパッケージ構成の TypeScript ワークスペース内での作業を対象とする。構成要素は、共有される内部コア、クライアントパッケージ、サーバーパッケージ、独立したスキーマパッケージ、そして薄いフレームワークアダプタまたはランタイムアダプタのパッケージ群である。厳密な型チェック、ES モジュール、および単一のパッケージマネージャーによって駆動されるワークスペース全体のスクリプトを前提とする。
あらゆる判断を支配する性質が二つある。第一に、公開されたサーフェスは契約である。パッケージのエントリーポイントから到達可能なものはすべて公開 API であり、安易に変更してはならない。第二に、このプロトコルは双方向である。すなわち、どちらのピアもリクエストを発行しうるため、一方の側での変更は通常、他方の側に鏡像となる対応義務を伴う。
編集に着手する前に、その変更がどの層に属するのか、そして公開サーフェスに触れるのかどうかを見極めること。触れる場合は API 変更として扱い、スタイル規約だけでなく、以下に述べるサーフェス規約と移行規約を適用する。
## 作業中は1つのテストだけ回し、終える前に全部のチェックを回す
ワークスペースは、リポジトリ全体を対象とする集約スクリプトを提供している。依存関係のインストール、全パッケージのビルド、リントとフォーマットのチェック、リントとフォーマットの自動修正、全パッケージの型チェック、テストスイート全体の実行、そして型チェックとリントを組み合わせた統合チェックスクリプトである。
反復作業中はループを狭めること。パッケージマネージャーのフィルタフラグを使って単一パッケージのテストスクリプトを実行し、ファイルパスを渡して単一のテストファイルを実行し、名前フィルタを渡して単一のテストケースを実行する。緊密な反復のためのウォッチモードのスクリプトも用意されている。
ワークスペース全体の実行は、作業完了を宣言する直前の段階に留め、そのときに統合チェックとテストスイートの双方を実行する。型チェックだけでは実行時の振る舞いを証明できない。変更を検証するテストを実際に実行せずに「動作する」と主張してはならない。
## 名前の付け方と書式を、このリポジトリの決まりに合わせる
- 全体を通して厳密な型チェックを行い、エクスポートされる関数には明示的な戻り値の型を付ける。
- クラスと型は PascalCase、関数と変数は camelCase とする。
- ファイル名は小文字とハイフンで構成し、テストファイルは拡張子にテストを示すサフィックスを含める。
- ES モジュールの import を用いる。拡張子の扱いはプロジェクトのモジュール解決設定に従う。バンドラー方式の解決を用いる場合、相対 import に拡張子は付けない。import は論理的にグループ分けする。
- インデントは半角スペース 2 つ、セミコロンは必須、クォートはシングルクォートを推奨する。
- テストは各パッケージ専用のテストディレクトリ配下に置き、テストランナーの include パターンで拾われるようにする。テスト名は内容が分かるように付ける。
- 公開 API にはドキュメンテーションコメントを付ける。インラインコメントは、本当に自明でないロジックに限って用いる。
規約の解釈が曖昧な場合は、周囲のファイルに合わせる。機能変更の中で無関係なコードを再フォーマットしたりリファクタリングしたりしないこと。
## ドキュメントのサンプルは、コンパイルされるサンプルファイルから取り込む
ドキュメンテーションコメント内に手書きされたスニペットは、コンパイルされることがないため、気付かないうちに陳腐化する。代わりに、ソースの他の部分と同様にコンパイルされ型チェックされる、対になるサンプル用モジュールからサンプルコードを取り込む。
対になるファイルは、対象モジュールの隣に、対応する名前で配置する。そのファイル内の名前付きリージョンを、ファイルとリージョンを指すソース属性によってドキュメントのコードフェンスから参照する。リージョン名は予測可能な規則に従う。すなわち、エクスポートされるシンボル名にバリアントを付けたもの、またはクラス名とメソッド名にバリアントを付けたものである。リージョン参照を省略した場合はファイル全体が取り込まれ、これはあらゆるファイル種別で機能する。
サンプルを編集したらスニペット同期スクリプトを実行し、最新の内容をドキュメンテーションコメントおよび Markdown ページへ反映させたうえで、同期後の出力を変更内容と一緒にコミットすること。
## 外へ出すものは名前を付けてエクスポートし、ルートはランタイムに依存させない
すべてをエクスポートし、private として指定され、ワークスペース内の兄弟パッケージからのみ利用される内部バレルと、型・エラークラス・定数・ガード・共有基底クラスを公開する精選済みの公開エクスポートモジュールとを分離する。パッケージのエントリーポイントは、この精選済みモジュールを再エクスポートし、それに加えて自身の明示的な名前付きエクスポートを提供する。
以下の規約は交渉の余地がない。
- エントリーポイントおよび精選済みモジュールでは、明示的な名前付きエクスポートを用いる。そこでワイルドカードによる再エクスポートを行ってはならない。
- パッケージのエントリーポイントにシンボルを追加することは、それを公開 API にすることを意味する。意図がある場合にのみ行うこと。
- 内部ヘルパーは内部バレルに留め、精選済みモジュールやエントリーポイントに追加してはならない。
- ブラウザやエッジランタイムのバンドラーが利用できるよう、各パッケージのルートエントリーはランタイム中立に保つ。モジュールグラフが推移的にプロセス生成や生ソケットといったプラットフォーム固有の組み込み機能に到達するエクスポートは、名前付きのサブパスエクスポートの背後に置き、ルートバレルがそれらを含まないことを検証するテストで担保しなければならない。
- バリデーションライブラリの型を含まないことを約束する公開サーフェスには、それらの型を持ち込まない。代わりに、専用のスキーマパッケージから生のランタイムスキーマを公開し、兄弟パッケージにはスキーマの私的な複製を持たせるのではなく、そのパッケージを実際の外部依存として解決させる。
## スキーマ層を土台にし、トランスポートは差し替えられるようにする
SDK は三つの層に整理する。
1. 型・スキーマ層 — 仕様に整合したプロトコルメッセージ型、バリデーションスキーマ、定数。これが唯一の信頼できる情報源であり、他のすべての層はここから派生する。
2. プロトコル層 — メッセージのルーティング、リクエストとレスポンスの対応付け、ケイパビリティのネゴシエーション、トランスポートのライフサイクルを扱う抽象基底クラス。双方のピア実装がこれを継承するため、双方向の振る舞いは構造上対称となる。
3. 高水準 API — 型付きメソッドを公開するクライアントクラスおよびサーバークラスに加え、ツール・リソース・プロンプトの登録を簡略化するより高水準のサーバーラッパー。
トランスポートは単一のインターフェースの背後にある、独立した差し替え可能な層である。リモートピアに推奨されるストリーミング HTTP トランスポート、後方互換性のために残されたレガシーなイベントストリーム型トランスポート、そしてローカルで起動されるプロセスとの統合に用いる標準入出力トランスポートがある。バリデーションも同じ思想で差し替え可能であり、既定のバリデータに加え、制約のあるランタイムと互換性のある代替実装が用意されている。
フレームワークアダプタおよびランタイムアダプタのパッケージは、薄い統合層である。これらがプロトコル機能を追加することは決してあってはならない。プロトコルの振る舞いを変えるものはすべてコア層に属する。
## 両方のピアで、リクエストが出ていく流れと入ってくる流れをつかむ
送信側。高水準メソッドは基底のリクエストメソッドに処理を委譲する。基底のリクエストメソッドは、一意のメッセージ識別子を割り当て、そのメソッドが必要とするケイパビリティをリモートピアが宣言していることを確認し、応答待ちのハンドラーを登録し、シリアライズされたリクエストをトランスポートへ引き渡し、対応するレスポンスの到着によって解決されるのを待機する。
受信側。トランスポートが受信メッセージを表出させ、基底のルーティングがそれをリクエスト・レスポンス・通知のいずれかとしてディスパッチする。リクエスト処理では、メソッド名をキーとしてハンドラーを検索し、コンテキストオブジェクトを構築し、サブクラスにトランスポート固有の情報でそのコンテキストを補強させ、ハンドラーを呼び出し、その結果をレスポンスとしてトランスポート経由で返す。
コンテキストの構造。セッション識別子、メッセージ識別子・メソッド名・リクエストメタデータ・キャンセル用の中断シグナル・関連するリクエストや通知をピアへ送り返すためのヘルパーを保持するリクエストグループ、そしてトランスポートが提供する場合に検証済みの認可情報を保持する任意のトランスポートグループから成る。サーバー側のコンテキストには、ロギング、ユーザー入力の引き出し、モデルサンプリングの各ヘルパーが追加される。
ケイパビリティの強制は双方向で行われ、さらにハンドラー登録時にも行われる。そのため、ピアが自ら宣言していない機能に対してハンドラーを登録することはできない。
## 新しいプロトコル操作を、両側のケイパビリティチェック付きで足す
1. 型層でリクエストパラメータのスキーマと結果のスキーマを定義する。
2. クライアントまたはサーバーのケイパビリティ型に、対応するケイパビリティフラグを追加する。
3. その操作を開始する側のピアに送信メソッドを実装する。
4. そのピアのケイパビリティチェック内にケイパビリティの検証を追加する。
5. 反対側のピアに受信ハンドラーを登録する。
6. その操作に必要性がある場合、簡略化されたサーバー API に高水準のラッパーを追加する。
7. 双方の側にテストを追加し、その操作が利用者向けであれば実行可能なサンプルも追加する。
手順 2 と手順 4 は最も飛ばされやすく、かつ飛ばした場合の代償が最も大きい。ケイパビリティのネゴシエーションを欠いた操作は、初期化時に明確に失敗するのではなく、その操作を実装していないピアに対して実行時に失敗することになる。
## 破壊的変更を、何が・なぜ・どう移るかの3点で記録する
破壊的変更はすべて、移行ガイドの該当サブシステムのセクション内に記録する。関連するセクションを探して加筆することとし、独立した新しいセクションを末尾に追加していく形は避ける。並列した一回限りの項目の集まりでできたガイドは、まさにそれを必要とする人が読むときに判読不能になる。単一のリリースノートファイルで完結する変更は、そちらに記載する。
各項目には、何が変わったか、なぜ変わったか、どのように移行するか、の三点を必ず記す。
機械的なリネームは、自動移行ツールのマッピングファイルに記述する。リネーム表を散文中に再掲するのではなく、マッピングへリンクすること。複製された表は、実際にリネームを実行するツールとの間で内容がずれていく。
判断の指針。加算的な変更を優先する。破壊的変更が避けられない場合は、可能な限り 1 サイクルの間は非推奨扱いのまま従来の経路を動作させ続け、静かな挙動変更ではなく明確なコンパイル時エラーとして表面化するように変更を設計する。
## サーバーとクライアントの組のサンプルを、CIでテストとして動かす
サンプルは飾りではなくテストとして扱う。各サンプルはそれ自体が一つのワークスペースパッケージであり、対になるサーバーとクライアントを含む。クライアントは接続し、期待する結果を検証し、不一致があれば失敗ステータスで終了する。ランナーは、設定されたトランスポートとプロトコル世代の組み合わせすべてにわたって各サンプルを実行し、そのジョブは変更ごとの継続的インテグレーションのゲートに組み込まれている。
サンプル共通の下回りは、ルートエクスポートを最小限に保った単一の共有パッケージに置く。すなわち、引数のパース、アサーションのヘルパー、兄弟パスの解決である。デモ用の認可プロバイダーやインメモリのイベントストアといった、より重い任意依存はサブパスエクスポートの背後に配置し、無関係なサンプルがそれらを先読みで読み込まないようにする。サンプルはこの下回りのみを import し、トランスポートはインラインで組み立てる。これにより、各サンプルは隠された初期化処理のラッパーではなく、実際の利用例として読める。
ドキュメントのガイドページにはページごとのスニペット用の対ファイルを用意し、同じスニペット同期の工程で同期する。そのうち実行可能なものは、継続的インテグレーションでも実行される。
