root-cause-tracing

systematic-debugging · 附屬文件

Root Cause Tracing

根因追蹤

Overview

概述

Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom.

Bug 往往在呼叫堆疊深處才顯現(git init 執行在錯誤的目錄、檔案建立到錯誤的位置、資料庫用錯誤的路徑開啟)。直覺會讓你想在錯誤出現的地方修,但那是治標不治本。

Core principle: Trace backward through the call chain until you find the original trigger, then fix at the source.

核心原則: 沿著呼叫鏈一路回溯,直到找到最初的觸發點,然後在源頭修復。

When to Use

使用時機

digraph when_to_use {
    "Bug appears deep in stack?" [shape=diamond];
    "Can trace backwards?" [shape=diamond];
    "Fix at symptom point" [shape=box];
    "Trace to original trigger" [shape=box];
    "BETTER: Also add defense-in-depth" [shape=box];

    "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
    "Can trace backwards?" -> "Trace to original trigger" [label="yes"];
    "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
    "Trace to original trigger" -> "BETTER: Also add defense-in-depth";
}
digraph when_to_use {
    "Bug appears deep in stack?" [shape=diamond];
    "Can trace backwards?" [shape=diamond];
    "Fix at symptom point" [shape=box];
    "Trace to original trigger" [shape=box];
    "BETTER: Also add defense-in-depth" [shape=box];

    "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
    "Can trace backwards?" -> "Trace to original trigger" [label="yes"];
    "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
    "Trace to original trigger" -> "BETTER: Also add defense-in-depth";
}

Use when: - Error happens deep in execution (not at entry point) - Stack trace shows long call chain - Unclear where invalid data originated - Need to find which test/code triggers the problem

適用時機: - 錯誤發生在執行過程深處(不是在進入點) - 堆疊追蹤顯示很長的呼叫鏈 - 不清楚無效資料從哪裡來 - 需要找出是哪個測試/程式碼觸發了問題

The Tracing Process

追蹤流程

1. Observe the Symptom

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

1. 觀察症狀

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

2. Find Immediate Cause

What code directly causes this?

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

2. 找出直接原因

直接造成這問題的程式碼是什麼?

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

3. Ask: What Called This?

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

3. 問:是誰呼叫它的?

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

4. Keep Tracing Up

What value was passed? - projectDir = '' (empty string!) - Empty string as cwd resolves to process.cwd() - That's the source code directory!

4. 繼續往上追

傳進來的是什麼值? - projectDir = ''(空字串!) - 空字串作為 cwd 會解析成 process.cwd() - 那就是原始碼目錄!

5. Find Original Trigger

Where did empty string come from?

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

5. 找出最初的觸發點

空字串從哪裡來的?

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

Adding Stack Traces

加入堆疊追蹤

When you can't trace manually, add instrumentation:

當你無法手動追蹤時,加入儀器:

// Before the problematic operation
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 });
}
// Before the problematic operation
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 });
}

Critical: Use console.error() in tests (not logger - may not show)

關鍵: 測試中用 console.error()(不要用 logger——可能不會顯示)

Run and capture:

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

執行並擷取:

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

Analyze stack traces: - Look for test file names - Find the line number triggering the call - Identify the pattern (same test? same parameter?)

分析堆疊追蹤: - 尋找測試檔名 - 找出觸發呼叫的行號 - 識別模式(同一個測試?同一個參數?)

Finding Which Test Causes Pollution

找出是哪個測試造成污染

If something appears during tests but you don't know which test:

如果測試期間出現某些東西,但你不知道是哪個測試:

Use the bisection script find-polluter.sh in this directory:

使用本目錄中的二分搜尋腳本 find-polluter.sh

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

Runs tests one-by-one, stops at first polluter. See script for usage.

它會一個一個跑測試,在第一個污染源停下。用法請見腳本。

Real Example: Empty projectDir

真實案例:空的 projectDir

Symptom: .git created in packages/core/ (source code)

症狀: .git 被建立在 packages/core/(原始碼)裡

