| name | writing-code |
| description | コードを書く時の実装原則(deep module設計・seam・テスタビリティ・シーム限定TDD・コメント規約・リーダブルコード)。プロダクションコードの新規実装・修正・機能追加・リファクタの開始時に常に使用。CSS・HTML・テーマ・テンプレート等のフロントエンド資産もプロダクションコードに含み、除外条件の「設定ファイルのみの変更」には当たらない(除外はトリビアルな1行修正と、lint/CI設定等の純粋な設定ファイルのみ)。TypeScript/Python/Goの言語別ガイドをreferencesに同梱。境界: バグ・エラーの原因調査はsystematic-debuggingが先(原因特定後の修正実装は本スキル)、提出前のdiff確認はself-review、要件定義はdesign-feature。 |
Writing Code
人間に読みやすいコードは、生成AIにとっても読み・変更しやすい。本スキルは、コードを書く時の設計判断を毎回同じプロセスに固定する。
バグ・エラーの修正では、先に /systematic-debugging スキルで根本原因を特定し、修正の実装段階で本スキルの原則に従う。
実装前ゲート: 既存パターンの確認と踏襲
コードを書き始める前に実行する:
- 触るものが同じ既存実装を洗い出して読む — 「同類」は機能の類似ではなく、触るものの同一性で選ぶ。変更が読み書きするもの(テーブル・レコード・セッションキー・URL空間・外部API・共有状態)と、担うUIの役割(フォームの結果表示 / トグル / モーダル / エラー・完了通知)を列挙し、それぞれについて既存の触っている箇所を検索ですべて挙げる。UI も機能も全く違っても、同じレコードを書くなら同類。UI 役割が同じものからは、アクセシビリティ属性(
role / aria-*)と状態表示の作りを引き継ぐ
- 各既存経路が持つガードを列挙して突き合わせる — 洗い出した各経路について、そこにあるガード(認可チェック / ロック / 入力検証 / 履歴記録 / 冪等性 / 削除ガード)を列挙し、自分の経路に要るか要らないかを1つずつ判断する
- 新たに依存する共通ラッパーは実装を読む — 自分が新しく呼ぶ共通ラッパー・ミドルウェア・フレームワークAPI(監査ログの計装、トランザクション境界、認可ガード、状態フック等)は、失敗時の挙動・副作用の実行順序・状態が反映されるタイミングを実装で確認する。「型が通る」「登録要件を満たした」は確認したことにならない
- 適切な理由がない限り踏襲する — 「シンプルだから」「今回は例外」は理由にならない。本スキルの設計原則と既存パターンが食い違う場合も、既存パターンが勝つ
- 逸脱はユーザー確認を経てのみ — 既存パターンが明らかな技術的負債になると判断した場合に限り、負債と考える根拠を添えてユーザーに確認を取った上で(Claude Code: AskUserQuestion)逸脱してよい。無断の逸脱は禁止
なぜ「機能の類似」で選んではいけないか: 新機能は見た目が似た画面・似た名前の module に引きずられて選ばれるが、データ整合性を守るガードは同じレコードを書く別機能の側にある。似た画面を1つ読んで「踏襲した」と判断すると、読んだファイルの中で印象に残った部分だけがコピーされ、そのファイルの他のガードごと落ちる。
完了基準: 「触るもの × 既存経路」の対応表が書けている(各経路のガードと、自分の経路での要否・不要な場合の理由まで埋まっている)。「既存実装を読んだ」「パスを挙げられる」は通過条件にしない — 読んだかどうかは検証できず、1件読めば自己申告で満たせてしまうため。同類が存在しない場合はその旨を記録する。逸脱する場合はユーザー確認済みである。
制約を追加するときの経路網羅
認可チェック・関門・レート制限・redirect ガード・バリデーションを新しく置くときは、その制約が守る対象への到達経路を列挙してから実装する。最低限:
- URL 直打ち — 途中の画面を飛ばして到達できるか。フレームワークの評価順(layout と page、middleware と handler)で制約が先に走るか
- クライアント側遷移 — soft navigation で制約が再評価されるか、初回描画時にしか効かないか
- API 直接呼び出し — 画面に置いた制約は API を守らない。同じ判定がサーバー側にあるか
1経路にだけ置いた制約は未完成とみなす。 守らない経路があるなら、なぜ守らなくてよいかを書く。
実装前ゲートの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・生成せず、引数で受け取る。時刻・乱数も依存として扱う
- 副作用より戻り値 — 引数を変異させたり外部状態を書き換えるより、結果を値として返す。テスト容易性と参照透過性が上がる
リーダブルコード原則
- 命名: 名前だけで役割が分かること。プロジェクトの用語集(CONTEXT.md等があれば)と一致させ、同じ概念には全体で同じ語を使う
- 命名は「それが何か」で付け、「どこから来たか・なぜ入ったか」で付けない: 識別子(モジュール・パッケージ・テーブル・変数・設定名)は現在のシステム内での役割・ドメイン概念から命名する。由来・経緯(移行元/連携先のシステム名・案件名・依頼者・新旧や暫定を示す相対語)は Why であり、コミットログ・ADR・仕様書に置く。識別子に固定化すると、由来を知らない読み手への説明コストと、改名されないまま残る負債になる。公開契約のリソース名は「システムが外に見せている名前」なので機能名として使ってよい。迷ったら命名表を作りユーザー確認(Claude Code: AskUserQuestion)
- docstring: 既存形式(JSDoc / docstring)を踏襲。本スキルで書き方を重複定義しない
コメント配分の四象限: How=コード / What=テスト / Why=コミットログ / Why not=コードコメント
役割を混同しない。コードコメントはWhy not専用(自明な代替案をなぜ採らなかったか)。Why(変更した理由・背景)はコミットログの仕事、What(振る舞いの仕様)はテストコードの仕事、How(処理の流れ)はコード自体が語る。コードを読めば分かる What / How をコメントに書くのは、二重メンテナンスの負債・コード変更時の陳腐化リスク・レビュー時のノイズを増やすだけ。削除するとレビュアーが混乱するかを自問し、混乱しないなら書かない。
本節は新規コード、および確立したコメント規約を持たないコードへのデフォルト方針。既存ファイルに確立されたコメント密度・慣習がある場合はそちらを優先する。
書かない例(Anti-pattern):
const user = getUser(id);
i++;
if (user == null) return;
function getUser(id: string): User { ... }
書く例(Why not):
if (err instanceof DomainError || err.name === "DomainError") { ... }
const toEpoch = dateInputToEpoch(dateInput, 23, 59, 59);
useEffect(() => setVotesPage(1), [pageId]);
判断規則:
- コメントを消してもコードだけで意図が分かる → 消す
- 「なぜこの変更をしたか」を残したい → commit message / PR 説明 / ADR に書く(コード内に書くと腐る。Whyの置き場所はコミットログ)
- コメントが 3 行以上 → 「関数抽出 + 命名で表現できないか」を先に検討
- コメントが「なぜこの自明でない選択をしたか(Why not)」を説明していない → 書き換える or 消す
テスト規律(シーム限定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) は構造的に必ず通る)。期待値は独立した真実源から取る: 既知の正しいリテラル・手計算した例・仕様書
- 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 等を優先。個別ケース列挙より状態空間の網羅性が上がる
完了基準に追加: 新設・変更した pure 関数について境界値網羅と bug-triggering input の回帰テストが揃っている。
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 呼び出し・通知・キュー投入・別テーブルへの追記)を列挙し、それぞれが失敗したときにユーザーが次に何をできるかを決めてから実装する。
- コミット済みの遷移に対して「失敗を投げて終わる」は選べない。呼び出し側には失敗に見えるのに状態は進んでおり、再試行は「もう遷移済み」ガードに弾かれる。想定内の失敗(期限切れ・既存行との衝突)は救済経路を用意し、想定外はログして完了として着地させる
- 同じ行を触る他経路がロックを取っているかを先に確認する。取っているなら自分も取る(片側だけのロックは相手の防御ごと無効化する)。ロック取得後は、判定に使う前提を読み直してから進める
破壊的操作の参照者
delete / disable / revoke / 無効化 / 全件上書き を書くときは、その対象を今参照している他者を列挙する: 別セッション・別ユーザー・共有済みのURLやトークン・キャッシュ・外部システム。
「今の画面から見て不要」は削除の理由にならない。発行済みのものを無効化する処理は、発行先が自分とは限らない。
アンチパターン(実装中の自己検知)
書いている最中に以下の兆候を検知したら手を止めて設計を見直す。いずれも判断材料でありハード違反ではない。PJの明文規約が常に勝つ。
| Smell | 兆候 | 対処 |
|---|
| Duplicated Code | 同じロジックが2箇所以上 | 共通化を検討(3箇所目で必須検討) |
| Duplicated State | サーバ/永続層が持つ状態を、クライアントのstateや操作の戻り値にも保持して表示に使う | 真実源を一方に決める。複製するなら、サーバ側だけが変わる経路(他ユーザーの更新・別タブ・再検証・複合操作の部分的失敗)を列挙し、追従を設計する |
| 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する。既存コードの数行修正では読まない。
完了基準
実装を完了とする前に、以下をすべて確認する:
出典: obra/superpowers および mattpocock/skills(いずれもMIT License)を翻案。詳細はリポジトリルートの NOTICE.md を参照。