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 → 重置 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
- 结论:顺序撤销——最终状态与按钮意图矛盾






