click-path-audit

click-path-audit

熱門

追蹤每個使用者可見的按鈕/觸控點,透過其完整的狀態變更序列,找出那些個別功能正常運作,但彼此抵消、產生錯誤最終狀態,或讓 UI 處於不一致狀態的錯誤。使用時機:系統性除錯未發現錯誤,但使用者回報按鈕失效;或任何重大重構觸及共用狀態儲存後。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
唯讀
名稱
click-path-audit
描述

追蹤每個使用者可見的按鈕/觸控點,透過其完整的狀態變更序列,找出那些個別功能正常運作,但彼此抵消、產生錯誤最終狀態,或讓 UI 處於不一致狀態的錯誤。使用時機:系統性除錯未發現錯誤,但使用者回報按鈕失效;或任何重大重構觸及共用狀態儲存後。

/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
  • 判定:順序取消——最終狀態與按鈕意圖矛盾