Trace every user-facing button/touchpoint through its full state change sequence to find bugs where functions individually work but cancel each other out, produce wrong final state, or leave the UI in an inconsistent state. Use when: systematic debugging found no bugs but users report broken buttons, or after any major refactor touching shared state stores.
/click-path-audit — 行為流程稽核
找出靜態程式碼審查遺漏的錯誤:狀態互動副作用、順序呼叫間的競爭條件,以及默默互相取消的處理函式。
此技能解決的問題
傳統除錯檢查:
- 函式是否存在?(缺少接線)
- 是否崩潰?(執行時期錯誤)
- 是否回傳正確型別?(資料流)
但它不檢查:
- 最終 UI 狀態是否與按鈕標籤承諾的一致?
- 函式 B 是否默默取消函式 A 剛做的變更?
- 共用狀態(Zustand/Redux/context)是否有副作用抵消預期動作?
真實案例:一個「新增郵件」按鈕呼叫了 setComposeMode(true) 然後 selectThread(null)。兩者個別都正常運作。但 selectThread 有一個副作用會重設 composeMode: false。按鈕完全沒作用。系統性除錯找到了 54 個錯誤——但這個被漏掉了。
運作方式
針對目標區域中的每個互動觸控點:
1. 識別處理函式(onClick, onSubmit, onChange 等)
2. 依序追蹤處理函式中的每個函式呼叫
3. 對每個函式呼叫:
a. 它讀取了哪些狀態?
b. 它寫入了哪些狀態?
c. 它對共用狀態有副作用嗎?
d. 它是否作為副作用重設/清除任何狀態?
4. 檢查:後面的呼叫是否取消了前面呼叫的狀態變更?
5. 檢查:最終狀態是否符合使用者對按鈕標籤的預期?
6. 檢查:是否存在競爭條件(非同步呼叫以錯誤順序解析)?
執行步驟
步驟 1:繪製狀態儲存對應圖
在稽核任何觸控點之前,為每個狀態儲存動作建立副作用對應圖:
對於範圍內的每個 Zustand store / React context:
對於每個 action/setter:
- 它設定了哪些欄位?
- 它是否作為副作用重設了其他欄位?
- 記錄: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 → resets everything
步驟 2:稽核每個觸控點
針對目標區域中的每個按鈕/切換/表單提交:
觸控點:[按鈕標籤] 在 [元件:行號]
處理函式:onClick → {
呼叫 1: functionA() → sets {X: true}
呼叫 2: functionB() → sets {Y: null} RESETS {X: false} ← 衝突
}
預期:使用者看到 [按鈕標籤承諾的描述]
實際:X 為 false,因為 functionB 重設了它
判定:錯誤 — [描述]
檢查以下每種錯誤模式:
模式 1:順序取消
handler() {
setState_A(true) // 設定 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:[嚴重性:CRITICAL/HIGH/MEDIUM/LOW]
觸控點:[按鈕標籤] 在 [檔案:行號]
模式:[順序取消 / 非同步競爭 / 過時閉包 / 缺少轉換 / 死路 / useEffect 干擾]
處理函式:[函式名稱或內聯]
追蹤:
1. [呼叫] → sets {欄位: 值}
2. [呼叫] → RESETS {欄位: 值} ← 衝突
預期:[使用者預期的結果]
實際:[實際發生的結果]
修正:[具體修正方式]
範圍控制
此稽核成本較高。請適當設定範圍:
- 完整應用程式稽核: 用於上線時或重大重構後。每個頁面啟動平行代理。
- 單頁稽核: 用於建立新頁面後,或使用者回報按鈕失效後。
- 儲存聚焦稽核: 用於修改 Zustand store 後——稽核所有使用變更動作的消費者。
完整應用程式的建議代理分割:
代理 1:繪製所有狀態儲存對應圖(步驟 1)——這是所有其他代理的共用上下文
代理 2:儀表板(任務、筆記、日誌、想法)
代理 3:聊天(DanteChatColumn, JustChatPage)
代理 4:郵件(ThreadList, DraftArea, EmailsPage)
代理 5:專案(ProjectsPage, ProjectOverviewTab, NewProjectWizard)
代理 6:CRM(所有子分頁)
代理 7:個人資料、設定、保險庫、通知
代理 8:管理套件(所有頁面)
代理 1 必須先完成。其輸出是所有其他代理的輸入。
使用時機
- 系統性除錯發現「無錯誤」,但使用者回報 UI 失效時
- 修改任何 Zustand store 動作後(檢查所有呼叫者)
- 任何觸及共用狀態的重構後
- 發行前,針對關鍵使用者流程
- 當按鈕「沒反應」時——這是專門的工具
不應使用時機
- API 層級錯誤(錯誤的回應結構、缺少端點)——使用 systematic-debugging
- 樣式/佈局問題——視覺檢查
- 效能問題——效能分析工具
與其他技能的整合
- 在
/superpowers:systematic-debugging之後執行(它會找出其他 54 種錯誤類型) - 在
/superpowers:verification-before-completion之前執行(它會驗證修正是否有效) - 輸入至
/superpowers:test-driven-development——此處發現的每個錯誤都應有對應測試
範例:啟發此技能的錯誤
ThreadList.tsx「新增郵件」按鈕:
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 追蹤處理函式:呼叫 1 設定 true,呼叫 2 重設 false
- 判定:順序取消——最終狀態與按鈕意圖矛盾






