click-path-audit

click-path-audit

热门

追踪每个面向用户的按钮/触点的完整状态变化序列,以发现那些单独工作正常但相互抵消、产生错误最终状态或使UI处于不一致状态的错误。在以下情况下使用:系统调试未发现错误但用户报告按钮失效,或在对共享状态存储进行重大重构之后。

23万Star
3.5万Fork
更新于 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 → 重置 composeMode(属于 setComposeMode)
  reset → 重置所有

步骤2:审计每个触点

对于目标区域中的每个按钮/开关/表单提交:

触点:[按钮标签] 在 [组件:行号]
  处理程序:onClick → {
    调用1:functionA() → 设置 {X: true}
    调用2:functionB() → 设置 {Y: null} 重置 {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. [调用] → 设置 {字段: 值}
    2. [调用] → 重置 {字段: 值}  ← 冲突
  预期:[用户期望的]
  实际:[实际发生的]
  修复:[具体修复]

范围控制

此审计成本较高。适当限定范围:

  • 全应用审计: 在发布或重大重构后使用。按页面启动并行代理。
  • 单页面审计: 在构建新页面或用户报告按钮失效后使用。
  • 存储聚焦审计: 在修改 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
  • 结论:顺序撤销——最终状态与按钮意图矛盾