工具链阿新聊ai

Superpowers 工作流 · systematic-debugging 根因分析四阶段

Superpowers 工作流 · systematic debugging 根因分析四阶段 更新日期:2025/06 TL;DR: Superpowers 把 debug 拆成四个强制阶段:先查根因、再找模式、然后假设测试、最后实现。不允许没查根因就修 bug,不允许「试试这个先」,不允许 3 次修复失败后继续修—...

Superpowers 工作流 · systematic-debugging 根因分析四阶段

更新日期:2025/06

TL;DR: Superpowers 把 debug 拆成四个强制阶段:先查根因、再找模式、然后假设测试、最后实现。不允许没查根因就修 bug,不允许「试试这个先」,不允许 3 次修复失败后继续修——3 次失败说明是架构问题,不是实现问题。多组件系统加诊断日志找断裂点,根因用 backward tracing,修复后加多层防御让 bug 结构性不可能。

为什么要强制四阶段

随机修 bug 是时间黑洞。你「试试这个」花了 10 分钟,没生效;再「试试那个」又花 15 分钟,还是不行。一小时后你试了 8 个修复,引入了 3 个新 bug,原问题还在。

系统化 debug 不是仪式,是效率。从真实会话数据看:系统化方法 15-30 分钟解决,随机尝试 2-3 小时还在打转。首次修复成功率 95% vs 40%,新 bug 引入率接近零 vs 常见。

但知道系统化好和做到系统化是两回事。压力之下谁都想「试试这个快速修复」。Superpowers 的解决方案是强制执行——AI 在修 bug 前必须走完四个阶段。跳过任何一个阶段,skill 都会拦截并要求回到第一阶段。

铁律

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

不完成阶段一,不允许提出修复。

这不是「建议」,是强制规则。AI 说「我觉得可能是 X」,skill 回:「阶段一是什么?」AI 说「我直接修这个试试」,skill 回:「回到阶段一,查根因。」

四阶段流程

阶段一:根因调查

在尝试任何修复前:

1. 仔细读错误信息

别跳过错误或警告。它们经常包含精确解决方案。完整读栈跟踪,注意行号、文件路径、错误代码。

2. 稳定复现

你能可靠触发吗?精确步骤是什么?每次都发生?不能稳定复现?收集更多数据,别猜。

3. 检查最近变更

什么变了可能导致这个?git diff、最近提交、新依赖、配置变更、环境差异。

4. 多组件系统收集证据

当系统有多个组件(CI → build → signing,API → service → database):

在提出修复前,加诊断工具:

对每个组件边界:
  - 记录进入组件的数据
  - 记录离开组件的数据
  - 验证环境/配置传播
  - 检查每层状态

跑一次收集证据显示在哪断裂
然后分析证据识别失败组件
然后调查那个特定组件

示例(多层系统):

# 层 1:Workflow
echo "=== Secrets available in workflow: ==="
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"

# 层 2:Build script
echo "=== Env vars in build script: ==="
env | grep IDENTITY || echo "IDENTITY not in environment"

# 层 3:Signing script
echo "=== Keychain state: ==="
security list-keychains
security find-identity -v

# 层 4:Actual signing
codesign --sign "$IDENTITY" --verbose=4 "$APP"

揭示: 哪层失败(secrets → workflow ✓,workflow → build ✗)

5. 追踪数据流

当错误在调用栈深处时:

用 backward tracing 技术(见下文「根因追踪」):

  • 坏值从哪来?
  • 什么调了这个传坏值?
  • 一直往上追直到找到源头
  • 在源头修,别在症状处修

阶段二:模式分析

修复前找模式:

1. 找工作示例

在同一个代码库里定位类似的工作代码。什么工作的和坏的类似?

2. 对照参考

如果实现模式,完整读完参考实现。别略读——逐行读。应用前完全理解模式。

3. 识别差异

工作和坏之间有什么不同?列出每个差异,无论多小。别假设「这不可能有关系」。

4. 理解依赖

这需要其他什么组件?什么设置、配置、环境?它做什么假设?

阶段三:假设和测试

科学方法:

1. 形成单一假设

清楚说:「我认为 X 是根因因为 Y」。写下来。具体,别模糊。

2. 最小化测试

做最小可能改变测试假设。一次一个变量。别一次修多个东西。

