no-invalid-states について
no-invalid-states は、正しさの制約をプログラムの型と構造で表すためのコードレビュー・リファクタリング用スキルです。境界での検証、内部表現の強化、既存動作を保つ検証をガイドします。
主な機能
- 01重複チェックと無効な状態の組み合わせを特定
- 02静的な不変条件と動的条件を分類
- 03境界で検証し表現を強化
- 04不変条件に応じた型の選択
- 05信頼できる値を受け取る API への変更
- 06状態遷移と動作維持のテスト
活用シーン
- 01ライフサイクルチェックの重複をリファクタリング
- 02相互排他的な状態を明示的に表現
- 03無効な操作順序がある API をレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vivswan/ skills --skill no-invalid-statesインストール後、Claude Code で/no-invalid-statesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSE.mdREADME.md.codex-plugin/plugin.jsonagents/openai.yamlreferences/other-languages.mdpython.mdrust.mdtypescript.md詳細
無効な状態をなくす
言語が提供する最も強力な仕組みで正しさの制約を担保できるよう、コードベースを見直してリファクタリングします。あちこちに防御的なチェックを散りばめても、その仕組みにはなりません。
基本原則:
境界で検証し、より強い表現に変換して、内部で無効な状態を構築しにくく、または不可能にします。
これは「検証するのではなく、パースする」という考え方です。
- 各事実は境界で一度だけチェックします。
- 結果は、プログラムの残りの部分が信頼できる表現でエンコードします。
- 実行時検証を削除するわけではありません。外部データ、動的データ、信頼できないデータに対するチェックは残します。
適用する場面
コードに次のような兆候がある場合は、このスキルを使ってください。
- 同じ条件を繰り返し実行時にチェックしている(
if !initialized、if value is None、assert ready) - ライフサイクルや相互排他的な状態を表すブール値
- 常にまとめて存在するか、まとめて存在しないかのどちらかでなければならない複数のオプショナルフィールド
- 別のメソッドを呼び出した後でなければ有効にならないメソッド
- 「Yの前にXを呼び出す必要がある」や「接続中のみ有効」といったコメント
- 呼び出し元がすでに検証したデータを再検証する関数
- 矛盾するフィールドの組み合わせを許してしまう構造体、クラス、インターフェース
- 静的に拒否できる誤った操作順序
- 型チェッカーを黙らせるために使われているキャスト、非 null アサーション、型チェックの無視
すでにシンプルなコードに抽象化を加えるためや、バグ探しの代わりにこのスキルを使わないでください。このスキルが改善するのは表現であり、動作ではありません。
基本原則
防御的なチェックをもう一つ追加する前に、この無効な状態をプログラムの表現から取り除けないか考えてください。その言語が提供する、慣用的で最も強力な仕組みを使います。設計は問題そのものより複雑にせず、むしろシンプルに保ってください。
ワークフロー
1. 不変条件の候補を見つける
典型的なパターンを検索します。
if !initialized if value is None state === ...
if !connected if value === undefined is_ready / is_valid
assert ready has_loaded instanceof / isinstance chains
次の点も確認します。
- 関連性のないオプショナル値に囲まれた、ブール値のライフサイクルフラグや状態列挙型
- 部分的にしか有効でないオブジェクトを生成するコンストラクター
- 構築後に不変条件を破れる、公開セッター
- 別のメソッドが先に呼ばれていないことだけを理由に失敗するメソッド
- 型チェッカーを納得させるために存在する、未チェックのキャストやアサーション
候補ごとに、不変条件を一文で書きます。たとえば、「接続が正常に完了した後でなければ、接続はデータを送信できない」です。
不変条件を明確に述べられない場合は、エンコードできません。その候補は対象外にします。
2. 各不変条件を静的か動的かに分類する
- 静的な内部不変条件は、プログラムの構造に組み込みます。例:初期化済みと未初期化、認証済みと未認証、パース済みと生データ、検証済み ID と任意の文字列、相互排他的な状態。
- 動的な外部条件は、実行時チェックを残します。HTTP リクエストが成功したこと、ファイルがまだ存在すること、トークンの有効期限が切れていないこと、ユーザー入力の形式が正しいこと、サービスに接続できることを型で証明することはできません。
目標とする形は、一方向のパイプラインです。
untrusted or raw value
-> validate / parse once, at the boundary
-> strong internal representation
-> trusted internal APIs (no re-checking)
3. 所有権を持つ箇所で解決する
表現を選ぶ前に、不変条件の背後にある状態を変更するのが誰なのかを特定してください。解決策を適用する場所は利用側ではなく、状態を変更する箇所です。
典型的な兆候は、同じガードを持つ呼び出し箇所が N 個あること、またはすべての利用側が考慮しなければならないフラグがあることです。これは、少数の書き込み箇所で防止できる問題を、N 個の読み取り箇所で検証している状態です。
具体例:関連付けられたエンドポイント URL が変更されると、保存済みの認証情報が古くなります。
- 利用側での設計: 6 つすべてのリクエスト経路で、使用前に
staleCredentialフラグを確認します。ガードが 6 つ、エラーが発生する箇所が 6 つあり、将来追加されるすべてのリクエスト経路でも、この確認を忘れずに行う必要があります。 - 所有者側での設計: URL の変更(設定 UI からの保存、設定ファイルの編集、インポート)はすべて、状態を管理する一つのトランジションを経由します。その時点で、「この認証情報を新しいエンドポイントでも保持しますか?」という問いを解決します。
- その後: リクエスト経路で古さを確認するガードは不要です。実行時の認証失敗は、動的に発生する条件として引き続き処理します。
所有者側での修正を適切なものにするには、次の 2 つのルールがあります。
- 完全に対応できていると判断する前に、すべての変更箇所を列挙してください。 利用側での確認は、忘れていた書き込み経路まで偶然カバーすることがあります。しかし、所有者側で修正する場合は、各経路を明示的に状態を管理するトランジションへ通さなければなりません。GUI と、その GUI が編集する設定ファイルは、1 つではなく 2 つの経路です。
- 変更箇所での解決に、必ずしも型を使う必要はありません。 検証付きのトランジション、正規化、または意図が表明された時点でユーザーに問いかける方法も有効です。より強い表現が適切な仕組みとなるケースについては、次の手順で説明します。
4. 慣用的で最も強い表現を選ぶ
好みの仕組みではなく、状況に応じて選んでください。
| 状況 | 表現 |
|---|---|
| 状態が実行時に変化し、コードが現在の状態を検査する | 直和型:データを持つ列挙型、判別共用体、シールドクラス |
| 実行可能な操作が状態によって異なり、誤った順序ではコンパイルに失敗すべき | Typestate:状態を型パラメーターで表す型、値を消費するトランジション |
| プリミティブに特別な意味がある、または一度検証すれば以降は信頼できる | スマートコンストラクターの背後に置く Newtype またはブランド型 |
| 2 つの状態が意味の異なるデータを持ち、オプショナル値でその違いを表現している | 状態ごとに分けた型 |
| 不変条件が構築時にまたがる(フィールド同士が一致する必要がある) | プライベートフィールド、検証を行うファクトリーまたはコンストラクター、不変性 |
| 不変条件が実際にはデータストアに関するもの | スキーマ制約:NOT NULL、CHECK、外部キー、一意制約 |
コード例を含む、詳細で慣用的なガイダンスは、言語ごとのリファレンスにあります。
- references/rust.md:列挙型、Newtype、
PhantomDataを使った Typestate、selfを消費するメソッド、TryFromの境界、安全な API の背後に置く unsafe コード - references/typescript.md:判別共用体、ブランド型、網羅性を確認する
neverチェック、境界スキーマ - references/python.md:状態ごとの不変データクラス、
Literal共用体、NewType、assert_never、厳密な型付け - references/other-languages.md:Go、Java、Kotlin、C#、Swift、関数型言語、動的型付け言語、データベース
対象の言語が含まれていない場合は、状況別の表を、その言語で利用できる機能に当てはめてください。シールド階層、スマートコンストラクター、不変性、モジュールのプライバシーは、形は異なってもほとんどの言語にあります。
5. リファクタリング
受け入れた候補ごとに、次のチェックリストに沿って作業します。
- 不変条件と、現在表現できてしまう無効な状態を明示します。
- より強い表現を導入します。
- 境界で検証します。生データを強い型に変換するパーサーまたはファクトリーを1つ用意し、実際にエラーを処理できる経路を設けます。不正な入力を新しい境界でどう扱うか(修復が一意に決まる場合は修復し、それ以外は拒否するか)は
/code-standardsスキルのルールに従います。以下のルールに従い、既存の境界ではこのリファクタリング中も従来の動作を維持します。 - 内部 API が強い型を受け取るように変更し、下流のコードに未検証の値が渡らないようにします。
- 新しい表現によって論理的にあり得なくなったチェックを削除します。
- 動的な要因により必要なチェックは残し、その理由を簡潔に説明します。
リファクタリング時のルール:
- 不正な構築を難しくする: 非公開フィールド、検証付きコンストラクター、ファクトリー関数、状態遷移関数、または凍結されたデータや読み取り専用のデータを使います。
- 検証は一度だけ行います。
検証 -> 使用、検証 -> 使用ではなく、一度解析 -> 使用、使用、使用にします。 - リファクタリングを無理に通すために、広範なキャスト、
any、非 null アサーション、無視コメントで型チェッカーを黙らせないでください。 - バグを確認した場合を除き、既存の動作を維持します。 バグを見つけた場合は、黙って修正するのではなく、別途報告します。
- 実用的な範囲で公開 API との互換性を維持します。 まず内部を強化し、その後で適用範囲を広げます。
- 無効な状態をなくすための、最小限のアーキテクチャ変更にとどめます。 すべてのブール値をステートマシンに変換しないでください。
- リファクタリングに価値があるのは、 繰り返しのチェックをなくす、誤った順序を防ぐ、あり得ないフィールドの組み合わせをなくす、または API の契約を明確にする場合に限ります。型システムを巧みに使うことより、読みやすさを優先します。
6. テストと検証
次の内容を対象とするテストを更新または追加します。
- 有効な状態遷移
- 境界で拒否される入力
- すべての状態に対する網羅的な処理
- 動作の維持
コンパイルエラーまたは型レベルのテストは、プロジェクトに既存の仕組みがある場合に限って追加します。
続いて、リポジトリ独自のツールを実行します。コマンド名を決めつけず、プロジェクト設定(package.json のスクリプト、Makefile、Cargo.toml、pyproject.toml、CI ワークフロー)を確認します。一般的なゲートは次のとおりです。
- Rust:
cargo fmt --all -- --check、cargo clippy --workspace --all-targets -- -D warnings、cargo test --workspace - TypeScript:プロジェクトの型チェック、リント、テスト、ビルドのスクリプト
- Python:設定されている ruff/pyright/mypy/pytest の構成
- その他:リポジトリの CI で実行されるもの
このスキルのためだけに新しい型チェッカーや依存関係を追加しないでください。ただし、リポジトリにとって本当に適切な場合は除きます。
7. 報告
コードの変更を依頼された場合は、説明するだけでなく変更を実装します。完了時に、次の内容を報告します。
- 改善した不変条件: 以前は慣習や実行時チェックによってのみ保証されていた内容
- なくした無効な状態: 以前は可能だった矛盾するデータや誤った順序
- 新しい表現: 各不変条件を担う仕組み(例:Rust の typestate、TypeScript の判別共用体、Python の NewType)
- 維持した実行時検証: 外部情報や動的情報に依存するため残したチェック
- 変更したファイルとその理由
- 検証: 実行したフォーマッター、型チェッカー、リント、ビルド、テストのコマンドと、それぞれの結果
レビュー基準
コードレビューを実施するスキル(/rubber-duck-review など)は、このスキルがインストールされている場合、このセクションをレビュアー向けプロンプトに展開します。レビュアーには、次の点を指摘するよう依頼してください。
- 実行時チェックや慣例だけで強制されている不変条件:
- ブール値のライフサイクルフラグ
- 同時に存在する必要がある、または存在してはならないオプションフィールド
- 実行時に強制される「Y の前に X を呼び出さなければならない」という順序
- すでに検証済みの値に対する検証の繰り返し
- 表現できないはずのフィールドの組み合わせ
- オーナー側で解決すれば不要になる、コンシューマー側のガード。同じチェックが N 個の呼び出し箇所で繰り返されるケースや、すべてのコンシューマーが忘れずに考慮しなければならないフラグが該当します。状態に列挙可能な変更箇所があり、そこで一度だけ問題を解決できる場合に適用してください
- 各指摘について、不正な状態をなくす、より強力な表現(サム型、新しい型、型状態、検証コンストラクター)またはオーナー側の解決方法
得られた指摘を、上記のワークフローに沿ってトリアージしてください。