


多くの連携先から作業の動きを集め、複数の表示先で見せるアプリを作る
たくさんのツールからイベントを集め、エディタ拡張と単体のサーバーの両方で見せるアプリを作る時に使う。層の依存を一方向に保ち、どの連携先も同じ形のイベントに変え、ほかを直さずに連携先を足せるようにし、状態を安全に保存する。
このAIルールを使うには
## 依存の向きを一方向にし、表示先どうしを互いに読み込ませない
依存関係は、名前を持つレイヤーを通じて一方向にだけ流れるようにする。`core` レイヤーにはプロトコルとインターフェースの定義だけを置き、ランタイム上の副作用は一切持たせない。ランタイムレイヤーは `core` にのみ依存する。各ホストサーフェス——たとえばエディタ拡張用のアダプタとスタンドアロンのコマンドラインサーバー——は `core` とランタイムに依存するが、サーフェス同士が互いをインポートすることは決してない。許可される依存の方向を明示的に宣言し、それを強制する。境界を越えるインポートがひとつでも紛れ込むと、本来は独立して差し替え可能であるべき2つのサーフェスが、気づかないうちに結合してしまうからだ。
### この構造が機能する理由
依存関係を持たない契約レイヤーがあることで、すべてのサーフェスとすべての連携先は、ホスト固有のコードを取り込まずに同じ型に対してコンパイルできる。新しいサーフェスは共有部品を組み合わせて作られるものであり、他のサーフェスの内部へ横から手を伸ばすことは決してない。既存のサーフェスを編集しなければ新しいサーフェスを追加できないとしたら、その時点でレイヤー構造はすでに壊れている。
## 1つのプロトコルの定義から型付きバインディングを作り、CIで差分を確かめる
通信プロトコルについては、機械可読なひとつの契約ファイルだけを唯一の正とし、型付きバインディングはそこから生成する。手書きはしない。生成されたファイルは読み取り専用として扱う——自動生成であることを示すバナーを入れ、手で編集することは決してない。CI(継続的インテグレーション)にバインディングを再生成するステップを追加し、差分がひとつでも出たらビルドを失敗させる。これにより契約とコードが乖離することはなくなる。
### ジェネレーターのバージョンを固定する
契約のバージョンは、コードジェネレーターが実際にサポートしているバージョンに固定する。ジェネレーターが処理できない新しいスキーマバージョンを与えると、生成される型が黙って何でも受け入れる緩い型に成り下がり、目立ったエラーもないまま型安全性が失われることがある。なぜそのバージョンに固定しているのかを記録しておき、ツール側が追いついたときにだけ見直す。
## どの連携先のイベントも、1つのハブで同じ形のイベントに変える
すべてのアクティビティは単一のハブを通じて集約する。ソースが何であっても——HTTP経由でプッシュされるイベントであれ、ログやトランスクリプトファイルをパースして得た行であれ——ランタイムに入る前に、ひとつの正規イベント型へ正規化する。下流のコードはイベントの種類(kind)で処理を振り分け、ソース固有のアクション名やツール名では決して振り分けない。こうすることで、ソースを追加してもランタイムやUIへ波及することがない。
### イベントの種類は小さく閉じた集合に保つ
イベントの種類は、セッションの開始と終了、作業の開始と終了、サブタスクのライフサイクル、進捗、権限リクエストといった、小さく閉じた集合として定義する。各取り込み経路が担う責務は、これらの正規イベントを生成することだけである。ハブより先の処理はすべてソースに依存せず、新しいソースが加わっても安定したまま保たれる。
## 通信の中身を、1つのトランスポートインターフェースの裏に隠す
通信そのものは、小さなインターフェースを持つ単一のトランスポートインターフェースの背後に隠す。メッセージ送信、受信メッセージの購読、準備完了を表すプロミスと接続状態の公開、そして破棄——これだけの表面積にする。実装は環境ごとに一度ずつ用意する——たとえば組み込み型のサーフェス向けにはプロセス内メッセージチャネル、スタンドアロン向けには再接続可能なソケットといった具合だ。実装の切り替えはただひとつのファクトリ関数の中だけで行う。下流のコードはすべてこのインターフェースにのみ依存し、どのトランスポートが実際に動いているかを一切知らない。
### 耐障害性は実装の中に持たせる
ネットワーク越しの実装には、指数バックオフと、再接続時にはけ出す送信キューを持たせる。プロセス内実装のほうは、生存期間の間ずっと「接続済み」と報告して構わない。呼び出し側からはどちらの実装でも同じ振る舞いに見えるため、機能側のコードにトランスポート固有の分岐は一切現れない。
## 連携先はプロバイダとして足し、ほかのコードを変えずに済むようにする
外部との連携はすべて、ひとつのインターフェースを実装するプロバイダとしてモデル化する。生のイベントを正規イベントへ正規化する、フックのインストール・アンインストール・照会を行う、ステータスを整形する、特定のルールから除外すべきアクション名を宣言する——これらを備えたインターフェースだ。ポーリングによるフォールバックや拡張版のマルチアクターモードのような任意の機能はオプションのメソッドとして提供し、単純な連携先の実装は小さいままにしておく。
### 境界がきれいかどうかを見分ける基準
新しい連携先を追加する作業は、このインターフェースを実装する新しいサブディレクトリをひとつ作るだけで済み、ランタイム・UI・既存のどのプロバイダにも変更が要らないはずだ。ソースを追加するたびに他の場所の編集を強いられるなら、その境界には漏れがある。連携先がさらに積み上がってひび割れが広がる前に、締め直す必要がある。
## 2つの表示先で、1つのランタイムコアと1つのストアを共有する
両方のホストサーフェスに、同じランタイムコアを組み合わせて使わせる。違いがあってよいのは、永続化の名前空間・トランスポート・ホスト固有のアダプタだけだ。状態の変更はすべて、データを所有し型付きの変更イベントを発行する単一のストアを経由させる。このストアが唯一の真実の源となる。ブロードキャスト層をひとつだけ用意してこれらのイベントを購読させ、そこで初めて送信用のプロトコルメッセージへ変換する。それ以外のどのモジュールも、トランスポートのメソッドを直接呼び出してはならない。
### ホストとの接点は狭く保つ
ランタイムとホストの間で公開するコールバックインターフェースはただひとつに絞る。作業の起動、フォルダを開く、インポートとエクスポートといった、本当にホスト固有と言える少数の事柄のためだけに用意する。この接点を狭く保つことこそが、2つのサーフェスがコアをそのまま共有できる理由である。広げるのは、実際にホストの違いがそれを必要とする時だけにする。
## 速いプッシュの知らせを先に使い、ファイルのポーリングを予備に残す
速く信頼できるシグナルが存在するなら、それを優先し、それが得られない場合に備えて遅いフォールバックを残しておく。HTTP経由のイベントコールバックによるプッシュチャネルは、即時かつ順序を保ったシグナルを与えてくれる。プッシュが導入されていない場合のフォールバックがファイルのポーリングだ。この2つは二重カウントが起きないようゲートで制御する——アイテムごとの配信済みフラグとグローバルなスイッチの組み合わせで、フォールバック側のタイマーを動かすかどうかを決める。
### 「内容を読む」ことと「状態を導く」ことを分ける
きめ細かな進捗やステータステキストのように、フォールバック経路からしか得られないデータもある。そのため、プッシュモードであってもそうしたデータのポーリングは動かし続ける。ただし、そのアイテムについてプッシュが確認できた時点で、フォールバック側が導出するタイマー(権限プロンプト・アイドル検知など)は抑制する。「内容を読む」ことと「状態を導く」ことを切り分けることこそが、2つのモードを衝突なく共存させる鍵になる。
## 状態は表示先ごとに分けて保存し、写しを確かめてから古い方を消す
永続化する状態はサーフェスごとに名前空間を分け、2つのサーフェスが並行して動いていても互いを上書きしないようにする。ファイルの書き込みはアトミックに行う——一時ファイルへ書き込んでからリネームする。また、自分自身が行った書き込みには印を付け、プロセスをまたぐウォッチャーがそれを外部からの変更として再読み込みしないようにする。ファイルウォッチにはポーリングによるバックアップを組み合わせる。OS標準のファイルウォッチャーはプラットフォームによっては信頼できないからだ。
### 消す前に検証する
旧来の状態を移行する際は、消す前に検証する——新しい保存先に書き込み、それを読み戻してから、初めて古いキーを削除する。まだ移行が済んでいないものが残っている間は、成功したものとみなさず、処理を止めない警告を表示する。コピーがディスク上で確認できるまでは、真実の源を決して削除しない。
## 定数を1か所に集め、リントのルールで守らせる
マジックナンバーや文字列——タイミング定数、識別子、プロトコルのリテラル、スタイルトークン——はすべて、レイヤーごとにひとつずつ用意した少数の定数モジュールに集約し、決してインラインで書かない。インラインの値は時間とともにずれ、重複し、後のリファクタリングでつまずくような隠れた結合を生む。
### 「気をつける」ではなく、リントで強制する
このルールは、違反があればマージをブロックするエラーレベルのカスタムリントルールで裏付ける。トークンファイルの外での色やスタイルのインラインリテラルを禁止し、共有のシャドウやスペーシングのトークンを必須にし、共有のフォント参照を必須にする。機械が強制するルールは、貢献者が入れ替わっても生き残る。文書に書いてあるだけの慣習は生き残らない。
## テストを層に分け、よく変わるコードはエンドツーエンドテストで守る
テスト戦略もレイヤーに分け、各レイヤーに適切なツールを当てる。高速なユニットテストと統合テストで、状態の変更・イベントのルーティング・パース処理・マイグレーション・インストーラのアトミック性といった純粋なロジックをカバーする。エンドツーエンドテストは実際のサーフェスを動かし、内部実装ではなくユーザーから見える結果を検証する。
### オープンな貢献のためには安定した接点を優先する
UIの内部実装のように、貢献者が頻繁に手を入れるコードに対しては、内部実装に結びついたユニットテストよりも、ユーザーに見える振る舞いを固定するエンドツーエンドテストを優先する。そうすればリファクタリングのたびに、機能追加の作業に加えて内部テストまで書き直させられることがなくなる。すべてのテストは独立させる——それぞれ専用の一時ホーム、ワークスペース、データディレクトリを持たせ、テスト間で状態が漏れないようにする。自動生成されたテスト一覧についても、生成コードと同じようにCIで差分チェックする。
## 壊れた入力は飛ばして止まらないようにし、ログの行に印を付ける
優雅に劣化させる——エラーは捕捉してログに残すが、ホストをクラッシュさせることは決してない。不正な形式の入力行があっても、ストリーム全体を中断するのではなく、黙ってスキップする。アセットやリソースが見つからない場合は、警告をログに残し、処理全体を失敗させるのではなく、nullやフォールバック値で処理を続行する。
### ログをgrepしやすくする
すべてのログ行の先頭には、サブシステム名を示す角括弧付きのコンテキストタグを付ける。そうすればフラットなコンソールのログストリームでも検索性が保たれる。集中型のエラーレポートやテレメトリが必要かどうかは、意図的に判断する。導入しないと決めたなら、その判断を明記し、ローカルなログ出力は一貫性を保っておく。そうすれば、後で読む人が推測に頼らずに済む。
## 書き足され続けるファイルを安全に読み、非同期のテストに十分な待ち時間を取る
これらのルールは、口で言うのは簡単だが、身をもって再び学び直すのは高くつく。
### 追記専用ファイルを読む
OS標準のファイルウォッチャーには、短い間隔での定期ポーリングを必ず組み合わせる。また、まだ終端していない部分行はバッファに保持して次の読み取りへ持ち越す。ファイルの書き込み途中を読み手が捕まえてしまうことがあるからだ。セッションがクリアまたはリセットされると、新しいファイルがまっさらに始まり、古いファイルはそのまま止まることが多い。そのため、同じハンドルが伸び続けているものと決めつけず、内容の変化からファイルの切り替わりを検知する。
### テストにおける時間の見積もり
非同期パイプラインの最後で結果をアサートするときは、各段階のレイテンシをすべて見積もりに入れる——トリガーとなるシグナルと、そこから導出されるシグナルとの間のずれ、タイマーの遅延、プロセス間通信やレンダリングの余裕分だ。この合計より短いタイムアウトは、構造的に不安定(flaky)になる。
### レンダリングとバッチ処理
高頻度で更新されるアニメーションの状態は、フレームワークの状態管理ではなく命令的なストアに持たせる。また「作業完了」への遷移はわずかに遅らせ、UIのバッチ処理によって、ユーザーから見える一瞬のアクティブ状態が隠れてしまわないようにする。
