良い仕様書の書き方
真っ白なページから仕様書を書くことはめったにありません。変更内容を平易な言葉で説明し、/opsx:propose が要件とシナリオの草案を作成したら、それを良いものに仕上げます。このページはその最後の部分、つまり「良い」とはどういうことか、そしてAIをそこへ導く方法について説明します。
これは変更のレビューの姉妹編です。レビューは草案の弱点を見つけること、書き方は強力な草案が何でできているかを知ることです。
仕様書は動作であり、コードではありません
仕様書は、システムが何をするかを、誰でも確認できる形で述べたものであり、その実装方法ではありません。要件(動作の記述)とシナリオ(それを証明する具体的な例)で構成されます。
### 要件: セッションタイムアウト
システムは、30分間操作がない場合にセッションを期限切れにするものとする (SHALL)。
#### シナリオ: アイドルタイムアウト
- GIVEN 認証されたセッション
- WHEN 30分間何も操作がない
- THEN セッションは無効化され、ユーザーは再認証が必要になる実装方法(キュー、ライブラリ、テーブルスキーマなど)は design.md かコードに残してください。動作と実装が一つの要件に混ざると、その要件はテスト可能でなくなり、コードが変更された瞬間に陳腐化し始めます。
優れた要件の条件
優れた要件とは、他の人に渡してテストしてもらえるほど明確に記述された、一つの動作です。
一つの文、一つの
SHALL/MUST。 要件に3つの「さらに…」が含まれているなら、それは実際には3つの要件です。分割しましょう。観測可能。 コードの外にいる人がその要件が満たされているか判断できるべきです。「アップロードが10MBを超えた場合にエラーバナーを表示するものとする (SHALL)」は観測可能ですが、「システムは大きなアップロードを適切に処理するものとする」はそうではありません。
適切な強制力。 OpenSpecではRFC 2119のキーワードを使用しますが、それぞれ意味が異なります。
キーワード 意味 MUST/SHALL厳格な要件。例外は許容されません。 SHOULD強い推奨。正当な理由がある場合の例外は許容されます。 MAY完全に任意です。 デフォルトでは
MUST/SHALLを用います。SHOULDは「正当な理由がない限り」ということを本当に意味する場合にのみ使います。
要件に対するテスト: コードを見たことのないテスターが合格したかどうか判断できるか? できない場合は、要件を明確にする必要があります。
優れたシナリオの条件
シナリオは、要件が実際に役立つことを示す場所です。それぞれが自動化テストになり得る具体的なGIVEN/WHEN/THENです。
- 要件を実際にテストする。 要件を別の言葉で言い換えただけのシナリオは何もテストしません。特定の状況と特定の結果を具体的に記述します。
- 正常系だけでなく重要なケースを網羅する。 有効なログインは簡単です。空の入力、期限切れのトークン、二度目のクリック、問題が発生するケース --- そこにバグが潜んでおり、シナリオが最も価値を発揮する場所です。
- ケース名をタイトルに含める。 「シナリオ: 期限切れトークンを拒否する」は、何がカバーされているかレビュー担当者が一目でわかります。「シナリオ: テスト2」ではそうはいきません。
有用な習慣: 承認前に、「壊れてしまったら腹が立つのはどのケースか?」と自問し、そのケースがシナリオに名前として含まれていることを確認します。
適切な差分の種類を選ぶ
変更は、3つのセクションタイプを使って仕様書への編集内容を記述します。適切なものを使用することで、アーカイブされた仕様の正確さが保たれます。
## ADDED 要件— 以前には存在しなかった全く新しい動作。## MODIFIED 要件— 既に存在していたが変更される動作。新しい完全なバージョンを含めます。何が変わったかの簡単なメモはレビュー担当者の役に立ちます。## REMOVED 要件— 削除される動作と、その理由の一行。
アーカイブ時、ADDEDはメインの仕様に追加され、MODIFIEDは古いバージョンと置き換えられ、REMOVEDは削除されます。あるケイパビリティの最後の要件を削除すると、そのケイパビリティは廃止されます。空の仕様を残すのではなく、アーカイブによって openspec/specs/<capability>/spec.md が削除されます。これはファイルを削除する唯一のアーカイブ手順であるため、明示的に要求する必要があります。変更の .openspec.yaml に retire_capabilities: true を追加します(そのファイルに既に必要な schema: と共に)。これがないとアーカイブは中断され、その旨を通知します。廃止はファイル全体を削除するため、仕様がタイトル、## Purpose、要件ブロック以外のもの(## Notes セクションや要件下のコメントなど)を含む場合も拒否されます。中断時にはそれらの行が名前で示されます。それらを ## Purpose または要件に移動するか、手動で仕様を削除します。呼び出し元のチェックアウトにある仕様の場合、アーカイブ出力はコミット済みファイルを復元する git checkout も名前で示します。選択されたストアには、チェックアウトスコープのリカバリガイダンスが代わりに提供されます。実際の変更をADDEDとしてマークすると、競合する2つの要件ができてしまいます。新しい動作をMODIFIEDとして記述すると、置き換えるべきものがありません。迷ったら、現在の仕様を開いて要件が既に存在するか確認します。
もう一つ知っておくべきセクションがあります。差分がまだ存在しないケイパビリティを作成する場合、そのケイパビリティの目的を一文か二文で ## Purpose として開始します。アーカイブはそれを生成するメイン仕様のPurposeとして使用します。省略すると、手動で埋める TBD プレースホルダが残ります。既存の仕様には既にPurposeが存在するため、差分のものは無視されます。変更するには openspec/specs/<capability-path>/spec.md を直接編集します。ここで <capability-path> は specs/ からの相対ディレクトリで、例えばフラットなプロジェクトでは user-auth、ドメインで整理されたプロジェクトでは identity/user-auth のようになります。
変更の適切なサイズを考える
最もよくある作成上の間違いは、表現が悪い要件ではなく、1つの変更が3つの変更を兼ねようとしていることです。
良い変更は、1つの意図を一文で言い表せるものです。 「ダークモード切替を追加する」「ログインエンドポイントにレート制限をかける」「セッションをCookieから移行する」。変更を説明するのに「さらに…」がたくさん必要なら、それは分割すべきサインです。
変更が大きすぎる兆候:
- 提案のスコープが無関係な機能のリストのように読める。
- レビューに午後いっぱいかかりそうで、誰もやらなくなる。
- 二人の人が衝突せずに作業できない。
- タスクの半分が単独でリリース可能。
小さな変更は、レビューが容易で、1回の集中したセッションで構築しやすく、半年後にアーカイブだけが残ったときにも理由を理解しやすいです。常に複数の変更を並行して実行できます。詳細は編集と反復およびワークフローを参照してください。
逆もまた起こります。一行のタイポ修正に3つの要件と設計ドキュメントは必要ありません。儀式をリスクに見合ったものにします。
AIを良い草案へ導く方法
/opsx:proposeが最初の草案を作成するため、返ってくる品質は与える情報の質に比例します。要件を手書きする必要はありません。AIを適切に導く必要があります。
- 意図と範囲を述べます。 「OSの設定に従い、初回読み込み時に動作するダークモード切替を追加します。既存のテーマAPIには触れないでください。」 スコープ外の部分はスコープ内の部分と同じくらい重要です。
- 気を付けたいケースを挙げます。 「すでに手動でテーマを選択したユーザー向けのシナリオがあることを確認してください。」 AIはあなたが指摘した点をカバーします。
- そして編集します。 あとはプレーンなMarkdownです。曖昧な
SHALLを厳密にし、何もテストしないシナリオを削除し、欠けているケースを追加します。あるいはAIに依頼します: 「タイムアウト要件が曖昧なので、30分に固定してください。」
草案を作り、磨き、繰り返します。この数ラウンドで、信頼できる仕様書が完成します。それが本来の目的です。