write-discoverable-code について
write-discoverable-codeは、検索でコードを見つけて理解しやすくするためのコーディングエージェント向けスキルです。命名、型、ドキュメント、ファイル構成、コミット前の確認項目を扱います。
主な機能
- 01検索しやすいシンボル名
- 02ブランド型IDと明示的な状態型
- 03検索可能なエクスポート文書
- 04ドメイン単位のファイル構成
- 05テストの併置と非推奨マーカー
活用シーン
- 01新しいエクスポート関数の命名
- 02コード検索を意識したモジュール構成
- 03コミット前のコード確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ modem-dev/ skills --skill write-discoverable-codeインストール後、Claude Code で/write-discoverable-codeを入力して読み込まれたか確認します。
詳細
検索で見つけやすいコードを書く
コーディングエージェントは、文字列を検索し、ヒットした箇所の前後を少し読んでコードを見つけます。ホバーテキストも、定義へのジャンプも、セッションをまたぐ記憶もありません。これらのルールに従えば、5回ではなく1回の検索でコードを特定できます。
1. 名前は検索クエリです
- エクスポートするシンボルには、2〜4語の名前を付け、そのうち少なくとも1語はドメインを表す語にします。
diffではなくdiffUserObjects、queueではなくqueueEventForDispatchとします。 約70万行のモノレポで測定したところ、1語のエクスポート名がリポジトリ全体で一意である割合は61%、3語の名前は96%、4語以上では98%でした。曲線の変曲点は3語です。grepで一意に見つかる最短の名前を使い、残りはドキュメントコメントに記載してください。 - 汎用的な動詞には対象を付けます。
sanitizeではなくsanitizeEmailHtml、validateConfigではなくvalidateSmtpConfigとします。一意性を確保するために必要な範囲だけ具体化し、それ以上は付けません。 - シンボルの定義箇所は1つにします。 関数をファイル間でコピーしてはいけません。移動する場合は、同じ変更で元の定義を削除してください。共有ヘルパーは概念を表す名前の場所に1つだけ置き、それ以外の場所からはインポートします。
- 汎用的な名前を区別するためにモジュールのパスに頼ってはいけません。
users/diff.tsとorders/diff.tsを区別するインポートはファイルの先頭にありますが、検索ヒットは300行目にあるかもしれません。コンテキストはフォルダーではなく、シンボル名(formatDurationMsなど)に含めてください。例外:パス自体が意味を持つ、厳格で絶対的な規約がある場合(たとえば、すべてのコントラクトファイルがInput/Outputをエクスポートする場合)。 - 1つの概念には1つの表記を使います。
organizationIdかorgIdのどちらかを選び、どこでも統一してください。同義語を使うと、今後の検索がそのたびに分散します。類義語に近い別の表現を導入するのではなく、編集対象のコードベースにある語彙を再利用してください。 - 動作や対象読者が変わったら、同じコミットで名前も変更します。 古い名前は開封率100%の誤情報です。これは可視性を示す印にも当てはまります。他のモジュールからインポートされるようになった
_privateヘルパーには、公開名が必要です。 - ファイル名も名前です。役割だけを表すファイル名は使わないでください。
config.ts、types.ts、utils.ts、helpers.ts、handlers.tsは、検索結果だけでは意味がわからず、リポジトリ内のほかのすべてのモジュールにある config/types/utils と名前が衝突します。ドメインを先頭に付け、config.tsではなくbilling-plan-config.tsとしてください。(薄い再エクスポート用エントリーポイントとしてのみ、index.tsは許容されます。)
2. 型はエージェントが読み飛ばせないドキュメントです
- プリミティブなIDにはブランド型を付けます。
z.string().brand<'UserId'>()(TS)または newtype(Rust)を使います。transferOwnership(userId: string, orgId: string)というシグネチャでは、引数を入れ替えても気付けません。ブランド型を使えば、概念名が示されるコンパイルエラーになります。 - 権限が必要な操作には、ケイパビリティトークン型のパラメーターを使います(たとえば、生の接続ではなく
OrgScopedDbを必須にします)。コメントは依頼ですが、必須の型は物理法則です。 - 状態は判別可能なユニオンでモデル化し、暗黙のルールを持つnull許容フィールドの寄せ集めは避けます。
- エージェントがそのまま引用できるように型に名前を付けます。 型名は、エージェントが自己修正に使うコンパイラーエラーで引用されます。
OrgScopedDbは名前だけで意味がわかりますが、Ctx2ではわかりません。anyは避けてください。anyを使うたびにコンパイラーが黙り込み、エージェントは再び推測することになります。
3. 検索でたどり着く場所に書く
- すべてのエクスポートに1行のドキュメントコメントを付け、コード自体からは分からない最も重要な制約(単位、タイムゾーン、「挿入時刻ではなくソースの時刻」、所有権など)を記します。名前の定義が検索でたどり着く場所です。その1行がメッセージのすべてです。
- ドキュメントコメントには、平易な言葉で表現を記します。 検索語は自然言語(「rate limit」「retry delay」など)で入力されますが、camelCase の識別子はフレーズ検索に一致しません。
RateLimiterは「rate limit」で検索しても見つかりません。各エクスポートの上にあるドキュメントコメントには、検索する人が使うフレーズを、通常のスペース区切りの言葉で含めてください。たとえば、SessionExpiryCheckerには /*_ Checks whether the user session has expired. _/ と記載します。そうすれば、「session expired」や「session has expired」の検索でここにたどり着けます。 - インポートを読まなくても、モジュールの内容が分かるようにします。 インポートした各名前とそのドキュメント行だけで、読み手がソースモジュールを開かなくても十分に分かるようにします。開かなければならないなら、問題があるのはインポートの名前であり、読み手ではありません。
- 文字列を分割しないでください。 イベント名、フラグ、エラーコードをテンプレート補間で組み立ててはいけません(
`github.${entity}.${action}`のようにすると、github.pr.mergedを検索できなくなります)。ループのほうが簡潔に思えても、文字列全体をリテラルで記述してください。 - エラーメッセージは、一意なリテラルのプレフィックスで始めます。 ログで見つけたメッセージを検索すれば、すぐに throw の箇所にたどり着けるようにします。
throw new Error(`Webhook signature mismatch for ${id}`)とし、throw new Error(`${prefix}: mismatch`)とはしません。 - 1ファイルにつき検索可能な概念を1つにし、オーケストレーターは薄く保ちます。 「X はどこで実行されるのか?」に答えるコードは、コーディネーター、パイプライン、サービスクラスにインラインで書くのではなく、X の名前を付けたモジュールに置きます。名前は内部の仕組みではなく、読み手が知りたい対象に合わせます。オーケストレーターは、適切な名前のモジュールを呼び出す処理の流れとして読めるようにします。読み手が検索からオーケストレーターにたどり着いた場合、どの行からも、本当の実装まで1ステップでたどれるようにします。1つの大きなファイルに複数の概念の実装を埋め込むと、それらのどれを検索しても同じコードの壁に行き着きます。質問単位の概念ごとに名前の付いた置き場所が1つになるまで分割し、そこで止めます。1つの概念の内部でしか意味をなさないヘルパーはインラインに置き、些細な関数ごとにファイルを作って1つの答えを複数回の読み込みに分散させないでください。判定基準は両方向です。無関係な多くの質問に答えるモジュールは、複数の概念を抱えています。
- テストを同じ場所に配置します(
foo.test.tsをfoo.tsの隣に置きます)。そうすれば、1回の検索で動作とその仕様を一緒に見つけられます。 - 行き止まりに印を付けます。 古い経路に
@deprecatedを付け、新しい経路への参照を記載します。
コミット前の簡単なチェックリスト
- 新しいエクスポート名ごとに1回検索するだけで、実装を見つけられますか?
- 新しい関数の引数を2つ入れ替えたら、ビルドに失敗しますか?
- 呼び出し元が知っておくべきことのうち、シグネチャでは伝えられない唯一の情報(単位、タイムゾーン、所有権、順序など)を、定義のすぐそばに記載していますか?
- ログやエラーの文字列はすべて、ソース内にそのまま存在しますか?
- 名前を変えずに動作を変更したものはありますか?
- コードを移動したとき、元の場所から削除しましたか?
コメント
Markdown 対応