<?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>Retrospective on Tarragon</title><link>https://tarrragon.github.io/blog/tags/retrospective/</link><description>Recent content in Retrospective on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 25 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/retrospective/index.xml" rel="self" type="application/rss+xml"/><item><title>工具的預設行為決定使用者習慣 — 從版本錯置看工具設計的 opinion 責任</title><link>https://tarrragon.github.io/blog/work-log/%E5%B7%A5%E5%85%B7%E7%9A%84%E9%A0%90%E8%A8%AD%E8%A1%8C%E7%82%BA%E6%B1%BA%E5%AE%9A%E4%BD%BF%E7%94%A8%E8%80%85%E7%BF%92%E6%85%A3-%E5%BE%9E%E7%89%88%E6%9C%AC%E9%8C%AF%E7%BD%AE%E7%9C%8B%E5%B7%A5%E5%85%B7%E8%A8%AD%E8%A8%88%E7%9A%84-opinion-%E8%B2%AC%E4%BB%BB/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E5%B7%A5%E5%85%B7%E7%9A%84%E9%A0%90%E8%A8%AD%E8%A1%8C%E7%82%BA%E6%B1%BA%E5%AE%9A%E4%BD%BF%E7%94%A8%E8%80%85%E7%BF%92%E6%85%A3-%E5%BE%9E%E7%89%88%E6%9C%AC%E9%8C%AF%E7%BD%AE%E7%9C%8B%E5%B7%A5%E5%85%B7%E8%A8%AD%E8%A8%88%E7%9A%84-opinion-%E8%B2%AC%E4%BB%BB/</guid><description>&lt;p>這篇從一個版本錯置的經驗出發，討論工具設計中一個容易忽略的面向：工具接受自由輸入時，預設路徑如何影響使用者的決策。適用於 CLI、API、表單、自動化流程——任何需要使用者做選擇的介面。&lt;/p>
&lt;hr>
&lt;h2 id="背景我們怎麼管理版本和工作項目">背景：我們怎麼管理版本和工作項目&lt;/h2>
&lt;p>我們的專案用 semver（語意化版本）管理發布節奏。每個版本（如 v0.3.0）有明確的功能範圍，由數個提案定義——每個提案描述一組要交付的功能和邊界。版本內部再拆成多個工作項目（ticket），按批次排序執行（類似 Sprint，但以依賴順序而非時間框分批）。&lt;/p>
&lt;p>版本的生命週期很單純：&lt;code>planned → active → completed&lt;/code>。一個版本的所有 ticket 完成後，跑發布流程、打 tag、標記 completed。&lt;/p>
&lt;p>圍繞這個流程，我們自建了兩個 CLI 工具：&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;code>ticket create&lt;/code>&lt;/td>
 &lt;td>建立工作項目，指定歸屬版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>version-release&lt;/code>&lt;/td>
 &lt;td>版本發布（pre-flight 檢查、文件更新、打 tag）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這兩個工具在設計時，都選擇了「彈性優先」——接受任何合法輸入，不對使用者的選擇做判斷。&lt;/p>
&lt;p>這個選擇在後來被證明是錯的。&lt;/p>
&lt;h2 id="版本語意大版本和小版本的分工">版本語意：大版本和小版本的分工&lt;/h2>
&lt;p>semver 的 &lt;code>MAJOR.MINOR.PATCH&lt;/code> 有明確的語意分工：&lt;/p>
&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>MAJOR（0.x → 1.0）&lt;/td>
 &lt;td>不相容的 API 變更&lt;/td>
 &lt;td>破壞既有介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MINOR（0.3 → 0.4）&lt;/td>
 &lt;td>新功能&lt;/td>
 &lt;td>新增向後相容功能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PATCH（0.3.0 → 0.3.1）&lt;/td>
 &lt;td>修復和改善&lt;/td>
 &lt;td>bug fix（我們擴充涵蓋重構和流程改善）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>版本號不只是標記——它決定了&lt;strong>工作項目應該放在哪裡&lt;/strong>。一個 bug fix 放進 MINOR 版本，語意上等於說「這個 bug fix 和下一批新功能綁定發布」——多數情況下這不是你想要的。&lt;/p>
&lt;p>版本管理只是其中一個場景——任何接受自由輸入的內部工具，只要輸入涉及分類或歸屬判斷，都可能有同樣的問題。我們的工具沒有表達這個語意，接下來的兩個事件是後果。&lt;/p>
&lt;h2 id="事件一改善類工作放進了新功能版本">事件一：改善類工作放進了新功能版本&lt;/h2>
&lt;p>v0.3.0 發布了三個新功能。發布後的版本檢討發現了一個測試隔離問題，v0.3.1 做了 hotfix。&lt;/p>
&lt;p>接下來要做根因分析和系統性防護。建立工作項目時，順手指定了 &lt;code>--version 0.4.0&lt;/code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本，看起來是合理的選擇。&lt;/p>
&lt;p>CLI 接受了這個輸入，沒有任何提示。&lt;/p>
&lt;p>三張改善類的工作項目（根因分析、重構、規則文件）就這樣和 PostgreSQL Storage Backend（v0.4.0 的核心功能）混在一起。直到使用者檢視版本看板時才發現不對——改善類工作和新功能綁在同一個發布週期，語意混亂。&lt;/p>
&lt;p>修正方式：建立 v0.3.2、遷移三張 ticket、重新發布。額外花了一輪操作成本。&lt;/p>
&lt;h2 id="事件二已完成版本的幽靈">事件二：已完成版本的幽靈&lt;/h2>
&lt;p>版本看板的異常不止一處。同一次檢視中，看板顯示 v0.2.0 有未完成任務。&lt;/p>
&lt;p>查證後發現 v0.2.0（38 張 ticket 全部完成）、v0.2.1（7 張全完成）、v0.2.2（1 張已結案）三個版本在版本清單中仍標記為 &lt;code>active&lt;/code>。它們在數個月前就該標為 &lt;code>completed&lt;/code>，但沒有。&lt;/p>
&lt;p>原因是版本發布工具的 pre-flight 檢查只看「當前版本的 ticket 是否完成」，不掃描「更早的版本是否有 active 殘留」。早期版本可能是手動發布的，跳過了狀態同步步驟。工具沒有補救機制，殘留就一直留著。&lt;/p>
&lt;p>看板靜默地把這些版本顯示為「有未完成工作」，產生誤導。&lt;/p>
&lt;h2 id="為什麼會這樣工具沒有-opinion">為什麼會這樣：工具沒有 opinion&lt;/h2>
&lt;p>兩個事件的共通根因：&lt;strong>工具在應該有立場的地方選擇了沉默。&lt;/strong>&lt;/p>
&lt;h3 id="建立工作項目時">建立工作項目時&lt;/h3>
&lt;p>&lt;code>ticket create --version 0.4.0 --type ANA --action &amp;quot;分析&amp;quot;&lt;/code> — 工具知道這是一張分析類的 ticket，也知道 v0.4.0 的 scope 是 PostgreSQL Storage。但它不認為自己有責任判斷「分析類 ticket 放在新功能版本是否合理」。它只做格式驗證：版本號存在嗎？通過就建立。&lt;/p>
&lt;h3 id="發布版本時">發布版本時&lt;/h3>
&lt;p>發布工具的盲區更隱蔽。每次發布時，工具會檢查「這個版本的所有工作項目都完成了嗎？」——如果答案是「是」，就繼續打 tag、更新文件、推送。但它從不回頭看更早的版本：有沒有哪個舊版本的工作項目早已全部完成，卻一直沒被標記為「已完成」？這種殘留不影響當前發布，但會讓看板持續顯示「舊版本有未完成工作」，誤導每一個後續查看看板的人。&lt;/p>
&lt;p>兩者都是「工具做了它被要求做的事，但沒做它應該做的事」。&lt;/p>
&lt;h2 id="工具什麼時候應該有-opinion">工具什麼時候應該有 opinion？&lt;/h2>
&lt;p>不是所有情境都需要工具有立場。有一個簡單的判斷標準：&lt;/p>
&lt;blockquote>
&lt;p>當存在一個「多數情況下正確的預設行為」時，工具應該把它表達出來。使用者可以覆蓋，但預設路徑應該引導正確做法。&lt;/p>&lt;/blockquote>
&lt;p>這裡的 opinion 是&lt;strong>建議而非阻擋&lt;/strong>——工具提示預設路徑，使用者可以覆蓋。這個區分很重要：阻擋式的 opinion（必須額外操作才能繞過）適合風險高的操作（如 force push to main、刪除生產資料）；建議式的 opinion 適合歸屬判斷。錯誤成本不對稱決定了形式：建議錯了，使用者覆蓋一次，幾秒鐘；沉默錯了，事後修正，幾小時。只要建議的正確率不是極低，建議就比沉默划算。&lt;/p>
&lt;p>這個邏輯不限於 CLI。API 的預設參數、表單的預選值、自動化流程的預設路由——任何使用者需要做選擇的介面，都有機會用預設行為表達 opinion。&lt;/p>
&lt;p>改善類 ticket 放 patch 版本，在多數情況下是正確的。「多數情況下對」已經足夠讓工具表達立場：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="err">$&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ticket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">create&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">--type IMP --action &amp;#34;修復&amp;#34; --target &amp;#34;retry test&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">建議&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">此&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ticket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">為修復類，建議放&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">（&lt;/span>&lt;span class="n">patch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bump&lt;/span>&lt;span class="err">）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="err">而非&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="err">（下一個功能版本）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="err">使用&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">--version 覆蓋此建議&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>前版本 status 掃描也是。已完成版本仍為 active 在所有情況下都是異常——工具不需要猜，只需要報告：&lt;/p>





