Writing Code
人間に読みやすいコードは、生成AIにとっても読み・変更しやすい。本スキルは、コードを書く時の設計判断を毎回同じプロセスに固定する。
バグ・エラーの修正では、先に /systematic-debugging スキルで根本原因を特定し、修正の実装段階で本スキルの原則に従う。
実装前ゲート: 既存パターンの確認と踏襲
コードを書き始める前に実行する:
- 触るものが同じ既存実装を洗い出して読む — 「同類」は機能の類似ではなく、触るものの同一性で選ぶ。変更が読み書きするもの(テーブル・レコード・セッションキー・URL空間・外部API・共有状態)と、担うUIの役割(フォームの結果表示 / トグル / モーダル / エラー・完了通知)を列挙し、それぞれについて既存の触っている箇所を検索ですべて挙げる。UIも機能も全く違っても、同じレコードを書くなら同類。UI役割が同じものからは、アクセシビリティ属性(
role / aria-*)と状態表示の作りを引き継ぐ
- 各既存経路が持つガードを列挙して突き合わせる — 洗い出した各経路について、そこにあるガード(認可チェック / ロック / 入力検証 / 履歴記録 / 冪等性 / 削除ガード)を列挙し、自分の経路に要るか要らないかを1つずつ判断する
- 新たに依存する共通ラッパーは実装を読む — 自分が新しく呼ぶ共通ラッパー・ミドルウェア・フレームワークAPI(監査ログの計装、トランザクション境界、認可ガード、状態フック等)は、失敗時の挙動・副作用の実行順序・状態が反映されるタイミングを実装で確認する。「型が通る」「登録要件を満たした」は確認したことにならない
- 適切な理由がない限り踏襲する — 「シンプルだから」「今回は例外」は理由にならない。本スキルの設計原則と既存パターンが食い違う場合も、既存パターンを優先する
- 逸脱はユーザー確認を経てのみ — 既存パターンが明らかな技術的負債になると判断した場合に限り、負債と考える根拠を添えてユーザーに確認を取った上で(Claude Code: AskUserQuestion)逸脱してよい。無断の逸脱は禁止
なぜ「機能の類似」で選んではいけないか: 新機能は見た目が似た画面・似た名前のmoduleに引きずられて選ばれるが、データ整合性を守るガードは同じレコードを書く別機能の側にある。似た画面を1つ読んで「踏襲した」と判断すると、読んだファイルの中で印象に残った部分だけがコピーされ、そのファイルにある他のガードはコピーから漏れる。
このゲートの成果物は「触るもの × 既存経路」の対応表(各経路のガードと、自分の経路での要否・不要な場合の理由)。「既存実装を読んだ」は成果物にしない(1件読めば満たせてしまい、漏れが表に現れないため)。同類が存在しない場合はその旨を記録する。数行の修正では、触るものが1つなら表は1行で足りる。
制約を追加するときの経路網羅
認可チェック・関門・レート制限・redirectガード・バリデーションを新しく置くときは、その制約が守る対象への到達経路を列挙してから実装する。最低限:
- URL直打ち — 途中の画面を飛ばして到達できるか。フレームワークの評価順(layoutとpage、middlewareとhandler)で制約が先に走るか
- クライアント側遷移 — soft navigationで制約が再評価されるか、初回描画時にしか評価されないか
- API直接呼び出し — 画面に置いた制約ではAPIの直接呼び出しを防げない。同じ判定がサーバー側にあるか
1経路にだけ置いた制約は未完成とみなす。 守らない経路があるなら、なぜ守らなくてよいかを書く。
実装前ゲートの2と対になる。ゲート2が「既存の制約 → 新しい経路」への転記漏れを防ぎ、本節が「新しい制約 → 既存の経路」への展開漏れを防ぐ。どちらも「制約 × 経路」の直積が埋まっていないことが原因で、ゲート2と本節のどちらか一方だけでは埋まらない。
検証境界の確定(過剰防御の抑制)
防御的コード(nullチェック・再バリデーション・フォールバック)は局所的には常に正当化できる(「nullかもしれない」)ため、迷い自体を追加の根拠にしない。書く前に保証元を確認する:
- 入力ごとに保証元を確認する — 変更が受け取る各入力について「どこで検証済みか」(型・上流のスキーマバリデーション・DB制約・フレームワーク保証)を特定する。保証済みの値には再ガードを書かない(parse, don't validate: 境界で検証して型に落とし、内部は型を信頼する)
- 保証が確認できないときは、足すのではなく前提を確認する — 確認せずにガードを足すと検証境界の位置が実装のたびに変わり、防御コードが増え続ける。境界をどこに置くかは既存方針・ユーザーへの確認事項として扱う
- 前節との関係 — 経路網羅は「制約を置くべき経路の漏れ」を防ぎ、本節は「保証済み領域への重複配置」を防ぐ。制約は境界に1箇所、内部はゼロが目標
レビュー指摘で防御追加を求められた場合も同じ基準で判断する(保証済みなら却下候補。類型は ~/.claude/context/code-review-checklist.md §16)。
設計語彙(Glossary)
設計判断の記述・議論では以下の用語を正確に使う。コード上の既存命名・フレームワーク公式用語(React component等)はそのまま尊重する(既存パターン踏襲と両立させる)。
- Module — interfaceとimplementationを持つすべて。スケール非依存: 関数・クラス・パッケージのどれでもよい。設計議論でunit / component / serviceと言い換えない
- Interface — 呼び出し側がmoduleを正しく使うために知るべき全て。型シグネチャに加え、不変条件・呼び出し順序の制約・エラーモード・必要な設定・性能特性を含む
- Seam(Feathers) — その場所を編集せずに振る舞いを差し替えられる場所。interfaceを置く位置。boundaryと言わない(DDDのbounded contextと衝突するため)
- Depth — interfaceにおけるleverage。呼び出し側が学ぶinterface量あたりに引き出せる振る舞いの量。実装行数とinterface行数の比ではない(水増しを誘発するため)
- Adapter — seamでinterfaceを満たす具体物。役割の名前であり、中身の大小は問わない
設計原則
- Deep moduleを設計する — 小さいinterfaceの背後に多くの振る舞いを置く。interfaceを設計する時に自問する: メソッド数を減らせるか / パラメータを単純化できるか / より多くの複雑さを内側に隠せるか
- The deletion test — そのmoduleを削除したと想像する。複雑さが消えるなら通過型(pass-through)であり不要。複雑さがN箇所の呼び出し側に再出現するなら、moduleは価値を生んでいる
- The interface is the test surface — 呼び出し側とテストは同じseamを通る。interfaceの内側をテストしたくなったら、moduleの形が間違っているサイン
- One adapter = hypothetical seam. Two adapters = real seam. — 実際に差し替わらないものにseam(port・抽象層)を作らない。adapterが1つしかないseamはただの間接参照であり、Speculative Generality(投機的一般化)
- 依存は生成せず受け取る(DI) — module内部で依存をnew・生成せず、引数で受け取る。時刻・乱数も依存として扱う
- 副作用より戻り値 — 引数を変異させたり外部状態を書き換えるより、結果を値として返す。テスト容易性と参照透過性が上がる
- 数行のために依存を足さない — 探す順序は AGENTS.md「新しく作る前に」に従う(既存の helper → 標準ライブラリ → 導入済みの依存)。新しい依存は、数行で書ける処理のためには入れない(依存は更新・脆弱性対応・ライセンス確認をずっと払い続ける)
- 内部 API を置き換えるときは、同じ変更で呼び出し元を移して旧 API を消す — 外部の利用者がいない限り、互換層を残さない(旧 API が残ると、それを写した新しい呼び出しが増え続ける)。旧 API の振る舞いだけを守っていたテストも消す
リーダブルコード原則
- 命名: 名前だけで役割が分かること。プロジェクトの用語集(CONTEXT.md等があれば)と一致させ、同じ概念には全体で同じ語を使う
- 命名は「それが何か」で付け、「どこから来たか・なぜ入ったか」で付けない: 識別子(モジュール・パッケージ・テーブル・変数・設定名)は現在のシステム内での役割・ドメイン概念から命名する。由来・経緯(移行元/連携先のシステム名・案件名・依頼者・新旧や暫定を示す相対語)はWhyであり、コミットログ・ADR・仕様書に置く。識別子に固定化すると、由来を知らない読み手への説明コストと、改名されないまま残る負債になる。公開契約のリソース名は「システムが外に見せている名前」なので機能名として使ってよい。迷ったら命名表を作りユーザー確認(Claude Code: AskUserQuestion)
コメント
How はコード、What はテスト、Why(変更した理由・経緯)はコミットログに置く。コードコメントに残すのは、コードとテストとログのどれにも置けないものだけにする(コメントはコードの変更時に更新されず、古くなってもレビューで気づかれないため)。
- 既定はコメントを書かない。書くのは Why not(自明な代替案を採らない理由)・隠れた不変条件・外部要因の workaround・lint 抑制の理由・消せる条件つきの TODO・限界を承知で取った近道だけで、1〜2行にする
- 近道は
shortcut: <限界>, <見直す条件> の形で書く。限界と見直す条件の無い近道は、条件に達しても誰も気づかないまま残る
- 関数名や型の言い換えになる docstring、PR 番号・依頼者・経緯のコメントは書かない(経緯はコミットログ)
- 他のコードは
file:line でなくシンボル名で指す。リポジトリ外の文書や外部ツールの内部記号(画面番号・チケット内の連番)だけを根拠にしない(そのコードだけを読む人が辿れない)
- 3行以上になりそうなら、先に関数の抽出と命名で表せないか考える
- 書いたコメントは1つずつ「消すとレビュアーが誤読するか」を問い、誤読しないものは消す
- 既存ファイルに確立したコメント密度・docstring の形式があればそれに従う
書く例・書かない例は references/comments.md。迷ったときに読む。
テスト規律(シーム限定TDD)
テスト基盤が存在するPJでプロダクションコードを書く時に適用する。適用外: テスト基盤が存在しないPJ / 設定・ドキュメントのみの変更 / UIの微調整。適用外で進めた場合は完了報告にその旨を明示する。
ループの規則
- Test at pre-agreed seams — テストを書く前に、テスト対象のseamを列挙する。実装計画(30_plan.md等)にseamが列挙済みならそれを合意とみなし自律実行してよい。計画外の公開interface新設・変更が必要になった時のみユーザーに確認する(Claude Code: AskUserQuestion)
- Red before green — 失敗するテストを先に書き、それを通す最小限のコードだけを書く。将来のテストを先取りした投機的実装をしない
- Vertical slices — 1テスト→1実装→繰り返し。各テストは前のサイクルの学びに応答するtracer bullet。全テストの先書き(horizontal slicing)は想像上の振る舞いを固定するため行わない
- リファクタはループの外 — red→greenのサイクル内でリファクタしない。green後・レビュー段階の責務として分離する
テストの質
- テストはpublic interfaceを通して振る舞いを検証する。内部実装が全て変わってもテストは変わらないのが良いテスト。良いテストは仕様書のように読める(「user can checkout with valid cart」)
- トートロジー禁止 — 期待値をコードと同じ方法で再計算しない(
expect(add(a, b)).toBe(a + b) は構造的に必ず通る)。期待値は独立した真実源から取る: 既知の正しいリテラル・手計算した例・仕様書
- 2つの想像上の変更で判定する — 振る舞いを壊す変更(誤った値を返す・副作用を飛ばす・2回実行する)をしても通るテストは何も守っていない。観測できる結果を変えずに内部だけ組み替えると落ちるテストは、実装に縛られている。どちらかに当たるテストは書き直すか消す
- Mockは外部境界のみ — 外部API・DB(テストDBを優先)・時刻/乱数・ファイルシステム。自分のmodule・内部コラボレータ・自分が制御するものはmockしない。内部をmockしたテストは、振る舞いが変わっていないリファクタで失敗する
Pure関数の回帰テスト固定
Parser / diff / tokenizer / formatter / 日付境界ヘルパー等、入力から出力が決定論的に定まるpure関数を新設・変更する時は、以下を必ず整備する:
- 境界値の網羅 —
0 / 空文字 / 空配列 / null / undefined / MAX_SAFE_INTEGER / 極端に長い文字列。日付はtimezone境界 / DST / 閏年。数値はinteger overflow / floating point誤差
- CJKとASCIIの両方 — Tokenize / diff / word-wrap等は必ず両方をテスト(片方だけだと日本語圏で失敗するバグを見逃す)
- バグ修正時の回帰固定 — 修正したバグの bug-triggering inputを回帰テストとして残す(後日同じロジック変更で再発した時に検出する)
- Property-based testingの判断 — Invariantを明示できるロジック(
mapToDsl(dslToMap(x)) === x のround-trip、sort(sort(arr)) === sort(arr) のidempotency、可換性、逆演算成立)は fast-check 等を優先。個別ケース列挙より状態空間の網羅性が上がる
Falsy check(言語横断のよくある罠)
- ID / count / revision番号は
0 を有効値として持ちうる。!!id / if (id) は 0 を無効値として扱う
!= null(null と undefined のみfalse) をdefaultにする
- 「truthyチェックかnullチェックか」を意識しない実装が本番バグの典型的な原因。lint rule化を検討する
部分更新の undefined
optionalフィールドを更新処理に渡すとき、undefined が「変更しない」なのか「削除する」なのかを、呼び出し側と保存側の両方で確認する。保存側が全列を書く実装(渡されなかった列を null で埋める)なら、optionalをそのまま渡すと意図しない削除になる。
型は両者を区別しない(どちらも T | undefined)ため、シグネチャを見ても判別できない。保存側の実装を読むまで結論を出さない。
不可逆な状態遷移とコミット後の副作用
一度実行するとユーザーが自力で戻せない状態遷移(確定・昇格・削除・フラグの片道更新)を書くときは、コミットの後ろに並ぶ副作用(外部API呼び出し・通知・キュー投入・別テーブルへの追記)を列挙し、それぞれが失敗したときにユーザーが次に何をできるかを決めてから実装する。
- コミット済みの遷移に対して「失敗を投げて終わる」は選べない。呼び出し側には失敗に見えるのに状態は進んでおり、再試行は「もう遷移済み」ガードで拒否される。想定内の失敗(期限切れ・既存行との衝突)は救済経路を用意し、想定外はログに記録して完了として扱う
- 同じ行を触る他経路がロックを取っているかを先に確認する。取っているなら自分も取る(一方の経路だけがロックを取ると、他経路のロックも防御として機能しない)。ロック取得後は、判定に使う前提を読み直してから進める
再実行に耐える操作
状態を変えるスクリプト・ジョブ・移行・起動や停止の処理は、前回の実行が意図したことではなく今ある状態を読み、目標との差分だけを当てる形にする。2回続けて走っても、前回がどこで落ちていても、同じ結果に収束させる(リトライ・再起動・クラッシュが普通に起きる場所で動くため)。
- 前回が残すもの(ロック・一時ファイル・書きかけのレコード・取りかけのジョブ)は、どこで止まっても次の実行が「古い」と判定できる形にする。一時パスに書いてから rename で置き換える、作成順でなく中身で比べる、など
- 「残った状態しだい」としか答えられない操作には、状態を突き合わせて直す手順を足す
破壊的操作の参照者
delete / disable / revoke / 無効化 / 全件上書き を書くときは、その対象を今参照している他者を列挙する: 別セッション・別ユーザー・共有済みのURLやトークン・キャッシュ・外部システム。
「今の画面から見て不要」は削除の理由にならない。発行済みのものを無効化する処理は、発行先が自分とは限らない。
アンチパターン(実装中の自己検知)
書いている最中に以下の兆候を検知したら手を止めて設計を見直す。いずれも判断材料でありハード違反ではない。PJの明文規約を常に優先する。
| Smell | 兆候 | 対処 |
|---|
| Duplicated Code | 同じロジックが2箇所以上 | 共通化を検討(3箇所目で必須検討) |
| Duplicated State | サーバ/永続層が持つ状態を、クライアントのstateや操作の戻り値にも保持して表示に使う | 真実源を一方に決める。複製するなら、サーバ側だけが変わる経路(他ユーザーの更新・別タブ・再検証・複合操作の部分的失敗)を列挙し、追従を設計する |
| Scattered State | 同期させる boolean が2つ目になる、if/else に分岐を1つ足すだけで機能を追加する、同じ形の前提を複数ファイルで繰り返す | 状態機械・判別共用体・表(registry)で状態と分岐を1か所に表す。形が今のままで十分小さく、増える見込みが無いなら作らない |
| Feature Envy | 他moduleのデータばかり触るメソッド | ロジックをデータ側へ移す |
| Primitive Obsession | ドメイン概念を裸のstring/intで表現 | 専用型・値オブジェクトへ |
| Data Clumps | 同じ引数群がいつも一緒に移動 | まとめて型にする |
| Shotgun Surgery | 1つの変更が多数ファイルに飛散 | 変更が集まるようmoduleを再配置(localityの欠如) |
| Speculative Generality | 「いつか使うかも」の抽象層・引数 | 削除(two adapters ruleで判定) |
| Redundant Guard | 型・上流・DB制約で保証済みの値への再チェック | 保証元を確認し、あるなら書かない。無いなら境界(入口)に1箇所置く |
| Message Chains | a.b().c().d() の連鎖 | 深いinterfaceで隠蔽 |
言語別ガイド(Read when)
新規module作成・公開interface変更・テスト設計を伴う実装の開始時に、対象言語のガイドをReadする。既存コードの数行修正では読まない。
完了基準
- 実装前ゲートの対応表が埋まっている(既存パターンから逸脱した箇所はユーザー確認済み)
- PJ 規定の品質チェック(lint / format / typecheck / test)が通っている
- 追加したコメント行を1つずつ取捨した(「コメント」節の基準で、残したものは誤読を防ぐものだけ)
出典: obra/superpowers、mattpocock/skills、DietrichGebert/ponytail、cursor/plugins の pstack(いずれもMIT License)を翻案。詳細はリポジトリルートのNOTICE.mdを参照。