<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tdd on Tarragon</title><link>https://tarrragon.github.io/blog/tags/tdd/</link><description>Recent content in Tdd on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 23 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/tdd/index.xml" rel="self" type="application/rss+xml"/><item><title>Test-First：先看到 RED 才相信 GREEN</title><link>https://tarrragon.github.io/blog/report/test-first-red-before-green/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/test-first-red-before-green/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>測試本身需要被驗證。&lt;/strong> 一個從沒看過 RED 的測試 = 未驗證的訊號、不是「會抓回歸的測試」。&lt;/p>
&lt;p>驗證一個測試真的有用、需要看到兩個訊號：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>RED&lt;/strong>：測試在「該失敗的版本」上失敗（buggy code → 紅）&lt;/li>
&lt;li>&lt;strong>GREEN&lt;/strong>：測試在「該通過的版本」上通過（fixed code → 綠）&lt;/li>
&lt;/ol>
&lt;p>只看過 GREEN = 不知道測試有沒有 catch 能力；只看過 RED = 不知道修復有沒有真的解問題。&lt;strong>兩個都看到 = 測試 + 修復都被驗證&lt;/strong>。&lt;/p>
&lt;p>跳過 RED 把驗收標準降到「測試跑得通」、漏掉「測試自己有沒有 bug」這層。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼測試需要被驗證">為什麼測試需要被驗證&lt;/h2>
&lt;h3 id="測試是程式-about-程式會有-bug">測試是程式 about 程式、會有 bug&lt;/h3>
&lt;p>測試本身是程式碼、跟其他程式碼一樣會有 bug：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>測試 bug 類型&lt;/th>
 &lt;th>症狀&lt;/th>
 &lt;th>為什麼跳過 RED 看不到&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Selector 寫錯&lt;/td>
 &lt;td>永遠抓不到目標元素、assertion always 過&lt;/td>
 &lt;td>GREEN（因為沒 assert 到任何東西）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Assertion 太寬&lt;/td>
 &lt;td>&lt;code>expect(x).toBeDefined()&lt;/code> 對 buggy / fixed 都過&lt;/td>
 &lt;td>GREEN（assertion 通過範圍太大）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Setup / fixture 錯&lt;/td>
 &lt;td>測試根本沒跑、報告假性綠&lt;/td>
 &lt;td>GREEN（測試被 skip 但沒人注意）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Race condition / 時機錯&lt;/td>
 &lt;td>Buggy 時剛好在 race window 過、fixed 時也過&lt;/td>
 &lt;td>GREEN（取決於非常規 case）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試對象選錯&lt;/td>
 &lt;td>測 happy path、bug 在邊界&lt;/td>
 &lt;td>GREEN（沒覆蓋 bug 所在的範圍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這五種都會讓「跑測試一次就 GREEN」是個假訊號 — 測試 pass 不代表測試 catch 到該 catch 的東西。&lt;/p>
&lt;h3 id="red-是測試的使用者驗收">RED 是測試的「使用者驗收」&lt;/h3>
&lt;p>對使用者代碼、我們會用「驗收訊號」（功能跑得對）證明它有用。測試也需要驗收訊號。&lt;/p>
&lt;p>「測試 catch 到 bug」這個能力的驗收訊號 = &lt;strong>「在有 bug 的代碼上失敗」&lt;/strong>。沒看過這個訊號就相信測試 = 跳過驗收。&lt;/p>
&lt;p>對應 &lt;a href="../two-occurrence-threshold/">#42 2 次門檻&lt;/a>：一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑（一次 fail 一次 pass）的高資訊量訊號。&lt;/p>
&lt;hr>
&lt;h2 id="多面向四種情境的-red-green-應用">多面向：四種情境的 RED-GREEN 應用&lt;/h2>
&lt;h3 id="情境-1修-bug">情境 1：修 bug&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">1. 先寫一個 test 重現 bug 為失敗 — 例：「filter 後 0 筆但 source 還有未載入時、應該顯示 explicit empty 而非 silent」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. 跑測試 → RED（證明測試抓到 bug、bug 真的存在）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. 修 code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. 跑測試 → GREEN（證明修對了 + 測試會抓回歸）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跳過第 2 步 = 不知道測試會不會抓到、不知道 bug 真的有沒有。&lt;/p>
&lt;h3 id="情境-2加-feature">情境 2：加 feature&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">1. 寫 acceptance test 描述新 feature 該有的行為
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. 跑測試 → RED（feature 還沒實作、應該 fail；如果 GREEN 就表示 feature 已經存在或測試太寬）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. 實作 feature
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. 跑測試 → GREEN&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>加 feature 時跳過 RED 風險：feature 被誤以為實作但實際是 stub、或測試根本沒驗到 feature。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>測試本身需要被驗證。</strong> 一個從沒看過 RED 的測試 = 未驗證的訊號、不是「會抓回歸的測試」。</p>
<p>驗證一個測試真的有用、需要看到兩個訊號：</p>
<ol>
<li><strong>RED</strong>：測試在「該失敗的版本」上失敗（buggy code → 紅）</li>
<li><strong>GREEN</strong>：測試在「該通過的版本」上通過（fixed code → 綠）</li>
</ol>
<p>只看過 GREEN = 不知道測試有沒有 catch 能力；只看過 RED = 不知道修復有沒有真的解問題。<strong>兩個都看到 = 測試 + 修復都被驗證</strong>。</p>
<p>跳過 RED 把驗收標準降到「測試跑得通」、漏掉「測試自己有沒有 bug」這層。</p>
<hr>
<h2 id="為什麼測試需要被驗證">為什麼測試需要被驗證</h2>
<h3 id="測試是程式-about-程式會有-bug">測試是程式 about 程式、會有 bug</h3>
<p>測試本身是程式碼、跟其他程式碼一樣會有 bug：</p>
<table>
  <thead>
      <tr>
          <th>測試 bug 類型</th>
          <th>症狀</th>
          <th>為什麼跳過 RED 看不到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Selector 寫錯</td>
          <td>永遠抓不到目標元素、assertion always 過</td>
          <td>GREEN（因為沒 assert 到任何東西）</td>
      </tr>
      <tr>
          <td>Assertion 太寬</td>
          <td><code>expect(x).toBeDefined()</code> 對 buggy / fixed 都過</td>
          <td>GREEN（assertion 通過範圍太大）</td>
      </tr>
      <tr>
          <td>Setup / fixture 錯</td>
          <td>測試根本沒跑、報告假性綠</td>
          <td>GREEN（測試被 skip 但沒人注意）</td>
      </tr>
      <tr>
          <td>Race condition / 時機錯</td>
          <td>Buggy 時剛好在 race window 過、fixed 時也過</td>
          <td>GREEN（取決於非常規 case）</td>
      </tr>
      <tr>
          <td>測試對象選錯</td>
          <td>測 happy path、bug 在邊界</td>
          <td>GREEN（沒覆蓋 bug 所在的範圍）</td>
      </tr>
  </tbody>
</table>
<p>這五種都會讓「跑測試一次就 GREEN」是個假訊號 — 測試 pass 不代表測試 catch 到該 catch 的東西。</p>
<h3 id="red-是測試的使用者驗收">RED 是測試的「使用者驗收」</h3>
<p>對使用者代碼、我們會用「驗收訊號」（功能跑得對）證明它有用。測試也需要驗收訊號。</p>
<p>「測試 catch 到 bug」這個能力的驗收訊號 = <strong>「在有 bug 的代碼上失敗」</strong>。沒看過這個訊號就相信測試 = 跳過驗收。</p>
<p>對應 <a href="../two-occurrence-threshold/">#42 2 次門檻</a>：一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑（一次 fail 一次 pass）的高資訊量訊號。</p>
<hr>
<h2 id="多面向四種情境的-red-green-應用">多面向：四種情境的 RED-GREEN 應用</h2>
<h3 id="情境-1修-bug">情境 1：修 bug</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 先寫一個 test 重現 bug 為失敗 — 例：「filter 後 0 筆但 source 還有未載入時、應該顯示 explicit empty 而非 silent」
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 跑測試 → RED（證明測試抓到 bug、bug 真的存在）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 修 code
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 跑測試 → GREEN（證明修對了 + 測試會抓回歸）</span></span></code></pre></div><p>跳過第 2 步 = 不知道測試會不會抓到、不知道 bug 真的有沒有。</p>
<h3 id="情境-2加-feature">情境 2：加 feature</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 寫 acceptance test 描述新 feature 該有的行為
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 跑測試 → RED（feature 還沒實作、應該 fail；如果 GREEN 就表示 feature 已經存在或測試太寬）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 實作 feature
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 跑測試 → GREEN</span></span></code></pre></div><p>加 feature 時跳過 RED 風險：feature 被誤以為實作但實際是 stub、或測試根本沒驗到 feature。</p>
<h3 id="情境-3refactor">情境 3：Refactor</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 確認當前測試 GREEN（baseline）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Refactor（不改 behavior）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 跑測試 → 仍 GREEN</span></span></code></pre></div><p>Refactor <strong>不需要</strong> RED — 因為 behavior 沒變。如果 refactor 後變 RED、表示 refactor 改到了 behavior（變成隱性 bug）、要回頭看。</p>
<h3 id="情境-4偵錯不確定-bug-是什麼">情境 4：偵錯（不確定 bug 是什麼）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 寫一個 test 嘗試重現問題
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 跑測試 → 看是 RED 還是 GREEN：
</span></span><span class="line"><span class="ln">3</span><span class="cl">   - RED → 重現成功、現在可以著手修
</span></span><span class="line"><span class="ln">4</span><span class="cl">   - GREEN → 沒重現到 / 測試寫錯 / bug 在別處 → 重新理解 bug
</span></span><span class="line"><span class="ln">5</span><span class="cl">3. 修
</span></span><span class="line"><span class="ln">6</span><span class="cl">4. 跑測試 → GREEN</span></span></code></pre></div><p>「看是 RED 還是 GREEN」這個動作本身是 debug 訊號 — 比單純猜根因有用。</p>
<hr>
<h2 id="只看-green-不看-red是反模式">「只看 GREEN 不看 RED」是反模式</h2>
<h3 id="反模式-1修完才補測試test-after">反模式 1：修完才補測試（Test-after）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 修 bug code
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 寫測試
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 跑測試 → GREEN
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. ship</span></span></code></pre></div><p>問題：測試從沒跑過 buggy code、不知道它能不能抓到 bug。未來 regression 進來、測試可能仍然 GREEN（測試本身有 bug）。</p>
<h3 id="反模式-2快速跑一下測試沒看訊號">反模式 2：「快速跑一下測試」沒看訊號</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 寫測試
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 跑「應該 pass 吧」、不仔細看輸出
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 看到 PASS → 安心</span></span></code></pre></div><p>問題：可能測試 skip 了、可能測試 zero assertions、可能環境錯了。需要看「具體 catch 到什麼」、不只是「是否 PASS」。</p>
<h3 id="反模式-3測試-pass-但-coverage-是-0">反模式 3：測試 PASS 但 coverage 是 0</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 寫測試 file
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. CI 跑、看到「all green」
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 沒看 coverage report</span></span></code></pre></div><p>問題：測試文件存在但實際沒 import / 沒執行、CI 報告 GREEN 是因為「沒 fail」不是「有 catch」。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「先看 RED 再看 GREEN」原則在大多數情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure refactor</td>
          <td>沒 behavior 變更、本來就 GREEN、RED 反而表示出問題</td>
      </tr>
      <tr>
          <td>純探索 / spike</td>
          <td>不寫測試、用 console / 手動驗證、不在「測試驗收」範圍</td>
      </tr>
      <tr>
          <td>Build / config 改動沒邏輯</td>
          <td>沒 testable behavior、沒測試可言</td>
      </tr>
      <tr>
          <td>顯眼的 syntax 錯誤修復</td>
          <td>改一個 typo、測試會在 build 階段就 fail、不需要刻意 RED</td>
      </tr>
  </tbody>
</table>
<p>四類共同特徵：<strong>沒有「行為差異」可被測試 catch</strong> — 本原則建立在「測試該 catch 的事」上、沒事可 catch 時自然不適用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑（一次 fail 一次 pass）的真訊號</td>
      </tr>
      <tr>
          <td><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a></td>
          <td>測試 PASS ≠ 測試 verified；同個「訊號需要驗證」結構</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>跳過 RED 是便利（不用切 branch / 不重 build）、走 RED-GREEN 是對齊</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>本卡是 Checkpoint 2「開發中」+ Checkpoint 3「Ship 前」內部的具體協議</td>
      </tr>
  </tbody>