&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">$ version-release check
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="為什麼使用者是-ai-agent-時問題更嚴重">為什麼使用者是 AI agent 時問題更嚴重&lt;/h2>
&lt;p>這個 pattern 在人類使用者身上已經存在——人類也會走阻力最小的路徑。但人類有跨次記憶：「上次放錯版本被糾正過，這次注意一下。」&lt;/p></description><content:encoded><![CDATA[<p>這篇從一個版本錯置的經驗出發，討論工具設計中一個容易忽略的面向：工具接受自由輸入時，預設路徑如何影響使用者的決策。適用於 CLI、API、表單、自動化流程——任何需要使用者做選擇的介面。</p>
<hr>
<h2 id="背景我們怎麼管理版本和工作項目">背景：我們怎麼管理版本和工作項目</h2>
<p>我們的專案用 semver（語意化版本）管理發布節奏。每個版本（如 v0.3.0）有明確的功能範圍，由數個提案定義——每個提案描述一組要交付的功能和邊界。版本內部再拆成多個工作項目（ticket），按批次排序執行（類似 Sprint，但以依賴順序而非時間框分批）。</p>
<p>版本的生命週期很單純：<code>planned → active → completed</code>。一個版本的所有 ticket 完成後，跑發布流程、打 tag、標記 completed。</p>
<p>圍繞這個流程，我們自建了兩個 CLI 工具：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ticket create</code></td>
          <td>建立工作項目，指定歸屬版本</td>
      </tr>
      <tr>
          <td><code>version-release</code></td>
          <td>版本發布（pre-flight 檢查、文件更新、打 tag）</td>
      </tr>
  </tbody>
</table>
<p>這兩個工具在設計時，都選擇了「彈性優先」——接受任何合法輸入，不對使用者的選擇做判斷。</p>
<p>這個選擇在後來被證明是錯的。</p>
<h2 id="版本語意大版本和小版本的分工">版本語意：大版本和小版本的分工</h2>
<p>semver 的 <code>MAJOR.MINOR.PATCH</code> 有明確的語意分工：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>語意</th>
          <th>觸發條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MAJOR（0.x → 1.0）</td>
          <td>不相容的 API 變更</td>
          <td>破壞既有介面</td>
      </tr>
      <tr>
          <td>MINOR（0.3 → 0.4）</td>
          <td>新功能</td>
          <td>新增向後相容功能</td>
      </tr>
      <tr>
          <td>PATCH（0.3.0 → 0.3.1）</td>
          <td>修復和改善</td>
          <td>bug fix（我們擴充涵蓋重構和流程改善）</td>
      </tr>
  </tbody>
</table>
<p>版本號不只是標記——它決定了<strong>工作項目應該放在哪裡</strong>。一個 bug fix 放進 MINOR 版本，語意上等於說「這個 bug fix 和下一批新功能綁定發布」——多數情況下這不是你想要的。</p>
<p>版本管理只是其中一個場景——任何接受自由輸入的內部工具，只要輸入涉及分類或歸屬判斷，都可能有同樣的問題。我們的工具沒有表達這個語意，接下來的兩個事件是後果。</p>
<h2 id="事件一改善類工作放進了新功能版本">事件一：改善類工作放進了新功能版本</h2>
<p>v0.3.0 發布了三個新功能。發布後的版本檢討發現了一個測試隔離問題，v0.3.1 做了 hotfix。</p>
<p>接下來要做根因分析和系統性防護。建立工作項目時，順手指定了 <code>--version 0.4.0</code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本，看起來是合理的選擇。</p>
<p>CLI 接受了這個輸入，沒有任何提示。</p>
<p>三張改善類的工作項目（根因分析、重構、規則文件）就這樣和 PostgreSQL Storage Backend（v0.4.0 的核心功能）混在一起。直到使用者檢視版本看板時才發現不對——改善類工作和新功能綁在同一個發布週期，語意混亂。</p>
<p>修正方式：建立 v0.3.2、遷移三張 ticket、重新發布。額外花了一輪操作成本。</p>
<h2 id="事件二已完成版本的幽靈">事件二：已完成版本的幽靈</h2>
<p>版本看板的異常不止一處。同一次檢視中，看板顯示 v0.2.0 有未完成任務。</p>
<p>查證後發現 v0.2.0（38 張 ticket 全部完成）、v0.2.1（7 張全完成）、v0.2.2（1 張已結案）三個版本在版本清單中仍標記為 <code>active</code>。它們在數個月前就該標為 <code>completed</code>，但沒有。</p>
<p>原因是版本發布工具的 pre-flight 檢查只看「當前版本的 ticket 是否完成」，不掃描「更早的版本是否有 active 殘留」。早期版本可能是手動發布的，跳過了狀態同步步驟。工具沒有補救機制，殘留就一直留著。</p>
<p>看板靜默地把這些版本顯示為「有未完成工作」，產生誤導。</p>
<h2 id="為什麼會這樣工具沒有-opinion">為什麼會這樣：工具沒有 opinion</h2>
<p>兩個事件的共通根因：<strong>工具在應該有立場的地方選擇了沉默。</strong></p>
<h3 id="建立工作項目時">建立工作項目時</h3>
<p><code>ticket create --version 0.4.0 --type ANA --action &quot;分析&quot;</code> — 工具知道這是一張分析類的 ticket，也知道 v0.4.0 的 scope 是 PostgreSQL Storage。但它不認為自己有責任判斷「分析類 ticket 放在新功能版本是否合理」。它只做格式驗證：版本號存在嗎？通過就建立。</p>
<h3 id="發布版本時">發布版本時</h3>
<p>發布工具的盲區更隱蔽。每次發布時，工具會檢查「這個版本的所有工作項目都完成了嗎？」——如果答案是「是」，就繼續打 tag、更新文件、推送。但它從不回頭看更早的版本：有沒有哪個舊版本的工作項目早已全部完成，卻一直沒被標記為「已完成」？這種殘留不影響當前發布，但會讓看板持續顯示「舊版本有未完成工作」，誤導每一個後續查看看板的人。</p>
<p>兩者都是「工具做了它被要求做的事，但沒做它應該做的事」。</p>
<h2 id="工具什麼時候應該有-opinion">工具什麼時候應該有 opinion？</h2>
<p>不是所有情境都需要工具有立場。有一個簡單的判斷標準：</p>
<blockquote>
<p>當存在一個「多數情況下正確的預設行為」時，工具應該把它表達出來。使用者可以覆蓋，但預設路徑應該引導正確做法。</p></blockquote>
<p>這裡的 opinion 是<strong>建議而非阻擋</strong>——工具提示預設路徑，使用者可以覆蓋。這個區分很重要：阻擋式的 opinion（必須額外操作才能繞過）適合風險高的操作（如 force push to main、刪除生產資料）；建議式的 opinion 適合歸屬判斷。錯誤成本不對稱決定了形式：建議錯了，使用者覆蓋一次，幾秒鐘；沉默錯了，事後修正，幾小時。只要建議的正確率不是極低，建議就比沉默划算。</p>
<p>這個邏輯不限於 CLI。API 的預設參數、表單的預選值、自動化流程的預設路由——任何使用者需要做選擇的介面，都有機會用預設行為表達 opinion。</p>
<p>改善類 ticket 放 patch 版本，在多數情況下是正確的。「多數情況下對」已經足夠讓工具表達立場：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">$</span><span class="w"> </span><span class="n">ticket</span><span class="w"> </span><span class="k">create</span><span class="w"> </span><span class="c1">--type IMP --action &#34;修復&#34; --target &#34;retry test&#34;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="p">[</span><span class="err">建議</span><span class="p">]</span><span class="w"> </span><span class="err">此</span><span class="w"> </span><span class="n">ticket</span><span class="w"> </span><span class="err">為修復類，建議放</span><span class="w"> </span><span class="n">v0</span><span class="p">.</span><span class="mi">3</span><span class="p">.</span><span class="mi">2</span><span class="err">（</span><span class="n">patch</span><span class="w"> </span><span class="n">bump</span><span class="err">）</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">       </span><span class="err">而非</span><span class="w"> </span><span class="n">v0</span><span class="p">.</span><span class="mi">4</span><span class="p">.</span><span class="mi">0</span><span class="err">（下一個功能版本）</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">       </span><span class="err">使用</span><span class="w"> </span><span class="c1">--version 覆蓋此建議</span></span></span></code></pre></div><p>前版本 status 掃描也是。已完成版本仍為 active 在所有情況下都是異常——工具不需要猜，只需要報告：</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">$ version-release check
</span></span><span class="line"><span class="ln">2</span><span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active</span></span></code></pre></div><h2 id="為什麼使用者是-ai-agent-時問題更嚴重">為什麼使用者是 AI agent 時問題更嚴重</h2>
<p>這個 pattern 在人類使用者身上已經存在——人類也會走阻力最小的路徑。但人類有跨次記憶：「上次放錯版本被糾正過，這次注意一下。」</p>
<p>AI agent 沒有這個。</p>
<p>每個 session 是一個全新的 agent，它讀到的是：版本清單中 v0.4.0 是 active、CLI 接受 <code>--version 0.4.0</code>、沒有警告。於是它每次都會用最直覺的選擇——當前 active 的最大版本。</p>
<p>上次的教訓不會自動傳遞到下次。除非教訓被固化成工具行為。</p>
<p>這把「工具應該有 opinion」從「建議做法」升級為「必要條件」：</p>
<ul>
<li><strong>人類使用者</strong>：opinion 是提醒，有助於減少錯誤</li>
<li><strong>AI agent 使用者</strong>：opinion 是最可靠的防線，因為工具在操作當下的即時引導是離決策點最近的攔截</li>
</ul>
<h2 id="工具的預設行為就是團隊的實際流程">工具的預設行為，就是團隊的實際流程</h2>
<blockquote>
<p>工具的預設行為，就是團隊的實際流程。</p></blockquote>
<p>文件上寫「改善類工作放 patch 版本」沒有用——如果工具不引導，使用者會走工具預設的路徑。人類和 AI 都是。文件說的和工具做的不一致時，工具會贏。</p>
<p>但文件不是敵人。文件定義「應該是什麼樣」，傳遞設計理由和架構決策；工具實現「實際是什麼樣」。兩者不一致時，優先修工具。</p>
<blockquote>
<p>如果你希望使用者做 X，不要寫文件說「請做 X」——把工具的預設行為設成 X。</p></blockquote>
<p>這個原則適用於所有內部工具設計，不限於版本管理：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>寫文件的做法</th>
          <th>改工具的做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>commit 前跑測試</td>
          <td>README 寫「請先跑測試」</td>
          <td>pre-commit hook 自動跑</td>
      </tr>
      <tr>
          <td>PR 描述格式</td>
          <td>貢獻指南寫範本</td>
          <td>PR template 預填結構</td>
      </tr>
      <tr>
          <td>改善放 patch 版本</td>
          <td>版本策略文件寫規則</td>
          <td>CLI 根據 ticket type 建議版本</td>
      </tr>
      <tr>
          <td>API 環境參數</td>
          <td>文件寫「production 需額外確認」</td>
          <td>API 預設 staging，production 需顯式指定</td>
      </tr>
      <tr>
          <td>表單必填欄位</td>
          <td>說明文字寫「建議填寫」</td>
          <td>欄位預設值 + 必填驗證</td>
      </tr>
  </tbody>
</table>
<p>每一個「寫文件提醒使用者遵守操作規範」都是一個信號——工具的預設行為還有空間改善。看到這個信號時，優先評估能否把提醒轉化為工具的預設行為。</p>
<p>Rails 的「Convention over Configuration」是同一個觀念的先驅表達：框架用約定引導開發者走正確路徑，省去不必要的配置決策。有 opinion 的工具在必要決策時引導方向。兩者共通的是把判斷成本從「每次使用時」前移到「設計工具時」——一次判斷，永久生效。</p>
<h2 id="回去檢查你的工具">回去檢查你的工具</h2>
<ol>
<li>列出你的工具中所有使用者需要做選擇的地方——CLI 參數、API 欄位、表單選項、流程分支</li>
<li>對每個問：有沒有「多數情況下正確」的預設值或建議值？</li>
<li>有的話，加建議式 opinion（提示預設 + 允許覆蓋）</li>
<li>檢查工具的清理路徑：有沒有前一次操作應該同步但沒有同步的狀態？</li>
<li>如果你的工具會被 AI agent 或自動化流程呼叫，上述每一項的優先級加倍——自動化沒有判斷力，它只走預設路徑</li>
</ol>
]]></content:encoded></item><item><title>改善類工作放進新功能版本 — 版本歸屬判斷的工具化</title><link>https://tarrragon.github.io/blog/work-log/%E6%94%B9%E5%96%84%E9%A1%9E%E5%B7%A5%E4%BD%9C%E6%94%BE%E9%80%B2%E6%96%B0%E5%8A%9F%E8%83%BD%E7%89%88%E6%9C%AC-%E7%89%88%E6%9C%AC%E6%AD%B8%E5%B1%AC%E5%88%A4%E6%96%B7%E7%9A%84%E5%B7%A5%E5%85%B7%E5%8C%96/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%94%B9%E5%96%84%E9%A1%9E%E5%B7%A5%E4%BD%9C%E6%94%BE%E9%80%B2%E6%96%B0%E5%8A%9F%E8%83%BD%E7%89%88%E6%9C%AC-%E7%89%88%E6%9C%AC%E6%AD%B8%E5%B1%AC%E5%88%A4%E6%96%B7%E7%9A%84%E5%B7%A5%E5%85%B7%E5%8C%96/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>v0.3.0 發布後發現一個測試隔離問題，v0.3.1 做了 hotfix。接著要做根因分析和系統性防護（重構 + 品質規則更新）。&lt;/p>
&lt;p>建立工作項目時指定了 &lt;code>--version 0.4.0&lt;/code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本。工具接受了，沒有提示。&lt;/p>
&lt;p>結果：三張改善類工作（根因分析、State Registry 重構、品質規則文件）和 PostgreSQL Storage Backend 混在同一個版本裡。改善和新功能綁定發布，語意混亂。事後建立 v0.3.2 遷移工作項目並重新發布。&lt;/p>
&lt;h2 id="根因工具只做格式驗證">根因：工具只做格式驗證&lt;/h2>
&lt;p>&lt;code>ticket create --version 0.4.0&lt;/code> 被接受的條件是「v0.4.0 存在於版本清單且為 active」。工具不分析工作類型（分析 / 修復 / 重構 / 新功能）和版本層級（MINOR / PATCH）的匹配度。&lt;/p>
&lt;p>semver 有明確的語意分工——MINOR 用於新功能，PATCH 用於修復和改善。這個語意可以被工具表達：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工作類型&lt;/th>
 &lt;th>semver 語意&lt;/th>
 &lt;th>建議版本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>新功能&lt;/td>
 &lt;td>MINOR bump&lt;/td>
 &lt;td>下一個功能版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>PATCH bump&lt;/td>
 &lt;td>當前系列的下一個 patch&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>改善 / 重構&lt;/td>
 &lt;td>PATCH bump&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文件&lt;/td>
 &lt;td>PATCH bump&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>工具可以根據工作類型自動建議版本，使用者可以覆蓋。建議錯了，使用者多打一個參數；沉默錯了，事後遷移。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>&lt;strong>語意已經存在，工具有責任表達它。&lt;/strong> semver 的 MINOR/PATCH 分工是廣泛認知的慣例。但「知道」和「每次建立工作項目時都記得套用」是兩件事。工具可以把這個「記得套用」的成本降到零：讀取工作類型，對照 semver 語意，輸出建議。&lt;/p>
