/click-path-audit — 行動フロー監査
静的なコード読み取りでは見逃されるバグ、すなわち状態相互作用の副作用、逐次的呼び出し間の競合状態、そして互いに静かに打ち消し合うハンドラを発見します。
これが解決する問題
従来のデバッグでは以下をチェックします:
- 関数は存在するか?(配線不足)
- クラッシュするか?(ランタイムエラー)
- 正しい型を返すか?(データフロー)
しかし、以下はチェックしません:
- 最終的な UI 状態はボタンラベルが約束する内容と一致しているか?
- 関数 B は関数 A がやったことを静かに元に戻していないか?
- 共有状態(Zustand/Redux/context)に意図したアクションを打ち消す副作用はないか?
実例:"New Email" ボタンが setComposeMode(true) を呼び出し、次に selectThread(null) を呼び出しました。どちらも個別には動作します。しかし selectThread には副作用として composeMode: false をリセットする処理がありました。ボタンは何もしませんでした。体系的なデバッグでは54個のバグが見つかりましたが、このバグは見逃されました。
動作の仕組み
対象エリア内のすべてのインタラクティブタッチポイントに対して:
1. ハンドラを特定する(onClick, onSubmit, onChange など)
2. ハンドラ内のすべての関数呼び出しを順番にトレースする
3. 各関数呼び出しに対して:
a. どの状態を READ するか?
b. どの状態を WRITE するか?
c. 共有状態に SIDE EFFECTS はあるか?
d. 副作用として状態をリセット/クリアするか?
4. チェック:後続の呼び出しが先行の呼び出しからの状態変更を UNDO していないか?
5. チェック:最終状態はユーザーがボタンラベルから期待するものか?
6. チェック:競合状態(非同期呼び出しが間違った順序で解決される)はないか?
実行手順
ステップ 1: 状態ストアをマッピングする
どのタッチポイントを監査する前に、すべての状態ストアアクションの副作用マップを構築します:
対象スコープ内の Zustand ストア / React コンテキストごとに:
各アクション/セッターに対して:
- どのフィールドを設定するか?
- 他のフィールドを RESET するか(副作用として)?
- 文書化: actionName → {sets: [...], resets: [...]}
これが重要な参照情報です。「New Email」バグは、selectThread が composeMode をリセットすることを知らなければ発見できませんでした。
出力形式:
STORE: emailStore
setComposeMode(bool) → sets: {composeMode}
selectThread(thread|null) → sets: {selectedThread, selectedThreadId, messages, drafts, selectedDraft, summary} RESETS: {composeMode: false, composeData: null, redraftOpen: false}
setDraftGenerating(bool) → sets: {draftGenerating}
...
危険なリセット(自分が所有していない状態をクリアするアクション):
selectThread → resets composeMode (setComposeMode が所有)
reset → resets everything
ステップ 2: 各タッチポイントを監査する
対象エリア内のすべてのボタン/トグル/フォーム送信に対して:
TOUCHPOINT: [ボタンラベル] in [コンポーネント:行]
HANDLER: onClick → {
call 1: functionA() → sets {X: true}
call 2: functionB() → sets {Y: null} RESETS {X: false} ← 競合
}
EXPECTED: ユーザーは [ボタンラベルが約束する内容の説明] を期待する
ACTUAL: X は false(functionB がリセットしたため)
VERDICT: BUG — [説明]
以下の各バグパターンをチェック:
パターン 1: 逐次的な取り消し(Sequential Undo)
handler() {
setState_A(true) // X = true に設定
setState_B(null) // 副作用: X = false にリセット
}
// 結果: X は false。最初の呼び出しは無駄だった。
パターン 2: 非同期レース(Async Race)
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// 結果: 最終的な loading 状態はどちらが先に解決するかに依存する
パターン 3: 古いクロージャ(Stale Closure)
const [count, setCount] = useState(0)
const handler = useCallback(() => {
setCount(count + 1) // 古い count をキャプチャ
setCount(count + 1) // 同じ古い count — 1 増加、2 ではない
}, [count])
パターン 4: 欠落した状態遷移(Missing State Transition)
// ボタンに "Save" と表示されているが、ハンドラは検証するだけで実際には保存しない
// ボタンに "Delete" と表示されているが、ハンドラはフラグを設定するだけで API を呼び出さない
// ボタンに "Send" と表示されているが、API エンドポイントが削除/壊れている
パターン 5: 条件付きデッドパス(Conditional Dead Path)
handler() {
if (someState) { // someState はこの時点で常に false
doTheActualThing() // 到達しない
}
}
パターン 6: useEffect の干渉(useEffect Interference)
// ボタンが stateX = true を設定
// useEffect が stateX を監視し、false にリセットする
// ユーザーは何も起こらないと感じる
ステップ 3: 報告
見つかったバグごとに:
CLICK-PATH-NNN: [重要度: CRITICAL/HIGH/MEDIUM/LOW]
Touchpoint: [ボタンラベル] in [ファイル:行]
Pattern: [Sequential Undo / Async Race / Stale Closure / Missing Transition / Dead Path / useEffect Interference]
Handler: [関数名またはインライン]
Trace:
1. [呼び出し] → sets {フィールド: 値}
2. [呼び出し] → RESETS {フィールド: 値} ← 競合
Expected: [ユーザーが期待するもの]
Actual: [実際に起こること]
Fix: [具体的な修正]
スコープ制御
この監査は負荷が大きいです。適切にスコープを設定してください:
- アプリ全体の監査: 新規リリース時または大規模リファクタリング後に使用。ページごとに並列エージェントを起動。
- 単一ページの監査: 新しいページを構築した後、またはユーザーが壊れたボタンを報告した後に使用。
- ストア重視の監査: Zustand ストアを変更した後、変更されたアクションのすべてのコンシューマを監査。
アプリ全体の推奨エージェント分割:
Agent 1: すべての状態ストアをマッピング(ステップ1)— 他のすべてのエージェントの共有コンテキスト
Agent 2: ダッシュボード(Tasks, Notes, Journal, Ideas)
Agent 3: チャット(DanteChatColumn, JustChatPage)
Agent 4: メール(ThreadList, DraftArea, EmailsPage)
Agent 5: プロジェクト(ProjectsPage, ProjectOverviewTab, NewProjectWizard)
Agent 6: CRM(すべてのサブタブ)
Agent 7: プロフィール、設定、Vault、通知
Agent 8: 管理スイート(すべてのページ)
Agent 1 は必ず先に完了しなければなりません。その出力は他のすべてのエージェントの入力となります。
使用するタイミング
- 体系的なデバッグで「バグなし」と判断されたが、ユーザーが壊れた UI を報告する場合
- Zustand ストアのアクションを変更した後(すべての呼び出し元をチェック)
- 共有状態に触れるリファクタリングを行った後
- リリース前の重要なユーザーフロー
- ボタンが「何も実行しない」場合 — これが最適なツールです
使用しないタイミング
- API レベルのバグ(間違ったレスポンス形状、欠落したエンドポイント)—
systematic-debuggingを使用 - スタイリング/レイアウトの問題 — 視覚的検査
- パフォーマンスの問題 — プロファイリングツール
他のスキルとの連携
/superpowers:systematic-debugging(他の54種類のバグを発見)の後に実行/superpowers:verification-before-completion(修正が機能することを検証)の前に実行/superpowers:test-driven-developmentにフィード — ここで見つかった各バグに対してテストを追加
例: このスキルを生み出したバグ
ThreadList.tsx "New Email" ボタン:
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ composeMode = true を設定
useEmailStore.getState().selectThread(null) // ✗ composeMode = false にリセット
}}
ストア定義:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← この静かなリセットがボタンを殺した
composeData: null,
redraftOpen: false,
})
体系的なデバッグでは見逃された理由:
- ボタンに onClick ハンドラがある(デッドではない)
- 両方の関数が存在する(配線不足ではない)
- どちらの関数もクラッシュしない(ランタイムエラーなし)
- データ型は正しい(型不一致なし)
クリックパス監査で発見される理由:
- ステップ1 で
selectThreadがcomposeModeをリセットすることをマッピング - ステップ2 でハンドラをトレース:最初の呼び出しは true を設定、二番目の呼び出しは false にリセット
- 判定:逐次的な取り消し — 最終状態がボタンの意図と矛盾