</table>
<p>本卡是把「測試這個動作本身」放進驗收體系：寫測試是動作、跑測試的訊號才是驗收。動作完成 ≠ 驗收完成。</p>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<p>把測試固化的實作 case 都該套用本卡：</p>
<ul>
<li><a href="../playwright-early-in-loop/">#11 playwright-early-in-loop</a> — 第 2 次推理失敗切 playwright；切過去後寫的 evaluate query 跑 RED-GREEN 才驗證</li>
<li><a href="../layout-tests-with-playwright/">#15 layout-tests-with-playwright</a> — 版型 debug 兩次以上寫測試固化；測試該先在「未修版型」跑 RED 才相信</li>
<li><a href="../verification-method-timing/">#23 verification-method-timing</a> — 驗證方法選對之後、實際驗證需要 RED-GREEN</li>
</ul>
<hr>
<h2 id="retrospective-補驗證的協議">Retrospective 補驗證的協議</h2>
<p>如果已經修完才寫測試（test-after）、可以 retrospectively 補 RED-GREEN 驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. Stash 現有變動 / 切到修前 commit</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">git stash
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">git checkout &lt;pre-fix-commit&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. Cherry-pick 測試 commit（或手動複製 test files）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">git cherry-pick &lt;test-commit&gt;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 或：cp ../tests/foo.spec.ts tests/  # 複製測試檔過來</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. Build + 跑測試</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">make site <span class="o">&amp;&amp;</span> npm <span class="nb">test</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 預期：RED（測試抓到 bug）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4. 切回 main / 修後版本</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">git checkout main
</span></span><span class="line"><span class="ln">15</span><span class="cl">git stash pop
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 5. 跑測試</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 預期：GREEN</span></span></span></code></pre></div><p>兩次跑 + 兩個訊號（RED + GREEN）都對、測試才被驗證。<strong>Retrospective 補驗證 ≠ 不能補</strong> — 比完全跳過 RED 好、比 test-first 弱。</p>
<p>協議已 codify 為 <code>make verify-red-green PRE_FIX=&lt;commit-sha&gt;</code>（見 Makefile）— 五步驟自動化、不需要每次手動 stash / checkout / build / restore。</p>
<h3 id="self-case本卡誕生過程的-dogfooding-失敗">Self-case：本卡誕生過程的 dogfooding 失敗</h3>
<p>本卡是從一次真實的 dogfooding 失敗抽出來的。修 <a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a> bug 時、流程是：</p>
<ol>
<li>修 code（multi-index 策略）</li>
<li>寫 4 個 Playwright tests</li>
<li>跑測試 → 4/4 GREEN</li>
<li>看起來完工</li>
</ol>
<p>User 問「修改之前有先寫測試確保符合預測狀態嗎」— 才意識到沒走 RED。Retrospective 補驗證後發現：<strong>4 個測試只有 1 個真的 catch 到 bug、其他 3 個對 buggy code 也 PASS</strong>（placebo 測試）。</p>
<p>強化後（用 network-level + structural assertion 替換弱 invariant）：buggy code 上 1/4 PASS、3/4 FAIL。Fixed code 上 4/4 PASS。RED-GREEN 兩個訊號都看到、測試才真的驗證。</p>
<p>如果不做 retrospective、會帶著 3/4 placebo 測試 ship — 表面 4/4 GREEN、實際只有 1 個真的防回歸。<strong>「跑得通」≠「會 catch」這個區別、只有走過 RED 才知道</strong>。</p>
<p>跳過 RED 是 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發的工作</a> 在測試協議的展現 — 修法不是「下次記得」（L1 紀律會失敗）、是 <code>make verify-red-green PRE_FIX=&lt;sha&gt;</code>（L3 工具觸發）+ pre-commit hook 提醒（L3 結構觸發）。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完測試第一次跑就 GREEN</td>
          <td>警訊 — 確認測試是不是真的有 catch 能力（覆蓋 bug case 嗎？）</td>
      </tr>
      <tr>
          <td>修了 bug 但沒看過該測試 RED 過</td>
          <td>補 retrospective 驗證、或下次採 test-first</td>
      </tr>
      <tr>
          <td>「我等下會跑一下」但沒實際跑</td>
          <td>跟「我等下會 refactor」同類謊言、補不回來</td>
      </tr>
      <tr>
          <td>CI 永遠 GREEN、沒有人改過測試</td>
          <td>看 coverage、可能測試沒在跑</td>
      </tr>
      <tr>
          <td>加了 feature、測試一寫就 GREEN</td>
          <td>feature 可能已經存在、或測試太寬</td>
      </tr>
      <tr>
          <td>測試環境跟 production 環境差太多</td>
          <td>RED 在 dev 但 prod 仍 fail = 測試環境沒 catch 真實 case</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：測試不是「跑得通就有用」、是「跑出該有的訊號才有用」。RED 是測試的驗收訊號、跳過 = 接受測試本身可能是壞的。RED → GREEN 兩次跑、才證明「測試真的會 catch + 修復真的解掉 bug」。</p>
]]></content:encoded></item><item><title>10 個 Ticket、57 個綠燈、0 條追溯：從需求文件到測試的銜接檢討</title><link>https://tarrragon.github.io/blog/work-log/10-%E5%80%8B-ticket57-%E5%80%8B%E7%B6%A0%E7%87%880-%E6%A2%9D%E8%BF%BD%E6%BA%AF%E5%BE%9E%E9%9C%80%E6%B1%82%E6%96%87%E4%BB%B6%E5%88%B0%E6%B8%AC%E8%A9%A6%E7%9A%84%E9%8A%9C%E6%8E%A5%E6%AA%A2%E8%A8%8E/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/10-%E5%80%8B-ticket57-%E5%80%8B%E7%B6%A0%E7%87%880-%E6%A2%9D%E8%BF%BD%E6%BA%AF%E5%BE%9E%E9%9C%80%E6%B1%82%E6%96%87%E4%BB%B6%E5%88%B0%E6%B8%AC%E8%A9%A6%E7%9A%84%E9%8A%9C%E6%8E%A5%E6%AA%A2%E8%A8%8E/</guid><description>&lt;h2 id="這篇要解決什麼">這篇要解決什麼&lt;/h2>
&lt;blockquote>
&lt;p>57 個 unit test 全綠，但沒有任何機制能回答「這些測試覆蓋了哪些 UseCase 場景」。&lt;/p>&lt;/blockquote>
&lt;p>monitor 專案 v0.1.0 從需求文件系統（Proposal → Spec → UseCase）一路走到 Collector 實作，中間經過 BDD 測試設計、紅燈測試撰寫、骨架實作讓綠。流程表面上順暢——10 個根 Ticket 全部完成、Collector 可啟動、所有 unit test 通過。但回頭檢視發現：需求→測試的銜接是單向管道，沒有反向追溯，也沒有邊界回補流程。&lt;/p>
&lt;p>本文記錄 v0.1.0 的完整流程、發現的五個結構性差異、和落地的解決方案。&lt;/p>
&lt;hr>
&lt;h2 id="實際走過的流程">實際走過的流程&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">saas 選型訪談
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → Proposal（MVP 範圍界定）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → Spec（14 份，涵蓋 schema/ingestion/query/storage/rule-engine/SDK）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → UseCase（5 個，UC-01 端到端事件流 ~ UC-05 Web 監控）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> → BDD 測試設計 ANA（全專案 26 個行為場景 → 整合/單元/協議測試清單）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> → 紅燈測試（9 個 Ticket 並行，72 個測試 FAIL）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> → 骨架實作（1 個 Ticket，57 個 unit test GREEN）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個箭頭都有對應的框架機制：saas→doc 有 Stage 6 銜接、doc→TDD 有 doc-handoff 映射表。但箭頭只往右——沒有任何箭頭往左。&lt;/p>
&lt;hr>
&lt;h2 id="五個結構性差異">五個結構性差異&lt;/h2>
&lt;h3 id="差異-1全專案-bdd-設計不在-tdd-phase-模型中">差異 1：「全專案 BDD 設計」不在 TDD Phase 模型中&lt;/h3>
&lt;p>TDD Skill 定義 Phase 0→1→2→3→4 的逐功能流程。v0.1.0 做的是「全專案 UseCase 一次性展開為 BDD 測試設計」，跨越 Phase 1 和 Phase 2 的邊界，粒度是專案級不是功能級。&lt;/p>
&lt;p>這不是 Phase 設計的錯——Phase 模型適合增量開發（每次加一個功能）。新專案起手是不同的工作模式：批量設計、模組群組粒度。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：在 doc-handoff 新增「新專案起手模式」章節，描述批量 BDD 設計流程、Phase 0 豁免條件、模組群組粒度。&lt;/p>
&lt;h3 id="差異-2紅燈測試需要存根stub">差異 2：紅燈測試需要存根（stub）&lt;/h3>
&lt;p>Go 是靜態語言，&lt;code>go test&lt;/code> 必須編譯通過才能執行。紅燈測試引用的 type/interface 不存在時直接編譯失敗，不是「測試 FAIL」。&lt;/p>
&lt;p>TDD Skill 的 Phase 2 說「設計測試」、Phase 3b 說「讓測試綠」，但中間的「建存根讓測試可紅」沒有定義。&lt;/p>
&lt;p>&lt;strong>實作驗證&lt;/strong>：v0.1.0 的每個紅燈 Ticket 都自帶建立存根（空 function return nil / 空 struct / 回 501 的 HTTP handler），存根讓 &lt;code>go test&lt;/code> 編譯通過，合法測試 PASS、非法測試 FAIL = 紅燈狀態。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：Phase 3 rules 新增「存根策略」章節，涵蓋靜態語言（Go/Dart）和動態語言（Python/JS）的不同處理。&lt;/p>
&lt;h3 id="差異-3測試usecase-沒有反向追溯">差異 3：測試→UseCase 沒有反向追溯&lt;/h3>
&lt;p>寫完 57 個 unit test 後，問「UC-01 的替代場景 01a（批次部分失敗 → 207）被哪些測試覆蓋？」——沒有任何機制能回答。&lt;/p>
&lt;p>&lt;code>doc test-map UC-01&lt;/code> 工具存在但回傳 0 個測試——因為它搜尋 UC frontmatter 的 &lt;code>ticket_refs&lt;/code>，和測試檔案沒有連結。Spec 的「三方交叉比對」是建 Ticket 時的一次性動作，不是持續追溯。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：建立 &lt;code>docs/traceability.yaml&lt;/code> 追溯矩陣，三層追溯（UC 場景 → 整合測試 IT-* → 單元測試 UT-* → Spec FR）。每個 entry 標記 &lt;code>covered&lt;/code> / &lt;code>gap&lt;/code> / &lt;code>deferred&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要解決什麼">這篇要解決什麼</h2>
<blockquote>
<p>57 個 unit test 全綠，但沒有任何機制能回答「這些測試覆蓋了哪些 UseCase 場景」。</p></blockquote>
<p>monitor 專案 v0.1.0 從需求文件系統（Proposal → Spec → UseCase）一路走到 Collector 實作，中間經過 BDD 測試設計、紅燈測試撰寫、骨架實作讓綠。流程表面上順暢——10 個根 Ticket 全部完成、Collector 可啟動、所有 unit test 通過。但回頭檢視發現：需求→測試的銜接是單向管道，沒有反向追溯，也沒有邊界回補流程。</p>
<p>本文記錄 v0.1.0 的完整流程、發現的五個結構性差異、和落地的解決方案。</p>
<hr>
<h2 id="實際走過的流程">實際走過的流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">saas 選型訪談
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → Proposal（MVP 範圍界定）
</span></span><span class="line"><span class="ln">3</span><span class="cl">    → Spec（14 份，涵蓋 schema/ingestion/query/storage/rule-engine/SDK）
</span></span><span class="line"><span class="ln">4</span><span class="cl">      → UseCase（5 個，UC-01 端到端事件流 ~ UC-05 Web 監控）
</span></span><span class="line"><span class="ln">5</span><span class="cl">        → BDD 測試設計 ANA（全專案 26 個行為場景 → 整合/單元/協議測試清單）
</span></span><span class="line"><span class="ln">6</span><span class="cl">          → 紅燈測試（9 個 Ticket 並行，72 個測試 FAIL）
</span></span><span class="line"><span class="ln">7</span><span class="cl">            → 骨架實作（1 個 Ticket，57 個 unit test GREEN）</span></span></code></pre></div><p>每個箭頭都有對應的框架機制：saas→doc 有 Stage 6 銜接、doc→TDD 有 doc-handoff 映射表。但箭頭只往右——沒有任何箭頭往左。</p>
<hr>
<h2 id="五個結構性差異">五個結構性差異</h2>
<h3 id="差異-1全專案-bdd-設計不在-tdd-phase-模型中">差異 1：「全專案 BDD 設計」不在 TDD Phase 模型中</h3>
<p>TDD Skill 定義 Phase 0→1→2→3→4 的逐功能流程。v0.1.0 做的是「全專案 UseCase 一次性展開為 BDD 測試設計」，跨越 Phase 1 和 Phase 2 的邊界，粒度是專案級不是功能級。</p>
<p>這不是 Phase 設計的錯——Phase 模型適合增量開發（每次加一個功能）。新專案起手是不同的工作模式：批量設計、模組群組粒度。</p>
<p><strong>解法</strong>：在 doc-handoff 新增「新專案起手模式」章節，描述批量 BDD 設計流程、Phase 0 豁免條件、模組群組粒度。</p>
<h3 id="差異-2紅燈測試需要存根stub">差異 2：紅燈測試需要存根（stub）</h3>
<p>Go 是靜態語言，<code>go test</code> 必須編譯通過才能執行。紅燈測試引用的 type/interface 不存在時直接編譯失敗，不是「測試 FAIL」。</p>
<p>TDD Skill 的 Phase 2 說「設計測試」、Phase 3b 說「讓測試綠」，但中間的「建存根讓測試可紅」沒有定義。</p>
<p><strong>實作驗證</strong>：v0.1.0 的每個紅燈 Ticket 都自帶建立存根（空 function return nil / 空 struct / 回 501 的 HTTP handler），存根讓 <code>go test</code> 編譯通過，合法測試 PASS、非法測試 FAIL = 紅燈狀態。</p>
<p><strong>解法</strong>：Phase 3 rules 新增「存根策略」章節，涵蓋靜態語言（Go/Dart）和動態語言（Python/JS）的不同處理。</p>
<h3 id="差異-3測試usecase-沒有反向追溯">差異 3：測試→UseCase 沒有反向追溯</h3>
<p>寫完 57 個 unit test 後，問「UC-01 的替代場景 01a（批次部分失敗 → 207）被哪些測試覆蓋？」——沒有任何機制能回答。</p>
<p><code>doc test-map UC-01</code> 工具存在但回傳 0 個測試——因為它搜尋 UC frontmatter 的 <code>ticket_refs</code>，和測試檔案沒有連結。Spec 的「三方交叉比對」是建 Ticket 時的一次性動作，不是持續追溯。</p>
<p><strong>解法</strong>：建立 <code>docs/traceability.yaml</code> 追溯矩陣，三層追溯（UC 場景 → 整合測試 IT-* → 單元測試 UT-* → Spec FR）。每個 entry 標記 <code>covered</code> / <code>gap</code> / <code>deferred</code>。</p>
<h3 id="差異-4邊界條件發現後沒有回補-uc-的流程">差異 4：邊界條件發現後沒有回補 UC 的流程</h3>
<p>寫 Ingest Handler 測試時發現：「如果 POST body 不是 JSON 怎麼辦？」「如果 Content-Type 是 text/plain（sendBeacon）怎麼辦？」這些邊界在 UC-01 的場景描述中不存在。</p>
<p>測試設計的 BDD ANA 有涵蓋這些邊界場景，但 UC 文件本身沒有更新。邊界條件「住」在測試設計文件而非 UseCase——下次有人讀 UC 不會知道這些邊界存在。</p>
<p><strong>解法</strong>：追溯矩陣增加 <code>boundaries:</code> 區段，測試撰寫者發現新邊界時加 gap entry，PM 建 DOC Ticket 回補 UC/Spec。Phase 4d 掃描所有 gap 確認無遺漏。</p>
<h3 id="差異-5ticket-拆分邊界未對齊測試變綠驗收點">差異 5：Ticket 拆分邊界未對齊測試變綠驗收點</h3>
<p>Collector 實作被拆為 4 個 Ticket：骨架（interface 定義）/ Storage / Ingestion Handler / Query Handler。骨架 Ticket 指派做「main.go + Config + Storage interface」，代理人完成了所有模組實作——57 個 unit test 從紅全部變綠，其餘 3 個 Ticket 的 acceptance 全被涵蓋。</p>
<p>初看像是「代理人超額完成」，回頭用判讀三問檢查骨架 Ticket：完成後有測試變綠嗎？→ 沒有（只定義 interface）。能獨立跑測試嗎？→ 不能（其他模組引用骨架的 type）。共用 type？→ 是。三問全部指向「不應獨立拆」。<strong>根因是 Ticket 拆分設計</strong>，不是代理人行為——按 Spec FR 拆（輸入驅動）導致骨架 Ticket 完成後 0 個測試狀態改變，不是有意義的驗收點。</p>
<p><strong>判讀規則</strong>：實作 Ticket 的拆分邊界必須對齊「測試從紅變綠」的驗收點。一個 Ticket 完成後若沒有任何測試狀態改變，它不應該是獨立 Ticket。</p>
<p>判讀三問：</p>
<ol>
<li>這個 Ticket 完成後，有測試從 FAIL 變 PASS 嗎？</li>
<li>拆出的各部分能獨立跑測試嗎？</li>
<li>不同部分共用同一組 type/error/constant 嗎？</li>
</ol>
<p><strong>反模式</strong>：按 Spec FR 拆（輸入驅動）。<strong>正確做法</strong>：按「哪組測試變綠」拆（輸出驅動）。</p>
<hr>
<h2 id="追溯矩陣的設計">追溯矩陣的設計</h2>
<p>追溯矩陣是三個問題（向上追溯 + 覆蓋驗證 + 邊界回補）的統一解法。</p>
<h3 id="結構">結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">UC-01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">端到端事件流</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">scenarios</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">main</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">integration_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">IT-01-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">unit_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">UT-COL-01-01, UT-COL-02-01, UT-COL-04-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">spec_frs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">SPEC-002-FR-01, SPEC-003-FR-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">covered</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">alt-01a</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">integration_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">IT-01-02]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">unit_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">UT-COL-01-03, UT-COL-02-03]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">spec_frs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">SPEC-002-FR-02]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">covered</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="nt">boundaries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">batch-limit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">discovered_during</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ingestion-handler-red-tests&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">gap </span><span class="w"> </span><span class="c"># 需回補 UC/Spec</span></span></span></code></pre></div><h3 id="三個問題的對應">三個問題的對應</h3>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>矩陣欄位</th>
          <th>查法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這個 UT 為了哪個 UC？</td>
          <td><code>unit_tests</code></td>
          <td>搜尋 UT ID → 找到歸屬的 scenario</td>
      </tr>
      <tr>
          <td>UC 場景都有測試嗎？</td>
          <td><code>status</code></td>
          <td>掃描 <code>gap</code> entry</td>
      </tr>
      <tr>
          <td>新邊界怎麼回補 UC？</td>
          <td><code>boundaries</code></td>
          <td>gap entry → DOC Ticket → 回補 → covered</td>
      </tr>
  </tbody>
