/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 儲存庫 / React Context:
對於每個動作/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 → 重置 composeMode(由 setComposeMode 擁有)
reset → 重置所有
步驟 2:審計每個觸點
針對目標區域中的每個按鈕/切換/表單提交:
TOUCHPOINT: [按鈕標籤] 位於 [元件:行號]
HANDLER: 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]
Touchpoint:[按鈕標籤] 位於 [檔案:行號]
模式:[順序撤銷 / 非同步競態 / 閉包過時 / 缺失轉換 / 死路 / useEffect 干擾]
處理常式:[函式名稱或內嵌]
追蹤:
1. [呼叫] → 設定 {欄位: 值}
2. [呼叫] → 重置 {欄位: 值} ← 衝突
預期:[用戶的預期]
實際:[實際發生的情況]
修復:[具體修復方法]
範圍控制
此審計成本高昂。請適當設定範圍:
- 完整應用審計: 在發布時或重大重構後使用。按頁面啟動並行代理。
- 單一頁面審計: 在建立新頁面後,或用戶回報按鈕失效時使用。
- 儲存庫聚焦審計: 在修改 Zustand 儲存庫後使用——審計所有受影響動作的使用者。
進行完整應用的建議代理拆分:
Agent 1:映射所有狀態儲存庫(步驟 1)——此為所有其他代理的共享上下文
Agent 2:儀表板(任務、筆記、日誌、想法)
Agent 3:聊天(DanteChatColumn、JustChatPage)
Agent 4:電子郵件(ThreadList、DraftArea、EmailsPage)
Agent 5:專案(ProjectsPage、ProjectOverviewTab、NewProjectWizard)
Agent 6:CRM(所有子分頁)
Agent 7:個人資料、設定、保險庫、通知
Agent 8:管理套件(所有頁面)
Agent 1 必須先完成。其輸出是所有其他代理的輸入。
使用時機
- 系統性除錯發現「沒有錯誤」,但用戶回報 UI 失效時
- 修改任何 Zustand 儲存庫動作後(檢查所有呼叫者)
- 任何涉及共享狀態的重構後
- 發布前,在關鍵用戶流程上
- 當按鈕「毫無反應」時——這是專門的工具
不應使用時機
- 針對 API 層級的錯誤(錯誤的回應形狀、遺漏端點)——使用 system-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
- 判決:順序撤銷——最終狀態與按鈕意圖矛盾