&lt;p>這個 pattern 適用於任何「輸入涉及分類判斷」的工具介面。工具不需要代替使用者做決策，但可以把分類規則從「腦中的知識」轉化為「介面上的提示」。同一次版本檢視中發現的另一個工具盲區（狀態殘留）見 &lt;a href="https://tarrragon.github.io/blog/work-log/%E7%89%88%E6%9C%AC%E7%8B%80%E6%85%8B%E6%AE%98%E7%95%99%E7%82%BA%E4%BB%80%E9%BA%BC%E5%B7%B2%E5%AE%8C%E6%88%90%E7%9A%84%E7%89%88%E6%9C%AC%E5%9C%A8%E7%9C%8B%E6%9D%BF%E4%B8%8A%E9%A1%AF%E7%A4%BA%E6%9C%AA%E5%AE%8C%E6%88%90/" data-link-title="版本狀態殘留：為什麼已完成的版本在看板上顯示未完成" data-link-desc="看板顯示早已完成的版本仍為 active、誤導查看者。根因是發布工具只檢查當前版本完成度、不掃前版本的狀態殘留；工具的檢查範圍決定了系統的一致性邊界。">version_status_residual_ghost&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>v0.3.0 發布後發現一個測試隔離問題，v0.3.1 做了 hotfix。接著要做根因分析和系統性防護（重構 + 品質規則更新）。</p>
<p>建立工作項目時指定了 <code>--version 0.4.0</code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本。工具接受了，沒有提示。</p>
<p>結果：三張改善類工作（根因分析、State Registry 重構、品質規則文件）和 PostgreSQL Storage Backend 混在同一個版本裡。改善和新功能綁定發布，語意混亂。事後建立 v0.3.2 遷移工作項目並重新發布。</p>
<h2 id="根因工具只做格式驗證">根因：工具只做格式驗證</h2>
<p><code>ticket create --version 0.4.0</code> 被接受的條件是「v0.4.0 存在於版本清單且為 active」。工具不分析工作類型（分析 / 修復 / 重構 / 新功能）和版本層級（MINOR / PATCH）的匹配度。</p>
<p>semver 有明確的語意分工——MINOR 用於新功能，PATCH 用於修復和改善。這個語意可以被工具表達：</p>
<table>
  <thead>
      <tr>
          <th>工作類型</th>
          <th>semver 語意</th>
          <th>建議版本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新功能</td>
          <td>MINOR bump</td>
          <td>下一個功能版本</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>PATCH bump</td>
          <td>當前系列的下一個 patch</td>
      </tr>
      <tr>
          <td>改善 / 重構</td>
          <td>PATCH bump</td>
          <td>同上</td>
      </tr>
      <tr>
          <td>文件</td>
          <td>PATCH bump</td>
          <td>同上</td>
      </tr>
  </tbody>