</table>
<h3 id="整合點">整合點</h3>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>時機</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>doc-handoff</td>
          <td>銜接時</td>
          <td>初始化矩陣骨架（UC scenario 空映射）</td>
      </tr>
      <tr>
          <td>紅燈測試撰寫</td>
          <td>Phase 2→3</td>
          <td>填入 unit_tests 映射</td>
      </tr>
      <tr>
          <td>邊界發現</td>
          <td>實作中</td>
          <td>加 boundary gap entry</td>
      </tr>
      <tr>
          <td>Phase 4d</td>
          <td>重構評估</td>
          <td>掃描所有 gap，建 DOC Ticket</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="附帶發現並行派發的-git-隔離問題">附帶發現：並行派發的 Git 隔離問題</h2>
<p>5 個代理人以 worktree 並行派發時，commit 內容交叉混入——A 代理人的 commit 包含 B 代理人的檔案。根因：主 repo 不在 main 分支，多個 worktree 共用同一分支 ref，<code>git add + commit</code> race condition。</p>
<p><strong>防護</strong>：派發前確保主 repo 在 main + 已 push。單一代理人和正確條件下的多代理人都驗證通過。</p>
<hr>
<h2 id="結論">結論</h2>
<p>v0.1.0 的流程不是失敗——Collector 可用、57 個 test GREEN。問題在於「走到終點後沒有辦法回頭驗證起點」。需求→測試的管道是單向的：Proposal 說了什麼、Spec 定了什麼 FR、UC 描述了什麼場景，和最終的測試之間沒有結構化連結。</p>
<p>追溯矩陣不增加任何程式碼——它是一個 YAML 檔案，記錄「每個測試為什麼存在」。維護成本是每次寫測試多填一行映射。回報是：任何時候都能回答「這個 UC 場景有沒有被測試保護」。</p>
]]></content:encoded></item><item><title>Cards-Skills 系統的活案例：從一個 search bug 到 14 張新卡的閉環</title><link>https://tarrragon.github.io/blog/posts/cards-skills-%E7%B3%BB%E7%B5%B1%E7%9A%84%E6%B4%BB%E6%A1%88%E4%BE%8B%E5%BE%9E%E4%B8%80%E5%80%8B-search-bug-%E5%88%B0-14-%E5%BC%B5%E6%96%B0%E5%8D%A1%E7%9A%84%E9%96%89%E7%92%B0/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/cards-skills-%E7%B3%BB%E7%B5%B1%E7%9A%84%E6%B4%BB%E6%A1%88%E4%BE%8B%E5%BE%9E%E4%B8%80%E5%80%8B-search-bug-%E5%88%B0-14-%E5%BC%B5%E6%96%B0%E5%8D%A1%E7%9A%84%E9%96%89%E7%92%B0/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>&lt;code>content/report/&lt;/code> 累積了 70+ 張原子化事後檢討卡片、&lt;code>.claude/skills/&lt;/code> 收錄三個 protocol skill。這些是用來指導下一輪實作、又會被下一輪實作的學習回流修正的活基礎建設。&lt;/p>
&lt;p>本文把這套系統實際跑一輪的歷程紀錄下來、當未來「想用這套系統的人」的 onboarding case study。主軸是修一個 search filter bug — 看似一週工作、實際走完八輪迭代、產出 14 張新卡片 + 兩個 skill 的 v0.2 + 4 個 CI test、過程中還抓到自己的 dogfooding 失敗、回頭修一次。&lt;/p>
&lt;hr>
&lt;h2 id="起點使用者問題">起點：使用者問題&lt;/h2>
&lt;p>&amp;ldquo;我們搜尋頁的 標題/內文篩選功能現在雖然做出來了、但是還是有一個很嚴重的 BUG&amp;rdquo;&lt;/p>
&lt;p>具體：Pagefind 分批 load、view 層 post-filter；切到 title-only 後、第二批 load more 的 8 筆全部 title 不含 query → 全 hidden、畫面閃但內容沒變、使用者看到「load more 沒效果」silent 失敗。&lt;/p>
&lt;p>User 還明確補了一句：「&lt;strong>所以除了用 JS 取巧解決畫面、但是實際功能面上怎麼配合跟實作 我們並沒有解決&lt;/strong>」— 這已經點到核心：問題不在畫面、在抽象層。&lt;/p>
&lt;hr>
&lt;h2 id="第一輪拆卡片之前先想清楚">第一輪：拆卡片之前先想清楚&lt;/h2>
&lt;p>直接修 bug 是可選但不是 user 要的。User 強調：「&lt;strong>先思考我的需求、然後思考各種狀況的邊界&lt;/strong>」。&lt;/p>
&lt;p>依當時的兩個 skill — &lt;code>requirement-protocol&lt;/code>（對話協議）跟 &lt;code>frontend-with-playwright&lt;/code>（前端執行協議）— 把問題分解：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Bug 的結構性根因&lt;/strong>：filter 寫在視覺層、source 在資料層分批、兩層的「一筆」定義不一致 → silent 缺口&lt;/li>
&lt;li>&lt;strong>解法策略空間&lt;/strong>：5 個合理選項（推進 query / 自動續抓 / 多 index / 誠實 UX / 明示縮小）— 每個機會成本不同&lt;/li>
&lt;li>&lt;strong>跨領域通用性&lt;/strong>：這結構不只前端有 — 後端 middleware filter、map-reduce、SQL view 都同模式&lt;/li>
&lt;/ol>
&lt;p>User 的關鍵回應：「&lt;strong>這部份可以補充 SKILL 中演算法不足的原因 &amp;hellip; 卡片是經過多次迭代、擴充、然後分拆、再擴充、最後做連結&lt;/strong>」。&lt;/p>
&lt;p>明確了協作方式：先建卡片、再灌進 skill、最後才修。卡片本身要走原子化拆解 → 補充 → 反向擴充 → 連結的多輪迭代。&lt;/p>
&lt;hr>
&lt;h2 id="14-張卡片的拆解第一冷啟">14 張卡片的拆解（第一冷啟）&lt;/h2>
&lt;p>依 user 對 atomic 的標準（一卡一議題、一個議題多面向 OK、議題太多就拆），列出 10 張卡片提案：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分組&lt;/th>
 &lt;th>卡片&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>問題分析&lt;/td>
 &lt;td>#55 層錯位 / #56 視覺完成 ≠ 功能完成 / #57 三狀態區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>指令澄清&lt;/td>
 &lt;td>#58 篩選類指令的澄清時機&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>解法策略&lt;/td>
 &lt;td>#59 五策略對照 + #60-62 三張 pattern 卡（自動續抓 / 推進 query / 誠實 UX）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抽象原則&lt;/td>
 &lt;td>#63 資料源形狀 / #64 同層合成&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>冷啟版本一次寫完不求完美 — 約 1700 行、各卡 self-contained。&lt;/p>
&lt;hr>
&lt;h2 id="七輪迭代">七輪迭代&lt;/h2>
&lt;h3 id="迭代-1抽-pattern--瘦身">迭代 1：抽 Pattern + 瘦身&lt;/h3>
&lt;p>寫完 #59 五策略後、發現 A/B/C/D/E 中 C（多 index）、E（明示縮小）沒對應 pattern 卡。抽出 #65 / #66 補完 pattern 卡組。同時瘦身 #59 → 純路由（細節留 pattern 卡）、#55 + #57 移除跟 #63 重複的「四類資料源」段。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p><code>content/report/</code> 累積了 70+ 張原子化事後檢討卡片、<code>.claude/skills/</code> 收錄三個 protocol skill。這些是用來指導下一輪實作、又會被下一輪實作的學習回流修正的活基礎建設。</p>
<p>本文把這套系統實際跑一輪的歷程紀錄下來、當未來「想用這套系統的人」的 onboarding case study。主軸是修一個 search filter bug — 看似一週工作、實際走完八輪迭代、產出 14 張新卡片 + 兩個 skill 的 v0.2 + 4 個 CI test、過程中還抓到自己的 dogfooding 失敗、回頭修一次。</p>
<hr>
<h2 id="起點使用者問題">起點：使用者問題</h2>
<p>&ldquo;我們搜尋頁的 標題/內文篩選功能現在雖然做出來了、但是還是有一個很嚴重的 BUG&rdquo;</p>
<p>具體：Pagefind 分批 load、view 層 post-filter；切到 title-only 後、第二批 load more 的 8 筆全部 title 不含 query → 全 hidden、畫面閃但內容沒變、使用者看到「load more 沒效果」silent 失敗。</p>
<p>User 還明確補了一句：「<strong>所以除了用 JS 取巧解決畫面、但是實際功能面上怎麼配合跟實作 我們並沒有解決</strong>」— 這已經點到核心：問題不在畫面、在抽象層。</p>
<hr>
<h2 id="第一輪拆卡片之前先想清楚">第一輪：拆卡片之前先想清楚</h2>
<p>直接修 bug 是可選但不是 user 要的。User 強調：「<strong>先思考我的需求、然後思考各種狀況的邊界</strong>」。</p>
<p>依當時的兩個 skill — <code>requirement-protocol</code>（對話協議）跟 <code>frontend-with-playwright</code>（前端執行協議）— 把問題分解：</p>
<ol>
<li><strong>Bug 的結構性根因</strong>：filter 寫在視覺層、source 在資料層分批、兩層的「一筆」定義不一致 → silent 缺口</li>
<li><strong>解法策略空間</strong>：5 個合理選項（推進 query / 自動續抓 / 多 index / 誠實 UX / 明示縮小）— 每個機會成本不同</li>
<li><strong>跨領域通用性</strong>：這結構不只前端有 — 後端 middleware filter、map-reduce、SQL view 都同模式</li>
</ol>
<p>User 的關鍵回應：「<strong>這部份可以補充 SKILL 中演算法不足的原因 &hellip; 卡片是經過多次迭代、擴充、然後分拆、再擴充、最後做連結</strong>」。</p>
<p>明確了協作方式：先建卡片、再灌進 skill、最後才修。卡片本身要走原子化拆解 → 補充 → 反向擴充 → 連結的多輪迭代。</p>
<hr>
<h2 id="14-張卡片的拆解第一冷啟">14 張卡片的拆解（第一冷啟）</h2>
<p>依 user 對 atomic 的標準（一卡一議題、一個議題多面向 OK、議題太多就拆），列出 10 張卡片提案：</p>
<table>
  <thead>
      <tr>
          <th>分組</th>
          <th>卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題分析</td>
          <td>#55 層錯位 / #56 視覺完成 ≠ 功能完成 / #57 三狀態區分</td>
      </tr>
      <tr>
          <td>指令澄清</td>
          <td>#58 篩選類指令的澄清時機</td>
      </tr>
      <tr>
          <td>解法策略</td>
          <td>#59 五策略對照 + #60-62 三張 pattern 卡（自動續抓 / 推進 query / 誠實 UX）</td>
      </tr>
      <tr>
          <td>抽象原則</td>
          <td>#63 資料源形狀 / #64 同層合成</td>
      </tr>
  </tbody>