3. 继续前验证

生效了?→ 阶段四。没生效?形成新假设。别在上面叠加更多修复。

4. 你不知道时

说「我不理解 X」。别假装知道。求助。继续研究。

阶段四:实现

修根因,不修症状:

1. 创建失败测试用例

最简单可能复现。可能的话自动化测试。没框架就一次性测试脚本。修复前必须有。

superpowers:test-driven-development skill 写正确的失败测试。

2. 实现单一修复

处理识别的根因。一次一个改变。没有「顺便改进」,没有打包重构。

3. 验证修复

测试现在通过了?其他测试没坏?问题真的解决了?

4. 修复不生效

停止。 计数:你试了几个修复?

  • 如果 < 3:回到阶段一,用新信息重分析
  • 如果 ≥ 3:停止并质疑架构(步骤 5)
  • 别不问架构就尝试修复 #4

5. 如果 3+ 修复失败:质疑架构

表明架构问题的模式:

  • 每个修复在不同地方揭示新共享状态/耦合/问题
  • 修复需要「大规模重构」才能实现
  • 每个修复在其他地方创造新症状

停止并质疑基础:

  • 这个模式从根本上合理吗?
  • 我们是「靠惯性坚持」吗?
  • 我们应该重构架构 vs 继续修症状?

尝试更多修复前和人类伙伴讨论

这不是失败的假设——这是错的架构。

根因追踪技术

Bug 经常在调用栈深处表现(git init 在错误目录、文件在错误位置创建、数据库用错误路径打开)。直觉是在错误出现处修,但那是治症状。

核心原则: 向后追踪调用链直到找到原始触发器,然后在源头修。

追踪过程

1. 观察症状

Error: git init failed in ~/project/packages/core

2. 找直接原因

什么代码直接导致这个?

await execFileAsync('git', ['init'], { cwd: projectDir });

3. 问:什么调用了这个?

WorktreeManager.createSessionWorktree(projectDir, sessionId)
  → called by Session.initializeWorkspace()
  → called by Session.create()
  → called by test at Project.create()

4. 继续往上追踪

传了什么值?

  • projectDir = '' (空字符串!)
  • 空字符串作为 cwd 解析为 process.cwd()
  • 那是源代码目录!

5. 找原始触发器

空字符串从哪来?

const context = setupCoreTest(); // Returns { tempDir: '' }
Project.create('name', context.tempDir); // beforeEach 前访问!

添加栈跟踪

不能手动追踪时加工具:

// 问题操作前
async function gitInit(directory: string) {
  const stack = new Error().stack;
  console.error('DEBUG git init:', {
    directory,
    cwd: process.cwd(),
    nodeEnv: process.env.NODE_ENV,
    stack,
  });

  await execFileAsync('git', ['init'], { cwd: directory });
}

关键: 测试里用 console.error()(不是 logger - 可能不显示)

运行并捕获:

npm test 2>&#x26;1 | grep 'DEBUG git init'

分析栈跟踪:

  • 找测试文件名
  • 找触发调用的行号
  • 识别模式(同一个测试?同一个参数?)

Defense-in-Depth 验证

当你修了一个由无效数据引起的 bug,在一个地方加验证感觉够了。但那个单一检查能被不同代码路径、重构、mock 绕过。

核心原则: 数据经过的每一层都验证。让 bug 结构性不可能。

四层防御

层 1:入口点验证 目的:在 API 边界拒绝明显无效输入

function createProject(name: string, workingDirectory: string) {
  if (!workingDirectory || workingDirectory.trim() === '') {
    throw new Error('workingDirectory cannot be empty');
  }
  if (!existsSync(workingDirectory)) {
    throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
  }
  // ... proceed
}

层 2:业务逻辑验证 目的:确保数据对这个操作合理

function initializeWorkspace(projectDir: string, sessionId: string) {
  if (!projectDir) {
    throw new Error('projectDir required for workspace initialization');
  }
  // ... proceed
}

层 3:环境守卫 目的:在特定上下文阻止危险操作

async function gitInit(directory: string) {
  // 测试中,拒绝 tmpdir 外的 git init
  if (process.env.NODE_ENV === 'test') {
    const normalized = normalize(resolve(directory));
    const tmpDir = normalize(resolve(tmpdir()));

    if (!normalized.startsWith(tmpDir)) {
      throw new Error(
        `Refusing git init outside temp dir during tests: ${directory}`
      );
    }
  }
  // ... proceed
}