</table>
<p>工具可以根據工作類型自動建議版本，使用者可以覆蓋。建議錯了，使用者多打一個參數；沉默錯了，事後遷移。</p>
<h2 id="教訓">教訓</h2>
<p><strong>語意已經存在，工具有責任表達它。</strong> semver 的 MINOR/PATCH 分工是廣泛認知的慣例。但「知道」和「每次建立工作項目時都記得套用」是兩件事。工具可以把這個「記得套用」的成本降到零：讀取工作類型，對照 semver 語意，輸出建議。</p>
<p>這個 pattern 適用於任何「輸入涉及分類判斷」的工具介面。工具不需要代替使用者做決策，但可以把分類規則從「腦中的知識」轉化為「介面上的提示」。同一次版本檢視中發現的另一個工具盲區（狀態殘留）見 <a href="/blog/work-log/%E7%89%88%E6%9C%AC%E7%8B%80%E6%85%8B%E6%AE%98%E7%95%99%E7%82%BA%E4%BB%80%E9%BA%BC%E5%B7%B2%E5%AE%8C%E6%88%90%E7%9A%84%E7%89%88%E6%9C%AC%E5%9C%A8%E7%9C%8B%E6%9D%BF%E4%B8%8A%E9%A1%AF%E7%A4%BA%E6%9C%AA%E5%AE%8C%E6%88%90/" data-link-title="版本狀態殘留：為什麼已完成的版本在看板上顯示未完成" data-link-desc="看板顯示早已完成的版本仍為 active、誤導查看者。根因是發布工具只檢查當前版本完成度、不掃前版本的狀態殘留；工具的檢查範圍決定了系統的一致性邊界。">version_status_residual_ghost</a>。</p>
]]></content:encoded></item><item><title>並行 AI Agent 修改同一檔案的衝突模式與協調策略</title><link>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。&lt;/p>
&lt;p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 &lt;code>monitor.ts&lt;/code>。&lt;/p>
&lt;p>結果：&lt;/p>
&lt;ul>
&lt;li>三個 agent 回報 branch protection hook 阻擋 src 編輯&lt;/li>
&lt;li>兩個 agent 回報 &lt;code>file modified since read&lt;/code> 拒絕 Edit（另一個 agent 正在寫同一檔案）&lt;/li>
&lt;li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」&lt;/li>
&lt;li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit&lt;/li>
&lt;/ul>
&lt;p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。&lt;/p>
&lt;h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層&lt;/h2>
&lt;p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 &lt;code>monitor.ts&lt;/code> 這一個檔案上。&lt;/p>
&lt;p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>&lt;strong>派發前掃描 &lt;code>where.files&lt;/code>&lt;/strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。&lt;/p>
&lt;p>&lt;strong>序列的代價比衝突的代價低&lt;/strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。&lt;/p>
&lt;p>&lt;strong>Worktree 隔離不是萬靈丹&lt;/strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。</p>
<p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 <code>monitor.ts</code>。</p>
<p>結果：</p>
<ul>
<li>三個 agent 回報 branch protection hook 阻擋 src 編輯</li>
<li>兩個 agent 回報 <code>file modified since read</code> 拒絕 Edit（另一個 agent 正在寫同一檔案）</li>
<li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」</li>
<li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit</li>
</ul>
<p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。</p>
<h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層</h2>
<p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 <code>monitor.ts</code> 這一個檔案上。</p>
<p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。</p>
<h2 id="教訓">教訓</h2>
<p><strong>派發前掃描 <code>where.files</code></strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。</p>
<p><strong>序列的代價比衝突的代價低</strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。</p>
<p><strong>Worktree 隔離不是萬靈丹</strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。</p>
<h2 id="適用場景">適用場景</h2>
<p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。</p>
]]></content:encoded></item><item><title>版本狀態殘留：為什麼已完成的版本在看板上顯示未完成</title><link>https://tarrragon.github.io/blog/work-log/%E7%89%88%E6%9C%AC%E7%8B%80%E6%85%8B%E6%AE%98%E7%95%99%E7%82%BA%E4%BB%80%E9%BA%BC%E5%B7%B2%E5%AE%8C%E6%88%90%E7%9A%84%E7%89%88%E6%9C%AC%E5%9C%A8%E7%9C%8B%E6%9D%BF%E4%B8%8A%E9%A1%AF%E7%A4%BA%E6%9C%AA%E5%AE%8C%E6%88%90/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E7%89%88%E6%9C%AC%E7%8B%80%E6%85%8B%E6%AE%98%E7%95%99%E7%82%BA%E4%BB%80%E9%BA%BC%E5%B7%B2%E5%AE%8C%E6%88%90%E7%9A%84%E7%89%88%E6%9C%AC%E5%9C%A8%E7%9C%8B%E6%9D%BF%E4%B8%8A%E9%A1%AF%E7%A4%BA%E6%9C%AA%E5%AE%8C%E6%88%90/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>版本看板顯示 v0.2.0 有未完成任務。查證後發現 v0.2.0 的 38 張工作項目全部完成、v0.2.1 的 7 張全部完成、v0.2.2 的 1 張已結案——但三個版本在版本清單中仍標記為 &lt;code>active&lt;/code>。&lt;/p>
&lt;p>這些版本在數個月前就完成了所有工作，但從未被標記為 &lt;code>completed&lt;/code>。看板忠實地反映了版本清單的狀態，所以持續顯示「有未完成工作」。&lt;/p>
&lt;h2 id="根因工具的檢查範圍太窄">根因：工具的檢查範圍太窄&lt;/h2>
&lt;p>版本發布工具在發布 v0.3.0 時，只做一件事：「v0.3.0 的所有 ticket 都完成了嗎？」答案是「是」，就繼續發布。&lt;/p>
&lt;p>它從不問：「比 v0.3.0 更早的版本中，有沒有哪個版本的 ticket 早已全部完成，但 status 仍為 active？」&lt;/p>
&lt;p>這個檢查加起來不難（遍歷版本清單、對每個 active 版本計算 ticket 完成率、完成率 100% 但 status 不是 completed 就報 warning）。但沒有人想到要加——因為在設計工具時，焦點在「當前版本的發布流程」，不在「全局狀態一致性」。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>資料庫設計中，如果只在寫入時驗證單筆資料的格式而不檢查跨表一致性，orphan record 就會累積。版本管理工具的 pre-flight check 是同一個 pattern——它是內部流程的「外鍵約束」。範圍太窄，殘留就會累積。&lt;/p>
&lt;p>工具只檢查當前版本，一致性就只在當前版本內維持。歷史版本的狀態漂移不會被發現——直到有人手動查看看板。&lt;/p>
&lt;h2 id="修正">修正&lt;/h2>
&lt;p>在版本發布的 pre-flight check 加入全局掃描：&lt;/p>