</table>
<p>冷啟版本一次寫完不求完美 — 約 1700 行、各卡 self-contained。</p>
<hr>
<h2 id="七輪迭代">七輪迭代</h2>
<h3 id="迭代-1抽-pattern--瘦身">迭代 1：抽 Pattern + 瘦身</h3>
<p>寫完 #59 五策略後、發現 A/B/C/D/E 中 C（多 index）、E（明示縮小）沒對應 pattern 卡。抽出 #65 / #66 補完 pattern 卡組。同時瘦身 #59 → 純路由（細節留 pattern 卡）、#55 + #57 移除跟 #63 重複的「四類資料源」段。</p>
<h3 id="迭代-2補概念深度">迭代 2：補概念深度</h3>
<p>回頭讀 #56 / #63 / #64、補抽象層的「為什麼」：</p>
<ul>
<li>#56 加「驗收的時間軸：四個 checkpoint」概念</li>
<li>#63 加「形狀識別 protocol」+「形狀混合」+「形狀的可改造性」</li>
<li>#64 加「跨領域通用的本質 = 資訊可見範圍」+「上推代價」</li>
</ul>
<h3 id="迭代-3跨卡連結">迭代 3：跨卡連結</h3>
<p>新卡跟 #1-#54 既有卡互相補連結。例如 #55 ↔ #11 playwright、#57 ↔ #38 aria-live、#58 ↔ #21 decide-vs-confirm、#64 ↔ #43 minimum-scope + #44 SSOT。整個 collection 從兩個獨立輪次變一張互連網。</p>
<h3 id="迭代-4抽更高層原則">迭代 4：抽更高層原則</h3>
<p>重讀新卡發現兩個議題夠 abstract、值得抽獨立卡：</p>
<ul>
<li><strong>#67 寫作便利度跟意圖對齊反相關</strong> — 從「為什麼層錯位 bug 容易寫出來」抽出。發現它是 #43 / #44 / #45 / #64 的共同上位原則：<strong>便利位置 vs 對齊位置永遠反相關</strong></li>
<li><strong>#68 驗收的時間軸：四個 checkpoint</strong> — 從 #56 抽出獨立成卡</li>
</ul>
<h3 id="迭代-5跨輪共骨">迭代 5：跨輪共骨</h3>
<p>系統性掃 #1-#54 找跟新系列共骨的、加連結。例：#6 filter-order ↔ #58 / #59、#10 placeholder ↔ #68、#15 layout-test ↔ #68、#14 selector / #20 failure / #28 class-toggle ↔ #67。</p>
<h3 id="迭代-66768-加深">迭代 6：#67/#68 加深</h3>
<p>再讀兩張抽象卡、補「為什麼人會違反這條規則」的結構性解釋：</p>
<ul>
<li>#67 加「便利度的時間維度：當下便利 vs 未來便利反向」+「我等下會 refactor 是個謊言」</li>
<li>#68 加「為什麼 Ship 前 checkpoint 最常被跳過」（沒便利路徑）+「瀑布原則：漏一層代價指數放大」</li>
</ul>
<p>從「規則陳述」進到「結構性解釋」 — 不只說「該怎麼做」、也說「為什麼人會違反」。</p>
<h3 id="迭代-7compositional-writing-規範稽核">迭代 7：compositional-writing 規範稽核</h3>
<p>User 提醒「再做一次 compositional-writing 的檢查」。發現兩類違規：</p>
<ol>
<li><strong>Rule 7 違規</strong>：26 處「X 才合理的情境：實務上幾乎不存在」假反模式 — 改成「X 是反模式：理由」格式</li>
<li><strong>結構違規</strong>：#67/#68 是抽象層原則卡、不該寫設計取捨 ABCD（情境檢討卡的格式）— 改成「不該套用本原則的情境」（適用邊界）</li>
</ol>
<p>修完 31 張卡片（含既有 #1-#54）。整個 collection 對齊 v0.6 規範。</p>
<hr>
<h2 id="灌進-skills">灌進 Skills</h2>
<p>把 #55-#68 系列接進兩個 skill：</p>
<ul>
<li><strong>requirement-protocol v0.2</strong>：clarifying-ambiguous-instructions 加第 5 類「篩選類」+ 三問模板（呼應 #58）；SKILL.md 加「相關抽象層原則」段路由 #42-45 + #67-68</li>
<li><strong>frontend-with-playwright v0.2</strong>：新增第 7 份 reference <code>data-flow-and-filter-composition</code>（涵蓋 #55-#66 跨領域範例）；強調「不只前端、適用後端 / 演算法 / DB」</li>
</ul>
<p>Skill 的角色 = 路由器、Reports = 深度內容 — 兩層分工不重述。</p>
<hr>
<h2 id="實作策略-c--phase-1-4">實作：策略 C + Phase 1-4</h2>
<p>依 #59 + Pagefind 1.5.2 capabilities：</p>
<ul>
<li><strong>A 推進 query</strong>：不可行（Pagefind 無 native title filter API）</li>
<li><strong>C 多 index</strong>：採用（最對齊意圖）</li>
<li>B / D / E 是 fallback</li>
</ul>
<p>Phase 1-4：</p>
<ol>
<li>Makefile 跑 3 輪 pagefind（all / title / content）</li>
<li>single.html <code>&lt;content&gt;</code> → <code>&lt;div class=&quot;article-body&quot; data-pagefind-body&gt;</code></li>
<li>search.html 移除 view 層 post-filter、改 destroy + new PagefindUI(bundlePath)</li>
<li>4 個 Playwright tests 固化</li>
</ol>
<p>跑出來：<code>make site</code> 三 index 成功、<code>make test</code> 4/4 PASS、live 驗證 sparse case 顯示 explicit empty。<strong>看起來完工</strong>。</p>
<hr>
<h2 id="user-抓到-dogfooding-失敗--第-8-輪">User 抓到 dogfooding 失敗 — 第 8 輪</h2>
<p>User 問：「<strong>剛剛的過程我不確定、你開始修改之前有先寫測試確保符合預測狀態、然後才調整嗎？</strong>」</p>
<p>沒有。流程是：先修 → 才補測試 → 4/4 GREEN。<strong>沒走 RED</strong>。</p>
<p>這是 #67「便利驅動」+ #68「Checkpoint 2/3 內部協議」的 dogfooding 失敗。我寫了 #67/#68 教這些原則、自己卻違反。</p>
<p>依 user 規範：先建卡片再修。抽 <strong>#69 Test-First：先看到 RED 才相信 GREEN</strong>：</p>
<ul>
<li>測試本身是程式、會有 bug（5 種失敗模式）</li>
<li>沒看過 RED = 不知道測試有沒有 catch 能力</li>
<li>RED → GREEN 兩個訊號都看到 = 測試 + 修復都被驗證</li>
</ul>
<p>retrospective 補驗證流程：checkout pre-fix commit → cherry-pick test → build → run（看 RED）→ restore → run（看 GREEN）。</p>
<p>跑下去 — 結果震撼：<strong>4 個測試只有 1 個真的 catch 到 bug、其他 3 個對 buggy code 也 PASS</strong>（placebo）。如果不做 retrospective、會帶著 3/4 placebo 測試 ship。</p>
<p>強化測試（network-level + structural assertion 替換弱 invariant）：buggy code 1 PASS / 3 FAIL、fixed code 4 PASS。RED-GREEN 真的 catch 到 bug + 真的解掉。</p>
<hr>
<h2 id="user-抓到第二個-dogfooding-失敗--checkpoint-1">User 抓到第二個 dogfooding 失敗 — Checkpoint 1</h2>
<p>我問 user 還有什麼該迭代。User 列了 7 項、選 1+2：</p>
<ol>
<li>補 Checkpoint 1（列使用者意圖完整集）</li>
<li>跟 user 確認 known limitations</li>
</ol>
<p>跑 Checkpoint 1 retrospective — 用 Playwright MCP 系統性測 5 維度（data / interaction / URL / a11y / performance）。發現 3 個 silent 缺口：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>漏掉的 case</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>URL state</td>
          <td><code>?q=X&amp;scope=Y</code> 持久化</td>
          <td>完全沒實作</td>
      </tr>
      <tr>
          <td>A11y</td>
          <td>Tab order: scope 在 search input 之前</td>
          <td>反 mental model</td>
      </tr>
      <tr>
          <td>Filter UX</td>
          <td>type/tag filter 在 sub-mode 完全消失</td>
          <td>Silent 限制</td>
      </tr>
  </tbody>
</table>
<p>依 user 規範：<strong>先建卡片再修</strong>。抽：</p>
<ul>
<li><strong>#70 URL 是 stateful UI 的儲存層</strong> — 5 個儲存層特性對照 + 三問判準</li>
<li><strong>#71 Tab Order = DOM Order = Mental Model 三者對齊</strong> — DOM 順序 = tab 順序、不對齊時優先重排 DOM</li>
<li>更新 #68 加「為什麼 Checkpoint 1 也常被跳過」段、用本次任務當 self-case</li>
</ul>
<p>然後實作 — 依 #69 RED-GREEN 順序：</p>
<ol>
<li>寫 4 個 RED tests</li>
<li>跑 → 4 個 fail（confirms RED）</li>
<li>修 search.html（URL persist + DOM reorder + UI hint）</li>
<li>跑 → 8/8 GREEN</li>
</ol>
<hr>
<h2 id="ci--自動化">CI + 自動化</h2>
<p>最後補 CI 防護：</p>
<ul>
<li><strong><code>.github/workflows/playwright.yml</code></strong> — push / PR 自動跑 8 個 tests</li>
<li><strong><code>deploy.yml</code> 修 critical bug</strong> — production 一直只 build 單 index、現在 build 三份對齊本地</li>
<li><strong><code>make test</code> + <code>make verify-red-green PRE_FIX=&lt;sha&gt;</code></strong> — codify retrospective 流程、不需手動 stash / checkout / restore</li>
</ul>
<hr>
<h2 id="數字總結">數字總結</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>30+</td>
      </tr>
      <tr>
          <td>新卡片</td>
          <td>17（#55-#71）</td>
      </tr>
      <tr>
          <td>既有卡修改</td>
          <td>31 張（rule 7 稽核）</td>
      </tr>
      <tr>
          <td>新 skill reference</td>
          <td>1（data-flow-and-filter-composition）</td>
      </tr>
      <tr>
          <td>Skill 版本</td>
          <td>requirement-protocol v0.1 → v0.2、frontend-with-playwright v0.1 → v0.2</td>
      </tr>
      <tr>
          <td>Playwright tests</td>
          <td>8</td>
      </tr>
      <tr>
          <td>RED-GREEN cycles</td>
          <td>2（初版測試 + 強化版）</td>
      </tr>
      <tr>
          <td>CI workflows 加 / 修</td>
          <td>2（新增 playwright + 修 deploy multi-index）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="學到什麼">學到什麼</h2>
