/click-path-audit — 行为流审计
发现静态代码阅读遗漏的缺陷:状态交互副作用、顺序调用间的竞态条件、以及处理函数互相静默撤销。
此技能解决的问题
传统调试检查:
- 函数是否存在?(缺少接线)
- 是否会崩溃?(运行时错误)
- 是否返回正确类型?(数据流)
但它不检查:
- 最终UI状态是否与按钮标签所承诺的一致?
- 函数B是否静默撤销了函数A刚刚完成的操作?
- 共享状态(Zustand/Redux/上下文)是否存在副作用,抵消了预期操作?
真实案例:一个“新建邮件”按钮调用了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}
...
危险重置(清除非自身所有状态的 action):
selectThread → 重置 composeMode(由 setComposeMode 拥有)
reset → 重置所有内容
步骤2:审计每个触点
针对目标区域内的每个按钮/开关/表单提交:
TOUCHPOINT: [按钮标签] in [组件:行号]
HANDLER: onClick → {
call 1: functionA() → sets {X: true}
call 2: functionB() → sets {Y: null} RESETS {X: false} ← 冲突
}
EXPECTED: 用户看到 [按钮标签所承诺内容的描述]
ACTUAL: X 为 false,因为 functionB 重置了它
VERDICT: 缺陷 — [描述]
检查以下每种缺陷模式:
模式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: [按钮标签] in [文件:行号]
Pattern: [顺序撤销 / 异步竞态 / 闭包过期 / 缺失转换 / 死路径 / useEffect干扰]
Handler: [函数名或内联]
Trace:
1. [调用] → sets {字段: 值}
2. [调用] → RESETS {字段: 值} ← 冲突
Expected: [用户期望的结果]
Actual: [实际发生的结果]
Fix: [具体修复方案]
范围控制
此审计成本较高。请合理确定范围:
- 全应用审计: 发布时或重大重构后使用。按页面启动并行代理。
- 单页面审计: 构建新页面后或用户报告按钮失效后使用。
- 存储聚焦审计: 修改某个 Zustand store 后使用——审计所有使用变更操作的消费者。
推荐的全应用代理分工:
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 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
}}
Store 定义:
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
- 判定:顺序撤销——最终状态与按钮意图相矛盾