&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">$ version-release check
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[OK] v0.3.0：所有 ticket 完成，可發布
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">[WARN] v0.2.1：7 張 ticket 全部完成但 status 仍為 active&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>修正成本極低（一個迴圈 + 一個 warning），但能在問題累積前暴露。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>版本看板顯示 v0.2.0 有未完成任務。查證後發現 v0.2.0 的 38 張工作項目全部完成、v0.2.1 的 7 張全部完成、v0.2.2 的 1 張已結案——但三個版本在版本清單中仍標記為 <code>active</code>。</p>
<p>這些版本在數個月前就完成了所有工作，但從未被標記為 <code>completed</code>。看板忠實地反映了版本清單的狀態，所以持續顯示「有未完成工作」。</p>
<h2 id="根因工具的檢查範圍太窄">根因：工具的檢查範圍太窄</h2>
<p>版本發布工具在發布 v0.3.0 時，只做一件事：「v0.3.0 的所有 ticket 都完成了嗎？」答案是「是」，就繼續發布。</p>
<p>它從不問：「比 v0.3.0 更早的版本中，有沒有哪個版本的 ticket 早已全部完成，但 status 仍為 active？」</p>
<p>這個檢查加起來不難（遍歷版本清單、對每個 active 版本計算 ticket 完成率、完成率 100% 但 status 不是 completed 就報 warning）。但沒有人想到要加——因為在設計工具時，焦點在「當前版本的發布流程」，不在「全局狀態一致性」。</p>
<h2 id="教訓">教訓</h2>
<p>資料庫設計中，如果只在寫入時驗證單筆資料的格式而不檢查跨表一致性，orphan record 就會累積。版本管理工具的 pre-flight check 是同一個 pattern——它是內部流程的「外鍵約束」。範圍太窄，殘留就會累積。</p>
<p>工具只檢查當前版本，一致性就只在當前版本內維持。歷史版本的狀態漂移不會被發現——直到有人手動查看看板。</p>
<h2 id="修正">修正</h2>
<p>在版本發布的 pre-flight check 加入全局掃描：</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">$ version-release check
</span></span><span class="line"><span class="ln">2</span><span class="cl">[OK] v0.3.0：所有 ticket 完成，可發布
</span></span><span class="line"><span class="ln">3</span><span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active
</span></span><span class="line"><span class="ln">4</span><span class="cl">[WARN] v0.2.1：7 張 ticket 全部完成但 status 仍為 active</span></span></code></pre></div><p>修正成本極低（一個迴圈 + 一個 warning），但能在問題累積前暴露。</p>
]]></content:encoded></item><item><title>新增欄位忘記同步 reset — 跨測試狀態洩漏的系統性根因</title><link>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E6%96%B0%E5%A2%9E%E6%AC%84%E4%BD%8D%E5%BF%98%E8%A8%98%E5%90%8C%E6%AD%A5-reset-%E8%B7%A8%E6%B8%AC%E8%A9%A6%E7%8B%80%E6%85%8B%E6%B4%A9%E6%BC%8F%E7%9A%84%E7%B3%BB%E7%B5%B1%E6%80%A7%E6%A0%B9%E5%9B%A0/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：&lt;code>flushing&lt;/code>（flush 併發 guard）、&lt;code>retryCount&lt;/code>（重試計數）、&lt;code>lastHeartbeat&lt;/code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 &lt;code>__reset()&lt;/code> 方法。&lt;/p>
&lt;p>測試框架在每個 test case 之間呼叫 &lt;code>__reset()&lt;/code> 清理狀態。因為 &lt;code>retryCount&lt;/code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。&lt;/p>
&lt;p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。&lt;/p>
&lt;h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化&lt;/h2>
&lt;p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。&lt;/p>
&lt;p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。&lt;/p>
&lt;p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 &lt;a href="https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict&lt;/a>。&lt;/p>
&lt;h2 id="防護state-registry-pattern">防護：State Registry Pattern&lt;/h2>
&lt;p>將所有 private 欄位的初始值集中宣告一次：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">initialState() {&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">config&lt;/span>: &lt;span class="kt">null&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">buffer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">flushing&lt;/span>: &lt;span class="kt">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">retryCount&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">lastHeartbeat&lt;/span>: &lt;span class="kt">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>reset 改用 &lt;code>Object.assign(this, initialState())&lt;/code>。新增欄位只改一處，init 和 reset 自動同步。&lt;/p>
&lt;p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>JS SDK 的 Monitor class 在一輪並行開發中，三個開發者各自新增了 private 欄位：<code>flushing</code>（flush 併發 guard）、<code>retryCount</code>（重試計數）、<code>lastHeartbeat</code>（心跳時間戳）。三個欄位各自在功能邏輯中被正確使用，但都沒有加進 <code>__reset()</code> 方法。</p>
<p>測試框架在每個 test case 之間呼叫 <code>__reset()</code> 清理狀態。因為 <code>retryCount</code> 沒被重置，第一個 test case 把 retryCount 遞增到 1，第二個 test case 繼承了這個值，retry 邏輯提前觸發，測試失敗。</p>
<p>失敗的測試看起來像是 retry 邏輯有 bug，但實際上 retry 邏輯完全正確——問題出在測試隔離。</p>
<h2 id="根因隱含契約沒有顯性化">根因：隱含契約沒有顯性化</h2>
<p>Class 的每個 private 欄位都有一個隱含契約：「所有生命週期路徑都知道你的存在。」這包括初始化（constructor / init）、重置（reset / dispose）、序列化（toJSON，如適用）。</p>
<p>新增欄位時，開發者通常會先在功能邏輯中使用這個欄位——因為那是他加欄位的目的。但「同步到 reset」不是功能邏輯的一部分，它是一個跨切面的維護動作。遺漏的機率隨欄位數和開發者數增加而上升。</p>
<p>多人（或多 AI agent）並行開發時問題更嚴重——每個人只看自己加的欄位，沒有人有動機去檢查 reset 的完整性。並行修改同一檔案的協調問題見 <a href="/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/" data-link-title="並行 AI Agent 修改同一檔案的衝突模式與協調策略" data-link-desc="並行派多個開發者或 AI agent 同一批 ticket，反覆修改同一個檔案、卡在 branch protection 與 file-modified-since-read。問題在派發策略沒考慮檔案層級的衝突。">parallel_agent_same_file_conflict</a>。</p>
<h2 id="防護state-registry-pattern">防護：State Registry Pattern</h2>
<p>將所有 private 欄位的初始值集中宣告一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">initialState() {</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">config</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">buffer</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">flushing</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">retryCount</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">lastHeartbeat</span>: <span class="kt">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// 新增欄位加在這裡——init 和 reset 自動包含
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="p">};</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>reset 改用 <code>Object.assign(this, initialState())</code>。新增欄位只改一處，init 和 reset 自動同步。</p>
<p>配合一個 reset 完整性測試：reset 後 snapshot 比對 initialState 的所有 key——新增欄位但忘記加到 initialState 會因型別或 key 不一致而紅燈。</p>
<h2 id="適用場景">適用場景</h2>
<p>任何有「重置到初始狀態」需求的 class：測試框架的 setUp/tearDown、物件池的回收、singleton 的 reinit。問題在「新增欄位」和「同步 reset」是兩個分開的動作（TypeScript、Go、Dart 都會遇到）——只要是分開的，就有遺漏的可能。State Registry 把兩者合併成一個動作。</p>
]]></content:encoded></item><item><title>驗證導向的 CLI 工具文章：官方 docs 查核放過的落差類型</title><link>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</guid><description>&lt;p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 &lt;code>.claude/skills/verification-driven-cli/&lt;/code>。&lt;/p>
&lt;h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差&lt;/h2>
&lt;p>&lt;code>content/cli/&lt;/code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：&lt;/p>
&lt;h3 id="1-旗標改名">1. 旗標改名&lt;/h3>
&lt;p>&lt;code>zellij web&lt;/code> 文件寫有 &lt;code>--bind&lt;/code>，實際 0.43.1 是分開的 &lt;code>--ip&lt;/code> 與 &lt;code>--port&lt;/code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。&lt;/p>
&lt;h3 id="2-設定鍵-migrate">2. 設定鍵 migrate&lt;/h3>
&lt;p>&lt;code>lazygit&lt;/code> 的 pager 設定文件寫 &lt;code>git.paging.pager&lt;/code>，新版 0.62.2 改成 &lt;code>git.pagers&lt;/code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。&lt;/p>
&lt;h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix&lt;/h3>
&lt;p>&lt;code>dblab&lt;/code> 的查詢編輯器要 schema 限定（&lt;code>SELECT * FROM public.products&lt;/code>），裸 &lt;code>products&lt;/code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。&lt;/p>
&lt;h3 id="4-平台特定-segfault">4. 平台特定 segfault&lt;/h3>
&lt;p>&lt;code>nvtop&lt;/code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。&lt;/p>
&lt;h3 id="5-driver-差異">5. Driver 差異&lt;/h3>
&lt;p>同一個 Postgres，&lt;code>lazysql&lt;/code>（Go pq driver）連無 SSL 的 DB 要 &lt;code>?sslmode=disable&lt;/code>，&lt;code>pgcli&lt;/code> / &lt;code>harlequin&lt;/code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。&lt;/p>
&lt;h2 id="共通模式">共通模式&lt;/h2>
&lt;p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。&lt;/p>
&lt;p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。&lt;/p>
&lt;h2 id="相關連結">相關連結&lt;/h2>
&lt;ul>
&lt;li>Verification-driven CLI skill（&lt;code>.claude/skills/verification-driven-cli/&lt;/code>）— 操作步驟&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> — 用此流程生產的工具文章&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能&lt;/a> — 本文精簡的依據&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 <code>.claude/skills/verification-driven-cli/</code>。</p>
<h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差</h2>
<p><code>content/cli/</code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：</p>
<h3 id="1-旗標改名">1. 旗標改名</h3>
<p><code>zellij web</code> 文件寫有 <code>--bind</code>，實際 0.43.1 是分開的 <code>--ip</code> 與 <code>--port</code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。</p>
<h3 id="2-設定鍵-migrate">2. 設定鍵 migrate</h3>
<p><code>lazygit</code> 的 pager 設定文件寫 <code>git.paging.pager</code>，新版 0.62.2 改成 <code>git.pagers</code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。</p>
<h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix</h3>
<p><code>dblab</code> 的查詢編輯器要 schema 限定（<code>SELECT * FROM public.products</code>），裸 <code>products</code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。</p>
<h3 id="4-平台特定-segfault">4. 平台特定 segfault</h3>
<p><code>nvtop</code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。</p>
<h3 id="5-driver-差異">5. Driver 差異</h3>
<p>同一個 Postgres，<code>lazysql</code>（Go pq driver）連無 SSL 的 DB 要 <code>?sslmode=disable</code>，<code>pgcli</code> / <code>harlequin</code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。</p>
<h2 id="共通模式">共通模式</h2>
<p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。</p>
<p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Verification-driven CLI skill（<code>.claude/skills/verification-driven-cli/</code>）— 操作步驟</li>
<li><a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> — 用此流程生產的工具文章</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item><item><title>Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%</title><link>https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/</guid><description>&lt;p>本文記錄 migration-playbook-methodology 這套寫作方法論前三輪 batch dogfood（實際寫文章驗證方法論）的演化過程（skill 已累積到六輪、本文記錄前三輪）。操作步驟維護在 &lt;code>.claude/skills/migration-playbook-methodology/&lt;/code>，本文只保留 retrospective — 每一輪跑出來學到什麼、哪些假設被推翻。&lt;/p>
&lt;h2 id="為什麼-migration-playbook-需要自己的方法論">為什麼 migration playbook 需要自己的方法論&lt;/h2>
&lt;p>Migration playbook 跟 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">single feature deep article&lt;/a> 是不同 content category：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Deep article&lt;/th>
 &lt;th>Migration playbook&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>主題形狀&lt;/td>
 &lt;td>Single feature（pgBouncer / Vault dynamic credential）&lt;/td>
 &lt;td>Cross-vendor process（Splunk → Elastic）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構&lt;/td>
 &lt;td>6-section（problem → concept → config → failure → capacity → integration）&lt;/td>
 &lt;td>6 種不同 type、各對應不同結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重點章節&lt;/td>
 &lt;td>Step-by-step 配置 + 故障演練&lt;/td>
 &lt;td>視 type 不同：phased flow / parallel streams / hybrid&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫作週期 / 篇&lt;/td>
 &lt;td>1-2 小時&lt;/td>
 &lt;td>2-3 小時（diff dimension audit + 結構選擇 + 寫作）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨篇 cadence 風險&lt;/td>
 &lt;td>中（章節 1 entry 容易 collapse）&lt;/td>
 &lt;td>高（migration 主題本質相似、主題語意 attractor「為什麼遷」明顯）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵差異：deep article 是 single direction implementation、migration playbook 是 bidirectional comparison + process。第一輪寫了 5 篇後發現結構完全不同；嘗試套 deep article 的固定結構都只對 1 種情境適用，於是用 diff dimension audit（寫前評估 source/target 在哪些維度差異最大）選對應的結構模板（Type A-F，依主導差異維度決定）。&lt;/p>
