/click-path-audit — 行動フロー監査(Behavioural Flow Audit)
静的コード解析では見逃されるバグ、すなわち状態相互作用の副作用、逐次呼び出し間の競合状態、そして互いに暗黙のうちに元に戻すハンドラを発見します。
これが解決する問題
従来のデバッグでチェックされること:
- 関数は存在するか?(配線漏れ)
- クラッシュするか?(実行時エラー)
- 正しい型を返すか?(データフロー)
しかし、以下はチェックされません:
- 最終的なUI状態は、ボタンラベルが約束するものと一致しているか?
- 関数Bは、関数Aが設定した状態を静かに元に戻していないか?
- 共有状態(Zustand/Redux/コンテキスト)に、意図したアクションを打ち消す副作用はないか?
実例: 「新規メール」ボタンが 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コンテキストについて:
各アクション/セッターについて:
- どのフィールドを設定するか?
- 副作用として他のフィールドをリセットするか?
- ドキュメント: actionName → {sets: [...], resets: [...]}
これが重要なリファレンスです。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 → すべてをリセット
ステップ2: 各タッチポイントの監査
対象領域の各ボタン/トグル/フォーム送信について:
TOUCHPOINT: [ボタンラベル] in [コンポーネント:行]
ハンドラ: onClick → {
call 1: functionA() → sets {X: true}
call 2: functionB() → sets {Y: null} RESETS {X: false} ← 競合
}
期待値: ユーザーは [ボタンラベルが約束する内容の説明] を目にする
実際の動作: functionB が X をリセットしたため X は false
判定: バグ — [説明]
以下の各バグパターンをチェックします:
パターン1: 逐次アンドゥ
handler() {
setState_A(true) // sets X = true
setState_B(null) // 副作用: X を false にリセット
}
// 結果: X は false。最初の呼び出しは無意味だった。
パターン2: 非同期レース
handler() {
fetchA().then(() => setState({ loading: false }))
fetchB().then(() => setState({ loading: true }))
}
// 結果: 最終的な loading 状態は、どちらが先に解決するかに依存する
パターン3: スタルクロージャ
const [count, setCount] = useState(0)
const handler = useCallback(() => {
setCount(count + 1) // 古い count をキャプチャ
setCount(count + 1) // 同じ古い count — 1 だけ増加、2 ではない
}, [count])
パターン4: 状態遷移欠落
// ボタンは「保存」だが、ハンドラは検証のみで実際には保存しない
// ボタンは「削除」だが、ハンドラはフラグを設定するだけでAPIを呼び出さない
// ボタンは「送信」だが、APIエンドポイントが削除されている/壊れている
パターン5: 条件付きデッドパス
handler() {
if (someState) { // someState はこの時点で常に false
doTheActualThing() // 決して到達しない
}
}
パターン6: useEffect 干渉
// ボタンが stateX = true を設定
// useEffect が stateX を監視し、それを false にリセット
// ユーザーには何も起こっていないように見える
ステップ3: 報告
見つかったバグごとに:
CLICK-PATH-NNN: [重要度: 致命的/高/中/低]
タッチポイント: [ボタンラベル] in [ファイル:行]
パターン: [逐次アンドゥ / 非同期レース / スタルクロージャ / 状態遷移欠落 / デッドパス / useEffect 干渉]
ハンドラ: [関数名またはインライン]
トレース:
1. [呼び出し] → sets {フィールド: 値}
2. [呼び出し] → RESETS {フィールド: 値} ← 競合
期待値: [ユーザーが期待する内容]
実際の動作: [実際に起こること]
修正: [具体的な修正方法]
スコープ管理
この監査はコストが高いため、適切に範囲を設定します:
- 完全なアプリ監査: リリース時または大規模リファクタリング後に使用。ページごとに並列エージェントを起動。
- 単一ページ監査: 新しいページを構築した後、またはユーザーがボタンの不具合を報告した後に使用。
- ストア集中監査: 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 「新規メール」ボタン:
onClick={() => {
useEmailStore.getState().setComposeMode(true) // ✓ sets composeMode = true
useEmailStore.getState().selectThread(null) // ✗ RESETS composeMode = false
}}
ストア定義:
selectThread: (thread) => set({
selectedThread: thread,
selectedThreadId: thread?.id ?? null,
messages: [],
drafts: [],
selectedDraft: null,
summary: null,
composeMode: false, // ← この静かなリセットがボタンを殺した
composeData: null,
redraftOpen: false,
})
システムデバッグが見逃した理由:
- ボタンには onClick ハンドラがある(無効ではない)
- 両方の関数が存在する(配線漏れがない)
- どちらの関数もクラッシュしない(実行時エラーがない)
- データ型は正しい(型の不一致がない)
Click-path 監査がそれを捉えた理由:
- ステップ1:
selectThreadがcomposeModeをリセットすることをマッピング - ステップ2: ハンドラをトレース: call 1 は true を設定、call 2 は false にリセット
- 判定: 逐次アンドゥ — 最終状態がボタンの意図と矛盾