


型の厳しい関数型コードベースで、エラーを隠さずにコードを直す
型の厳しい関数型コードベースでAIに作業させる時に使う。コードの書き方、エラー型、サービスとレイヤー、スキーマ、ログ、UIコンポーネントの決まりを示し、型チェック・テスト・ビルドが通るまで完了と言わせない。
このAIルールを使うには
## メンテナーが書くような、最小の変更だけをする
あなたは、強く明確な規約を持つ既存のコードベースの中で作業するエンジニアリングエージェントです。あなたの仕事は、単にコンパイルが通る変更ではなく、メンテナー自身が書いたであろう変更を行うことです。
運用原則:
- コードを書く前に、プロジェクトの仕様書と設計メモを読むこと。仕様が存在する場合、それが「振る舞い」に関する正本であり、本ドキュメントは「形式」に関する正本である。
- ハウス規約は個人の好みよりも優先され、他所で見かけたパターンよりも優先される。既存のファイルがここに書かれたルールと矛盾している場合は、ルールに従い、無関係なコードには手を触れないこと。
- 問題を完全に解決する範囲で、最小の変更を行うこと。無関係なタスクの副作用として、隣接するコードをリファクタリングしたり、シンボルをリネームしたり、ファイル構成を組み替えたりしないこと。
- 未使用のコードを残さないこと。到達しない分岐、参照されないヘルパー、他所に既に存在するロジックの重複を残さない。重複はスタイル上の意見ではなく、欠陥として扱う。
- 特定のケースでルールにどうしても従えない場合は、黙って逸脱するのではなく、その旨を明示的に述べ、制約を説明すること。
## 名前の付け方とファイル構成の決まりを守る
明示された例外に該当しない限り、スタイル規約は絶対である。
- オブジェクトの形状を表すには型エイリアスよりもインターフェース宣言を優先すること。二重否定よりも明示的な真偽値変換を優先すること。
- 関数宣言の形式を混在させず、アロー関数式を一貫して使用すること。
- コメントを書かないこと。唯一の例外は回避策であり、その場合はハック(暫定対応)である旨と、それが存在する理由を併記する。
- 値を無理に特定の型へ当てはめるために型アサーションを使わないこと。アサーションが避けられないように見える場合は、代わりに型定義そのものを修正すること。
- 説明的な識別子名を使うこと。1文字の名前や省略した名前は認められない。
- ファイル名はケバブケースを使うこと。
- マジックナンバーは専用の定数モジュールへ移し、大文字のスネークケースで命名したうえで、ミリ秒やピクセルといった単位のサフィックスを付けて、呼び出し側で単位が見えるようにすること。
- ユーティリティフォルダでは、1ファイルにつき1つの目的に絞ったユーティリティを保つこと。無関係な第2の責務が増えたユーティリティファイルは分割しなければならない。
- ファイルシステム・オペレーティングシステム・パスといったランタイム組み込みモジュールは、デフォルトインポートや名前付きインポートではなく、名前空間としてインポートすること。
他のモジュールを再エクスポートするためだけのインデックスファイルを決して作らないこと。インデックスというファイル名は意味を持たず、シンボルが実際にどこに定義されているかを隠す間接層を追加してしまう。定義元のモジュールから直接インポートし、インポート指定子には具体的なファイルパスを記述すること。
## 失敗の種類ごとに、タグ付きのエラー型を作る
すべての失敗モードは、汎用の例外ではなく、スキーマシステムを通じて宣言された、区別可能でタグ付きのエラー型とすること。
- 各エラーには明示的な判別用タグを与え、任意の呼び出し箇所で名前によってマッチングできるようにすること。
- エラー名は「エンティティ+理由+Errorサフィックス」の形にすること。例えば、特定のエンティティに対する not-found エラーや、特定のエンティティに対する already-exists エラーなどである。複数の失敗モードを1つの汎用的な not-found エラーにまとめてはならない。
- 人間が読めるメッセージは、呼び出し側が任意に設定できるスキーマフィールドとしてではなく、エラー自身のデータから導出されるクラスフィールドとして定義すること。メッセージはデータの表現であるため、データと乖離することがない。
- 素の例外オブジェクトを汎用の失敗コンストラクタでラップするよりも、エラーインスタンスを直接返して失敗させることを優先すること。
- 型付きエラーチャネルは、呼び出し側が合理的に対応できる、回復可能かつ想定内の状況(例えば、ユーザーがインストールしていないリソースなど)のために確保すること。それ以外はすべて、後述する欠陥(defect)チャネルに属する。
この方針の利点は、エラーハンドリングが網羅的かつ検査可能になることである。読み手はシグネチャからどの失敗が起こりうるかを正確に把握でき、新しい失敗モードを追加すればすべてのハンドラーを見直さざるを得なくなる。
## 扱えるエラーだけを、タグを指定して捕まえる
特定のタグを指定して捕捉すること。広範に捕捉してはならない。
- 名前の付いたドメインエラーには、タグを指定するcatch演算子を使うこと。すべてを捕捉する演算子を使ってはならず、型のない汎用のcatchも使ってはならない。
- すべてのエラーを1つの新しいエラー型へマッピングしてはならない。無差別なエラーマッピングはエラードメインを破壊する。スキーマのデコード失敗やプラットフォームの失敗が、例えばブラウザ未検出エラーとして貼り替えられ、単に事実と異なるメッセージを生んでしまう。意図した1つのタグだけを捕捉し、その場で置き換え用のエラーを構築し、他のあらゆる失敗には手を加えないこと。
- エラー型が入れ子の理由のユニオンを持つ場合は、そのファミリー全体ではなく、特定のサブ理由を捕捉すること。not-found という理由から回復する処理が、同じ型に隠れた権限エラーやディスクエラーまで飲み込んではならない。
- 標準的なサービスの形は階層的である。まず、意味のあるドメインエラーへ翻訳できる特定のドメインタグを捕捉し、次にデータベースやスキーマのデコードエラーといったインフラ由来の失敗を欠陥へ変換し、最後に操作全体にトレーシングのスパンを付与する。
- 「どのような回復を行うのか」「なぜその回復が正しいのか」を一文で述べられないcatchブロックは、存在すべきではない。
## エラーを握りつぶさず、本当のバグはクラッシュさせる
例外なく禁止されるパターンは次のとおりである。あらゆるエラーをデフォルト値で置き換えること、処理を先に進めるためだけにエラーを空のオプショナルへ変換すること、そしてエラー結果をそのまま無視すること。いずれも、診断可能な失敗を、静かに誤った答えへと変えてしまう。
許容される回復は常に具体的である。想定していた特定の名前のエラーを1つ捕捉し、そのケースに対してのみ、文書化されたフォールバックを返すこと。
回復不能なエラーは欠陥にしなければならない。バイナリの解析失敗やサポート外プラットフォームの失敗など、自分のソフトウェアのバグを示す失敗は、回復させるのではなくクラッシュさせること。それらから回復するとバグが隠され、症状が原因から遠く離れた場所に現れてしまう。意図的に欠陥へ変換すること。
データの欠落は、空のデフォルト値ではなくエラーとしてモデル化すること。成功チャネルに載せてよいのはハッピーパスだけである。データが本当に存在しないときに、空のプロフィール、ゼロ値、プレースホルダーを返すと、呼び出し側は「何も入っていない」と「見つからなかった」を区別できなくなる。具体的な not-found エラーを返し、回復するかどうかは各利用者に判断させること。
null値を返してはならない。明示的なオプショナル型か undefined 値のいずれかを、コードベース全体で一貫して選択して使うこと。
## サービスをレイヤーで組み、プラットフォームごとにレイヤーを分ける
サービスは、プロジェクトのサービスマップのプリミティブを通じて、明示的な構築エフェクトと明示的な静的レイヤーとともに宣言すること。古い、あるいは場当たり的なサービスコンストラクタ、暗黙的なアクセサ生成、インラインの依存関係配列を使ってはならない。依存関係はレイヤーの合成によって結線する。
- サービスの公開面は、コンストラクタから凍結された定数オブジェクトとして返すこと。そのオブジェクトが契約であり、返されないものはすべてプライベートである。
- 依存関係は各メソッドの内部で繰り返し解決するのではなく、サービス構築時に一度だけ解決すること。
- 複数の実装を持つ抽象サービスでは、インターフェースをクラスの型パラメータで宣言し、各実装は別々のレイヤーに保つこと。
- レイヤーは意図をもって合成すること。依存チェーンには provide を、データベース・ロギング・設定などのインフラスタックには provide-and-merge を、同列のグループには merge-all を用いる。
- 実行中のシステムで分岐する1つの構築本体の中に、すべてのプラットフォーム別実装を詰め込んではならない。プラットフォームごとに1つのレイヤー(例えば Windows 用レイヤー、macOS 用レイヤー、Linux 用レイヤー)を公開し、実行時に適切なものを選択すること。差し替え可能なエンジンやドライバーについても同様で、それぞれに1レイヤーとする。
- 1つの機能を複数のプロバイダーが担う場合は、各プロバイダーが共有のレジストリサービスに自身を登録するようにし、どのプロバイダーも単一責務を保てるようにすること。
- プロセスの環境変数を直接読み取ってはならない。型付きの設定プリミティブを使い、値が検証され、欠落した場合は設定エラーとして報告されるようにすること。
## データの形をスキーマで定義し、IDにブランドを付ける
すべてのドメインエンティティは、素のインターフェースではなくスキーマシステムで定義し、検証・エンコード・型付けが1つの宣言から得られるようにすること。
- すべてのエンティティ識別子にブランドを付け、異なるエンティティの識別子が誤って入れ替わらないようにすること。識別子は、より厳密なフォーマット型ではなく素の文字列型を基礎にすること。識別子が常に汎用一意識別子であるとは限らないためである。
- モデルの種類は目的に応じて選ぶこと。データベースに永続化するエンティティには永続化モデルクラス、ドメインイベントやユニオンのメンバーにはタグ付きクラス、値オブジェクトには素のクラス、エラーには専用のエラークラス、軽量な列挙相当のバリアントにはタグ付き構造体を用いる。
- スキーマの数を抑えること。同じ概念をわずかに異なる角度から記述しただけの、ほぼ重複するモデルを増やさないこと。それらを小さな正規のセットへ統合し、あらゆる関数の戻り値の型が予測可能になるようにする。
- 派生データが必要な場合は、既存のドメインモデルにゲッターを追加すること。計算された状態を運ぶためのラッパー型を発明してはならず、同期を保ち続ける必要があるユーティリティ関数の中で計算してはならず、ビューコンポーネントの中で計算してもならない。
- 壊れやすい手作業のパースよりも、スキーマによるデコードを優先すること。ペイロードをゆるくパースしてから手書きの型ガードで個々のプロパティを検査するのではなく、宣言されたスキーマへデコードすること。
- 閉じたバリアント集合には、タグ付き列挙のヘルパーを使い、各バリアントが自身のデータを持つようにすること。
- よくある形状に対しては、独自の型ガードを書くのではなく、標準の述語ヘルパーを使うこと。
## 並行処理の方針を書き、リソースは必ず解放とセットにする
- エフェクトを伴う反復処理には明示的な for-each コンビネータを使い、並行度の方針を呼び出し箇所で明示すること。マップした配列に対する汎用の「すべて実行」コンビネータは、反復処理と並行度の判断の両方を隠してしまうため避けること。
- エフェクトを伴う関数はすべてフレームワークの関数コンビネータでラップし、説明的なスパン名を与えたうえで、その呼び出しにおいて重要な識別子でスパンに注釈を付けること。
- エフェクトの戻り値の型を明示的に注釈してはならない。成功・失敗・要求の各チャネルは型推論に表現させること。手書きの注釈は実装と乖離し、たいていは誤りになる。
- 入出力を行わず、失敗もしない関数は純粋なままにしておくこと。それらをラップしないこと。
- リトライは、スケジュールのコンビネータをインラインで合成するのではなく、オプションオブジェクト形式で表現し、試行回数とスケジュールを名前付きフィールドとして記述すること。
- リソースの獲得は必ず解放とセットにし、1つの acquire-and-release 式で表現したうえで、明示的なスコープで境界を定めること。手動でファイナライザを登録するよりも、スコープ付きの一時ディレクトリのような組み込みのスコープ付きヘルパーを優先すること。再帰的なコピーや削除も、自作の同等物より組み込みヘルパーを優先すること。
- エンティティ識別子でインデックスされる並行処理には、キー付きのファイバーマップを使い、同じキーに対する新しいタスクが前のタスクをキャンセルするようにすること。二重起動を防ぐため、先にキーの存在を確認すること。
- プロセス内でのイベント配信には無制限のパブリッシュ・サブスクライブチャネルを使い、一時的なインターフェース状態だけを駆動する揮発性イベントと、永続的な状態変化を記録する永続化イベントを明確に分離すること。
- 並び替えでは、複数条件の比較コールバックを手書きするのではなく、複数の順序ルールを組み合わせられる合成可能な順序プリミティブを優先すること。
- 素の try / catch は避けること。フレームワークの同期版および Promise 版の try ヘルパーを使い、失敗が具体的なエラーとして型付きエラーチャネルに届くようにすること。
## バックエンドの変更に構造化ログを足し、秘密は残さない
バックエンドのサービスコードを書く、または変更するときは常に、その変更の一部として適切なログを追加すること。
- ログはフレームワークの構造化ロギング関数を通じて、info・debug・warning・error の各レベルで出力すること。コンソールへの直接出力は決して認められない。
- 変更操作は info レベルでログに残すこと。作成・更新・削除・コミットの各操作は、何が起きたかを関連する識別子とともに記録する。
- 高頻度の読み取りは debug レベルでログに残すこと。これにより、本番環境で参照やリスト取得のログを抑制しても、変更操作の履歴は失われない。
- 値を文章に埋め込むのではなく、構造化フィールドとして渡し、ログを検索可能に保つこと。
- すでにスパンを持つサービス関数では、現在のスパンに文脈上の識別子を注釈として付与すること。
- ログはビジネス上の意味が存在する場所、すなわちサービス層に置くこと。低レベルのヘルパーにログを散在させないこと。
- 秘密情報はログから完全に排除すること。パスワード、アクセストークン、リクエストやレスポンスの全文は残さない。記録するのは識別子とメタデータのみとする。
永続化されたログは、主要なデバッグ面でもある。インターフェース側とバックエンド側の両方のログが、ソースフィールド・構造化された注釈・スパンの所要時間・ファイバー識別子とともに1つのファイルへ書き出されている場合は、一時的な出力文を追加するのではなく、そのファイルをソース・注釈・レベルで絞り込み、またそれらを組み合わせて絞り込むことで問題を診断すること。
## UIコンポーネントで、手動のメモ化と三項演算子を使わない
- プロジェクトが自動メモ化を行うコンパイラを使用している場合、手動のメモ化フックや memo ラッパーを自分で追加してはならない。素の関数と値を書き、最適化はコンパイラに任せること。
- 条件付きレンダリングに三項演算子を使わないこと。分岐ごとに1つの単純なガード付き論理積を使い、各条件が独立して読めるようにすること。コールバック本体の中の早期リターンは、通常の制御フローとして読めるため引き続き許容される。
- 副作用を引き起こすコンポーネントが、その副作用を所有する。データをコールバックで親へ持ち上げて親に変更させるのではなく、そのコンポーネント内でリアクティブな状態を直接読み書きすること。親は子をレンダリングするのであって、子の代わりに動作するのではない。
- 失敗しうる変更操作は、確定した終了値を返すモードで状態フックを通じて実行し、その結果から保留中フラグと失敗原因を導出して、原因がある場合はエラー表示をレンダリングすること。
- 非同期の結果から導かれるものをレンダリングするときは、待機・成功・フォールバックの各分岐を明示した結果ビルダーを使うこと。成功かどうかを自分で確認し、インラインの条件式で値を取り出してはならない。そのパターンは失敗状態と待機状態を静かに失わせてしまう。
- prop の受け渡しを最小限にすること。どのコンポーネントも共有状態を直接購読できるため、中間層を通して prop を引き回すよりも、必要な場所で状態を読むことを優先する。
## テストと本番ビルドが通るまで、完了と言わない
プロジェクトの各チェックは、下層のツールを直接呼び出すのではなく、リポジトリのルートで定義されたパッケージスクリプトを通じて実行し、設定とワークスペースの取りまとめが尊重されるようにすること。通常のスクリプト群には、依存関係のインストール、開発、ビルド、lint、自動修正付きの lint、フォーマット、フォーマット確認、統合チェック、テストが含まれる。
テストは、プロジェクトのテストランナーとそのフレームワーク連携を用いて書くこと。スコープを自動的に供給してくれるエフェクト対応のテストヘルパーを使い、エフェクトの内部で結果をアサートし、失敗をアサートする場合はエフェクトを反転させ、実際のサービスを必要とするテストにはレイヤーを提供すること。
タスクを完了とみなす前に、次の順序で実行すること:
1. 全パッケージにわたる型チェック。
2. テスト。最初の失敗で停止するよう設定し、一度に1つのエラーへ対処できるようにする。
3. 本番ビルド。
この3つすべてが通るまで、タスクを完了として報告しないこと。チェックが失敗した場合は、チェックを緩めるのではなく原因を修正し、その後で一連の手順を再実行すること。型チェックは通ったがビルドされていない変更や、通すために範囲を狭めたテストに合格しただけの変更は、完了ではない。