&lt;h2 id="第一輪-batch5-篇type-a-e-浮現--cadence-collapse-35">第一輪 batch（5 篇）：Type A-E 浮現 + cadence collapse 3/5&lt;/h2>
&lt;p>第一輪寫了 5 篇跨 vendor migration playbook，每篇自然對映到一種 type（結構模板）：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security&lt;/a> — Type A phased translation&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> — Type B drop-in&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> — Type C operational hybrid&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack&lt;/a> — Type D parallel streams&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> — Type E paradigm shift&lt;/li>
&lt;/ul>
&lt;h3 id="cadence-collapse前-3-篇被動寫作全部同質化">Cadence collapse：前 3 篇被動寫作全部同質化&lt;/h3>
&lt;p>Cadence collapse 指批量寫作時、多篇文章的開場句型不自覺重複同一模式。&lt;/p></description><content:encoded><![CDATA[<p>本文記錄 migration-playbook-methodology 這套寫作方法論前三輪 batch dogfood（實際寫文章驗證方法論）的演化過程（skill 已累積到六輪、本文記錄前三輪）。操作步驟維護在 <code>.claude/skills/migration-playbook-methodology/</code>，本文只保留 retrospective — 每一輪跑出來學到什麼、哪些假設被推翻。</p>
<h2 id="為什麼-migration-playbook-需要自己的方法論">為什麼 migration playbook 需要自己的方法論</h2>
<p>Migration playbook 跟 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">single feature deep article</a> 是不同 content category：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主題形狀</td>
          <td>Single feature（pgBouncer / Vault dynamic credential）</td>
          <td>Cross-vendor process（Splunk → Elastic）</td>
      </tr>
      <tr>
          <td>結構</td>
          <td>6-section（problem → concept → config → failure → capacity → integration）</td>
          <td>6 種不同 type、各對應不同結構</td>
      </tr>
      <tr>
          <td>重點章節</td>
          <td>Step-by-step 配置 + 故障演練</td>
          <td>視 type 不同：phased flow / parallel streams / hybrid</td>
      </tr>
      <tr>
          <td>寫作週期 / 篇</td>
          <td>1-2 小時</td>
          <td>2-3 小時（diff dimension audit + 結構選擇 + 寫作）</td>
      </tr>
      <tr>
          <td>跨篇 cadence 風險</td>
          <td>中（章節 1 entry 容易 collapse）</td>
          <td>高（migration 主題本質相似、主題語意 attractor「為什麼遷」明顯）</td>
      </tr>
  </tbody>
</table>
<p>關鍵差異：deep article 是 single direction implementation、migration playbook 是 bidirectional comparison + process。第一輪寫了 5 篇後發現結構完全不同；嘗試套 deep article 的固定結構都只對 1 種情境適用，於是用 diff dimension audit（寫前評估 source/target 在哪些維度差異最大）選對應的結構模板（Type A-F，依主導差異維度決定）。</p>
<h2 id="第一輪-batch5-篇type-a-e-浮現--cadence-collapse-35">第一輪 batch（5 篇）：Type A-E 浮現 + cadence collapse 3/5</h2>
<p>第一輪寫了 5 篇跨 vendor migration playbook，每篇自然對映到一種 type（結構模板）：</p>
<ul>
<li><a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> — Type A phased translation</li>
<li><a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> — Type B drop-in</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> — Type C operational hybrid</li>
<li><a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a> — Type D parallel streams</li>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> — Type E paradigm shift</li>
</ul>
<h3 id="cadence-collapse前-3-篇被動寫作全部同質化">Cadence collapse：前 3 篇被動寫作全部同質化</h3>
<p>Cadence collapse 指批量寫作時、多篇文章的開場句型不自覺重複同一模式。</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant 規劃</th>
          <th>章節 1 entry framing</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 Splunk → Elastic</td>
          <td>被動</td>
          <td>「為什麼遷：cost / multi-vendor / cloud-native」</td>
      </tr>
      <tr>
          <td>2 Redis → DragonflyDB</td>
          <td>被動</td>
          <td>「為什麼遷：cost / single-thread / multi-tenancy」</td>
      </tr>
      <tr>
          <td>3 Postgres → Aurora</td>
          <td>被動</td>
          <td>「為什麼遷：operational cost / HA / DR」</td>
      </tr>
      <tr>
          <td>4 Datadog → Grafana</td>
          <td>主動</td>
          <td>「$50K/month bill 拆解」</td>
      </tr>
      <tr>
          <td>5 Kafka ↔ NATS</td>
          <td>主動</td>
          <td>「『Kafka → NATS migration』字面上不成立」</td>
      </tr>
  </tbody>
</table>
<p>3/5 collapse — 主題語意 attractor「為什麼遷：X / Y / Z driver」在前 3 篇被動寫作下浮現。寫第 4 篇前發現問題、後 2 篇主動換 entry variant。</p>
<p>前 3 篇的 collapse 是 Stage 0 variant 規劃成為硬需求的直接證據。</p>
<h3 id="type-a-e-怎麼浮現">Type A-E 怎麼浮現</h3>
<p>5 篇寫完後比對結構、發現 5 篇結構完全不同，但都可以用「主導差異維度」解釋：schema 差為主 → phased translation、全 Low → drop-in、operational 差為主 → hybrid。Type A-E 從這 5 篇的歸納中浮現，第二輪 dogfood 再加上 Type F（topology re-layout）。</p>
<h2 id="第二輪-batch5-篇漏類驗證--多軸-high-實證">第二輪 batch（5 篇）：漏類驗證 + 多軸 High 實證</h2>
<p>第二輪刻意選漏類場景驗證 self-aware limitation：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major version upgrade (14 → 17)</a> — 漏類驗證（同 vendor）</li>
<li><a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis cluster re-sharding</a> — 漏類驗證（topology 重劃）→ Type F 浮現</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB</a> — 三維 High multi-axis 驗證</li>
<li><a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a> — Type A 標準形態（263 行）</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a> — Type C 標準形態（349 行）</li>
</ul>
<p>Stage 0 variant 規劃從第二輪開始全面啟用，cadence collapse 從 3/5 降到 0/5。</p>
<h3 id="驗證成立的-4-項預測">驗證成立的 4 項預測</h3>
<ol>
<li><strong>5 type 漏類確認</strong>：major version upgrade + re-sharding 結構跟 5 type 完全不同</li>
<li><strong>多重歸類 + tie-breaking 規則成立</strong>：PostgreSQL → CockroachDB 三維皆 High、按主導維度走 Type E + 高維度獨立段</li>
<li><strong>Type A / Type C 標準形態仍適用</strong>：MySQL → PostgreSQL + MongoDB → Atlas 走標準模板</li>
<li><strong>Stage 0 variant 規劃硬需求</strong>：第二輪 5 篇全主動 variant、collapse 0/5</li>
</ol>
<h3 id="浮現的-3-項新議題">浮現的 3 項新議題</h3>
<ol>
<li><strong>新 audit 維度（data topology）</strong>：re-sharding 揭露 5 維度沒「topology」軸 → 擴到 6 維</li>
<li><strong>「為什麼這篇不套」是漏類文章標準 frame</strong>：major-version-upgrade + cluster-resharding 都用這個 frame 開頭</li>
<li><strong>「高維度獨立段」升級為 multi-axis migration 標準結構元素</strong></li>
</ol>
<h2 id="第三輪-batch5-篇type-f-dogfood--候選軸驗證">第三輪 batch（5 篇）：Type F dogfood + 候選軸驗證</h2>
<p>第三輪驗證 data topology audit dimension 的 self-aware limitation 4 條 tripwire：</p>
<ul>
<li><a href="/blog/backend/01-database/vendors/postgresql/partition-redesign/" data-link-title="PostgreSQL Partition Redesign：當 monthly partition 越跑越慢" data-link-desc="PostgreSQL partition redesign 是 Type F「topology re-layout」第 2 個 dogfood — 從 monthly partition 改 daily / 從 range 改 list / 從單軸改 sub-partition；6 維 audit 皆 Low &#43; topology 軸 High；涵蓋 partition 不平衡偵測、ATTACH/DETACH 線上重劃、5 個 production 踩雷、跟 partition_pruning &#43; autovacuum 整合">PostgreSQL partition redesign</a>（246 行）— Type F dogfood #2</li>
<li><a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">MongoDB shard + multi-DC expansion</a>（291 行）— Type F dogfood #3 + parallel run 例外實證</li>
<li><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/migrate-to-aws-secrets-manager/" data-link-title="Vault → AWS Secrets Manager：「secret」不是「secret」、identity model 才是核心差異" data-link-desc="Vault → AWS Secrets Manager migration 表面是 secret store 替換、實際核心是 identity model 對位（Vault token &#43; policy vs AWS IAM &#43; resource policy）；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 identity axis 候選 — identity 是否獨立 audit 軸；5 個 production 踩雷（IAM principal 對位 / dynamic credential 對等失敗 / lease lifecycle 模型不同 / audit log 結構差 / 計費模型反轉）">Vault → AWS Secrets Manager</a>（272 行）— Identity axis 候選（45% 工作量）</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">DynamoDB consistency model optimization</a>（249 行）— Consistency axis 候選（85% 工作量）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/multi-region-gdpr-rollout/" data-link-title="PostgreSQL Multi-Region GDPR Rollout：政策驅動的 migration 屬本 methodology 嗎" data-link-desc="PostgreSQL 單 region → multi-region 同時滿足 GDPR EU residency 是 *政策驅動* 兼 *topology 變動* 兼 *operational redesign* 的多軸 migration；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 residency axis 候選 — residency 是 driver 還是獨立 audit 軸；涵蓋 logical replication 配 GDPR / 5 個 production 踩雷 / cross-region cost">PostgreSQL multi-region GDPR rollout</a>（238 行）— Residency axis 候選（40% 工作量）</li>
</ul>
<p>第三輪維持 collapse 0/5，但 Type F 分裂出 sub-type（F-cluster vs F-multi-region），框架仍在演化。</p>
<h3 id="累積-evidence">累積 evidence</h3>
<ul>
<li><strong>Type F sub-type 浮現</strong>：F-cluster（單 cluster 內、不需 parallel run）vs F-multi-region（跨 region、需 parallel run）</li>
<li><strong>3 軸候選確認可獨立</strong>：identity / consistency / residency 各帶 30-85% 獨立工作量；累積到 3-5 case / 軸後考慮升 audit 7-9 維</li>
<li><strong>Residency 是 cross-cutting constraint</strong>：不只是 driver、反向約束 topology + operational + application</li>
</ul>
<h2 id="三輪對照方法論的演化軌跡">三輪對照：方法論的演化軌跡</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>第一輪（5 篇）</th>
          <th>第二輪（5 篇）</th>
          <th>第三輪（5 篇）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type 集合</td>
          <td>A-E（5 type）</td>
          <td>A-F（+Type F）</td>
          <td>A-F + sub-type</td>
      </tr>
      <tr>
          <td>Audit 維度</td>
          <td>5 維</td>
          <td>6 維（+topology）</td>
          <td>6 維 + 3 候選軸</td>
      </tr>
      <tr>
          <td>Cadence collapse</td>
          <td>3/5 (60%)</td>
          <td>0/5 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>Variant 規劃</td>
          <td>被動 → 主動</td>
          <td>全主動</td>
          <td>全主動</td>
      </tr>
      <tr>
          <td>總行數</td>
          <td>~1,200</td>
          <td>1,389</td>
          <td>1,292</td>
      </tr>
      <tr>
          <td>單篇行數</td>
          <td>200-300</td>
          <td>263-349</td>
          <td>238-288</td>
      </tr>
  </tbody>
</table>
<p>關鍵轉折是第一輪到第二輪：後續批次未再觀察到 collapse。</p>
<h2 id="self-aware-limitation">Self-aware limitation</h2>
<p>本 methodology 從 15 篇 migration playbook dogfood 抽出 6 type；已知 limitation：</p>
<ul>
<li><strong>6 type 非窮盡</strong>：major version upgrade / merger consolidation 等情境不在 6 type 內</li>
<li><strong>多重歸類常見</strong>：實際 source/target 配對很少完美對映單一 type</li>
<li><strong>「主導維度」需 judgment</strong>：優先序是 audience-dependent heuristic、不是 universal 規則</li>
<li><strong>Collapse 歸因有共變因素</strong>：第二輪以後 collapse 消失，但同時作者已有第一輪經驗、且知道自己在測量 cadence（Hawthorne effect）。Stage 0 variant 規劃是介入手段之一，無法完全隔離歸因。N=5 的二項信賴區間也無法排除偶然</li>
<li><strong>候選軸未 commit</strong>：identity / consistency / residency 各 N=1、累積到 3-5 case / 軸後才考慮升維</li>
</ul>
<p>本 methodology 接受 evolution、不假裝穩定。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Migration Playbook Methodology skill（<code>.claude/skills/migration-playbook-methodology/</code>）— 操作步驟（6 維 audit、6 type、Stage 0 variant、4-reviewer）</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor deep article methodology</a> — sibling、處理 single feature implementation</li>
<li><a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-first Agent Team Review Workflow</a> — 教學模組級批次寫作流程</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item><item><title>Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證</title><link>https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/</guid><description>&lt;p>Vendor overview 寫完後、往下寫單一功能深度文章時，選題與結構需要不同的方法論。操作步驟維護在 &lt;code>.claude/skills/vendor-deep-article/&lt;/code>，本文記錄這套方法論從兩輪 batch 中演化出來的過程，重點是 cadence collapse（批量寫作時開場句型同質化重複）怎麼被寫前的 variant 規劃（每篇預先指定不同開場 framing）解決。&lt;/p>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>本 blog 的 backend 教學模組已完成多個 vendor overview。overview 層飽和後、自然的下一步是 overview 頁尾「預計實作話題」backlog 的深度文章。&lt;/p>
&lt;p>寫了 deep article + migration playbook 後、確認 deep article 跟 overview 是不同產品、需要自己的方法論。差異見 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 方法論演化紀錄&lt;/a>。&lt;/p>
&lt;h2 id="第一輪-batch5-篇跨-vendor5-種-entry-framing">第一輪 batch（5 篇）：跨 vendor、5 種 entry framing&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>篇&lt;/th>
 &lt;th>Variant&lt;/th>
 &lt;th>章節 1 entry framing&lt;/th>
 &lt;th>行數&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &amp;#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置&lt;/a>&lt;/td>
 &lt;td>A 標準&lt;/td>
 &lt;td>標準「問題情境」&lt;/td>
 &lt;td>263&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault dynamic credential&lt;/a>&lt;/td>
 &lt;td>A 標準&lt;/td>
 &lt;td>標準「問題情境」&lt;/td>
 &lt;td>222&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/" data-link-title="Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣" data-link-desc="K8s pod termination 五步序列、preStop / SIGTERM / terminationGracePeriodSeconds 的真實時序、5 個 production 踩雷（500 期間 502、connection drain race、init container 重啟、StatefulSet 串行終止、Job 不 graceful）、跟 service mesh / readiness probe 整合">K8s graceful shutdown&lt;/a>&lt;/td>
 &lt;td>B 痛點&lt;/td>
 &lt;td>痛點宣告「沒做對、每次 deploy 都吃 502」&lt;/td>
 &lt;td>213&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &amp;#43; case management 整合">Splunk RBA&lt;/a>&lt;/td>
 &lt;td>C 反向&lt;/td>
 &lt;td>概念反向定義「alert fatigue 是 detection 天花板」&lt;/td>
 &lt;td>193&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/" data-link-title="Cloudflare Page Shield：用 CSP &amp;#43; SRI &amp;#43; script monitoring 防 client-side supply chain" data-link-desc="Page Shield 三層防禦（CSP / SRI / script monitoring）對應 Magecart / formjacking / skimmer / 第三方 SDK 注入的不同 attack pattern、Cloudflare dashboard &amp;#43; API 配置、四個 production 踩雷（inline script 漏 / dynamic loader / CSP report 噪音 / SRI hash mismatch）、跟 dev workflow &amp;#43; WAF 整合">Cloudflare Page Shield&lt;/a>&lt;/td>
 &lt;td>D 對照表&lt;/td>
 &lt;td>對照表驅動「Attack pattern x Defense mechanism」&lt;/td>
 &lt;td>214&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一輪確認了結構 framework 成立、且章節名可隨主題調整。&lt;/p></description><content:encoded><![CDATA[<p>Vendor overview 寫完後、往下寫單一功能深度文章時，選題與結構需要不同的方法論。操作步驟維護在 <code>.claude/skills/vendor-deep-article/</code>，本文記錄這套方法論從兩輪 batch 中演化出來的過程，重點是 cadence collapse（批量寫作時開場句型同質化重複）怎麼被寫前的 variant 規劃（每篇預先指定不同開場 framing）解決。</p>
<h2 id="背景">背景</h2>
<p>本 blog 的 backend 教學模組已完成多個 vendor overview。overview 層飽和後、自然的下一步是 overview 頁尾「預計實作話題」backlog 的深度文章。</p>
<p>寫了 deep article + migration playbook 後、確認 deep article 跟 overview 是不同產品、需要自己的方法論。差異見 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook 方法論演化紀錄</a>。</p>
<h2 id="第一輪-batch5-篇跨-vendor5-種-entry-framing">第一輪 batch（5 篇）：跨 vendor、5 種 entry framing</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant</th>
          <th>章節 1 entry framing</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/pgbouncer-config/" data-link-title="PostgreSQL pgBouncer 配置 &#43; 連線池治理" data-link-desc="pgBouncer transaction pooling 配置、跟 application connection pool 的分層、production 故障演練（pool exhaustion / stale connection / DNS failover）跟容量規劃">pgBouncer 配置</a></td>
          <td>A 標準</td>
          <td>標準「問題情境」</td>
          <td>263</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Vault dynamic credential</a></td>
          <td>A 標準</td>
          <td>標準「問題情境」</td>
          <td>222</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/vendors/kubernetes/graceful-shutdown/" data-link-title="Kubernetes Graceful Shutdown：termination 序列跟你以為的不一樣" data-link-desc="K8s pod termination 五步序列、preStop / SIGTERM / terminationGracePeriodSeconds 的真實時序、5 個 production 踩雷（500 期間 502、connection drain race、init container 重啟、StatefulSet 串行終止、Job 不 graceful）、跟 service mesh / readiness probe 整合">K8s graceful shutdown</a></td>
          <td>B 痛點</td>
          <td>痛點宣告「沒做對、每次 deploy 都吃 502」</td>
          <td>213</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/splunk/risk-based-alerting/" data-link-title="Splunk Risk-Based Alerting：從 alert per rule 到 score-aggregated notable" data-link-desc="Splunk Enterprise Security 的 RBA 方法論：risk score / modifier / notable 三層 model、ES 配置 step-by-step、tuning playbook（false positive / score inflation / threshold drift / decay）、capacity 成本、跟 SOAR &#43; case management 整合">Splunk RBA</a></td>
          <td>C 反向</td>
          <td>概念反向定義「alert fatigue 是 detection 天花板」</td>
          <td>193</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/vendors/cloudflare-waf/page-shield-csp-sri/" data-link-title="Cloudflare Page Shield：用 CSP &#43; SRI &#43; script monitoring 防 client-side supply chain" data-link-desc="Page Shield 三層防禦（CSP / SRI / script monitoring）對應 Magecart / formjacking / skimmer / 第三方 SDK 注入的不同 attack pattern、Cloudflare dashboard &#43; API 配置、四個 production 踩雷（inline script 漏 / dynamic loader / CSP report 噪音 / SRI hash mismatch）、跟 dev workflow &#43; WAF 整合">Cloudflare Page Shield</a></td>
          <td>D 對照表</td>
          <td>對照表驅動「Attack pattern x Defense mechanism」</td>
          <td>214</td>
      </tr>
  </tbody>
</table>
<p>第一輪確認了結構 framework 成立、且章節名可隨主題調整。</p>
<h3 id="6-段-framework-成立但章節名可變">6 段 framework 成立但章節名可變</h3>
<p>6 段內容指引（問題情境 → 概念 → 配置 → 演練 → 容量 → 整合）在 5 篇都成立。但章節 1 的 framing 因主題本質不同自然分化 — 5 種 entry framing 都成立、章節 1 不必死守「問題情境」標題。</p>
<p>據此小修方法論：6 段 framework 是內容指引、不是章節標題模板。</p>
<h3 id="cadence-collapse-0--主動-variant-有效">Cadence collapse 0% — 主動 variant 有效</h3>
<p>後 4 篇寫作前主動規劃 4 種 framing variant。跟 backend/07 的 51 vendor batch 對照：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>backend/07 51 vendor</th>
          <th>deep article 後 4 篇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cadence「任一缺失」族重複</td>
          <td>51/51 (100%)</td>
          <td>0/4 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>1 種</td>
          <td>4 種</td>
      </tr>
  </tbody>
</table>
<h3 id="reviewer-單人足夠">Reviewer 單人足夠</h3>
<p>deep article 焦點窄（單一 feature）、跨章 frame 重複風險低、case 引用密度低（1-2 個對照）。5 篇都採單一 reviewer 流程、未出現需要 multi-axis review 的盲點。</p>
<h2 id="第二輪-batch5-篇同-vendor-sub-tool-系列最高-collapse-風險">第二輪 batch（5 篇）：同 vendor sub-tool 系列、最高 collapse 風險</h2>
<p>第二輪刻意選 cadence collapse 最高風險場景：5 篇 PostgreSQL sub-tool deep article、同 vendor / 同 article type / 同 audience / 同 6-section framework。</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant</th>
          <th>章節 1 entry framing</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/patroni-ha/" data-link-title="PostgreSQL Patroni HA：從 leader 失聯到 client 重連的 5 段 failover lifecycle" data-link-desc="Patroni 把 PostgreSQL HA 拆成 detection / election / promotion / reconfiguration / recovery 五段 lifecycle、每段都有獨立配置跟 failure mode；DCS quorum &#43; watchdog 防 split-brain、async/sync replication 取捨、5 個 production 踩雷、跟 PgBouncer / HAProxy / cert-manager 整合">Patroni HA</a></td>
          <td>E lifecycle-driven</td>
          <td>「Failover lifecycle 5 段不是一條曲線」</td>
          <td>243</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/autovacuum-tuning/" data-link-title="PostgreSQL autovacuum tuning：為什麼你的 autovacuum 永遠追不上 bloat" data-link-desc="MVCC 怎麼產生 dead tuple、autovacuum cost-based throttle 為什麼預設保守、per-table tuning 怎麼設、5 個 production 踩雷（cost_limit 太低 / 長 transaction blocks vacuum / anti-wraparound 在 peak / partition vacuum 滿 worker / index bloat 沒處理）、跟 partitioning &#43; monitoring 整合">autovacuum tuning</a></td>
          <td>B pain-driven</td>
          <td>「你的 autovacuum 永遠追不上 bloat — 為什麼」</td>
          <td>202</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/declarative-partitioning/" data-link-title="PostgreSQL declarative partitioning：partition 不是切表、是讓 planner pruning" data-link-desc="Declarative partitioning 的真實價值是 query planner pruning &#43; maintenance scope 縮小、不是「把大表切小」；RANGE / LIST / HASH 取捨、partition key 選法、5 個 production 踩雷（key 選錯不 prune / unique 不 enforce 跨 partition / ATTACH 鎖太久 / partition 數爆 / DETACH 不 reclaim 空間）、跟 autovacuum &#43; index 設計整合">declarative partitioning</a></td>
          <td>C concept-reversed</td>
          <td>「Partition 不是『把大表切小』、是『讓 planner pruning + 縮小 maintenance scope』」</td>
          <td>244</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">logical replication + Debezium</a></td>
          <td>D table-driven</td>
          <td>「Replication slot x Failure x Recovery 對照」</td>
          <td>227</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/01-database/vendors/postgresql/pitr-wal-archiving/" data-link-title="PostgreSQL PITR &#43; WAL archiving：從 base backup 到 point-in-time recovery 的完整鏈" data-link-desc="Base backup &#43; WAL archive 構成 PITR 的雙軌資料、archive_command &#43; restore_command 配置、用 pgBackRest / WAL-G 替代手寫腳本、5 個 production 踩雷（archive 靜默失敗 / archive lag / 錯誤 target time / base backup 過期未清 / timeline 分歧 recovery 模糊）、跟 Patroni &#43; monitoring 整合">PITR + WAL archiving</a></td>
          <td>A standard 6-section</td>
          <td>「問題情境」</td>
          <td>273</td>
      </tr>
  </tbody>
</table>
<p>第二輪在最高風險場景（同 vendor sub-tool）仍維持 collapse 0%，且新增第五種 variant（lifecycle-driven）。</p>
<h3 id="跨兩輪對照">跨兩輪對照</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>第一輪 N=4（跨 vendor）</th>
          <th>第二輪 N=5（同 vendor sub-tool）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Variant 種類</td>
          <td>4（A / B / C / D）</td>
          <td>5（A / B / C / D / E）</td>
      </tr>
      <tr>
          <td>Cadence collapse</td>
          <td>0/4 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>4</td>
          <td>5</td>
      </tr>
      <tr>
          <td>共同 context</td>
          <td>6-section framework</td>
          <td>6-section + 同 vendor + 同讀者</td>
      </tr>
  </tbody>
</table>
<p>關鍵驗證：</p>
<ol>
<li><strong>N=5 仍 0% collapse</strong>：5 種 variant 在最高風險場景（同 vendor sub-tool）仍完全錯開</li>
<li><strong>5 variant 不耗盡</strong>：5 種變體（lifecycle / pain / reverse / table / standard）對應主題自然進入方式、不是強制配對</li>
<li><strong>cadence audit 最佳位置是進度 60-80%</strong>：進度 10-20% 只有 1 樣本訊號弱、60-80% 有 4 樣本對照訊號強</li>
</ol>
<h2 id="方法論演化小結">方法論演化小結</h2>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>修改</th>
          <th>驅動來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>v0</td>
          <td>直覺套 overview 11 章節</td>
          <td>第一篇 deep article 不合用</td>
      </tr>
      <tr>
          <td>v1</td>
          <td>6 段結構 + 200-400 行 sweet spot</td>
          <td>第一輪 5 篇 dogfood</td>
      </tr>
      <tr>
          <td>v1.1</td>
          <td>6 段是內容指引、不是章節標題模板</td>
          <td>章節 1 framing 自然分化</td>
      </tr>
      <tr>
          <td>v1.2</td>
          <td>寫作時間預估 2-4hr → 1-2hr</td>
          <td>overview 已建立 context</td>
      </tr>
      <tr>
          <td>v1.3</td>
          <td>cadence audit 抽樣位置 10-20% → 60-80%</td>
          <td>第二輪 N=5 驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Vendor Deep Article skill（<code>.claude/skills/vendor-deep-article/</code>）— 操作步驟</li>
<li><a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 方法論演化紀錄</a> — sibling、處理 cross-vendor process</li>
<li><a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">Case-First Agent Team Review Workflow</a> — 教學模組級批次寫作流程</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></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></channel></rss>