层 4:调试工具 目的:捕获上下文取证

async function gitInit(directory: string) {
  const stack = new Error().stack;
  logger.debug('About to git init', {
    directory,
    cwd: process.cwd(),
    stack,
  });
  // ... proceed
}

Condition-Based Waiting

Flaky 测试经常用任意延迟猜时机。这创造竞态条件——快机器通过但 CI 或负载下失败。

核心原则: 等你真正关心的条件,不是猜它要多久。

核心模式

// ❌ 之前:猜时机
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();

// ✅ 之后:等条件
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();

快速模式

场景 模式
等事件 waitFor(() => events.find(e => e.type === 'DONE'))
等状态 waitFor(() => machine.state === 'ready')
等计数 waitFor(() => items.length >= 5)
等文件 waitFor(() => fs.existsSync(path))
复杂条件 waitFor(() => obj.ready && obj.value > 10)

实现

通用轮询函数:

async function waitFor&#x3C;T>(
  condition: () => T | undefined | null | false,
  description: string,
  timeoutMs = 5000
): Promise&#x3C;T> {
  const startTime = Date.now();

  while (true) {
    const result = condition();
    if (result) return result;

    if (Date.now() - startTime > timeoutMs) {
      throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
    }

    await new Promise(r => setTimeout(r, 10)); // 每 10ms 轮询
  }
}

find-polluter.sh 用法

如果有东西在测试期间出现但你不知道哪个测试引起的:

用二分脚本 find-polluter.sh

./find-polluter.sh '.git' 'src/**/*.test.ts'

逐个跑测试,停在第一个 polluter。见脚本用法。

工作原理: 二分法测试执行。第一次跑一半测试,如果有污染跑那半的一半,直到定位单个测试文件。

##红旗标志

抓到自己在想这些时:

  • 「先快速修复,回头调查」
  • 「试试改 X 看是否生效」
  • 「加多个改变,跑测试」
  • 「跳过测试,我手动验证」
  • 「可能是 X,让我修那个」
  • 「我不完全理解但这可能生效」
  • 「模式说 X 但我要不同适配」
  • 「主要问题是这些:[列出修复但没调查]」
  • 追踪数据流前提解决方案
  • 「再试一次修复」(已经试了 2+ 次)
  • 每次修复在不同地方揭示新问题

所有这些意味着:停止。回到阶段一。

如果 3+ 修复失败: 质疑架构(见阶段四步骤 5)

合理化借口表

借口 现实
「问题简单,不需要流程」 简单问题也有根因。流程对简单 bug 快。
「紧急,没时间走流程」 系统化 debug 比猜-检查打转快。
「先试试这个,再调查」 首个修复设定模式。从开始就做对。
「确认修复生效后我再写测试」 未测试修复不持久。测试先证明它。
「一次多个修复省时间」 无法分离什么生效。导致新 bug。
「参考太长,我要适配模式」 部分理解保证 bug。完整读。
「我看到问题,让我修它」 看到症状 ≠ 理解根因。
「再试一次修复」(2+ 失败后) 3+ 失败 = 架构问题。质疑模式,别再修。

权衡与局限

Superpowers 的系统化 debug 不是免费午餐。

时间感知: 走完四个阶段感觉比「试试这个」慢。第一个 10 分钟在调查而不是修代码。但真实数据表明:系统化 15-30 分钟解决,随机尝试 2-3 小时还在打转。慢是错觉。

学习曲线: 新人觉得「为什么不能直接改那行?」但经验丰富后知道:改那行经常破坏别的东西。系统化预防「一修多坏」。

紧急情况压力: 生产故障时管理层要「现在就修」。但紧急时系统化更关键——随机修复会导致中断更长。解释给管理层听:15 分钟调查 vs 2 小时中断扩展。

95% 的「无根因」是不完整调查: 有时你真的找不到根因——时序依赖、外部服务、环境特定。但绝大多数「无根因」是调查没做完。当你想放弃时,问自己:「我真的查了数据流在每个层的状态吗?」

延伸阅读

评论

0
登录后可以参与评论和讨论。
💬

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。