Trace chain: 1. git init runs in process.cwd() ← empty cwd parameter 2. WorktreeManager called with empty projectDir 3. Session.create() passed empty string 4. Test accessed context.tempDir before beforeEach 5. setupCoreTest() returns { tempDir: '' } initially

追蹤鏈: 1. git initprocess.cwd() 執行 ← 空的 cwd 參數 2. WorktreeManager 收到空的 projectDir 3. Session.create() 傳入空字串 4. 測試在 beforeEach 之前就讀取 context.tempDir 5. setupCoreTest() 初始回傳 { tempDir: '' }

Root cause: Top-level variable initialization accessing empty value

根因: 頂層變數初始化時讀取了空值

Fix: Made tempDir a getter that throws if accessed before beforeEach

修復: 把 tempDir 改成 getter,若在 beforeEach 之前被讀取就丟例外

Also added defense-in-depth: - Layer 1: Project.create() validates directory - Layer 2: WorkspaceManager validates not empty - Layer 3: NODE_ENV guard refuses git init outside tmpdir - Layer 4: Stack trace logging before git init

同時加入縱深防禦: - 第一層:Project.create() 驗證目錄 - 第二層:WorkspaceManager 驗證非空 - 第三層:NODE_ENV 守衛,拒絕在 tmpdir 之外執行 git init - 第四層:git init 之前記錄堆疊追蹤

Key Principle

核心原則

digraph principle {
    "Found immediate cause" [shape=ellipse];
    "Can trace one level up?" [shape=diamond];
    "Trace backwards" [shape=box];
    "Is this the source?" [shape=diamond];
    "Fix at source" [shape=box];
    "Add validation at each layer" [shape=box];
    "Bug impossible" [shape=doublecircle];
    "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];

    "Found immediate cause" -> "Can trace one level up?";
    "Can trace one level up?" -> "Trace backwards" [label="yes"];
    "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
    "Trace backwards" -> "Is this the source?";
    "Is this the source?" -> "Trace backwards" [label="no - keeps going"];
    "Is this the source?" -> "Fix at source" [label="yes"];
    "Fix at source" -> "Add validation at each layer";
    "Add validation at each layer" -> "Bug impossible";
}
digraph principle {
    "Found immediate cause" [shape=ellipse];
    "Can trace one level up?" [shape=diamond];
    "Trace backwards" [shape=box];
    "Is this the source?" [shape=diamond];
    "Fix at source" [shape=box];
    "Add validation at each layer" [shape=box];
    "Bug impossible" [shape=doublecircle];
    "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];

    "Found immediate cause" -> "Can trace one level up?";
    "Can trace one level up?" -> "Trace backwards" [label="yes"];
    "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
    "Trace backwards" -> "Is this the source?";
    "Is this the source?" -> "Trace backwards" [label="no - keeps going"];
    "Is this the source?" -> "Fix at source" [label="yes"];
    "Fix at source" -> "Add validation at each layer";
    "Add validation at each layer" -> "Bug impossible";
}

NEVER fix just where the error appears. Trace back to find the original trigger.

絕對不要只修錯誤出現的地方。 回溯找出最初的觸發點。

Stack Trace Tips

堆疊追蹤小技巧

In tests: Use console.error() not logger - logger may be suppressed Before operation: Log before the dangerous operation, not after it fails Include context: Directory, cwd, environment variables, timestamps Capture stack: new Error().stack shows complete call chain

測試中:console.error(),不要用 logger——logger 可能被抑制 操作之前: 在危險操作「之前」記錄,不要在失敗之後 包含上下文: 目錄、cwd、環境變數、時間戳 擷取堆疊: new Error().stack 會顯示完整的呼叫鏈

Real-World Impact

實際影響

From debugging session (2025-10-03): - Found root cause through 5-level trace - Fixed at source (getter validation) - Added 4 layers of defense - 1847 tests passed, zero pollution

來自除錯 session(2025-10-03): - 透過 5 層追蹤找出根因 - 在源頭修復(getter 驗證) - 加入 4 層防禦 - 1847 個測試全數通過,零污染