Design by Story(ストーリー駆動デザインプロトタイプ) は何をしますか?
プロダクトの単一ファイルHTMLプロトタイプを、複数のデバイスで表示可能にし、かつユーザーストーリー単位で整理します。デバイスごとに分割するのと同じように、ストーリーごとに分割します。
上部バーには2つの切り替えスイッチャーがあります:
- デバイス: 例:Website・Android・iPhone。同じプロダクト、異なるフレーム。
- ストーリー: 「すべてのストーリー」+各ユーザーストーリーごとのエントリ。ストーリーを選択すると、共有データがそのストーリーのシードデータにリセットされ、すべてのデバイスがそのストーリーの開始画面に移動し、ストーリーの説明、優先度、ステータス、受け入れ基準、ウォークスルー、メモを表示するパネルが開きます。
どちらの選択もURLハッシュに保存されます(#story=S2&device=iphone)。そのため、特定のストーリーの特定のデバイスへのリンクをレビューやチケットに貼り付けることができます。
ワークフロー
-
入力を収集する(不足しているものだけを1回のメッセージで尋ねる):
- プロダクトの概要と誰が使うか。
- 表示するデバイス。デフォルト:Website+Android+iPhone。フィールド/キオスク用途の場合はタブレットも。
- ユーザーストーリー。任意の形式(リスト、バックログエクスポート、機能ドキュメント)。
- AC形式: Given/When/Then、またはプレーンなルール?デフォルトはGiven/When/Then。
- 優先度(MoSCoW)とステータス(もしあれば)。デフォルトは
should/draft。 - 主要データモデル(メインレコード+キーフィールド)。またはストーリーから推測。
すべてのストーリーをストーリー形式に正規化します。ユーザーがストーリーを持っていない場合は、プロダクト説明から3〜6個のストーリーをドラフトし、構築前に確認してもらいます。
-
ストーリーをチェックする:品質チェックリストに照らして。問題を1つの短いリストで提示します。ユーザーの表現を黙って書き換えることはしません。
-
ストーリーごとの画面を計画する。 各ストーリーについて、各デバイスで触れる画面と必要なシードデータをリストアップします。ストーリーが特定のデバイスをスキップするのは、プロダクトが実際にそのように動作する場合のみです(例:管理画面はWebsiteのみ)。その
devicesリストにその旨を記載し、そのストーリーが選択されている間、スイッチャーは他のデバイスを無効にします。 -
assets/shell.htmlをコピーして、ユーザーが指定したパス(デフォルトはdocs/reference/prototype.html)に配置し、内容を埋めます。references/prototype-spec.mdを読み、視覚ルール(フレーム、トークン、タイポグラフィ、カード、ピル、トースト、ドロワー、アイコン、トーン)に従います。 -
画面を構築し、ストーリーごとに構築しない。 画面は共有され、ストーリーはそれらへのエントリポイントとシードデータです。ストーリーごとに画面を決して複製しないでください。ストーリーにバリエーションが必要な場合は、シードデータから駆動します。
-
ブラウザで検証する。 ブラウザツールがあればそれを使います。なければヘッドレスChromeを使用します:
--dump-dom "file:///…/prototype.html#story=S2&device=iphone"でレンダリング内容を確認、--screenshotで見た目を確認、--enable-logging=stderrでコンソールエラーをキャッチします。以下を確認します:- 各ストーリーがリストするすべてのデバイスで正しい画面を開くこと。
- すべての受け入れ基準(AC)が実演可能であり、ウォークスルーをクリックスルーできること。
- ストーリー途中でデバイスを切り替えても状態が保持されること。
- あるストーリーのコントロールが別のストーリーの下に現れず、「すべてのストーリー」ですべてのストーリーのコントロールが一緒に動作すること。
- コンソールがクリーンであること。
-
同期を保つ。 後でストーリーが追加・変更された場合、同じ編集でその
#story-textエントリ、BEHAVIOURエントリ、およびリストされている各デバイスを更新します。
ストーリー形式
ストーリーはシェル内の2か所に存在し、どちらもIDでキー付けされます:
- テキストを
#story-textJSONブロックに、ストーリーパネルで読み取り専用表示。 - 振る舞いを
BEHAVIOURに、これはコードのみ。
<script type="application/json" id="story-text">
{
"S1": {
"title": "検索を保存する",
"story": "訪問者として、検索を保存できる。そうすることで、再度検索しなくても新しいマッチングの通知を受け取れる。",
"priority": "must",
"status": "draft",
"ac": [
{ "id": "S1-AC1", "text": "Given 「オークのテーブル」で検索した、when 「保存」をタップする、then 「保存済み」の下に表示される" },
{ "id": "S1-AC2", "text": "Given その検索は既に保存されている、when 再度保存する、then 「保存済み」にはまだ1回だけ表示される" }
],
"acNext": 2,
"walkthrough": ["「オークのテーブル」で検索", "「保存」をタップ", "「保存済み」を開く"],
"outOfScope": ["メールアラート"],
"assumptions": ["保存するには訪問者がサインインしている必要がある"],
"questions": ["保存できる検索数に上限はあるか?"]
}
}
</script>
const BEHAVIOUR = {
S1: {
devices: ['web', 'android', 'iphone'], // このストーリーが適用されるフレーム
start: { web: 'search', android: 'search', iphone: 'search' }, // デバイスごとの開始画面
features: ['save-search'], // このストーリーが有効にする機能
seed: () => ({ user: SIGNED_IN, saved: [] }), // このストーリー用の初期データ
},
};
| フィールド | ルール |
|---|---|
story | As a <ペルソナ>, I can <アクション>, so that <ベネフィット>。ベネフィットはユーザーが得るものであり、アクションの言い換えではない。 |
priority | must・should・could・won't(MoSCoW)。 |
status | draft・in review・agreed。 |
ac | オブジェクト { id, text }。IDは<ストーリー>-AC<n>、再採番はせず、再利用もしない。後述参照。 |
acNext | このストーリーで発行されたAC番号の最大値。新しいACはacNext + 1を取得し、acNextも更新される。ACを追加するたびに更新する。 |
walkthrough | レビュアーがクリックスルーするデモスクリプト。受け入れ基準ではないので簡潔に。 |
outOfScope、assumptions、questions | プレーンなリスト。埋め草を発明するよりも空のままにしておく。ユーザーが不明な点は質問として記録する。 |
seed()は呼び出しごとに新しいオブジェクトを返すため、ストーリーを再選択するときは常にクリーンに再開されます。「すべてのストーリー」ではDEFAULT_SEEDと各デバイスのホーム画面を使用します。
受け入れ基準(AC)
- ACごとに1つのテスト可能な振る舞いを、ユーザーが選択した形式で。Given/When/Thenは1行:
Given <コンテキスト>, when <アクション>, then <観測可能な結果>。 - 正確なコピー: ACがメッセージに関する場合、メッセージを引用する(
then I see "Nothing open right now.")。「フレンドリーなメッセージ」のような抽象的な表現は避ける。 - 結果、スタイリングではない: 「完了として表示される」はOK、「緑色のピルが表示される」はスタイリング。視覚的な詳細が要件そのものである場合のみACとする。
- 1つのデバイスでテスト可能。 他のデバイスに関するACを書かない(「すべてのデバイスに表示される」「Webのみ」「スマホでは変更できない」など)。どのデバイスがストーリーをカバーするかは
devicesに属し、デバイス間の同期はバックエンドの関心事です。 - IDは永続的。
S1-AC3は編集、移動、子ストーリーへの分割後もIDを維持するため、テストやチケットに追跡可能です。新しいACはacNext + 1を取得します。削除されたIDは再発行されず、再採番もされません。
品質チェックリスト
各ストーリーを構築する前にチェックし、変更があるたびに再度チェックします:
- ペルソナは実際のユーザーロールか?ベネフィットはそのユーザーが言いそうなことか?
- 単独で価値を提供しているか(独立性)、そして1分以内にデモ可能な小ささか?そうでなければ分割する。
- すべてのACにID、単一の振る舞い、観測可能な結果があるか?
- 空、エラー、制限のケースがカバーされているか、または
questionsにリストされているか? - 他のストーリーへの依存関係が可視化されているか?(共有機能キー、および
assumptionsでの言及、例:「S2に基づく」)
ストーリーの機能は、そのストーリーが選択されるまでオフ
各ストーリーが導入するすべてのインタラクションには機能キーが与えられ、そのストーリーのfeaturesにリストされます。can('save-search')は、そのストーリーがリストされている間のみtrueになります。
- レンダリング: ストーリーのコントロールを
can(...)でラップし、選択されていない場合は存在しないようにします(ボタン、フォーム、メニュー項目なし)。無効表示はしません。レビュアーは選択されたストーリーが追加するものだけを見るべきです。 - ハンドラ: 各ストーリーハンドラの先頭に
if (!can('save-search')) return;を記述し、古いDOMやコンソールからでもストーリー外では何も起動しないようにします。 - 「すべてのストーリー」 はすべてのストーリーの機能を同時に有効にします:プロダクト全体を
DEFAULT_SEEDで表示。ストーリー同士が競合しないか確認します(例:同じ場所に2つのコントロール)。 - 別のストーリーのインタラクションに依存するストーリーは、両方のキーをリストします(例:「ステータスでフィルター」は
['filter', 'complete']をリストし、タスクのチェックオフがフィルターリストから消える様子を示せるようにする)。 - ストーリーが表示のみするデータ(例:担当者のイニシャル)はゲートしません。変更可能な機能のみをゲートします。
ストーリーの変更
プロトタイプはストーリーを表示するのみで、編集や保存はしません。すべてのストーリー変更はClaudeを介してチャットで行います:
- 新しいストーリー: ストーリー、AC、質問をドラフトし、ユーザーと一緒に品質チェックリストを実行してから、
#story-textエントリとBEHAVIOURエントリを書き、画面を一度の編集で構築します。ストーリーはテキストだけの状態で存在することはありません。もしそうなった場合、シェルはスイッチャーからそのストーリーを除外し、コンソールにエラーを記録します。 - 変更されたストーリー(文言、AC、優先度、ステータス、メモの変更):
#story-textを編集します。変更がストーリーの動作を変える場合(新しいAC、異なるウォークスルー)、画面とBEHAVIOURも同じ編集で更新します。 - レビューフィードバック: ユーザーがレビュアーのコメントを伝えてきた場合、それぞれを上記のいずれかの変更に変換するか、まだ決着していないものは
questionsのエントリにします。 - 何を構築・変更したかを、ストーリーとAC IDで常に明示します。
ユーザーがストーリーをトラッカー(Jiraなど)で管理している場合、トラッカーがマスターコピーです。プロトタイプ上だけでストーリーを編集するのではなく、ここでもその文言を反映させます。
ストーリーの分割
ユーザーがストーリーの分割を依頼した場合、またはストーリーに約5つ以上のAC、複数の主要機能キー、または別々に出荷可能なACがある場合、分割します。
-
分割パターンを選択し、使用したパターンを明示します:
- ワークフローステップ(作成→レビュー→公開)
- 操作(追加/編集/削除)
- ハッピーパスを先に、次にエッジケース(空、重複、制限到達)
- デバイスまたはロール別(スマホ撮影 vs. Web管理)
- データバリエーション別(1アイテム→多数、1通貨→複数)
- シンプルを先に、次に拡張(プレーンソート→保存済みソート設定)
各子ストーリーは依然として完全な
As a… I can… so that…であり、独自の価値を持たなければなりません。 -
親から子に番号を付ける:
S3→S3.1、S3.2、… 親を#story-textとBEHAVIOURの両方から削除し、子をその場所に配置します。シェルのreadHashは古い#story=S3リンクをS3.1に送るため、共有リンクは引き続き機能します。 -
分割し、複製しない:
- 受け入れ基準。 各親ACは、元のIDを保持したまま、正確に1つの子に移動します(
S3-AC2はS3.1内でS3-AC2のまま)。ACが2つの子にまたがる場合は、2つの新しいAC(S3.1-AC1、S3.2-AC1)に書き換え、どの親IDを置き換えたかをメモします。 - 機能キー。 インタラクションが分割される場合はキーも分割します(
filter→filter-status、filter-counts)。子は、依存関係ルールに従い、兄弟のキーをその上に構築する場合にリストすることもあります。 - デバイス。 各子は、実際に触れるデバイスのみをリストします。
- シードデータ。 後の子の
seed()は、前の子が残す状態から始まるようにします(例:S3.2はいくつかのアイテムが既に保存されている状態から始まる)。これにより各子を単独でレビューできます。 - メモ。 各子に、該当するスコープ外、前提、質問を与えます。子は特に指定がない限り親の優先度を引き継ぎます。
- 受け入れ基準。 各親ACは、元のIDを保持したまま、正確に1つの子に移動します(
-
画面を更新する: 別のキーに移動したコントロールを再度ゲートします。その後、各子がリストするすべてのデバイスでチェックし、親のIDがリダイレクトされることを確認します。
-
分割を報告する: 短いテーブル(子ID、ストーリー、優先度、デバイス、AC ID)で報告し、ユーザーがすべての親ACがどこかに配置されたことを確認できるようにします。ユーザーがバックログや機能ドキュメントを保持している場合は、そこにも分割を反映します。
ルール
- 単一のHTMLファイル、インラインCSS/JS、ビルドステップなし、フレームワークなし、バックエンドなし。ディスクから直接開く。
- すべてのデータは1つの共有インメモリ
stateにあり、すべてのデバイスが読み書きする。 - レンダリング関数はデバイスIDを受け取り、
stateから再描画する(シェル参照)。 - 発明された中立的なサンプルデータを使用する。他のプロジェクトから名前、ブランド、コンテンツをプロトタイプにコピーしない。
- モバイルフレームは同じアプリマークアップをレンダリングする。AndroidとiPhoneの違いは、ユーザーがプラットフォーム固有のUIを要求しない限り、クローム(ステータスバー、ノッチ/アイランド、ジェスチャーバー)のみであり、別々のアプリではない。