<h3 id="1-cards-skills-系統是雙向的">1. Cards-skills 系統是雙向的</h3>
<p>不是「先寫卡片、再用卡片」。是「卡片指導實作、實作問題回流卡片」。每一輪迭代都把學到的東西反饋。本次 14 張新卡有 8 張是修過程中實際遇到的問題抽出來的、不是預先想的。</p>
<h3 id="2-user-提問是外部觸發">2. User 提問是「外部觸發」</h3>
<p>我自己跑 #67 / #68 / Checkpoint 1 的機率低 — 因為這些都是「沒便利路徑」的工作。User 的兩次提問（「有先寫測試嗎」+「需求確認最重要功能」）剛好對應 #69 + Checkpoint 1 的觸發。<strong>結構性偏差需要外部觸發來修正、不能靠自我提醒</strong>。</p>
<h3 id="3-test-過--對齊使用者意圖">3. Test 過 ≠ 對齊使用者意圖</h3>
<p>第一輪修完、跑 4/4 GREEN、看起來完工。實際漏了：</p>
<ul>
<li>3 個測試是 placebo（沒做 RED 不知道）</li>
<li>3 個 silent 缺口（沒做 Checkpoint 1 不知道）</li>
</ul>
<p>任何「跑得通就 OK」的訊號都低資訊量。Real 訊號 = 對照「使用者意圖完整集合」逐一驗收。</p>
<h3 id="4-一個-bug-修完--一個-case-study-起點">4. 一個 bug 修完 = 一個 case study 起點</h3>
<p>如果停在「bug 修了、test 過了」、這次任務 5 個 commits 結束。User 的兩次提問把它變成 30+ 個 commits 的 case study、產出 17 張新卡 + 兩個 skill 升級 + CI 補強。<strong>修 bug 是 trigger、不是終點</strong>。</p>
<hr>
<h2 id="適合-reuse-這個流程的條件">適合 reuse 這個流程的條件</h2>
<p>不是每個 bug 都該走這套。適合的訊號：</p>
<ul>
<li>Bug 修法不直觀、會碰到多種策略選項（→ 需要 #59 類取捨架構）</li>
<li>修法可能影響其他 feature 或產生新案例（→ 需要 Checkpoint 1）</li>
<li>需要長期 regression 防護（→ 需要 #69 RED-GREEN 驗證）</li>
<li>修的過程中發現新原則（→ 抽卡片）</li>
</ul>
<p>不適合：純 typo / config / build 失敗 — 直接修。</p>
<hr>
<h2 id="對未來想用這套系統的人">對未來想用這套系統的人</h2>
<p>進入點：</p>
<ol>
<li>讀 <code>content/skills/_index.md</code> — 三個 skill 的 routing table</li>
<li>從你的問題情境找對應 skill：
<ul>
<li>不確定怎麼跟 user 溝通 → <code>requirement-protocol</code></li>
<li>前端 / 資料流實作 → <code>frontend-with-playwright</code></li>
<li>寫文件 / 註解 / log → <code>compositional-writing</code></li>
</ul>
</li>
<li>Skill 路由你到 specific reference、reference 路由你到 <code>content/report/</code> 深度卡片</li>
<li>修問題過程中發現新原則 → 抽卡片回流</li>
</ol>
<p>「卡片不是在實作之前一次寫完、是在實作之中持續累積」 — 這套系統的 leverage 在於「下一個類似問題能直接用、不用重新發明」。</p>
<hr>
<h2 id="結語">結語</h2>
<p><code>content/report/</code> 從 54 張長到 71 張、<code>.claude/skills/</code> 從 v0.1 進到 v0.2、CI 從假 pass 變真防護、search bug 從 silent 失敗變到 8/8 regression test 守護。</p>
<p>過程不是線性。是「先做 → 抓到 dogfooding 失敗 → 抽卡片 → 回頭修 → 再被抓失敗 → 再抽卡片 → 再修」。每一輪都讓系統往對齊使用者意圖的方向多走一點。</p>
<p>User 的角色關鍵：兩次提問都不在「指出 bug」、是在「指出我跳過的 checkpoint」。這是純執行者看不到的盲點 — 自己的 dogfooding 失敗。<strong>外部 reviewer 是 cards-skills 系統的必要組件、不是 optional</strong>。</p>
<p>下次有類似情境的人 — 不需要把這條路再走一遍、直接用 #55-#71 + 三個 skill 起步。如果發現新 case、抽新卡回流。系統的價值在每次使用都會變強。</p>
]]></content:encoded></item><item><title>BDD 測試方法論</title><link>https://tarrragon.github.io/blog/record/bdd-%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/bdd-%E6%B8%AC%E8%A9%A6%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;p>三個月的重構週期結束後，我們檢視了測試套件，發現一個令人沮喪的問題：每次修改內部實作，即使業務邏輯完全沒變，也需要跟著修改大量測試。一個 Repository 實作替換，導致二十幾個測試需要逐一調整。&lt;/p>
&lt;p>這不是測試該有的樣子。問題根源在於測試耦合了實作細節，而非行為。&lt;/p></description><content:encoded><![CDATA[<p>三個月的重構週期結束後，我們檢視了測試套件，發現一個令人沮喪的問題：每次修改內部實作，即使業務邏輯完全沒變，也需要跟著修改大量測試。一個 Repository 實作替換，導致二十幾個測試需要逐一調整。</p>
<p>這不是測試該有的樣子。問題根源在於測試耦合了實作細節，而非行為。</p>
<h2 id="bdd-的核心定位">BDD 的核心定位</h2>
<p>BDD 是 TDD 的演進，它要求測試描述系統的「行為」而非「實作」。</p>
<p>行為是使用者視角觀察到的系統反應；實作是程式內部的技術細節。這個區別看起來簡單，實際撰寫測試時卻很容易模糊。</p>
<p>BDD 解決三個問題：</p>
<p><strong>測試維護成本高</strong>。傳統單元測試緊密耦合實作細節，重構時即使行為沒變，測試仍需大量修改。BDD 讓重構時測試保持穩定。</p>
<p><strong>需求追溯困難</strong>。測試充滿技術細節，無法對應業務需求。Given-When-Then 場景即是需求文件，測試即規格。</p>
<p><strong>溝通成本高</strong>。開發、測試和業務人員用不同語言描述系統行為。BDD 統一使用業務語言，建立共通溝通基礎。</p>
<p>我們的分工是：Clean Architecture 定義架構分層，TDD 四階段流程定義開發節奏，BDD 定義測試內容和撰寫規範。</p>
<h2 id="given-when-then-結構">Given-When-Then 結構</h2>
<p>Given 描述系統的初始狀態，必須明確完整，只包含與此場景相關的資料。常見錯誤是前置條件模糊，或包含大量無關測試資料。</p>
<p>When 描述使用者執行的操作，必須是單一動作，使用業務語言。「呼叫 Repository 的 save 方法」是技術術語；「使用者提交訂單」是業務語言。一個 When 不能包含多個動作。</p>
<p>Then 描述執行後的狀態變化或結果，必須是可觀察的行為。「Repository 的 save 方法被呼叫一次」是實作細節；「訂單成功儲存並回傳訂單編號」是可觀察的行為。</p>
<p>判斷行為還是實作的方法很簡單：使用者能否觀察到？改變實作會影響這個結果嗎？產品經理需要關心嗎？都是「能觀察、不影響、需要關心」就是行為，反之是實作細節。</p>
<h2 id="行為測試和實作測試的差異">行為測試和實作測試的差異</h2>
<p>測試實作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;OrderRepository.save should call database.insert&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">repository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">verify</span><span class="p">(</span><span class="n">database</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="s1">&#39;orders&#39;</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="n">toJson</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>這個測試關注「如何儲存」，替換資料庫或重構儲存邏輯就會失敗。</p>
<p>測試行為：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單 - 訂單成功儲存&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: 使用者已選擇商品並填寫完整資訊
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">validOrder</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// When: 使用者提交訂單
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">submitOrderUseCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// Then: 系統確認訂單已儲存
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">orderId</span><span class="p">,</span> <span class="n">isNotEmpty</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>這個測試關注「訂單是否成功儲存」，重構儲存機制不會影響結果。</p>
<p>測試描述的視角同樣重要。從技術元件角度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;當 Repository 回傳 null 時 UseCase 拋出例外&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>從使用者視角：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單失敗 - 商品庫存不足&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// Given: 商品庫存為 0
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// When: 使用者嘗試提交訂單
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="c1">// Then: 系統回應「庫存不足」錯誤
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h2 id="分層測試策略">分層測試策略</h2>
<p>BDD 不適用所有架構層級，每層特性不同，測試策略也不同。</p>
<p><strong>UseCase 層</strong>是 BDD 的核心應用層，代表完整的使用者操作流程，必須使用 Given-When-Then 結構，涵蓋所有業務場景。</p>
<p><strong>Domain 層</strong>包含核心業務規則、值物件驗證和實體不變量，需要細緻的邊界條件測試，單元測試更適合。</p>
<p><strong>Behavior 層</strong>負責 ViewModel 轉換和事件處理，只有複雜轉換邏輯需要獨立測試，簡單轉換由 UseCase 層覆蓋即可。</p>
<p><strong>UI 層</strong>測試成本高，只測試關鍵互動路徑，使用整合測試。</p>
<p><strong>Interface 層</strong>只定義契約，沒有實作邏輯，不需要測試。</p>
<h2 id="mock-策略">Mock 策略</h2>
<p>核心原則：只 Mock 外層依賴，不 Mock 內層邏輯。</p>
<p>外層依賴（Repository、Service、Event Publisher）透過 Interface 進行 Mock，隔離外部系統。內層邏輯（Domain Entity、Value Object）必須使用真實物件，確保測試涵蓋真實業務邏輯。</p>
<p>正確寫法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Mock Repository（外層依賴）
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">mockRepository</span> <span class="o">=</span> <span class="n">MockOrderRepository</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 使用真實的 Domain Entity（內層邏輯）
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kd">final</span> <span class="n">useCase</span> <span class="o">=</span> <span class="n">SubmitOrderUseCase</span><span class="p">(</span><span class="nl">repository:</span> <span class="n">mockRepository</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">orderId</span><span class="p">,</span> <span class="s1">&#39;order-123&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>錯誤寫法是 Mock Domain Entity：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">mockOrder</span> <span class="o">=</span> <span class="n">MockOrder</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">.</span><span class="n">validate</span><span class="p">()).</span><span class="n">thenReturn</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="c1">// 沒有測試到任何真實業務邏輯
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h2 id="與-tdd-階段整合">與 TDD 階段整合</h2>
<p><strong>階段一（功能設計）</strong>：從需求識別使用者行為場景。「使用者可以提交訂單」需要提取多個場景：成功提交、庫存不足失敗、金額無效失敗等，每個場景涵蓋正常流程、異常流程和邊界條件。</p>
<p><strong>階段二（測試設計）</strong>：將行為場景轉換為可執行的測試程式碼，先建立結構，設置 Mock，再依 Given-When-Then 填入邏輯。</p>
<p><strong>階段三（實作策略）</strong>：測試先行。先完成所有測試場景並確認失敗（Red），才開始實作 UseCase 讓測試通過（Green）。</p>
<p><strong>階段四（重構優化）</strong>：重構時，行為測試必須保持穩定。重構導致測試需要修改，代表測試耦合了實作。</p>
<p>判斷重構品質的標準很清楚：替換 Repository 實作、改變演算法，不應讓測試失敗；改變業務規則、調整可觀察的錯誤訊息，才應讓測試失敗。</p>
<h2 id="常見挑戰">常見挑戰</h2>
<h3 id="測試覆蓋率盲點">測試覆蓋率盲點</h3>
<p>BDD 強調測試「重要行為」，可能讓某些程式碼未被覆蓋。混合策略解決這個問題：UseCase 層 100% BDD 測試，Domain 層複雜邏輯 100% 單元測試，整體維持 80% 程式碼覆蓋率目標。</p>
<h3 id="學習曲線">學習曲線</h3>
<p>從「測試實作」轉向「測試行為」需要思維轉換，初期容易寫出「假行為測試」（實際上還是在測試實作）。建立範例庫和測試模板很有幫助：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;[業務場景描述] - 成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: [前置條件]
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">input</span> <span class="o">=</span> <span class="p">[</span><span class="err">準備測試資料</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">[</span><span class="err">設置</span> <span class="n">Mock</span> <span class="err">行為</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// When: [觸發動作]
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">input</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// Then: [預期結果]
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">expect</span><span class="p">([</span><span class="err">驗證業務結果</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="邊界條件容易被忽略">邊界條件容易被忽略</h3>
<p>業務場景描述容易遺漏技術性的邊界條件（null、異常、極端值）。每個 UseCase 最少需要：一個正常流程、兩個異常流程、三個邊界條件。建立技術性測試檢查清單並在 Code Review 重點確認。</p>
<h3 id="測試設置複雜度">測試設置複雜度</h3>
<p>UseCase 層的 BDD 測試需要 Mock 多個依賴，建立 Test Helper 和 Builder Pattern 減少重複：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">class</span> <span class="nc">UseCaseTestHelper</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">static</span> <span class="n">MockOrderRepository</span> <span class="n">createMockRepository</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">required</span> <span class="n">SaveResult</span> <span class="n">saveResult</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">final</span> <span class="n">mock</span> <span class="o">=</span> <span class="n">MockOrderRepository</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">when</span><span class="p">(</span><span class="n">mock</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">)).</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">saveResult</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="n">mock</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kd">class</span> <span class="nc">OrderBuilder</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kt">int</span> <span class="n">_amount</span> <span class="o">=</span> <span class="m">100</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kt">String</span> <span class="n">_userId</span> <span class="o">=</span> <span class="s1">&#39;user-001&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="n">OrderBuilder</span> <span class="n">withAmount</span><span class="p">(</span><span class="kt">int</span> <span class="n">amount</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">_amount</span> <span class="o">=</span> <span class="n">amount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="n">Order</span> <span class="n">build</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="n">_amount</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="n">_userId</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="行為粒度">行為粒度</h3>
<p>粒度太粗，失敗時難以定位；太細則接近單元測試，失去 BDD 優勢。採用「一個 UseCase 等於一個核心行為」的原則：UseCase 代表完整業務流程，名稱以動詞開頭（Submit, Cancel, Query），所有測試場景屬於同一個業務流程。</p>
<h3 id="業務需求變更">業務需求變更</h3>
<p>需求變更時測試場景仍需更新。集中管理業務規則常數減少影響範圍：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">OrderBusinessRules</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">static</span> <span class="kd">const</span> <span class="kt">int</span> <span class="n">freeShippingThreshold</span> <span class="o">=</span> <span class="m">1000</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">static</span> <span class="kd">const</span> <span class="kt">int</span> <span class="n">maxOrderAmount</span> <span class="o">=</span> <span class="m">100000</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">static</span> <span class="kd">const</span> <span class="kt">int</span> <span class="n">minOrderAmount</span> <span class="o">=</span> <span class="m">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="完整範例">完整範例</h2>
<p>以「使用者提交訂單」為例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">  1</span><span class="cl"><span class="n">group</span><span class="p">(</span><span class="s1">&#39;SubmitOrderUseCase&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  2</span><span class="cl">  <span class="n">late</span> <span class="n">MockOrderRepository</span> <span class="n">mockRepository</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  3</span><span class="cl">  <span class="n">late</span> <span class="n">MockInventoryService</span> <span class="n">mockInventoryService</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  4</span><span class="cl">  <span class="n">late</span> <span class="n">MockEventPublisher</span> <span class="n">mockEventPublisher</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  5</span><span class="cl">  <span class="n">late</span> <span class="n">SubmitOrderUseCase</span> <span class="n">useCase</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">  6</span><span class="cl">
</span></span><span class="line"><span class="ln">  7</span><span class="cl">  <span class="n">setUp</span><span class="p">(()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">  8</span><span class="cl">    <span class="n">mockRepository</span> <span class="o">=</span> <span class="n">MockOrderRepository</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">  9</span><span class="cl">    <span class="n">mockInventoryService</span> <span class="o">=</span> <span class="n">MockInventoryService</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 10</span><span class="cl">    <span class="n">mockEventPublisher</span> <span class="o">=</span> <span class="n">MockEventPublisher</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 11</span><span class="cl">    <span class="n">useCase</span> <span class="o">=</span> <span class="n">SubmitOrderUseCase</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 12</span><span class="cl">      <span class="nl">repository:</span> <span class="n">mockRepository</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 13</span><span class="cl">      <span class="nl">inventoryService:</span> <span class="n">mockInventoryService</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 14</span><span class="cl">      <span class="nl">eventPublisher:</span> <span class="n">mockEventPublisher</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 15</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 16</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 17</span><span class="cl">
</span></span><span class="line"><span class="ln"> 18</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;正常流程&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 19</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 20</span><span class="cl">      <span class="c1">// Given: 使用者已選擇商品且填寫完整資訊
</span></span></span><span class="line"><span class="ln"> 21</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 22</span><span class="cl">        <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 23</span><span class="cl">        <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 24</span><span class="cl">        <span class="nl">items:</span> <span class="p">[</span><span class="n">OrderItem</span><span class="p">(</span><span class="nl">productId:</span> <span class="s1">&#39;prod-001&#39;</span><span class="p">,</span> <span class="nl">quantity:</span> <span class="m">2</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 25</span><span class="cl">        <span class="nl">shippingAddress:</span> <span class="n">Address</span><span class="p">(</span><span class="nl">city:</span> <span class="s1">&#39;台北市&#39;</span><span class="p">,</span> <span class="nl">district:</span> <span class="s1">&#39;信義區&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 26</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 27</span><span class="cl">      <span class="n">when</span><span class="p">(</span><span class="n">mockInventoryService</span><span class="p">.</span><span class="n">checkStock</span><span class="p">(</span><span class="s1">&#39;prod-001&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 28</span><span class="cl">          <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">StockStatus</span><span class="p">.</span><span class="n">available</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 29</span><span class="cl">      <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 30</span><span class="cl">          <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 31</span><span class="cl">
</span></span><span class="line"><span class="ln"> 32</span><span class="cl">      <span class="c1">// When: 使用者點擊「提交訂單」
</span></span></span><span class="line"><span class="ln"> 33</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 34</span><span class="cl">
</span></span><span class="line"><span class="ln"> 35</span><span class="cl">      <span class="c1">// Then: 系統確認訂單已儲存並回傳訂單編號
</span></span></span><span class="line"><span class="ln"> 36</span><span class="cl"><span class="c1"></span>      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 37</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">orderId</span><span class="p">,</span> <span class="s1">&#39;order-123&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 38</span><span class="cl">      <span class="n">verify</span><span class="p">(</span><span class="n">mockEventPublisher</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">any</span><span class="p">.</span><span class="n">having</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 39</span><span class="cl">        <span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">e</span><span class="p">.</span><span class="n">type</span><span class="p">,</span> <span class="s1">&#39;event type&#39;</span><span class="p">,</span> <span class="n">EventType</span><span class="p">.</span><span class="n">orderCreated</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 40</span><span class="cl">      <span class="p">))).</span><span class="n">called</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 41</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 42</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 43</span><span class="cl">
</span></span><span class="line"><span class="ln"> 44</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;異常流程&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 45</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單失敗 - 商品庫存不足&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 46</span><span class="cl">      <span class="c1">// Given: 選擇的商品庫存為 0
</span></span></span><span class="line"><span class="ln"> 47</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 48</span><span class="cl">        <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 49</span><span class="cl">        <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 50</span><span class="cl">        <span class="nl">items:</span> <span class="p">[</span><span class="n">OrderItem</span><span class="p">(</span><span class="nl">productId:</span> <span class="s1">&#39;prod-001&#39;</span><span class="p">,</span> <span class="nl">quantity:</span> <span class="m">2</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 51</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 52</span><span class="cl">      <span class="n">when</span><span class="p">(</span><span class="n">mockInventoryService</span><span class="p">.</span><span class="n">checkStock</span><span class="p">(</span><span class="s1">&#39;prod-001&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 53</span><span class="cl">          <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">StockStatus</span><span class="p">.</span><span class="n">outOfStock</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 54</span><span class="cl">
</span></span><span class="line"><span class="ln"> 55</span><span class="cl">      <span class="c1">// When: 使用者點擊「提交訂單」
</span></span></span><span class="line"><span class="ln"> 56</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 57</span><span class="cl">
</span></span><span class="line"><span class="ln"> 58</span><span class="cl">      <span class="c1">// Then: 系統回應庫存不足錯誤，不儲存訂單
</span></span></span><span class="line"><span class="ln"> 59</span><span class="cl"><span class="c1"></span>      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 60</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">error</span><span class="p">,</span> <span class="n">ErrorType</span><span class="p">.</span><span class="n">outOfStock</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 61</span><span class="cl">      <span class="n">verifyNever</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 62</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 63</span><span class="cl">
</span></span><span class="line"><span class="ln"> 64</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單失敗 - Repository 儲存失敗&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 65</span><span class="cl">      <span class="c1">// Given: Repository 無法儲存（網路錯誤）
</span></span></span><span class="line"><span class="ln"> 66</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 67</span><span class="cl">        <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 68</span><span class="cl">        <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 69</span><span class="cl">        <span class="nl">items:</span> <span class="p">[</span><span class="n">OrderItem</span><span class="p">(</span><span class="nl">productId:</span> <span class="s1">&#39;prod-001&#39;</span><span class="p">,</span> <span class="nl">quantity:</span> <span class="m">1</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 70</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 71</span><span class="cl">      <span class="n">when</span><span class="p">(</span><span class="n">mockInventoryService</span><span class="p">.</span><span class="n">checkStock</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 72</span><span class="cl">          <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">StockStatus</span><span class="p">.</span><span class="n">available</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 73</span><span class="cl">      <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 74</span><span class="cl">          <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">failure</span><span class="p">(</span><span class="s1">&#39;網路連線失敗&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 75</span><span class="cl">
</span></span><span class="line"><span class="ln"> 76</span><span class="cl">      <span class="c1">// When: 使用者點擊「提交訂單」
</span></span></span><span class="line"><span class="ln"> 77</span><span class="cl"><span class="c1"></span>      <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 78</span><span class="cl">
</span></span><span class="line"><span class="ln"> 79</span><span class="cl">      <span class="c1">// Then: 系統回應訂單提交失敗
</span></span></span><span class="line"><span class="ln"> 80</span><span class="cl"><span class="c1"></span>      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 81</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">error</span><span class="p">,</span> <span class="n">ErrorType</span><span class="p">.</span><span class="n">saveFailed</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 82</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 83</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 84</span><span class="cl">
</span></span><span class="line"><span class="ln"> 85</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;邊界條件&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 86</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單失敗 - 訂單金額為 0&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 87</span><span class="cl">      <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 88</span><span class="cl">        <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">0</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 89</span><span class="cl">        <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 90</span><span class="cl">        <span class="nl">items:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln"> 91</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 92</span><span class="cl">      <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 93</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 94</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">error</span><span class="p">,</span> <span class="n">ErrorType</span><span class="p">.</span><span class="n">invalidAmount</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 95</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 96</span><span class="cl">
</span></span><span class="line"><span class="ln"> 97</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;建立負數金額訂單拋出例外&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 98</span><span class="cl">      <span class="n">expect</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 99</span><span class="cl">        <span class="p">()</span> <span class="o">=&gt;</span> <span class="n">Order</span><span class="p">(</span><span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="o">-</span><span class="m">100</span><span class="p">),</span> <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">)),</span>
</span></span><span class="line"><span class="ln">100</span><span class="cl">        <span class="n">throwsA</span><span class="p">(</span><span class="n">isA</span><span class="o">&lt;</span><span class="n">InvalidAmountException</span><span class="o">&gt;</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">101</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">102</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">103</span><span class="cl">
</span></span><span class="line"><span class="ln">104</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單失敗 - 訂單金額超過上限&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">105</span><span class="cl">      <span class="kd">final</span> <span class="n">order</span> <span class="o">=</span> <span class="n">Order</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">106</span><span class="cl">        <span class="nl">amount:</span> <span class="n">OrderAmount</span><span class="p">(</span><span class="m">1000001</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">107</span><span class="cl">        <span class="nl">userId:</span> <span class="n">UserId</span><span class="p">(</span><span class="s1">&#39;user-001&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">108</span><span class="cl">        <span class="nl">items:</span> <span class="p">[</span><span class="n">OrderItem</span><span class="p">(</span><span class="nl">productId:</span> <span class="s1">&#39;prod-001&#39;</span><span class="p">,</span> <span class="nl">quantity:</span> <span class="m">10000</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln">109</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">110</span><span class="cl">      <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">useCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">111</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">112</span><span class="cl">      <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">error</span><span class="p">,</span> <span class="n">ErrorType</span><span class="p">.</span><span class="n">amountExceedsLimit</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">113</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">114</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">115</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h2 id="結論">結論</h2>
<p>回頭看最初那個重構週期，二十幾個因為替換 Repository 實作而失敗的測試，問題很清楚：測試在監視實作細節，而不是守護業務行為。</p>
<p>切換到 BDD 之後，同樣的重構只需確認業務行為沒有改變，測試套件就能保持穩定。</p>
<p>但 BDD 不是萬靈丹。它需要思維轉換，需要建立明確規範，需要持續 Code Review 維持品質。混合策略（UseCase 層 BDD、Domain 層單元測試、UI 層整合測試）才能真正發揮效果。</p>]]></content:encoded></item><item><title>TDD-Ticket 整合方法論 - 測試驅動開發與任務追蹤的無縫銜接</title><link>https://tarrragon.github.io/blog/record/tdd-ticket-%E6%95%B4%E5%90%88%E6%96%B9%E6%B3%95%E8%AB%96-%E6%B8%AC%E8%A9%A6%E9%A9%85%E5%8B%95%E9%96%8B%E7%99%BC%E8%88%87%E4%BB%BB%E5%8B%99%E8%BF%BD%E8%B9%A4%E7%9A%84%E7%84%A1%E7%B8%AB%E9%8A%9C%E6%8E%A5/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/tdd-ticket-%E6%95%B4%E5%90%88%E6%96%B9%E6%B3%95%E8%AB%96-%E6%B8%AC%E8%A9%A6%E9%A9%85%E5%8B%95%E9%96%8B%E7%99%BC%E8%88%87%E4%BB%BB%E5%8B%99%E8%BF%BD%E8%B9%A4%E7%9A%84%E7%84%A1%E7%B8%AB%E9%8A%9C%E6%8E%A5/</guid><description>&lt;p>有一段時間，我們的開發流程有個裂縫。&lt;/p>
&lt;p>TDD 走得好好的：Phase 1 設計介面，Phase 2 寫測試規格，Phase 3 實作，Phase 4 重構評估。Ticket 系統也運作正常：建立、認領、完成、歸檔。但這兩套系統各走各的路。結果是開發者在 Phase 3 開始實作時，才發現手上的任務其實包含三件事。或者更糟：沒發現，就這樣把三件事塞進一個 Ticket 完成了。&lt;/p></description><content:encoded><![CDATA[<p>有一段時間，我們的開發流程有個裂縫。</p>
<p>TDD 走得好好的：Phase 1 設計介面，Phase 2 寫測試規格，Phase 3 實作，Phase 4 重構評估。Ticket 系統也運作正常：建立、認領、完成、歸檔。但這兩套系統各走各的路。結果是開發者在 Phase 3 開始實作時，才發現手上的任務其實包含三件事。或者更糟：沒發現，就這樣把三件事塞進一個 Ticket 完成了。</p>
<p>問題根源很清楚：Ticket 設計決策沒有固定時間點，也沒有固定標準。有人在需求進來時就設計 Ticket，有人在 Phase 1 結束後，有人在開始實作前隨手建一個。技術債務藏在職責混亂的 Ticket 裡，等到 Phase 4 才浮出水面，代價已經翻了好幾倍。</p>
<p>解決方案不複雜，但需要紀律：把 Ticket 設計決策集中到 Phase 3a，只用一套固定標準來判斷。</p>
<h2 id="phase-3a-是唯一的決策點">Phase 3a 是唯一的決策點</h2>
<p>第一條規則：Ticket 設計決策只在 Phase 3a 進行。</p>
<p>Phase 1 專注功能設計，Phase 2 專注測試設計，這時候分心去設計 Ticket 結構反而干擾品質。等到 Phase 3a，手上已經有完整的測試規格，此時問「這個任務需不需要拆分」才有足夠資訊，而且判斷結果會直接影響 Phase 3b 的執行方式——決策和執行緊密相連。</p>
<p>Phase 3b 按 Phase 3a 的評估結果執行，Phase 4 做跨 Ticket 的重構評估，但不新增 Ticket。</p>
<h2 id="丟掉量化指標">丟掉量化指標</h2>
<p>用什麼標準判斷要不要拆分？</p>
<p>過去常見做法是量化估計：修改幾個檔案？幾個測試案例？這些數字看起來有根據，用起來卻不可靠。</p>
<p>一個修改 10 個檔案的任務，如果全都是針對同一件事（把某個 API 改名，所有用到它的地方跟著更新），那就是原子任務，不需要拆分。反過來，只改 2 個檔案，如果同時做「驗證邏輯」和「效能最佳化」，就該拆成兩個。</p>
<p>工作量大小和職責數量不是同一件事。</p>
<h2 id="四大檢查唯一的評估標準">四大檢查：唯一的評估標準</h2>
<p>四個問題，確認同一件事：這個任務是不是只做一件事？</p>
<p><strong>語義檢查</strong>：能用「動詞 + 單一目標」描述嗎？「實作 startScan() 方法」通過，「實作掃描功能和離線支援」不通過。有時候任務的問題在名字上就看得出來。</p>
<p><strong>修改原因檢查</strong>：這個任務只有一個修改原因嗎？來自 SRP 的概念，搬到 Ticket 層次：如果這個 Ticket 將來需要修改，觸發修改的原因只有一個嗎？同時受到「API 規格變更」和「離線儲存格式變更」影響的，就是兩個修改原因，應該拆開。這樣 API 改變時，只需要動一個 Ticket。</p>
<p><strong>驗收一致性檢查</strong>：所有驗收條件都指向同一個目標嗎？驗收清單同時包含「startScan() 通過測試」、「stopScan() 通過測試」、「離線快取功能正常」，這個 Ticket 在追求三個目標，需要拆分。</p>
<p><strong>依賴獨立性檢查</strong>：拆分後的部分之間會不會產生循環依賴？有時候兩件事看起來應該分開，但 Ticket A 的實作需要 Ticket B 完成，Ticket B 又需要 Ticket A 完成——這種情況保持為同一個 Ticket 才對。</p>
<p>決策邏輯直接：四項全部通過就繼續執行，任何一項未通過就拆分。不確定的預設為未通過。過度拆分的代價，遠低於讓職責混亂的任務進入實作。</p>
<h2 id="不確定時選擇拆分">不確定時，選擇拆分</h2>
<p>這條原則違反一些人的直覺：任務感覺稍微大了一點，但四大檢查又沒有明確說要拆，這時候怎麼辦？</p>
<p>拆。</p>
<p>原因是非對稱性。拆了但其實不需要拆，代價是多幾個 Ticket、多一些追蹤開銷。沒拆但其實應該拆，代價是職責混亂的實作、測試難以隔離、Phase 4 牽一髮動全身。後者的代價明顯更高。</p>
<h2 id="拆分之後">拆分之後</h2>
<p>判定需要拆分，Phase 3a 的工作是把任務分解成多個各自通過四大檢查的 Atomic Ticket，並建立它們之間的依賴關係（哪個必須先完成，哪些可以並行）。</p>
<p>規劃結果記錄到工作日誌，PM 審核確認後按 Wave 順序執行——有依賴的先完成，無依賴的並行。每個 Ticket 完成後立即 Review，不等全部完成再回頭看。</p>
<p>一個細節：拆分出來的每個 Ticket 本身也要通過四大檢查。如果某個拆出來的 Ticket 還是不通過，繼續拆。</p>
<h2 id="在實作中發現需要拆分">在實作中發現需要拆分</h2>
<p>有時 Phase 3a 評估沒問題，但 Phase 3b 實作途中才發現任務包含多個職責——這表示 Phase 3a 有遺漏。</p>
<p>正確做法：停下來，回到 Phase 3a 重新評估，拆分，從拆分後的第一個 Ticket 重新開始。</p>
<p>這聽起來很傷，但繼續實作一個已知職責混亂的任務，只是在未來製造更大的麻煩。</p>
<h2 id="積極派發子任務">積極派發子任務</h2>
<p>實作中遇到預期外的情況：發現新問題需要調查、範圍比預期大、需要做技術決策——原則是積極建立子任務，不要在一個 Ticket 裡處理所有事情。</p>
<p>目的是保持可追蹤性。每個被處理的問題都有對應的 Ticket，日後審查開發歷程時，能清楚看到每個決策的前因後果。</p>
<h2 id="整合之後">整合之後</h2>
<p>整合後得到的不只是更整齊的任務管理。Phase 3b 的開發者拿到手的每個任務都是職責明確的，不需要在實作途中自己判斷「這個應不應該一起做」。Phase 4 的重構評估也更聚焦，每個 Ticket 邊界清晰，影響範圍好估計。</p>
<p>這套整合需要紀律——Phase 3a 的四大檢查不是走過場，決策如果散落在各個階段，整合就失去意義。</p>
<p>但兩套系統會相互強化：好的 Ticket 設計讓 TDD 執行更流暢，嚴格的 TDD 流程讓 Ticket 的職責判斷更有依據。</p>]]></content:encoded></item><item><title>行為優先的TDD方法論 - Sociable Unit Tests實踐指南</title><link>https://tarrragon.github.io/blog/record/%E8%A1%8C%E7%82%BA%E5%84%AA%E5%85%88%E7%9A%84tdd%E6%96%B9%E6%B3%95%E8%AB%96-sociable-unit-tests%E5%AF%A6%E8%B8%90%E6%8C%87%E5%8D%97/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E8%A1%8C%E7%82%BA%E5%84%AA%E5%85%88%E7%9A%84tdd%E6%96%B9%E6%B3%95%E8%AB%96-sociable-unit-tests%E5%AF%A6%E8%B8%90%E6%8C%87%E5%8D%97/</guid><description>&lt;p>曾經有一段時間，我們團隊對TDD又愛又恨。「寫測試讓我們更有信心」，但「重構時要改一堆測試，還不如不寫」。這種矛盾讓我們反覆懷疑：TDD到底有沒有用？&lt;/p>
&lt;p>深入研究Kent Beck的原著和Valentina Jemuović的演講後，才發現問題出在我們誤解了「測試單元」是什麼。&lt;/p></description><content:encoded><![CDATA[<p>曾經有一段時間，我們團隊對TDD又愛又恨。「寫測試讓我們更有信心」，但「重構時要改一堆測試，還不如不寫」。這種矛盾讓我們反覆懷疑：TDD到底有沒有用？</p>
<p>深入研究Kent Beck的原著和Valentina Jemuović的演講後，才發現問題出在我們誤解了「測試單元」是什麼。</p>
<h2 id="痛苦的根本原因">痛苦的根本原因</h2>
<p>許多團隊學TDD時，都被教導「每個class寫一個test class，每個method寫一個test method」。這個看似合理的原則，埋下了長期的痛苦。</p>
<p>問題在於，這樣的測試耦合到了程式的<strong>結構</strong>，而非<strong>行為</strong>。只要重構——把一個class拆成兩個、把方法提取到新類別——測試就跟著破裂。維護測試的時間甚至超過寫功能本身。</p>
<p>Kent Beck在《Test Driven Development By Example》第一頁就寫道：</p>
<blockquote>
<p>&ldquo;Programmer tests should be sensitive to behavior changes and insensitive to structure changes.&rdquo;</p></blockquote>
<p>測試應該對行為的改變敏感，對結構的改變不敏感。如果重構時測試跟著爆炸，原因就在這裡。</p>
<h2 id="測試是可執行的需求規格">測試是可執行的需求規格</h2>
<p>需要先轉換一個根本認知：測試不是「驗證實作正確的工具」，而是<strong>用程式碼表達的需求規格書</strong>。</p>
<p>需求定義系統「應該做什麼」，實作是「怎麼做」的一種方式。需求應該保持穩定，實作可以隨時改變。Martin Fowler在《Refactoring》中說：</p>
<blockquote>
<p>&ldquo;Refactoring is a way of restructuring an existing body of code, altering its internal structure without changing its external behavior.&rdquo;</p></blockquote>
<p>重構改變內部結構，不改變外部行為。耦合到行為的測試，在重構時自然保持穩定。</p>
<h2 id="sociable-unit-tests把module當作測試單元">Sociable Unit Tests：把Module當作測試單元</h2>
<p>TDD有兩種截然不同的流派。</p>
<p><strong>Classical TDD</strong>（Kent Beck、Martin Fowler的做法）把Unit定義為Module——一個或多個協同工作的類別組合，對外提供清晰的Public API。測試只透過這個Public API互動，不知道Module內部有哪些類別、它們如何協作。唯一需要Mock的是真正的外部依賴：資料庫、檔案系統、外部服務。這種風格稱為<strong>Sociable Unit Tests</strong>。</p>
<p><strong>Mockist TDD</strong>（London School）把Unit定義為單一Class，Mock所有協作者。這種風格稱為<strong>Solitary Unit Tests</strong>。</p>
<p>核心差異在耦合對象：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Sociable: Test → [Module API] → Module Implementation（黑盒）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Solitary: Test → Mock(B) → Class A → Class B
</span></span><span class="line"><span class="ln">3</span><span class="cl">                 Mock(C)           → Class C</span></span></code></pre></div><p>Sociable只有一條耦合線，Solitary有多條。每一條耦合線都是日後的維護成本。</p>
<h2 id="重構安全性的驗證">重構安全性的驗證</h2>
<p>判斷自己的測試是Sociable還是Solitary，有個簡單的驗證方法：</p>
<p>改變Module的內部邏輯、調整類別結構、重新命名內部方法。如果所有測試依然通過，不需要修改，那你寫的是Sociable（正確）。如果任何測試需要跟著改，那你寫的是Solitary（需要重新設計）。</p>
<p>以一個訂單提交的例子來說，Sociable測試看起來像這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: Mock外部依賴（只Mock Repository）
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// When: 透過Use Case API提交訂單
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">submitOrderUseCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// Then: 驗證可觀察的行為結果
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">orderId</span><span class="p">,</span> <span class="s1">&#39;order-123&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="c1">// 測試不知道Order內部如何計算、驗證
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">// 測試使用真實的Domain Entities
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>而Solitary測試會是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;OrderService.submitOrder calls Repository.save&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: Mock所有協作者
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">mockOrder</span> <span class="o">=</span> <span class="n">MockOrder</span><span class="p">();</span>          <span class="c1">// 連Order也Mock了
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">mockValidator</span> <span class="o">=</span> <span class="n">MockOrderValidator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">final</span> <span class="n">mockCalculator</span> <span class="o">=</span> <span class="n">MockPriceCalculator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockValidator</span><span class="p">.</span><span class="n">validate</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">thenReturn</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockCalculator</span><span class="p">.</span><span class="n">calculate</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">thenReturn</span><span class="p">(</span><span class="m">100</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="c1">// Then: 驗證方法呼叫次數（實作細節）
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="n">verify</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">called</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="c1">// 這個測試一旦重構OrderService的內部邏輯就會破裂
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h2 id="test-first的速度優勢">Test-First的速度優勢</h2>
<p>Test-First（先寫測試）比Test-Last（先寫程式再補測試）快，原因是問題被發現的時間點更早。</p>
<p>Test-First的Red-Green-Refactor循環強迫你在寫實作之前先思考介面：「這個功能怎麼用？」、「測試容不容易寫？」介面設計問題在寫測試時（最早期）就暴露，修復成本最低。</p>
<p>Test-Last則是程式寫完了才發現難以測試，這時通常意味著設計有問題，要改動的範圍更大。Kent Beck說TDD更快，指的正是這個。</p>
<h2 id="bdd不是新方法是修正命名">BDD不是新方法，是修正命名</h2>
<p>Dan North在2006年創造「BDD」，目的是修正TDD命名造成的混淆。</p>
<p>他發現「Test」這個詞讓開發人員誤以為要測試每個類別和方法，於是用「Behavior」取代，讓意圖更清楚：測試的是行為，不是程式結構。這和Kent Beck 2003年說的完全一致，只是換了個能讓人更直覺理解的詞。</p>
<p>Google在《Software Engineering at Google》中也驗證同樣的結論：「Don&rsquo;t write a test for each method. Write a test for each behavior.」</p>
<h2 id="與clean-architecture的結合">與Clean Architecture的結合</h2>
<p>Sociable Unit Tests和Clean Architecture是天然的組合，因為建立在相同原則上：業務邏輯獨立於外部世界。</p>
<p>在Clean Architecture中，Use Cases層是業務邏輯的進入點，對外提供清晰的API，對內只使用Domain Entities和透過介面隔離的外部依賴（Repository、Gateway等）。這個結構天然對應Sociable的需求：Use Cases的Public API就是測試邊界，Domain Entities用真實物件，只有Repository需要Mock。</p>
<p>更重要的是，對Use Cases的Unit Test同時就是業務驗收測試。一個寫著「使用者提交訂單成功」的案例，不需要啟動UI也不需要真實資料庫，但驗證了完整的業務流程。Alistair Cockburn在提出Hexagonal Architecture時說：「Tests are another user of the system.」</p>
<p>並非所有情況都適合Sociable。數學演算法、加密系統這類需要細粒度驗證的場景，精確定位到具體類別比重構穩定性更重要，用Solitary合理。但大多數商業應用不是這類。</p>
<h2 id="結論">結論</h2>
<p>我們曾以為TDD很痛苦，但那是因為我們測試的是程式<strong>長什麼樣子</strong>，而不是它<strong>做什麼</strong>。</p>
<p>正確的做法只有一句話：測試透過Module的Public API互動，只Mock真正的外部依賴，使用真實的Domain Entities。</p>
<p>這樣的測試在重構時保持穩定，在功能改變時精準報警。Kent Beck、Dan North、Martin Fowler在不同年代說的是同一件事：<strong>測試行為，而非結構</strong>。</p>
<hr>
<p>參考資料：</p>
<ul>
<li>Kent Beck，《Test Driven Development By Example》，2003</li>
<li>Martin Fowler，《Refactoring: Improving the Design of Existing Code》，1999</li>
<li>Dan North，《Introducing BDD》，2006</li>
<li>Google，《Software Engineering at Google》，2020</li>
<li>Valentina (Cupać) Jemuović，<a href="https://www.youtube.com/watch?v=3wxiQB2-m2k">TDD and Clean Architecture - Driven by Behaviour</a></li>
</ul>]]></content:encoded></item><item><title>Ticket 生命週期流程 - AI 協作開發的任務管理系統</title><link>https://tarrragon.github.io/blog/record/ticket-%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%B5%81%E7%A8%8B-ai-%E5%8D%94%E4%BD%9C%E9%96%8B%E7%99%BC%E7%9A%84%E4%BB%BB%E5%8B%99%E7%AE%A1%E7%90%86%E7%B3%BB%E7%B5%B1/</link><pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/ticket-%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%B5%81%E7%A8%8B-ai-%E5%8D%94%E4%BD%9C%E9%96%8B%E7%99%BC%E7%9A%84%E4%BB%BB%E5%8B%99%E7%AE%A1%E7%90%86%E7%B3%BB%E7%B5%B1/</guid><description>&lt;p>本文件定義 Ticket 從建立到完成的完整生命週期。這套系統是我在 AI 協作開發（Claude Code）過程中逐步建立的任務追蹤機制。&lt;/p>
&lt;h2 id="生命週期總覽">生命週期總覽&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">需求/問題產生
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">建立 Ticket (/ticket create)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">Ticket 狀態: pending
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">認領 Ticket (/ticket track claim)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">Ticket 狀態: in_progress
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> +-- 正常完成 --&amp;gt; /ticket track complete --&amp;gt; 狀態: completed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> +-- 無法繼續 --&amp;gt; /ticket track release --&amp;gt; 狀態: blocked
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> | |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> | v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> | 升級到 PM 處理
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> v
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">完成&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這套系統的核心目標是將任務需求有邏輯地拆分拆細。任務進來後先分析，拆分成平行的子任務；子任務若仍太大，可繼續往下切分。執行時從最底層開始，完成後檢查平行任務，再往上驗收父任務，直到整個任務鏈完成。&lt;/p>
&lt;p>任務拆小的好處：降低執行時的認知負擔，也讓驗收檢查更容易發現疏失。&lt;/p>
&lt;h2 id="ticket-狀態定義">Ticket 狀態定義&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;th>允許操作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>pending&lt;/td>
 &lt;td>等待處理&lt;/td>
 &lt;td>claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>in_progress&lt;/td>
 &lt;td>處理中&lt;/td>
 &lt;td>complete, release&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>completed&lt;/td>
 &lt;td>已完成&lt;/td>
 &lt;td>-&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>blocked&lt;/td>
 &lt;td>被阻塞&lt;/td>
 &lt;td>claim（重新認領）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="階段-標準流程對照表">階段-標準流程對照表&lt;/h2>
&lt;p>每個生命週期階段都有對應的標準流程和提示，防止關鍵步驟被遺漏。&lt;/p>
&lt;h3 id="建立階段">建立階段&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>標準流程&lt;/th>
 &lt;th>提示強度&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SA 前置審查評估&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>新功能/架構變更時需要 SA 審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任務拆分評估&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>認知負擔 &amp;gt; 10 時需要拆分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗收條件 4V 檢查&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>確保驗收條件可驗證、可量化、可追溯、可記錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>blockedBy 設定&lt;/td>
 &lt;td>提示&lt;/td>
 &lt;td>提醒設定依賴關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>decision_tree_path 填寫&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>派發驗證必需&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="認領階段">認領階段&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>標準流程&lt;/th>
 &lt;th>提示強度&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>阻塞依賴檢查&lt;/td>
 &lt;td>警告&lt;/td>
 &lt;td>如有阻塞依賴，顯示警告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設計文件閱讀&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>提醒閱讀相關規格和設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗收條件理解&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>確保理解驗收標準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error-patterns 查詢&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>IMP/ADJ 類型時建議查詢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="執行階段">執行階段&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>標準流程&lt;/th>
 &lt;th>提示強度&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>問題派發 incident-responder&lt;/td>
 &lt;td>強制&lt;/td>
 &lt;td>遇到錯誤時強制派發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工作日誌更新&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>執行過程記錄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="完成階段">完成階段&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>標準流程&lt;/th>
 &lt;th>提示強度&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>驗收條件勾選確認&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>所有條件必須勾選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>建議處理確認&lt;/td>
 &lt;td>建議&lt;/td>
 &lt;td>無 pending 建議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>派發 acceptance-auditor&lt;/td>
 &lt;td>強制&lt;/td>
 &lt;td>IMP/ADJ 類型必須執行驗收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任務鏈後續步驟建議&lt;/td>
 &lt;td>提示&lt;/td>
 &lt;td>分析並建議下一個 Ticket&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="驗收後階段">驗收後階段&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>標準流程&lt;/th>
 &lt;th>目的&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>技術債務記錄&lt;/td>
 &lt;td>將執行過程中發現的技術債務正式記錄，避免遺忘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CHANGELOG 更新&lt;/td>
 &lt;td>在版本發布時更新變更日誌，維護版本歷史的完整性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>學習經驗記錄&lt;/td>
 &lt;td>萃取任務中的學習經驗，建構團隊知識網絡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任務鏈進度更新&lt;/td>
 &lt;td>追蹤整體任務鏈完成度，便於掌握專案整體進度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="任務鏈後續步驟建議">任務鏈後續步驟建議&lt;/h2>
&lt;p>當 Ticket 完成時，系統會自動分析任務鏈狀態並建議下一步。&lt;/p></description><content:encoded><![CDATA[<p>本文件定義 Ticket 從建立到完成的完整生命週期。這套系統是我在 AI 協作開發（Claude Code）過程中逐步建立的任務追蹤機制。</p>
<h2 id="生命週期總覽">生命週期總覽</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">需求/問題產生
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">建立 Ticket (/ticket create)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">Ticket 狀態: pending
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    v
</span></span><span class="line"><span class="ln">10</span><span class="cl">認領 Ticket (/ticket track claim)
</span></span><span class="line"><span class="ln">11</span><span class="cl">    |
</span></span><span class="line"><span class="ln">12</span><span class="cl">    v
</span></span><span class="line"><span class="ln">13</span><span class="cl">Ticket 狀態: in_progress
</span></span><span class="line"><span class="ln">14</span><span class="cl">    |
</span></span><span class="line"><span class="ln">15</span><span class="cl">    +-- 正常完成 --&gt; /ticket track complete --&gt; 狀態: completed
</span></span><span class="line"><span class="ln">16</span><span class="cl">    |
</span></span><span class="line"><span class="ln">17</span><span class="cl">    +-- 無法繼續 --&gt; /ticket track release --&gt; 狀態: blocked
</span></span><span class="line"><span class="ln">18</span><span class="cl">    |                                              |
</span></span><span class="line"><span class="ln">19</span><span class="cl">    |                                              v
</span></span><span class="line"><span class="ln">20</span><span class="cl">    |                                         升級到 PM 處理
</span></span><span class="line"><span class="ln">21</span><span class="cl">    |
</span></span><span class="line"><span class="ln">22</span><span class="cl">    v
</span></span><span class="line"><span class="ln">23</span><span class="cl">完成</span></span></code></pre></div><p>這套系統的核心目標是將任務需求有邏輯地拆分拆細。任務進來後先分析，拆分成平行的子任務；子任務若仍太大，可繼續往下切分。執行時從最底層開始，完成後檢查平行任務，再往上驗收父任務，直到整個任務鏈完成。</p>
<p>任務拆小的好處：降低執行時的認知負擔，也讓驗收檢查更容易發現疏失。</p>
<h2 id="ticket-狀態定義">Ticket 狀態定義</h2>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>說明</th>
          <th>允許操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>pending</td>
          <td>等待處理</td>
          <td>claim</td>
      </tr>
      <tr>
          <td>in_progress</td>
          <td>處理中</td>
          <td>complete, release</td>
      </tr>
      <tr>
          <td>completed</td>
          <td>已完成</td>
          <td>-</td>
      </tr>
      <tr>
          <td>blocked</td>
          <td>被阻塞</td>
          <td>claim（重新認領）</td>
      </tr>
  </tbody>
</table>
<h2 id="階段-標準流程對照表">階段-標準流程對照表</h2>
<p>每個生命週期階段都有對應的標準流程和提示，防止關鍵步驟被遺漏。</p>
<h3 id="建立階段">建立階段</h3>
<table>
  <thead>
      <tr>
          <th>標準流程</th>
          <th>提示強度</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SA 前置審查評估</td>
          <td>建議</td>
          <td>新功能/架構變更時需要 SA 審查</td>
      </tr>
      <tr>
          <td>任務拆分評估</td>
          <td>建議</td>
          <td>認知負擔 &gt; 10 時需要拆分</td>
      </tr>
      <tr>
          <td>驗收條件 4V 檢查</td>
          <td>建議</td>
          <td>確保驗收條件可驗證、可量化、可追溯、可記錄</td>
      </tr>
      <tr>
          <td>blockedBy 設定</td>
          <td>提示</td>
          <td>提醒設定依賴關係</td>
      </tr>
      <tr>
          <td>decision_tree_path 填寫</td>
          <td>建議</td>
          <td>派發驗證必需</td>
      </tr>
  </tbody>
</table>
<h3 id="認領階段">認領階段</h3>
<table>
  <thead>
      <tr>
          <th>標準流程</th>
          <th>提示強度</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>阻塞依賴檢查</td>
          <td>警告</td>
          <td>如有阻塞依賴，顯示警告</td>
      </tr>
      <tr>
          <td>設計文件閱讀</td>
          <td>建議</td>
          <td>提醒閱讀相關規格和設計</td>
      </tr>
      <tr>
          <td>驗收條件理解</td>
          <td>建議</td>
          <td>確保理解驗收標準</td>
      </tr>
      <tr>
          <td>error-patterns 查詢</td>
          <td>建議</td>
          <td>IMP/ADJ 類型時建議查詢</td>
      </tr>
  </tbody>
</table>
<h3 id="執行階段">執行階段</h3>
<table>
  <thead>
      <tr>
          <th>標準流程</th>
          <th>提示強度</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題派發 incident-responder</td>
          <td>強制</td>
          <td>遇到錯誤時強制派發</td>
      </tr>
      <tr>
          <td>工作日誌更新</td>
          <td>建議</td>
          <td>執行過程記錄</td>
      </tr>
  </tbody>
</table>
<h3 id="完成階段">完成階段</h3>
<table>
  <thead>
      <tr>
          <th>標準流程</th>
          <th>提示強度</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗收條件勾選確認</td>
          <td>建議</td>
          <td>所有條件必須勾選</td>
      </tr>
      <tr>
          <td>建議處理確認</td>
          <td>建議</td>
          <td>無 pending 建議</td>
      </tr>
      <tr>
          <td>派發 acceptance-auditor</td>
          <td>強制</td>
          <td>IMP/ADJ 類型必須執行驗收</td>
      </tr>
      <tr>
          <td>任務鏈後續步驟建議</td>
          <td>提示</td>
          <td>分析並建議下一個 Ticket</td>
      </tr>
  </tbody>
</table>
<h3 id="驗收後階段">驗收後階段</h3>
<table>
  <thead>
      <tr>
          <th>標準流程</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>技術債務記錄</td>
          <td>將執行過程中發現的技術債務正式記錄，避免遺忘</td>
      </tr>
      <tr>
          <td>CHANGELOG 更新</td>
          <td>在版本發布時更新變更日誌，維護版本歷史的完整性</td>
      </tr>
      <tr>
          <td>學習經驗記錄</td>
          <td>萃取任務中的學習經驗，建構團隊知識網絡</td>
      </tr>
      <tr>
          <td>任務鏈進度更新</td>
          <td>追蹤整體任務鏈完成度，便於掌握專案整體進度</td>
      </tr>
  </tbody>
</table>
<h2 id="任務鏈後續步驟建議">任務鏈後續步驟建議</h2>
<p>當 Ticket 完成時，系統會自動分析任務鏈狀態並建議下一步。</p>
<h3 id="分析優先級">分析優先級</h3>
<table>
  <thead>
      <tr>
          <th>優先級</th>
          <th>情境</th>
          <th>建議內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>有子 Ticket 可開始</td>
          <td>「子 Ticket {id} 現在可以開始」</td>
      </tr>
      <tr>
          <td>2</td>
          <td>有被解除阻塞的 Ticket</td>
          <td>「{id} 的阻塞已解除」</td>
      </tr>
      <tr>
          <td>3</td>
          <td>有同層兄弟 Ticket</td>
          <td>「同層還有 {id} 待處理」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>同 Wave 有其他 pending</td>
          <td>「同 Wave 還有 N 個待處理」</td>
      </tr>
      <tr>
          <td>5</td>
          <td>任務鏈全部完成</td>
          <td>「任務鏈 {root} 全部完成」</td>
      </tr>
  </tbody>
</table>
<h3 id="輸出範例">輸出範例</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">============================================================
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">[任務鏈後續步驟建議]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">============================================================
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">已完成: 0.31.0-W4-007.1
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        [實作 track P0 功能]
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">任務鏈進度: 1/3 completed
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   Root: 0.31.0-W4-007
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">建議下一步:
</span></span><span class="line"><span class="ln">12</span><span class="cl">   1. 0.31.0-W4-007.2
</span></span><span class="line"><span class="ln">13</span><span class="cl">      [實作 track P1 功能]
</span></span><span class="line"><span class="ln">14</span><span class="cl">      原因: 阻塞已解除（blockedBy 0.31.0-W4-007.1 已完成）
</span></span><span class="line"><span class="ln">15</span><span class="cl">      狀態: pending → 可認領</span></span></code></pre></div><h2 id="任務鏈-id-格式">任務鏈 ID 格式</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">0.31.0-W3-002              # ticket-handoff 功能（根）
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── 0.31.0-W3-002.1        # chain_analyzer 模組
</span></span><span class="line"><span class="ln">3</span><span class="cl">│   ├── 0.31.0-W3-002.1.1  # 問題修復
</span></span><span class="line"><span class="ln">4</span><span class="cl">│   └── 0.31.0-W3-002.1.2  # 測試補充
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── 0.31.0-W3-002.2        # handoff_executor 模組
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── 0.31.0-W3-002.3        # 文件更新</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>類型</th>
          <th>格式</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>根任務</td>
          <td><code>{版本}-W{波次}-{序號}</code></td>
          <td><code>0.31.0-W3-002</code></td>
      </tr>
      <tr>
          <td>子任務</td>
          <td><code>{根ID}.{n}[.{n}...]</code></td>
          <td><code>0.31.0-W3-002.1.1</code></td>
      </tr>
  </tbody>
</table>
<h3 id="chain-欄位">chain 欄位</h3>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>類型</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>root</td>
          <td>string</td>
          <td>任務鏈根 ID</td>
      </tr>
      <tr>
          <td>parent</td>
          <td>string/null</td>
          <td>直接父任務 ID</td>
      </tr>
      <tr>
          <td>depth</td>
          <td>number</td>
          <td>深度（根=0）</td>
      </tr>
      <tr>
          <td>sequence</td>
          <td>array</td>
          <td>序號路徑陣列</td>
      </tr>
  </tbody>
</table>
<p>根任務 <code>0.31.0-W3-002</code> 的 chain：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">chain</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">root</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.31.0-W3-002&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">parent</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">sequence</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">2</span><span class="p">]</span></span></span></code></pre></div><p>子任務 <code>0.31.0-W3-002.1.1</code> 的 chain：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">chain</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">root</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.31.0-W3-002&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">parent</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.31.0-W3-002.1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="nt">depth</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">sequence</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="m">1</span><span class="p">]</span></span></span></code></pre></div><p>ID 正則表達式：<code>^(\d+\.\d+\.\d+)-W(\d+)-(\d+(?:\.\d+)*)$</code></p>
<h2 id="ticket-建立流程">Ticket 建立流程</h2>
<h3 id="任務層級判斷">任務層級判斷</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">任務層級判斷
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">這個任務是否因為執行現有 Ticket 而產生？
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    +-- 是 → 來源 Ticket 是什麼？
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    |       |
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    |       └── 確定來源 Ticket ID → 建立該 Ticket 的子任務
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    |           ├── 來源: 010.4 → 子任務 ID: 010.4.x
</span></span><span class="line"><span class="ln">10</span><span class="cl">    |           ├── 來源: 010.4.1 → 子任務 ID: 010.4.1.x
</span></span><span class="line"><span class="ln">11</span><span class="cl">    |           └── 來源: 010 → 子任務 ID: 010.x
</span></span><span class="line"><span class="ln">12</span><span class="cl">    |
</span></span><span class="line"><span class="ln">13</span><span class="cl">    └-- 否 → 建立新任務鏈（新的 Wx-00n）</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>應建立子任務</th>
          <th>應建立新任務鏈</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>問題在執行特定 Ticket 時發現</td>
          <td>問題與任何執行中的 Ticket 無關</td>
      </tr>
      <tr>
          <td>問題直接影響該 Ticket 的完成</td>
          <td>問題是系統性的獨立問題</td>
      </tr>
      <tr>
          <td>「執行 X 時發現 Y 問題」</td>
          <td>「發現系統有 Z 問題」</td>
      </tr>
  </tbody>
</table>
<p>核心判斷問題：「這個任務是在執行哪個 Ticket 時產生的？」若有明確來源，建立子任務；若無關聯，建立新任務鏈。</p>
<h3 id="建立格式">建立格式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl">---
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">id: {版本}-W{波次}-{序號}
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">title: {動詞} {目標}
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">type: IMP/RES/ANA/INV/DOC
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">status: pending
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">priority: P0/P1/P2
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">assignee: pending
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">created: {日期}
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">---
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="gh"># {Ticket ID}: {標題}
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="gu">## 目標
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">{目標描述}
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="gu">## 驗收條件
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">- [ ]</span> {條件1}
</span></span><span class="line"><span class="ln">20</span><span class="cl">- [ ] {條件2}</span></span></code></pre></div><h3 id="atomic-ticket-檢查">Atomic Ticket 檢查</h3>
<table>
  <thead>
      <tr>
          <th>檢查項目</th>
          <th>標準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>語義檢查</td>
          <td>能用「動詞 + 單一目標」表達</td>
      </tr>
      <tr>
          <td>修改原因</td>
          <td>只有一個修改原因</td>
      </tr>
      <tr>
          <td>驗收一致</td>
          <td>所有驗收條件指向同一目標</td>
      </tr>
      <tr>
          <td>依賴獨立</td>
          <td>無循環依賴</td>
      </tr>
  </tbody>
</table>
<h3 id="驗收條件格式要求">驗收條件格式要求</h3>
<p>驗收條件必須符合 4V 原則：<strong>可驗證、可量化、可追溯、可記錄</strong>。</p>
<table>
  <thead>
      <tr>
          <th>要求</th>
          <th>說明</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>必須有編號</td>
          <td>每個驗收項目都有編號</td>
          <td><code>1.</code>, <code>2.</code>, &hellip;</td>
      </tr>
      <tr>
          <td>必須有來源</td>
          <td>引用設計文件或需求</td>
          <td><code>SKILL.md L97</code></td>
      </tr>
      <tr>
          <td>必須有確認方法</td>
          <td>定義如何驗證完成</td>
          <td><code>執行命令驗證輸出</code></td>
      </tr>
      <tr>
          <td>禁止模糊詞彙</td>
          <td>不可用「完成」「正常」「適當」</td>
          <td>用具體描述取代</td>
      </tr>
  </tbody>
</table>
<p><strong>標準格式（表格式）</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## Acceptance Criteria
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">| #   | 項目       | 來源       | 確認方法   | 狀態 |
</span></span><span class="line"><span class="ln">4</span><span class="cl">| --- | ---------- | ---------- | ---------- | ---- |
</span></span><span class="line"><span class="ln">5</span><span class="cl">| 1   | {項目描述} | {來源引用} | {確認方法} | [ ]  |
</span></span><span class="line"><span class="ln">6</span><span class="cl">| 2   | {項目描述} | {來源引用} | {確認方法} | [ ]  |</span></span></code></pre></div><h2 id="ticket-有效性驗證">Ticket 有效性驗證</h2>
<h3 id="有效-ticket-定義">有效 Ticket 定義</h3>
<p>有效的 Ticket 必須滿足以下條件：</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>說明</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>決策樹欄位</td>
          <td>包含 <code>decision_tree_path</code> 欄位</td>
          <td>YAML frontmatter 檢查</td>
      </tr>
      <tr>
          <td>或決策樹區段</td>
          <td>包含「## 決策樹路徑」Markdown 區段</td>
          <td>內容檢查</td>
      </tr>
  </tbody>
</table>
<h3 id="驗證時機">驗證時機</h3>
<table>
  <thead>
      <tr>
          <th>時機</th>
          <th>驗證者</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建立 Ticket</td>
          <td>/ticket create</td>
          <td>自動要求填寫決策樹欄位</td>
      </tr>
      <tr>
          <td>派發任務</td>
          <td>agent-ticket-validation-hook</td>
          <td>阻止使用無效 Ticket</td>
      </tr>
      <tr>
          <td>認領 Ticket</td>
          <td>/ticket track claim</td>
          <td>確認 Ticket 有效性</td>
      </tr>
  </tbody>
</table>
<h3 id="無效-ticket-處理">無效 Ticket 處理</h3>
<p>無效 Ticket（缺少決策樹欄位）：</p>
<ul>
<li>無法用於 Task 派發（被 Hook 阻止）</li>
<li>需要補充決策樹欄位才能使用</li>
<li>建議使用 /ticket create 重新建立</li>
</ul>
<h3 id="補充決策樹欄位">補充決策樹欄位</h3>
<p>如果 Ticket 缺少決策樹欄位，可手動補充：</p>
<ol>
<li>
<p><strong>YAML 格式</strong>（在 frontmatter 中）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">decision_tree_path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">entry_point</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;第X層&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">decision_nodes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span>- <span class="nt">layer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;X&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">question</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;決策問題&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span><span class="nt">answer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;答案&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span><span class="nt">next_action</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;下一步&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">  </span><span class="nt">final_decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;最終決策&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">  </span><span class="nt">rationale</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;決策理由&#34;</span></span></span></code></pre></div></li>
<li>
<p><strong>Markdown 格式</strong>（在內容中）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 決策樹路徑
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gu">### 進入點
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">-</span> **層級**: 第X層
</span></span><span class="line"><span class="ln">6</span><span class="cl">- <span class="gs">**觸發條件**</span>: ...</span></span></code></pre></div></li>
</ol>
<h2 id="ticket-認領流程">Ticket 認領流程</h2>
<h3 id="認領規則">認領規則</h3>
<table>
  <thead>
      <tr>
          <th>規則</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一認領</td>
          <td>同一時間只能有一個代理人處理</td>
      </tr>
      <tr>
          <td>階段匹配</td>
          <td>只能認領對應階段的 Ticket</td>
      </tr>
      <tr>
          <td>依賴檢查</td>
          <td>前置 Ticket 必須完成</td>
      </tr>
  </tbody>
</table>
<h2 id="ticket-執行流程">Ticket 執行流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">認領 Ticket
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">執行對應階段工作
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">更新工作日誌
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    v
</span></span><span class="line"><span class="ln">10</span><span class="cl">驗證驗收條件
</span></span><span class="line"><span class="ln">11</span><span class="cl">    |
</span></span><span class="line"><span class="ln">12</span><span class="cl">    +-- 全部通過 --&gt; 完成 Ticket
</span></span><span class="line"><span class="ln">13</span><span class="cl">    +-- 部分通過 --&gt; 繼續處理或升級
</span></span><span class="line"><span class="ln">14</span><span class="cl">    +-- 無法完成 --&gt; 釋放 Ticket</span></span></code></pre></div><h3 id="完成檢查">完成檢查</h3>
<table>
  <thead>
      <tr>
          <th>檢查項目</th>
          <th>標準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗收條件</td>
          <td>所有條件都已勾選</td>
      </tr>
      <tr>
          <td>測試通過</td>
          <td>相關測試全部通過</td>
      </tr>
      <tr>
          <td>文件更新</td>
          <td>相關文件已更新</td>
      </tr>
      <tr>
          <td>工作日誌</td>
          <td>執行記錄完整</td>
      </tr>
  </tbody>
</table>
<h2 id="ticket-釋放流程">Ticket 釋放流程</h2>
<h3 id="釋放時機">釋放時機</h3>
<table>
  <thead>
      <tr>
          <th>時機</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>被阻塞</td>
          <td>依賴其他 Ticket 完成</td>
      </tr>
      <tr>
          <td>超出範圍</td>
          <td>發現需要額外工作</td>
      </tr>
      <tr>
          <td>技術限制</td>
          <td>當前無法解決</td>
      </tr>
      <tr>
          <td>資訊不足</td>
          <td>需要更多資訊</td>
      </tr>
  </tbody>
</table>
<h2 id="ticket-類型說明">Ticket 類型說明</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>代碼</th>
          <th>用途</th>
          <th>典型時長</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Research</td>
          <td>RES</td>
          <td>探索未知領域</td>
          <td>1-2 小時</td>
      </tr>
      <tr>
          <td>Analysis</td>
          <td>ANA</td>
          <td>理解現狀和問題</td>
          <td>30 分鐘 - 1 小時</td>
      </tr>
      <tr>
          <td>Implementation</td>
          <td>IMP</td>
          <td>執行具體任務</td>
          <td>1-4 小時</td>
      </tr>
      <tr>
          <td>Investigation</td>
          <td>INV</td>
          <td>深入追蹤問題根因</td>
          <td>1-2 小時</td>
      </tr>
      <tr>
          <td>Documentation</td>
          <td>DOC</td>
          <td>記錄和傳承經驗</td>
          <td>30 分鐘 - 1 小時</td>
      </tr>
  </tbody>
</table>
<h2 id="版本歷史">版本歷史</h2>
<ul>
<li>v2.8.0 (2026-02-01): 取消驗收豁免機制，改為契約式驗收</li>
<li>v2.7.0 (2026-02-01): 強化驗收代理人派發要求</li>
<li>v2.6.0 (2026-01-31): 新增任務層級判斷規則</li>
<li>v2.5.0 (2026-01-30): 新增階段-標準流程對照表和任務鏈後續步驟建議</li>
<li>v2.4.0 (2026-01-30): 新增建議追蹤流程整合章節</li>
<li>v2.3.0 (2026-01-30): 新增驗收條件格式要求章節</li>
<li>v2.2.0 (2026-01-29): 新增任務鏈 ID 格式章節</li>
<li>v2.1.0 (2026-01-27): 新增 Ticket 有效性驗證章節</li>
<li>v2.0.0 (2026-01-23): 重構為 TDD 含 SA 前置審查流程版本</li>
</ul>
]]></content:encoded></item></channel></rss>