<?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>抽象層 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E6%8A%BD%E8%B1%A1%E5%B1%A4/</link><description>Recent content in 抽象層 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E6%8A%BD%E8%B1%A1%E5%B1%A4/index.xml" rel="self" type="application/rss+xml"/><item><title>2 次門檻：第一次是運氣、第二次是訊號</title><link>https://tarrragon.github.io/blog/report/two-occurrence-threshold/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/two-occurrence-threshold/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>第 1 次失敗是運氣、第 2 次失敗是訊號。&lt;/strong> 同一個問題、同一個方向、同一類錯誤出現第 2 次時、停下來把處理層級升一階 — 不要繼續用同層方法第 3 次嘗試。第 1 次失敗的資訊不足以判斷「這條路是否值得繼續」、第 2 次提供「在同類條件下重複失敗」的證據、值得付出升級成本。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼是-2-次不是-1-次或-3-次">為什麼是 2 次、不是 1 次或 3 次&lt;/h2>
&lt;h3 id="1-次失敗的資訊量不足">1 次失敗的資訊量不足&lt;/h3>
&lt;p>第 1 次失敗時可能的原因：&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>實作細節錯了（typo、API 用錯）&lt;/td>
 &lt;td>修細節再試一次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>假設在邊界 case 不成立&lt;/td>
 &lt;td>微調假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路徑本身錯了&lt;/td>
 &lt;td>換路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>環境因素（cache、暫時性問題）&lt;/td>
 &lt;td>重試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第 1 次失敗無法區分這四種 — 證據量不夠做決策。預設用最便宜的應對（修細節重試）是合理的。&lt;/p>
&lt;h3 id="2-次失敗的證據量足夠">2 次失敗的證據量足夠&lt;/h3>
&lt;p>同方向第 2 次失敗、四種原因中三種被排除：&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>否 — 第 2 次已經修過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界 case&lt;/td>
 &lt;td>否 — 第 2 次的 case 不一樣、仍然失敗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>環境因素&lt;/td>
 &lt;td>否 — 兩次環境不同仍失敗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>路徑本身錯了&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>是&lt;/strong> — 唯一沒排除的選項&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第 2 次失敗 ≈ 「路徑本身有問題」的證據。繼續用同層方法第 3 次嘗試 = 忽略這個證據。&lt;/p>
&lt;h3 id="3-次以上是浪費">3 次以上是浪費&lt;/h3>
&lt;p>到第 3 次嘗試還沒升級、表示已經錯過訊號。每次失敗的學習回報遞減、執行者的耐心遞減、心智負擔累積。事後檢視常常會發現：「第 2 次就該停了、第 4 次才停浪費了兩輪」。&lt;/p>
&lt;h3 id="為什麼不是-1-次就升級">為什麼不是 1 次就升級&lt;/h3>
&lt;p>預防式升級成本太高 — 大部分問題第 1 次嘗試就解決。如果每次失敗都立刻升級、會在「修個 typo 就好」的場景過度反應。&lt;strong>門檻定在 2 = 不過度反應、又不錯過真訊號&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="三個應用面向">三個應用面向&lt;/h2>
&lt;p>「2 次門檻」不是單一規則、是一條跨層級套用的元規則。三個典型升級方向：&lt;/p>
&lt;h3 id="應用-1推理--量測11-在開發循環裡早一點用-playwright">應用 1：推理 → 量測（&lt;a href="../playwright-early-in-loop/">#11 在開發循環裡早一點用 playwright&lt;/a>）&lt;/h3>
&lt;p>&lt;strong>情境&lt;/strong>：CSS 行為跟預期不符、靜態推理該怎麼改。&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>第 1 次推理&lt;/td>
 &lt;td>低 — 假設對時一次到位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 次推理&lt;/td>
 &lt;td>高 — 假設錯了得重來、多輪試錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>升級&lt;/strong>：停止靜態推理、用 playwright &lt;code>browser_evaluate&lt;/code> 直接讀 live DOM。把「四個變數（CSS / DOM / 繼承 / framework）」中假設的部分變成已知。&lt;/p>
&lt;h3 id="應用-2手動驗證--自動化測試15-用前端測試把排版問題自動化">應用 2：手動驗證 → 自動化測試（&lt;a href="../layout-tests-with-playwright/">#15 用前端測試把排版問題自動化&lt;/a>）&lt;/h3>
&lt;p>&lt;strong>情境&lt;/strong>：版型 bug 已經 debug 過、未來是否會回歸。&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>第 1 次&lt;/td>
 &lt;td>修完即可、不寫測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 次&lt;/td>
 &lt;td>表示這地方容易壞、寫測試固化契約&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>升級&lt;/strong>：把手動「改 CSS → 開頁面看」的驗證迴圈、換成 playwright 測試。下次有人改 CSS 立刻紅、不用人記得回歸驗證。&lt;/p>
&lt;h3 id="應用-3同方向嘗試--換思路20-同方向反覆失敗的轉折點">應用 3：同方向嘗試 → 換思路（&lt;a href="../failure-direction-pivot-point/">#20 同方向反覆失敗的轉折點&lt;/a>）&lt;/h3>
&lt;p>&lt;strong>情境&lt;/strong>：用 grid 解某個 layout 問題、第一次沒解決。&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>第 1 次 grid 失敗&lt;/td>
 &lt;td>微調 grid 參數重試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 次 grid 失敗&lt;/td>
 &lt;td>停下來假設「grid 本身可能不對」、改用 absolute / flex&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>升級&lt;/strong>：從「同層 retry」升到「換思路」。第 2 次失敗的訊號是「底層假設可能錯」、不是「再調一次參數就行」。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>第 1 次失敗是運氣、第 2 次失敗是訊號。</strong> 同一個問題、同一個方向、同一類錯誤出現第 2 次時、停下來把處理層級升一階 — 不要繼續用同層方法第 3 次嘗試。第 1 次失敗的資訊不足以判斷「這條路是否值得繼續」、第 2 次提供「在同類條件下重複失敗」的證據、值得付出升級成本。</p>
<hr>
<h2 id="為什麼是-2-次不是-1-次或-3-次">為什麼是 2 次、不是 1 次或 3 次</h2>
<h3 id="1-次失敗的資訊量不足">1 次失敗的資訊量不足</h3>
<p>第 1 次失敗時可能的原因：</p>
<table>
  <thead>
      <tr>
          <th>原因</th>
          <th>應對</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作細節錯了（typo、API 用錯）</td>
          <td>修細節再試一次</td>
      </tr>
      <tr>
          <td>假設在邊界 case 不成立</td>
          <td>微調假設</td>
      </tr>
      <tr>
          <td>路徑本身錯了</td>
          <td>換路徑</td>
      </tr>
      <tr>
          <td>環境因素（cache、暫時性問題）</td>
          <td>重試</td>
      </tr>
  </tbody>
</table>
<p>第 1 次失敗無法區分這四種 — 證據量不夠做決策。預設用最便宜的應對（修細節重試）是合理的。</p>
<h3 id="2-次失敗的證據量足夠">2 次失敗的證據量足夠</h3>
<p>同方向第 2 次失敗、四種原因中三種被排除：</p>
<table>
  <thead>
      <tr>
          <th>原因</th>
          <th>還可能嗎</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作細節錯了</td>
          <td>否 — 第 2 次已經修過</td>
      </tr>
      <tr>
          <td>邊界 case</td>
          <td>否 — 第 2 次的 case 不一樣、仍然失敗</td>
      </tr>
      <tr>
          <td>環境因素</td>
          <td>否 — 兩次環境不同仍失敗</td>
      </tr>
      <tr>
          <td><strong>路徑本身錯了</strong></td>
          <td><strong>是</strong> — 唯一沒排除的選項</td>
      </tr>
  </tbody>
</table>
<p>第 2 次失敗 ≈ 「路徑本身有問題」的證據。繼續用同層方法第 3 次嘗試 = 忽略這個證據。</p>
<h3 id="3-次以上是浪費">3 次以上是浪費</h3>
<p>到第 3 次嘗試還沒升級、表示已經錯過訊號。每次失敗的學習回報遞減、執行者的耐心遞減、心智負擔累積。事後檢視常常會發現：「第 2 次就該停了、第 4 次才停浪費了兩輪」。</p>
<h3 id="為什麼不是-1-次就升級">為什麼不是 1 次就升級</h3>
<p>預防式升級成本太高 — 大部分問題第 1 次嘗試就解決。如果每次失敗都立刻升級、會在「修個 typo 就好」的場景過度反應。<strong>門檻定在 2 = 不過度反應、又不錯過真訊號</strong>。</p>
<hr>
<h2 id="三個應用面向">三個應用面向</h2>
<p>「2 次門檻」不是單一規則、是一條跨層級套用的元規則。三個典型升級方向：</p>
<h3 id="應用-1推理--量測11-在開發循環裡早一點用-playwright">應用 1：推理 → 量測（<a href="../playwright-early-in-loop/">#11 在開發循環裡早一點用 playwright</a>）</h3>
<p><strong>情境</strong>：CSS 行為跟預期不符、靜態推理該怎麼改。</p>
<table>
  <thead>
      <tr>
          <th>嘗試次數</th>
          <th>成本曲線</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次推理</td>
          <td>低 — 假設對時一次到位</td>
      </tr>
      <tr>
          <td>第 2 次推理</td>
          <td>高 — 假設錯了得重來、多輪試錯</td>
      </tr>
  </tbody>
</table>
<p><strong>升級</strong>：停止靜態推理、用 playwright <code>browser_evaluate</code> 直接讀 live DOM。把「四個變數（CSS / DOM / 繼承 / framework）」中假設的部分變成已知。</p>
<h3 id="應用-2手動驗證--自動化測試15-用前端測試把排版問題自動化">應用 2：手動驗證 → 自動化測試（<a href="../layout-tests-with-playwright/">#15 用前端測試把排版問題自動化</a>）</h3>
<p><strong>情境</strong>：版型 bug 已經 debug 過、未來是否會回歸。</p>
<table>
  <thead>
      <tr>
          <th>出現次數</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次</td>
          <td>修完即可、不寫測試</td>
      </tr>
      <tr>
          <td>第 2 次</td>
          <td>表示這地方容易壞、寫測試固化契約</td>
      </tr>
  </tbody>
</table>
<p><strong>升級</strong>：把手動「改 CSS → 開頁面看」的驗證迴圈、換成 playwright 測試。下次有人改 CSS 立刻紅、不用人記得回歸驗證。</p>
<h3 id="應用-3同方向嘗試--換思路20-同方向反覆失敗的轉折點">應用 3：同方向嘗試 → 換思路（<a href="../failure-direction-pivot-point/">#20 同方向反覆失敗的轉折點</a>）</h3>
<p><strong>情境</strong>：用 grid 解某個 layout 問題、第一次沒解決。</p>
<table>
  <thead>
      <tr>
          <th>嘗試次數</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次 grid 失敗</td>
          <td>微調 grid 參數重試</td>
      </tr>
      <tr>
          <td>第 2 次 grid 失敗</td>
          <td>停下來假設「grid 本身可能不對」、改用 absolute / flex</td>
      </tr>
  </tbody>
</table>
<p><strong>升級</strong>：從「同層 retry」升到「換思路」。第 2 次失敗的訊號是「底層假設可能錯」、不是「再調一次參數就行」。</p>
<hr>
<h2 id="識別同方向避免誤判-2-次門檻">識別「同方向」：避免誤判 2 次門檻</h2>
<p>「2 次失敗」必須是<strong>同方向</strong>才算門檻。誤判同方向會錯誤觸發升級、誤判不同方向會錯過真訊號。</p>
<h3 id="同方向的判準">同方向的判準</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>同方向</th>
          <th>不同方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>假設基礎</td>
          <td>共用同一假設</td>
          <td>換了核心假設</td>
      </tr>
      <tr>
          <td>工具層</td>
          <td>同一抽象層</td>
          <td>換到不同抽象層</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>同類錯誤訊息 / 症狀</td>
          <td>完全不同的失敗</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀問題</strong>：「兩次嘗試的差異、有沒有挑戰底層假設？」</p>
<ul>
<li>沒有 → 同方向、第 2 次失敗 = 升級訊號</li>
<li>有 → 不同方向、相當於兩次第 1 次嘗試、繼續嘗試合理</li>
</ul>
<h3 id="反例誤判為同方向">反例：誤判為同方向</h3>
<p>第 1 次：用 <code>grid-row: 1</code> 把 drawer 放到第一列、失敗
第 2 次：用 <code>grid-row: 1 / span 2</code> 跨兩列、失敗</p>
<p>兩次都基於「drawer 是 grid item」的假設 — 是同方向。第 2 次該停下來檢查這個假設（drawer 實際是 form 子節點、不是 grid item）。</p>
<h3 id="反例誤判為不同方向">反例：誤判為不同方向</h3>
<p>第 1 次：寫 CSS 試
第 2 次：寫 JS 試（覺得「這算換工具」）</p>
<p>但兩次都基於「drawer 在 grid 第一列」的目標 — 假設沒變、只換了實作工具。仍是同方向。</p>
<hr>
<h2 id="不該套用-2-次門檻的情境">不該套用 2 次門檻的情境</h2>
<p>這條原則有適用範圍、不是所有失敗都套用：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>探索性學習新技術</td>
          <td>學習過程的失敗本身有價值、不是訊號</td>
      </tr>
      <tr>
          <td>已知 flaky 的測試 / 環境</td>
          <td>失敗來自環境、不是路徑問題</td>
      </tr>
      <tr>
          <td>嘗試次數很便宜（&lt; 1 秒）</td>
          <td>預設成本太低、3-5 次嘗試比升級便宜</td>
      </tr>
      <tr>
          <td>真正的不同方向（換假設換工具）</td>
          <td>計數歸零、重新從第 1 次開始</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：升級成本 vs 繼續嘗試成本的比較。當每次嘗試都很貴（人類幾分鐘 / 來回溝通 / 上下文切換）、2 次就該升級；當嘗試很便宜、可以放寬到 3-5 次。</p>
<hr>
<h2 id="內在屬性比較四種失敗回應">內在屬性比較：四種失敗回應</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>效率</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次失敗就升級</td>
          <td>低 — 過度反應、簡單問題複雜化</td>
          <td>低 — 但放棄太多便宜路徑</td>
      </tr>
      <tr>
          <td>第 2 次失敗升級</td>
          <td>高 — 平衡不過度反應與不錯失訊號</td>
          <td>低</td>
      </tr>
      <tr>
          <td>第 4-5 次才升級</td>
          <td>低 — 浪費前 3 次的時間</td>
          <td>中 — 累積心智負擔</td>
      </tr>
      <tr>
          <td>不升級、無限試</td>
          <td>最低 — 在錯方向裡打轉</td>
          <td>高 — 可能永遠到不了正解</td>
      </tr>
  </tbody>
</table>
<p><strong>推薦</strong>：第 2 次失敗升級。例外情境（探索 / flaky）顯式判斷再放寬。</p>
<hr>
<h2 id="跨情境辨識訊號表">跨情境辨識訊號表</h2>
<p>下次工作中看到這些訊號、回想「2 次門檻」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>在做什麼</th>
          <th>升級到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「我覺得改這個應該就好了」第 2 次</td>
          <td>靜態推理</td>
          <td>量測（#11）</td>
      </tr>
      <tr>
          <td>「上次也是這個 bug」</td>
          <td>手動驗證</td>
          <td>自動化測試（#15）</td>
      </tr>
      <tr>
          <td>「再調一次 grid 參數」第 2 次</td>
          <td>同方向嘗試</td>
          <td>換思路（#20）</td>
      </tr>
      <tr>
          <td>「再加一條 CSS 應該就蓋過了」第 2 次</td>
          <td>specificity 戰</td>
          <td>換維度（<a href="../css-layers-over-specificity/">#24 CSS Layers</a>）</td>
      </tr>
      <tr>
          <td>「再多寫一個 if 處理這 case」第 2 次</td>
          <td>patch 補丁</td>
          <td>看是否該重新設計</td>
      </tr>
  </tbody>
</table>
<p><strong>通用形式</strong>：「再做一次同方向的 X」第 2 次出現 = 該升級的訊號。</p>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<p>每篇示範這個原則的不同面向、各自展開具體場景：</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>升級方向</th>
          <th>場景特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../playwright-early-in-loop/">#11 早一點用 playwright</a></td>
          <td>推理 → 量測</td>
          <td>CSS 行為跟預期不符</td>
      </tr>
      <tr>
          <td><a href="../layout-tests-with-playwright/">#15 用前端測試把排版自動化</a></td>
          <td>手動驗證 → 自動化</td>
          <td>同版型 bug 第 2 次</td>
      </tr>
      <tr>
          <td><a href="../failure-direction-pivot-point/">#20 同方向反覆失敗的轉折點</a></td>
          <td>同層 retry → 換思路</td>
          <td>同方向第 2 次失敗</td>
      </tr>
      <tr>
          <td><a href="../verification-method-timing/">#23 驗證方法的選擇時機</a></td>
          <td>被動等指令 → 主動提工具</td>
          <td>反覆試錯時的溝通</td>
      </tr>
  </tbody>
</table>
<p>讀的時候從本篇出發、依場景挑實作篇 — 不需要逐篇讀完。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>自問</th>
          <th>回應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「再試一次同方向應該就好」</td>
          <td>這是第幾次同方向嘗試？</td>
          <td>第 2 次 → 停下來升級</td>
      </tr>
      <tr>
          <td>對話中「上次也是這樣」</td>
          <td>這個 bug 已經修過？</td>
          <td>是 → 寫測試固化</td>
      </tr>
      <tr>
          <td>同個假設用第 3 種寫法</td>
          <td>假設本身可能錯？</td>
          <td>是 → 改假設、不換寫法</td>
      </tr>
      <tr>
          <td>累積心智負擔但還沒進展</td>
          <td>是不是錯過 2 次門檻？</td>
          <td>是 → 立刻升級、不再試</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：失敗的價值在於提供資訊。第 1 次失敗資訊不足以決策、第 2 次失敗資訊足以決策 — 兩者要用不同方式回應。混為一談會讓人在錯方向裡無限重試、或對小錯過度反應。</p>
<p>第 5 個面向：<strong>驗收訊號</strong> — 「畫面對一次」是低資訊量訊號、跟「程式跑通一次」「測試過一次」是同類錯誤。詳見 <a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a> 把驗收訊號的時間軸跟 2 次門檻接起來、<a href="../verification-timeline-checkpoints/">#68 驗收的時間軸：四個 checkpoint</a> 把驗收分散到多個時點。</p>
<p>第 6 個面向：<strong>測試訊號</strong> — 「測試 PASS 一次」是低資訊量訊號（測試本身可能有 bug、可能太寬）。要 RED → GREEN 兩個訊號 — 一次 fail 一次 pass — 才能相信測試真的會 catch。詳見 <a href="../test-first-red-before-green/">#69 Test-First：先看到 RED 才相信 GREEN</a>。</p>
<p>第 7 個面向：<strong>跨檔 emergence 訊號</strong> — 在批量寫作 / 批量產出情境下、「第 2 次」要區分 <em>同檔</em> vs <em>跨檔</em> 兩種強度。同檔同 pattern 第 2 次出現 = 直接訊號、立即升級；跨檔同 cadence 第 2 次出現 = 弱訊號、樣本數通常要到 5-10 才強到 catch。對應 <a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a> 跟 <a href="../emergence-violations-need-in-stream-sampling/">#124 Emergence-class 違規規則化不了</a> — 跨檔 emergence 的 2 次門檻不在「寫第 2 篇就 catch」、而在「寫到 batch 進度 10-20% 時抽樣 catch」、過了這位置修正成本就會 N 倍上升。</p>
]]></content:encoded></item><item><title>最小必要範圍是 sanity 防線：保護行為可預測性</title><link>https://tarrragon.github.io/blog/report/minimum-necessary-scope-is-sanity-defense/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/minimum-necessary-scope-is-sanity-defense/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「最小必要範圍」是 sanity 防線、不是優化選項。&lt;/strong> 縮 selector / observer / DOM 操作的範圍、目的不是為了讓程式跑更快、是為了&lt;strong>讓行為可預測&lt;/strong>：不誤命中、不過度觸發、不被未來頁面結構變動打破。從具體放寬比從寬泛收緊容易得多 — 兩者的成本曲線完全不對稱。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼是sanity-防線不是優化">為什麼是「sanity 防線」、不是「優化」&lt;/h2>
&lt;h3 id="兩個概念常被混為一談">兩個概念常被混為一談&lt;/h3>
&lt;p>「縮範圍」聽起來像效能優化（少做一點工 = 快一點）— 這誤解掩蓋了它真正的價值。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>優化&lt;/th>
 &lt;th>Sanity 防線&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>目標&lt;/td>
 &lt;td>提升某個量化指標（速度、記憶體）&lt;/td>
 &lt;td>防止某類錯誤發生&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>衡量&lt;/td>
 &lt;td>跑得多快、用多少資源&lt;/td>
 &lt;td>行為是否可預測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗代價&lt;/td>
 &lt;td>慢一點&lt;/td>
 &lt;td>出錯時 debug 困難&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>該追求的時機&lt;/td>
 &lt;td>有量到瓶頸時&lt;/td>
 &lt;td>寫第一行就該追求&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把「縮範圍」當優化的後果：以為「現在沒效能問題、之後再縮」 — 但 sanity 防線錯過了第一行就追求的時機、未來補救成本更高。&lt;/p>
&lt;h3 id="寬範圍的代價不是慢">寬範圍的代價不是「慢」&lt;/h3>
&lt;p>寬 selector / observer / 操作範圍的失敗模式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>Debug 難度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>誤命中其他元素&lt;/td>
 &lt;td>動了不該動的、且通常不報錯&lt;/td>
 &lt;td>高 — 安靜失敗、bug 表現遠離 root cause&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>過度觸發&lt;/td>
 &lt;td>apply 跑了 N 次、其中 N-1 次無意義&lt;/td>
 &lt;td>中 — 看 callstack 不知道為什麼觸發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跟未來結構變動衝突&lt;/td>
 &lt;td>加了一個 widget 後原本的程式壞掉&lt;/td>
 &lt;td>高 — 不知道哪個假設被打破&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跟 framework 渲染週期競爭&lt;/td>
 &lt;td>在 layout 還沒穩時跑、視覺閃爍&lt;/td>
 &lt;td>高 — 時序問題、難以重現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四種代價都不是「慢」 — 都是「行為不可預測」。Sanity 防線守的是這個。&lt;/p>
&lt;hr>
&lt;h2 id="從具體放寬vs從寬泛收緊的不對稱性">「從具體放寬」vs「從寬泛收緊」的不對稱性&lt;/h2>
&lt;p>兩個方向在表面上對稱、實際成本曲線完全不對稱：&lt;/p>
&lt;h3 id="從具體放寬推薦">從具體放寬（推薦）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">寫第一版：用最具體的 selector / 最小的 observer 範圍 / 最窄的操作邊界
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">發現某個 case 沒覆蓋
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">顯式評估「該放寬到什麼程度」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">擴大範圍、知道擴大後的影響範圍&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每次擴大都是顯式決定 — 知道「我為什麼擴大、擴大到哪」。&lt;/p>
&lt;h3 id="從寬泛收緊反推薦">從寬泛收緊（反推薦）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">寫第一版：用最寬的 selector / subtree observer / 全頁面操作
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">某天發現某個 bug（誤命中、過度觸發、framework 衝突）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">不確定哪些地方是「故意要寬」、哪些是「意外寬了」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">試著縮、可能漏掉某些故意要寬的場景、引發新 bug&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>收緊時面對的問題：&lt;strong>寬範圍的程式碼裡看不出「哪些是故意寬、哪些是意外寬」&lt;/strong>。原作者也不一定記得當初為什麼寫寬。&lt;/p>
&lt;h3 id="不對稱性的根源">不對稱性的根源&lt;/h3>
&lt;p>這個不對稱不是工程偏好、是&lt;strong>資訊量的差異&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從具體放寬：每次擴大時、有當前需求當證據（「為了 X case 才擴大」）&lt;/li>
&lt;li>從寬泛收緊：縮的時候、不知道原本依賴哪些寬範圍特性&lt;/li>
&lt;/ul>
&lt;p>寫程式時的「具體 → 寬泛」走法保留了決策軌跡；「寬泛 → 具體」走法丟失了軌跡。&lt;/p>
&lt;hr>
&lt;h2 id="三類範圍的共同骨架">三類範圍的共同骨架&lt;/h2>
&lt;p>「最小必要範圍」原則跨三類獨立議題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>範圍對象&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;th>對應實作篇&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>JS 元件邊界&lt;/td>
 &lt;td>「我可以動什麼」的契約&lt;/td>
 &lt;td>越界操作 framework 管的部分、被重繪清掉&lt;/td>
 &lt;td>&lt;a href="../component-boundary-and-js-impact/">#13 元件邊界與 JS 操作的影響範圍&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Selector 精準度&lt;/td>
 &lt;td>「query 命中哪些元素」&lt;/td>
 &lt;td>誤命中、未來結構變動就壞&lt;/td>
 &lt;td>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Observer 範圍&lt;/td>
 &lt;td>「監聽哪些變動」&lt;/td>
 &lt;td>過度觸發、layout 抖動、無限循環&lt;/td>
 &lt;td>&lt;a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者表現不同、機制不同、但&lt;strong>底層都是同一條原則的應用&lt;/strong> — 越寬越脆弱、越具體越穩定。&lt;/p>
&lt;h3 id="共通的設計工具">共通的設計工具&lt;/h3>
&lt;p>跨三類議題、設計「最小必要範圍」的工具有共通模式：&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;strong>起點 / 邊界&lt;/strong>&lt;/td>
 &lt;td>JS：元件邊界契約；Selector：query 起點；Observer：root&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>深度&lt;/strong>&lt;/td>
 &lt;td>JS：操作層級；Selector：是否找深層；Observer：subtree&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>過濾&lt;/strong>&lt;/td>
 &lt;td>JS：操作前界定；Selector：attribute filter / :not；Observer：option flag&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個議題都有「起點 / 深度 / 過濾」三維度可顯式設計 — 同樣的設計骨架在不同情境重現。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「最小必要範圍」是 sanity 防線、不是優化選項。</strong> 縮 selector / observer / DOM 操作的範圍、目的不是為了讓程式跑更快、是為了<strong>讓行為可預測</strong>：不誤命中、不過度觸發、不被未來頁面結構變動打破。從具體放寬比從寬泛收緊容易得多 — 兩者的成本曲線完全不對稱。</p>
<hr>
<h2 id="為什麼是sanity-防線不是優化">為什麼是「sanity 防線」、不是「優化」</h2>
<h3 id="兩個概念常被混為一談">兩個概念常被混為一談</h3>
<p>「縮範圍」聽起來像效能優化（少做一點工 = 快一點）— 這誤解掩蓋了它真正的價值。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>優化</th>
          <th>Sanity 防線</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>目標</td>
          <td>提升某個量化指標（速度、記憶體）</td>
          <td>防止某類錯誤發生</td>
      </tr>
      <tr>
          <td>衡量</td>
          <td>跑得多快、用多少資源</td>
          <td>行為是否可預測</td>
      </tr>
      <tr>
          <td>失敗代價</td>
          <td>慢一點</td>
          <td>出錯時 debug 困難</td>
      </tr>
      <tr>
          <td>該追求的時機</td>
          <td>有量到瓶頸時</td>
          <td>寫第一行就該追求</td>
      </tr>
  </tbody>
</table>
<p>把「縮範圍」當優化的後果：以為「現在沒效能問題、之後再縮」 — 但 sanity 防線錯過了第一行就追求的時機、未來補救成本更高。</p>
<h3 id="寬範圍的代價不是慢">寬範圍的代價不是「慢」</h3>
<p>寬 selector / observer / 操作範圍的失敗模式：</p>
<table>
  <thead>
      <tr>
          <th>失敗</th>
          <th>表現</th>
          <th>Debug 難度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>誤命中其他元素</td>
          <td>動了不該動的、且通常不報錯</td>
          <td>高 — 安靜失敗、bug 表現遠離 root cause</td>
      </tr>
      <tr>
          <td>過度觸發</td>
          <td>apply 跑了 N 次、其中 N-1 次無意義</td>
          <td>中 — 看 callstack 不知道為什麼觸發</td>
      </tr>
      <tr>
          <td>跟未來結構變動衝突</td>
          <td>加了一個 widget 後原本的程式壞掉</td>
          <td>高 — 不知道哪個假設被打破</td>
      </tr>
      <tr>
          <td>跟 framework 渲染週期競爭</td>
          <td>在 layout 還沒穩時跑、視覺閃爍</td>
          <td>高 — 時序問題、難以重現</td>
      </tr>
  </tbody>
</table>
<p>四種代價都不是「慢」 — 都是「行為不可預測」。Sanity 防線守的是這個。</p>
<hr>
<h2 id="從具體放寬vs從寬泛收緊的不對稱性">「從具體放寬」vs「從寬泛收緊」的不對稱性</h2>
<p>兩個方向在表面上對稱、實際成本曲線完全不對稱：</p>
<h3 id="從具體放寬推薦">從具體放寬（推薦）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">寫第一版：用最具體的 selector / 最小的 observer 範圍 / 最窄的操作邊界
</span></span><span class="line"><span class="ln">2</span><span class="cl">↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">發現某個 case 沒覆蓋
</span></span><span class="line"><span class="ln">4</span><span class="cl">↓
</span></span><span class="line"><span class="ln">5</span><span class="cl">顯式評估「該放寬到什麼程度」
</span></span><span class="line"><span class="ln">6</span><span class="cl">↓
</span></span><span class="line"><span class="ln">7</span><span class="cl">擴大範圍、知道擴大後的影響範圍</span></span></code></pre></div><p>每次擴大都是顯式決定 — 知道「我為什麼擴大、擴大到哪」。</p>
<h3 id="從寬泛收緊反推薦">從寬泛收緊（反推薦）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">寫第一版：用最寬的 selector / subtree observer / 全頁面操作
</span></span><span class="line"><span class="ln">2</span><span class="cl">↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">某天發現某個 bug（誤命中、過度觸發、framework 衝突）
</span></span><span class="line"><span class="ln">4</span><span class="cl">↓
</span></span><span class="line"><span class="ln">5</span><span class="cl">不確定哪些地方是「故意要寬」、哪些是「意外寬了」
</span></span><span class="line"><span class="ln">6</span><span class="cl">↓
</span></span><span class="line"><span class="ln">7</span><span class="cl">試著縮、可能漏掉某些故意要寬的場景、引發新 bug</span></span></code></pre></div><p>收緊時面對的問題：<strong>寬範圍的程式碼裡看不出「哪些是故意寬、哪些是意外寬」</strong>。原作者也不一定記得當初為什麼寫寬。</p>
<h3 id="不對稱性的根源">不對稱性的根源</h3>
<p>這個不對稱不是工程偏好、是<strong>資訊量的差異</strong>：</p>
<ul>
<li>從具體放寬：每次擴大時、有當前需求當證據（「為了 X case 才擴大」）</li>
<li>從寬泛收緊：縮的時候、不知道原本依賴哪些寬範圍特性</li>
</ul>
<p>寫程式時的「具體 → 寬泛」走法保留了決策軌跡；「寬泛 → 具體」走法丟失了軌跡。</p>
<hr>
<h2 id="三類範圍的共同骨架">三類範圍的共同骨架</h2>
<p>「最小必要範圍」原則跨三類獨立議題：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>範圍對象</th>
          <th>失敗模式</th>
          <th>對應實作篇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JS 元件邊界</td>
          <td>「我可以動什麼」的契約</td>
          <td>越界操作 framework 管的部分、被重繪清掉</td>
          <td><a href="../component-boundary-and-js-impact/">#13 元件邊界與 JS 操作的影響範圍</a></td>
      </tr>
      <tr>
          <td>Selector 精準度</td>
          <td>「query 命中哪些元素」</td>
          <td>誤命中、未來結構變動就壞</td>
          <td><a href="../dom-selector-precision/">#14 Selector 精準度</a></td>
      </tr>
      <tr>
          <td>Observer 範圍</td>
          <td>「監聽哪些變動」</td>
          <td>過度觸發、layout 抖動、無限循環</td>
          <td><a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a></td>
      </tr>
  </tbody>
</table>
<p>三者表現不同、機制不同、但<strong>底層都是同一條原則的應用</strong> — 越寬越脆弱、越具體越穩定。</p>
<h3 id="共通的設計工具">共通的設計工具</h3>
<p>跨三類議題、設計「最小必要範圍」的工具有共通模式：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>三類議題的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>起點 / 邊界</strong></td>
          <td>JS：元件邊界契約；Selector：query 起點；Observer：root</td>
      </tr>
      <tr>
          <td><strong>深度</strong></td>
          <td>JS：操作層級；Selector：是否找深層；Observer：subtree</td>
      </tr>
      <tr>
          <td><strong>過濾</strong></td>
          <td>JS：操作前界定；Selector：attribute filter / :not；Observer：option flag</td>
      </tr>
  </tbody>
</table>
<p>每個議題都有「起點 / 深度 / 過濾」三維度可顯式設計 — 同樣的設計骨架在不同情境重現。</p>
<hr>
<h2 id="應用辨識訊號">應用辨識訊號</h2>
<p>下次工作中、看到這些訊號該想到「最小必要範圍」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應議題</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「我先寫寬一點、之後有問題再縮」</td>
          <td>任一類</td>
          <td>反向、第一版就寫具體</td>
      </tr>
      <tr>
          <td>「現在只一個元件、document.query 也行」</td>
          <td>Selector 起點</td>
          <td>用元件根變數、預防未來擴展</td>
      </tr>
      <tr>
          <td>「subtree: true 比較保險」</td>
          <td>Observer 範圍</td>
          <td>縮到實際關心的子節點</td>
      </tr>
      <tr>
          <td>「先 framework 內注入個 element 看看」</td>
          <td>JS 元件邊界</td>
          <td>留在 framework 邊界外</td>
      </tr>
      <tr>
          <td>「同樣的 bug 出現在不同元件」</td>
          <td>任一類</td>
          <td>範圍寬了、影響跨越元件邊界</td>
      </tr>
      <tr>
          <td>「改了 X、Y 跟 Z 也壞」</td>
          <td>任一類</td>
          <td>範圍寬了、改動波及</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="不該套用最小必要範圍的情境">不該套用「最小必要範圍」的情境</h2>
<p>這條原則有適用邊界、不是所有「縮」都有意義：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>探索 / debug 階段</td>
          <td>寬範圍幫助觀察全貌、確認問題範圍後再縮</td>
      </tr>
      <tr>
          <td>一次性 script、跑完就丟</td>
          <td>沒有「未來變動」的問題、簡潔優先</td>
      </tr>
      <tr>
          <td>確實需要看深層變動的 observer</td>
          <td>subtree: true 是必要、不是 over-broad</td>
      </tr>
      <tr>
          <td>確實要對全頁套用的操作（theme 切換）</td>
          <td>全頁面才是「最小必要範圍」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：「<strong>最小必要範圍</strong> = 滿足當前需求的最窄範圍」 — 不是極致最小、是「不再小就會漏」的點。盲目縮到「比需要還小」會犧牲覆蓋率、是另一種錯誤。</p>
<hr>
<h2 id="跟2-次門檻的協同">跟「2 次門檻」的協同</h2>
<p><a href="../two-occurrence-threshold/">#42 2 次門檻</a> 處理「失敗第 N 次該換策略」、本原則處理「第一次設計時範圍該多大」。兩者方向互補：</p>
<ul>
<li><strong>2 次門檻</strong>：失敗發生後、何時該升級處理層級</li>
<li><strong>最小必要範圍</strong>：寫第一版時、就該追求 sanity 防線</li>
</ul>
<p>如果第一版就遵循「最小必要範圍」、後續觸發 2 次門檻的機率會降低 — sanity 防線是預防、2 次門檻是補救。</p>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<p>每篇示範這個原則在不同議題的應用：</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>議題</th>
          <th>範圍對象</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../component-boundary-and-js-impact/">#13 元件邊界與 JS 操作的影響範圍</a></td>
          <td>JS 元件邊界</td>
          <td>「我可以動什麼」契約</td>
      </tr>
      <tr>
          <td><a href="../dom-selector-precision/">#14 Selector 精準度</a></td>
          <td>DOM query 範圍</td>
          <td>起點 / 範圍 / 過濾三維度</td>
      </tr>
      <tr>
          <td><a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a></td>
          <td>Observer 監聽範圍</td>
          <td>root / option / 頻率三維度</td>
      </tr>
  </tbody>
</table>
<p>讀的時候從本篇出發、依議題挑實作篇。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>自問</th>
          <th>回應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「之後有問題再縮」</td>
          <td>縮的時候會知道哪些是故意寬嗎？</td>
          <td>否 → 第一版就寫具體</td>
      </tr>
      <tr>
          <td>「以防萬一勾 subtree」</td>
          <td>真的有深層變動需要監聽嗎？</td>
          <td>否 → 移除 subtree</td>
      </tr>
      <tr>
          <td>「document.query 比較簡單」</td>
          <td>未來頁面會不會有第二個同名元素？</td>
          <td>不確定 → 用元件根變數</td>
      </tr>
      <tr>
          <td>「怕 selector 太窄漏掉」</td>
          <td>漏掉時會怎樣、可以擴大嗎？</td>
          <td>可以 → 從具體開始、漏了再擴</td>
      </tr>
      <tr>
          <td>Bug 表現「不知道哪改的」</td>
          <td>範圍是否寬了、波及不該動的地方？</td>
          <td>是 → 縮範圍</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：最小必要範圍守護的是「行為可預測」 — 寫的時候多想一點、debug 時少痛一點。寬範圍的代價不是慢、是出錯時定位困難 — 這也是為什麼這條原則在「沒效能瓶頸」的情境下仍然成立。</p>
<p>延伸到 stream 操作：<a href="../compose-feature-at-source-layer/">#64 Feature 操作要跟 Source 同層合成</a> 是本原則在 stream 領域的應用 — 「合成位置」就是「操作的範圍邊界」、選錯位置 = 範圍錯。寬範圍便利、窄範圍對齊、兩者反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>Single Source of Truth：值的住址只能有一處</title><link>https://tarrragon.github.io/blog/report/single-source-of-truth/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/single-source-of-truth/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>同一個值的權威來源只能有一個位置。&lt;/strong> 「位置」可以是 CSS selector、可以是定義機制、可以是函式 — 重點是&lt;strong>讀者能明確指認「這個值的真相在哪」&lt;/strong>。多個來源會在時間維度上分歧、漏改、debug 時不知道哪個生效。SSoT 不是潔癖、是維護性的物理基礎。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼多源會-drift">為什麼多源會 drift&lt;/h2>
&lt;h3 id="時間維度的失敗模式">時間維度的失敗模式&lt;/h3>
&lt;p>寫程式的當下、多源沒問題 — 兩個值剛寫進去都是 &lt;code>64px&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>第 1 次寫&lt;/td>
 &lt;td>寫一處&lt;/td>
 &lt;td>寫多處（手動同步）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 個月、需求變動要改值&lt;/td>
 &lt;td>改一處、所有引用點自動跟上&lt;/td>
 &lt;td>改多處、可能漏掉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 6 個月、新人接手&lt;/td>
 &lt;td>看一處就知道&lt;/td>
 &lt;td>不知道哪個是「真的」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 1 年、不同人改不同源&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>多源開始分歧、產生隱性 bug&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>多源的隱形成本在時間維度累積&lt;/strong>。寫的當下看不出問題、是因為當下還沒分歧。&lt;/p>
&lt;h3 id="寫程式時的觀察盲點">寫程式時的觀察盲點&lt;/h3>
&lt;p>寫多源的人通常&lt;strong>不是不知道 SSoT 原則&lt;/strong>、是&lt;strong>沒意識到自己在寫多源&lt;/strong>。常見盲點：&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>「複製過來改一下、改完就同步了」&lt;/td>
 &lt;td>兩處數值同步&lt;/td>
 &lt;td>多源的開始 — 之後改哪一處不一定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「這個常數兩個檔案都會用、各自宣告比較清楚」&lt;/td>
 &lt;td>各檔自包含&lt;/td>
 &lt;td>多源 — 改的時候要 grep 找全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「寫死 + 量測雙保險、哪個對用哪個」&lt;/td>
 &lt;td>防禦設計&lt;/td>
 &lt;td>多源 — 不知道哪個生效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先寫一個、之後重構抽出 token」&lt;/td>
 &lt;td>漸進式&lt;/td>
 &lt;td>多源固化 — 通常沒有後續重構&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>辨識自己寫多源、需要主動的 sanity check：「這個值的真相在哪？只有一個答案嗎？」&lt;/p>
&lt;hr>
&lt;h2 id="fact-vs-derivation-的區分">Fact vs Derivation 的區分&lt;/h2>
&lt;p>SSoT 適用對象是&lt;strong>值&lt;/strong>、但不是所有「值」都該 SSoT — 區分兩種：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>定義&lt;/th>
 &lt;th>SSoT 規則&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Fact&lt;/strong>（事實值）&lt;/td>
 &lt;td>設計決定、不能從別處算出&lt;/td>
 &lt;td>只能在一處宣告&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Derivation&lt;/strong>（導出值）&lt;/td>
 &lt;td>從 fact 計算得出&lt;/td>
 &lt;td>完全用 fact 計算、不重複宣告 fact&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>例子（CSS）&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">/* Fact — 設計決定 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&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="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* H1 高度是設計選擇 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* form 高度是設計選擇 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 間距是設計選擇 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c">/* Derivation — 從 fact 計算 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">scope-position&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="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">form&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">gap&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="c">/* 不該寫 152px、那是 derivation 重複了 facts */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把 derivation 寫成具體值（&lt;code>152px&lt;/code>）= 把 fact 在 derivation 裡再宣告一次 = 多源。&lt;/p>
&lt;h3 id="區分-fact--derivation-的判讀問題">區分 fact / derivation 的判讀問題&lt;/h3>
&lt;p>寫一個值時自問：「這個值是設計選擇、還是從別的值算出來的？」&lt;/p>
&lt;ul>
&lt;li>設計選擇 → fact、找住址定義&lt;/li>
&lt;li>從別的值算出來 → derivation、用 calc / function 表達、不寫具體數字&lt;/li>
&lt;/ul>
&lt;p>混淆兩者會固化多源 — 把 derivation 寫成數字、未來 fact 改了 derivation 不會跟著改。&lt;/p>
&lt;hr>
&lt;h2 id="三類-ssot-違反">三類 SSoT 違反&lt;/h2>
&lt;p>跨 #3 / #26 / #27 的三類具體表現：&lt;/p>
&lt;h3 id="違反-1定義位置散落住址多源">違反 1：定義位置散落（住址多源）&lt;/h3>
&lt;p>同一個值在多個 selector 重複定義：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">/* 散在三處 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">root&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--search-title-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">64&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&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="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--search-form-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">68&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&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="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--search-gap&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不是錯、但維護者要 grep 才知道哪些值在哪、改的時候容易漏掉某處。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>同一個值的權威來源只能有一個位置。</strong> 「位置」可以是 CSS selector、可以是定義機制、可以是函式 — 重點是<strong>讀者能明確指認「這個值的真相在哪」</strong>。多個來源會在時間維度上分歧、漏改、debug 時不知道哪個生效。SSoT 不是潔癖、是維護性的物理基礎。</p>
<hr>
<h2 id="為什麼多源會-drift">為什麼多源會 drift</h2>
<h3 id="時間維度的失敗模式">時間維度的失敗模式</h3>
<p>寫程式的當下、多源沒問題 — 兩個值剛寫進去都是 <code>64px</code>、看起來一致。問題出在後續：</p>
<table>
  <thead>
      <tr>
          <th>時間點</th>
          <th>一源情境</th>
          <th>多源情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次寫</td>
          <td>寫一處</td>
          <td>寫多處（手動同步）</td>
      </tr>
      <tr>
          <td>第 2 個月、需求變動要改值</td>
          <td>改一處、所有引用點自動跟上</td>
          <td>改多處、可能漏掉</td>
      </tr>
      <tr>
          <td>第 6 個月、新人接手</td>
          <td>看一處就知道</td>
          <td>不知道哪個是「真的」</td>
      </tr>
      <tr>
          <td>第 1 年、不同人改不同源</td>
          <td>—</td>
          <td>多源開始分歧、產生隱性 bug</td>
      </tr>
  </tbody>
</table>
<p><strong>多源的隱形成本在時間維度累積</strong>。寫的當下看不出問題、是因為當下還沒分歧。</p>
<h3 id="寫程式時的觀察盲點">寫程式時的觀察盲點</h3>
<p>寫多源的人通常<strong>不是不知道 SSoT 原則</strong>、是<strong>沒意識到自己在寫多源</strong>。常見盲點：</p>
<table>
  <thead>
      <tr>
          <th>盲點</th>
          <th>看起來像</th>
          <th>實際上是</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「複製過來改一下、改完就同步了」</td>
          <td>兩處數值同步</td>
          <td>多源的開始 — 之後改哪一處不一定</td>
      </tr>
      <tr>
          <td>「這個常數兩個檔案都會用、各自宣告比較清楚」</td>
          <td>各檔自包含</td>
          <td>多源 — 改的時候要 grep 找全</td>
      </tr>
      <tr>
          <td>「寫死 + 量測雙保險、哪個對用哪個」</td>
          <td>防禦設計</td>
          <td>多源 — 不知道哪個生效</td>
      </tr>
      <tr>
          <td>「先寫一個、之後重構抽出 token」</td>
          <td>漸進式</td>
          <td>多源固化 — 通常沒有後續重構</td>
      </tr>
  </tbody>
</table>
<p>辨識自己寫多源、需要主動的 sanity check：「這個值的真相在哪？只有一個答案嗎？」</p>
<hr>
<h2 id="fact-vs-derivation-的區分">Fact vs Derivation 的區分</h2>
<p>SSoT 適用對象是<strong>值</strong>、但不是所有「值」都該 SSoT — 區分兩種：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>定義</th>
          <th>SSoT 規則</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Fact</strong>（事實值）</td>
          <td>設計決定、不能從別處算出</td>
          <td>只能在一處宣告</td>
      </tr>
      <tr>
          <td><strong>Derivation</strong>（導出值）</td>
          <td>從 fact 計算得出</td>
          <td>完全用 fact 計算、不重複宣告 fact</td>
      </tr>
  </tbody>
</table>
<p><strong>例子（CSS）</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* Fact — 設計決定 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">:</span><span class="nd">root</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>        <span class="c">/* H1 高度是設計選擇 */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>         <span class="c">/* form 高度是設計選擇 */</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>            <span class="c">/* 間距是設計選擇 */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c">/* Derivation — 從 fact 計算 */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">.</span><span class="nc">scope-position</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">form</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">gap</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="c">/* 不該寫 152px、那是 derivation 重複了 facts */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>把 derivation 寫成具體值（<code>152px</code>）= 把 fact 在 derivation 裡再宣告一次 = 多源。</p>
<h3 id="區分-fact--derivation-的判讀問題">區分 fact / derivation 的判讀問題</h3>
<p>寫一個值時自問：「這個值是設計選擇、還是從別的值算出來的？」</p>
<ul>
<li>設計選擇 → fact、找住址定義</li>
<li>從別的值算出來 → derivation、用 calc / function 表達、不寫具體數字</li>
</ul>
<p>混淆兩者會固化多源 — 把 derivation 寫成數字、未來 fact 改了 derivation 不會跟著改。</p>
<hr>
<h2 id="三類-ssot-違反">三類 SSoT 違反</h2>
<p>跨 #3 / #26 / #27 的三類具體表現：</p>
<h3 id="違反-1定義位置散落住址多源">違反 1：定義位置散落（住址多源）</h3>
<p>同一個值在多個 selector 重複定義：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* 散在三處 */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">:</span><span class="nd">root</span>           <span class="p">{</span> <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span> <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span>   <span class="p">{</span> <span class="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>不是錯、但維護者要 grep 才知道哪些值在哪、改的時候容易漏掉某處。</p>
<p><strong>解法</strong>：定義集中在「跟使用範圍最匹配的最高層 selector」、其他地方只 <code>var()</code> 引用。</p>
<p>對應實作：<a href="../css-variable-single-location/">#26 CSS 變數定義位置統一</a>。</p>
<h3 id="違反-2來源機制混搭機制多源">違反 2：來源機制混搭（機制多源）</h3>
<p>同一個值有多種「取得方式」並存：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 對齊基準上四個值、其中三個寫死、一個量測
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="o">--</span><span class="nx">search</span><span class="o">-</span><span class="nx">title</span><span class="o">-</span><span class="nx">h</span><span class="o">:</span> <span class="mi">64</span><span class="nx">px</span><span class="p">;</span>            <span class="c1">// 寫死
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="o">--</span><span class="nx">search</span><span class="o">-</span><span class="nx">form</span><span class="o">-</span><span class="nx">h</span><span class="o">:</span> <span class="mi">68</span><span class="nx">px</span><span class="p">;</span>             <span class="c1">// 寫死
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="o">--</span><span class="nx">search</span><span class="o">-</span><span class="nx">gap</span><span class="o">:</span> <span class="mi">20</span><span class="nx">px</span><span class="p">;</span>                <span class="c1">// 寫死
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="o">--</span><span class="nx">search</span><span class="o">-</span><span class="nx">scope</span><span class="o">-</span><span class="nx">h</span><span class="o">:</span> <span class="nx">ResizeObserver</span><span class="p">;</span>  <span class="c1">// runtime 量測寫回
</span></span></span></code></pre></div><p>寫死值依賴的渲染條件變了（字型、theme、scale）— 量測值會跟著變、寫死值不會、兩者錯位。</p>
<p><strong>解法</strong>：選一邊走到底。內容靜態 → 全寫死；內容動態 → 全量測。混搭就是多源。</p>
<p>對應實作：<a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a>。</p>
<h3 id="違反-3對齊基準的真相分散語意多源">違反 3：對齊基準的真相分散（語意多源）</h3>
<p>對齊問題本質是「方程組」 — 每個參與對齊的值都是基準的一個分量。任何一個分量值不確定、整個基準都靠不住。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">問題：filter padding-top 對不準 H1 + form 下緣
</span></span><span class="line"><span class="ln">2</span><span class="cl">分量：H1 (64px) + form (68px) + gap (20px) = 152px
</span></span><span class="line"><span class="ln">3</span><span class="cl">若任一分量是「我估的」、整個 152px 就不可信</span></span></code></pre></div><p><strong>解法</strong>：每個分量都要有明確的「真相位置」 — 寫死的 token 或 ResizeObserver 量測寫回變數、二選一。沒有「估算」這個選項。</p>
<p>對應實作：<a href="../visual-alignment-single-source-of-truth/">#3 視覺對齊用單一真實來源</a>。</p>
<hr>
<h2 id="設計工具">設計工具</h2>
<h3 id="1-定義集中引用分散">1. 定義集中、引用分散</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c">/* 集中定義 */</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nv">--search-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">--search-form-h</span><span class="p">:</span> <span class="mi">68</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">.</span><span class="nc">pagefind-ui__drawer</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="c">/* 分散引用 */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="k">margin-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">title</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">search</span><span class="o">-</span><span class="n">form</span><span class="o">-</span><span class="n">h</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「集中定義」= fact 一個住址；「分散引用」= derivation 不重新宣告 fact。</p>
<h3 id="2-命名前綴標明範圍">2. 命名前綴標明範圍</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">--token-</span><span class="o">*</span>           <span class="c">/* 全站 design token */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">--page-search-</span><span class="o">*</span>     <span class="c">/* 搜尋頁專用 */</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">--pagefind-ui-</span><span class="o">*</span>     <span class="c">/* 組件 hook */</span></span></span></code></pre></div><p>前綴讓維護者一眼看出值的「歸屬」 — 改的時候知道影響範圍、不會誤改別處。</p>
<h3 id="3-js-寫入跟-css-定義同-selector">3. JS 寫入跟 CSS 定義同 selector</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span>  <span class="c">/* fallback */</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">h</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 寫到 body.style、跟 CSS 定義同 selector、cascade 一致
</span></span></span></code></pre></div><p>JS 寫入位置跟 CSS fallback 在同一 selector — 兩套機制保持一致來源。</p>
<h3 id="4-用-calc-表達-derivation不寫具體數字">4. 用 calc 表達 derivation、不寫具體數字</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* 好 — derivation 用 calc */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">scope-position</span> <span class="p">{</span> <span class="k">top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">a</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">b</span><span class="p">));</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c">/* 較差 — derivation 寫具體數字 */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">.</span><span class="nc">scope-position</span> <span class="p">{</span> <span class="k">top</span><span class="p">:</span> <span class="mi">152</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>  <span class="c">/* 152 是 a + b 算出來的、現在固化在這裡 */</span></span></span></code></pre></div><p><code>calc</code> 把 derivation 顯式表達 — 未來 fact 改了、derivation 自動跟上。</p>
<hr>
<h2 id="不該套用-ssot-的情境">不該套用 SSoT 的情境</h2>
<p>跟其他原則一樣、SSoT 也有適用邊界：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼可以多源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨系統的紀錄（DB + cache）</td>
          <td>多源是效能 / 可用性的設計、有顯式同步機制</td>
      </tr>
      <tr>
          <td>跨服務的 reference data</td>
          <td>微服務各自存一份是常態、有 eventual consistency</td>
      </tr>
      <tr>
          <td>國際化字串（en + zh-TW）</td>
          <td>各語言版本是「不同 fact」、不是同一 fact 的多源</td>
      </tr>
      <tr>
          <td>開發 / production 環境的設定</td>
          <td>各環境是「不同 fact」、不是同源 drift</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：多源是不是「同一 fact 的多份拷貝」？是 → 違反 SSoT；不是 → 各自是獨立 fact、不違反。</p>
<hr>
<h2 id="跟其他抽象原則的關係">跟其他抽象原則的關係</h2>
<p><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a> 處理「範圍越窄越穩定」、本原則處理「值的住址越唯一越穩定」。兩者方向不同、目的相同 — 都是讓行為可預測：</p>
<ul>
<li><strong>最小必要範圍</strong>：縮影響範圍、避免誤命中</li>
<li><strong>SSoT</strong>：縮值的來源數、避免分歧</li>
</ul>
<p>兩者經常同時出現：縮 selector 範圍時、selector 本身的定義也該 SSoT（避免散在多處）。</p>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<p>每篇示範這個原則在不同議題的應用：</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>SSoT 違反類型</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../visual-alignment-single-source-of-truth/">#3 視覺對齊用單一真實來源</a></td>
          <td>對齊基準分量值來源不明</td>
          <td>每分量都要有明確住址</td>
      </tr>
      <tr>
          <td><a href="../css-variable-single-location/">#26 CSS 變數定義位置統一</a></td>
          <td>變數定義散多 selector</td>
          <td>集中在使用範圍的最高層</td>
      </tr>
      <tr>
          <td><a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a></td>
          <td>寫死跟量測混搭</td>
          <td>選一邊走到底</td>
      </tr>
  </tbody>
</table>
<p>讀的時候從本篇出發、依議題挑實作篇。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>自問</th>
          <th>回應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「改一個 token 要 grep 找定義位置」</td>
          <td>定義是否散落？</td>
          <td>是 → 集中到一處</td>
      </tr>
      <tr>
          <td>「不知道哪個值生效」</td>
          <td>來源是否多源？</td>
          <td>是 → 找出多源、保留一個權威來源</td>
      </tr>
      <tr>
          <td>「我估的值跟實際差 2px」</td>
          <td>該值是否該量測或從 fact 算？</td>
          <td>是 → 補真相位置、不用估算</td>
      </tr>
      <tr>
          <td>「兩處數值看起來該一致、實際分歧了」</td>
          <td>是否多源 drift？</td>
          <td>是 → 抽出共同 fact、各處引用</td>
      </tr>
      <tr>
          <td>「複製這個值過來改一下」</td>
          <td>寫多源前自問？</td>
          <td>警訊 → 抽 token、不要複製</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：SSoT 守的是「未來改值時、知道改哪裡」 — 寫的時候多想一秒、未來改的時候少痛半天。多源是時間維度的隱形成本、寫程式當下看不出來、是因為時間還沒到。</p>
<p>延伸到 stream 操作：<a href="../compose-feature-at-source-layer/">#64 Feature 操作要跟 Source 同層合成</a> 是本原則在 stream 領域的應用 — 在下游做 filter / sort = 等於建了個第二定義（subset 上的「filter 結果」）跟 stream 全集競爭、是另一種形式的 SSoT 違反。多源便利（就地寫個值）、單源對齊（找 fact 位置）— 這個反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
<p>延伸到 UI state：<a href="../url-as-state-container/">#70 URL 是 stateful UI 的儲存層</a> 是本原則在「可分享 state」的應用 — URL 是該類 state 的 SSOT、不寫 URL = state 多源（in-memory + 使用者期望的 URL 但實際不存在）。</p>
]]></content:encoded></item><item><title>跟外部組件合作的層次：離介面越近、合作越穩</title><link>https://tarrragon.github.io/blog/report/external-component-collaboration-layers/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-component-collaboration-layers/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製外部組件的穩定性與「離組件作者保證的對外介面多遠」成反比。&lt;/strong> 客製貼著介面做、跟組件作者站在同一邊、組件升級時客製不會打到；客製挖到組件內部實作、跟作者每個版本對抗、依賴前提隨時可能崩潰。理解四層的代價差異、是「跟外部組件合作」這件事的工程基礎。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼有層次這個概念">為什麼有「層次」這個概念&lt;/h2>
&lt;h3 id="組件--對外契約--內部實作">組件 = 對外契約 + 內部實作&lt;/h3>
&lt;p>任何外部組件可以分成兩部分：&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>對外契約&lt;/td>
 &lt;td>CLI 參數、props、CSS class hook、CSS variable hook、event 介面&lt;/td>
 &lt;td>跨版本相容（major 升級可能 break）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部實作&lt;/td>
 &lt;td>內部 DOM 結構、private function、framework hash class、自家 CSS specificity&lt;/td>
 &lt;td>不保證、隨時可能變動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對外契約是「跟使用者的合約」、內部實作是「達成契約的手段」。客製貼著契約做 = 在合約範圍內、作者會維持；挖內部 = 跨越合約、作者沒義務維持。&lt;/p>
&lt;h3 id="層次反映依賴強度">「層次」反映依賴強度&lt;/h3>
&lt;p>從合約走向內部、依賴強度遞增：&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">最穩 ─────────────────────────────────────────── 最不穩
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">介面層 → 鄰接層 → 邊界內 DOM → 內部邏輯&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不是「能不能做」、是「做了之後依賴什麼會不會變」。介面層依賴公開契約（穩）、內部邏輯依賴 source code（隨時變）。&lt;/p>
&lt;hr>
&lt;h2 id="四個層次">四個層次&lt;/h2>
&lt;h3 id="第-1-層介面最穩">第 1 層：介面（最穩）&lt;/h3>
&lt;p>&lt;strong>內容&lt;/strong>：組件作者公開設計的客製機制。&lt;/p>
&lt;ul>
&lt;li>CLI flag（如 &lt;code>--root-selector&lt;/code>）&lt;/li>
&lt;li>Props / config（如 &lt;code>pageSize: 10&lt;/code>）&lt;/li>
&lt;li>CSS variable hook（如 &lt;code>--component-color&lt;/code>）&lt;/li>
&lt;li>官方提供的 event / callback&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>穩定性&lt;/strong>：作者保證跨版本（minor / patch 升級不 break）。&lt;/p>
&lt;p>&lt;strong>例&lt;/strong>：Pagefind 的 &lt;code>--root-selector main&lt;/code> — 用作者設計的索引邊界客製。&lt;/p>
&lt;p>&lt;strong>升級成本&lt;/strong>：通常零 — 作者改實作不影響介面。&lt;/p>
&lt;h3 id="第-2-層鄰接半穩">第 2 層：鄰接（半穩）&lt;/h3>
&lt;p>&lt;strong>內容&lt;/strong>：組件邊界元素的可辨識特徵 — class name、id、CSS reset 邊界。&lt;/p>
&lt;p>&lt;strong>穩定性&lt;/strong>：作者通常維持、但不像介面那麼正式。Major 版本可能改名。&lt;/p>
&lt;p>&lt;strong>例&lt;/strong>：Pagefind 的 &lt;code>.pagefind-ui--reset&lt;/code> class 邊界 — 不是官方介面、但 &lt;code>.pagefind-ui&lt;/code> 這個 class 名相對穩定。&lt;/p>
&lt;p>&lt;strong>升級成本&lt;/strong>：低-中 — class 改名才會打到、通常會在 release note 提及。&lt;/p>
&lt;h3 id="第-3-層邊界內-dom不穩">第 3 層：邊界內 DOM（不穩）&lt;/h3>
&lt;p>&lt;strong>內容&lt;/strong>：組件內部的 DOM 結構、framework 生成的 hash class、子節點的相對位置。&lt;/p>
&lt;p>&lt;strong>穩定性&lt;/strong>：隨 framework 渲染週期 / 版本變動。&lt;/p>
&lt;p>&lt;strong>例&lt;/strong>：Pagefind 的 &lt;code>.svelte-yyy&lt;/code> hash class、&lt;code>.pagefind-ui__drawer&lt;/code> 在 form 內部的層級結構。&lt;/p>
&lt;p>&lt;strong>升級成本&lt;/strong>：高 — framework 升級可能立即打破。&lt;/p>
&lt;h3 id="第-4-層內部邏輯最不穩">第 4 層：內部邏輯（最不穩）&lt;/h3>
&lt;p>&lt;strong>內容&lt;/strong>：組件 source code 的行為、private function、內部 state 流。&lt;/p>
&lt;p>&lt;strong>穩定性&lt;/strong>：完全不保證。&lt;/p>
&lt;p>&lt;strong>例&lt;/strong>：fork 組件改一個 function、monkey-patch 一個 private method。&lt;/p>
&lt;p>&lt;strong>升級成本&lt;/strong>：每次升級都要重新 merge / patch。&lt;/p>
&lt;hr>
&lt;h2 id="每往內一層的代價">每往內一層的代價&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>依賴前提&lt;/th>
 &lt;th>升級風險&lt;/th>
 &lt;th>可逆性&lt;/th>
 &lt;th>客製成本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>介面&lt;/td>
 &lt;td>公開契約&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>高 — 改參數即還原&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>鄰接&lt;/td>
 &lt;td>邊界元素辨識特徵&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中 — 改 selector&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>邊界內 DOM&lt;/td>
 &lt;td>內部結構穩定&lt;/td>
 &lt;td>高 — 渲染週期可能即時打破&lt;/td>
 &lt;td>低 — 客製跟內部結構深耦合&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部邏輯&lt;/td>
 &lt;td>source code&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;td>最低 — 重新 merge&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四個維度全部隨層級遞增。&lt;strong>沒有「往內推一層、某個維度反而變好」的情境&lt;/strong> — 完全單向。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製外部組件的穩定性與「離組件作者保證的對外介面多遠」成反比。</strong> 客製貼著介面做、跟組件作者站在同一邊、組件升級時客製不會打到；客製挖到組件內部實作、跟作者每個版本對抗、依賴前提隨時可能崩潰。理解四層的代價差異、是「跟外部組件合作」這件事的工程基礎。</p>
<hr>
<h2 id="為什麼有層次這個概念">為什麼有「層次」這個概念</h2>
<h3 id="組件--對外契約--內部實作">組件 = 對外契約 + 內部實作</h3>
<p>任何外部組件可以分成兩部分：</p>
<table>
  <thead>
      <tr>
          <th>部分</th>
          <th>內容</th>
          <th>作者保證</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對外契約</td>
          <td>CLI 參數、props、CSS class hook、CSS variable hook、event 介面</td>
          <td>跨版本相容（major 升級可能 break）</td>
      </tr>
      <tr>
          <td>內部實作</td>
          <td>內部 DOM 結構、private function、framework hash class、自家 CSS specificity</td>
          <td>不保證、隨時可能變動</td>
      </tr>
  </tbody>
</table>
<p>對外契約是「跟使用者的合約」、內部實作是「達成契約的手段」。客製貼著契約做 = 在合約範圍內、作者會維持；挖內部 = 跨越合約、作者沒義務維持。</p>
<h3 id="層次反映依賴強度">「層次」反映依賴強度</h3>
<p>從合約走向內部、依賴強度遞增：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">最穩 ─────────────────────────────────────────── 最不穩
</span></span><span class="line"><span class="ln">2</span><span class="cl">介面層 → 鄰接層 → 邊界內 DOM → 內部邏輯</span></span></code></pre></div><p>不是「能不能做」、是「做了之後依賴什麼會不會變」。介面層依賴公開契約（穩）、內部邏輯依賴 source code（隨時變）。</p>
<hr>
<h2 id="四個層次">四個層次</h2>
<h3 id="第-1-層介面最穩">第 1 層：介面（最穩）</h3>
<p><strong>內容</strong>：組件作者公開設計的客製機制。</p>
<ul>
<li>CLI flag（如 <code>--root-selector</code>）</li>
<li>Props / config（如 <code>pageSize: 10</code>）</li>
<li>CSS variable hook（如 <code>--component-color</code>）</li>
<li>官方提供的 event / callback</li>
</ul>
<p><strong>穩定性</strong>：作者保證跨版本（minor / patch 升級不 break）。</p>
<p><strong>例</strong>：Pagefind 的 <code>--root-selector main</code> — 用作者設計的索引邊界客製。</p>
<p><strong>升級成本</strong>：通常零 — 作者改實作不影響介面。</p>
<h3 id="第-2-層鄰接半穩">第 2 層：鄰接（半穩）</h3>
<p><strong>內容</strong>：組件邊界元素的可辨識特徵 — class name、id、CSS reset 邊界。</p>
<p><strong>穩定性</strong>：作者通常維持、但不像介面那麼正式。Major 版本可能改名。</p>
<p><strong>例</strong>：Pagefind 的 <code>.pagefind-ui--reset</code> class 邊界 — 不是官方介面、但 <code>.pagefind-ui</code> 這個 class 名相對穩定。</p>
<p><strong>升級成本</strong>：低-中 — class 改名才會打到、通常會在 release note 提及。</p>
<h3 id="第-3-層邊界內-dom不穩">第 3 層：邊界內 DOM（不穩）</h3>
<p><strong>內容</strong>：組件內部的 DOM 結構、framework 生成的 hash class、子節點的相對位置。</p>
<p><strong>穩定性</strong>：隨 framework 渲染週期 / 版本變動。</p>
<p><strong>例</strong>：Pagefind 的 <code>.svelte-yyy</code> hash class、<code>.pagefind-ui__drawer</code> 在 form 內部的層級結構。</p>
<p><strong>升級成本</strong>：高 — framework 升級可能立即打破。</p>
<h3 id="第-4-層內部邏輯最不穩">第 4 層：內部邏輯（最不穩）</h3>
<p><strong>內容</strong>：組件 source code 的行為、private function、內部 state 流。</p>
<p><strong>穩定性</strong>：完全不保證。</p>
<p><strong>例</strong>：fork 組件改一個 function、monkey-patch 一個 private method。</p>
<p><strong>升級成本</strong>：每次升級都要重新 merge / patch。</p>
<hr>
<h2 id="每往內一層的代價">每往內一層的代價</h2>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>依賴前提</th>
          <th>升級風險</th>
          <th>可逆性</th>
          <th>客製成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>介面</td>
          <td>公開契約</td>
          <td>低</td>
          <td>高 — 改參數即還原</td>
          <td>低</td>
      </tr>
      <tr>
          <td>鄰接</td>
          <td>邊界元素辨識特徵</td>
          <td>中</td>
          <td>中 — 改 selector</td>
          <td>中</td>
      </tr>
      <tr>
          <td>邊界內 DOM</td>
          <td>內部結構穩定</td>
          <td>高 — 渲染週期可能即時打破</td>
          <td>低 — 客製跟內部結構深耦合</td>
          <td>高</td>
      </tr>
      <tr>
          <td>內部邏輯</td>
          <td>source code</td>
          <td>最高</td>
          <td>最低 — 重新 merge</td>
          <td>最高</td>
      </tr>
  </tbody>
</table>
<p>四個維度全部隨層級遞增。<strong>沒有「往內推一層、某個維度反而變好」的情境</strong> — 完全單向。</p>
<h3 id="為什麼工程師仍會挖內部">為什麼工程師仍會挖內部</h3>
<p>知道有代價、為什麼仍會挖？通常因為：</p>
<table>
  <thead>
      <tr>
          <th>動機</th>
          <th>為什麼錯誤</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「介面不夠用」</td>
          <td>通常是沒找全 — 介面比想像中多</td>
      </tr>
      <tr>
          <td>「直接挖比較快」</td>
          <td>第一次快、之後每次升級慢</td>
      </tr>
      <tr>
          <td>「現在能動就好」</td>
          <td>沒考慮升級成本</td>
      </tr>
      <tr>
          <td>「組件不會更新」</td>
          <td>通常會更新、只是時機不可控</td>
      </tr>
  </tbody>
</table>
<p>挖內部的真實成本在升級時顯現、寫的當下看不出來。</p>
<hr>
<h2 id="三類常見的想做但成本高情境">三類常見的「想做但成本高」情境</h2>
<h3 id="情境-a覆寫組件-specificity">情境 A：覆寫組件 specificity</h3>
<p>組件用 hash class（<code>.x.svelte-y.svelte-y</code>）把 specificity 拉到 30、自家 CSS 蓋不過。</p>
<p><strong>第 3 層做法（不穩）</strong>：寫 <code>.x.x</code> 雙寫、加 <code>!important</code>、跟組件 specificity 對抗。</p>
<p><strong>跳出層級的做法</strong>：用 CSS Layers 把組件 CSS 包進 layer、自家 CSS 留 unlayered — 跳出 specificity 線性比較戰場。</p>
<p>對應實作：<a href="../css-layers-over-specificity/">#24 CSS Layers 取代 specificity 戰</a>。</p>
<p><strong>通則</strong>：當「往內挖」會無限升級成本、找「跳出比較維度」的機制（layers / shadow DOM / portal）。</p>
<h3 id="情境-b在-framework-管的-dom-內注入元素">情境 B：在 framework 管的 DOM 內注入元素</h3>
<p>想在組件內部塞一個自家 UI element — framework 重繪時清掉、需要 observer 補打、跟渲染週期競爭。</p>
<p><strong>第 3 層做法（不穩）</strong>：注入 + observer 補打 + 跟 framework re-render 賽跑。</p>
<p><strong>留在邊界外的做法</strong>：把客製 UI 留在 framework 邊界外、用 CSS（absolute、margin spacer）控制視覺位置 — 不進入 framework 的 children list。</p>
<p>對應實作：<a href="../coexisting-with-framework-managed-dom/">#5 與 framework-managed DOM 共處</a>。</p>
<p><strong>通則</strong>：客製跟 framework 各自有 DOM 邊界、要共存就用 CSS 控制位置、不要互相侵入。</p>
<h3 id="情境-c覆寫深度成本累積">情境 C：覆寫深度成本累積</h3>
<p>要做的覆寫需要對抗 UA + 跨瀏覽器 + framework 三層、寫 5+ 條 CSS 才完成、改善的 UX 價值小。</p>
<p><strong>第 3+4 層做法（不穩）</strong>：硬寫到底、最後產出脆弱客製。</p>
<p><strong>事先告知 + 接受原設計的做法</strong>：開工前報告成本（需要寫多少、跨多少瀏覽器、有什麼殘留風險）、讓決策者用「成本 vs 改善價值」判斷。常常結論是「接受原設計」。</p>
<p>對應實作：<a href="../override-depth-cost-report/">#19 覆寫深度的成本告知</a>。</p>
<p><strong>通則</strong>：客製深度要事先評估、不要默默承擔。「接受原設計」是合理選項。</p>
<hr>
<h2 id="設計工具">設計工具</h2>
<h3 id="工具-1邊界辨識先於改動">工具 1：邊界辨識先於改動</h3>
<p>寫客製之前、先列出組件提供的邊界：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">組件邊界清單：
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 索引邊界：--root-selector （介面層）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 重置邊界：.pagefind-ui--reset class （鄰接層）
</span></span><span class="line"><span class="ln">4</span><span class="cl">- specificity 邊界：svelte hash class （邊界內 DOM 層）
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 樣式 hook：CSS variables （介面層、若有）</span></span></code></pre></div><p>對應每個客製需求、找最外層能滿足的邊界 — 介面夠用就用介面、不夠才推到鄰接、再不夠才考慮邊界內。</p>
<h3 id="工具-2跳維度的機制">工具 2：跳維度的機制</h3>
<p>當「同層對抗」會無限升級、找「跳到不同維度」的機制：</p>
<table>
  <thead>
      <tr>
          <th>同層對抗</th>
          <th>跳維度的機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Specificity 數字戰</td>
          <td>CSS Layers（分組權重）</td>
      </tr>
      <tr>
          <td>Framework children 競爭</td>
          <td>CSS 控制位置（不進 children）</td>
      </tr>
      <tr>
          <td>DOM 結構深耦合</td>
          <td>Shadow DOM / portal（隔離）</td>
      </tr>
      <tr>
          <td>樣式覆寫戰</td>
          <td>CSS-in-JS scope / namespace</td>
      </tr>
  </tbody>
</table>
<p>跳維度通常是一次性設計成本、之後免疫於同層的累積成本。</p>
<h3 id="工具-3覆寫成本告知-protocol">工具 3：覆寫成本告知 protocol</h3>
<p>開工前評估三個累積層：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>評估問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UA 預設</td>
          <td>跨瀏覽器有差異嗎？需要幾種 pseudo？</td>
      </tr>
      <tr>
          <td>Framework specificity</td>
          <td>需要 layers / important / 雙寫嗎？</td>
      </tr>
      <tr>
          <td>Framework 渲染週期</td>
          <td>改了會被 reset 嗎？需要 observer 補打嗎？</td>
      </tr>
  </tbody>
</table>
<p>任一層需要對抗、把成本攤開讓使用者決定值不值得做。</p>
<hr>
<h2 id="何時接受原設計不打覆寫戰">何時接受原設計、不打覆寫戰</h2>
<p>「接受原設計」常被當作放棄、實際上是評估後的合理選擇。判讀條件：</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>接受原設計的訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改善的使用者價值低（純視覺微調）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>實作累積三層成本（UA + 跨瀏覽器 + framework）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>覆寫深度在第 3 層以上（邊界內 DOM 或內部邏輯）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>沒有跳維度的機制可用</td>
          <td>是</td>
      </tr>
      <tr>
          <td>寫了 5+ 條 CSS 還沒蓋過</td>
          <td>是</td>
      </tr>
  </tbody>
</table>
<p>四個條件中三個符合 — 強烈建議接受原設計。</p>
<hr>
<h2 id="不該套用貼著邊界的情境">不該套用「貼著邊界」的情境</h2>
<p>這條原則有適用邊界、不是所有客製都能停在介面層：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼可以挖內部</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Fork 組件作為內部維護版本（不再升級上游）</td>
          <td>已經跟原組件分離、沒有升級成本</td>
      </tr>
      <tr>
          <td>組件已停止維護、必須自行接手</td>
          <td>上游不更新、internal 跟 external 等價</td>
      </tr>
      <tr>
          <td>為組件作者貢獻 PR</td>
          <td>是改 source code、不是覆寫</td>
      </tr>
      <tr>
          <td>學習用途</td>
          <td>不在乎升級、想理解內部</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：<strong>這個客製要跟組件升級共存嗎？</strong> 是 → 貼邊界；否 → 怎麼做都行。</p>
<hr>
<h2 id="跟其他抽象原則的關係">跟其他抽象原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象原則</th>
          <th>跟本原則的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>跟外部組件合作時、客製範圍也該最小必要 — 兩者疊加</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSoT</a></td>
          <td>組件提供的 hook（CSS variable）是 fact 的一個來源、自家 token 應該對齊到組件 hook</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>同一個覆寫戰打第 2 次失敗 = 該換維度（從同層對抗跳到 layers）</td>
      </tr>
  </tbody>
</table>
<p>跟外部組件合作的設計、通常需要同時應用多條原則。</p>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<p>每篇示範這個原則在不同情境的應用：</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>對應層次議題</th>
          <th>焦點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-customization/">#1 在外部組件上加客製功能：以邊界為中心</a></td>
          <td>邊界辨識</td>
          <td>索引 / 重置 / specificity 三邊界的辨識與選擇</td>
      </tr>
      <tr>
          <td><a href="../coexisting-with-framework-managed-dom/">#5 與 framework-managed DOM 共處</a></td>
          <td>邊界內 DOM 的隔離</td>
          <td>客製 UI 留邊界外、CSS 控制位置</td>
      </tr>
      <tr>
          <td><a href="../override-depth-cost-report/">#19 覆寫深度的成本告知</a></td>
          <td>多層覆寫的成本管理</td>
          <td>開工前報成本、讓使用者決定</td>
      </tr>
      <tr>
          <td><a href="../css-layers-over-specificity/">#24 CSS Layers 取代 specificity 戰</a></td>
          <td>跳出層級對抗</td>
          <td>用 layers 跳出 specificity 線性比較</td>
      </tr>
  </tbody>
</table>
<p>讀的時候從本篇出發、依情境挑實作篇。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>自問</th>
          <th>回應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「這個組件介面不夠用」</td>
          <td>真的找全了嗎？官方 hook / config / event 都看過？</td>
          <td>通常沒找全 → 補找</td>
      </tr>
      <tr>
          <td>「再加一條 <code>!important</code> 就好」</td>
          <td>是不是已經在第 3 層對抗了？</td>
          <td>是 → 跳維度（layers）</td>
      </tr>
      <tr>
          <td>「在組件內塞個 div」</td>
          <td>會不會被 framework 重繪清掉？</td>
          <td>是 → 留邊界外、用 CSS 定位</td>
      </tr>
      <tr>
          <td>「為了這個小視覺改善寫 5 條 CSS」</td>
          <td>改善價值 vs 成本對得起來嗎？</td>
          <td>否 → 接受原設計</td>
      </tr>
      <tr>
          <td>「組件升級後客製失效」</td>
          <td>客製深度是不是太深？</td>
          <td>是 → 重寫到淺層</td>
      </tr>
      <tr>
          <td>「fork 組件改 function」</td>
          <td>升級成本能承擔嗎？</td>
          <td>否 → 找介面層做法、或正式 fork 為內部版本</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：跟外部組件合作的工程基礎是「層次認知」 — 先辨識能做的範圍、選最外層能滿足需求的位置、跳維度而非同層對抗、必要時接受原設計。「組件不能客製成這樣」常常是「該層做不到、要往外或跳維度找」、不是真的不能。</p>
<p>跟 <a href="../filter-source-composition-strategies/">#59 Filter × Source 合成策略</a> 同構：本卡的「四層合作」跟 #59 的「五策略」都是「離 source 公共介面越近、合作越穩」— 介面層 ≈ 推進 query (A)、邊界層 ≈ 多 index (C)、邊界 DOM ≈ 自動續抓 (B)、內部結構 ≈ 接受 D / E。同個原則套用在「客製 UI vs 客製 filter」兩個情境。</p>
]]></content:encoded></item><item><title>寫作便利度跟意圖對齊反相關</title><link>https://tarrragon.github.io/blog/report/ease-of-writing-vs-intent-alignment/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/ease-of-writing-vs-intent-alignment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>寫程式時最容易寫出的版本、通常是離意圖最遠的版本。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變數&lt;/th>
 &lt;th>寫作便利度高的特徵&lt;/th>
 &lt;th>意圖對齊高的特徵&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>起點&lt;/td>
 &lt;td>用現成的 context / API&lt;/td>
 &lt;td>找到正確的層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範圍&lt;/td>
 &lt;td>寬（捕魚式撈一遍）&lt;/td>
 &lt;td>窄（精準命中）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作位置&lt;/td>
 &lt;td>下游（已 materialize）&lt;/td>
 &lt;td>上游（stream / source）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>認知負擔&lt;/td>
 &lt;td>低（就地能解）&lt;/td>
 &lt;td>中-高（要回到上層分析）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Silent 風險&lt;/td>
 &lt;td>高（看起來能用）&lt;/td>
 &lt;td>低（強制處理邊界）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩個方向反相關 — &lt;strong>越容易寫、越容易錯位&lt;/strong>。識別這個反相關 = 識別自己正在掉進「容易寫的陷阱」、不是寫出對的東西。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼便利度跟正確性反向">為什麼便利度跟正確性反向&lt;/h2>
&lt;h3 id="便利度的來源">便利度的來源&lt;/h3>
&lt;p>寫程式當下、能「快速寫出」的條件是：&lt;/p>
&lt;ul>
&lt;li>手邊已經有需要的資料（已 fetch、已 render、已 materialize）&lt;/li>
&lt;li>現成的 API 能直接呼叫（&lt;code>document.querySelectorAll&lt;/code>、&lt;code>Array.from&lt;/code>、&lt;code>results.filter&lt;/code>）&lt;/li>
&lt;li>不需要跨抽象層（不用回到 source / framework 邊界 / build pipeline）&lt;/li>
&lt;/ul>
&lt;p>這些條件都建立在「&lt;strong>已是 subset / 已展開 / 已下游&lt;/strong>」的位置 — 因為下游才有「現成上下文」。&lt;/p>
&lt;h3 id="意圖對齊的代價">意圖對齊的代價&lt;/h3>
&lt;p>「跟使用者意圖對齊」的條件相反：&lt;/p>
&lt;ul>
&lt;li>操作 stream 全集（不是 subset）&lt;/li>
&lt;li>在 source 層處理（不是 view 層）&lt;/li>
&lt;li>處理 build-time 抽象（不是 runtime 取巧）&lt;/li>
&lt;/ul>
&lt;p>這些條件要求&lt;strong>回到上游 / 跨抽象層 / 處理沒被 materialize 的東西&lt;/strong> — 而上游沒有「現成上下文」、需要刻意建立。&lt;/p>
&lt;h3 id="反相關的本質">反相關的本質&lt;/h3>
&lt;p>便利度 = 用已有資訊；意圖對齊 = 處理還沒有的資訊。&lt;strong>資訊狀態相反 → 兩個目標反相關&lt;/strong>。&lt;/p>
&lt;p>「容易寫」這件事本身就是「在錯位的層」的徵兆。不是「容易寫的有時候錯」、是「容易寫的多半錯」。&lt;/p>
&lt;hr>
&lt;h2 id="多面向跨領域的同個結構">多面向：跨領域的同個結構&lt;/h2>
&lt;h3 id="面向-1filter-在-view-層55-的-case">面向 1：Filter 在 view 層（#55 的 case）&lt;/h3>
&lt;p>容易寫：&lt;code>document.querySelectorAll('.result').forEach(el =&amp;gt; el.hidden = !matches(el))&lt;/code> — 5 行、用現成 DOM。&lt;/p>
&lt;p>意圖對齊：把 filter 推到 source 層（&lt;a href="../pattern-query-side-pushdown/">#61&lt;/a>）— 改 SDK 呼叫、可能改 build。&lt;/p>
&lt;p>「為什麼層錯位的 bug 容易寫出來」見 &lt;a href="../view-layer-filter-vs-source-layer/">#55 Filter 與 Source 的層錯位&lt;/a>。&lt;/p>
&lt;h3 id="面向-2selector-用過寬範圍">面向 2：Selector 用過寬範圍&lt;/h3>
&lt;p>容易寫：&lt;code>document.querySelectorAll('.title')&lt;/code> — 一行命中所有 &lt;code>.title&lt;/code>。&lt;/p>
&lt;p>意圖對齊：&lt;code>document.querySelector('.pagefind-ui').querySelectorAll(':scope &amp;gt; .results &amp;gt; .result &amp;gt; .title')&lt;/code> — 起點 + 範圍 + 過濾顯式設計（&lt;a href="../dom-selector-precision/">#14&lt;/a> / &lt;a href="../minimum-necessary-scope-is-sanity-defense/">#43&lt;/a>）。&lt;/p>
&lt;p>過寬 selector 的代價是「命中無關元素 → 副作用未知」 — 但寫的時候不會看到。&lt;/p>
&lt;h3 id="面向-3inline-style--important">面向 3：Inline style + !important&lt;/h3>
&lt;p>容易寫：&lt;code>el.style.setProperty('display', 'none', 'important')&lt;/code> — 立刻生效。&lt;/p>
&lt;p>意圖對齊：&lt;code>el.classList.toggle('is-hidden')&lt;/code> + CSS class（&lt;a href="../class-toggle-over-important/">#28&lt;/a>）— 樣式留 CSS、JS 只 toggle state。&lt;/p>
&lt;p>Important 是「立刻生效」的便利、代價是「DevTools 看不出為什麼」、改視覺要 grep 多處。&lt;/p>
&lt;h3 id="面向-4middleware-filter後端-case">面向 4：Middleware filter（後端 case）&lt;/h3>
&lt;p>容易寫：在 API response 後加 filter middleware — 對 response array 做 &lt;code>.filter()&lt;/code>。&lt;/p>
&lt;p>意圖對齊：把 filter 推進 ORM query / SQL &lt;code>WHERE&lt;/code> — 改 query、可能加 index。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>寫程式時最容易寫出的版本、通常是離意圖最遠的版本。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>變數</th>
          <th>寫作便利度高的特徵</th>
          <th>意圖對齊高的特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>起點</td>
          <td>用現成的 context / API</td>
          <td>找到正確的層</td>
      </tr>
      <tr>
          <td>範圍</td>
          <td>寬（捕魚式撈一遍）</td>
          <td>窄（精準命中）</td>
      </tr>
      <tr>
          <td>操作位置</td>
          <td>下游（已 materialize）</td>
          <td>上游（stream / source）</td>
      </tr>
      <tr>
          <td>認知負擔</td>
          <td>低（就地能解）</td>
          <td>中-高（要回到上層分析）</td>
      </tr>
      <tr>
          <td>Silent 風險</td>
          <td>高（看起來能用）</td>
          <td>低（強制處理邊界）</td>
      </tr>
  </tbody>
</table>
<p>兩個方向反相關 — <strong>越容易寫、越容易錯位</strong>。識別這個反相關 = 識別自己正在掉進「容易寫的陷阱」、不是寫出對的東西。</p>
<hr>
<h2 id="為什麼便利度跟正確性反向">為什麼便利度跟正確性反向</h2>
<h3 id="便利度的來源">便利度的來源</h3>
<p>寫程式當下、能「快速寫出」的條件是：</p>
<ul>
<li>手邊已經有需要的資料（已 fetch、已 render、已 materialize）</li>
<li>現成的 API 能直接呼叫（<code>document.querySelectorAll</code>、<code>Array.from</code>、<code>results.filter</code>）</li>
<li>不需要跨抽象層（不用回到 source / framework 邊界 / build pipeline）</li>
</ul>
<p>這些條件都建立在「<strong>已是 subset / 已展開 / 已下游</strong>」的位置 — 因為下游才有「現成上下文」。</p>
<h3 id="意圖對齊的代價">意圖對齊的代價</h3>
<p>「跟使用者意圖對齊」的條件相反：</p>
<ul>
<li>操作 stream 全集（不是 subset）</li>
<li>在 source 層處理（不是 view 層）</li>
<li>處理 build-time 抽象（不是 runtime 取巧）</li>
</ul>
<p>這些條件要求<strong>回到上游 / 跨抽象層 / 處理沒被 materialize 的東西</strong> — 而上游沒有「現成上下文」、需要刻意建立。</p>
<h3 id="反相關的本質">反相關的本質</h3>
<p>便利度 = 用已有資訊；意圖對齊 = 處理還沒有的資訊。<strong>資訊狀態相反 → 兩個目標反相關</strong>。</p>
<p>「容易寫」這件事本身就是「在錯位的層」的徵兆。不是「容易寫的有時候錯」、是「容易寫的多半錯」。</p>
<hr>
<h2 id="多面向跨領域的同個結構">多面向：跨領域的同個結構</h2>
<h3 id="面向-1filter-在-view-層55-的-case">面向 1：Filter 在 view 層（#55 的 case）</h3>
<p>容易寫：<code>document.querySelectorAll('.result').forEach(el =&gt; el.hidden = !matches(el))</code> — 5 行、用現成 DOM。</p>
<p>意圖對齊：把 filter 推到 source 層（<a href="../pattern-query-side-pushdown/">#61</a>）— 改 SDK 呼叫、可能改 build。</p>
<p>「為什麼層錯位的 bug 容易寫出來」見 <a href="../view-layer-filter-vs-source-layer/">#55 Filter 與 Source 的層錯位</a>。</p>
<h3 id="面向-2selector-用過寬範圍">面向 2：Selector 用過寬範圍</h3>
<p>容易寫：<code>document.querySelectorAll('.title')</code> — 一行命中所有 <code>.title</code>。</p>
<p>意圖對齊：<code>document.querySelector('.pagefind-ui').querySelectorAll(':scope &gt; .results &gt; .result &gt; .title')</code> — 起點 + 範圍 + 過濾顯式設計（<a href="../dom-selector-precision/">#14</a> / <a href="../minimum-necessary-scope-is-sanity-defense/">#43</a>）。</p>
<p>過寬 selector 的代價是「命中無關元素 → 副作用未知」 — 但寫的時候不會看到。</p>
<h3 id="面向-3inline-style--important">面向 3：Inline style + !important</h3>
<p>容易寫：<code>el.style.setProperty('display', 'none', 'important')</code> — 立刻生效。</p>
<p>意圖對齊：<code>el.classList.toggle('is-hidden')</code> + CSS class（<a href="../class-toggle-over-important/">#28</a>）— 樣式留 CSS、JS 只 toggle state。</p>
<p>Important 是「立刻生效」的便利、代價是「DevTools 看不出為什麼」、改視覺要 grep 多處。</p>
<h3 id="面向-4middleware-filter後端-case">面向 4：Middleware filter（後端 case）</h3>
<p>容易寫：在 API response 後加 filter middleware — 對 response array 做 <code>.filter()</code>。</p>
<p>意圖對齊：把 filter 推進 ORM query / SQL <code>WHERE</code> — 改 query、可能加 index。</p>
<p>Middleware 在 pagination 之後、漏掉沒在這頁的符合項（<a href="../compose-feature-at-source-layer/">#64</a>）。</p>
<h3 id="面向-5cached-subset-上算統計">面向 5：Cached subset 上算統計</h3>
<p>容易寫：<code>stats.average = cache.values().reduce(...) / cache.size</code> — 直接用 cache。</p>
<p>意圖對齊：先 revalidate、再算；或標明「statistic on cached subset」（<a href="../pattern-explicit-semantic-narrowing/">#66</a>）。</p>
<p>Cache subset 算出的統計跟 fresh dataset 算出的不同、但寫的時候看不到差異。</p>
<p><strong>五個面向共用結構</strong>：用「已存在的東西」5 行解決、產出對「沒處理到的東西」silent 失敗的版本。</p>
<hr>
<h2 id="便利度的時間維度當下便利-vs-未來便利反向">便利度的時間維度：當下便利 vs 未來便利反向</h2>
<p>便利度有兩個尺度、方向相反：</p>
<table>
  <thead>
      <tr>
          <th>尺度</th>
          <th>什麼是便利</th>
          <th>對誰便利</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>當下便利</td>
          <td>用現成 context、5 行解決、不跨層</td>
          <td>寫的當下的我</td>
      </tr>
      <tr>
          <td>未來便利</td>
          <td>清楚的層次、明確的契約、可預測的行為</td>
          <td>五年後讀 code 的人</td>
      </tr>
  </tbody>
</table>
<p>「五年後讀 code 的人」包括五年後的自己 — 那時候不會記得當下為什麼選 view 層 filter、只會看到「為什麼這個 filter 漏掉了沒載入的東西」。</p>
<h3 id="為什麼兩個尺度反向">為什麼兩個尺度反向</h3>
<p>當下便利的條件是「<strong>用已存在的東西</strong>」：</p>
<ul>
<li>已 materialize 的資料（不用追上游）</li>
<li>已存在的 API（不用設計介面）</li>
<li>已有的命名（不用想新名字）</li>
</ul>
<p>未來便利的條件是「<strong>留下可預測的結構</strong>」：</p>
<ul>
<li>操作位置跟意圖對齊（不用 debug 為什麼結果怪）</li>
<li>抽象層清楚（不用穿三層才理解一行）</li>
<li>命名反映意圖（不用讀 commit history 才懂）</li>
</ul>
<p>兩個條件方向相反 — 用已存在的東西 = 順著當下慣性；留下可預測結構 = 抵抗當下慣性、為未來付出。</p>
<h3 id="我等下會-refactor是個謊言">「我等下會 refactor」是個謊言</h3>
<p>寫便利版時內心 OS 常常是「先這樣、晚點 refactor 補回來」 — 但補回來這件事在實務上幾乎不發生：</p>
<ul>
<li>Refactor 沒有功能訊號驅動（壞掉才修、能用不修）</li>
<li>重新理解當時為什麼這樣寫、需要把整個 context 重建一次（成本反而高）</li>
<li>寫的時候的決策已經影響了周邊代碼（要 refactor 一處要連帶改五處）</li>
</ul>
<p>所以「現在便利、未來再對齊」這個 plan 實際上是「現在便利、未來繼承這個錯位」。<strong>當下的選擇就是長期的選擇</strong>、沒有「之後補」這個選項。</p>
<p>要嘛當下對齊、要嘛接受 <a href="../pattern-explicit-semantic-narrowing/">#66 explicit 縮小</a> 把限制攤開。沒有第三條路。</p>
<hr>
<h2 id="識別訊號什麼時候你正掉進這個陷阱">識別訊號：什麼時候你正掉進這個陷阱</h2>
<h3 id="訊號-1這樣寫最快">訊號 1：「這樣寫最快」</h3>
<p>內心 OS「直接 forEach + filter 就好」「就用現成的 API 啊」 — 「最快 / 現成」這兩個詞通常標記下游 / subset 位置。</p>
<h3 id="訊號-2跨層的成本看起來高但本層解看起來夠">訊號 2：跨層的成本看起來高、但本層解看起來夠</h3>
<p>「為了一個 filter 改 build pipeline 太誇張了吧」「直接前端 filter 不就好了」 — 這個內心 OS 在錯估、因為下游解的 silent 風險不在當下顯露。</p>
<h3 id="訊號-3寫完手動測一次就過">訊號 3：寫完手動測一次就過</h3>
<p>第 1 次 happy path 過了、覺得對。但 happy path 過 = 子集裡有命中、不證明 stream 全集對齊。同 <a href="../two-occurrence-threshold/">#42 2 次門檻</a>：第 1 次成功是低資訊量訊號。</p>
<h3 id="訊號-4先這樣晚點補資料層">訊號 4：「先這樣、晚點補資料層」</h3>
<p>這個想法本身就是「我知道這寫法不對齊意圖、但便利度太高」 — 補不回來、會 ship 進 production silent 失敗。同 <a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a>。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「便利度跟意圖對齊反相關」這條原則在絕大多數開發情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純原型 / hackathon</td>
          <td>預期幾天後丟掉、未來便利根本沒有未來、便利優先合理</td>
      </tr>
      <tr>
          <td>一次性 throw-away script</td>
          <td>跑完就刪、不維護、寫完馬上產生價值、對齊成本沒回報</td>
      </tr>
      <tr>
          <td>探索性 spike</td>
          <td>目的是驗證可行性、不是建立可維護結構、便利對齊不是議題</td>
      </tr>
      <tr>
          <td>Code review 之前的 sketch</td>
          <td>寫出來是為了討論、不是 ship、之後會重寫</td>
      </tr>
  </tbody>
</table>
<p>這四類共同特徵：<strong>「未來便利」這個變數的權重 ≈ 0</strong> — 因為沒有未來（不會被讀、不會被改、不會被擴）。本原則的反相關建立在「未來便利有權重」上、權重 0 時自然不適用。</p>
<p>判讀：寫之前自問「這代碼三個月後會不會有人讀」 — 否 → 本原則可放寬；是 → 本原則嚴格適用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>「容易寫」是低資訊量訊號、跟「第 1 次成功」同類</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>寬範圍是便利、窄範圍是對齊 — 同個反相關</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>多源是便利（就地寫個值）、單源是對齊（找 fact 位置）</td>
      </tr>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 外部組件合作四層</a></td>
          <td>內部結構層便利、公共介面層對齊</td>
      </tr>
      <tr>
          <td><a href="../compose-feature-at-source-layer/">#64 同層合成</a></td>
          <td>下游合成便利、上游合成對齊</td>
      </tr>
  </tbody>
</table>
<p>本卡是這幾條的共同上位原則 — 它們都是「<strong>便利 vs 正確性的取捨</strong>」在不同情境的具體展現。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內心 OS：「這樣寫最快」「直接用現成 API」</td>
          <td>停 — 評估「快」是不是「在錯層」的徵兆</td>
      </tr>
      <tr>
          <td>5 行解決一個原本應該跨層的問題</td>
          <td>是 — 跨層通常 50+ 行、5 行是訊號</td>
      </tr>
      <tr>
          <td>跨層解的工程量看起來「不值得」</td>
          <td>注意 — 你可能在錯估 silent 風險的代價</td>
      </tr>
      <tr>
          <td>「先做、晚點補上游」</td>
          <td>補不回來、要嘛當下做、要嘛接受 explicit 縮小</td>
      </tr>
      <tr>
          <td>寫完 happy path 一次就過</td>
          <td>補規模 / 稀疏 / 跨情境驗證</td>
      </tr>
      <tr>
          <td>程式跑得通、但你說不出為什麼這個位置是對的</td>
          <td>這是「便利驅動」而不是「意圖驅動」的訊號</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：寫程式當下的便利度跟正確性反相關、是因為兩者用的資訊狀態相反。識別「我現在在容易的位置」 = 識別「我可能在錯的層」。<strong>便利度本身是個診斷訊號</strong>、不是好東西。</p>
<p>延伸到測試驗證：跳過 RED 階段（不切 branch / 不重 build / 不在 buggy code 上跑測試）是便利、走 RED-GREEN 是對齊。詳見 <a href="../test-first-red-before-green/">#69 Test-First：先看到 RED 才相信 GREEN</a>。</p>
<h3 id="self-case本系統建立過程的便利驅動失敗">Self-case：本系統建立過程的便利驅動失敗</h3>
<p>修 <a href="../view-layer-filter-vs-source-layer/">#55 search bug</a> 時、跳過了 <a href="../verification-timeline-checkpoints/">#68 Checkpoint 1</a>（列使用者意圖完整集合）— 因為 Checkpoint 1 沒便利路徑（要刻意停下 5 分鐘想），直接從 bug 描述進策略選擇。完工後 retrospective 才發現漏了 3 個 silent 缺口（URL state / tab order / filter UI hint）。</p>
<p>對應本卡：「沒寫 Checkpoint 1 list 是當下便利、補完整意圖才是對齊」。我修了便利版（直接修 bug）、漏掉的 3 個案例之後才被 retrospective 抓到、又花一輪迭代回頭做。<strong>便利驅動的代價、就是事後要做兩次</strong>。</p>
<p>「<a href="#%e6%88%91%e7%ad%89%e4%b8%8b%e6%9c%83-refactor-%e6%98%af%e5%80%8b%e8%ac%8a%e8%a8%80">#67 Refactor 是個謊言</a>」延伸版：「之後做 Checkpoint 1」也是個謊言 — 動手之後 context 已經跑完、回頭重列意圖完整集成本反而高。要嘛當下做、要嘛接受漏案例。</p>
<p>更上位的解釋見 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發的工作會被結構性跳過</a> — 本卡是 #72 在「寫程式當下選哪條路」面向的展現。</p>
]]></content:encoded></item><item><title>Test-First：先看到 RED 才相信 GREEN</title><link>https://tarrragon.github.io/blog/report/test-first-red-before-green/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/test-first-red-before-green/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>測試本身需要被驗證。&lt;/strong> 一個從沒看過 RED 的測試 = 未驗證的訊號、不是「會抓回歸的測試」。&lt;/p>
&lt;p>驗證一個測試真的有用、需要看到兩個訊號：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>RED&lt;/strong>：測試在「該失敗的版本」上失敗（buggy code → 紅）&lt;/li>
&lt;li>&lt;strong>GREEN&lt;/strong>：測試在「該通過的版本」上通過（fixed code → 綠）&lt;/li>
&lt;/ol>
&lt;p>只看過 GREEN = 不知道測試有沒有 catch 能力；只看過 RED = 不知道修復有沒有真的解問題。&lt;strong>兩個都看到 = 測試 + 修復都被驗證&lt;/strong>。&lt;/p>
&lt;p>跳過 RED 把驗收標準降到「測試跑得通」、漏掉「測試自己有沒有 bug」這層。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼測試需要被驗證">為什麼測試需要被驗證&lt;/h2>
&lt;h3 id="測試是程式-about-程式會有-bug">測試是程式 about 程式、會有 bug&lt;/h3>
&lt;p>測試本身是程式碼、跟其他程式碼一樣會有 bug：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>測試 bug 類型&lt;/th>
 &lt;th>症狀&lt;/th>
 &lt;th>為什麼跳過 RED 看不到&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Selector 寫錯&lt;/td>
 &lt;td>永遠抓不到目標元素、assertion always 過&lt;/td>
 &lt;td>GREEN（因為沒 assert 到任何東西）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Assertion 太寬&lt;/td>
 &lt;td>&lt;code>expect(x).toBeDefined()&lt;/code> 對 buggy / fixed 都過&lt;/td>
 &lt;td>GREEN（assertion 通過範圍太大）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Setup / fixture 錯&lt;/td>
 &lt;td>測試根本沒跑、報告假性綠&lt;/td>
 &lt;td>GREEN（測試被 skip 但沒人注意）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Race condition / 時機錯&lt;/td>
 &lt;td>Buggy 時剛好在 race window 過、fixed 時也過&lt;/td>
 &lt;td>GREEN（取決於非常規 case）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試對象選錯&lt;/td>
 &lt;td>測 happy path、bug 在邊界&lt;/td>
 &lt;td>GREEN（沒覆蓋 bug 所在的範圍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這五種都會讓「跑測試一次就 GREEN」是個假訊號 — 測試 pass 不代表測試 catch 到該 catch 的東西。&lt;/p>
&lt;h3 id="red-是測試的使用者驗收">RED 是測試的「使用者驗收」&lt;/h3>
&lt;p>對使用者代碼、我們會用「驗收訊號」（功能跑得對）證明它有用。測試也需要驗收訊號。&lt;/p>
&lt;p>「測試 catch 到 bug」這個能力的驗收訊號 = &lt;strong>「在有 bug 的代碼上失敗」&lt;/strong>。沒看過這個訊號就相信測試 = 跳過驗收。&lt;/p>
&lt;p>對應 &lt;a href="../two-occurrence-threshold/">#42 2 次門檻&lt;/a>：一次 GREEN 是低資訊量訊號、RED → GREEN 是 2 次跑（一次 fail 一次 pass）的高資訊量訊號。&lt;/p>
&lt;hr>
&lt;h2 id="多面向四種情境的-red-green-應用">多面向：四種情境的 RED-GREEN 應用&lt;/h2>
&lt;h3 id="情境-1修-bug">情境 1：修 bug&lt;/h3>





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





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





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





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





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





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





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





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





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





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. Stash 現有變動 / 切到修前 commit</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">git stash
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">git checkout &lt;pre-fix-commit&gt;
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. Cherry-pick 測試 commit（或手動複製 test files）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">git cherry-pick &lt;test-commit&gt;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 或：cp ../tests/foo.spec.ts tests/  # 複製測試檔過來</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. Build + 跑測試</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">make site <span class="o">&amp;&amp;</span> npm <span class="nb">test</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 預期：RED（測試抓到 bug）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4. 切回 main / 修後版本</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">git checkout main
</span></span><span class="line"><span class="ln">15</span><span class="cl">git stash pop
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 5. 跑測試</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 預期：GREEN</span></span></span></code></pre></div><p>兩次跑 + 兩個訊號（RED + GREEN）都對、測試才被驗證。<strong>Retrospective 補驗證 ≠ 不能補</strong> — 比完全跳過 RED 好、比 test-first 弱。</p>
<p>協議已 codify 為 <code>make verify-red-green PRE_FIX=&lt;commit-sha&gt;</code>（見 Makefile）— 五步驟自動化、不需要每次手動 stash / checkout / build / restore。</p>
<h3 id="self-case本卡誕生過程的-dogfooding-失敗">Self-case：本卡誕生過程的 dogfooding 失敗</h3>
<p>本卡是從一次真實的 dogfooding 失敗抽出來的。修 <a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a> bug 時、流程是：</p>
<ol>
<li>修 code（multi-index 策略）</li>
<li>寫 4 個 Playwright tests</li>
<li>跑測試 → 4/4 GREEN</li>
<li>看起來完工</li>
</ol>
<p>User 問「修改之前有先寫測試確保符合預測狀態嗎」— 才意識到沒走 RED。Retrospective 補驗證後發現：<strong>4 個測試只有 1 個真的 catch 到 bug、其他 3 個對 buggy code 也 PASS</strong>（placebo 測試）。</p>
<p>強化後（用 network-level + structural assertion 替換弱 invariant）：buggy code 上 1/4 PASS、3/4 FAIL。Fixed code 上 4/4 PASS。RED-GREEN 兩個訊號都看到、測試才真的驗證。</p>
<p>如果不做 retrospective、會帶著 3/4 placebo 測試 ship — 表面 4/4 GREEN、實際只有 1 個真的防回歸。<strong>「跑得通」≠「會 catch」這個區別、只有走過 RED 才知道</strong>。</p>
<p>跳過 RED 是 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發的工作</a> 在測試協議的展現 — 修法不是「下次記得」（L1 紀律會失敗）、是 <code>make verify-red-green PRE_FIX=&lt;sha&gt;</code>（L3 工具觸發）+ pre-commit hook 提醒（L3 結構觸發）。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完測試第一次跑就 GREEN</td>
          <td>警訊 — 確認測試是不是真的有 catch 能力（覆蓋 bug case 嗎？）</td>
      </tr>
      <tr>
          <td>修了 bug 但沒看過該測試 RED 過</td>
          <td>補 retrospective 驗證、或下次採 test-first</td>
      </tr>
      <tr>
          <td>「我等下會跑一下」但沒實際跑</td>
          <td>跟「我等下會 refactor」同類謊言、補不回來</td>
      </tr>
      <tr>
          <td>CI 永遠 GREEN、沒有人改過測試</td>
          <td>看 coverage、可能測試沒在跑</td>
      </tr>
      <tr>
          <td>加了 feature、測試一寫就 GREEN</td>
          <td>feature 可能已經存在、或測試太寬</td>
      </tr>
      <tr>
          <td>測試環境跟 production 環境差太多</td>
          <td>RED 在 dev 但 prod 仍 fail = 測試環境沒 catch 真實 case</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：測試不是「跑得通就有用」、是「跑出該有的訊號才有用」。RED 是測試的驗收訊號、跳過 = 接受測試本身可能是壞的。RED → GREEN 兩次跑、才證明「測試真的會 catch + 修復真的解掉 bug」。</p>
]]></content:encoded></item><item><title>URL 是 stateful UI 的儲存層 — 哪些 state 該寫進 URL</title><link>https://tarrragon.github.io/blog/report/url-as-state-container/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/url-as-state-container/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>State 的儲存層決定它的特性 — 可分享 / 可恢復 / 可導航 的 state 該寫進 URL、不寫進 = silent 把這些特性犧牲掉。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>儲存層&lt;/th>
 &lt;th>可分享&lt;/th>
 &lt;th>可 reload 恢復&lt;/th>
 &lt;th>可 back/forward 導航&lt;/th>
 &lt;th>跨 tab 同步&lt;/th>
 &lt;th>跨 device 同步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>In-memory&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>URL&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>部分（同 URL）&lt;/td>
 &lt;td>部分（複製連結）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>sessionStorage&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>localStorage&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>是（同 origin）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Server&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>是&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>寫 stateful UI 時、每個 state 的儲存位置是個設計選擇 — 不選 = 預設用 in-memory = 預設犧牲所有上面五個特性。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-url-容易被忽略">為什麼 URL 容易被忽略&lt;/h2>
&lt;h3 id="url-是隱形維度">URL 是隱形維度&lt;/h3>
&lt;p>In-memory state 在 React useState / Vue ref / vanilla 變數裡 — 寫起來最便利、是「預設位置」。URL state 需要 &lt;code>URLSearchParams&lt;/code> + &lt;code>history.pushState&lt;/code> + &lt;code>popstate&lt;/code> listener、寫起來成本高。&lt;/p>
&lt;p>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a> 直接解釋為什麼：URL state 是「對齊使用者期望」的位置（使用者預期 URL 包含 state、能分享）、in-memory 是「便利位置」。預設便利、要刻意才走對齊。&lt;/p>
&lt;h3 id="沒寫-url-state-的失敗訊號是-silent">沒寫 URL state 的失敗訊號是 silent&lt;/h3>
&lt;p>使用者打開搜尋頁、輸入「pagefind」、選擇 title-only filter、看到結果。這時：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>複製 URL 分享給朋友&lt;/strong> → 朋友打開看到空白搜尋框（query 不在 URL）&lt;/li>
&lt;li>&lt;strong>重整頁面&lt;/strong> → 自己也看到空白搜尋框&lt;/li>
&lt;li>&lt;strong>點 back&lt;/strong> → browser back 跳離搜尋頁、不是回到「沒 filter 的同個搜尋」&lt;/li>
&lt;/ul>
&lt;p>這三個動作沒有 error、沒有崩潰、就是「state 不見了」。使用者通常以為「網站就這樣」、不會 report bug。Silent 失敗 = 維護者永遠不知道有問題。&lt;/p>
&lt;p>對照 &lt;a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位&lt;/a> — 都是 silent 失敗、都是「該存在的東西不在」。&lt;/p>
&lt;hr>
&lt;h2 id="state-該寫進-url-的判準">State 該寫進 URL 的判準&lt;/h2>
&lt;h3 id="三問">三問&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>使用者會分享這個 state 嗎&lt;/strong>？— 是 → URL（複製連結即帶 state）&lt;/li>
&lt;li>&lt;strong>使用者 reload 後預期 state 還在嗎&lt;/strong>？— 是 → URL 或 sessionStorage&lt;/li>
&lt;li>&lt;strong>使用者期望 browser back/forward 在 state 之間導航嗎&lt;/strong>？— 是 → URL&lt;/li>
&lt;/ol>
&lt;p>任一個「是」 → URL。&lt;/p>
&lt;h3 id="反向判準什麼不該寫進-url">反向判準：什麼不該寫進 URL&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>State 類型&lt;/th>
 &lt;th>為什麼不該寫進 URL&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Scroll position&lt;/td>
 &lt;td>頻繁變動破壞 history、且每個瀏覽器自己管&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Focus / hover state&lt;/td>
 &lt;td>Ephemeral、跟使用者操作直接綁定、寫進 URL 沒意義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Form 編輯中的暫存值&lt;/td>
 &lt;td>使用者沒提交、不該被分享&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>敏感資訊（token / 密碼）&lt;/td>
 &lt;td>URL 進 history / referer header / log、安全性問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高頻 polling 結果&lt;/td>
 &lt;td>每秒變、history 爆炸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內部 component state（折疊 / 展開動畫進度）&lt;/td>
 &lt;td>跟 UI 細節綁、不是使用者意圖&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="多面向常見-ui-元素的-url-state-對照">多面向：常見 UI 元素的 URL state 對照&lt;/h2>
&lt;h3 id="面向-1search-filter這次任務的-case">面向 1：Search filter（這次任務的 case）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Query string、scope filter、type filter、tag filter
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">→ 都該進 URL：使用者會分享「我搜什麼 + 怎麼篩」&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範例 URL：&lt;code>/search/?q=pagefind&amp;amp;scope=title&amp;amp;type=post&amp;amp;tag=js&lt;/code>&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>State 的儲存層決定它的特性 — 可分享 / 可恢復 / 可導航 的 state 該寫進 URL、不寫進 = silent 把這些特性犧牲掉。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>儲存層</th>
          <th>可分享</th>
          <th>可 reload 恢復</th>
          <th>可 back/forward 導航</th>
          <th>跨 tab 同步</th>
          <th>跨 device 同步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>In-memory</td>
          <td>否</td>
          <td>否</td>
          <td>否</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>URL</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td>部分（同 URL）</td>
          <td>部分（複製連結）</td>
      </tr>
      <tr>
          <td>sessionStorage</td>
          <td>否</td>
          <td>是</td>
          <td>否</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>localStorage</td>
          <td>否</td>
          <td>是</td>
          <td>否</td>
          <td>是（同 origin）</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Server</td>
          <td>是</td>
          <td>是</td>
          <td>否</td>
          <td>是</td>
          <td>是</td>
      </tr>
  </tbody>
</table>
<p>寫 stateful UI 時、每個 state 的儲存位置是個設計選擇 — 不選 = 預設用 in-memory = 預設犧牲所有上面五個特性。</p>
<hr>
<h2 id="為什麼-url-容易被忽略">為什麼 URL 容易被忽略</h2>
<h3 id="url-是隱形維度">URL 是隱形維度</h3>
<p>In-memory state 在 React useState / Vue ref / vanilla 變數裡 — 寫起來最便利、是「預設位置」。URL state 需要 <code>URLSearchParams</code> + <code>history.pushState</code> + <code>popstate</code> listener、寫起來成本高。</p>
<p><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 直接解釋為什麼：URL state 是「對齊使用者期望」的位置（使用者預期 URL 包含 state、能分享）、in-memory 是「便利位置」。預設便利、要刻意才走對齊。</p>
<h3 id="沒寫-url-state-的失敗訊號是-silent">沒寫 URL state 的失敗訊號是 silent</h3>
<p>使用者打開搜尋頁、輸入「pagefind」、選擇 title-only filter、看到結果。這時：</p>
<ul>
<li><strong>複製 URL 分享給朋友</strong> → 朋友打開看到空白搜尋框（query 不在 URL）</li>
<li><strong>重整頁面</strong> → 自己也看到空白搜尋框</li>
<li><strong>點 back</strong> → browser back 跳離搜尋頁、不是回到「沒 filter 的同個搜尋」</li>
</ul>
<p>這三個動作沒有 error、沒有崩潰、就是「state 不見了」。使用者通常以為「網站就這樣」、不會 report bug。Silent 失敗 = 維護者永遠不知道有問題。</p>
<p>對照 <a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a> — 都是 silent 失敗、都是「該存在的東西不在」。</p>
<hr>
<h2 id="state-該寫進-url-的判準">State 該寫進 URL 的判準</h2>
<h3 id="三問">三問</h3>
<ol>
<li><strong>使用者會分享這個 state 嗎</strong>？— 是 → URL（複製連結即帶 state）</li>
<li><strong>使用者 reload 後預期 state 還在嗎</strong>？— 是 → URL 或 sessionStorage</li>
<li><strong>使用者期望 browser back/forward 在 state 之間導航嗎</strong>？— 是 → URL</li>
</ol>
<p>任一個「是」 → URL。</p>
<h3 id="反向判準什麼不該寫進-url">反向判準：什麼不該寫進 URL</h3>
<table>
  <thead>
      <tr>
          <th>State 類型</th>
          <th>為什麼不該寫進 URL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scroll position</td>
          <td>頻繁變動破壞 history、且每個瀏覽器自己管</td>
      </tr>
      <tr>
          <td>Focus / hover state</td>
          <td>Ephemeral、跟使用者操作直接綁定、寫進 URL 沒意義</td>
      </tr>
      <tr>
          <td>Form 編輯中的暫存值</td>
          <td>使用者沒提交、不該被分享</td>
      </tr>
      <tr>
          <td>敏感資訊（token / 密碼）</td>
          <td>URL 進 history / referer header / log、安全性問題</td>
      </tr>
      <tr>
          <td>高頻 polling 結果</td>
          <td>每秒變、history 爆炸</td>
      </tr>
      <tr>
          <td>內部 component state（折疊 / 展開動畫進度）</td>
          <td>跟 UI 細節綁、不是使用者意圖</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="多面向常見-ui-元素的-url-state-對照">多面向：常見 UI 元素的 URL state 對照</h2>
<h3 id="面向-1search-filter這次任務的-case">面向 1：Search filter（這次任務的 case）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Query string、scope filter、type filter、tag filter
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ 都該進 URL：使用者會分享「我搜什麼 + 怎麼篩」</span></span></code></pre></div><p>範例 URL：<code>/search/?q=pagefind&amp;scope=title&amp;type=post&amp;tag=js</code></p>
<h3 id="面向-2tab--step-navigation">面向 2：Tab / step navigation</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Active tab、wizard step
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ 該進 URL：分享 = 直接打開該 tab/step</span></span></code></pre></div><p>範例：<code>/settings/?tab=notifications</code>、<code>/checkout/?step=payment</code></p>
<h3 id="面向-3sort--pagination">面向 3：Sort / pagination</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">排序欄位、頁碼
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ 該進 URL：分享 = 朋友看到同樣排序的同一頁</span></span></code></pre></div><p>範例：<code>/posts/?sort=date_desc&amp;page=3</code></p>
<h3 id="面向-4modal--drawer-開合">面向 4：Modal / drawer 開合</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">看情境：
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 重要 modal（圖片預覽、編輯對話框）→ URL（可分享 / back 關閉）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 純 UX 提示 modal（welcome tour）→ in-memory（不該分享）</span></span></code></pre></div><h3 id="面向-5theme--ui-preference">面向 5：Theme / UI preference</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Dark mode、字型大小
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ localStorage（跨 session 但不分享、跟 device 綁）
</span></span><span class="line"><span class="ln">3</span><span class="cl">不進 URL（不會「分享你的 dark mode 設定」）</span></span></code></pre></div><hr>
<h2 id="url-state-的實作模式">URL state 的實作模式</h2>
<h3 id="讀載入時從-url-同步到-component-state">讀：載入時從 URL 同步到 component state</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">getInitialState</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kr">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">query</span><span class="o">:</span> <span class="nx">params</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">)</span> <span class="o">||</span> <span class="s1">&#39;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">scope</span><span class="o">:</span> <span class="nx">params</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;scope&#39;</span><span class="p">)</span> <span class="o">||</span> <span class="s1">&#39;all&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">type</span><span class="o">:</span> <span class="nx">params</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;type&#39;</span><span class="p">)</span> <span class="o">||</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kr">const</span> <span class="nx">initialState</span> <span class="o">=</span> <span class="nx">getInitialState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// component 用 initialState 初始化
</span></span></span></code></pre></div><h3 id="寫state-變動時同步到-url">寫：state 變動時同步到 URL</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">syncUrl</span><span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kr">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URLSearchParams</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">query</span><span class="p">)</span> <span class="nx">params</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">query</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">scope</span> <span class="o">&amp;&amp;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">scope</span> <span class="o">!==</span> <span class="s1">&#39;all&#39;</span><span class="p">)</span> <span class="nx">params</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s1">&#39;scope&#39;</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="nx">params</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">type</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="sb">`</span><span class="si">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">pathname</span><span class="si">}${</span><span class="nx">params</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="o">?</span> <span class="s1">&#39;?&#39;</span> <span class="o">+</span> <span class="nx">params</span><span class="p">.</span><span class="nx">toString</span><span class="p">()</span> <span class="o">:</span> <span class="s1">&#39;&#39;</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nx">history</span><span class="p">.</span><span class="nx">replaceState</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 每次 state 變動觸發
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="nx">onStateChange</span><span class="p">((</span><span class="nx">newState</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">syncUrl</span><span class="p">(</span><span class="nx">newState</span><span class="p">));</span></span></span></code></pre></div><p>選擇 <code>replaceState</code> vs <code>pushState</code>：</p>
<ul>
<li><code>replaceState</code>：每次 state 變動覆蓋當前 history entry — back/forward 跳過中間狀態</li>
<li><code>pushState</code>：每次 state 變動加新 history entry — back 回到上一個 state</li>
</ul>
<p>通常 search filter / sort / pagination 用 <code>replaceState</code>（typing 太快、不該每個字符一個 history entry）；tab / step 用 <code>pushState</code>（每個 step 該 back 回上一個）。</p>
<h3 id="雙向聽-popstate-處理-backforward">雙向：聽 popstate 處理 back/forward</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;popstate&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">state</span> <span class="o">=</span> <span class="nx">getInitialState</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">applyStateToUI</span><span class="p">(</span><span class="nx">state</span><span class="p">);</span>  <span class="c1">// back/forward 後、把 state 套回 UI
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>沒 listen popstate = back/forward 不會觸發 UI 更新、URL 跟 UI 不同步。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「URL 是 state 儲存層」原則在「公開可分享的 UI」成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部 admin 工具</td>
          <td>不分享、不公開、URL persistence ROI 低</td>
      </tr>
      <tr>
          <td>Single-page wizard 強制流程</td>
          <td>不該允許 deep link 跳關卡（業務規則需要照順序走）</td>
      </tr>
      <tr>
          <td>一次性確認對話框</td>
          <td>不該被 back 回來、不該分享</td>
      </tr>
      <tr>
          <td>開發中的 prototype</td>
          <td>還沒穩定的 UI、不該固化 URL contract</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>URL 是 state 的 SSOT 候選 — 選對位置 = 一處可改、不選 = 多源 drift</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>In-memory state 是便利位置、URL state 是對齊（使用者預期）位置</td>
      </tr>
      <tr>
          <td><a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a></td>
          <td>都是 silent 失敗結構 — state 該在的位置不在、使用者沒訊號</td>
      </tr>
      <tr>
          <td><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a></td>
          <td>URL state 沒做 = 「畫面對了但 reload 後不見」是同類功能缺口</td>
      </tr>
      <tr>
          <td><a href="../pattern-explicit-semantic-narrowing/">#66 明示語意縮小</a></td>
          <td>「URL 不持久化」如果是設計選擇、要明示（「重整會清除狀態」hint）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<ul>
<li>搜尋頁的 scope filter URL persistence — Phase 1+2 修完後 retrospective Checkpoint 1 才發現遺漏（#68 dogfooding）</li>
<li>任何 search / list / dashboard UI — 都該檢視 URL state coverage</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫互動 UI 但沒寫 URL 同步</td>
          <td>跑三問、確認該不該寫進 URL</td>
      </tr>
      <tr>
          <td>使用者 report「我分享連結給朋友、他看不到我看到的」</td>
          <td>URL state 缺漏的 silent 訊號顯現</td>
      </tr>
      <tr>
          <td><code>replaceState</code> 跟 <code>pushState</code> 沒區分、所有 state 變動用同一個</td>
          <td>評估：哪些是 history entry 該被記、哪些不該</td>
      </tr>
      <tr>
          <td>沒 listen <code>popstate</code></td>
          <td>back/forward 會 silent 失效、補 listener</td>
      </tr>
      <tr>
          <td>URL 變超長、含 ephemeral state</td>
          <td>過度寫進 URL、用反向判準砍掉不該寫的</td>
      </tr>
      <tr>
          <td>內心 OS：「state 用 useState 就好、URL 之後再說」</td>
          <td>「之後再說」= <a href="../ease-of-writing-vs-intent-alignment/">#67 reformer 謊言</a>、補不回來</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：URL 是 stateful UI 的隱形儲存層。沒寫 URL state = silent 犧牲分享 / 恢復 / 導航三個 UX 特性。寫之前跑三問（分享？reload？back/forward？）、任一個是 → URL。</p>
]]></content:encoded></item><item><title>Tab Order = DOM Order = Mental Model 三者對齊</title><link>https://tarrragon.github.io/blog/report/tab-order-mental-model-alignment/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/tab-order-mental-model-alignment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>Tab 順序 = DOM 順序 = 使用者 mental model 的互動順序、三者該對齊。&lt;/p>&lt;/blockquote>
&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>DOM 順序&lt;/td>
 &lt;td>HTML / template 結構&lt;/td>
 &lt;td>Mental model 的互動順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab 順序&lt;/td>
 &lt;td>DOM 順序（除非 tabindex 強制覆寫）&lt;/td>
 &lt;td>DOM 順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mental model 順序&lt;/td>
 &lt;td>使用者預期「先做 X 再做 Y」的流程&lt;/td>
 &lt;td>UI 設計意圖&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者偏差的後果：&lt;/p>
&lt;ul>
&lt;li>DOM ≠ mental model：視覺 / tab 順序跟使用者期望不一致、a11y 體驗差&lt;/li>
&lt;li>DOM ≠ tab order（用 &lt;code>tabindex &amp;gt; 0&lt;/code>）：DOM 改變時 tab 順序維護成本爆炸（#52 反模式）&lt;/li>
&lt;li>全對齊：DOM 簡單、tab 自然、a11y 預設正確&lt;/li>
&lt;/ul>
&lt;p>要解決不對齊、&lt;strong>優先重排 DOM&lt;/strong>、不要用 &lt;code>tabindex&lt;/code> 強制覆寫。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼三者該對齊到-dom-順序">為什麼三者該對齊到 DOM 順序&lt;/h2>
&lt;h3 id="tab-順序跟-dom-順序綁定是-spec-規定">Tab 順序跟 DOM 順序綁定是 spec 規定&lt;/h3>
&lt;p>HTML5 spec：tabbable elements 預設依 source order（DOM 順序）navigate。要改變只能用 &lt;code>tabindex&lt;/code> 覆寫。&lt;/p>
&lt;p>&lt;code>tabindex&lt;/code> 三種值：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>tabindex&lt;/th>
 &lt;th>行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>0&lt;/code> 或不寫&lt;/td>
 &lt;td>跟 DOM 順序、可 tab 到（依元素本身的 tabbability）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-1&lt;/code>&lt;/td>
 &lt;td>不能 tab 到、但可被 &lt;code>.focus()&lt;/code> 程式 focus&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>&amp;gt; 0&lt;/code>（如 &lt;code>1&lt;/code>、&lt;code>2&lt;/code>）&lt;/td>
 &lt;td>強制覆寫順序、所有 &lt;code>&amp;gt; 0&lt;/code> 的元素先 tab、按數值升序&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>tabindex &amp;gt; 0&lt;/code> 反模式（同 &lt;a href="../keyboard-accessibility/">#52 鍵盤可達性&lt;/a>）：&lt;/p>
&lt;ul>
&lt;li>全頁面只要有任何元素用 &lt;code>tabindex &amp;gt; 0&lt;/code>、整個 tab 順序變混亂（其他 &lt;code>0&lt;/code> / 不寫的元素都被推到後面）&lt;/li>
&lt;li>維護成本：DOM 改了、所有 &lt;code>tabindex &amp;gt; 0&lt;/code> 的數值都要重排&lt;/li>
&lt;li>A11y：screen reader 跟視覺使用者體驗到不同順序&lt;/li>
&lt;/ul>
&lt;p>唯一合法用法：要把元素「移出 tab cycle」用 &lt;code>tabindex=&amp;quot;-1&amp;quot;&lt;/code>（例如 modal 開啟時鎖住背景）。&lt;/p>
&lt;h3 id="mental-model-順序由-ui-設計決定">Mental model 順序由 UI 設計決定&lt;/h3>
&lt;p>互動式 UI 隱含一個流程：使用者預期「先做 X 再做 Y」。例如：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>UI 類型&lt;/th>
 &lt;th>預期 mental model 順序&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>搜尋頁&lt;/td>
 &lt;td>1. 打 query → 2. 篩選範圍 → 3. 看結果 → 4. 載入更多&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>表單&lt;/td>
 &lt;td>從上到下、必填欄位先、subtmit 在最後&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Wizard&lt;/td>
 &lt;td>Step 1 → Step 2 → Step 3 → Submit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品列表&lt;/td>
 &lt;td>1. Sort / filter → 2. 看商品 → 3. 加入購物車&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Modal&lt;/td>
 &lt;td>Modal 內容 → primary action → secondary action → close&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>設計者腦中有這個順序、寫 HTML 時要把它具體化成 DOM 順序。&lt;strong>DOM 順序就是把 mental model 寫進 code 的方式&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>Tab 順序 = DOM 順序 = 使用者 mental model 的互動順序、三者該對齊。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>由什麼決定</th>
          <th>該對齊到什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 順序</td>
          <td>HTML / template 結構</td>
          <td>Mental model 的互動順序</td>
      </tr>
      <tr>
          <td>Tab 順序</td>
          <td>DOM 順序（除非 tabindex 強制覆寫）</td>
          <td>DOM 順序</td>
      </tr>
      <tr>
          <td>Mental model 順序</td>
          <td>使用者預期「先做 X 再做 Y」的流程</td>
          <td>UI 設計意圖</td>
      </tr>
  </tbody>
</table>
<p>三者偏差的後果：</p>
<ul>
<li>DOM ≠ mental model：視覺 / tab 順序跟使用者期望不一致、a11y 體驗差</li>
<li>DOM ≠ tab order（用 <code>tabindex &gt; 0</code>）：DOM 改變時 tab 順序維護成本爆炸（#52 反模式）</li>
<li>全對齊：DOM 簡單、tab 自然、a11y 預設正確</li>
</ul>
<p>要解決不對齊、<strong>優先重排 DOM</strong>、不要用 <code>tabindex</code> 強制覆寫。</p>
<hr>
<h2 id="為什麼三者該對齊到-dom-順序">為什麼三者該對齊到 DOM 順序</h2>
<h3 id="tab-順序跟-dom-順序綁定是-spec-規定">Tab 順序跟 DOM 順序綁定是 spec 規定</h3>
<p>HTML5 spec：tabbable elements 預設依 source order（DOM 順序）navigate。要改變只能用 <code>tabindex</code> 覆寫。</p>
<p><code>tabindex</code> 三種值：</p>
<table>
  <thead>
      <tr>
          <th>tabindex</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>0</code> 或不寫</td>
          <td>跟 DOM 順序、可 tab 到（依元素本身的 tabbability）</td>
      </tr>
      <tr>
          <td><code>-1</code></td>
          <td>不能 tab 到、但可被 <code>.focus()</code> 程式 focus</td>
      </tr>
      <tr>
          <td><code>&gt; 0</code>（如 <code>1</code>、<code>2</code>）</td>
          <td>強制覆寫順序、所有 <code>&gt; 0</code> 的元素先 tab、按數值升序</td>
      </tr>
  </tbody>
</table>
<p><code>tabindex &gt; 0</code> 反模式（同 <a href="../keyboard-accessibility/">#52 鍵盤可達性</a>）：</p>
<ul>
<li>全頁面只要有任何元素用 <code>tabindex &gt; 0</code>、整個 tab 順序變混亂（其他 <code>0</code> / 不寫的元素都被推到後面）</li>
<li>維護成本：DOM 改了、所有 <code>tabindex &gt; 0</code> 的數值都要重排</li>
<li>A11y：screen reader 跟視覺使用者體驗到不同順序</li>
</ul>
<p>唯一合法用法：要把元素「移出 tab cycle」用 <code>tabindex=&quot;-1&quot;</code>（例如 modal 開啟時鎖住背景）。</p>
<h3 id="mental-model-順序由-ui-設計決定">Mental model 順序由 UI 設計決定</h3>
<p>互動式 UI 隱含一個流程：使用者預期「先做 X 再做 Y」。例如：</p>
<table>
  <thead>
      <tr>
          <th>UI 類型</th>
          <th>預期 mental model 順序</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>搜尋頁</td>
          <td>1. 打 query → 2. 篩選範圍 → 3. 看結果 → 4. 載入更多</td>
      </tr>
      <tr>
          <td>表單</td>
          <td>從上到下、必填欄位先、subtmit 在最後</td>
      </tr>
      <tr>
          <td>Wizard</td>
          <td>Step 1 → Step 2 → Step 3 → Submit</td>
      </tr>
      <tr>
          <td>商品列表</td>
          <td>1. Sort / filter → 2. 看商品 → 3. 加入購物車</td>
      </tr>
      <tr>
          <td>Modal</td>
          <td>Modal 內容 → primary action → secondary action → close</td>
      </tr>
  </tbody>
</table>
<p>設計者腦中有這個順序、寫 HTML 時要把它具體化成 DOM 順序。<strong>DOM 順序就是把 mental model 寫進 code 的方式</strong>。</p>
<hr>
<h2 id="多面向常見不對齊-case">多面向：常見不對齊 case</h2>
<h3 id="面向-1filter-在-search-input-之前這次任務的-case">面向 1：Filter 在 search input 之前（這次任務的 case）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- DOM 順序：scope 先 → search input 後 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>  <span class="c">&lt;!-- pagefind input 在裡面 --&gt;</span></span></span></code></pre></div><p>Tab 順序：scope radios → search input。但 mental model 是「先打字再篩選」、Tab 應該先到 input。</p>
<p><strong>修法</strong>：DOM 重排、把 scope 移到 #search 之後。視覺位置由 CSS <code>position: absolute</code> 控制、不受 DOM 順序影響。</p>
<h3 id="面向-2submit-按鈕在-form-中間">面向 2：Submit 按鈕在 form 中間</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">form</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;email&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span><span class="p">&gt;</span>送出<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 太早 --&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">&lt;</span><span class="nt">textarea</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;message&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">textarea</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span></span></span></code></pre></div><p>Tab 順序：email → submit → textarea。使用者打完 email 按 Enter 就送出、textarea 還沒填。</p>
<p><strong>修法</strong>：submit 移到所有 input 之後。</p>
<h3 id="面向-3logo--nav-在主要-cta-之前">面向 3：Logo / nav 在主要 CTA 之前</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">header</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/&#34;</span><span class="p">&gt;</span>Logo<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">nav</span><span class="p">&gt;</span>... 5 個 links ...<span class="p">&lt;/</span><span class="nt">nav</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">header</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;</span><span class="nt">main</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>主要 CTA<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 使用者要按這個 --&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">main</span><span class="p">&gt;</span></span></span></code></pre></div><p>Tab 順序：6 個 nav links → CTA。使用者要 tab 6 次才到 CTA。</p>
<p><strong>修法</strong>：考慮加 「skip to main content」link（A11y 標準做法）— <code>&lt;a href=&quot;#main-content&quot; class=&quot;skip-link&quot;&gt;</code>。第一個 tab 就跳過 nav 到 main。</p>
<h3 id="面向-4modal-開啟時-background-仍-tabbable">面向 4：Modal 開啟時 background 仍 tabbable</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;background-content&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;...&#34;</span><span class="p">&gt;</span>某連結<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 仍可 tab 到 --&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">role</span><span class="o">=</span><span class="s">&#34;dialog&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">&lt;</span><span class="nt">input</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>確認<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>Tab 順序：背景連結 → modal input → confirm。使用者 tab 出 modal 跑回背景。</p>
<p><strong>修法</strong>：modal 開啟時、用 <code>inert</code> attribute（modern）或所有背景元素設 <code>tabindex=&quot;-1&quot;</code>（傳統）把它們踢出 cycle。<code>&lt;dialog&gt;</code> native 自動處理。</p>
<hr>
<h2 id="不對齊的修法優先重排-dom">不對齊的修法：優先重排 DOM</h2>
<h3 id="第一順位重排-dom">第一順位：重排 DOM</h3>
<p>把元素照 mental model 順序排在 HTML / template 裡。視覺位置如果跟 DOM 順序不同、用 CSS <code>order</code>（flex / grid）、<code>position: absolute</code>、<code>grid-template-areas</code> 控制。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- DOM 順序對齊 mental model：input → scope → drawer --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* 視覺：scope 浮在 input 跟 drawer 之間（跟 DOM 順序無關） */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">input</span><span class="o">-</span><span class="n">h</span><span class="p">)</span> <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>Tab 順序自然對齊 DOM、視覺位置由 CSS 獨立控制</strong> — 兩個維度解耦、不互相影響。</p>
<h3 id="第二順位js-動態移動-dom">第二順位：JS 動態移動 DOM</h3>
<p>如果元素因為 framework 限制無法 hard-coded 在對的位置（例如某 vendor library 強制 mount 點）、用 JS 在 mount 後 reparent 元素到對的位置。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// PagefindUI mount 後、把 scope 移到 input 跟 drawer 之間（如果 framework 允許）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">drawer</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">scope</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">);</span></span></span></code></pre></div><p>風險：framework 重渲染可能 reparent 回去（<a href="../coexisting-with-framework-managed-dom/">#5 framework-managed DOM</a>）。要驗證穩定性。</p>
<h3 id="第三順位不推薦tabindex-強制">第三順位（不推薦）：tabindex 強制</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">tabindex</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 反模式：tabindex &gt; 0 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">tabindex</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>只在前兩種都做不到時用。維護成本高、a11y 跟設計工具支援差。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「DOM = tab = mental model 三者對齊」原則在多數情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該強制對齊</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純展示頁面（無互動）</td>
          <td>沒 mental model 順序可言、預設 DOM 順序就好</td>
      </tr>
      <tr>
          <td>動態生成 list 元素</td>
          <td>List 元素數量不固定、tab order 跟著 DOM 自然走是對的</td>
      </tr>
      <tr>
          <td>模糊的 mental model</td>
          <td>當 UI 設計沒明確流程、DOM 自然順序通常已經夠用</td>
      </tr>
      <tr>
          <td>Framework 不允許重排</td>
          <td>接受次優、加 explicit hint 告知使用者</td>
      </tr>
  </tbody>
</table>
<p>四類共同特徵：<strong>沒有清楚的「使用者該先做 X 再做 Y」流程</strong> — 本原則建立在「有 mental model 可對齊」上、沒有時自然不適用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../keyboard-accessibility/">#52 鍵盤可達性</a></td>
          <td>本卡是 #52「邏輯 tab 順序」要素的展開、含 tabindex &gt; 0 反模式詳解</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>DOM 順序便利（先寫先 render）、mental model 對齊需要刻意設計 — 反相關</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>tabindex &gt; 0 是「擴張範圍」反模式 — 一個 tabindex &gt; 0 影響整頁 tab 順序</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 native HTML &gt; ARIA</a></td>
          <td>Native HTML 元素自帶正確 tab 行為、不需要 ARIA tabindex 補</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<ul>
<li>搜尋頁 scope filter 在 search input 之前的 tab 順序問題 — Checkpoint 1 retrospective 找到（<a href="../verification-timeline-checkpoints/">#68</a> dogfooding）</li>
<li>任何「先選範圍再操作」vs「先操作再選範圍」的 UI 設計 — 都該檢視 tab order 是否對齊</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫了 <code>tabindex=&quot;1&quot;</code> 或更大的數字</td>
          <td>換重排 DOM、避免 tabindex &gt; 0</td>
      </tr>
      <tr>
          <td>Tab 順序跟「使用者會先做什麼」感覺反</td>
          <td>列 mental model 流程、檢查 DOM 順序</td>
      </tr>
      <tr>
          <td>做 a11y review 才發現 tab 順序怪</td>
          <td>Checkpoint 1 沒列鍵盤使用 case、補進開工前清單</td>
      </tr>
      <tr>
          <td>用 JS reparent 元素改順序、framework 改回來</td>
          <td>重新評估架構、把元素放在 framework 邊界外</td>
      </tr>
      <tr>
          <td>內心 OS：「視覺位置是 X、所以 DOM 也該在 X」</td>
          <td>視覺跟 DOM 解耦才是對的設計</td>
      </tr>
      <tr>
          <td>看到 <code>tabindex=&quot;-1&quot;</code> 在不該被 tab 的元素上</td>
          <td>合理使用（modal 背景 / 先 focus 後 reveal）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：DOM 順序是寫進 code 的 mental model、tab 順序是使用者體驗的 mental model — 兩者該由「重排 DOM」對齊、不該由「tabindex」強制。視覺位置跟 DOM 順序解耦（用 CSS 控制）、讓兩者各自獨立優化。</p>
]]></content:encoded></item><item><title>高 ROI 無外部觸發的工作會被結構性跳過</title><link>https://tarrragon.github.io/blog/report/external-trigger-for-high-roi-work/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-trigger-for-high-roi-work/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>工作有兩個獨立維度：ROI 高低 × 是否有外部觸發。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>ROI / 觸發&lt;/th>
 &lt;th>有外部觸發&lt;/th>
 &lt;th>沒外部觸發&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>高 ROI&lt;/strong>&lt;/td>
 &lt;td>順利做（happy path）&lt;/td>
 &lt;td>&lt;strong>被結構性跳過&lt;/strong>（本卡焦點）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>低 ROI&lt;/strong>&lt;/td>
 &lt;td>該砍掉、不該做&lt;/td>
 &lt;td>自然不做（也對）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「&lt;strong>高 ROI + 沒外部觸發&lt;/strong>」是個結構性陷阱 — 知道該做、做了有大回報、但永遠不做。靠「我下次記得」不可行。修法是&lt;strong>結構性對策&lt;/strong>：把外部觸發補上。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼靠紀律不可行">為什麼靠紀律不可行&lt;/h2>
&lt;h3 id="之後做是個謊言共同結構">「之後做」是個謊言（共同結構）&lt;/h3>
&lt;p>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 我等下會 refactor 是個謊言&lt;/a> 已經點到一個面向。把它推廣：&lt;/p>
&lt;p>「之後做 X」這個 plan 在 X 屬於「高 ROI + 無觸發」時、預期完成率接近 0。不是個人意志問題、是結構問題：&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>~95%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 卡死流程&lt;/td>
 &lt;td>~95%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Calendar reminder&lt;/td>
 &lt;td>~70%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sprint planning&lt;/td>
 &lt;td>~60%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自己記下的 TODO&lt;/td>
 &lt;td>~30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「下次有空我做」&lt;/td>
 &lt;td>~5%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>往下走、外部觸發越弱、執行率越低。最弱的「下次有空我做」≈ 0% — 因為「下次」永遠是「現在」、「現在」永遠有更急的事。&lt;/p>
&lt;h3 id="為什麼結構性不是動機問題">為什麼結構性、不是動機問題&lt;/h3>
&lt;p>「沒外部觸發」 = 沒人催、沒 deadline、沒 alarm、沒 PR review 提醒。腦中有 working memory 限制、優先處理「正在叫」的事。&lt;strong>「叫」這個動作只有外部能做&lt;/strong> — 自己對自己叫沒用（因為「自己叫自己時」跟「自己接受自己叫時」是同個 context）。&lt;/p>
&lt;p>這跟意志力、自律、責任感無關 — 即使最自律的人、面對「沒人催的高 ROI 工作」，執行率也大幅下降。靠紀律 = 預期失敗、然後責怪自己。&lt;/p>
&lt;hr>
&lt;h2 id="多面向高-roi--無觸發的工作清單">多面向：高 ROI + 無觸發的工作清單&lt;/h2>
&lt;p>每一條都對應某張既有卡的具體展現：&lt;/p>
&lt;h3 id="寫程式類">寫程式類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Refactor（沒功能壓力）&lt;/strong> — &lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>&lt;/li>
&lt;li>&lt;strong>Test-first 的 RED 階段（修完才補測試）&lt;/strong> — &lt;a href="../test-first-red-before-green/">#69&lt;/a>&lt;/li>
&lt;li>&lt;strong>Checkpoint 1（列使用者意圖完整集）&lt;/strong> — &lt;a href="../verification-timeline-checkpoints/">#68&lt;/a>&lt;/li>
&lt;li>&lt;strong>Ship 前 E2E case 設計&lt;/strong> — &lt;a href="../verification-timeline-checkpoints/">#68&lt;/a>&lt;/li>
&lt;li>&lt;strong>Code review feedback 的 follow-up&lt;/strong>（reviewer 留 comment、作者回「之後改」）&lt;/li>
&lt;/ul>
&lt;h3 id="維護類">維護類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Migration cleanup（feature flag 拔除、舊 path 砍掉）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Deprecated 程式碼移除&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Dependency upgrade（沒 breaking 但該升）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Performance regression 修復（測量上有但使用者沒抱怨）&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="文件類">文件類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>API doc / README 更新&lt;/strong>&lt;/li>
&lt;li>&lt;strong>事後檢討卡片寫入&lt;/strong>（這個 cards-skills 系統就是 case — 沒 user 提醒就不會做）&lt;/li>
&lt;li>&lt;strong>Decision log / ADR&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="監控類">監控類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Setup observability / log monitor（&lt;a href="../verification-timeline-checkpoints/">#68&lt;/a> Checkpoint 4）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Alert 規則 review&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Dashboard 維護&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="知識類">知識類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Onboarding doc 更新&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Post-mortem 寫完發出去&lt;/strong>&lt;/li>
&lt;li>&lt;strong>跨團隊 share session&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>共通結構&lt;/strong>：每一項都「知道該做、做了有大回報、沒人催就不做」。即使是寫過卡片教自己原則的人（meta-level dogfooding 失敗）也一樣會跳過。&lt;/p>
&lt;hr>
&lt;h2 id="修法結構性對策的五個層級">修法：結構性對策的五個層級&lt;/h2>
&lt;p>從弱到強：&lt;/p>
&lt;h3 id="l1個人紀律最弱不可行">L1：個人紀律（最弱、不可行）&lt;/h3>
&lt;p>「我下次記得」「我會自律」 — 已經證明 ≈ 0% 執行率。不該寫進 plan。&lt;/p>
&lt;h3 id="l2自我排程弱">L2：自我排程（弱）&lt;/h3>
&lt;p>「每週五下午 refactor 1 小時」「每個月初 review TODO」。比 L1 強、但仍依賴自己當下不分心、不被「更急」的事拉走。執行率約 30-50%。&lt;/p>
&lt;h3 id="l3外部工具觸發中-強">L3：外部工具觸發（中-強）&lt;/h3>
&lt;p>把觸發外化到工具：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CI / pre-commit hook&lt;/strong>：commit test file 自動提醒「跑過 RED 嗎」&lt;/li>
&lt;li>&lt;strong>Scheduled scripts&lt;/strong>：cron job 跑 lint / dep audit / migration cleanup detector&lt;/li>
&lt;li>&lt;strong>Calendar event&lt;/strong>：固定時間、有 alarm&lt;/li>
&lt;li>&lt;strong>PR template&lt;/strong>：強制填「Checkpoint 1 列了哪些 case」&lt;/li>
&lt;/ul>
&lt;p>工具不會忘、不會拖、不會選擇性執行。執行率 80-95%。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>工作有兩個獨立維度：ROI 高低 × 是否有外部觸發。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>ROI / 觸發</th>
          <th>有外部觸發</th>
          <th>沒外部觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>高 ROI</strong></td>
          <td>順利做（happy path）</td>
          <td><strong>被結構性跳過</strong>（本卡焦點）</td>
      </tr>
      <tr>
          <td><strong>低 ROI</strong></td>
          <td>該砍掉、不該做</td>
          <td>自然不做（也對）</td>
      </tr>
  </tbody>
</table>
<p>「<strong>高 ROI + 沒外部觸發</strong>」是個結構性陷阱 — 知道該做、做了有大回報、但永遠不做。靠「我下次記得」不可行。修法是<strong>結構性對策</strong>：把外部觸發補上。</p>
<hr>
<h2 id="為什麼靠紀律不可行">為什麼靠紀律不可行</h2>
<h3 id="之後做是個謊言共同結構">「之後做」是個謊言（共同結構）</h3>
<p><a href="../ease-of-writing-vs-intent-alignment/">#67 我等下會 refactor 是個謊言</a> 已經點到一個面向。把它推廣：</p>
<p>「之後做 X」這個 plan 在 X 屬於「高 ROI + 無觸發」時、預期完成率接近 0。不是個人意志問題、是結構問題：</p>
<table>
  <thead>
      <tr>
          <th>工作觸發來源</th>
          <th>「之後做」的執行率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶來信催</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>Bug 卡死流程</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>Calendar reminder</td>
          <td>~70%</td>
      </tr>
      <tr>
          <td>Sprint planning</td>
          <td>~60%</td>
      </tr>
      <tr>
          <td>自己記下的 TODO</td>
          <td>~30%</td>
      </tr>
      <tr>
          <td>「下次有空我做」</td>
          <td>~5%</td>
      </tr>
  </tbody>
</table>
<p>往下走、外部觸發越弱、執行率越低。最弱的「下次有空我做」≈ 0% — 因為「下次」永遠是「現在」、「現在」永遠有更急的事。</p>
<h3 id="為什麼結構性不是動機問題">為什麼結構性、不是動機問題</h3>
<p>「沒外部觸發」 = 沒人催、沒 deadline、沒 alarm、沒 PR review 提醒。腦中有 working memory 限制、優先處理「正在叫」的事。<strong>「叫」這個動作只有外部能做</strong> — 自己對自己叫沒用（因為「自己叫自己時」跟「自己接受自己叫時」是同個 context）。</p>
<p>這跟意志力、自律、責任感無關 — 即使最自律的人、面對「沒人催的高 ROI 工作」，執行率也大幅下降。靠紀律 = 預期失敗、然後責怪自己。</p>
<hr>
<h2 id="多面向高-roi--無觸發的工作清單">多面向：高 ROI + 無觸發的工作清單</h2>
<p>每一條都對應某張既有卡的具體展現：</p>
<h3 id="寫程式類">寫程式類</h3>
<ul>
<li><strong>Refactor（沒功能壓力）</strong> — <a href="../ease-of-writing-vs-intent-alignment/">#67</a></li>
<li><strong>Test-first 的 RED 階段（修完才補測試）</strong> — <a href="../test-first-red-before-green/">#69</a></li>
<li><strong>Checkpoint 1（列使用者意圖完整集）</strong> — <a href="../verification-timeline-checkpoints/">#68</a></li>
<li><strong>Ship 前 E2E case 設計</strong> — <a href="../verification-timeline-checkpoints/">#68</a></li>
<li><strong>Code review feedback 的 follow-up</strong>（reviewer 留 comment、作者回「之後改」）</li>
</ul>
<h3 id="維護類">維護類</h3>
<ul>
<li><strong>Migration cleanup（feature flag 拔除、舊 path 砍掉）</strong></li>
<li><strong>Deprecated 程式碼移除</strong></li>
<li><strong>Dependency upgrade（沒 breaking 但該升）</strong></li>
<li><strong>Performance regression 修復（測量上有但使用者沒抱怨）</strong></li>
</ul>
<h3 id="文件類">文件類</h3>
<ul>
<li><strong>API doc / README 更新</strong></li>
<li><strong>事後檢討卡片寫入</strong>（這個 cards-skills 系統就是 case — 沒 user 提醒就不會做）</li>
<li><strong>Decision log / ADR</strong></li>
</ul>
<h3 id="監控類">監控類</h3>
<ul>
<li><strong>Setup observability / log monitor（<a href="../verification-timeline-checkpoints/">#68</a> Checkpoint 4）</strong></li>
<li><strong>Alert 規則 review</strong></li>
<li><strong>Dashboard 維護</strong></li>
</ul>
<h3 id="知識類">知識類</h3>
<ul>
<li><strong>Onboarding doc 更新</strong></li>
<li><strong>Post-mortem 寫完發出去</strong></li>
<li><strong>跨團隊 share session</strong></li>
</ul>
<p><strong>共通結構</strong>：每一項都「知道該做、做了有大回報、沒人催就不做」。即使是寫過卡片教自己原則的人（meta-level dogfooding 失敗）也一樣會跳過。</p>
<hr>
<h2 id="修法結構性對策的五個層級">修法：結構性對策的五個層級</h2>
<p>從弱到強：</p>
<h3 id="l1個人紀律最弱不可行">L1：個人紀律（最弱、不可行）</h3>
<p>「我下次記得」「我會自律」 — 已經證明 ≈ 0% 執行率。不該寫進 plan。</p>
<h3 id="l2自我排程弱">L2：自我排程（弱）</h3>
<p>「每週五下午 refactor 1 小時」「每個月初 review TODO」。比 L1 強、但仍依賴自己當下不分心、不被「更急」的事拉走。執行率約 30-50%。</p>
<h3 id="l3外部工具觸發中-強">L3：外部工具觸發（中-強）</h3>
<p>把觸發外化到工具：</p>
<ul>
<li><strong>CI / pre-commit hook</strong>：commit test file 自動提醒「跑過 RED 嗎」</li>
<li><strong>Scheduled scripts</strong>：cron job 跑 lint / dep audit / migration cleanup detector</li>
<li><strong>Calendar event</strong>：固定時間、有 alarm</li>
<li><strong>PR template</strong>：強制填「Checkpoint 1 列了哪些 case」</li>
</ul>
<p>工具不會忘、不會拖、不會選擇性執行。執行率 80-95%。</p>
<h3 id="l4團隊流程強">L4：團隊流程（強）</h3>
<p>把觸發外化到別人：</p>
<ul>
<li><strong>Pair programming</strong>：另一個人在旁邊、會問「為什麼跳過 X」</li>
<li><strong>Code review block</strong>：reviewer 不通過 PR 直到 X 完成</li>
<li><strong>Standup commitment</strong>：公開講出「我這週要修 X」、隔天會被問</li>
<li><strong>Retro action items</strong>：團隊紀錄 + 追蹤、不個人擁有</li>
</ul>
<p>執行率 90-99%。</p>
<h3 id="l5結構性不可能最強">L5：結構性不可能（最強）</h3>
<p>讓不做 X 變成 ship 不出去：</p>
<ul>
<li><strong>Tests required</strong>：CI fail 不能 merge</li>
<li><strong>Build fails on stale doc</strong>：lint 規則檢查 doc 跟 code 同步</li>
<li><strong>Feature flag 自動 expire</strong>：超過某時間、flag 被自動移除</li>
<li><strong>Linter 禁用 deprecated API</strong>：用了就 build 錯</li>
</ul>
<p>100% 執行率（系統強制）。代價：建立成本高、要團隊認可。</p>
<p>選擇法則：<strong>先看哪個層級剛好夠</strong>、不要用 L5 解 L3 能解的問題（過度工程）、也不要用 L1 解 L4 才能解的問題（會失敗）。</p>
<hr>
<h2 id="想到就動手是次優不是最優">「想到就動手」是次優、不是最優</h2>
<p>直覺反應是「想到該做就立刻做」、避免拖延。這在「想到時剛好沒手邊事」可行、但實際多半「想到時手邊有事」 — 變成中斷當前工作、context switch 高昂。</p>
<p>更穩定的策略：<strong>把想到的東西塞進已存在的觸發機制</strong>：</p>
<ul>
<li>想到「這個重複了該抽 helper」 → 開 issue / TODO 給下次 refactor session</li>
<li>想到「這個 case 沒測」 → 加進 PR template 的 Checkpoint 1 list</li>
<li>想到「這個 doc 過時了」 → 打開 doc 在 commit 寫 <code>// TODO: 更新 X</code></li>
</ul>
<p>「動手」的時機由觸發決定、不由「想到」決定。<strong>想到 = 觸發機制的 input、不是執行的 trigger</strong>。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「高 ROI + 無觸發 = 結構性跳過」原則在多數情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純探索 / 興趣專案</td>
          <td>沒 ROI 概念、做了爽就好、不需要結構性對策</td>
      </tr>
      <tr>
          <td>一次性極小工作</td>
          <td>5 分鐘內完成、加 trigger 反而成本高</td>
      </tr>
      <tr>
          <td>緊急 incident</td>
          <td>已有最強觸發（系統壞了）、不需額外結構</td>
      </tr>
      <tr>
          <td>還沒穩定的探索期</td>
          <td>規則還在演化、結構性對策可能會卡死探索</td>
      </tr>
      <tr>
          <td>學習新技術 / 練習</td>
          <td>自己選、沒外部 ROI 衡量、跳過也不損失</td>
      </tr>
  </tbody>
</table>
<p>四類共同特徵：<strong>「外部觸發」這個變數已經有解或不存在</strong> — 本原則建立在「沒觸發 = 跳過」上、有觸發或不需要時自然不適用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>#67 是本卡在「寫程式當下選哪條路」面向的展現 — 對齊 = 高 ROI 但無觸發</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>#68 的「Ship 前 / Checkpoint 1 結構性偏差」是本卡在驗收動作的展現</td>
      </tr>
      <tr>
          <td><a href="../test-first-red-before-green/">#69 Test-First</a></td>
          <td>RED 階段被跳過 = 本卡在測試協議的展現</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>失敗訊號需要被「外部承認」才能觸發轉折 — 跟本卡共骨</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>本卡的 ceiling — L5 hook 只擋字面、行為錯誤需要 L4 review / multi-pass spiral、不是「再寫一條 hook 規則」</td>
      </tr>
  </tbody>
</table>
<p>本卡是 meta-#67/#68/#69 — 把「為什麼這些動作會被跳過」抽出來、答案是「沒外部觸發 + 靠紀律失敗 = 結構性跳過」。三張卡的修法都是「補外部觸發」、不是「自己更努力」。</p>
<hr>
<h2 id="對應的實作篇--系統建設">對應的實作篇 / 系統建設</h2>
<p>把本原則套用到本系統的具體 case：</p>
<ul>
<li><strong><code>make verify-red-green</code> script</strong>（<a href="../test-first-red-before-green/">#69</a>）— L3 工具觸發、把 retrospective 流程從文字協議升級成可執行 target</li>
<li><strong>playwright CI workflow</strong>（push / PR 觸發）— L5 結構性、test 不過就無法 merge</li>
<li><strong>md-check workflow</strong> — L5 強制、卡片格式不對 build fail</li>
<li><strong>本卡誕生過程</strong> — User 提問是 L4 外部觸發、把「該回頭抽 meta」變成有壓力的動作（不然不會做）</li>
</ul>
<p>每一個都是「把高 ROI + 無觸發的工作、補上對應層級的觸發」。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan 含「之後我會 X」</td>
          <td>是 L1 紀律、預期失敗、改成 L3+ 觸發</td>
      </tr>
      <tr>
          <td>TODO list 累積 30+ 項、半年沒減少</td>
          <td>觸發機制壞了、不是「太忙」</td>
      </tr>
      <tr>
          <td>某類重要工作（refactor / doc / monitor）長期沒做</td>
          <td>沒外部觸發、補 L3-L5</td>
      </tr>
      <tr>
          <td>自己責怪「我又拖延了」</td>
          <td>結構問題不是個人問題、停止責怪、改機制</td>
      </tr>
      <tr>
          <td>同團隊不同人做同類工作的執行率差很多</td>
          <td>個別人差是表象、機制設計問題（流程不一致）</td>
      </tr>
      <tr>
          <td>某個 lint / CI rule 改完所有人都自動跟上</td>
          <td>L5 對策成功、適合複用到其他類似工作</td>
      </tr>
      <tr>
          <td>「想到就立刻做」打斷正在做的事</td>
          <td>動作該由觸發排程、不由 thoughts 觸發</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：高 ROI 但無外部觸發的工作 = 結構性跳過、不是個人問題。修法是把觸發外化（工具 / 流程 / 結構）、不是「我下次記得」。「之後我會 X」是 plan-level 警訊、應該轉成「X 會被 Y 觸發」的具體機制。</p>
]]></content:encoded></item><item><title>主策略 + 補強策略：選擇不必互斥</title><link>https://tarrragon.github.io/blog/report/main-strategy-plus-supplementary/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/main-strategy-plus-supplementary/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>多策略選擇（如 &lt;a href="../filter-source-composition-strategies/">#59 五策略&lt;/a>、&lt;a href="../search-engine-matching-mode-mismatch/">#73 五匹配模式&lt;/a>）&lt;strong>預設不是單選&lt;/strong>。能疊加的策略應該疊加、互斥的才需要選。&lt;/p>
&lt;p>最常見的疊加：&lt;strong>root-cause 結構性修法 + 使用者感知補強&lt;/strong>（例如 multi-index 解層錯位 + UX hint 解 prefix-match 預期落差）— 解不同層、互不干擾、合在一起的覆蓋面 &amp;gt; 單選任一。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼預設單選是錯誤前提">為什麼預設單選是錯誤前提&lt;/h2>
&lt;p>呈現多選項時容易進「適配性比較表 → 選最高分」的單選思維。這個思維對「互斥工具選擇」（Vue / React、Postgres / MySQL）成立、對「補強型策略」不成立：&lt;/p>
&lt;ul>
&lt;li>結構性修法（修正根因、長期穩）— 通常需要時間 + 風險&lt;/li>
&lt;li>UX 補強（解使用者感知、立即可見）— 通常 ROI 立刻、但不解根因&lt;/li>
&lt;/ul>
&lt;p>兩者&lt;strong>解的問題層不同&lt;/strong>：根因解了、使用者立刻感受到的混亂仍在；UX 蓋過去了、根因仍在累積技術債。預設單選 = 強迫使用者在「立即解使用者痛苦」與「長期解結構問題」之間二選一、其實兩個都該做。&lt;/p>
&lt;hr>
&lt;h2 id="疊加可行的三條判準">疊加可行的三條判準&lt;/h2>
&lt;p>某兩個策略 X + Y 可疊加 ⇔ 滿足以下全部：&lt;/p>
&lt;h3 id="1-解不同層">1. 解不同層&lt;/h3>
&lt;p>X 動結構 / 資料 / 演算法、Y 動 UI / 訊息 / 預期管理。同層的兩個策略通常衝突（兩種 cache 策略、兩種 routing 策略），不同層的多半互補。&lt;/p>
&lt;p>判讀：把問題分成「根因 / 訊號 / 補償」三層、每層挑 1 個策略 = 疊加組合。&lt;/p>
&lt;h3 id="2-沒副作用衝突">2. 沒副作用衝突&lt;/h3>
&lt;p>X 加上 Y 不會放大彼此副作用、不會產生新 bug。例：multi-index（佔 build time）+ UX hint（佔畫面空間）— 兩個 cost 維度不同、不互相放大。&lt;/p>
&lt;p>反例：fetch-until-quota（多次 round trip）+ aggressive prefetch（更多 round trip）— 同維度副作用會疊加、可能爆炸。&lt;/p>
&lt;h3 id="3-增量成本--預算">3. 增量成本 ≤ 預算&lt;/h3>
&lt;p>第二個策略的實作 + 維護成本 ≤ 它解的問題價值。如果 X 已經解掉 80% 問題、Y 解剩下 20% 但成本是 X 的兩倍 → Y 就是過度工程、不該疊加。&lt;/p>
&lt;hr>
&lt;h2 id="典型疊加模式">典型疊加模式&lt;/h2>
&lt;h3 id="模式一structural-fix--ux-patch">模式一：Structural fix + UX patch&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Structural&lt;/th>
 &lt;th>UX&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-index (&lt;a href="../pattern-multiple-indexes/">#65&lt;/a>)&lt;/td>
 &lt;td>Honest progress UI (&lt;a href="../pattern-honest-progress-ui/">#62&lt;/a>)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query-side pushdown (&lt;a href="../pattern-query-side-pushdown/">#61&lt;/a>)&lt;/td>
 &lt;td>Empty state 三狀態 (&lt;a href="../loading-empty-end-state-distinction/">#57&lt;/a>)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build-time pre-tokenize&lt;/td>
 &lt;td>Prefix-match 限制提示 (&lt;a href="../search-engine-matching-mode-mismatch/">#73&lt;/a>)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Structural 解根因、UX 解使用者當下混亂。即使 structural 還沒 ship、UX patch 可以先 ship 解眼前問題。&lt;/p>
&lt;h3 id="模式二defensive--optimistic">模式二：Defensive + Optimistic&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Defensive&lt;/th>
 &lt;th>Optimistic&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>輸入驗證 / 邊界檢查&lt;/td>
 &lt;td>Default 值合理 / 自動修正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>錯誤訊息精準&lt;/td>
 &lt;td>操作回 undo&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retry with backoff&lt;/td>
 &lt;td>預測性 prefetch&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Defensive 處理失敗、Optimistic 處理成功 — 兩個 happy path 共存、不衝突。&lt;/p>
&lt;h3 id="模式三now--later">模式三：Now + Later&lt;/h3>
&lt;p>「先 ship X 解眼前、Y 下輪做」是一種隱式疊加 — 不是放棄 Y、是延後到風險更可承受的 release window。判準見 &lt;a href="../incremental-shipping-criteria/">#76 分批 ship&lt;/a>。&lt;/p>
&lt;h3 id="模式四selector-strategy-疊加46-50">模式四：Selector strategy 疊加（#46-#50）&lt;/h3>
&lt;p>&lt;a href="../pattern-document-query/">#46&lt;/a> / &lt;a href="../pattern-component-root/">#47&lt;/a> / &lt;a href="../pattern-root-as-parameter/">#48&lt;/a> / &lt;a href="../pattern-closest-lookup/">#49&lt;/a> 四張 selector 起點 pattern 卡乍看互斥（每個元件只能選一個起點）、實際在同一個 handler 內可疊加：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>多策略選擇（如 <a href="../filter-source-composition-strategies/">#59 五策略</a>、<a href="../search-engine-matching-mode-mismatch/">#73 五匹配模式</a>）<strong>預設不是單選</strong>。能疊加的策略應該疊加、互斥的才需要選。</p>
<p>最常見的疊加：<strong>root-cause 結構性修法 + 使用者感知補強</strong>（例如 multi-index 解層錯位 + UX hint 解 prefix-match 預期落差）— 解不同層、互不干擾、合在一起的覆蓋面 &gt; 單選任一。</p>
<hr>
<h2 id="為什麼預設單選是錯誤前提">為什麼預設單選是錯誤前提</h2>
<p>呈現多選項時容易進「適配性比較表 → 選最高分」的單選思維。這個思維對「互斥工具選擇」（Vue / React、Postgres / MySQL）成立、對「補強型策略」不成立：</p>
<ul>
<li>結構性修法（修正根因、長期穩）— 通常需要時間 + 風險</li>
<li>UX 補強（解使用者感知、立即可見）— 通常 ROI 立刻、但不解根因</li>
</ul>
<p>兩者<strong>解的問題層不同</strong>：根因解了、使用者立刻感受到的混亂仍在；UX 蓋過去了、根因仍在累積技術債。預設單選 = 強迫使用者在「立即解使用者痛苦」與「長期解結構問題」之間二選一、其實兩個都該做。</p>
<hr>
<h2 id="疊加可行的三條判準">疊加可行的三條判準</h2>
<p>某兩個策略 X + Y 可疊加 ⇔ 滿足以下全部：</p>
<h3 id="1-解不同層">1. 解不同層</h3>
<p>X 動結構 / 資料 / 演算法、Y 動 UI / 訊息 / 預期管理。同層的兩個策略通常衝突（兩種 cache 策略、兩種 routing 策略），不同層的多半互補。</p>
<p>判讀：把問題分成「根因 / 訊號 / 補償」三層、每層挑 1 個策略 = 疊加組合。</p>
<h3 id="2-沒副作用衝突">2. 沒副作用衝突</h3>
<p>X 加上 Y 不會放大彼此副作用、不會產生新 bug。例：multi-index（佔 build time）+ UX hint（佔畫面空間）— 兩個 cost 維度不同、不互相放大。</p>
<p>反例：fetch-until-quota（多次 round trip）+ aggressive prefetch（更多 round trip）— 同維度副作用會疊加、可能爆炸。</p>
<h3 id="3-增量成本--預算">3. 增量成本 ≤ 預算</h3>
<p>第二個策略的實作 + 維護成本 ≤ 它解的問題價值。如果 X 已經解掉 80% 問題、Y 解剩下 20% 但成本是 X 的兩倍 → Y 就是過度工程、不該疊加。</p>
<hr>
<h2 id="典型疊加模式">典型疊加模式</h2>
<h3 id="模式一structural-fix--ux-patch">模式一：Structural fix + UX patch</h3>
<table>
  <thead>
      <tr>
          <th>Structural</th>
          <th>UX</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-index (<a href="../pattern-multiple-indexes/">#65</a>)</td>
          <td>Honest progress UI (<a href="../pattern-honest-progress-ui/">#62</a>)</td>
      </tr>
      <tr>
          <td>Query-side pushdown (<a href="../pattern-query-side-pushdown/">#61</a>)</td>
          <td>Empty state 三狀態 (<a href="../loading-empty-end-state-distinction/">#57</a>)</td>
      </tr>
      <tr>
          <td>Build-time pre-tokenize</td>
          <td>Prefix-match 限制提示 (<a href="../search-engine-matching-mode-mismatch/">#73</a>)</td>
      </tr>
  </tbody>
</table>
<p>Structural 解根因、UX 解使用者當下混亂。即使 structural 還沒 ship、UX patch 可以先 ship 解眼前問題。</p>
<h3 id="模式二defensive--optimistic">模式二：Defensive + Optimistic</h3>
<table>
  <thead>
      <tr>
          <th>Defensive</th>
          <th>Optimistic</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>輸入驗證 / 邊界檢查</td>
          <td>Default 值合理 / 自動修正</td>
      </tr>
      <tr>
          <td>錯誤訊息精準</td>
          <td>操作回 undo</td>
      </tr>
      <tr>
          <td>Retry with backoff</td>
          <td>預測性 prefetch</td>
      </tr>
  </tbody>
</table>
<p>Defensive 處理失敗、Optimistic 處理成功 — 兩個 happy path 共存、不衝突。</p>
<h3 id="模式三now--later">模式三：Now + Later</h3>
<p>「先 ship X 解眼前、Y 下輪做」是一種隱式疊加 — 不是放棄 Y、是延後到風險更可承受的 release window。判準見 <a href="../incremental-shipping-criteria/">#76 分批 ship</a>。</p>
<h3 id="模式四selector-strategy-疊加46-50">模式四：Selector strategy 疊加（#46-#50）</h3>
<p><a href="../pattern-document-query/">#46</a> / <a href="../pattern-component-root/">#47</a> / <a href="../pattern-root-as-parameter/">#48</a> / <a href="../pattern-closest-lookup/">#49</a> 四張 selector 起點 pattern 卡乍看互斥（每個元件只能選一個起點）、實際在同一個 handler 內可疊加：</p>
<table>
  <thead>
      <tr>
          <th>元件位置</th>
          <th>適合 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Modal / dialog 內定位元素</td>
          <td>#47 元件根變數</td>
      </tr>
      <tr>
          <td>跨 modal 邊界元素（toast、portal）</td>
          <td>#46 全文件 query</td>
      </tr>
      <tr>
          <td>Event target → 找最近容器</td>
          <td>#49 closest</td>
      </tr>
      <tr>
          <td>Test / 多實例</td>
          <td>#48 函式參數</td>
      </tr>
  </tbody>
</table>
<p>同一份 component code 可同時用 #46 + #49（外部 portal 用 document、內部用 closest）— 解不同 selector context、不衝突、增量成本低 = 滿足三條判準。</p>
<p>判讀：「這幾個 pattern 是同層次（互斥）還是不同 context（互補）？」不同 context = 疊加。</p>
<hr>
<h2 id="反模式強迫單選的代價">反模式：強迫單選的代價</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「五選一」當預設</td>
          <td>放掉 80% 互補可能</td>
      </tr>
      <tr>
          <td>用「最佳策略」當銀彈</td>
          <td>漏掉解不同層的問題</td>
      </tr>
      <tr>
          <td>「先做 X、Y 永遠延後」</td>
          <td>Y 變成 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 結構性跳過</td>
      </tr>
      <tr>
          <td>「Y 才是真正的 fix、X 是 hack」</td>
          <td>道德判斷阻止 X 的價值、使用者多受苦一段時間</td>
      </tr>
      <tr>
          <td>把 UX 補強當「掩蓋問題」</td>
          <td>忽略掉「使用者預期管理」也是真實價值</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時該堅持單選">何時該堅持單選</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>真正互斥（同 slot 只能放一個）</td>
          <td>例：UI framework、DB engine、protocol — 選了就排他</td>
      </tr>
      <tr>
          <td>維護成本不可接受</td>
          <td>兩條 path 並存的 cognitive load &gt; 收益</td>
      </tr>
      <tr>
          <td>一致性比覆蓋面重要</td>
          <td>例：UI 設計語言、API 慣例 — 多選會稀釋</td>
      </tr>
      <tr>
          <td>探索期、還沒驗證</td>
          <td>多選 = 多戰線、超過驗證能力</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>疊加的代價 &gt; 疊加的收益</strong>。其他情境都該先檢查「能不能疊加」。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>#59 列了五策略、本卡點出「不必選一個、常配對使用」</td>
      </tr>
      <tr>
          <td><a href="../pattern-honest-progress-ui/">#62 誠實進度 UI</a></td>
          <td>UX 補強的範本、跟結構修法疊加效果好</td>
      </tr>
      <tr>
          <td><a href="../pattern-multiple-indexes/">#65 多 index pattern</a></td>
          <td>結構修法的範本</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 搜尋匹配模式不對齊</a></td>
          <td>五個策略中 D（UX hint）+ B/C（結構修法）就是疊加典型</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship 準則</a></td>
          <td>「先 X 後 Y」是疊加在時間軸上的展開</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「策略數」維度的展開 — 單選 vs 主+補強疊加</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「五策略選一」當預設</td>
          <td>檢查能不能疊加、列出組合</td>
      </tr>
      <tr>
          <td>推薦時只給一個策略、沒講「也可以加 X」</td>
          <td>補上「再加 Y 風險不大」的選項</td>
      </tr>
      <tr>
          <td>使用者問「那 Y 還做嗎」</td>
          <td>你已經把 Y 隱式排除、講清楚 Y 的位置</td>
      </tr>
      <tr>
          <td>「真正的 fix 是 Z、其他是 hack」道德判斷</td>
          <td>退一步檢查：在 Z 完成前、有沒有便宜的減痛</td>
      </tr>
      <tr>
          <td>兩個策略放一起就互相打架</td>
          <td>違反判準 1 或 2、退回單選</td>
      </tr>
      <tr>
          <td>第二個策略 ROI 邊際</td>
          <td>違反判準 3、不疊加</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：策略選擇問「能不能疊加」優先於「選哪個」 — 多數工程問題的最佳解是「多層次組合」、不是「找出唯一答案」。</p>
]]></content:encoded></item><item><title>分批 ship：低風險可見價值先行、結構性下輪</title><link>https://tarrragon.github.io/blog/report/incremental-shipping-criteria/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/incremental-shipping-criteria/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫到「該 ship 哪些」時、預設&lt;strong>分批&lt;/strong>：把 changes 沿三軸切 — &lt;strong>使用者可見性高 + 風險低 + 驗證簡單&lt;/strong> 的先 ship、&lt;strong>結構性 + 風險高 + 需驗證&lt;/strong> 的下輪。對抗「都做完才能 ship」的整體性衝動。&lt;/p>
&lt;p>分批的真正價值：&lt;strong>降低每次 review 的 cognitive load + 加速使用者拿到價值 + 讓回退單位更小&lt;/strong>。整批 ship 的代價是 review 變慢、bug 排查面變大、出問題回退要拖整批。&lt;/p>
&lt;hr>
&lt;h2 id="三軸切分">三軸切分&lt;/h2>
&lt;p>切「現在 ship vs 下輪 ship」用三個維度：&lt;/p>
&lt;h3 id="軸-1使用者可見性">軸 1：使用者可見性&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>高&lt;/strong>：使用者立刻能感受到差異（UI 改變、訊息精準、互動更順）&lt;/li>
&lt;li>&lt;strong>低&lt;/strong>：純內部結構（refactor、index 重建、protocol 升級）&lt;/li>
&lt;/ul>
&lt;p>可見性高 → 早 ship 拿價值；可見性低 → 早晚 ship 差別不大、可以等更多 confidence。&lt;/p>
&lt;h3 id="軸-2風險暴露面">軸 2：風險暴露面&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>低&lt;/strong>：純加法（新檔案、新欄位、新 endpoint）— 不影響既有 path&lt;/li>
&lt;li>&lt;strong>中&lt;/strong>：修改既有 code path 但有 fallback / 開關&lt;/li>
&lt;li>&lt;strong>高&lt;/strong>：替換、刪除、結構重組 — 沒退路或退路成本高&lt;/li>
&lt;/ul>
&lt;p>低風險 → 早 ship、出問題範圍小；高風險 → 等 confidence、配 staged rollout / feature flag。&lt;/p>
&lt;h3 id="軸-3驗證需求">軸 3：驗證需求&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>低&lt;/strong>：邏輯簡單、unit test 夠、可肉眼驗收&lt;/li>
&lt;li>&lt;strong>中&lt;/strong>：需要 E2E、多瀏覽器 / 多裝置驗證&lt;/li>
&lt;li>&lt;strong>高&lt;/strong>：需要長時觀測、production 流量壓測、A/B 比較&lt;/li>
&lt;/ul>
&lt;p>低驗證需求 → 早 ship；高驗證需求 → 等驗證流程跑完、不為趕時間跳過驗收。&lt;/p>
&lt;hr>
&lt;h2 id="切分矩陣">切分矩陣&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>可見性&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;th>驗證&lt;/th>
 &lt;th>建議&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>&lt;strong>立刻 ship&lt;/strong>（最高 ROI / 風險比）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跑完 E2E 就 ship&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;td>配 feature flag、staged rollout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>順便 ship、合併進其他 PR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>&lt;strong>下輪&lt;/strong>（沒急、值得等驗證）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>看 batch 是否方便、不單獨 ship&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵 row：&lt;strong>「高可見 + 低風險 + 低驗證」就是先 ship 的甜蜜點&lt;/strong> — 例：UX hint、empty state 訊息、明顯的 UI 修正。&lt;/p>
&lt;hr>
&lt;h2 id="先-ship-dbc-下輪的典型範例">「先 ship D、B/C 下輪」的典型範例&lt;/h2>
&lt;p>來源：&lt;a href="../search-engine-matching-mode-mismatch/">#73 prefix-match 限制&lt;/a>&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>軸 1 可見性&lt;/th>
 &lt;th>軸 2 風險&lt;/th>
 &lt;th>軸 3 驗證&lt;/th>
 &lt;th>排序&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>D（UX hint：「搜尋為前綴匹配」）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>低（純加 UI 文字）&lt;/td>
 &lt;td>低（不影響既有功能）&lt;/td>
 &lt;td>&lt;strong>先 ship&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C（client-side substring fallback）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中（多一條 path）&lt;/td>
 &lt;td>中（要驗證效能）&lt;/td>
 &lt;td>下輪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B（build-time pre-tokenize）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高（改 build pipeline）&lt;/td>
 &lt;td>高（要驗證 index size、search ranking）&lt;/td>
 &lt;td>下輪&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>D 滿足「高可見 + 低風險 + 低驗證」、立刻 ship 解眼前混亂。B/C 解根因、但風險與驗證需求高、下輪做。&lt;strong>這個排序不是「重要程度」、是「ship 順序」&lt;/strong> — 重要程度 B/C &amp;gt; D、但 ship 順序 D &amp;gt; B &amp;gt; C。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫到「該 ship 哪些」時、預設<strong>分批</strong>：把 changes 沿三軸切 — <strong>使用者可見性高 + 風險低 + 驗證簡單</strong> 的先 ship、<strong>結構性 + 風險高 + 需驗證</strong> 的下輪。對抗「都做完才能 ship」的整體性衝動。</p>
<p>分批的真正價值：<strong>降低每次 review 的 cognitive load + 加速使用者拿到價值 + 讓回退單位更小</strong>。整批 ship 的代價是 review 變慢、bug 排查面變大、出問題回退要拖整批。</p>
<hr>
<h2 id="三軸切分">三軸切分</h2>
<p>切「現在 ship vs 下輪 ship」用三個維度：</p>
<h3 id="軸-1使用者可見性">軸 1：使用者可見性</h3>
<ul>
<li><strong>高</strong>：使用者立刻能感受到差異（UI 改變、訊息精準、互動更順）</li>
<li><strong>低</strong>：純內部結構（refactor、index 重建、protocol 升級）</li>
</ul>
<p>可見性高 → 早 ship 拿價值；可見性低 → 早晚 ship 差別不大、可以等更多 confidence。</p>
<h3 id="軸-2風險暴露面">軸 2：風險暴露面</h3>
<ul>
<li><strong>低</strong>：純加法（新檔案、新欄位、新 endpoint）— 不影響既有 path</li>
<li><strong>中</strong>：修改既有 code path 但有 fallback / 開關</li>
<li><strong>高</strong>：替換、刪除、結構重組 — 沒退路或退路成本高</li>
</ul>
<p>低風險 → 早 ship、出問題範圍小；高風險 → 等 confidence、配 staged rollout / feature flag。</p>
<h3 id="軸-3驗證需求">軸 3：驗證需求</h3>
<ul>
<li><strong>低</strong>：邏輯簡單、unit test 夠、可肉眼驗收</li>
<li><strong>中</strong>：需要 E2E、多瀏覽器 / 多裝置驗證</li>
<li><strong>高</strong>：需要長時觀測、production 流量壓測、A/B 比較</li>
</ul>
<p>低驗證需求 → 早 ship；高驗證需求 → 等驗證流程跑完、不為趕時間跳過驗收。</p>
<hr>
<h2 id="切分矩陣">切分矩陣</h2>
<table>
  <thead>
      <tr>
          <th>可見性</th>
          <th>風險</th>
          <th>驗證</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>低</td>
          <td>低</td>
          <td><strong>立刻 ship</strong>（最高 ROI / 風險比）</td>
      </tr>
      <tr>
          <td>高</td>
          <td>低</td>
          <td>中</td>
          <td>跑完 E2E 就 ship</td>
      </tr>
      <tr>
          <td>高</td>
          <td>高</td>
          <td>中-高</td>
          <td>配 feature flag、staged rollout</td>
      </tr>
      <tr>
          <td>低</td>
          <td>低</td>
          <td>低</td>
          <td>順便 ship、合併進其他 PR</td>
      </tr>
      <tr>
          <td>低</td>
          <td>高</td>
          <td>高</td>
          <td><strong>下輪</strong>（沒急、值得等驗證）</td>
      </tr>
      <tr>
          <td>低</td>
          <td>中</td>
          <td>中</td>
          <td>看 batch 是否方便、不單獨 ship</td>
      </tr>
  </tbody>
</table>
<p>關鍵 row：<strong>「高可見 + 低風險 + 低驗證」就是先 ship 的甜蜜點</strong> — 例：UX hint、empty state 訊息、明顯的 UI 修正。</p>
<hr>
<h2 id="先-ship-dbc-下輪的典型範例">「先 ship D、B/C 下輪」的典型範例</h2>
<p>來源：<a href="../search-engine-matching-mode-mismatch/">#73 prefix-match 限制</a></p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>軸 1 可見性</th>
          <th>軸 2 風險</th>
          <th>軸 3 驗證</th>
          <th>排序</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>D（UX hint：「搜尋為前綴匹配」）</td>
          <td>高</td>
          <td>低（純加 UI 文字）</td>
          <td>低（不影響既有功能）</td>
          <td><strong>先 ship</strong></td>
      </tr>
      <tr>
          <td>C（client-side substring fallback）</td>
          <td>中</td>
          <td>中（多一條 path）</td>
          <td>中（要驗證效能）</td>
          <td>下輪</td>
      </tr>
      <tr>
          <td>B（build-time pre-tokenize）</td>
          <td>中</td>
          <td>高（改 build pipeline）</td>
          <td>高（要驗證 index size、search ranking）</td>
          <td>下輪</td>
      </tr>
  </tbody>
</table>
<p>D 滿足「高可見 + 低風險 + 低驗證」、立刻 ship 解眼前混亂。B/C 解根因、但風險與驗證需求高、下輪做。<strong>這個排序不是「重要程度」、是「ship 順序」</strong> — 重要程度 B/C &gt; D、但 ship 順序 D &gt; B &gt; C。</p>
<hr>
<h2 id="為什麼全做完才-ship是反模式">為什麼「全做完才 ship」是反模式</h2>
<p>幾個常見藉口 + 為什麼站不住：</p>
<table>
  <thead>
      <tr>
          <th>藉口</th>
          <th>為什麼站不住</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「分批 ship 不完整」</td>
          <td>完整是工程師視角、使用者只看自己當下能不能用上</td>
      </tr>
      <tr>
          <td>「PR 越大越好 review」</td>
          <td>反、PR 越大 review 越粗、bug 越多漏</td>
      </tr>
      <tr>
          <td>「下輪我會做完」</td>
          <td>違反 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> — 沒 trigger 會跳過</td>
      </tr>
      <tr>
          <td>「測試一起 ship 比較好驗」</td>
          <td>反、批次測試會放大 noise、各個獨立驗證更乾淨</td>
      </tr>
      <tr>
          <td>「regression 一起爆比較好排查」</td>
          <td>反、regression 範圍越大越難 bisect</td>
      </tr>
  </tbody>
</table>
<p>實際上「全做完才 ship」最常見的真實原因是：<strong>沒花時間想分批</strong>。預設分批就會自然分。</p>
<hr>
<h2 id="分批反模式">分批反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把高風險砍進「先 ship」 batch 為了趕 demo</td>
          <td>風險爆炸時所有先 ship 的內容跟著退</td>
          <td>用 feature flag、不要硬塞</td>
      </tr>
      <tr>
          <td>「下輪做 X」沒寫進系統</td>
          <td>X 變成 <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a></td>
          <td>寫成 issue / TODO with deadline</td>
      </tr>
      <tr>
          <td>第一批漏掉 telemetry</td>
          <td>下輪沒資料判斷 X 該怎麼設計</td>
          <td>第一批就埋觀測</td>
      </tr>
      <tr>
          <td>分太細、每個 PR 都太小、整體 review 成本反而高</td>
          <td>分批本身有 overhead</td>
          <td>每批 ≥ 一個完整使用者 user-story 的價值</td>
      </tr>
      <tr>
          <td>第一批 ship 後就鬆懈、忘了下輪</td>
          <td>結構性陷阱</td>
          <td>把下輪寫進 calendar / sprint plan</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時該堅持一次完整-ship">何時該堅持「一次完整 ship」</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Feature 拆了不能用（atomic from user view）</td>
          <td>強制 atomic、用 feature flag 控制可見性</td>
      </tr>
      <tr>
          <td>Migration / Schema change</td>
          <td>半 ship 會破壞既有資料 / 流程一致性</td>
      </tr>
      <tr>
          <td>安全修補</td>
          <td>不能 leak 知道一半</td>
      </tr>
      <tr>
          <td>跨服務 protocol upgrade（client + server 必須對齊）</td>
          <td>半邊改另一半就破</td>
      </tr>
      <tr>
          <td>第一次設定 baseline</td>
          <td>沒 baseline 可比較、下輪改才有 reference</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>ship 一半比都不 ship 更壞</strong>。其他情境分批優先。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>分批 ship 對應「Ship 前 / Ship 後」分散 — 每批各自走完四 checkpoint</td>
      </tr>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強</a></td>
          <td>補強策略通常先 ship、主策略下輪 — 兩卡互補</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「下輪做」需要結構性 trigger（issue + deadline）、不靠紀律</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>每批的範圍從窄起、有證據再擴張</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「批次邊界」維度的展開 — 一次 vs 分批</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PR diff &gt; 800 行、含多個 feature</td>
          <td>拆批、各自走 review</td>
      </tr>
      <tr>
          <td>「等 X 做完一起 ship」</td>
          <td>用三軸檢查 X 是否該獨立 ship</td>
      </tr>
      <tr>
          <td>Feature flag 名稱長期堆積、沒清掉</td>
          <td>「下輪清掉」沒 trigger、補 <a href="../external-trigger-for-high-roi-work/">#72 L3-L5 對策</a></td>
      </tr>
      <tr>
          <td>「這次先這樣、下次再優化」每次都不發生</td>
          <td>下輪沒 trigger、把它寫進系統</td>
      </tr>
      <tr>
          <td>第一批 ship 後 production 出問題、回退範圍大</td>
          <td>第一批塞太多、檢查為什麼沒分更細</td>
      </tr>
      <tr>
          <td>使用者抱怨「等很久才有 X」</td>
          <td>可能 X 早就可分批 ship、檢查阻塞點</td>
      </tr>
      <tr>
          <td>推薦「等 B/C 都做完再 ship」</td>
          <td>違反三軸、應該 D 先 ship</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：「ship 順序 ≠ 重要程度」。使用者可見性高 + 風險低 + 驗證需求低 = 先 ship 甜蜜點、即使在重要程度上不是 top。等所有結構性修法都做完才 ship、是把重要程度誤當成 ship 順序的常見錯誤。</p>
]]></content:encoded></item><item><title>決策對話的五個維度：保持完整選擇空間</title><link>https://tarrragon.github.io/blog/report/decision-dialogue-dimensions/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decision-dialogue-dimensions/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>對話中要使用者決策時、有五個獨立維度可以選擇 — 不該預設 collapse 到單一格子：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>預設窄格（常見）&lt;/th>
 &lt;th>鬆綁後（多數情境）&lt;/th>
 &lt;th>對應卡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>呈現格式&lt;/td>
 &lt;td>開放問&lt;/td>
 &lt;td>選項表 + 推薦&lt;/td>
 &lt;td>&lt;a href="../decision-presentation-options-recommendation/">#74&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略數&lt;/td>
 &lt;td>單選&lt;/td>
 &lt;td>主 + 補強疊加&lt;/td>
 &lt;td>&lt;a href="../main-strategy-plus-supplementary/">#75&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>批次邊界&lt;/td>
 &lt;td>一次做完&lt;/td>
 &lt;td>分批 ship&lt;/td>
 &lt;td>&lt;a href="../incremental-shipping-criteria/">#76&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間軸&lt;/td>
 &lt;td>立刻決&lt;/td>
 &lt;td>結構性延後&lt;/td>
 &lt;td>&lt;a href="../decide-later-as-valid-option/">#77&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>選項類型&lt;/td>
 &lt;td>單選 radio&lt;/td>
 &lt;td>複選 checkbox&lt;/td>
 &lt;td>&lt;a href="../retrospective-multi-select-default/">#78&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心命題&lt;/strong>：每個維度都是獨立的、五個維度展開後是 2^5 = 32 種組合。預設都選窄格 = 對使用者問最窄的問題、結果通常品質低。應該針對每個情境 reason about「這維度該選哪邊」、不是無腦套預設。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼預設都是窄格">為什麼預設都是窄格&lt;/h2>
&lt;p>每個維度的窄格都是「最容易寫」的選項：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>開放問&lt;/strong>比結構表少打字&lt;/li>
&lt;li>&lt;strong>單策略&lt;/strong>比「策略 A + 補強 B」少思考&lt;/li>
&lt;li>&lt;strong>一次做完&lt;/strong>比設計分批邊界少規劃&lt;/li>
&lt;li>&lt;strong>立刻決&lt;/strong>比寫延後條件少協議&lt;/li>
&lt;li>&lt;strong>單選 radio&lt;/strong> 比寫「互不衝突、可全選」少說明&lt;/li>
&lt;/ul>
&lt;p>合起來：窄格是 &lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度&lt;/a> 的具體展現 — 每一維都是「容易寫但跟使用者意圖反相關」的方向。&lt;/p>
&lt;p>預設窄格的真正代價：使用者被迫在錯位的問題空間中作答、即使最終做了決定、決定的品質受呈現格式 cap。&lt;/p>
&lt;hr>
&lt;h2 id="五維度的判讀次序">五維度的判讀次序&lt;/h2>
&lt;p>實務上、依序檢查五個維度：&lt;/p>
&lt;h3 id="步驟-1選項類型78-是執行還是反省">步驟 1：選項類型（#78）— 是執行還是反省？&lt;/h3>
&lt;p>執行類決策（用 A 還是 B 工具、選哪個策略） → 通常單選。
反省類決策（這次學到什麼、下一步該往哪走） → 通常複選。&lt;/p>
&lt;p>判讀：「這次 output 該收斂到一個答案還是攤開多面向？」收斂 → 單選；攤開 → 複選。&lt;/p>
&lt;h3 id="步驟-2時間軸77-現在能決嗎">步驟 2：時間軸（#77）— 現在能決嗎？&lt;/h3>
&lt;p>context 完整 → 現在決。
context 缺 → 延後 + 寫條件。&lt;/p>
&lt;p>判讀：「我（agent）有沒有提供能讓使用者下決定的全部資訊？」沒有 → 主動標延後選項。&lt;/p>
&lt;h3 id="步驟-3策略數75-單選還是疊加">步驟 3：策略數（#75）— 單選還是疊加？&lt;/h3>
&lt;p>策略間互斥（同 slot 只能放一個） → 單選。
策略間互補（解不同層） → 疊加。&lt;/p>
&lt;p>判讀：「這些策略是否解不同層？」是 → 提疊加組合（如 structural + UX）。&lt;/p>
&lt;h3 id="步驟-4批次邊界76-一次還是分批">步驟 4：批次邊界（#76）— 一次還是分批？&lt;/h3>
&lt;p>純 atomic（拆了不能用） → 一次。
可分（高可見 + 低風險的部分能獨立 ship） → 分批。&lt;/p>
&lt;p>判讀：「先 ship 高 ROI / 低風險那部分、剩下下輪」是否可行？可行 → 分批。&lt;/p>
&lt;h3 id="步驟-5呈現格式74-開放還是結構">步驟 5：呈現格式（#74）— 開放還是結構？&lt;/h3>
&lt;p>純探索 / 主觀偏好 → 開放。
有客觀適配性可比 → 結構表 + 推薦。&lt;/p>
&lt;p>判讀：「我能不能列選項 + 適配性 + 推薦？」能 → 結構；不能 → 探索性開放。&lt;/p>
&lt;hr>
&lt;h2 id="反模式collapse-到單一格子的常見變種">反模式：collapse 到單一格子的常見變種&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變種&lt;/th>
 &lt;th>五維選擇&lt;/th>
 &lt;th>為什麼錯&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;ldquo;你想怎麼做？&amp;rdquo;&lt;/td>
 &lt;td>開放問 + 立刻決 + 單選 + 一次 + 單策略&lt;/td>
 &lt;td>最窄、把整個問題空間丟回去&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;推薦 A、要嗎？&amp;rdquo;&lt;/td>
 &lt;td>結構但只列推薦 + 立刻 + 單選 + 一次 + 單策略&lt;/td>
 &lt;td>隱藏選項、推薦不可質疑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;ABCDE 你選哪個？&amp;rdquo;&lt;/td>
 &lt;td>結構 + 立刻 + 單選 radio + 一次 + 單策略&lt;/td>
 &lt;td>漏掉「全選」「延後」「疊加」三種合法回應&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;做完 X 才能繼續、要做嗎？&amp;rdquo;&lt;/td>
 &lt;td>結構 + 立刻 + 單選 + 一次 + 單策略&lt;/td>
 &lt;td>漏掉分批選項&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;這次學到 X、下次注意&amp;rdquo;&lt;/td>
 &lt;td>反省題壓單選 + 立刻 + 一次&lt;/td>
 &lt;td>反省維度被 collapse、其他學習面向被丟&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個變種都是「五個維度都選窄格」的具體展現。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>對話中要使用者決策時、有五個獨立維度可以選擇 — 不該預設 collapse 到單一格子：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>預設窄格（常見）</th>
          <th>鬆綁後（多數情境）</th>
          <th>對應卡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>呈現格式</td>
          <td>開放問</td>
          <td>選項表 + 推薦</td>
          <td><a href="../decision-presentation-options-recommendation/">#74</a></td>
      </tr>
      <tr>
          <td>策略數</td>
          <td>單選</td>
          <td>主 + 補強疊加</td>
          <td><a href="../main-strategy-plus-supplementary/">#75</a></td>
      </tr>
      <tr>
          <td>批次邊界</td>
          <td>一次做完</td>
          <td>分批 ship</td>
          <td><a href="../incremental-shipping-criteria/">#76</a></td>
      </tr>
      <tr>
          <td>時間軸</td>
          <td>立刻決</td>
          <td>結構性延後</td>
          <td><a href="../decide-later-as-valid-option/">#77</a></td>
      </tr>
      <tr>
          <td>選項類型</td>
          <td>單選 radio</td>
          <td>複選 checkbox</td>
          <td><a href="../retrospective-multi-select-default/">#78</a></td>
      </tr>
  </tbody>
</table>
<p><strong>核心命題</strong>：每個維度都是獨立的、五個維度展開後是 2^5 = 32 種組合。預設都選窄格 = 對使用者問最窄的問題、結果通常品質低。應該針對每個情境 reason about「這維度該選哪邊」、不是無腦套預設。</p>
<hr>
<h2 id="為什麼預設都是窄格">為什麼預設都是窄格</h2>
<p>每個維度的窄格都是「最容易寫」的選項：</p>
<ul>
<li><strong>開放問</strong>比結構表少打字</li>
<li><strong>單策略</strong>比「策略 A + 補強 B」少思考</li>
<li><strong>一次做完</strong>比設計分批邊界少規劃</li>
<li><strong>立刻決</strong>比寫延後條件少協議</li>
<li><strong>單選 radio</strong> 比寫「互不衝突、可全選」少說明</li>
</ul>
<p>合起來：窄格是 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a> 的具體展現 — 每一維都是「容易寫但跟使用者意圖反相關」的方向。</p>
<p>預設窄格的真正代價：使用者被迫在錯位的問題空間中作答、即使最終做了決定、決定的品質受呈現格式 cap。</p>
<hr>
<h2 id="五維度的判讀次序">五維度的判讀次序</h2>
<p>實務上、依序檢查五個維度：</p>
<h3 id="步驟-1選項類型78-是執行還是反省">步驟 1：選項類型（#78）— 是執行還是反省？</h3>
<p>執行類決策（用 A 還是 B 工具、選哪個策略） → 通常單選。
反省類決策（這次學到什麼、下一步該往哪走） → 通常複選。</p>
<p>判讀：「這次 output 該收斂到一個答案還是攤開多面向？」收斂 → 單選；攤開 → 複選。</p>
<h3 id="步驟-2時間軸77-現在能決嗎">步驟 2：時間軸（#77）— 現在能決嗎？</h3>
<p>context 完整 → 現在決。
context 缺 → 延後 + 寫條件。</p>
<p>判讀：「我（agent）有沒有提供能讓使用者下決定的全部資訊？」沒有 → 主動標延後選項。</p>
<h3 id="步驟-3策略數75-單選還是疊加">步驟 3：策略數（#75）— 單選還是疊加？</h3>
<p>策略間互斥（同 slot 只能放一個） → 單選。
策略間互補（解不同層） → 疊加。</p>
<p>判讀：「這些策略是否解不同層？」是 → 提疊加組合（如 structural + UX）。</p>
<h3 id="步驟-4批次邊界76-一次還是分批">步驟 4：批次邊界（#76）— 一次還是分批？</h3>
<p>純 atomic（拆了不能用） → 一次。
可分（高可見 + 低風險的部分能獨立 ship） → 分批。</p>
<p>判讀：「先 ship 高 ROI / 低風險那部分、剩下下輪」是否可行？可行 → 分批。</p>
<h3 id="步驟-5呈現格式74-開放還是結構">步驟 5：呈現格式（#74）— 開放還是結構？</h3>
<p>純探索 / 主觀偏好 → 開放。
有客觀適配性可比 → 結構表 + 推薦。</p>
<p>判讀：「我能不能列選項 + 適配性 + 推薦？」能 → 結構；不能 → 探索性開放。</p>
<hr>
<h2 id="反模式collapse-到單一格子的常見變種">反模式：collapse 到單一格子的常見變種</h2>
<table>
  <thead>
      <tr>
          <th>變種</th>
          <th>五維選擇</th>
          <th>為什麼錯</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ldquo;你想怎麼做？&rdquo;</td>
          <td>開放問 + 立刻決 + 單選 + 一次 + 單策略</td>
          <td>最窄、把整個問題空間丟回去</td>
      </tr>
      <tr>
          <td>&ldquo;推薦 A、要嗎？&rdquo;</td>
          <td>結構但只列推薦 + 立刻 + 單選 + 一次 + 單策略</td>
          <td>隱藏選項、推薦不可質疑</td>
      </tr>
      <tr>
          <td>&ldquo;ABCDE 你選哪個？&rdquo;</td>
          <td>結構 + 立刻 + 單選 radio + 一次 + 單策略</td>
          <td>漏掉「全選」「延後」「疊加」三種合法回應</td>
      </tr>
      <tr>
          <td>&ldquo;做完 X 才能繼續、要做嗎？&rdquo;</td>
          <td>結構 + 立刻 + 單選 + 一次 + 單策略</td>
          <td>漏掉分批選項</td>
      </tr>
      <tr>
          <td>&ldquo;這次學到 X、下次注意&rdquo;</td>
          <td>反省題壓單選 + 立刻 + 一次</td>
          <td>反省維度被 collapse、其他學習面向被丟</td>
      </tr>
  </tbody>
</table>
<p>每個變種都是「五個維度都選窄格」的具體展現。</p>
<hr>
<h2 id="鬆綁後的範本">鬆綁後的範本</h2>
<p>把五維選擇全部明示的決策呈現：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">## 我看到的選項
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">| 選項 | 適配性 | 取捨 |
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">|---|---|---|
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">| A 結構性修法 | 解根因 | 風險高、要驗證 |
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">| B UX 補強 | 立即可見 | 不解根因 |
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">| C 不做 | 0 成本 | 使用者繼續手動 |
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">| **延後（補 X 再決）** | 等 context | 條件：跑完 telemetry |
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">## 推薦組合
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">**B 先 ship、A 下輪**（疊加 + 分批）— B 解眼前痛、A 在 telemetry 證實後再投入結構修法。C 不選因為使用者會抱怨。
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">## 你的選擇空間
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">- 同意（B 現在、A 下輪）
</span></span><span class="line"><span class="ln">17</span><span class="cl">- 改順序（A 先、B 下輪）
</span></span><span class="line"><span class="ln">18</span><span class="cl">- 加 / 減：把 C 加進來、或把 B 拿掉
</span></span><span class="line"><span class="ln">19</span><span class="cl">- 延後：先補 telemetry 再決
</span></span><span class="line"><span class="ln">20</span><span class="cl">- **任意組合可複選**（除非說明互斥）</span></span></code></pre></div><p>關鍵：<strong>主動展開五個維度的選擇空間</strong>、不要預設 collapse。使用者要選窄格是他們的選擇、不是你預設替他們選。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>五個維度的「窄格」都是「容易寫」、本卡是 #67 在決策對話的具體展現</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>「展開五維度」是高 ROI 但無觸發的工作（多打字、慢）、需要協議結構強制</td>
      </tr>
      <tr>
          <td><a href="../filter-instruction-clarification/">#58 模糊指令的篩選三問</a></td>
          <td>三問就是 agent 對使用者的決策呈現、本卡點出三問之外還有四個維度可調</td>
      </tr>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>#59 的五策略 × 適配性表是「呈現維度」+「策略疊加維度」的展現</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>「分批 ship」維度 = 範圍從窄起、有證據再擴張</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="子卡片地圖">子卡片地圖</h2>
<p>#74-#78 各自對應一個維度、互不重疊、合起來覆蓋 32 種決策對話組合。讀法建議：</p>
<ul>
<li><strong>遇到具體情境</strong>：依步驟 1-5 找對應卡（例如「這個是反省題嗎？」→ #78）</li>
<li><strong>第一次接觸</strong>：先讀本卡（#79）建立五維 mental model、再讀子卡學模板</li>
<li><strong>review 自己對話</strong>：拿五維 checklist 掃一遍、看哪維 collapse 了</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「你想怎麼做？」</td>
          <td>五維全 collapse、退回展開</td>
      </tr>
      <tr>
          <td>推薦時只列一個選項</td>
          <td>漏「策略疊加」+「延後」維度</td>
      </tr>
      <tr>
          <td>「等做完再 ship」一次塞太多</td>
          <td>漏「分批」維度</td>
      </tr>
      <tr>
          <td>反省題用單選格式</td>
          <td>漏「複選」維度</td>
      </tr>
      <tr>
          <td>使用者每次都回 &ldquo;都做&rdquo; 或 &ldquo;你決定&rdquo;</td>
          <td>你問太窄、他們在掙脫格子</td>
      </tr>
      <tr>
          <td>推薦後總是被反對</td>
          <td>推薦的維度組合錯位、讓使用者 reverse engineer</td>
      </tr>
      <tr>
          <td>想不起來該怎麼呈現</td>
          <td>套五步判讀、依序檢查</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：對話中的「決策」是多維選擇空間、不是單點題目。<strong>預設展開、選窄格要證明</strong> — 跟 #78「不互斥是預設、互斥要證明」同一條結構。把選擇空間攤開的成本是「多打幾段字」、不攤開的代價是「使用者長期被塞進錯位的格子」。</p>
]]></content:encoded></item><item><title>卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開</title><link>https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>知識卡片系統的成型不是「想清楚再寫」、是&lt;strong>多輪迭代浮現&lt;/strong>：&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">原始對話素材
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ 識別重複結構
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">原子卡（每張一個小現象）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ 串連、識別共同骨架
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">meta-卡（抽上層原則）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> ↓ 沉澱成可重複使用的 protocol
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">reference（可直接套用的 checklist + 模板）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> ↓ L3 觸發機制
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">SKILL（自動觸發 reference）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層都解上一層的限制、不是替代。&lt;strong>原子卡保留具體 case 的細節&lt;/strong>（被反例反駁時可保留）、&lt;strong>meta-卡提供跨情境的判讀框架&lt;/strong>（避免每次重新推理）、&lt;strong>reference 沉澱成可直接套用的步驟&lt;/strong>（消除「知道但忘記用」的鴻溝）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼一次寫不完">為什麼一次寫不完&lt;/h2>
&lt;p>第一次接觸現象時、看到的是&lt;strong>具體 case 的表面&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>看到「使用者說『我再想想』」 → 先寫成「[#77] 延後是合法選項」&lt;/li>
&lt;li>看到「使用者說『1+2』」 → 先寫成「[#78] 反省題複選」&lt;/li>
&lt;li>看到「使用者反駁推薦」 → 先寫成「[#74] 決策呈現格式」&lt;/li>
&lt;/ul>
&lt;p>每張原子卡解 1 個情境、自包含可讀。但&lt;strong>串連在一起時才浮現的結構&lt;/strong>（例：「五個獨立維度」）需要看到 ≥ 3-5 張原子卡之後才看得出。&lt;strong>第一次寫不出來、不是因為沒想清楚、是因為原料不夠&lt;/strong>。&lt;/p>
&lt;p>催熟原子卡之前先寫 meta-卡 = 從少數 case 過度推論、產生 over-fit 結構、後續發現新 case 不符就要重寫。&lt;/p>
&lt;hr>
&lt;h2 id="三層的職責分工">三層的職責分工&lt;/h2>
&lt;h3 id="layer-1原子卡">Layer 1：原子卡&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：單一現象 / 單一錯誤 / 單一情境。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從具體事件浮現（事後檢討）&lt;/li>
&lt;li>自包含、不依賴其他卡也能讀&lt;/li>
&lt;li>含「反模式 / 修法 / 何時不適用」三段&lt;/li>
&lt;li>給未來自己看：「啊我再次遇到這個」&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項&lt;/a> 是從一次具體對話中「使用者說『不用現在決策』、agent 加壓」浮現。&lt;/p>
&lt;h3 id="layer-2meta-卡">Layer 2：Meta-卡&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：N 張原子卡的共同骨架。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不是新原則、是把已存在的原則上抽&lt;/li>
&lt;li>通常出現在「寫 N 張原子卡之後、發現他們其實同一件事」&lt;/li>
&lt;li>提供跨情境判讀（&amp;ldquo;這個情境屬於哪一維度?&amp;quot;）&lt;/li>
&lt;li>給「已有 mental model 的讀者」加深、不取代原子卡&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度&lt;/a> 是寫完 [#74-#78] 五張原子卡後、發現他們各對應一個獨立維度。沒寫 #79 之前 #74-#78 是五張平行卡、寫完 #79 後形成有結構的網。&lt;/p>
&lt;h3 id="layer-3reference">Layer 3：Reference&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：把 N 張卡的判讀流程沉澱成可直接套用的 step-by-step。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不是教學、是 lookup table + checklist&lt;/li>
&lt;li>在實作中被翻開、不是讀爽的&lt;/li>
&lt;li>結尾有 self-check 讓使用者驗證自己沒漏&lt;/li>
&lt;li>跟一張具體任務 / 觸發情境對應&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;code>references/decision-dialogue.md&lt;/code>（在 SKILL 內）— 把 #74-#79 翻譯成「五步判讀 + 完整模板 + self-check」、agent 寫 decision 之前看一遍就夠了。&lt;/p>
&lt;hr>
&lt;h2 id="多層迭代的訊號什麼時候該往上抽">多層迭代的訊號：什麼時候該往上抽？&lt;/h2>
&lt;h3 id="訊號-1寫第-n-張卡時發現大段內容跟前一張重複">訊號 1：寫第 N 張卡時、發現大段內容跟前一張重複&lt;/h3>
&lt;p>→ 兩張卡共用某個結構、抽出 meta-卡。例：寫 [#78] 反省題複選時、引用 [#74] 推薦格式 = 暗示有上層共骨。&lt;/p>
&lt;h3 id="訊號-2跨卡-cross-link-變密單張卡的跟其他卡的關係段持續長">訊號 2：跨卡 cross-link 變密、單張卡的「跟其他卡的關係」段持續長&lt;/h3>
&lt;p>→ 知識網密度足夠、可抽 meta-卡作為樞紐。&lt;/p>
&lt;h3 id="訊號-3實作中要回查多張卡才能完整-apply">訊號 3：實作中要回查多張卡才能完整 apply&lt;/h3>
&lt;p>→ 沉澱成 reference、減少回查成本。&lt;/p>
&lt;h3 id="訊號-4我之前是不是寫過類似的第-3-次出現">訊號 4：「我之前是不是寫過類似的」第 3 次出現&lt;/h3>
&lt;p>→ 不是「沒寫過」、是 meta-結構模糊、無法用既有卡 frame 新情境。需要 meta-卡。&lt;/p>
&lt;hr>
&lt;h2 id="反模式跳層的代價">反模式：跳層的代價&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>反模式&lt;/th>
 &lt;th>為什麼不好&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>直接從對話寫 meta-卡（沒原子卡支撐）&lt;/td>
 &lt;td>over-fit 少數 case、新 case 不符就要重寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只寫 reference 不寫卡片&lt;/td>
 &lt;td>reference 是「怎麼做」、原子卡是「為什麼」、缺少 why 後續難 maintain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>卡片寫完不抽 meta&lt;/td>
 &lt;td>知識散落、跨情境無法判讀、實作中要回查多張&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta-卡寫太早（寫第 1-2 張就抽）&lt;/td>
 &lt;td>沒足夠 N 看出共骨、結構強加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一張卡裡塞多個現象&lt;/td>
 &lt;td>卡片該原子、混合會干擾 cross-link&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reference 沒對應觸發情境&lt;/td>
 &lt;td>寫了沒人看、變另一份未來才會被翻的文件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>卡片寫完不回頭 cross-link&lt;/td>
 &lt;td>知識網不形成、留下孤兒卡&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="觀察多層迭代不是線性是-spiral">觀察：多層迭代不是線性、是 spiral&lt;/h2>
&lt;p>實際上的迭代不是「Layer 1 全寫完才寫 Layer 2」、而是：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>知識卡片系統的成型不是「想清楚再寫」、是<strong>多輪迭代浮現</strong>：</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">原始對話素材
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 識別重複結構
</span></span><span class="line"><span class="ln">3</span><span class="cl">原子卡（每張一個小現象）
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 串連、識別共同骨架
</span></span><span class="line"><span class="ln">5</span><span class="cl">meta-卡（抽上層原則）
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ 沉澱成可重複使用的 protocol
</span></span><span class="line"><span class="ln">7</span><span class="cl">reference（可直接套用的 checklist + 模板）
</span></span><span class="line"><span class="ln">8</span><span class="cl">   ↓ L3 觸發機制
</span></span><span class="line"><span class="ln">9</span><span class="cl">SKILL（自動觸發 reference）</span></span></code></pre></div><p>每層都解上一層的限制、不是替代。<strong>原子卡保留具體 case 的細節</strong>（被反例反駁時可保留）、<strong>meta-卡提供跨情境的判讀框架</strong>（避免每次重新推理）、<strong>reference 沉澱成可直接套用的步驟</strong>（消除「知道但忘記用」的鴻溝）。</p>
<hr>
<h2 id="為什麼一次寫不完">為什麼一次寫不完</h2>
<p>第一次接觸現象時、看到的是<strong>具體 case 的表面</strong>：</p>
<ul>
<li>看到「使用者說『我再想想』」 → 先寫成「[#77] 延後是合法選項」</li>
<li>看到「使用者說『1+2』」 → 先寫成「[#78] 反省題複選」</li>
<li>看到「使用者反駁推薦」 → 先寫成「[#74] 決策呈現格式」</li>
</ul>
<p>每張原子卡解 1 個情境、自包含可讀。但<strong>串連在一起時才浮現的結構</strong>（例：「五個獨立維度」）需要看到 ≥ 3-5 張原子卡之後才看得出。<strong>第一次寫不出來、不是因為沒想清楚、是因為原料不夠</strong>。</p>
<p>催熟原子卡之前先寫 meta-卡 = 從少數 case 過度推論、產生 over-fit 結構、後續發現新 case 不符就要重寫。</p>
<hr>
<h2 id="三層的職責分工">三層的職責分工</h2>
<h3 id="layer-1原子卡">Layer 1：原子卡</h3>
<p><strong>範圍</strong>：單一現象 / 單一錯誤 / 單一情境。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>從具體事件浮現（事後檢討）</li>
<li>自包含、不依賴其他卡也能讀</li>
<li>含「反模式 / 修法 / 何時不適用」三段</li>
<li>給未來自己看：「啊我再次遇到這個」</li>
</ul>
<p><strong>例</strong>：<a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項</a> 是從一次具體對話中「使用者說『不用現在決策』、agent 加壓」浮現。</p>
<h3 id="layer-2meta-卡">Layer 2：Meta-卡</h3>
<p><strong>範圍</strong>：N 張原子卡的共同骨架。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>不是新原則、是把已存在的原則上抽</li>
<li>通常出現在「寫 N 張原子卡之後、發現他們其實同一件事」</li>
<li>提供跨情境判讀（&ldquo;這個情境屬於哪一維度?&quot;）</li>
<li>給「已有 mental model 的讀者」加深、不取代原子卡</li>
</ul>
<p><strong>例</strong>：<a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a> 是寫完 [#74-#78] 五張原子卡後、發現他們各對應一個獨立維度。沒寫 #79 之前 #74-#78 是五張平行卡、寫完 #79 後形成有結構的網。</p>
<h3 id="layer-3reference">Layer 3：Reference</h3>
<p><strong>範圍</strong>：把 N 張卡的判讀流程沉澱成可直接套用的 step-by-step。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>不是教學、是 lookup table + checklist</li>
<li>在實作中被翻開、不是讀爽的</li>
<li>結尾有 self-check 讓使用者驗證自己沒漏</li>
<li>跟一張具體任務 / 觸發情境對應</li>
</ul>
<p><strong>例</strong>：<code>references/decision-dialogue.md</code>（在 SKILL 內）— 把 #74-#79 翻譯成「五步判讀 + 完整模板 + self-check」、agent 寫 decision 之前看一遍就夠了。</p>
<hr>
<h2 id="多層迭代的訊號什麼時候該往上抽">多層迭代的訊號：什麼時候該往上抽？</h2>
<h3 id="訊號-1寫第-n-張卡時發現大段內容跟前一張重複">訊號 1：寫第 N 張卡時、發現大段內容跟前一張重複</h3>
<p>→ 兩張卡共用某個結構、抽出 meta-卡。例：寫 [#78] 反省題複選時、引用 [#74] 推薦格式 = 暗示有上層共骨。</p>
<h3 id="訊號-2跨卡-cross-link-變密單張卡的跟其他卡的關係段持續長">訊號 2：跨卡 cross-link 變密、單張卡的「跟其他卡的關係」段持續長</h3>
<p>→ 知識網密度足夠、可抽 meta-卡作為樞紐。</p>
<h3 id="訊號-3實作中要回查多張卡才能完整-apply">訊號 3：實作中要回查多張卡才能完整 apply</h3>
<p>→ 沉澱成 reference、減少回查成本。</p>
<h3 id="訊號-4我之前是不是寫過類似的第-3-次出現">訊號 4：「我之前是不是寫過類似的」第 3 次出現</h3>
<p>→ 不是「沒寫過」、是 meta-結構模糊、無法用既有卡 frame 新情境。需要 meta-卡。</p>
<hr>
<h2 id="反模式跳層的代價">反模式：跳層的代價</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>直接從對話寫 meta-卡（沒原子卡支撐）</td>
          <td>over-fit 少數 case、新 case 不符就要重寫</td>
      </tr>
      <tr>
          <td>只寫 reference 不寫卡片</td>
          <td>reference 是「怎麼做」、原子卡是「為什麼」、缺少 why 後續難 maintain</td>
      </tr>
      <tr>
          <td>卡片寫完不抽 meta</td>
          <td>知識散落、跨情境無法判讀、實作中要回查多張</td>
      </tr>
      <tr>
          <td>Meta-卡寫太早（寫第 1-2 張就抽）</td>
          <td>沒足夠 N 看出共骨、結構強加</td>
      </tr>
      <tr>
          <td>一張卡裡塞多個現象</td>
          <td>卡片該原子、混合會干擾 cross-link</td>
      </tr>
      <tr>
          <td>Reference 沒對應觸發情境</td>
          <td>寫了沒人看、變另一份未來才會被翻的文件</td>
      </tr>
      <tr>
          <td>卡片寫完不回頭 cross-link</td>
          <td>知識網不形成、留下孤兒卡</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="觀察多層迭代不是線性是-spiral">觀察：多層迭代不是線性、是 spiral</h2>
<p>實際上的迭代不是「Layer 1 全寫完才寫 Layer 2」、而是：</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">寫 #74 → 寫 #75 → (浮現 meta) → 草稿 #79 →
</span></span><span class="line"><span class="ln">2</span><span class="cl">寫 #76 → (補 #79) → 寫 #77 → (補 #79) →
</span></span><span class="line"><span class="ln">3</span><span class="cl">寫 #78 → 完成 #79 → 寫 reference → SKILL 整合</span></span></code></pre></div><p>每次新卡可能反過來修改 meta-卡、reference 也可能反過來指出原子卡缺角。<strong>Spiral 結構接受迭代修正、線性結構假裝一次寫對</strong>。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>寫 meta-卡的訊號：第 2 次看到類似結構、抽出來</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>先寫原子卡、有證據再抽 meta、跟「先窄後寬」同構</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>meta-卡是上層 SSOT、原子卡保留 case-specific 細節、各層分工</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>「直接寫 meta」容易但會 over-fit、迭代浮現難寫但對齊真實結構</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「回頭抽 meta + 寫 reference」是高 ROI 但無觸發、需要協議 / pair / 對話結構驅動</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a></td>
          <td>本卡的 spiral 過程剛好就是 #79 浮現的實例 — meta-卡 + reference 都是後寫</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>spiral 是 multi-pass refinement 的具體實現 — 卡片內容對不對、抽 meta 抽得對不對都是行為錯誤、靠 spiral 收斂、不靠 hook 攔截</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到本系統的具體-case">套用到本系統的具體 case</h2>
<p><code>content/report/</code> 的 80+ 卡片成型路徑：</p>
<ol>
<li><strong>第 1-2 輪</strong>（#1-#30）：純事後檢討、單張原子卡、互不串連</li>
<li><strong>第 3 輪</strong>（#31-#45）：開始抽 pattern 卡、識別重複結構</li>
<li><strong>第 4 輪</strong>（#42-#45 + #67-#72）：抽出第一批 meta-卡</li>
<li><strong>第 5 輪</strong>（#55-#73）：寫 #59 五策略時發現 meta-卡需求、回補 #67-#73</li>
<li><strong>第 6 輪</strong>（#74-#80）：dialogue 中浮現決策協議、寫原子卡 + meta + reference</li>
<li><strong>下一輪</strong>：可能會在 #80 上面浮現另一層 meta（process 反思的 meta）</li>
</ol>
<p>每輪都不是「一次寫完」、是 spiral 中的一個 lap。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第 N 張卡、結構大段重複前卡</td>
          <td>抽 meta-卡</td>
      </tr>
      <tr>
          <td>卡片網的 cross-link 變密</td>
          <td>加 meta-卡作為樞紐</td>
      </tr>
      <tr>
          <td>實作中要翻 ≥ 3 張卡</td>
          <td>沉澱 reference</td>
      </tr>
      <tr>
          <td>「之前好像寫過類似的」第 3 次</td>
          <td>缺 meta-frame、補上</td>
      </tr>
      <tr>
          <td>Reference 寫完沒人翻</td>
          <td>沒接到觸發情境、補 SKILL trigger route</td>
      </tr>
      <tr>
          <td>Meta-卡寫太早、後續新 case 一直破壞</td>
          <td>退回原子卡層、累積到 ≥ 3-5 張再抽</td>
      </tr>
      <tr>
          <td>原子卡卡得很細、單張看完不知道幹嘛</td>
          <td>缺 meta-上下文、補 meta-卡或 reference</td>
      </tr>
      <tr>
          <td>Cross-link 偏單向（只引用、沒被引用）</td>
          <td>孤兒卡、反向 link 補回</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：知識卡片系統不是寫一次的文件、是長期 spiral 迭代的 living system。<strong>接受「第一次寫不對、會迭代」這個前提</strong>、就會在每次接觸新現象時先寫原子、累積到一定 N 後抽 meta、最後沉澱 reference。<strong>反過來的「想清楚再寫」是模仿線性開發、跟知識浮現的真實結構不對齊</strong>。</p>
]]></content:encoded></item><item><title>字面攔截 vs 行為精煉：驗證手段跟錯誤層次的對齊</title><link>https://tarrragon.github.io/blog/report/literal-interception-vs-behavioral-refinement/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/literal-interception-vs-behavioral-refinement/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>驗證手段（hook / lint / CI / review / spiral / test / production observation）有不同的「錯誤偵測粒度」、必須跟&lt;strong>錯誤的層次&lt;/strong>對齊：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>錯誤層次&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>適合手段&lt;/th>
 &lt;th>不適合手段&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>字面&lt;/td>
 &lt;td>typo、缺 field、syntax 錯、檔案沒 frontmatter&lt;/td>
 &lt;td>hook、lint、type checker、schema validation&lt;/td>
 &lt;td>multi-pass review（過殺）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>行為&lt;/td>
 &lt;td>推薦騎牆、yes/no collapse、思考偏差、judgment 錯位&lt;/td>
 &lt;td>multi-pass spiral、review、dogfood&lt;/td>
 &lt;td>hook（catch 不到、假裝有保護）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「攔截」這個動作預設&lt;strong>已經知道錯誤的形狀&lt;/strong>（hook 寫死規則 = 已知錯誤）。&lt;strong>真正會出錯的是「不知道形狀」的錯誤&lt;/strong> — 那需要多輪 review / spiral 收斂、不是即時攔截。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-hook-對行為錯誤無能為力">為什麼 hook 對行為錯誤無能為力&lt;/h2>
&lt;p>Hook / lint / type checker 的本質是 &lt;strong>字串匹配 / structural check&lt;/strong> — 看得到形狀、看不到意圖。所以：&lt;/p>
&lt;ul>
&lt;li>抓得到「commit message 沒含 issue 號」 — 字面 pattern&lt;/li>
&lt;li>抓得到「test file 沒對應 source file」 — 結構檢查&lt;/li>
&lt;li>抓得到「YAML frontmatter 缺欄位」 — schema check&lt;/li>
&lt;li>抓不到「這個推薦不夠明確、騎牆」 — 需要理解語意&lt;/li>
&lt;li>抓不到「決策 collapse 到 yes/no、漏五維」 — 需要判斷意圖&lt;/li>
&lt;li>抓不到「思考路徑跳過 RED phase」 — 需要追溯 reasoning&lt;/li>
&lt;li>抓不到「過度疊加策略、超過必要」 — 需要 judgment&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Hook 試圖用字串規則模擬語意檢查 = 規則永遠 over-fit 或 under-fit&lt;/strong>：寫太嚴 → 大量 false positive 把好的也擋掉、寫太鬆 → 行為錯誤照樣通過。&lt;/p>
&lt;hr>
&lt;h2 id="反模式用-hook-蓋行為錯誤的代價">反模式：用 hook 蓋行為錯誤的代價&lt;/h2>
&lt;h3 id="false-confidence-比沒保護更危險">False confidence 比沒保護更危險&lt;/h3>
&lt;p>寫了 hook 之後、心理上會覺得「有保護」。實際上 hook 只擋字面、行為錯誤照常發生 — 但作者不再警覺、因為「CI 通過了應該沒事」。&lt;/p>
&lt;p>對比沒 hook 的情境：作者知道沒保護、會主動多看一次。&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>沒 hook&lt;/td>
 &lt;td>高（知道沒保護）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hook 抓不到的範圍誤以為有保護&lt;/td>
 &lt;td>低（誤以為有）&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>（行為錯誤通過）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hook 真的夠（純字面領域）&lt;/td>
 &lt;td>適中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第二行是最危險的組合&lt;/strong> — 加 hook 卻不知道 hook 範圍、會比沒 hook 更糟。&lt;/p>
&lt;h3 id="規則膨脹嘗試再寫一條-hook永遠補不完">規則膨脹：嘗試「再寫一條 hook」永遠補不完&lt;/h3>
&lt;p>每次行為錯誤通過、直覺反應是「再加一條 hook 規則」。但行為錯誤的形狀是無限的、規則永遠補不完。最終結果：&lt;/p>
&lt;ul>
&lt;li>規則越來越多、越來越複雜&lt;/li>
&lt;li>維護成本爆炸&lt;/li>
&lt;li>仍然漏接行為錯誤&lt;/li>
&lt;li>還產生越來越多 false positive 把好的擋掉&lt;/li>
&lt;/ul>
&lt;p>→ 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。&lt;/p>
&lt;hr>
&lt;h2 id="多輪精煉的設計spiral-取代攔截">多輪精煉的設計：spiral 取代攔截&lt;/h2>
&lt;p>行為錯誤的正確驗證手段是 &lt;strong>multi-pass spiral&lt;/strong>：&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">第 1 輪：先做、看結果
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ 發現 N 個問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">第 2 輪：依結果調整 / 補強
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ 發現 N-k 個問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">第 3 輪：dogfood / 實際使用 / 反向自查
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> ↓ 收斂
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">（沒新問題 → 結束、有新問題 → 繼續迭代）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵設計：&lt;strong>不是「攔截錯誤」、是「設計每輪能 catch 不同層的錯誤」&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>驗證手段（hook / lint / CI / review / spiral / test / production observation）有不同的「錯誤偵測粒度」、必須跟<strong>錯誤的層次</strong>對齊：</p>
<table>
  <thead>
      <tr>
          <th>錯誤層次</th>
          <th>例子</th>
          <th>適合手段</th>
          <th>不適合手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字面</td>
          <td>typo、缺 field、syntax 錯、檔案沒 frontmatter</td>
          <td>hook、lint、type checker、schema validation</td>
          <td>multi-pass review（過殺）</td>
      </tr>
      <tr>
          <td>行為</td>
          <td>推薦騎牆、yes/no collapse、思考偏差、judgment 錯位</td>
          <td>multi-pass spiral、review、dogfood</td>
          <td>hook（catch 不到、假裝有保護）</td>
      </tr>
  </tbody>
</table>
<p>「攔截」這個動作預設<strong>已經知道錯誤的形狀</strong>（hook 寫死規則 = 已知錯誤）。<strong>真正會出錯的是「不知道形狀」的錯誤</strong> — 那需要多輪 review / spiral 收斂、不是即時攔截。</p>
<hr>
<h2 id="為什麼-hook-對行為錯誤無能為力">為什麼 hook 對行為錯誤無能為力</h2>
<p>Hook / lint / type checker 的本質是 <strong>字串匹配 / structural check</strong> — 看得到形狀、看不到意圖。所以：</p>
<ul>
<li>抓得到「commit message 沒含 issue 號」 — 字面 pattern</li>
<li>抓得到「test file 沒對應 source file」 — 結構檢查</li>
<li>抓得到「YAML frontmatter 缺欄位」 — schema check</li>
<li>抓不到「這個推薦不夠明確、騎牆」 — 需要理解語意</li>
<li>抓不到「決策 collapse 到 yes/no、漏五維」 — 需要判斷意圖</li>
<li>抓不到「思考路徑跳過 RED phase」 — 需要追溯 reasoning</li>
<li>抓不到「過度疊加策略、超過必要」 — 需要 judgment</li>
</ul>
<p><strong>Hook 試圖用字串規則模擬語意檢查 = 規則永遠 over-fit 或 under-fit</strong>：寫太嚴 → 大量 false positive 把好的也擋掉、寫太鬆 → 行為錯誤照樣通過。</p>
<hr>
<h2 id="反模式用-hook-蓋行為錯誤的代價">反模式：用 hook 蓋行為錯誤的代價</h2>
<h3 id="false-confidence-比沒保護更危險">False confidence 比沒保護更危險</h3>
<p>寫了 hook 之後、心理上會覺得「有保護」。實際上 hook 只擋字面、行為錯誤照常發生 — 但作者不再警覺、因為「CI 通過了應該沒事」。</p>
<p>對比沒 hook 的情境：作者知道沒保護、會主動多看一次。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>警覺度</th>
          <th>實際漏接率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>沒 hook</td>
          <td>高（知道沒保護）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Hook 抓不到的範圍誤以為有保護</td>
          <td>低（誤以為有）</td>
          <td><strong>高</strong>（行為錯誤通過）</td>
      </tr>
      <tr>
          <td>Hook 真的夠（純字面領域）</td>
          <td>適中</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>第二行是最危險的組合</strong> — 加 hook 卻不知道 hook 範圍、會比沒 hook 更糟。</p>
<h3 id="規則膨脹嘗試再寫一條-hook永遠補不完">規則膨脹：嘗試「再寫一條 hook」永遠補不完</h3>
<p>每次行為錯誤通過、直覺反應是「再加一條 hook 規則」。但行為錯誤的形狀是無限的、規則永遠補不完。最終結果：</p>
<ul>
<li>規則越來越多、越來越複雜</li>
<li>維護成本爆炸</li>
<li>仍然漏接行為錯誤</li>
<li>還產生越來越多 false positive 把好的擋掉</li>
</ul>
<p>→ 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。</p>
<hr>
<h2 id="多輪精煉的設計spiral-取代攔截">多輪精煉的設計：spiral 取代攔截</h2>
<p>行為錯誤的正確驗證手段是 <strong>multi-pass spiral</strong>：</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">第 1 輪：先做、看結果
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 發現 N 個問題
</span></span><span class="line"><span class="ln">3</span><span class="cl">第 2 輪：依結果調整 / 補強
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 發現 N-k 個問題
</span></span><span class="line"><span class="ln">5</span><span class="cl">第 3 輪：dogfood / 實際使用 / 反向自查
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ 收斂
</span></span><span class="line"><span class="ln">7</span><span class="cl">（沒新問題 → 結束、有新問題 → 繼續迭代）</span></span></code></pre></div><p>關鍵設計：<strong>不是「攔截錯誤」、是「設計每輪能 catch 不同層的錯誤」</strong>。</p>
<h3 id="各輪的職責分工">各輪的職責分工</h3>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>適合 catch 什麼</th>
          <th>怎麼設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 輪：實作</td>
          <td>純執行、預期會有錯</td>
          <td>不要追求 perfect、跑起來看結果</td>
      </tr>
      <tr>
          <td>第 2 輪：自查 / 對比需求</td>
          <td>邏輯偏差、漏 case</td>
          <td>對比原始需求、列 Checkpoint 1（<a href="../verification-timeline-checkpoints/">#68</a>）</td>
      </tr>
      <tr>
          <td>第 3 輪：dogfood / production</td>
          <td>實際使用才浮現的問題</td>
          <td>真實 user / 真實流量、看回饋</td>
      </tr>
      <tr>
          <td>第 N 輪：反向自查</td>
          <td>上幾輪沒看到的盲點</td>
          <td>改換 frame（例如「假裝是另一個人 review」）</td>
      </tr>
  </tbody>
</table>
<p>每輪解上一輪沒看到的問題、不是重複同一檢查。</p>
<h3 id="不同輪適合不同的不對齊">不同輪適合不同的「不對齊」</h3>
<ul>
<li>第 1 輪 vs 需求 → 看「做出來的跟要的對不對齊」</li>
<li>第 2 輪 vs 邊界 case → 看「漏哪些情境」</li>
<li>第 3 輪 vs 真實使用 → 看「用起來感覺對不對」</li>
<li>第 N 輪 vs 上層原則 → 看「有沒有違反某個 meta-原則」</li>
</ul>
<p>每輪有不同的角度、新角度才能 catch 上一輪 miss 的東西。</p>
<hr>
<h2 id="何時-hook-真的足夠">何時 hook 真的足夠</h2>
<p>某些情境純字面就夠、加 hook 是對的：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 hook 夠</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema validation（API、DB、config）</td>
          <td>結構是 spec、字面對 = 行為對</td>
      </tr>
      <tr>
          <td>已知的 anti-pattern 字串（<code>TODO:</code>、<code>FIXME:</code>、<code>console.log</code>）</td>
          <td>字面就是 evidence</td>
      </tr>
      <tr>
          <td>格式統一（換行、縮排、import 順序）</td>
          <td>純美化、沒語意</td>
      </tr>
      <tr>
          <td>不可破壞的 invariant（commit 訊息含 issue 號、test 名格式）</td>
          <td>結構即正確</td>
      </tr>
      <tr>
          <td>安全 critical 的 surface check（沒 secret 在 code、license header 在）</td>
          <td>漏掉成本極高、字面檢查 ROI 高</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>錯誤形狀完全字面、且漏掉成本高 / 字面就是 evidence</strong>。其他情境 hook 都會在某個時點走到 ceiling。</p>
<hr>
<h2 id="識別-ceiling什麼時候該換手段">識別 ceiling：什麼時候該換手段</h2>
<p>ceiling 訊號：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換的手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「這個 lint 規則寫不出來、太多例外」</td>
          <td>改 review checklist、不寫 lint</td>
      </tr>
      <tr>
          <td>「hook pass 但 production 還是出錯」</td>
          <td>hook 已到 ceiling、補 multi-pass review</td>
      </tr>
      <tr>
          <td>「規則第 N 次補例外」</td>
          <td>規則膨脹、退回 review</td>
      </tr>
      <tr>
          <td>「false positive 比 true positive 多」</td>
          <td>hook 過殺、放寬 + 補 review</td>
      </tr>
      <tr>
          <td>「需要 understand intent 才能判斷」</td>
          <td>純字面不夠、要 LLM / human review</td>
      </tr>
      <tr>
          <td>「加了 hook 後 review 變草率」</td>
          <td>False confidence 在發生、警覺度降低</td>
      </tr>
  </tbody>
</table>
<p>看到任一訊號、不是「再寫一條 hook」、是<strong>接受 hook 對這個錯誤層次無能為力、改設計 multi-pass review</strong>。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>第 2 輪是 multi-pass 的最小單位、跟本卡的「多輪設計」同骨</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>#68 的四個 checkpoint = 多輪 review 的時間軸實現</td>
      </tr>
      <tr>
          <td><a href="../test-first-red-before-green/">#69 Test-First：RED before GREEN</a></td>
          <td>RED phase 是「testing the test」的多輪設計 — 純 hook 看不到</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>#72 提倡 L3-L5 結構性對策、本卡是 ceiling — L5 hook 抓不到行為錯誤、需要 L4 review / pair</td>
      </tr>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>spiral 浮現本身就是 multi-pass 的具體 case — 不靠單次「寫對」</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>「五維 collapse」是行為錯誤、hook 抓不到、要靠 reference dogfood + multi-pass review</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>本卡在「寫」這個動作的具體實例 — review 是 multi-pass、不是 hook</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>本卡在「命名」這個動作的具體實例 — 命名 lint 只擋字面、grep / 一致性 / impl 洩漏靠 review</td>
      </tr>
      <tr>
          <td><a href="../methodology-multi-pass-embedding/">#85 Methodology 的 multi-pass 該 embed 在 pillar</a></td>
          <td>本卡在「方法論設計本身」這一層的展現 — multi-pass 升 pillar 才結構性執行</td>
      </tr>
      <tr>
          <td><a href="../emergence-violations-need-in-stream-sampling/">#124 Emergence-class 違規規則化不了、要 stage 內抽樣</a></td>
          <td>三類分法擴展 — 本卡是 2 類分法（字面 / 行為）、#124 擴展為 3 類（字面 / 結構 / emergence）並補 timing 軸；emergence 是行為層中跨檔 / 跨樣本才浮現的子類</td>
      </tr>
  </tbody>
</table>
<p>本卡是 #72 的 sibling / 補強 — #72 推 L3-L5 結構性對策最強、本卡指出 L5 也有 ceiling、不是萬能。組合解：<strong>字面用 L5 hook、行為用 L4 pair + multi-pass</strong>。#124 進一步把行為層細分出 emergence 子類、補上對應 enforcement 時機。</p>
<hr>
<h2 id="套用到本系統的-case">套用到本系統的 case</h2>
<h3 id="case-1卡片系統本身">Case 1：卡片系統本身</h3>
<p><code>mdtools fmt --fix</code> 是 hook（字面）— 處理 frontmatter、table 對齊、檔名 slug。
卡片內容對不對、抽 meta 抽得對不對 = 行為錯誤 — 靠 spiral 浮現（<a href="../cards-as-living-system-iteration/">#81</a>）、不靠 hook。</p>
<h3 id="case-2搜尋頁-bug">Case 2：搜尋頁 bug</h3>
<p>CI 跑 playwright = 字面測試（給定輸入、output 是否符合）。
但「filter mode 切換有沒有 silent failure」這個 bug 一開始連 test case 都沒列、是 user 回報才浮現 — multi-pass dogfood 才 catch 到。</p>
<h3 id="case-3決策對話-collapse">Case 3：決策對話 collapse</h3>
<p>Hook 寫不出「這個回應 collapse 到 yes/no」的規則（語意理解）。
靠 reference 的 self-check + dogfood 例子 + 對話中 user 反饋的 multi-pass 才能 catch。</p>
<p>每個 case 都驗證同一條：<strong>字面層工具有用、但 ceiling 明確；行為層需要 multi-pass、不靠攔截</strong>。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想加 hook 防某個重複出現的問題</td>
          <td>先問「是字面還是行為？」、行為的話別寫 hook</td>
      </tr>
      <tr>
          <td>寫了 hook 規則但例外越來越多</td>
          <td>ceiling 到了、改 review</td>
      </tr>
      <tr>
          <td>「CI 通過 = 沒事」這個信念</td>
          <td>檢查 CI 範圍、行為錯誤可能漏接</td>
      </tr>
      <tr>
          <td>同類錯誤不斷以新形狀出現</td>
          <td>行為錯誤、hook 無解、補 multi-pass</td>
      </tr>
      <tr>
          <td>第 1 輪做完就 ship、沒第 2 輪</td>
          <td>假設一次寫對、多半會漏行為錯誤</td>
      </tr>
      <tr>
          <td>多輪 review 每輪用同樣 frame</td>
          <td>角度沒換、後續輪 = 重跑前輪、不會新發現</td>
      </tr>
      <tr>
          <td>「下次注意」當作驗證</td>
          <td>L1 紀律、不是 L4 結構、跟 <a href="../external-trigger-for-high-roi-work/">#72</a> 同病</td>
      </tr>
      <tr>
          <td>行為錯誤反覆出現、但「再加條 hook 規則」</td>
          <td>換工具、不是換規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：驗證手段的 ROI = 跟錯誤層次對齊 × 不超出 ceiling。<strong>Hook 不會思考、所以只能擋字面</strong>；<strong>行為錯誤需要 multi-pass spiral、用每輪不同角度收斂、不靠單次攔截</strong>。試圖用 hook 蓋 spiral 該做的工作 = 假裝有保護、實際比沒保護更危險。</p>
]]></content:encoded></item><item><title>Methodology 的 multi-pass 該升級為 pillar 層：核心結構才會被執行</title><link>https://tarrragon.github.io/blog/report/methodology-multi-pass-embedding/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/methodology-multi-pass-embedding/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>凡是教做事方法的東西（SKILL、playbook、methodology document、checklist）— 如果你認為 multi-pass refinement 是必要的、就要把它放在&lt;strong>核心結構層&lt;/strong>（pillar、principle、step）、不是放在&lt;strong>附帶段&lt;/strong>（appendix、tips、reminder、see also）。&lt;/p>
&lt;p>放在 appendix = 結構暗示「optional、看心情選擇」 = 在 &lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發&lt;/a> 的結構壓力下、永遠被跳過。&lt;strong>Pillar 層 = 結構性必跑、用結構強制行為、不靠紀律&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-pillar--appendix-的位置決定執行率">為什麼 pillar / appendix 的位置決定執行率&lt;/h2>
&lt;p>讀者看 SKILL / methodology 時、認知資源分配：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Pillar / Core Principles&lt;/strong>：必讀、會內化、實作中會回想&lt;/li>
&lt;li>&lt;strong>Steps / Reference&lt;/strong>：實作中翻&lt;/li>
&lt;li>&lt;strong>Tips / Appendix / &amp;ldquo;See also&amp;rdquo;&lt;/strong>：第一次讀掃過、之後忘記&lt;/li>
&lt;/ul>
&lt;p>把 multi-pass review 放 appendix = 結構暗示「這是進階、可選」。即使內容寫得很詳細、結構訊號蓋過內容。&lt;/p>
&lt;p>對比放 pillar：每次接觸 SKILL、第一眼看到 4-5 個 pillar 中包含 &amp;ldquo;Multi-pass Refinement&amp;rdquo; — 結構性提示「這跟其他 pillar 同樣重要」。&lt;/p>
&lt;hr>
&lt;h2 id="各-methodology-的-pillar--appendix-切分">各 methodology 的 pillar / appendix 切分&lt;/h2>
&lt;p>實際 methodology 文件的 pillar 應該包含 multi-pass、appendix 應該避免：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Methodology&lt;/th>
 &lt;th>適合的 pillar&lt;/th>
 &lt;th>不適合放 appendix&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>compositional-writing（寫作方法論）&lt;/td>
 &lt;td>第 6 原則「Re-read Pass」明示輪次&lt;/td>
 &lt;td>「最後 review 一下」三字附帶&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>requirement-protocol（需求協議）&lt;/td>
 &lt;td>第 4 pillar「Multi-pass Refinement」明示「第 1 輪實作預期不對」&lt;/td>
 &lt;td>「失敗多次再回頭看」零散提示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>frontend-with-playwright（前端 + 測試協議）&lt;/td>
 &lt;td>「漸進驗證」在 6 大原則中（已有）、再加「Multi-pass Review」串成系列&lt;/td>
 &lt;td>TODO 註解講「之後 review」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TDD（test-driven）&lt;/td>
 &lt;td>RED-GREEN-REFACTOR 三步本身就是 multi-pass&lt;/td>
 &lt;td>「重構是 optional」當 appendix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Agile（process）&lt;/td>
 &lt;td>Sprint review / retrospective 是 pillar&lt;/td>
 &lt;td>「有空回顧一下」當 appendix&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 methodology 的設計都該檢查：&lt;strong>multi-pass 是 pillar 還是 appendix？&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="如何識別該升-pillar-但被當-appendix">如何識別「該升 pillar 但被當 appendix」&lt;/h2>
&lt;p>訊號：&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>「最後再 review 一下」「有空再 polish」這類 disclaimer&lt;/td>
 &lt;td>升成獨立 pillar / 原則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-pass 內容散在多個 reference 角落、沒有單一定位&lt;/td>
 &lt;td>抽出 pillar、各 reference 引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pillar 列表只 3 條（看似簡潔）、但實作中常忘 review&lt;/td>
 &lt;td>缺 pillar、補上 multi-pass&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「第 1 輪原則」+「第 2 輪原則」分開兩個 SKILL&lt;/td>
 &lt;td>合併、multi-pass 是同 SKILL 的多輪、不是兩個 SKILL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文件結尾「最後注意事項」常被使用者引用為「我忘了」&lt;/td>
 &lt;td>結構問題、移到 pillar&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個訊號都是 &lt;strong>multi-pass 的位置太低&lt;/strong>、結構壓力把它當作 optional。&lt;/p>
&lt;hr>
&lt;h2 id="升-pillar-後的設計四個必要元素">升 pillar 後的設計：四個必要元素&lt;/h2>
&lt;p>把 multi-pass 升成 pillar、需要含這四個元素才完整：&lt;/p>
&lt;h3 id="1-明示第-1-輪不追求完美">1. 明示「第 1 輪不追求完美」&lt;/h3>
&lt;p>寫在 pillar 內容、第一句就講：「第 1 輪不要追求 perfect、預期會有未發現問題、設計第 2 輪去 catch」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>凡是教做事方法的東西（SKILL、playbook、methodology document、checklist）— 如果你認為 multi-pass refinement 是必要的、就要把它放在<strong>核心結構層</strong>（pillar、principle、step）、不是放在<strong>附帶段</strong>（appendix、tips、reminder、see also）。</p>
<p>放在 appendix = 結構暗示「optional、看心情選擇」 = 在 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 的結構壓力下、永遠被跳過。<strong>Pillar 層 = 結構性必跑、用結構強制行為、不靠紀律</strong>。</p>
<hr>
<h2 id="為什麼-pillar--appendix-的位置決定執行率">為什麼 pillar / appendix 的位置決定執行率</h2>
<p>讀者看 SKILL / methodology 時、認知資源分配：</p>
<ul>
<li><strong>Pillar / Core Principles</strong>：必讀、會內化、實作中會回想</li>
<li><strong>Steps / Reference</strong>：實作中翻</li>
<li><strong>Tips / Appendix / &ldquo;See also&rdquo;</strong>：第一次讀掃過、之後忘記</li>
</ul>
<p>把 multi-pass review 放 appendix = 結構暗示「這是進階、可選」。即使內容寫得很詳細、結構訊號蓋過內容。</p>
<p>對比放 pillar：每次接觸 SKILL、第一眼看到 4-5 個 pillar 中包含 &ldquo;Multi-pass Refinement&rdquo; — 結構性提示「這跟其他 pillar 同樣重要」。</p>
<hr>
<h2 id="各-methodology-的-pillar--appendix-切分">各 methodology 的 pillar / appendix 切分</h2>
<p>實際 methodology 文件的 pillar 應該包含 multi-pass、appendix 應該避免：</p>
<table>
  <thead>
      <tr>
          <th>Methodology</th>
          <th>適合的 pillar</th>
          <th>不適合放 appendix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>compositional-writing（寫作方法論）</td>
          <td>第 6 原則「Re-read Pass」明示輪次</td>
          <td>「最後 review 一下」三字附帶</td>
      </tr>
      <tr>
          <td>requirement-protocol（需求協議）</td>
          <td>第 4 pillar「Multi-pass Refinement」明示「第 1 輪實作預期不對」</td>
          <td>「失敗多次再回頭看」零散提示</td>
      </tr>
      <tr>
          <td>frontend-with-playwright（前端 + 測試協議）</td>
          <td>「漸進驗證」在 6 大原則中（已有）、再加「Multi-pass Review」串成系列</td>
          <td>TODO 註解講「之後 review」</td>
      </tr>
      <tr>
          <td>TDD（test-driven）</td>
          <td>RED-GREEN-REFACTOR 三步本身就是 multi-pass</td>
          <td>「重構是 optional」當 appendix</td>
      </tr>
      <tr>
          <td>Agile（process）</td>
          <td>Sprint review / retrospective 是 pillar</td>
          <td>「有空回顧一下」當 appendix</td>
      </tr>
  </tbody>
</table>
<p>每個 methodology 的設計都該檢查：<strong>multi-pass 是 pillar 還是 appendix？</strong></p>
<hr>
<h2 id="如何識別該升-pillar-但被當-appendix">如何識別「該升 pillar 但被當 appendix」</h2>
<p>訊號：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「最後再 review 一下」「有空再 polish」這類 disclaimer</td>
          <td>升成獨立 pillar / 原則</td>
      </tr>
      <tr>
          <td>Multi-pass 內容散在多個 reference 角落、沒有單一定位</td>
          <td>抽出 pillar、各 reference 引用</td>
      </tr>
      <tr>
          <td>Pillar 列表只 3 條（看似簡潔）、但實作中常忘 review</td>
          <td>缺 pillar、補上 multi-pass</td>
      </tr>
      <tr>
          <td>「第 1 輪原則」+「第 2 輪原則」分開兩個 SKILL</td>
          <td>合併、multi-pass 是同 SKILL 的多輪、不是兩個 SKILL</td>
      </tr>
      <tr>
          <td>文件結尾「最後注意事項」常被使用者引用為「我忘了」</td>
          <td>結構問題、移到 pillar</td>
      </tr>
  </tbody>
</table>
<p>每個訊號都是 <strong>multi-pass 的位置太低</strong>、結構壓力把它當作 optional。</p>
<hr>
<h2 id="升-pillar-後的設計四個必要元素">升 pillar 後的設計：四個必要元素</h2>
<p>把 multi-pass 升成 pillar、需要含這四個元素才完整：</p>
<h3 id="1-明示第-1-輪不追求完美">1. 明示「第 1 輪不追求完美」</h3>
<p>寫在 pillar 內容、第一句就講：「第 1 輪不要追求 perfect、預期會有未發現問題、設計第 2 輪去 catch」。</p>
<p>去掉「第 1 輪該寫對」的隱含預設、釋放認知資源。</p>
<h3 id="2-列出-n-輪的-frame-清單">2. 列出 N 輪的 frame 清單</h3>
<p>每輪用什麼 frame、catch 什麼。例：</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">輪 1：生成 — idea → 字
</span></span><span class="line"><span class="ln">2</span><span class="cl">輪 2：對意圖 — 跟原意對齊嗎
</span></span><span class="line"><span class="ln">3</span><span class="cl">輪 3：機會成本語氣 — 絕對主義詞翻成 trade-off
</span></span><span class="line"><span class="ln">4</span><span class="cl">輪 4：grep-ability — 關鍵字前置嗎
</span></span><span class="line"><span class="ln">5</span><span class="cl">輪 5：反例 / 邊界 — 何時不適用寫了嗎</span></span></code></pre></div><h3 id="3-何時可跳輪">3. 何時可跳輪</h3>
<p>不是所有情境都跑全輪。寫清楚「跳輪的合理情境」、避免「跑全輪 = 過度工程」的反彈。</p>
<h3 id="4-跨-frame-的不可替代性">4. 跨 frame 的不可替代性</h3>
<p>明示：<strong>輪 N 不能用「再跑一次輪 N-1」取代</strong> — 不同 frame 才能 catch 不同層。重複同 frame = 同類錯一直 miss。</p>
<hr>
<h2 id="反模式我自己會-review當-pillar-替代">反模式：「我自己會 review」當 pillar 替代</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">不該寫：「請務必在送出前自行 review。」
</span></span><span class="line"><span class="ln">2</span><span class="cl">應該寫：「此 methodology 的第 N 個 pillar 是 Multi-pass Review、含 1-5 輪 frame：⋯⋯」</span></span></code></pre></div><p>「自行 review」= L1 紀律（<a href="../external-trigger-for-high-roi-work/">#72</a>）= 預期失敗。</p>
<p>「列入 pillar + 列輪次 + 列 checklist」= L3-L5 結構性對策 = 結構強制執行。</p>
<hr>
<h2 id="套用到本系統的具體-case">套用到本系統的具體 case</h2>
<h3 id="case-1requirement-protocol-skill">Case 1：requirement-protocol skill</h3>
<ul>
<li><strong>現況</strong>：3 大支柱 + 6 大原則、multi-pass 散在「2 次門檻」「漸進驗證」「revert checkpoint」三條原則裡、沒明示</li>
<li><strong>應該</strong>：升第 4 支柱「Multi-pass Refinement」、把散落的多輪意涵集中</li>
</ul>
<h3 id="case-2compositional-writing-skill">Case 2：compositional-writing skill</h3>
<ul>
<li><strong>現況</strong>：3 大支柱 + 5 大原則、各 reference 結尾有「self-check」段（部分 multi-pass 跡象）</li>
<li><strong>應該</strong>：升第 6 原則「Re-read Pass」、引用 <a href="../writing-multi-pass-review/">#83</a> 的 5 輪 frame、各 reference 加「第 2 輪 review checklist」</li>
</ul>
<h3 id="case-3frontend-with-playwright-skill">Case 3：frontend-with-playwright skill</h3>
<ul>
<li><strong>現況</strong>：「漸進驗證」原則含 multi-pass、但跟「dogfood / 多輪測試」沒串連</li>
<li><strong>應該</strong>：補抽象層原則段、明示 multi-pass 跨「漸進驗證 → playwright dogfood → production observation」是同一條 spiral</li>
</ul>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>本卡是 #72 在 methodology 設計層的展現 — appendix-level 是 L1 紀律、pillar-level 是 L3-L5 結構</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>Methodology 設計這個動作本身就是 multi-pass 的對象 — 第一版 pillar 不對、要 review</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>寫 methodology 文件本身要套 #83 — methodology 文件也是 writing</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>Pillar 的命名要跑 multi-pass naming review</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>寫 methodology 時、便利的寫法是「核心 3 條 + 細節塞 appendix」、跟「使用者實際需要 multi-pass 跑」不對齊</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>Pillar 不該過度膨脹、但「該升的內容沒升」是反向偏差、本卡是補 #43 的另一邊</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Methodology 文件結尾有「最後 review 一下」</td>
          <td>升 pillar</td>
      </tr>
      <tr>
          <td>Pillar 列表只 3 條、但 reference 多次提到「再過一次」</td>
          <td>缺 multi-pass pillar</td>
      </tr>
      <tr>
          <td>Multi-pass 內容散在 ≥ 3 個地方</td>
          <td>抽 pillar、各 reference 引用</td>
      </tr>
      <tr>
          <td>「進階使用者再 review」這類分級</td>
          <td>結構訊號錯位 — multi-pass 不是進階、是 baseline</td>
      </tr>
      <tr>
          <td>使用者反饋「我忘了 review」</td>
          <td>結構問題、不是紀律問題、升 pillar</td>
      </tr>
      <tr>
          <td>Reference 結尾 self-check 沒人用</td>
          <td>位置太尾、提升結構地位</td>
      </tr>
      <tr>
          <td>新 methodology 文件第一版</td>
          <td>預設加 multi-pass pillar、不是寫完才補</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Methodology 設計的 pillar / appendix 切分<strong>不是內容深淺問題、是執行率問題</strong>。Pillar 層必跑、appendix 層不跑。把 multi-pass 視為「附帶」= 結構性確保它不被執行。<strong>真正必要的東西要升結構、不能藏在末尾</strong>。</p>
]]></content:encoded></item><item><title>Capability gap 的對策三層階梯：expectation → augment → rebuild</title><link>https://tarrragon.github.io/blog/report/capability-gap-three-layer-escalation/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/capability-gap-three-layer-escalation/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>當系統能力不滿足使用者預期（capability gap）時、對策有三層階梯、依序評估：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>對策&lt;/th>
 &lt;th>例&lt;/th>
 &lt;th>成本&lt;/th>
 &lt;th>覆蓋率&lt;/th>
 &lt;th>脆弱度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>L1 Expectation alignment&lt;/strong>&lt;/td>
 &lt;td>用文字 / UI / 訊息對齊使用者預期&lt;/td>
 &lt;td>UX hint「搜尋為前綴匹配、找 backpressure 請輸入 backpre」&lt;/td>
 &lt;td>極低&lt;/td>
 &lt;td>部分（需要使用者配合）&lt;/td>
 &lt;td>0&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>L2 Augmenting computation&lt;/strong>&lt;/td>
 &lt;td>在既有 engine 上加一層補強計算、close gap&lt;/td>
 &lt;td>Client-side substring fallback、retry with backoff、computed fallback&lt;/td>
 &lt;td>低-中&lt;/td>
 &lt;td>高（自動補齊）&lt;/td>
 &lt;td>中（多一條 path）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>L3 Structural rebuild&lt;/strong>&lt;/td>
 &lt;td>換 index / engine / 演算法本身&lt;/td>
 &lt;td>Build-time tokenize、換 search engine、重設計 schema&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;td>滿（從 source 解決）&lt;/td>
 &lt;td>高（動 build pipeline）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>預設順序&lt;/strong>：L1 → L2 → L3、依「成本最低先解」。&lt;strong>不必每次跳到 L3&lt;/strong> — L3 是最完整但也最貴、L1 在很多情境就夠。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼有階梯cost-coverage-trade-off-是真實的">為什麼有階梯：cost-coverage trade-off 是真實的&lt;/h2>
&lt;p>直覺反應遇到 capability gap 都想 L3「從根解決」。但 L3 的成本通常 10-100x 於 L1、覆蓋率提升可能只是 80% → 99%、邊際 ROI 低。&lt;/p>
&lt;p>實際分布：&lt;/p>
&lt;ul>
&lt;li>50% case：L1 就夠（gap 是「使用者誤解」、講清楚就好）&lt;/li>
&lt;li>30% case：L2 解掉（gap 是「engine 差一步運算」、補一層 close）&lt;/li>
&lt;li>20% case：必須 L3（gap 是「engine 模型錯位」、補不夠、要重來）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>先試 L1、再試 L2、最後 L3&lt;/strong> = 用真實 ROI 排序、不是用「完美主義」排序。&lt;/p>
&lt;hr>
&lt;h2 id="三層的判讀">三層的判讀&lt;/h2>
&lt;h3 id="l1expectation-alignment">L1：expectation alignment&lt;/h3>
&lt;p>&lt;strong>適合&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Gap 是「使用者預期跟 system capability 對不齊」、不是「system 算錯」&lt;/li>
&lt;li>使用者改變行為就能 close gap（打字方式、order operation、輸入格式）&lt;/li>
&lt;li>Production 真的有 capability、只是 affordance 不明顯&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不適合&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Gap 在 system 算錯、不是預期錯位&lt;/li>
&lt;li>使用者無法配合（流量大、不可能教育每個 user）&lt;/li>
&lt;li>訊息會被忽略（A/B test 證明 hint 沒人讀）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>領域&lt;/th>
 &lt;th>L1 對策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search prefix-match&lt;/td>
 &lt;td>UX hint「搜尋是前綴匹配」+ examples&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database eventual consistency&lt;/td>
 &lt;td>UX「資料同步可能延遲幾秒」+ refresh button&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM token limit&lt;/td>
 &lt;td>UI 提醒「附件太長、預期會被截斷」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network failure&lt;/td>
 &lt;td>Toast「網路不穩、稍後再試」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Concurrent edit&lt;/td>
 &lt;td>Banner「另一人也在編輯、你看到的是 5 秒前版本」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h3 id="l2augmenting-computation">L2：augmenting computation&lt;/h3>
&lt;p>&lt;strong>適合&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Engine 缺一層計算就能 close gap、額外計算不貴&lt;/li>
&lt;li>Client / proxy / wrapper 層可加運算、不動 engine&lt;/li>
&lt;li>預期 query 量在 augment 計算容量內&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不適合&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>補強計算成本爆炸（dataset 大、O(N) per query）&lt;/li>
&lt;li>Augmenting 跟 engine 結果語意不一致（產生 ghost results）&lt;/li>
&lt;li>需要兩 engine 同步狀態才正確&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>當系統能力不滿足使用者預期（capability gap）時、對策有三層階梯、依序評估：</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>對策</th>
          <th>例</th>
          <th>成本</th>
          <th>覆蓋率</th>
          <th>脆弱度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>L1 Expectation alignment</strong></td>
          <td>用文字 / UI / 訊息對齊使用者預期</td>
          <td>UX hint「搜尋為前綴匹配、找 backpressure 請輸入 backpre」</td>
          <td>極低</td>
          <td>部分（需要使用者配合）</td>
          <td>0</td>
      </tr>
      <tr>
          <td><strong>L2 Augmenting computation</strong></td>
          <td>在既有 engine 上加一層補強計算、close gap</td>
          <td>Client-side substring fallback、retry with backoff、computed fallback</td>
          <td>低-中</td>
          <td>高（自動補齊）</td>
          <td>中（多一條 path）</td>
      </tr>
      <tr>
          <td><strong>L3 Structural rebuild</strong></td>
          <td>換 index / engine / 演算法本身</td>
          <td>Build-time tokenize、換 search engine、重設計 schema</td>
          <td>中-高</td>
          <td>滿（從 source 解決）</td>
          <td>高（動 build pipeline）</td>
      </tr>
  </tbody>
</table>
<p><strong>預設順序</strong>：L1 → L2 → L3、依「成本最低先解」。<strong>不必每次跳到 L3</strong> — L3 是最完整但也最貴、L1 在很多情境就夠。</p>
<hr>
<h2 id="為什麼有階梯cost-coverage-trade-off-是真實的">為什麼有階梯：cost-coverage trade-off 是真實的</h2>
<p>直覺反應遇到 capability gap 都想 L3「從根解決」。但 L3 的成本通常 10-100x 於 L1、覆蓋率提升可能只是 80% → 99%、邊際 ROI 低。</p>
<p>實際分布：</p>
<ul>
<li>50% case：L1 就夠（gap 是「使用者誤解」、講清楚就好）</li>
<li>30% case：L2 解掉（gap 是「engine 差一步運算」、補一層 close）</li>
<li>20% case：必須 L3（gap 是「engine 模型錯位」、補不夠、要重來）</li>
</ul>
<p><strong>先試 L1、再試 L2、最後 L3</strong> = 用真實 ROI 排序、不是用「完美主義」排序。</p>
<hr>
<h2 id="三層的判讀">三層的判讀</h2>
<h3 id="l1expectation-alignment">L1：expectation alignment</h3>
<p><strong>適合</strong>：</p>
<ul>
<li>Gap 是「使用者預期跟 system capability 對不齊」、不是「system 算錯」</li>
<li>使用者改變行為就能 close gap（打字方式、order operation、輸入格式）</li>
<li>Production 真的有 capability、只是 affordance 不明顯</li>
</ul>
<p><strong>不適合</strong>：</p>
<ul>
<li>Gap 在 system 算錯、不是預期錯位</li>
<li>使用者無法配合（流量大、不可能教育每個 user）</li>
<li>訊息會被忽略（A/B test 證明 hint 沒人讀）</li>
</ul>
<p><strong>例</strong>：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>L1 對策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-match</td>
          <td>UX hint「搜尋是前綴匹配」+ examples</td>
      </tr>
      <tr>
          <td>Database eventual consistency</td>
          <td>UX「資料同步可能延遲幾秒」+ refresh button</td>
      </tr>
      <tr>
          <td>LLM token limit</td>
          <td>UI 提醒「附件太長、預期會被截斷」</td>
      </tr>
      <tr>
          <td>Network failure</td>
          <td>Toast「網路不穩、稍後再試」</td>
      </tr>
      <tr>
          <td>Concurrent edit</td>
          <td>Banner「另一人也在編輯、你看到的是 5 秒前版本」</td>
      </tr>
  </tbody>
</table>
<hr>
<h3 id="l2augmenting-computation">L2：augmenting computation</h3>
<p><strong>適合</strong>：</p>
<ul>
<li>Engine 缺一層計算就能 close gap、額外計算不貴</li>
<li>Client / proxy / wrapper 層可加運算、不動 engine</li>
<li>預期 query 量在 augment 計算容量內</li>
</ul>
<p><strong>不適合</strong>：</p>
<ul>
<li>補強計算成本爆炸（dataset 大、O(N) per query）</li>
<li>Augmenting 跟 engine 結果語意不一致（產生 ghost results）</li>
<li>需要兩 engine 同步狀態才正確</li>
</ul>
<p><strong>例</strong>：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>L2 對策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-match</td>
          <td>Client-side substring fallback（再掃 client cache）</td>
      </tr>
      <tr>
          <td>Distributed sort</td>
          <td>Client-side merge of partial sorted streams</td>
      </tr>
      <tr>
          <td>LLM context window</td>
          <td>RAG 切片 + retrieval 補齊</td>
      </tr>
      <tr>
          <td>Cache miss</td>
          <td>On-demand compute + write back</td>
      </tr>
      <tr>
          <td>Stale data</td>
          <td>Background refresh + serve stale-while-revalidate</td>
      </tr>
  </tbody>
</table>
<hr>
<h3 id="l3structural-rebuild">L3：structural rebuild</h3>
<p><strong>適合</strong>：</p>
<ul>
<li>L1 / L2 都不夠、capability gap 持續引發痛苦</li>
<li>Production scale 大、L1 教育成本爆 / L2 計算成本爆</li>
<li>系統還沒長太大、重 build 成本可承受</li>
<li>將來會反覆遇到同類 gap（一次重 build、長期解多個問題）</li>
</ul>
<p><strong>不適合</strong>：</p>
<ul>
<li>L1 / L2 還沒試</li>
<li>Production scale 不可動 build pipeline / schema</li>
<li>ROI 不確定（gap 影響範圍小、值得 L3 投入嗎？）</li>
</ul>
<p><strong>例</strong>：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>L3 對策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-match</td>
          <td>Build-time tokenize、換 search engine（Algolia / Elastic）</td>
      </tr>
      <tr>
          <td>Distributed sort</td>
          <td>Sharded sort + index in build pipeline</td>
      </tr>
      <tr>
          <td>LLM context window</td>
          <td>Larger model、custom fine-tune</td>
      </tr>
      <tr>
          <td>Cache miss</td>
          <td>Schema redesign、prefetch policy</td>
      </tr>
      <tr>
          <td>Stale data</td>
          <td>Event-driven invalidation、CRDT</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="從-l1-升級到-l2--l3-的訊號">從 L1 升級到 L2 / L3 的訊號</h2>
<p>不是「永遠先 L1」、是「依訊號逐層升級」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>升級到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 ship 後使用者抱怨「我看到 hint 但還是不會用」</td>
          <td>L2（hint 不夠、要 system 自動補強）</td>
      </tr>
      <tr>
          <td>L1 + L2 ship 後 search miss 率 &gt; X%</td>
          <td>L3（structural fix 必要）</td>
      </tr>
      <tr>
          <td>L1 + L2 ship 後 augment 計算成本 &gt; Y</td>
          <td>L3（換結構降低 marginal cost）</td>
      </tr>
      <tr>
          <td>Use case 從 cosmetic 升級成 production-critical</td>
          <td>L3（風險 / SLA 提升）</td>
      </tr>
      <tr>
          <td>同類 gap 在系統內出現第 3 次</td>
          <td>L3（重 build 一次解多個）</td>
      </tr>
  </tbody>
</table>
<p><strong>逐層升級</strong> vs <strong>一次跳 L3</strong>：前者是 #76 分批 ship 的具體展現；後者是「便利驅動偏移」（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>） — 容易寫的選項是 L3「一勞永逸」、跟實際 ROI 不對齊。</p>
<hr>
<h2 id="從-l3--l2-降級回-l1-的訊號">從 L3 / L2 降級回 L1 的訊號</h2>
<p>階梯不是只能升、也該能降 — L3 ship 後不該當「永久解」、是 ROI 動態的選擇。看到以下訊號、考慮降級：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>降級到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L3 transformation 每次 dependency upgrade 都要修</td>
          <td>L1 / L2（L3 維護成本 &gt; 收益）</td>
      </tr>
      <tr>
          <td>Use case 變化、L3 解的問題已不存在</td>
          <td>拔掉 L3、退到 L2 或不需要</td>
      </tr>
      <tr>
          <td>L3 ship 後 close gap 率 &lt; 10%（投入 / 受益不對等）</td>
          <td>可能該重設計、不只升降</td>
      </tr>
      <tr>
          <td>Pagefind / engine 升級後 native 支援了</td>
          <td>拔 L3 transformation、用 native</td>
      </tr>
      <tr>
          <td>L3 引入新 bug 比解的 gap 多</td>
          <td>退回 L1 + 顯式說「不支援」更誠實</td>
      </tr>
      <tr>
          <td>L1 hint 已經教育大多數 user 改變行為</td>
          <td>L2 / L3 fallback 觸發率低、可降級</td>
      </tr>
  </tbody>
</table>
<h3 id="為什麼降級難">為什麼降級難</h3>
<p>升級有「使用者抱怨」當外部觸發、降級沒有 — 沒人抱怨「我們的 transformation 太多」。所以降級是典型的 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 工作、需要結構性 trigger：</p>
<ul>
<li>Periodic review（每季 review「我們還需要這個 L3 嗎」）</li>
<li>Dependency upgrade event（升級觸發「L3 還相容嗎、還必要嗎」）</li>
<li>Maintenance cost log（紀錄 L3 修了 N 次、累積到 threshold 觸發 review）</li>
</ul>
<h3 id="pruning-是正常-lifecycle">Pruning 是正常 lifecycle</h3>
<p>降級不是「我們之前做錯」、是「ROI 變化、調整」。L3 在 ship 當下是最佳解、現在不是了 — 接受 capability gap 對策也會過時、跟其他工程決策同。</p>
<hr>
<h2 id="階梯-vs-疊加跟-75-的差別">階梯 vs 疊加：跟 #75 的差別</h2>
<p><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強策略</a> 講的是<strong>多策略疊加在不同層</strong>（structural + UX 並用）。本卡講的是<strong>同一個 gap 上、選哪一層</strong>（L1 vs L2 vs L3 通常選一個）。</p>
<p>兩卡互補：</p>
<ul>
<li>#75：選了 L3 後、要不要再加 L1 UX hint 當補強？（疊加維度）</li>
<li>#86（本卡）：先試 L1 還是直接 L3？（階梯維度）</li>
</ul>
<p>實際 case 通常兩條都用：先 #86 選層級、再 #75 看要不要疊加。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跳過 L1 直接 L3</td>
          <td>過度工程、ROI 邊際</td>
      </tr>
      <tr>
          <td>L1 ship 後不評估、預設要繼續 L3</td>
          <td>缺數據、可能 L1 已夠</td>
      </tr>
      <tr>
          <td>「L1 是 hack、L3 才是 real fix」道德判斷</td>
          <td>阻止 L1 的價值、使用者多受苦</td>
      </tr>
      <tr>
          <td>L2 augmenting 沒邊界、dataset 變大時 OOM</td>
          <td>L2 該升 L3 了沒升</td>
      </tr>
      <tr>
          <td>L1 hint 寫滿但 production 沒監測有沒有用</td>
          <td>不知道 hint 有沒有 close gap</td>
      </tr>
      <tr>
          <td>同類 gap 每次都 L3 一次</td>
          <td>缺 #75 疊加思維、每次重 build</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時直接跳-l3">何時直接跳 L3</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gap 是 security / data integrity</td>
          <td>L1 / L2 不夠、必須 root fix</td>
      </tr>
      <tr>
          <td>已 L1 / L2 過 N 次、gap 還在</td>
          <td>證據累積、L3 ROI 已正</td>
      </tr>
      <tr>
          <td>Production scale 不允許 L1 教育 / L2 計算</td>
          <td>跨過 L1 / L2 的可行區</td>
      </tr>
      <tr>
          <td>重 build 成本當前最低（系統還小）</td>
          <td>越早 L3 越便宜</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>L1 / L2 已知不夠、或 L3 真的最便宜</strong>。其他情境都該先試 L1。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強疊加</a></td>
          <td>#75 是「同 gap 上選不選疊加」、本卡是「先選哪層」 — 互補</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship</a></td>
          <td>L1 → L2 → L3 升級 = 分批 ship 在 capability 維度的展現</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>search prefix-match 是本卡 L1 / L2 / L3 三層的具體 case</td>
      </tr>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>#59 的五策略可重新映射到本卡三層（A 推進 query = L3、D UX hint = L1）</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>L1 / L2 多偏字面層、L3 動結構、選層需 multi-pass review</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「直接 L3」沒講為什麼不 L1</td>
          <td>補 L1 評估、確認真不夠</td>
      </tr>
      <tr>
          <td>L1 ship 後沒監測 close gap 率</td>
          <td>補 telemetry、決定要不要升 L2</td>
      </tr>
      <tr>
          <td>「這個 hint 沒用、user 不讀」抱怨</td>
          <td>確認是真不讀還是 hint 寫不對、不直接跳 L3</td>
      </tr>
      <tr>
          <td>L2 augmenting 成本越來越高</td>
          <td>升 L3 的訊號、不是 L2 寫得不夠好</td>
      </tr>
      <tr>
          <td>同類 gap 第 3 次 L1 解掉</td>
          <td>抽 pattern、可能該寫成 reusable component</td>
      </tr>
      <tr>
          <td>L3 ship 後 L1 hint 沒拔</td>
          <td>三層共存反而冗餘、清理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Capability gap 不是只有 L3 一條路 — L1 / L2 / L3 是 ROI 不同的三層階梯、依「成本最低先解」順序評估。<strong>「直接 L3」的便利感跟實際 ROI 反相關</strong>（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）— 寫 L3 在白板上很爽、但通常 L1 / L2 已夠。</p>
]]></content:encoded></item><item><title>Build-time 預處理 vs Runtime 計算的光譜：何時把成本前置</title><link>https://tarrragon.github.io/blog/report/build-time-vs-runtime-computation-spectrum/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/build-time-vs-runtime-computation-spectrum/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>計算放哪裡有光譜、不是二元：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>預付成本&lt;/th>
 &lt;th>Runtime 成本&lt;/th>
 &lt;th>儲存成本&lt;/th>
 &lt;th>Freshness&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Pure build-time&lt;/strong>&lt;/td>
 &lt;td>高（pipeline + 一次計算全部）&lt;/td>
 &lt;td>~0&lt;/td>
 &lt;td>高（存 N 種預算結果）&lt;/td>
 &lt;td>差（每次 build 才 refresh）&lt;/td>
 &lt;td>高頻 query、少變動、closed-set&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Hybrid hot-path&lt;/strong>&lt;/td>
 &lt;td>中（預算 top X%）&lt;/td>
 &lt;td>低（hot 命中 0、cold runtime）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中（hot stale 風險）&lt;/td>
 &lt;td>長尾分布、可分 hot/cold&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Pure runtime&lt;/strong>&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>高（per query）&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>即時&lt;/td>
 &lt;td>低頻、高變動、open-ended query&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>光譜兩端都有合理場景、不是「build-time 永遠贏」。&lt;strong>選哪個位置依四軸：query 頻率 / dataset 大小 / freshness 需求 / build pipeline 複雜度&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="build-time-的三個成本維度">build-time 的三個成本維度&lt;/h2>
&lt;p>直覺反應：「能 precompute 就 precompute、runtime 0 最好」。但這個直覺漏了三個維度：&lt;/p>
&lt;h3 id="1-freshness-成本">1. Freshness 成本&lt;/h3>
&lt;p>Build-time 結果是 build 那一刻的 snapshot。dataset 改變後、結果直到下次 build 才 refresh。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>適合 build-time&lt;/strong>：靜態 / 慢變的內容（blog post、product catalog）&lt;/li>
&lt;li>&lt;strong>不適合 build-time&lt;/strong>：頻繁更新（user posts、live data、search index over user content）&lt;/li>
&lt;/ul>
&lt;h3 id="2-儲存成本">2. 儲存成本&lt;/h3>
&lt;p>Precompute N 種 query 的結果 = 存 N 份。當 query 是 open-ended（任意組合 filter / sort / search term）、N 是組合爆炸。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>適合 build-time&lt;/strong>：closed-set（fixed list of routes、pre-defined search terms）&lt;/li>
&lt;li>&lt;strong>不適合 build-time&lt;/strong>：open-ended（任意 user input）&lt;/li>
&lt;/ul>
&lt;h3 id="3-pipeline-複雜度">3. Pipeline 複雜度&lt;/h3>
&lt;p>Build-time 計算需要 build pipeline 配合 — 加一條規則 = 加一份 artifact、需要 CI 跑、版本管理、deployment 同步。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>適合 build-time&lt;/strong>：已有 build pipeline、加一條規則便宜&lt;/li>
&lt;li>&lt;strong>不適合 build-time&lt;/strong>：純 dynamic system、加 build step = 引入新 infrastructure&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="四軸判準">四軸判準&lt;/h2>
&lt;p>評估某個計算該放哪一端：&lt;/p>
&lt;h3 id="軸-1query-頻率">軸 1：Query 頻率&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>高頻&lt;/strong>（同一 query 每秒被 call N 次）→ build-time 划算（一次算、N 次受益）&lt;/li>
&lt;li>&lt;strong>低頻&lt;/strong>（query 多樣、每個 query 唯一）→ runtime 划算（precompute 全部 = 浪費）&lt;/li>
&lt;/ul>
&lt;h3 id="軸-2dataset-大小">軸 2：Dataset 大小&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>小 dataset&lt;/strong> → 兩端都可以、依其他軸&lt;/li>
&lt;li>&lt;strong>大 dataset&lt;/strong> → build-time 的儲存成本爆炸、傾向 runtime / hybrid&lt;/li>
&lt;li>&lt;strong>超大&lt;/strong> → 幾乎強制 runtime（即使 hot path 也 partial precompute）&lt;/li>
&lt;/ul>
&lt;h3 id="軸-3freshness-需求">軸 3：Freshness 需求&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>可接受 stale&lt;/strong>（小時 / 天） → build-time 可行&lt;/li>
&lt;li>&lt;strong>要近即時&lt;/strong>（分鐘級） → runtime 或 hybrid + invalidation&lt;/li>
&lt;li>&lt;strong>強即時&lt;/strong>（秒級） → 強制 runtime&lt;/li>
&lt;/ul>
&lt;h3 id="軸-4build-pipeline-複雜度">軸 4：Build pipeline 複雜度&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>既有 pipeline 成熟&lt;/strong> → 加 build-time step 便宜&lt;/li>
&lt;li>&lt;strong>沒 pipeline 或脆弱&lt;/strong> → runtime 更實際（不引入新 infrastructure）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="三段光譜的實例對照">三段光譜的實例對照&lt;/h2>
&lt;h3 id="pure-build-time">Pure build-time&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>領域&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>靜態網站&lt;/td>
 &lt;td>Hugo / Jekyll generate HTML&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Search index&lt;/td>
 &lt;td>Pagefind build-time index、Algolia indexer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Image 變體&lt;/td>
 &lt;td>sharp / imagemin pre-generate sizes&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Route table&lt;/td>
 &lt;td>Compile-time routes（Next.js static export）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ML model&lt;/td>
 &lt;td>Train once、serve trained weights&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sitemap / RSS&lt;/td>
 &lt;td>Build-time generate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="hybrid-hot-path">Hybrid hot-path&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>領域&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cache (Redis)&lt;/td>
 &lt;td>Hot keys precompute、cold keys runtime + write-back&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN&lt;/td>
 &lt;td>Hot routes cached、cold routes hit origin&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM RAG&lt;/td>
 &lt;td>Hot embeddings precompute、cold runtime embed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Search autocomplete&lt;/td>
 &lt;td>Top N suggestions precompute、tail runtime&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Image responsive&lt;/td>
 &lt;td>Hot sizes precompute、edge cases runtime resize&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="pure-runtime">Pure runtime&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>領域&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Live search over user data&lt;/td>
 &lt;td>每 query 掃 DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>User-specific compute（dashboard、recommendation）&lt;/td>
 &lt;td>每 user 每次 reload 算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Real-time analytics&lt;/td>
 &lt;td>per-event 處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Open-ended NLP query&lt;/td>
 &lt;td>LLM call per query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Crypto / hash signature&lt;/td>
 &lt;td>Per-request 算&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="兩極之間的決策當不確定該選哪端">兩極之間的決策：當不確定該選哪端&lt;/h2>
&lt;h3 id="步驟-1列query-frequency--dataset-size象限">步驟 1：列「query frequency × dataset size」象限&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;/th>
 &lt;th>小 dataset&lt;/th>
 &lt;th>大 dataset&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>高頻 query&lt;/strong>&lt;/td>
 &lt;td>Build-time&lt;/td>
 &lt;td>Hybrid（hot precompute）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>低頻 query&lt;/strong>&lt;/td>
 &lt;td>兩端都可&lt;/td>
 &lt;td>Runtime&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="步驟-2套-freshness-限制">步驟 2：套 freshness 限制&lt;/h3>
&lt;p>如果 freshness 需求高、把 build-time 列從候選移除（除非有 incremental build / invalidation 機制）。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>計算放哪裡有光譜、不是二元：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>預付成本</th>
          <th>Runtime 成本</th>
          <th>儲存成本</th>
          <th>Freshness</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pure build-time</strong></td>
          <td>高（pipeline + 一次計算全部）</td>
          <td>~0</td>
          <td>高（存 N 種預算結果）</td>
          <td>差（每次 build 才 refresh）</td>
          <td>高頻 query、少變動、closed-set</td>
      </tr>
      <tr>
          <td><strong>Hybrid hot-path</strong></td>
          <td>中（預算 top X%）</td>
          <td>低（hot 命中 0、cold runtime）</td>
          <td>中</td>
          <td>中（hot stale 風險）</td>
          <td>長尾分布、可分 hot/cold</td>
      </tr>
      <tr>
          <td><strong>Pure runtime</strong></td>
          <td>0</td>
          <td>高（per query）</td>
          <td>0</td>
          <td>即時</td>
          <td>低頻、高變動、open-ended query</td>
      </tr>
  </tbody>
</table>
<p>光譜兩端都有合理場景、不是「build-time 永遠贏」。<strong>選哪個位置依四軸：query 頻率 / dataset 大小 / freshness 需求 / build pipeline 複雜度</strong>。</p>
<hr>
<h2 id="build-time-的三個成本維度">build-time 的三個成本維度</h2>
<p>直覺反應：「能 precompute 就 precompute、runtime 0 最好」。但這個直覺漏了三個維度：</p>
<h3 id="1-freshness-成本">1. Freshness 成本</h3>
<p>Build-time 結果是 build 那一刻的 snapshot。dataset 改變後、結果直到下次 build 才 refresh。</p>
<ul>
<li><strong>適合 build-time</strong>：靜態 / 慢變的內容（blog post、product catalog）</li>
<li><strong>不適合 build-time</strong>：頻繁更新（user posts、live data、search index over user content）</li>
</ul>
<h3 id="2-儲存成本">2. 儲存成本</h3>
<p>Precompute N 種 query 的結果 = 存 N 份。當 query 是 open-ended（任意組合 filter / sort / search term）、N 是組合爆炸。</p>
<ul>
<li><strong>適合 build-time</strong>：closed-set（fixed list of routes、pre-defined search terms）</li>
<li><strong>不適合 build-time</strong>：open-ended（任意 user input）</li>
</ul>
<h3 id="3-pipeline-複雜度">3. Pipeline 複雜度</h3>
<p>Build-time 計算需要 build pipeline 配合 — 加一條規則 = 加一份 artifact、需要 CI 跑、版本管理、deployment 同步。</p>
<ul>
<li><strong>適合 build-time</strong>：已有 build pipeline、加一條規則便宜</li>
<li><strong>不適合 build-time</strong>：純 dynamic system、加 build step = 引入新 infrastructure</li>
</ul>
<hr>
<h2 id="四軸判準">四軸判準</h2>
<p>評估某個計算該放哪一端：</p>
<h3 id="軸-1query-頻率">軸 1：Query 頻率</h3>
<ul>
<li><strong>高頻</strong>（同一 query 每秒被 call N 次）→ build-time 划算（一次算、N 次受益）</li>
<li><strong>低頻</strong>（query 多樣、每個 query 唯一）→ runtime 划算（precompute 全部 = 浪費）</li>
</ul>
<h3 id="軸-2dataset-大小">軸 2：Dataset 大小</h3>
<ul>
<li><strong>小 dataset</strong> → 兩端都可以、依其他軸</li>
<li><strong>大 dataset</strong> → build-time 的儲存成本爆炸、傾向 runtime / hybrid</li>
<li><strong>超大</strong> → 幾乎強制 runtime（即使 hot path 也 partial precompute）</li>
</ul>
<h3 id="軸-3freshness-需求">軸 3：Freshness 需求</h3>
<ul>
<li><strong>可接受 stale</strong>（小時 / 天） → build-time 可行</li>
<li><strong>要近即時</strong>（分鐘級） → runtime 或 hybrid + invalidation</li>
<li><strong>強即時</strong>（秒級） → 強制 runtime</li>
</ul>
<h3 id="軸-4build-pipeline-複雜度">軸 4：Build pipeline 複雜度</h3>
<ul>
<li><strong>既有 pipeline 成熟</strong> → 加 build-time step 便宜</li>
<li><strong>沒 pipeline 或脆弱</strong> → runtime 更實際（不引入新 infrastructure）</li>
</ul>
<hr>
<h2 id="三段光譜的實例對照">三段光譜的實例對照</h2>
<h3 id="pure-build-time">Pure build-time</h3>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態網站</td>
          <td>Hugo / Jekyll generate HTML</td>
      </tr>
      <tr>
          <td>Search index</td>
          <td>Pagefind build-time index、Algolia indexer</td>
      </tr>
      <tr>
          <td>Image 變體</td>
          <td>sharp / imagemin pre-generate sizes</td>
      </tr>
      <tr>
          <td>Route table</td>
          <td>Compile-time routes（Next.js static export）</td>
      </tr>
      <tr>
          <td>ML model</td>
          <td>Train once、serve trained weights</td>
      </tr>
      <tr>
          <td>Sitemap / RSS</td>
          <td>Build-time generate</td>
      </tr>
  </tbody>
</table>
<h3 id="hybrid-hot-path">Hybrid hot-path</h3>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cache (Redis)</td>
          <td>Hot keys precompute、cold keys runtime + write-back</td>
      </tr>
      <tr>
          <td>CDN</td>
          <td>Hot routes cached、cold routes hit origin</td>
      </tr>
      <tr>
          <td>LLM RAG</td>
          <td>Hot embeddings precompute、cold runtime embed</td>
      </tr>
      <tr>
          <td>Search autocomplete</td>
          <td>Top N suggestions precompute、tail runtime</td>
      </tr>
      <tr>
          <td>Image responsive</td>
          <td>Hot sizes precompute、edge cases runtime resize</td>
      </tr>
  </tbody>
</table>
<h3 id="pure-runtime">Pure runtime</h3>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Live search over user data</td>
          <td>每 query 掃 DB</td>
      </tr>
      <tr>
          <td>User-specific compute（dashboard、recommendation）</td>
          <td>每 user 每次 reload 算</td>
      </tr>
      <tr>
          <td>Real-time analytics</td>
          <td>per-event 處理</td>
      </tr>
      <tr>
          <td>Open-ended NLP query</td>
          <td>LLM call per query</td>
      </tr>
      <tr>
          <td>Crypto / hash signature</td>
          <td>Per-request 算</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="兩極之間的決策當不確定該選哪端">兩極之間的決策：當不確定該選哪端</h2>
<h3 id="步驟-1列query-frequency--dataset-size象限">步驟 1：列「query frequency × dataset size」象限</h3>
<table>
  <thead>
      <tr>
          <th></th>
          <th>小 dataset</th>
          <th>大 dataset</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>高頻 query</strong></td>
          <td>Build-time</td>
          <td>Hybrid（hot precompute）</td>
      </tr>
      <tr>
          <td><strong>低頻 query</strong></td>
          <td>兩端都可</td>
          <td>Runtime</td>
      </tr>
  </tbody>
</table>
<h3 id="步驟-2套-freshness-限制">步驟 2：套 freshness 限制</h3>
<p>如果 freshness 需求高、把 build-time 列從候選移除（除非有 incremental build / invalidation 機制）。</p>
<h3 id="步驟-3看-build-pipeline-cost">步驟 3：看 build pipeline cost</h3>
<p>如果 build-time 成本（新 step、新 artifact、新 deploy 流程）大於 runtime 成本（per query CPU）、選 runtime。</p>
<h3 id="步驟-4留-escape-hatch">步驟 4：留 escape hatch</h3>
<p>選了一端不代表永遠 — 設計 invalidation hook / runtime fallback、未來能重新平衡。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「能 precompute 就 precompute」當預設</td>
          <td>freshness / 儲存爆炸</td>
      </tr>
      <tr>
          <td>「runtime 比較動態」當預設</td>
          <td>高頻 query 浪費 CPU</td>
      </tr>
      <tr>
          <td>Build-time 沒留 invalidation hook</td>
          <td>dataset 改了無法 refresh</td>
      </tr>
      <tr>
          <td>Hybrid 沒明示 hot 邊界</td>
          <td>運作不穩、cold path 突然爆量</td>
      </tr>
      <tr>
          <td>把 freshness 假設成「不變」</td>
          <td>真實 dataset 會變、blowup</td>
      </tr>
      <tr>
          <td>Pre-build 全部 + runtime 又再算一次</td>
          <td>雙倍成本、無增益</td>
      </tr>
      <tr>
          <td>「先 runtime、之後 optimize 成 build-time」當口號</td>
          <td>optimize 那次永遠不發生（<a href="../external-trigger-for-high-roi-work/">#72</a>）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時是兩端都不對要重思-problem">何時是「兩端都不對、要重思 problem」</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build-time 結果 stale、runtime 又太慢</td>
          <td>Hybrid + invalidation 設計</td>
      </tr>
      <tr>
          <td>Hybrid hot 一直 miss、cold path 是常態</td>
          <td>重排 hot 邊界、可能整個翻成 pure runtime</td>
      </tr>
      <tr>
          <td>Open-ended query 試圖 build-time</td>
          <td>Reformulate problem、可能要分 query class</td>
      </tr>
      <tr>
          <td>加了 invalidation 後 build pipeline 太複雜</td>
          <td>改成 runtime + cache、別再強行 build-time</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>L3 structural rebuild 通常是「動 build-time 計算」、本卡是 L3 內部的具體取捨</td>
      </tr>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>A 推進 query（runtime）vs C 預先建 index（build-time）vs B 自動續抓（hybrid）— 五策略的本卡映射</td>
      </tr>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強疊加</a></td>
          <td>Hybrid hot-path 是 build-time + runtime 疊加的具體 case</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>Hybrid 的 hot 邊界 = 最小必要範圍、有證據再擴張</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>Build-time tokenize（B 策略）vs client-side fallback（C 策略）就是本卡兩極的具體 case</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「能 precompute 就 precompute」沒列軸</td>
          <td>套四軸（頻率 / 大小 / freshness / pipeline）</td>
      </tr>
      <tr>
          <td>Build-time artifact 越來越大</td>
          <td>檢查 query frequency 分布、可能該移到 hybrid</td>
      </tr>
      <tr>
          <td>Runtime 計算成本爆</td>
          <td>找 hot path、考慮 hybrid</td>
      </tr>
      <tr>
          <td>Freshness 抱怨</td>
          <td>Build-time 已不適用、改 hybrid + invalidation</td>
      </tr>
      <tr>
          <td>加了 build step 後 deploy 變慢</td>
          <td>Build pipeline 成本不可忽略、評估是否仍划算</td>
      </tr>
      <tr>
          <td>Hybrid 邊界從沒重新 review</td>
          <td>hot / cold 比例會漂移、定期重 baseline</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Build-time vs runtime 是光譜、不是二元 — 中間 hybrid 段是多數實務情境的最佳位置。<strong>「能 precompute 就 precompute」是便利驅動（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）的口號</strong>、實際要套四軸（頻率 / 大小 / freshness / pipeline）才知道該放哪。</p>
]]></content:encoded></item><item><title>視覺手段對齊錯誤層次：CSS / emoji 修不到語意 / 邏輯問題</title><link>https://tarrragon.github.io/blog/report/visual-tool-error-layer-alignment/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/visual-tool-error-layer-alignment/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫作 / UI 中的問題分三層、不同層需要不同修法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題層次&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>適合手段&lt;/th>
 &lt;th>不適合手段&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>邏輯&lt;/td>
 &lt;td>概念劃分混亂、論證不完整、兩個概念擠在一行&lt;/td>
 &lt;td>重新分概念、改結構&lt;/td>
 &lt;td>CSS / 排版 / emoji（蓋不到根因）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語意&lt;/td>
 &lt;td>用 emoji 作為唯一區分、用顏色傳達唯一資訊、用視覺替代結構&lt;/td>
 &lt;td>改表達結構、用文本標記、加分段&lt;/td>
 &lt;td>CSS 規則（false confidence）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>視覺&lt;/td>
 &lt;td>容器寬度、字體大小、顏色對比、跨瀏覽器排版&lt;/td>
 &lt;td>CSS、media query、渲染工具&lt;/td>
 &lt;td>改文章結構（過殺）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：修視覺工具預設&lt;strong>錯誤就在視覺層&lt;/strong>。當症狀其實來自語意 / 邏輯層、用視覺工具修 = 蓋掉表面、根因還在 + 作者誤以為解決了（false confidence、跟 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82&lt;/a> 的 hook 心理同病）。&lt;/p>
&lt;p>修法的順序是 &lt;strong>深層 → 淺層&lt;/strong>：先問「是不是邏輯 / 語意層的下游症狀」、是的話改結構；確認純視覺、再用視覺工具。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼視覺工具對語意--邏輯問題無能為力">為什麼視覺工具對語意 / 邏輯問題無能為力&lt;/h2>
&lt;p>視覺工具（CSS、emoji、顏色、字體、間距、圖示）的本質是 &lt;strong>呈現層的調整&lt;/strong> — 它能改變字怎麼顯示、不能改變字本身代表的概念。能改的、跟改不到的、有清楚的界線：&lt;/p>
&lt;p>&lt;strong>能改的（純呈現）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>文字在窄視窗會不會換行&lt;/li>
&lt;li>兩個區塊的視覺距離&lt;/li>
&lt;li>不同類型用什麼顏色標&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>改不到的（結構 / 語意 / 邏輯）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>兩個概念該不該擠在同一行（結構層）&lt;/li>
&lt;li>用 emoji 區分是否足夠承載語意（語意層）&lt;/li>
&lt;li>論證有沒有完整（邏輯層）&lt;/li>
&lt;/ul>
&lt;p>當作者試圖用視覺工具解語意 / 邏輯問題、結果是 &lt;strong>症狀被表面平整、但下次同類問題會用新形狀冒出來&lt;/strong> — 因為根因（概念混淆 / 結構錯位）沒動。&lt;/p>
&lt;hr>
&lt;h2 id="反模式用視覺修補蓋住下游症狀">反模式：用視覺修補蓋住下游症狀&lt;/h2>
&lt;h3 id="false-confidence-比沒修更危險">False confidence 比沒修更危險&lt;/h3>
&lt;p>修了 CSS 之後、心理上會覺得「處理完了」。實際上 CSS 只擋了表面斷行、語意混淆照舊存在 — 但作者不再警覺、因為「我修過了」。&lt;/p>
&lt;p>下一個讀者在不同 viewport / 不同設備 / 用螢幕閱讀器時、同樣的語意問題會用不同形狀重新出現（換行錯位、TTS 讀錯、複製貼上格式跑掉）。&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>沒修任何東西&lt;/td>
 &lt;td>高（看到症狀）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CSS 修了視覺、根因（語意混淆）還在&lt;/td>
 &lt;td>低（誤以為修了）&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>（換 context 就復發）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>改結構修了根因&lt;/td>
 &lt;td>適中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第二行是最危險組合 — 跟 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82&lt;/a> 的「Hook 抓不到的範圍誤以為有保護」同病。&lt;/p>
&lt;h3 id="症狀堆疊再加一條-css-規則永遠補不完">症狀堆疊：「再加一條 CSS 規則」永遠補不完&lt;/h3>
&lt;p>視覺修補的直覺反應是「加一條規則」。但語意 / 邏輯下游的症狀無限多、規則永遠補不完。實際軌跡：&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">emoji 在窄 viewport 斷行
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> → 加 white-space: nowrap
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> → 文字溢出容器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> → 加 overflow: hidden
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> → 文字被截斷看不見
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> → 加 text-overflow: ellipsis
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> → 螢幕閱讀器讀出「...」沒語意
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> → 加 aria-label 補語意
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> → 翻譯版本 aria-label 沒翻
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> → ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每加一層 CSS、根因（兩個概念擠在一行）更深埋、修起來更貴。&lt;/p>
&lt;p>→ CSS 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。跟 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82&lt;/a> 的 hook 規則膨脹同骨。&lt;/p>
&lt;hr>
&lt;h2 id="三層優先序邏輯--語意--視覺">三層優先序：邏輯 → 語意 → 視覺&lt;/h2>
&lt;h3 id="為什麼是這個順序">為什麼是這個順序&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>影響範圍&lt;/th>
 &lt;th>修起來成本&lt;/th>
 &lt;th>修不修的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>邏輯&lt;/td>
 &lt;td>整篇 / 整個 feature 的可理解性&lt;/td>
 &lt;td>高（要重新分概念）&lt;/td>
 &lt;td>高（讀者根本看不懂、抓不到重點）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語意&lt;/td>
 &lt;td>段落 / 區塊的表達精度&lt;/td>
 &lt;td>中（要改結構）&lt;/td>
 &lt;td>中（讀者抓得到但要花力氣推）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>視覺&lt;/td>
 &lt;td>局部呈現&lt;/td>
 &lt;td>低（改 CSS / 排版）&lt;/td>
 &lt;td>低（讀者覺得醜、但能讀）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>深層問題的影響範圍大、修不到根因的代價高&lt;/strong>。所以修法順序是先問深層、確認沒問題再修淺層。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫作 / UI 中的問題分三層、不同層需要不同修法：</p>
<table>
  <thead>
      <tr>
          <th>問題層次</th>
          <th>例子</th>
          <th>適合手段</th>
          <th>不適合手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邏輯</td>
          <td>概念劃分混亂、論證不完整、兩個概念擠在一行</td>
          <td>重新分概念、改結構</td>
          <td>CSS / 排版 / emoji（蓋不到根因）</td>
      </tr>
      <tr>
          <td>語意</td>
          <td>用 emoji 作為唯一區分、用顏色傳達唯一資訊、用視覺替代結構</td>
          <td>改表達結構、用文本標記、加分段</td>
          <td>CSS 規則（false confidence）</td>
      </tr>
      <tr>
          <td>視覺</td>
          <td>容器寬度、字體大小、顏色對比、跨瀏覽器排版</td>
          <td>CSS、media query、渲染工具</td>
          <td>改文章結構（過殺）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：修視覺工具預設<strong>錯誤就在視覺層</strong>。當症狀其實來自語意 / 邏輯層、用視覺工具修 = 蓋掉表面、根因還在 + 作者誤以為解決了（false confidence、跟 <a href="../literal-interception-vs-behavioral-refinement/">#82</a> 的 hook 心理同病）。</p>
<p>修法的順序是 <strong>深層 → 淺層</strong>：先問「是不是邏輯 / 語意層的下游症狀」、是的話改結構；確認純視覺、再用視覺工具。</p>
<hr>
<h2 id="為什麼視覺工具對語意--邏輯問題無能為力">為什麼視覺工具對語意 / 邏輯問題無能為力</h2>
<p>視覺工具（CSS、emoji、顏色、字體、間距、圖示）的本質是 <strong>呈現層的調整</strong> — 它能改變字怎麼顯示、不能改變字本身代表的概念。能改的、跟改不到的、有清楚的界線：</p>
<p><strong>能改的（純呈現）</strong>：</p>
<ul>
<li>文字在窄視窗會不會換行</li>
<li>兩個區塊的視覺距離</li>
<li>不同類型用什麼顏色標</li>
</ul>
<p><strong>改不到的（結構 / 語意 / 邏輯）</strong>：</p>
<ul>
<li>兩個概念該不該擠在同一行（結構層）</li>
<li>用 emoji 區分是否足夠承載語意（語意層）</li>
<li>論證有沒有完整（邏輯層）</li>
</ul>
<p>當作者試圖用視覺工具解語意 / 邏輯問題、結果是 <strong>症狀被表面平整、但下次同類問題會用新形狀冒出來</strong> — 因為根因（概念混淆 / 結構錯位）沒動。</p>
<hr>
<h2 id="反模式用視覺修補蓋住下游症狀">反模式：用視覺修補蓋住下游症狀</h2>
<h3 id="false-confidence-比沒修更危險">False confidence 比沒修更危險</h3>
<p>修了 CSS 之後、心理上會覺得「處理完了」。實際上 CSS 只擋了表面斷行、語意混淆照舊存在 — 但作者不再警覺、因為「我修過了」。</p>
<p>下一個讀者在不同 viewport / 不同設備 / 用螢幕閱讀器時、同樣的語意問題會用不同形狀重新出現（換行錯位、TTS 讀錯、複製貼上格式跑掉）。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>警覺度</th>
          <th>同類問題復發率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>沒修任何東西</td>
          <td>高（看到症狀）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>CSS 修了視覺、根因（語意混淆）還在</td>
          <td>低（誤以為修了）</td>
          <td><strong>高</strong>（換 context 就復發）</td>
      </tr>
      <tr>
          <td>改結構修了根因</td>
          <td>適中</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>第二行是最危險組合 — 跟 <a href="../literal-interception-vs-behavioral-refinement/">#82</a> 的「Hook 抓不到的範圍誤以為有保護」同病。</p>
<h3 id="症狀堆疊再加一條-css-規則永遠補不完">症狀堆疊：「再加一條 CSS 規則」永遠補不完</h3>
<p>視覺修補的直覺反應是「加一條規則」。但語意 / 邏輯下游的症狀無限多、規則永遠補不完。實際軌跡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">emoji 在窄 viewport 斷行
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  → 加 white-space: nowrap
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  → 文字溢出容器
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  → 加 overflow: hidden
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  → 文字被截斷看不見
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  → 加 text-overflow: ellipsis
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  → 螢幕閱讀器讀出「...」沒語意
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  → 加 aria-label 補語意
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  → 翻譯版本 aria-label 沒翻
</span></span><span class="line"><span class="ln">10</span><span class="cl">  → ...</span></span></code></pre></div><p>每加一層 CSS、根因（兩個概念擠在一行）更深埋、修起來更貴。</p>
<p>→ CSS 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。跟 <a href="../literal-interception-vs-behavioral-refinement/">#82</a> 的 hook 規則膨脹同骨。</p>
<hr>
<h2 id="三層優先序邏輯--語意--視覺">三層優先序：邏輯 → 語意 → 視覺</h2>
<h3 id="為什麼是這個順序">為什麼是這個順序</h3>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>影響範圍</th>
          <th>修起來成本</th>
          <th>修不修的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邏輯</td>
          <td>整篇 / 整個 feature 的可理解性</td>
          <td>高（要重新分概念）</td>
          <td>高（讀者根本看不懂、抓不到重點）</td>
      </tr>
      <tr>
          <td>語意</td>
          <td>段落 / 區塊的表達精度</td>
          <td>中（要改結構）</td>
          <td>中（讀者抓得到但要花力氣推）</td>
      </tr>
      <tr>
          <td>視覺</td>
          <td>局部呈現</td>
          <td>低（改 CSS / 排版）</td>
          <td>低（讀者覺得醜、但能讀）</td>
      </tr>
  </tbody>
</table>
<p><strong>深層問題的影響範圍大、修不到根因的代價高</strong>。所以修法順序是先問深層、確認沒問題再修淺層。</p>
<h3 id="修法的順序">修法的順序</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">看到症狀
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">這是邏輯層問題嗎？（兩個獨立概念被擠在一起？論證有缺口？）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   是 → 重新分概念 / 補論證、結構改了之後語意 / 視覺問題自然消失
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   否 → 下一層
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">這是語意層問題嗎？（依賴視覺標記傳達唯一資訊？emoji 是唯一區分？）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   是 → 改表達結構、加文本標籤、用列表 / 分段
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   否 → 下一層
</span></span><span class="line"><span class="ln">10</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">11</span><span class="cl">純視覺問題（容器寬度、字體大小、顏色對比）
</span></span><span class="line"><span class="ln">12</span><span class="cl">   是 → CSS / media query / 渲染配置</span></span></code></pre></div><p><strong>反向（從視覺往邏輯推）會 false confidence</strong>：先用 CSS 補了、表面平整、誤以為解決了、下次換 context 復發。</p>
<hr>
<h2 id="何時視覺修補真的足夠">何時視覺修補真的足夠</h2>
<p>某些情境純視覺就夠、用 CSS 是對的：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 CSS 夠</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨 viewport 排版適配（手機 / 桌面）</td>
          <td>內容沒變、只是顯示尺寸要適配</td>
      </tr>
      <tr>
          <td>字體大小 / 行高 / 顏色對比</td>
          <td>純呈現參數</td>
      </tr>
      <tr>
          <td>容器溢出 / 滾動條 / 換行控制</td>
          <td>layout 行為</td>
      </tr>
      <tr>
          <td>跨瀏覽器渲染差異</td>
          <td>引擎差異、不是內容問題</td>
      </tr>
      <tr>
          <td>主題切換（dark / light mode）</td>
          <td>純呈現變數</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>內容本身沒爭議、只是顯示方式要調</strong>。其他情境視覺工具都會在某個時點走到 ceiling。</p>
<hr>
<h2 id="識別-ceiling什麼時候該換手段">識別 ceiling：什麼時候該換手段</h2>
<p>ceiling 訊號：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換的手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「修了 CSS、換個 viewport / 設備又壞了」</td>
          <td>不是純視覺、有結構問題、改結構</td>
      </tr>
      <tr>
          <td>「加了 CSS rule 但又冒出新症狀」</td>
          <td>症狀堆疊、退回問層次</td>
      </tr>
      <tr>
          <td>「emoji / 顏色 / 圖示是唯一區分方式」</td>
          <td>語意層問題、加文本標記</td>
      </tr>
      <tr>
          <td>「需要 aria-label 補語意才能讀懂」</td>
          <td>結構層問題、aria 是補丁、根本要重排</td>
      </tr>
      <tr>
          <td>「同樣的內容、列表 vs 引用區塊閱讀差很多」</td>
          <td>結構層問題、選擇承載結構錯了</td>
      </tr>
      <tr>
          <td>「螢幕閱讀器讀出來的順序跟視覺順序不同」</td>
          <td>視覺順序跟邏輯順序錯位、改 DOM order</td>
      </tr>
      <tr>
          <td>「複製貼上後格式跑掉、語意也跟著跑掉」</td>
          <td>依賴視覺渲染傳語意、把語意寫進文本</td>
      </tr>
  </tbody>
</table>
<p>看到任一訊號、不是「再加一條 CSS / 換個 emoji」、是 <strong>接受視覺工具對這個層次的問題無能為力、改修結構</strong>。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>本卡的 sibling</strong> — #82 是「驗證工具 vs 錯誤層次」、本卡是「呈現工具 vs 內容層次」、同骨不同領域</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td><strong>本卡是 #83 缺的垂直軸</strong> — #83 的 5 輪是 horizontal frame、本卡的 3 層是 vertical layer、兩軸正交</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>用 emoji 區分概念是「便利寫法」、改結構是「對齊意圖」 — 本卡是 #67 在呈現選擇上的具體實例</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td>用 emoji 替代結構區分 = 把語意分散在「文字 + emoji」兩處、違反 SSoT、emoji 不渲染時語意就遺失</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 Native HTML 優先於 ARIA role</a></td>
          <td>同骨：semantic HTML 把語意寫進結構、ARIA 是補丁；emoji 是視覺補丁、文本標記 / 列表是 semantic 結構</td>
      </tr>
      <tr>
          <td><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a></td>
          <td>本卡是 #56 在「呈現層」的擴展 — 視覺驗收訊號早於語意驗收成立、容易誤判修好</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選是隱式 collapse</a></td>
          <td>「emoji 區分」是把多概念 collapse 進視覺維度、跟 yes/no collapse 同骨（多維度被壓成 1 維）</td>
      </tr>
  </tbody>
</table>
<p>本卡是 <a href="../literal-interception-vs-behavioral-refinement/">#82</a> 的 sibling — 兩者都在說「<strong>工具有能擋的層 / 擋不到的層、超出 ceiling 是 false confidence</strong>」。組合理解：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>#82</th>
          <th>本卡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>領域</td>
          <td>驗證 / 防呆</td>
          <td>呈現 / 寫作</td>
      </tr>
      <tr>
          <td>工具</td>
          <td>hook / lint / CI</td>
          <td>CSS / emoji / 顏色 / 排版</td>
      </tr>
      <tr>
          <td>該擋的層</td>
          <td>字面（typo / schema）</td>
          <td>視覺（容器 / 字體 / 顏色）</td>
      </tr>
      <tr>
          <td>抓不到的層</td>
          <td>行為（思考偏差）</td>
          <td>語意 / 邏輯（概念 / 結構）</td>
      </tr>
      <tr>
          <td>False confidence</td>
          <td>「CI 通過 = 沒事」</td>
          <td>「視覺修了 = 沒事」</td>
      </tr>
      <tr>
          <td>規則膨脹</td>
          <td>「再加一條 lint 規則」</td>
          <td>「再加一條 CSS rule」</td>
      </tr>
      <tr>
          <td>正解</td>
          <td>multi-pass review / spiral</td>
          <td>改結構 / 重新分概念</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到本系統的-case">套用到本系統的 case</h2>
<h3 id="case-1blog-文章-mermaid--emoji-圖例">Case 1：blog 文章 mermaid + emoji 圖例</h3>
<p>寫 <a href="../../work-log/git_move_partial_change_to_earlier_commit/">git rebase 搬部分檔案</a> 文章時、用 mermaid gitGraph 配 emoji 圖例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">&gt; </span><span class="ge">🟢 HIGHLIGHT = 接收檔案變更的目標 commit（A）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="ge"></span>&gt; 🔴 REVERSE = 含有不該屬於它的檔案變更的 commit（C）</span></span></code></pre></div><p>某 viewport 下 emoji 跟文字之間斷行錯位、🔴 在前一行、REVERSE = &hellip; 在下一行。</p>
<p><strong>第一直覺（錯）</strong>：修 emoji 渲染、加 white-space: nowrap。</p>
<p><strong>追問層次</strong>：</p>
<ul>
<li>視覺層：emoji 斷行 → 改 CSS 可以擋</li>
<li>語意層：HIGHLIGHT 跟 REVERSE 是兩個獨立概念、被擠在 <code>&gt; 引用區塊</code> 的兩行 + 用 emoji 作為唯一區分 — emoji 不渲染（終端 / 老瀏覽器）就完全失語意</li>
<li>邏輯層：兩個獨立概念本就不該擠在引用區塊裡、引用區塊的語意是「附加說明」、但兩個概念都是主要資訊</li>
</ul>
<p><strong>根因在邏輯層</strong>：兩個概念該分開承載。</p>
<p><strong>正解</strong>：拆成獨立列表項、每項獨立一個概念：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl">**四個 commit 的角色**：
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">-</span> **A**（接收目標）：commit C 中對檔案的修訂應該屬於這裡
</span></span><span class="line"><span class="ln">4</span><span class="cl">- <span class="gs">**C**</span>（變更來源）：同時改了目標檔案和其他 6 個檔案</span></span></code></pre></div><p>修了之後、emoji 斷行、aria-label、複製貼上格式、螢幕閱讀器順序等所有下游症狀同時消失 — 因為根因被處理了。</p>
<h3 id="case-2mermaid-gitgraph-type-顏色設定">Case 2：mermaid gitGraph type 顏色設定</h3>
<p>跟 Case 1 同篇文章的另一個議題：mermaid 的 <code>type: HIGHLIGHT</code> / <code>type: REVERSE</code> 自訂顏色不渲染（<a href="/blog/posts/mermaid_gitgraph_type_color_config/" data-link-title="Mermaid gitGraph：自訂 commit type 顏色不渲染的配置補洞" data-link-desc="Hugo &#43; Mermaid gitGraph 的 type: HIGHLIGHT / REVERSE 顏色不生效時的根因與修復。升級 Mermaid 版本時顏色變數命名會變、要重驗。">mermaid_gitgraph_type_color_config</a>）。</p>
<p>這個 <strong>是</strong> 純視覺問題 — 內容本身沒爭議、只是 mermaid themeVariables 缺配置。修 CSS / themeVariables 是對的。</p>
<p>兩個議題在同一篇文章、但層次不同 — 一個是邏輯層下游症狀（誤判為視覺）、一個是真視覺問題（CSS 修對位）。<strong>判讀層次比修法重要</strong>。</p>
<h3 id="case-3multi-pass-review-的層次盲點">Case 3：multi-pass review 的層次盲點</h3>
<p><a href="../writing-multi-pass-review/">#83</a> 的 5 輪 frame（生成 / 意圖 / 語氣 / grep / 反例）是 horizontal — 同一份文字、5 個視角輪流看。但這 5 輪都可能落在同一個 vertical layer（例如全部在看視覺層）、漏掉語意 / 邏輯層。</p>
<p>本卡補的是垂直軸：<strong>每輪 frame 內、要意識到問題在哪一層</strong>。第 1 輪生成可能寫出語意混淆、第 2 輪對意圖如果只看視覺呈現、整個 review 就停在視覺層。</p>
<p>實際軌跡：blog 文章寫完跑了 #83 的多輪 review、catch 到 emoji 斷行（視覺層）、但沒 catch 到 HIGHLIGHT/REVERSE 概念混淆（語意層）— 因為每輪 frame 都沒把 layer 當成獨立檢查維度。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看到視覺異常、第一直覺是改 CSS</td>
          <td>先問「換個 viewport / 設備會不會復發」、會的話是更深層</td>
      </tr>
      <tr>
          <td>用 emoji / 顏色 / 圖示作為唯一區分</td>
          <td>語意層問題、加文本標記 + 改結構</td>
      </tr>
      <tr>
          <td>加了 CSS 但又冒出新視覺症狀</td>
          <td>症狀堆疊、退回問層次、根因在更深層</td>
      </tr>
      <tr>
          <td>「需要 aria-label 補語意才能讀懂」</td>
          <td>結構層問題、改 DOM order / 重排</td>
      </tr>
      <tr>
          <td>Multi-pass review 跑了、但只 catch 視覺問題</td>
          <td>layer 沒當獨立維度、補垂直軸檢查</td>
      </tr>
      <tr>
          <td>一個改動「視覺好了、但語意感覺怪」</td>
          <td>語意層問題沒解、別停手</td>
      </tr>
      <tr>
          <td>「之後在不同設備 / 螢幕閱讀器再驗證」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 trigger</td>
      </tr>
      <tr>
          <td>Commit 訊息只寫「fix layout / fix emoji」</td>
          <td>訊息層級停在視覺、檢查根因是不是更深</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：視覺工具的 ROI = <strong>跟問題層次對齊</strong> × <strong>不超出 ceiling</strong>。<strong>CSS / emoji / 顏色不會理解語意、所以只能擋呈現</strong>；<strong>語意 / 邏輯問題需要改結構 / 改概念分組、不靠視覺工具</strong>。試圖用視覺工具蓋語意 / 邏輯問題 = 假裝修了、實際比沒修更危險（false confidence 阻止下次警覺）。</p>
]]></content:encoded></item><item><title>Collapse 是隱形預設：多維空間被壓成單格的三類典型</title><link>https://tarrragon.github.io/blog/report/collapse-is-implicit-default/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/collapse-is-implicit-default/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>「Collapse」是同骨 pattern — 高維選擇空間被便利驅動 reduce 到最少格子、且這個 reduction 看似中性、實際藏掉維度。三個 surface 各自的 collapse 典型：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Surface&lt;/th>
 &lt;th>高維原貌&lt;/th>
 &lt;th>Collapse 後&lt;/th>
 &lt;th>驅動力&lt;/th>
 &lt;th>對應卡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Decision surface&lt;/td>
 &lt;td>改 / 延後 / 疊加 / 分批 / 反問（多選空間）&lt;/td>
 &lt;td>Yes / No 二選&lt;/td>
 &lt;td>「最少字、最簡潔」&lt;/td>
 &lt;td>&lt;a href="../yes-no-binary-collapse/">#80&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dialogue surface&lt;/td>
 &lt;td>呈現格式 × 策略疊加 × 批次邊界 × 時間軸 × 選項類型&lt;/td>
 &lt;td>開放問 + 單策略 + 一次完成 + 立刻決 + 單選&lt;/td>
 &lt;td>「最容易寫的問句」&lt;/td>
 &lt;td>&lt;a href="../decision-dialogue-dimensions/">#79&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Output surface&lt;/td>
 &lt;td>N 種 framing × 多種 cadence × 多軸敘事視角&lt;/td>
 &lt;td>單一 framing 複製 N 篇&lt;/td>
 &lt;td>「合規最佳解」&lt;/td>
 &lt;td>&lt;a href="../compliance-optimum-converges-cadence/">#123&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者共通結構：&lt;/p>
&lt;ol>
&lt;li>真實選擇空間是 &lt;em>多維 / 多選&lt;/em>&lt;/li>
&lt;li>預設行為把它 &lt;em>reduce 到 1-2 維 / 1 選&lt;/em>&lt;/li>
&lt;li>這個 reduction 看起來「合理 / 簡潔 / 合規」、不被覺察是 collapse&lt;/li>
&lt;li>後果是 &lt;em>使用者 / 讀者被塞進最窄格子、要破格才能表達或回應&lt;/em>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="為什麼-collapse-是-default不是-violation">為什麼 Collapse 是 default、不是 violation&lt;/h2>
&lt;p>跟其他「明確違規」不同、collapse 預設 &lt;em>合規&lt;/em> — 沒有規則禁止 yes/no 問句、沒有規則禁止單一 framing、沒有規則禁止單一策略推薦。這是 collapse 最危險的特性：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>違規類型&lt;/th>
 &lt;th>偵測機制&lt;/th>
 &lt;th>Collapse 為什麼避開&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>字面違規&lt;/td>
 &lt;td>hook / lint&lt;/td>
 &lt;td>Collapse 沒有字面 pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構違規&lt;/td>
 &lt;td>schema / linter&lt;/td>
 &lt;td>Collapse 結構通常正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>行為違規&lt;/td>
 &lt;td>review&lt;/td>
 &lt;td>Collapse 看起來像「簡潔」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Collapse&lt;/td>
 &lt;td>跨對話 / 跨批比對才浮現&lt;/td>
 &lt;td>單樣本看不出、要對照「完整高維」才知道缺維度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Collapse 是隱形預設、原因在 &lt;em>對比標的不存在於眼前&lt;/em>。Yes/No 問句要 collapse 到 1 bit、需要使用者已經想過五維 collapse；五維 collapse 要看出、需要使用者已經理解 #79 五維框架；framing collapse 要看出、需要連讀多篇且預期有變體。沒有 &lt;em>對照原型&lt;/em> 在眼前、collapse 看起來就是「正常」。&lt;/p>
&lt;hr>
&lt;h2 id="collapse-不是該消除是該變顯性">Collapse 不是「該消除」、是「該變顯性」&lt;/h2>
&lt;p>對策不是去除 collapse — 多數情境下使用者 / 讀者確實受益於 reduction（不用每次都展開五維、不用每篇 cadence 都換）。對策是 &lt;em>讓 collapse 變顯性&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Collapse 隱性版&lt;/th>
 &lt;th>Collapse 顯性版&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Decision&lt;/td>
 &lt;td>「OK 嗎？」&lt;/td>
 &lt;td>「我推薦 A、但 B / C 可選；想改方向、延後、或疊加？」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dialogue&lt;/td>
 &lt;td>「你想怎麼做？」&lt;/td>
 &lt;td>「呈現 / 策略數 / 批次 / 時間 / 選項類型」五維各給預設 + 可改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Output&lt;/td>
 &lt;td>全篇用同一 framing&lt;/td>
 &lt;td>Pilot phase 準備 3-5 個 framing 變體、輪替使用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>顯性化的代價是 &lt;em>寫的人多打字 / 多設計&lt;/em>、得益是 &lt;em>接收方知道自由度在哪、可以選擇接受預設或破格&lt;/em>。預設展開、選窄格要證明 — 跟 #78「不互斥是預設」同條結構。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>「Collapse」是同骨 pattern — 高維選擇空間被便利驅動 reduce 到最少格子、且這個 reduction 看似中性、實際藏掉維度。三個 surface 各自的 collapse 典型：</p>
<table>
  <thead>
      <tr>
          <th>Surface</th>
          <th>高維原貌</th>
          <th>Collapse 後</th>
          <th>驅動力</th>
          <th>對應卡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decision surface</td>
          <td>改 / 延後 / 疊加 / 分批 / 反問（多選空間）</td>
          <td>Yes / No 二選</td>
          <td>「最少字、最簡潔」</td>
          <td><a href="../yes-no-binary-collapse/">#80</a></td>
      </tr>
      <tr>
          <td>Dialogue surface</td>
          <td>呈現格式 × 策略疊加 × 批次邊界 × 時間軸 × 選項類型</td>
          <td>開放問 + 單策略 + 一次完成 + 立刻決 + 單選</td>
          <td>「最容易寫的問句」</td>
          <td><a href="../decision-dialogue-dimensions/">#79</a></td>
      </tr>
      <tr>
          <td>Output surface</td>
          <td>N 種 framing × 多種 cadence × 多軸敘事視角</td>
          <td>單一 framing 複製 N 篇</td>
          <td>「合規最佳解」</td>
          <td><a href="../compliance-optimum-converges-cadence/">#123</a></td>
      </tr>
  </tbody>
</table>
<p>三者共通結構：</p>
<ol>
<li>真實選擇空間是 <em>多維 / 多選</em></li>
<li>預設行為把它 <em>reduce 到 1-2 維 / 1 選</em></li>
<li>這個 reduction 看起來「合理 / 簡潔 / 合規」、不被覺察是 collapse</li>
<li>後果是 <em>使用者 / 讀者被塞進最窄格子、要破格才能表達或回應</em></li>
</ol>
<hr>
<h2 id="為什麼-collapse-是-default不是-violation">為什麼 Collapse 是 default、不是 violation</h2>
<p>跟其他「明確違規」不同、collapse 預設 <em>合規</em> — 沒有規則禁止 yes/no 問句、沒有規則禁止單一 framing、沒有規則禁止單一策略推薦。這是 collapse 最危險的特性：</p>
<table>
  <thead>
      <tr>
          <th>違規類型</th>
          <th>偵測機制</th>
          <th>Collapse 為什麼避開</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字面違規</td>
          <td>hook / lint</td>
          <td>Collapse 沒有字面 pattern</td>
      </tr>
      <tr>
          <td>結構違規</td>
          <td>schema / linter</td>
          <td>Collapse 結構通常正確</td>
      </tr>
      <tr>
          <td>行為違規</td>
          <td>review</td>
          <td>Collapse 看起來像「簡潔」</td>
      </tr>
      <tr>
          <td>Collapse</td>
          <td>跨對話 / 跨批比對才浮現</td>
          <td>單樣本看不出、要對照「完整高維」才知道缺維度</td>
      </tr>
  </tbody>
</table>
<p>Collapse 是隱形預設、原因在 <em>對比標的不存在於眼前</em>。Yes/No 問句要 collapse 到 1 bit、需要使用者已經想過五維 collapse；五維 collapse 要看出、需要使用者已經理解 #79 五維框架；framing collapse 要看出、需要連讀多篇且預期有變體。沒有 <em>對照原型</em> 在眼前、collapse 看起來就是「正常」。</p>
<hr>
<h2 id="collapse-不是該消除是該變顯性">Collapse 不是「該消除」、是「該變顯性」</h2>
<p>對策不是去除 collapse — 多數情境下使用者 / 讀者確實受益於 reduction（不用每次都展開五維、不用每篇 cadence 都換）。對策是 <em>讓 collapse 變顯性</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Collapse 隱性版</th>
          <th>Collapse 顯性版</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decision</td>
          <td>「OK 嗎？」</td>
          <td>「我推薦 A、但 B / C 可選；想改方向、延後、或疊加？」</td>
      </tr>
      <tr>
          <td>Dialogue</td>
          <td>「你想怎麼做？」</td>
          <td>「呈現 / 策略數 / 批次 / 時間 / 選項類型」五維各給預設 + 可改</td>
      </tr>
      <tr>
          <td>Output</td>
          <td>全篇用同一 framing</td>
          <td>Pilot phase 準備 3-5 個 framing 變體、輪替使用</td>
      </tr>
  </tbody>
</table>
<p>顯性化的代價是 <em>寫的人多打字 / 多設計</em>、得益是 <em>接收方知道自由度在哪、可以選擇接受預設或破格</em>。預設展開、選窄格要證明 — 跟 #78「不互斥是預設」同條結構。</p>
<hr>
<h2 id="跨-surface-的判讀通則">跨 surface 的判讀通則</h2>
<p>判斷某個情境是不是 collapse、不是看「有沒有違規」、是問三個 diagnostic：</p>
<ol>
<li><strong>真實選擇空間是幾維 / 幾選？</strong> — 如果 ≥ 3、reduce 到 1-2 就是 collapse</li>
<li><strong>這個 reduction 是設計選擇還是預設?</strong> — 設計選擇會有「為什麼選窄格」的論述、預設沒有</li>
<li><strong>接收方破格的成本是多少?</strong> — 破格要破壞既有對話 / review / commit 結構就是高成本、表示 collapse 藏得深</li>
</ol>
<p>三個 diagnostic 全 yes、就是隱形 collapse。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「簡潔」當作目的、不評估 collapse 副作用</td>
          <td>把多維壓 1 bit、自以為對使用者好、實際藏掉維度</td>
      </tr>
      <tr>
          <td>看不到的維度視為不存在</td>
          <td>Decision space 真的有 N 維、不展開不代表只有 1 維</td>
      </tr>
      <tr>
          <td>加更多 constraint 想解品質問題</td>
          <td>越多 constraint、output space collapse 越快、品質反而下降</td>
      </tr>
      <tr>
          <td>用 hook / lint 想擋 collapse</td>
          <td>Collapse 字面合規、hook 抓不到</td>
      </tr>
      <tr>
          <td>「預設好就好」做設計選擇</td>
          <td>沒評估高自由度的成本 / 效益、所有預設都選窄格</td>
      </tr>
      <tr>
          <td>第一版定下來的 framing / 預設、之後不評估</td>
          <td>第一版幾乎都是窄格、需要 iterate</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>子卡 — Dialogue surface 的 collapse；本卡上一層、把 #79 / #80 / #123 統一為跨 surface 同骨</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選是隱式 collapse</a></td>
          <td>子卡 — Decision surface 的極致 collapse；本卡是 #80 的 meta、列出其他 surface 上的同骨 case</td>
      </tr>
      <tr>
          <td><a href="../compliance-optimum-converges-cadence/">#123 多重硬規範同時生效會把 cadence 推向便利解</a></td>
          <td>子卡 — Output surface 的 collapse；補上 batch writing 這個 surface 跟 decision / dialogue 並列</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>Driver 卡 — 三類 collapse 的共同 driver 都是「便利」、便利驅動 collapse 是 #67 的具體 manifestation</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>補充偵測手段 — Collapse 屬 emergence 類、hook 抓不到、要 multi-pass review；#82 的 ceiling 在 collapse 上特別明顯</td>
      </tr>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>Specific case — 給推薦不給選項是 decision surface 的 collapse 形式之一</td>
      </tr>
      <tr>
          <td><a href="../content-structure-by-max-diff-dimension/">#127 Process content 結構由最大差異維度決定</a></td>
          <td>子實例 — Content structure surface 的 collapse；把 universal phased / 6-section 模板套到 5 種不同 type 是本卡在「結構 layer」的具體形態</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>接收方反覆「破格」回應（用結構外的方式回答）</td>
          <td>你 collapse 太狠、展開維度</td>
      </tr>
      <tr>
          <td>預設選項只有 1-2 個</td>
          <td>評估真實選擇空間、看是否藏掉維度</td>
      </tr>
      <tr>
          <td>「簡潔」「乾淨」是設計理由</td>
          <td>警訊 — 簡潔 / 乾淨可能是 collapse 的別名</td>
      </tr>
      <tr>
          <td>加新 constraint 後品質下降</td>
          <td>Constraint collapse 了 output space、考慮拉開或加 anti-template</td>
      </tr>
      <tr>
          <td>想用 yes/no 結束對話</td>
          <td>Decision collapse、改 multi-option</td>
      </tr>
      <tr>
          <td>批量輸出全篇同 framing</td>
          <td>Output collapse、補 framing 變體</td>
      </tr>
      <tr>
          <td>「為什麼大家都這樣寫 / 都這樣回」</td>
          <td>系統性 collapse、不是個別事件、查 driver 跟 constraint</td>
      </tr>
      <tr>
          <td>設計新規範 / 新 default 時</td>
          <td>評估 collapse 副作用、不是只看「能不能用」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Collapse 是高維空間預設被 reduce 到 1-2 維、看似中性、實際藏掉維度。三個 surface（decision / dialogue / output）有同骨 collapse pattern、都被「便利 / 合規 / 簡潔」驅動、都需要顯性化。對策不是消除 collapse、是讓設計者主動選擇要 collapse 哪一維、預設展開、選窄格要證明。</p>
]]></content:encoded></item><item><title>寫作 review 是多軸完整性、不是單軸深度</title><link>https://tarrragon.github.io/blog/report/writing-review-multi-axis-completeness/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/writing-review-multi-axis-completeness/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫作 review 完整性的本質是 &lt;em>多軸交集&lt;/em>、不是 &lt;em>單軸深度&lt;/em>。七個軸已經從前面卡片浮現、缺任一軸就會 systematic miss 對應類型的問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>軸&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;th>缺失時的盲點&lt;/th>
 &lt;th>對應卡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Frame 軸&lt;/strong>&lt;/td>
 &lt;td>一個 reviewer 跑 N 輪不同 frame（生成 / 意圖 / 機會成本 / grep / 反例）&lt;/td>
 &lt;td>結構 OK 但意圖 / 機會成本錯&lt;/td>
 &lt;td>&lt;a href="../writing-multi-pass-review/">#83&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Instance 軸&lt;/strong>&lt;/td>
 &lt;td>N 個 reviewer 各自獨立、不同維度&lt;/td>
 &lt;td>單 reviewer 處理多維度互相干擾、context 污染&lt;/td>
 &lt;td>&lt;a href="../agent-team-context-isolation/">#121&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Surface 軸&lt;/strong>&lt;/td>
 &lt;td>Body / title / description / heading / link label / MOC hook&lt;/td>
 &lt;td>Body 完美但 metadata 失準、搜尋入口失效&lt;/td>
 &lt;td>&lt;a href="../metadata-surface-in-writing-review/">#97&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Scope 軸&lt;/strong>&lt;/td>
 &lt;td>同類風險區（不是改動區）&lt;/td>
 &lt;td>抓不到 corpus 內既有同類違規&lt;/td>
 &lt;td>&lt;a href="../multi-pass-scope-must-cover-risk-zone/">#95&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Cadence 軸&lt;/strong>&lt;/td>
 &lt;td>跨檔 framing 一致性 / 句型骨架 / 收尾語&lt;/td>
 &lt;td>單篇合規、連讀預期化&lt;/td>
 &lt;td>&lt;a href="../cadence-homogenization-in-batch-writing/">#122&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Timing 軸&lt;/strong>&lt;/td>
 &lt;td>寫作中抽樣 vs batch 後 review&lt;/td>
 &lt;td>違規累積到 batch 末才發現、修正成本 N 倍&lt;/td>
 &lt;td>&lt;a href="../emergence-violations-need-in-stream-sampling/">#124&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Granularity 軸&lt;/strong>&lt;/td>
 &lt;td>規則 frame vs 字句層信號&lt;/td>
 &lt;td>規則 catch 結構違規、字句層（口語修辭 / 廢話前綴）漏抓&lt;/td>
 &lt;td>&lt;a href="../multi-pass-review-frame-granularity-blindspot/">#114&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>七軸正交：每個軸獨立解一類盲點、不重疊；缺任一軸都會 systematic miss 對應類型問題。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼是多軸不是單軸越做越深">為什麼是多軸、不是單軸越做越深&lt;/h2>
&lt;p>單軸越做越深的失敗模式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Frame 軸跑 10 輪、不換 instance 軸&lt;/strong>：同一 reviewer 跑 10 輪、catch 的問題仍高度相關（#114 已點出）&lt;/li>
&lt;li>&lt;strong>Instance 軸開 10 個 reviewer、不換 frame 軸&lt;/strong>：10 個 reviewer 都跑「規則 check」這個 frame、catch 的盲點相同&lt;/li>
&lt;li>&lt;strong>Frame + Instance 都做、不管 Surface 軸&lt;/strong>：Body review 通過、但 title / description 沒被審、搜尋入口失效&lt;/li>
&lt;li>&lt;strong>Surface 都做、不管 Cadence 軸&lt;/strong>：51 篇個別合規、連讀預期化&lt;/li>
&lt;li>&lt;strong>Cadence 軸有抽樣、Timing 軸放在 batch 後&lt;/strong>：抽樣等於 batch 後 review、修正成本 N 倍&lt;/li>
&lt;/ol>
&lt;p>七軸缺任一條、就有對應類型違規逃過 review。&lt;/p>
&lt;hr>
&lt;h2 id="多軸是預設單軸是-collapse">多軸是預設、單軸是 collapse&lt;/h2>
&lt;p>跟 &lt;a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設&lt;/a> 同骨 — 把 review 設計 collapse 到單軸是預設行為（最便利）、但 collapse 掉的軸對應的違規會 systematic miss。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計時的便利選擇&lt;/th>
 &lt;th>對應 collapse 軸&lt;/th>
 &lt;th>系統性盲點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「找一個 reviewer 跑就好」&lt;/td>
 &lt;td>Instance 軸 collapse&lt;/td>
 &lt;td>維度盲點、context 污染&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「跑一輪就好」&lt;/td>
 &lt;td>Frame 軸 collapse&lt;/td>
 &lt;td>一個 frame 只 catch 一類問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「body review 就夠」&lt;/td>
 &lt;td>Surface 軸 collapse&lt;/td>
 &lt;td>Metadata 失準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「只 review 改動部分」&lt;/td>
 &lt;td>Scope 軸 collapse&lt;/td>
 &lt;td>既有 corpus 同類違規無解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「單篇 review」&lt;/td>
 &lt;td>Cadence 軸 collapse&lt;/td>
 &lt;td>Emergence 違規漏抓&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「等寫完再 review」&lt;/td>
 &lt;td>Timing 軸 collapse&lt;/td>
 &lt;td>Emergence 累積、修正成本 N 倍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「跑 lint + review 就完整」&lt;/td>
 &lt;td>Granularity 軸 collapse&lt;/td>
 &lt;td>字句層信號漏抓&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>預設展開七軸、選窄做要證明 — 跟 #78 / #79 / #80 / #125 同條結構。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫作 review 完整性的本質是 <em>多軸交集</em>、不是 <em>單軸深度</em>。七個軸已經從前面卡片浮現、缺任一軸就會 systematic miss 對應類型的問題：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>內容</th>
          <th>缺失時的盲點</th>
          <th>對應卡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Frame 軸</strong></td>
          <td>一個 reviewer 跑 N 輪不同 frame（生成 / 意圖 / 機會成本 / grep / 反例）</td>
          <td>結構 OK 但意圖 / 機會成本錯</td>
          <td><a href="../writing-multi-pass-review/">#83</a></td>
      </tr>
      <tr>
          <td><strong>Instance 軸</strong></td>
          <td>N 個 reviewer 各自獨立、不同維度</td>
          <td>單 reviewer 處理多維度互相干擾、context 污染</td>
          <td><a href="../agent-team-context-isolation/">#121</a></td>
      </tr>
      <tr>
          <td><strong>Surface 軸</strong></td>
          <td>Body / title / description / heading / link label / MOC hook</td>
          <td>Body 完美但 metadata 失準、搜尋入口失效</td>
          <td><a href="../metadata-surface-in-writing-review/">#97</a></td>
      </tr>
      <tr>
          <td><strong>Scope 軸</strong></td>
          <td>同類風險區（不是改動區）</td>
          <td>抓不到 corpus 內既有同類違規</td>
          <td><a href="../multi-pass-scope-must-cover-risk-zone/">#95</a></td>
      </tr>
      <tr>
          <td><strong>Cadence 軸</strong></td>
          <td>跨檔 framing 一致性 / 句型骨架 / 收尾語</td>
          <td>單篇合規、連讀預期化</td>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122</a></td>
      </tr>
      <tr>
          <td><strong>Timing 軸</strong></td>
          <td>寫作中抽樣 vs batch 後 review</td>
          <td>違規累積到 batch 末才發現、修正成本 N 倍</td>
          <td><a href="../emergence-violations-need-in-stream-sampling/">#124</a></td>
      </tr>
      <tr>
          <td><strong>Granularity 軸</strong></td>
          <td>規則 frame vs 字句層信號</td>
          <td>規則 catch 結構違規、字句層（口語修辭 / 廢話前綴）漏抓</td>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114</a></td>
      </tr>
  </tbody>
</table>
<p>七軸正交：每個軸獨立解一類盲點、不重疊；缺任一軸都會 systematic miss 對應類型問題。</p>
<hr>
<h2 id="為什麼是多軸不是單軸越做越深">為什麼是多軸、不是單軸越做越深</h2>
<p>單軸越做越深的失敗模式：</p>
<ol>
<li><strong>Frame 軸跑 10 輪、不換 instance 軸</strong>：同一 reviewer 跑 10 輪、catch 的問題仍高度相關（#114 已點出）</li>
<li><strong>Instance 軸開 10 個 reviewer、不換 frame 軸</strong>：10 個 reviewer 都跑「規則 check」這個 frame、catch 的盲點相同</li>
<li><strong>Frame + Instance 都做、不管 Surface 軸</strong>：Body review 通過、但 title / description 沒被審、搜尋入口失效</li>
<li><strong>Surface 都做、不管 Cadence 軸</strong>：51 篇個別合規、連讀預期化</li>
<li><strong>Cadence 軸有抽樣、Timing 軸放在 batch 後</strong>：抽樣等於 batch 後 review、修正成本 N 倍</li>
</ol>
<p>七軸缺任一條、就有對應類型違規逃過 review。</p>
<hr>
<h2 id="多軸是預設單軸是-collapse">多軸是預設、單軸是 collapse</h2>
<p>跟 <a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a> 同骨 — 把 review 設計 collapse 到單軸是預設行為（最便利）、但 collapse 掉的軸對應的違規會 systematic miss。</p>
<table>
  <thead>
      <tr>
          <th>設計時的便利選擇</th>
          <th>對應 collapse 軸</th>
          <th>系統性盲點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「找一個 reviewer 跑就好」</td>
          <td>Instance 軸 collapse</td>
          <td>維度盲點、context 污染</td>
      </tr>
      <tr>
          <td>「跑一輪就好」</td>
          <td>Frame 軸 collapse</td>
          <td>一個 frame 只 catch 一類問題</td>
      </tr>
      <tr>
          <td>「body review 就夠」</td>
          <td>Surface 軸 collapse</td>
          <td>Metadata 失準</td>
      </tr>
      <tr>
          <td>「只 review 改動部分」</td>
          <td>Scope 軸 collapse</td>
          <td>既有 corpus 同類違規無解</td>
      </tr>
      <tr>
          <td>「單篇 review」</td>
          <td>Cadence 軸 collapse</td>
          <td>Emergence 違規漏抓</td>
      </tr>
      <tr>
          <td>「等寫完再 review」</td>
          <td>Timing 軸 collapse</td>
          <td>Emergence 累積、修正成本 N 倍</td>
      </tr>
      <tr>
          <td>「跑 lint + review 就完整」</td>
          <td>Granularity 軸 collapse</td>
          <td>字句層信號漏抓</td>
      </tr>
  </tbody>
</table>
<p>預設展開七軸、選窄做要證明 — 跟 #78 / #79 / #80 / #125 同條結構。</p>
<hr>
<h2 id="review-設計時的-enumerate-紀律">Review 設計時的 enumerate 紀律</h2>
<p>設計新的 review 流程（人類 / agent / 自動化）時、不該只看「捕獲哪些違規」、要列七軸覆蓋狀況：</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>預設問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Frame</td>
          <td>這個 review 跑幾種 frame？哪一種 frame 是預設、哪些被跳過？</td>
      </tr>
      <tr>
          <td>Instance</td>
          <td>Reviewer 是 1 個還是 N 個？維度怎麼分？</td>
      </tr>
      <tr>
          <td>Surface</td>
          <td>Body / metadata / link label / heading 都覆蓋了嗎？</td>
      </tr>
      <tr>
          <td>Scope</td>
          <td>Review 的 scope 是「改動區」還是「同類風險區」？</td>
      </tr>
      <tr>
          <td>Cadence</td>
          <td>跨檔 cadence 有沒有抽樣比對？</td>
      </tr>
      <tr>
          <td>Timing</td>
          <td>是寫作中 checkpoint、還是 batch 後 review？</td>
      </tr>
      <tr>
          <td>Granularity</td>
          <td>規則 frame 跟字句 frame 都跑了嗎？</td>
      </tr>
  </tbody>
</table>
<p>七題都回答後、再判斷該不該補軸。如果某軸沒覆蓋、不一定要補（cost vs risk）、但要 <em>知道沒覆蓋對應什麼盲點</em>。</p>
<h3 id="cadence--timing-軸-dogfood-2026-05-18">Cadence + Timing 軸 dogfood (2026-05-18)</h3>
<p>4 篇 deep article batch 驗證 cadence + timing 兩軸的設計、不靠 reviewer 補、是靠 stage 2 寫作流程內抽樣：</p>
<ul>
<li><strong>Cadence 軸</strong>：4 篇 pilot phase 主動規劃 4 種 framing variant、跨檔 cadence audit 顯示「任一缺失」collapse 族 0/4、entry framing 種類 4 種</li>
<li><strong>Timing 軸</strong>：每篇寫作前做 cadence check（生成中 checkpoint）、不等 batch 完成後 reviewer；修正成本 ~5 分鐘 / 篇 vs 前批 batch 後 polish ~30-60 分鐘</li>
</ul>
<p>N=5 full-threshold 補強驗證（同日第二批）：再跑 5 篇 PostgreSQL sub-tool deep article、用 5 種 variant 覆蓋 <em>同 vendor 同 audience</em> 的 cadence collapse 最高風險場景；結果 5/5 framing 全錯開、過渡詞密度 0、cadence collapse 0/5。確認 Cadence 軸 + Timing 軸 <em>不靠 sample size、靠 stage 0 variant 規劃</em>。</p>
<p>詳細數據見 <a href="../cadence-homogenization-in-batch-writing/#dogfood-evidence-2026-05-18-n4-sub-threshold-%e9%a9%97%e8%ad%89">#122 cadence dogfood evidence</a> 跟 <a href="../emergence-violations-need-in-stream-sampling/#dogfood-evidence-2026-05-18-n4-sub-threshold-%e9%a9%97%e8%ad%89">#124 dogfood</a> — 兩軸都不必加 reviewer instance、是 Stage 2 寫作流程設計即可解。</p>
<hr>
<h2 id="七軸不是隨機湊出來有結構">七軸不是隨機湊出來、有結構</h2>
<p>七軸可以再 group 成三個 <em>上位 axis</em>：</p>
<table>
  <thead>
      <tr>
          <th>上位 axis</th>
          <th>涵蓋</th>
          <th>解什麼問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>誰來 review</strong></td>
          <td>Instance 軸</td>
          <td>維度盲點、context 污染</td>
      </tr>
      <tr>
          <td><strong>怎麼 review</strong></td>
          <td>Frame + Granularity 軸</td>
          <td>視角單一、catch 範圍狹窄</td>
      </tr>
      <tr>
          <td><strong>review 什麼</strong></td>
          <td>Surface + Scope + Cadence 軸</td>
          <td>範圍不全、跨檔 / metadata 漏抓</td>
      </tr>
      <tr>
          <td><strong>何時 review</strong></td>
          <td>Timing 軸</td>
          <td>太晚 catch、修正成本爆</td>
      </tr>
  </tbody>
</table>
<p>四上位 axis 各自獨立、合起來覆蓋 review 設計的所有 surface。當 review 出問題、依四上位 axis 找根因比依七子軸快。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「跑 mdtools lint 就完整」</td>
          <td>只覆蓋字面 frame、結構 / 行為 / cadence 全漏</td>
      </tr>
      <tr>
          <td>「Reviewer agent 跑一遍就完整」</td>
          <td>Instance 軸覆蓋了、但 frame / surface / scope / cadence 可能漏</td>
      </tr>
      <tr>
          <td>「Review 改動的檔就好」</td>
          <td>Scope 軸 collapse、既有 corpus 同類違規無解</td>
      </tr>
      <tr>
          <td>「Body review 完就 ship」</td>
          <td>Surface 軸 collapse、metadata 失準</td>
      </tr>
      <tr>
          <td>「Batch 完成後跑 reviewer」</td>
          <td>Timing 軸 collapse、emergence 違規修正成本 N 倍</td>
      </tr>
      <tr>
          <td>「Review 越多輪越完整」</td>
          <td>同 reviewer 同 frame 跑 10 輪仍 catch 同類問題、缺軸不缺深度</td>
      </tr>
      <tr>
          <td>設計 review 流程不 enumerate 七軸</td>
          <td>預設只覆蓋 1-2 軸、其他軸盲點變 systematic</td>
      </tr>
      <tr>
          <td>把 review 當成「validation gate」、不是「多軸完整性」</td>
          <td>心智模型錯位、把多軸問題誤解為單點 pass/fail</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>子軸（Frame）— #83 是 review 的 frame 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../agent-team-context-isolation/">#121 Agent team context 隔離</a></td>
          <td>子軸（Instance）— #121 是 review 的 instance 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 納入寫作 review 範圍</a></td>
          <td>子軸（Surface）— #97 是 review 的 surface 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區</a></td>
          <td>子軸（Scope）— #95 是 review 的 scope 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a></td>
          <td>子軸（Cadence）— #122 是 review 的 cadence 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../emergence-violations-need-in-stream-sampling/">#124 Emergence 違規要 stage 內抽樣</a></td>
          <td>子軸（Timing）— #124 是 review 的 timing 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review frame 顆粒度盲點</a></td>
          <td>子軸（Granularity）— #114 是 review 的 granularity 軸 anchor</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>Sibling meta-卡 — #79 是 decision 多軸 anchor、本卡是 review 多軸 anchor、兩者結構同骨</td>
      </tr>
      <tr>
          <td><a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a></td>
          <td>上位 driver — 把 review collapse 到單軸是 #125 在 review surface 的具體 instance</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>互補 — #82 是錯誤類型 × 工具粒度、本卡是 review 多軸；兩者交集點 = granularity 軸 + timing 軸的設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計新 review 流程沒 enumerate 七軸</td>
          <td>預設只 1-2 軸覆蓋、補軸對照</td>
      </tr>
      <tr>
          <td>Review 跑完還是有 systematic 違規漏抓</td>
          <td>查七軸缺哪條、不是加深 review</td>
      </tr>
      <tr>
          <td>同類問題在不同批次反覆出現</td>
          <td>Scope 軸 collapse、Review scope 應蓋同類風險區、不是改動區</td>
      </tr>
      <tr>
          <td>Reviewer 報告都是結構違規、沒字句層</td>
          <td>Granularity 軸 collapse、補字句 frame</td>
      </tr>
      <tr>
          <td>Batch 完成後 reviewer 抓大量 emergence 違規</td>
          <td>Timing 軸 collapse、補 stage 內 checkpoint</td>
      </tr>
      <tr>
          <td>Body lint 全綠但讀者搜不到 / 看不懂入口</td>
          <td>Surface 軸 collapse、補 metadata review</td>
      </tr>
      <tr>
          <td>1 個 reviewer 跑 10 輪、catch 範圍仍狹窄</td>
          <td>Instance 軸 collapse、補不同 reviewer instance</td>
      </tr>
      <tr>
          <td>「我們 review 已經很完整」但常被 user 點漏抓問題</td>
          <td>自我評估只看單軸、需要對照七軸 enumeration</td>
      </tr>
      <tr>
          <td>想加 review 第 11 輪</td>
          <td>警訊 — 多半是缺軸不缺深度、查七軸覆蓋而不是加輪</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：寫作 review 完整性是七軸交集、不是單軸深度；缺軸不缺深度。設計 review 流程時 enumerate 七軸覆蓋狀況、預設展開、選窄要證明；當 review 報告漏抓 systematic 違規、查的不是「再加一輪」、是「哪一軸沒覆蓋」。</p>
]]></content:encoded></item><item><title>Process content 結構由最大差異維度決定、不是 universal phased</title><link>https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>跨 X process content（migration / upgrade / rollout / 演練 / playbook）的結構不是 universal、由 source 跟 target 之間的 &lt;em>差異維度組合&lt;/em> 決定。固定套「6-phase playbook」「6-section deep article」會在 &lt;em>結構錯位&lt;/em> 的場景失效。&lt;/p>
&lt;p>實證：6 種 migration / process type 產出 6 種不同結構：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Migration / process type&lt;/th>
 &lt;th>主導差異維度&lt;/th>
 &lt;th>結構&lt;/th>
 &lt;th>結構元素數&lt;/th>
 &lt;th>週期&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高 schema 差&lt;/td>
 &lt;td>Schema / API&lt;/td>
 &lt;td>6-phase rule translation&lt;/td>
 &lt;td>11-12&lt;/td>
 &lt;td>4-9 個月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drop-in compatible&lt;/td>
 &lt;td>無顯著差異&lt;/td>
 &lt;td>6-section + audit prefix&lt;/td>
 &lt;td>7-8&lt;/td>
 &lt;td>1-4 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational redesign&lt;/td>
 &lt;td>Operational model&lt;/td>
 &lt;td>Hybrid (4-phase 含 audit + drop-in cutover)&lt;/td>
 &lt;td>11-12&lt;/td>
 &lt;td>6-12 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-tool 拆分&lt;/td>
 &lt;td>一站式 → 多 component&lt;/td>
 &lt;td>Parallel migration streams&lt;/td>
 &lt;td>10-11&lt;/td>
 &lt;td>2-4 個月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm shift&lt;/td>
 &lt;td>Abstraction model&lt;/td>
 &lt;td>Partial + 混合架構&lt;/td>
 &lt;td>10-11&lt;/td>
 &lt;td>不收斂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Topology re-layout&lt;/td>
 &lt;td>Data topology&lt;/td>
 &lt;td>機制 + execution flow（同 cluster 內重劃）&lt;/td>
 &lt;td>7-9&lt;/td>
 &lt;td>1 天-2 週&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>6 種結構是 &lt;em>常見 type&lt;/em>、不是窮盡分類；source / target 配對可能同時屬多 type（多軸 High）、或不屬任一 type（6 維皆 Medium）— 處理規則見「多重歸類跟 tie-breaking」段。本卡前身是「最大差異維度決定結構」+ 5 維 audit、Redis re-sharding dogfood 揭露 &lt;em>data topology&lt;/em> 是漏掉的第 6 維、Type F 是對應的第 6 type；本卡擴張為 6 維 audit + 6 type。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-universal-phased-模板會失效">為什麼 universal phased 模板會失效&lt;/h2>
&lt;p>寫第一篇 migration playbook 時自然會想：「6 phase 是 migration 的標準結構吧」 — 套到 drop-in compatible migration 後發現 80% phase 不需要、文章變成「為了 phase 而 phase」；套到 paradigm shift 後發現 phased 假設 &lt;em>線性收斂&lt;/em>、實際是 &lt;em>永遠混合架構&lt;/em>、phased 模板強迫一個 &lt;em>不存在&lt;/em> 的「cleanup phase」。&lt;/p>
&lt;p>Universal phased 失效的三個機制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Schema 差不顯著時、phased 多數 phase 變空白&lt;/strong>：drop-in compatible（如 Redis → DragonflyDB）的「Schema translation phase」內容空、強寫變廢話&lt;/li>
&lt;li>&lt;strong>Operational 差是主軸時、phased 把 operational redesign 壓進「phase 1」變太薄&lt;/strong>：PostgreSQL → Aurora 的 &lt;em>operational model 重設計&lt;/em> 是核心、不該壓在一個 phase&lt;/li>
&lt;li>&lt;strong>Paradigm 差時、phased 假設 source 完全消失&lt;/strong>：Kafka ↔ NATS 是 &lt;em>永遠共存&lt;/em>、phased cleanup phase 假設不存在&lt;/li>
&lt;/ol>
&lt;p>→ &lt;strong>結構必須跟差異維度對位、不能反向假設&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>跨 X process content（migration / upgrade / rollout / 演練 / playbook）的結構不是 universal、由 source 跟 target 之間的 <em>差異維度組合</em> 決定。固定套「6-phase playbook」「6-section deep article」會在 <em>結構錯位</em> 的場景失效。</p>
<p>實證：6 種 migration / process type 產出 6 種不同結構：</p>
<table>
  <thead>
      <tr>
          <th>Migration / process type</th>
          <th>主導差異維度</th>
          <th>結構</th>
          <th>結構元素數</th>
          <th>週期</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高 schema 差</td>
          <td>Schema / API</td>
          <td>6-phase rule translation</td>
          <td>11-12</td>
          <td>4-9 個月</td>
      </tr>
      <tr>
          <td>Drop-in compatible</td>
          <td>無顯著差異</td>
          <td>6-section + audit prefix</td>
          <td>7-8</td>
          <td>1-4 週</td>
      </tr>
      <tr>
          <td>Operational redesign</td>
          <td>Operational model</td>
          <td>Hybrid (4-phase 含 audit + drop-in cutover)</td>
          <td>11-12</td>
          <td>6-12 週</td>
      </tr>
      <tr>
          <td>Multi-tool 拆分</td>
          <td>一站式 → 多 component</td>
          <td>Parallel migration streams</td>
          <td>10-11</td>
          <td>2-4 個月</td>
      </tr>
      <tr>
          <td>Paradigm shift</td>
          <td>Abstraction model</td>
          <td>Partial + 混合架構</td>
          <td>10-11</td>
          <td>不收斂</td>
      </tr>
      <tr>
          <td>Topology re-layout</td>
          <td>Data topology</td>
          <td>機制 + execution flow（同 cluster 內重劃）</td>
          <td>7-9</td>
          <td>1 天-2 週</td>
      </tr>
  </tbody>
</table>
<p>6 種結構是 <em>常見 type</em>、不是窮盡分類；source / target 配對可能同時屬多 type（多軸 High）、或不屬任一 type（6 維皆 Medium）— 處理規則見「多重歸類跟 tie-breaking」段。本卡前身是「最大差異維度決定結構」+ 5 維 audit、Redis re-sharding dogfood 揭露 <em>data topology</em> 是漏掉的第 6 維、Type F 是對應的第 6 type；本卡擴張為 6 維 audit + 6 type。</p>
<hr>
<h2 id="為什麼-universal-phased-模板會失效">為什麼 universal phased 模板會失效</h2>
<p>寫第一篇 migration playbook 時自然會想：「6 phase 是 migration 的標準結構吧」 — 套到 drop-in compatible migration 後發現 80% phase 不需要、文章變成「為了 phase 而 phase」；套到 paradigm shift 後發現 phased 假設 <em>線性收斂</em>、實際是 <em>永遠混合架構</em>、phased 模板強迫一個 <em>不存在</em> 的「cleanup phase」。</p>
<p>Universal phased 失效的三個機制：</p>
<ol>
<li><strong>Schema 差不顯著時、phased 多數 phase 變空白</strong>：drop-in compatible（如 Redis → DragonflyDB）的「Schema translation phase」內容空、強寫變廢話</li>
<li><strong>Operational 差是主軸時、phased 把 operational redesign 壓進「phase 1」變太薄</strong>：PostgreSQL → Aurora 的 <em>operational model 重設計</em> 是核心、不該壓在一個 phase</li>
<li><strong>Paradigm 差時、phased 假設 source 完全消失</strong>：Kafka ↔ NATS 是 <em>永遠共存</em>、phased cleanup phase 假設不存在</li>
</ol>
<p>→ <strong>結構必須跟差異維度對位、不能反向假設</strong>。</p>
<hr>
<h2 id="diff-dimension-audit寫作前的必要-step">Diff dimension audit：寫作前的必要 step</h2>
<p>寫 process content 前先做 audit、列出 source 跟 target 在 6 個維度的差異程度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估問題</th>
          <th>High / Medium / Low</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>source 跟 target 的 API、data model、wire protocol 差異多大？</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>HA / backup / monitoring / capacity 邏輯差異多大？</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>兩端是否同類產品（同抽象層）？</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>一站式 vs multi-tool 是否需要拆分？</td>
          <td>-</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>application code 需要改多少？</td>
          <td>-</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>Sharding / partition / region / replication 拓樸是否變動？</strong></td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>主導差異維度對映常見 type：</p>
<ul>
<li><strong>Schema = High（其他 Low）</strong> → Type A phased rule translation</li>
<li><strong>Operational = High（其他 Low）</strong> → Type C operational redesign hybrid</li>
<li><strong>Paradigm = High</strong> → Type E partial + 混合架構</li>
<li><strong>Components = High（一站式 → multi-tool）</strong> → Type D parallel streams</li>
<li><strong>Topology = High（其他 Low）</strong> → Type F topology re-layout（見 <a href="../data-topology-as-audit-dimension/">#128</a>）</li>
<li><strong>全 Low</strong> → Type B drop-in、6-section + audit prefix</li>
</ul>
<p>第 6 維 <em>Data topology</em> 是後續從 Redis cluster re-sharding dogfood 浮現補位、見 <a href="../data-topology-as-audit-dimension/">#128 Data topology 是 process content 的第 6 audit 維度</a>；本卡原為 5 維 audit、被第二輪 batch evidence 揭露盲點後擴張為 6 維。</p>
<h2 id="多重歸類跟-tie-breaking">多重歸類跟 tie-breaking</h2>
<p>實際 source / target 配對 <em>很少</em> 完美對映單一 type；常見情境跟處理規則：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>例</th>
          <th>處理規則</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩維度都 High</td>
          <td>PostgreSQL → CockroachDB（Schema + Operational + Paradigm 三 High）</td>
          <td>主結構選 <em>讀者最關心</em> 的維度（多數情境 Schema &gt; Paradigm &gt; Operational &gt; Topology &gt; Components）、其他維度抽出獨立段補充</td>
      </tr>
      <tr>
          <td>三維度都 High</td>
          <td>同上</td>
          <td>結構走 Type E（paradigm 為主、partial + 混合）、用「為什麼這不是 drop-in」段交代另外兩維度</td>
      </tr>
      <tr>
          <td>全 Medium（無 High）</td>
          <td>Redis → KeyDB（API 微差 + ops 微差）</td>
          <td>走 Type B drop-in、用「相容性 audit」段列 medium 差異點</td>
      </tr>
      <tr>
          <td>一維 High 但 <em>application change</em> 連帶 High</td>
          <td>MySQL → PostgreSQL（Schema High + SQL dialect 連帶 application 改）</td>
          <td>走 Type A、application change 章節獨立段、不壓進 Phase 4 cutover</td>
      </tr>
      <tr>
          <td>Schema High + Components High</td>
          <td>Splunk → Elastic + Tines + PagerDuty</td>
          <td>主結構走 Type A（Schema 為主驅動 phased translation）、Type D 的 multi-tool 用「target stack 拆分」獨立段</td>
      </tr>
  </tbody>
</table>
<p>關鍵原則：<strong>主導維度決定主結構、其他高維度獨立加段</strong>、不強迫單一 type 標籤。Backlog 的「Type A/D 混合」「Type B/D 混合」標示是 <em>維度組合</em> 的簡記、不是承認 5 type 互斥失效；下表多重歸類處理規則才是正式判讀。</p>
<h2 id="6-type-是-axis-aligned-simplification非窮盡">6 type 是 axis-aligned simplification、非窮盡</h2>
<p>本卡 6 type 來自兩輪 migration playbook 的 dogfood 觀察（第一輪 5 篇 → Type A-E、第二輪 Redis cluster re-sharding → Type F）、是 <em>已浮現的 type</em>、不是 <em>涵蓋所有 migration 的完備分類</em>。已知漏類至少 3 種：</p>
<table>
  <thead>
      <tr>
          <th>漏類</th>
          <th>例</th>
          <th>為何現有 type 不覆蓋</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同 vendor major version upgrade</td>
          <td>PostgreSQL 14 → 17 / Kafka 3 → 4</td>
          <td>Source / target 是同 vendor、現有 type 預設跨 vendor、deep article methodology 也不完全 cover</td>
      </tr>
      <tr>
          <td>政策 / 合規驅動</td>
          <td>Atlassian server EOL / PCI 強制資料 region</td>
          <td>Driver 在外部、但資料層仍走 type A-F 之一；audit 重點是 evidence collection、不是結構</td>
      </tr>
      <tr>
          <td><del>容量重新規劃 / re-sharding</del> resolved</td>
          <td><del>單實例 → sharded / 單 region → multi-region</del></td>
          <td><del>Source / target 同 vendor、無 schema / paradigm 差、但 data topology 重劃；5 維度沒「topology」軸</del> — <strong>第二輪後已被 Type F 涵蓋</strong>、見 <a href="../data-topology-as-audit-dimension/">#128</a></td>
      </tr>
      <tr>
          <td>Acquisition / merger consolidation</td>
          <td>兩 Datadog org 合併 / 兩 K8s cluster federate</td>
          <td>Source / target 同產品、要處理 identity / RBAC / 歷史資料合併；6 type 不覆蓋</td>
      </tr>
  </tbody>
</table>
<p>未來累積更多 migration playbook 後、可能浮現第 7-9 type（identity / consistency / residency 候選 — 對應 <a href="../data-topology-as-audit-dimension/">#128</a> 跟 self-aware limitation update 揭露的候選軸）、或對 6 type 重構。本卡的 type 集合是 <em>open</em>、不是 <em>closed</em>。</p>
<hr>
<h2 id="6-種結構的-anatomy">6 種結構的 anatomy</h2>
<p>**「結構 differentiator」**是本系列引入的概念：每篇 process content 在開頭加一段、<em>明示這篇用什麼結構、跟其他同 category content 的結構差異在哪</em>。功能類似 type signature — 讓讀者一開始就知道接下來的章節組織方式、避免套錯預期。例：drop-in migration 的「結構 differentiator」段會說「跟 phased migration 對照、本篇是 6-section + audit、不是 6-phase」。</p>
<h3 id="type-aphased-translationschema-差為主">Type A：Phased translation（schema 差為主）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Phase 0 audit → Phase 1 schema 對位 → Phase 2 translation
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ Phase 3 parallel run → Phase 4 cutover → Phase 5 cleanup</span></span></code></pre></div><p>特徵：</p>
<ul>
<li><em>線性</em> 流程、phase 之間有 dependency</li>
<li>每 phase 有獨立 <em>回退邊界</em></li>
<li>Schema translation 是工作量主軸（4-12 週）</li>
</ul>
<p>適用：Splunk → Elastic / Datadog APM → New Relic / MySQL → Postgres</p>
<h3 id="type-b6-section--audit-prefixdrop-in-compatible">Type B：6-section + audit prefix（drop-in compatible）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">為什麼遷 → 結構 differentiator → 相容性 audit
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ Step-by-step cutover → 故障演練 → Capacity → 整合</span></span></code></pre></div><p>特徵：</p>
<ul>
<li>接近 deep article 6-section</li>
<li>多一段 <em>相容性 audit</em>（在 cutover 前列出風險點）</li>
<li>不需要 phased、單次 cutover</li>
</ul>
<p>適用：Redis → DragonflyDB / OpenJDK → Adoptium / MariaDB → MySQL（部分版本）</p>
<h3 id="type-coperational-redesign-hybrid">Type C：Operational redesign hybrid</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">為什麼遷 → 結構 differentiator → Operational redesign 對位
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ 4-phase operational migration（Phase 0 audit + 3 active phase）→ Drop-in cutover → 故障演練 → Capacity → 整合</span></span></code></pre></div><p>特徵：</p>
<ul>
<li>application code 不變、operational model 全換</li>
<li><em>operational 表格對位</em> 是內容主軸</li>
<li>Cutover 本身簡單（protocol 相容）、operational 準備複雜</li>
</ul>
<p>適用：PostgreSQL → Aurora / Self-managed Redis → ElastiCache / Self-managed Kafka → MSK</p>
<h3 id="type-dparallel-streamsmulti-tool-拆分">Type D：Parallel streams（multi-tool 拆分）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">為什麼遷 → 五個責任、五個 component → 5 parallel migration stream
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ Stream-level audit / deploy / dual-ship / cutover → 故障演練 → Capacity → 整合</span></span></code></pre></div><p>特徵：</p>
<ul>
<li>source 一站式、target N 個專責 component</li>
<li>每個 stream 獨立 audit / deploy / cutover、stream 間少 dependency</li>
<li>整體不是線性、是 <em>staggered parallel</em></li>
</ul>
<p>適用：Datadog → Grafana Stack / Splunk → Elastic + Tines + PagerDuty / Atlassian Suite → 各 specialized tool</p>
<h3 id="type-epartial--混合架構paradigm-shift">Type E：Partial + 混合架構（paradigm shift）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">「不是 migration、是 paradigm 重設計」→ Paradigm 對位
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ 什麼情境真的能換 → Application 重設計 → 部分 stream cutover → 長期混合架構</span></span></code></pre></div><p>特徵：</p>
<ul>
<li>不存在「complete migration」、是 <em>按 use case 拆分 + 共存</em></li>
<li>application 模式重設計（不是 SDK 換）</li>
<li><em>混合架構是 long-term default</em></li>
</ul>
<p>適用：Kafka ↔ NATS / REST → gRPC / SQL → NoSQL / VM → Serverless</p>
<h3 id="type-ftopology-re-layoutdata-topology--high">Type F：Topology re-layout（data topology = High）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">為什麼 re-layout → 結構 differentiator（re-layout 不是 migration）
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ Pre-layout analysis（topology audit）→ Re-layout 機制
</span></span><span class="line"><span class="ln">3</span><span class="cl">→ Execution flow（per-step + rollback boundary）
</span></span><span class="line"><span class="ln">4</span><span class="cl">→ 故障演練 → Capacity / cost → 整合</span></span></code></pre></div><p>特徵：</p>
<ul>
<li>Source / target 多數是 <em>同 cluster 不同 state</em>、不是跨 vendor</li>
<li>主軸是 <em>topology audit + 重劃機制</em>、不是 schema translation / paradigm shift</li>
<li>Pre-layout analysis（識別 hot key / 當前 distribution）是 Type F 的核心 audit 段</li>
<li>Execution flow per-step、含 <em>rollback boundary</em></li>
</ul>
<p>適用：Redis cluster re-sharding / PostgreSQL partition redesign / Kafka topic re-partitioning / Cassandra keyspace re-balance / 加 region / multi-master rollout</p>
<p>詳細 audit dimension 跟 sub-dimension 見 <a href="../data-topology-as-audit-dimension/">#128 Data topology 是 process content 的第 6 audit 維度</a>。</p>
<hr>
<h2 id="跟-deep-article-methodology-的關係">跟 deep article methodology 的關係</h2>
<p><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 系列）的實證。">Deep article methodology</a> 的 6-section structure（問題情境 → 概念 → 配置 → 演練 → 容量 → 整合）是 <em>single feature implementation</em> 的模板、不是 <em>cross-vendor process</em> 的模板。Migration playbook 是 <em>新 content category</em>、需要自己的 methodology。</p>
<p>兩者關係：</p>
<ul>
<li><strong>Single feature deep article</strong>：6-section、200-400 行、focused on <em>how to implement / debug feature X</em></li>
<li><strong>Migration playbook</strong>：6 種 structure（依 diff dimension）、200-400 行 / 篇、focused on <em>how to move from A to B</em></li>
<li>共同：問題情境 / 故障演練 / 容量 / 整合段；差異：中間「process / structure」段</li>
</ul>
<p>寫前的 <em>content category 判讀</em> 是新方法論議題、不是 deep article methodology 涵蓋。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫 migration playbook 前不做 diff dimension audit</td>
          <td>套錯結構模板、phase 變空白或 process 強行線性</td>
      </tr>
      <tr>
          <td>假設「migration 都 phased」</td>
          <td>drop-in / paradigm shift 套 phased 結構失真</td>
      </tr>
      <tr>
          <td>假設「跟 deep article methodology 一樣」</td>
          <td>6-section 套 cross-vendor process 缺 differentiation</td>
      </tr>
      <tr>
          <td>跨 type 強行套同一個結構</td>
          <td>5 種 type 內容差異被壓平、跨篇連讀預期化</td>
      </tr>
      <tr>
          <td>沒列「結構 differentiator」段</td>
          <td>讀者不知道為什麼這篇結構跟其他 migration playbook 不同</td>
      </tr>
      <tr>
          <td>Diff dimension audit 只看 schema</td>
          <td>忽略 operational / paradigm / components 維度、套錯結構</td>
      </tr>
      <tr>
          <td>把混合架構 paradigm shift 寫成 phased</td>
          <td>假設 source 會消失、cleanup phase 變 fiction</td>
      </tr>
      <tr>
          <td>把 drop-in 寫成 phased</td>
          <td>多 phase 變空白、文章拉長但無內容</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a></td>
          <td>補位 — #122 處理 <em>同 type 內的 framing collapse</em>、本卡處理 <em>跨 type 套錯結構</em>；兩者都跟「主題語意 attractor」相關</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 — 套既有結構模板最便利（不用判 diff dimension）、但意圖（跟主題本質對位）失準</td>
      </tr>
      <tr>
          <td><a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a></td>
          <td>子實例 — 結構模板 collapse 到單一 type 是 #125 在「content structure」surface 的具體形態</td>
      </tr>
      <tr>
          <td><a href="../standard-driven-vs-case-driven-domain-judgment/">#118 Standard-driven vs case-driven domain judgment</a></td>
          <td>Sibling — 兩卡都是 <em>寫作前的 domain audit</em>、#118 判 case-driven vs standard-driven、本卡判 process structure type</td>
      </tr>
      <tr>
          <td><a href="../routing-layer-chapter-recognition/">#119 章節已有 routing skeleton 走補強段</a></td>
          <td>同骨 — 都是「結構辨識先於內容生成」、#119 是章節內、本卡是文章層</td>
      </tr>
      <tr>
          <td><a href="../data-topology-as-audit-dimension/">#128 Data topology 是 process content 的第 6 audit 維度</a></td>
          <td>子卡 — 本卡 audit 框架從 5 維擴張到 6 維、新增 Type F；#128 是 6 維 audit 的 atomic 定義跟 Type F 詳細 anatomy</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫 migration playbook 前直覺套「6 phase」</td>
          <td>先跑 diff dimension audit、可能 type A-E 對應不同結構</td>
      </tr>
      <tr>
          <td>寫到一半某 phase 內容空白</td>
          <td>結構錯位、可能不需要這個 phase</td>
      </tr>
      <tr>
          <td>兩篇同 category content 連讀差異不大</td>
          <td>結構過於 universal、缺結構 differentiator 段</td>
      </tr>
      <tr>
          <td>「cleanup phase」寫不出內容</td>
          <td>可能是 paradigm shift type、source 不會消失</td>
      </tr>
      <tr>
          <td>章節數 ≥ 15 還沒寫完</td>
          <td>結構過 phased、考慮是不是 type B / E 不需要這麼多</td>
      </tr>
      <tr>
          <td>章節 4 「故障演練」段比其他段都簡單</td>
          <td>結構過 abstract、實作層細節缺</td>
      </tr>
      <tr>
          <td>寫作前沒列 source / target 的 diff dimension</td>
          <td>結構 risk、補 audit</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Process content 的結構由 <em>source / target 差異維度組合</em> 決定、不是 universal phased / 6-section 模板。寫作前必須跑 <em>6 維 diff dimension audit</em>（schema / operational / paradigm / components / application change / data topology）、選對應主結構、其他高維度獨立加段；跳過 audit 會套錯模板、phase 變空白或 process 強行線性。</p>
<hr>
<h2 id="self-aware-limitation本卡的-sample-driven-over-fit-風險">Self-aware limitation：本卡的 sample-driven over-fit 風險</h2>
<p>本卡 5 type 來自 5 篇 migration playbook 的 dogfood 觀察、本身就是 <em>N=5 sample 推導出 5 type taxonomy</em> — 跟本卡批判的「universal phased 模板」「<a href="../cadence-homogenization-in-batch-writing/">#122</a> cadence collapse」「<a href="../collapse-is-implicit-default/">#125</a> reduce 多維到單格」是 <em>同骨錯誤</em>。</p>
<table>
  <thead>
      <tr>
          <th>Reviewer 揭露的本卡 over-fit</th>
          <th>對應的本卡建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5 type 非窮盡（漏 4 種主流情境）</td>
          <td>「5 type 是 axis-aligned simplification、非窮盡」段、未來累積更多 sample 後可能重構</td>
      </tr>
      <tr>
          <td>5 type 互斥失效（多軸 High 配對）</td>
          <td>「多重歸類跟 tie-breaking」段、不強迫單一 type 標籤</td>
      </tr>
      <tr>
          <td>「最大維度」沒處理 tie</td>
          <td>主導維度判讀規則（Schema &gt; Paradigm &gt; Operational &gt; Topology &gt; Components；audience-dependent heuristic）</td>
      </tr>
      <tr>
          <td>「Partial collapse 教育價值高」是 post-hoc</td>
          <td>修正為 <a href="../cadence-homogenization-in-batch-writing/">#122 Update 段第 8 點</a> — partial collapse 是 attractor 訊號、不增強 principle</td>
      </tr>
  </tbody>
</table>
<p>本卡是 <em>current best understanding</em>、不是 <em>已驗證的完備理論</em>。Tripwire：</p>
<ul>
<li>若下一輪 migration batch 浮現 <em>無法歸進現有 5 type 的新 structure</em>、應該擴充 type 集合而不是強行歸類</li>
<li>若同一 source/target 配對出現 <em>結構翻轉</em>（例 PostgreSQL → CockroachDB 在不同 application context 走不同主結構）、應該檢視 <em>主導維度</em> 規則是否需要動態化</li>
<li>若 type 數量擴張到 8+、應該評估是否該重構為 <em>維度 × 維度 grid</em> 而不是 type list</li>
</ul>
<hr>
<p>承認 limitation 本身是 dogfood — <a href="../cadence-homogenization-in-batch-writing/">#122 cadence 同質化</a> 講「natural attractor 不規劃就 collapse」、本卡的 5 type 就是 <em>5 個 sample 的 natural attractor</em>；不在卡內承認、就重複了 <a href="../collapse-is-implicit-default/">#125 隱形預設</a> 的 collapse pattern。本段是 self-correction、不是 disclaimer。</p>
<h3 id="update2026-05-19第二輪-migration-batch-驗證-limitation">Update（2026-05-19）：第二輪 migration batch 驗證 limitation</h3>
<p>第二輪 migration batch（5 篇）跑完、self-aware limitation 三項預測得到驗證：</p>
<table>
  <thead>
      <tr>
          <th>預測（self-aware limitation 段）</th>
          <th>第二輪實證</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>漏類確實存在、未來累積更多 sample 後可能重構</td>
          <td>major version upgrade（<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</a>）跟 re-sharding（<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-resharding</a>）結構跟 5 type 完全不同、各有自己的 anatomy；漏類確認</td>
      </tr>
      <tr>
          <td>Multi-axis 處理規則（主導維度 + 高維度獨立段）</td>
          <td><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/migrate-to-cockroachdb</a> 三維皆 High、結構 = Type E 主結構 + Type A schema gap 段 + Type C operational redesign 段、不強迫單一 type 標籤；規則成立</td>
      </tr>
      <tr>
          <td>Type A / Type C 標準形態仍適用</td>
          <td><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/migrate-to-postgresql</a>（Type A）+ <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/migrate-to-atlas</a>（Type C）走標準模板、跟第一輪同 type 對應；標準形態驗證</td>
      </tr>
  </tbody>
</table>
<p>新發現（不在 self-aware limitation 預測內、需要後續處理）：</p>
<ul>
<li><strong>新 audit 維度浮現</strong>：re-sharding 揭露「data topology」是 5 維沒有的軸；audit 擴張為 6 維（加 topology 軸）已執行、見 <a href="../data-topology-as-audit-dimension/">#128 Data topology 是 process content 的第 6 audit 維度</a> + 本卡 audit table 新加 row 跟 Type F anatomy</li>
<li><strong>「為什麼這篇不套」是漏類文章的好結構模板</strong>：major-version-upgrade 跟 cluster-resharding 都用這個 frame 開頭、明示跟 5 type 的邊界</li>
<li><strong>「高維度獨立段」對照表</strong>自然在 multi-axis 文章浮現（cockroachdb 篇）— 應該升級為 multi-axis migration 的標準結構元素</li>
</ul>
<h3 id="update2026-05-19-第三輪-4-reviewer-audit-後6-維擴張的未解結構性質疑">Update（2026-05-19 第三輪 4-reviewer audit 後）：6 維擴張的未解結構性質疑</h3>
<p>第三輪 audit 揭露本卡擴 6 維 + Type F 仍有 6 項未解結構性 issue。完整列表跟 acknowledgment 見 <a href="../data-topology-as-audit-dimension/">#128 Self-aware limitation 段</a>、本卡這裡 cross-reference：</p>
<ol>
<li><strong>6 維非窮盡</strong>：identity / consistency / residency 三軸候選</li>
<li><strong>Type F 跟 Type B 結構重疊度高</strong>：實質差異只 2 段、可能下次 evolution 降為 Type B variant</li>
<li><strong>「不需要 parallel run」claim 部分不成立</strong>（multi-region rollout 例外）</li>
<li><strong>主導維度優先序是 audience-dependent heuristic</strong>、非 universal</li>
<li><strong>「topology 不能塞進既有 5 維」拒絕理由依賴 narrow 既有 5 維定義</strong></li>
<li><strong>既有 5 篇 playbook 沒 retroactive audit</strong>（silent grandfathering）</li>
</ol>
<p>本卡的 6 type / 6 維框架是 <em>current best understanding</em>、不是 <em>final taxonomy</em>；累積到 10+ migration playbook 後可能觸發 retroactive audit + framework restructure。</p>
]]></content:encoded></item><item><title>Data topology 是 process content 的第 6 audit 維度</title><link>https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Process content 的 &lt;a href="../content-structure-by-max-diff-dimension/">diff dimension audit&lt;/a> 原本 5 維 — schema / operational / paradigm / components / application change — 漏了 &lt;em>data topology&lt;/em> 這軸。Topology 是 &lt;em>資料在 cluster / partition / region 之間的分佈拓樸&lt;/em>、跟既有 5 維任一個都不對等：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>處理對象&lt;/th>
 &lt;th>對 topology 的關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>資料結構（column / type / index）&lt;/td>
 &lt;td>不同層、schema 不變 topology 可能變&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>運維 stack（HA / backup / monitoring）&lt;/td>
 &lt;td>topology 可能影響 ops、但不是同一概念&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>核心抽象（OLTP / log / pub-sub）&lt;/td>
 &lt;td>同 paradigm 內 topology 可變&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>元件數量（1 vs N）&lt;/td>
 &lt;td>同 component 數可有不同 topology&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>application code 改動量&lt;/td>
 &lt;td>topology 變不必然 application 改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Data topology&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>slot / shard / partition / region 分佈&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>本卡新增的第 6 維&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Data topology 是 &lt;em>資料分佈&lt;/em> 層級的概念&lt;/strong> — 跟資料結構（schema）、運維機制（operational）、抽象模型（paradigm）、組件數量（components）、application code 改動量（application change）並列為第 6 軸；topology 變動時其他 5 維可能完全不變、但 &lt;em>資料在 cluster / partition / region 之間的擺放方式&lt;/em> 改變、需要獨立的結構處理。&lt;/p>
&lt;p>擴 audit 到 6 維、新增 &lt;a href="../content-structure-by-max-diff-dimension/">Type F「Topology re-layout」&lt;/a> 結構對映 &lt;em>topology 高差異&lt;/em> 的 process content。&lt;/p>
&lt;h2 id="topology-的-5-個-sub-dimension">Topology 的 5 個 sub-dimension&lt;/h2>
&lt;p>不同 source/target 配對對 topology 的影響不同、用 5 sub-dimension 描述具體變化：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Sub-dimension&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;th>例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Sharding strategy&lt;/td>
 &lt;td>Slot / hash / range / consistent hash / key-based&lt;/td>
 &lt;td>Redis cluster slot 重分配&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Partition strategy&lt;/td>
 &lt;td>Declarative / range / list / hash / sub-partition&lt;/td>
 &lt;td>PostgreSQL monthly → daily partition&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication topology&lt;/td>
 &lt;td>Single primary / multi-master / star / hub-spoke / mesh&lt;/td>
 &lt;td>Single primary → multi-master 切換、或加 logical replication subscriber&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Region distribution&lt;/td>
 &lt;td>Single / multi-AZ / multi-region / global&lt;/td>
 &lt;td>Cassandra single DC → multi-DC&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Co-location / locality&lt;/td>
 &lt;td>Locality-aware queries / row-level region pinning&lt;/td>
 &lt;td>CockroachDB region 強制 row 對應&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>任一 sub-dimension 變動就構成 topology layout 變動；多個 sub-dimension 同時變更（如「sharding strategy + region distribution 同時改」）是 &lt;em>complex topology migration&lt;/em>、結構複雜度高。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Process content 的 <a href="../content-structure-by-max-diff-dimension/">diff dimension audit</a> 原本 5 維 — schema / operational / paradigm / components / application change — 漏了 <em>data topology</em> 這軸。Topology 是 <em>資料在 cluster / partition / region 之間的分佈拓樸</em>、跟既有 5 維任一個都不對等：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>處理對象</th>
          <th>對 topology 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>資料結構（column / type / index）</td>
          <td>不同層、schema 不變 topology 可能變</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>運維 stack（HA / backup / monitoring）</td>
          <td>topology 可能影響 ops、但不是同一概念</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>核心抽象（OLTP / log / pub-sub）</td>
          <td>同 paradigm 內 topology 可變</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>元件數量（1 vs N）</td>
          <td>同 component 數可有不同 topology</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>application code 改動量</td>
          <td>topology 變不必然 application 改</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>slot / shard / partition / region 分佈</strong></td>
          <td><strong>本卡新增的第 6 維</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>Data topology 是 <em>資料分佈</em> 層級的概念</strong> — 跟資料結構（schema）、運維機制（operational）、抽象模型（paradigm）、組件數量（components）、application code 改動量（application change）並列為第 6 軸；topology 變動時其他 5 維可能完全不變、但 <em>資料在 cluster / partition / region 之間的擺放方式</em> 改變、需要獨立的結構處理。</p>
<p>擴 audit 到 6 維、新增 <a href="../content-structure-by-max-diff-dimension/">Type F「Topology re-layout」</a> 結構對映 <em>topology 高差異</em> 的 process content。</p>
<h2 id="topology-的-5-個-sub-dimension">Topology 的 5 個 sub-dimension</h2>
<p>不同 source/target 配對對 topology 的影響不同、用 5 sub-dimension 描述具體變化：</p>
<table>
  <thead>
      <tr>
          <th>Sub-dimension</th>
          <th>內容</th>
          <th>例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sharding strategy</td>
          <td>Slot / hash / range / consistent hash / key-based</td>
          <td>Redis cluster slot 重分配</td>
      </tr>
      <tr>
          <td>Partition strategy</td>
          <td>Declarative / range / list / hash / sub-partition</td>
          <td>PostgreSQL monthly → daily partition</td>
      </tr>
      <tr>
          <td>Replication topology</td>
          <td>Single primary / multi-master / star / hub-spoke / mesh</td>
          <td>Single primary → multi-master 切換、或加 logical replication subscriber</td>
      </tr>
      <tr>
          <td>Region distribution</td>
          <td>Single / multi-AZ / multi-region / global</td>
          <td>Cassandra single DC → multi-DC</td>
      </tr>
      <tr>
          <td>Co-location / locality</td>
          <td>Locality-aware queries / row-level region pinning</td>
          <td>CockroachDB region 強制 row 對應</td>
      </tr>
  </tbody>
</table>
<p>任一 sub-dimension 變動就構成 topology layout 變動；多個 sub-dimension 同時變更（如「sharding strategy + region distribution 同時改」）是 <em>complex topology migration</em>、結構複雜度高。</p>
<h2 id="為什麼-topology-不能塞進既有-5-維">為什麼 topology 不能塞進既有 5 維</h2>
<p>Reviewer 質疑：為什麼不直接歸進 operational 或 paradigm？三個拒絕理由：</p>
<ol>
<li><strong>Schema 不變但 topology 變</strong>：PostgreSQL <code>partition strategy</code> 改（monthly → daily）— schema 完全相同、partition boundary 重劃；歸 Schema 維度錯位</li>
<li><strong>Operational stack 不變但 topology 變</strong>：Redis cluster 加 node 重分 slot — Sentinel / monitoring / backup 不變、純粹是 slot mapping 重劃；歸 Operational 維度太寬</li>
<li><strong>Paradigm 不變但 topology 變</strong>：Cassandra 從 single DC 加到 multi-DC — 同 distributed DB paradigm、co-location / replication topology 變；歸 Paradigm 維度誤導</li>
<li><strong>Components 不變但 topology 變</strong>：Kafka topic re-partition（10 partitions → 100）— 同 1 個 cluster、partition count 變；歸 Components 維度錯位</li>
</ol>
<p>Topology 是 <em>獨立的問題軸</em>、5 維 audit 漏掉時會誤判結構。</p>
<h2 id="觸發-type-f-的情境">觸發 Type F 的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>Topology 變化</th>
          <th>是否同 vendor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster re-sharding</td>
          <td>Slot / shard 重分配</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Partition redesign</td>
          <td>Partition boundary / strategy 重劃</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Single-region → multi-region</td>
          <td>Region distribution + replication topology 雙變</td>
          <td>多數 yes（同 vendor 加 region）</td>
      </tr>
      <tr>
          <td>Multi-master rollout</td>
          <td>Replication topology 從 single primary 變 multi-master</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>DynamoDB GSI / global tables</td>
          <td>Sharding + replication 雙變</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Kafka topic re-partitioning</td>
          <td>Sharding strategy 變</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>Cassandra keyspace re-balance</td>
          <td>Replication factor（sub-dim 3）+ token range（sub-dim 1）雙變</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>MongoDB sharded cluster 加 shard</td>
          <td>Sharding 重分布</td>
          <td>yes</td>
      </tr>
  </tbody>
</table>
<p>多數 Type F 場景是 <em>同 vendor</em> — 跟 <a href="../content-structure-by-max-diff-dimension/">#127</a> Type A-E 預設「跨 vendor」對應、Type F 是 <em>同 vendor 內 topology 重劃</em>。</p>
<h2 id="6-維-audit-decision-ruleupdated">6 維 audit decision rule（updated）</h2>
<p>擴 audit 到 6 維後、type 對映規則更新：</p>
<table>
  <thead>
      <tr>
          <th>維度組合</th>
          <th>對映 type</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema = High（其他 Low）</td>
          <td>Type A phased rule translation</td>
      </tr>
      <tr>
          <td>全 Low</td>
          <td>Type B drop-in</td>
      </tr>
      <tr>
          <td>Operational = High（其他 Low）</td>
          <td>Type C operational redesign hybrid</td>
      </tr>
      <tr>
          <td>Components = High</td>
          <td>Type D parallel streams</td>
      </tr>
      <tr>
          <td>Paradigm = High</td>
          <td>Type E partial + 混合架構</td>
      </tr>
      <tr>
          <td><strong>Topology = High（其他 Low）</strong></td>
          <td><strong>Type F topology re-layout</strong>（本卡新增）</td>
      </tr>
      <tr>
          <td>多軸 High</td>
          <td>按 <a href="../content-structure-by-max-diff-dimension/">#127 多重歸類</a> 規則</td>
      </tr>
  </tbody>
</table>
<p>主導維度判讀的優先序也擴張：Schema &gt; Paradigm &gt; Operational &gt; Topology &gt; Components。Topology 在 schema / paradigm / operational 之後、components 之前 — 因為 topology 對讀者 conceptual impact 通常比 components 拆分大、但比 schema / paradigm 小。</p>
<h2 id="type-ftopology-re-layout結構-anatomy">Type F「Topology re-layout」結構 anatomy</h2>
<p>從 <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> 抽出的標準形態：</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">1. 為什麼 re-layout（4-N 種 driver）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 結構 differentiator（re-layout 不是 migration）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Pre-layout analysis（current topology audit / hot key / slot 分佈）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Re-layout 機制（slot migration / partition split / shard rebalance）
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Execution flow（per-step、含 rollback boundary）
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Production 故障演練
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. Capacity / cost
</span></span><span class="line"><span class="ln">8</span><span class="cl">8. 整合 / 下一步</span></span></code></pre></div><p>7-9 章節、200-260 行。三個 <em>新元素</em> 是 Type F 的核心承擔：</p>
<ul>
<li><strong>Pre-layout analysis 段</strong>：在執行前列出當前 topology（slot 分佈 / hot key / replica lag / partition imbalance）、決定 <em>re-layout 的範圍跟順序</em>；缺這段、後續執行階段沒 baseline 可比、failure 偵測延遲</li>
<li><strong>Re-layout 機制段</strong>：解釋 vendor 的 <em>slot migration / partition split / shard rebalance</em> protocol —讀者要理解 vendor 內部機制才能預估 latency / locking / atomicity 邊界</li>
<li><strong>Execution flow per-step + rollback boundary</strong>：跟 Type A 的 phased 對照、Type F per-step 粒度更細（單 slot migration vs 整個 phase）、每 step 都要明示 <em>能否回退、回退時資料狀態</em></li>
</ul>
<p>跟 Type B 對照、Type F 多了「topology audit」段、Step-by-step 比 Type B 細（per-step 不是 per-cutover）；跟 Type A phased 對照、Type F 多數情境不需要 schema translation / parallel run / cleanup phase（source / target 同 cluster）；但 <em>multi-region rollout</em> 子情境例外、仍需 parallel run（兩 region 同跑後切流量）— 此時 Type F + Type A parallel run 段組合應用、見「多重歸類」規則。</p>
<p>注意 anatomy 列 8 row 是 <em>規範形態</em>、不是強制機械對映 — 實作上「結構 differentiator」+「pre-layout analysis」段可 inline 到開頭 audit 段（如 <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> 的「Source = Target，但 topology 重劃」段內聯處理）、實作 H2 數可能比 anatomy 列 row 少 1-2 個。</p>
<h2 id="production-反模式">Production 反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把 re-sharding 套 Type B drop-in</td>
          <td>漏掉 slot migration 機制段、cluster busy 跟 stale client cache 沒被處理</td>
      </tr>
      <tr>
          <td>把 multi-region rollout 套 Type C</td>
          <td>漏掉 locality-aware queries 跟 replication topology 設計</td>
      </tr>
      <tr>
          <td>Topology 變化只列在「容量」段</td>
          <td>讀者把 topology 當 capacity 子議題、忽略 <em>結構</em> 影響</td>
      </tr>
      <tr>
          <td>多 sub-dimension 同時變、只寫一個</td>
          <td>例：Cassandra 加 DC 同時改 replication factor、只寫前者</td>
      </tr>
      <tr>
          <td>Type F 套錯場景（topology 沒變的 migration）</td>
          <td>強迫 phased per-step、phase 空白</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../content-structure-by-max-diff-dimension/">#127 Process content 結構由最大差異維度決定</a></td>
          <td>父卡 — 本卡擴 #127 的 audit 框架從 5 維到 6 維、新增 Type F；#127 的 5 type 仍適用、本卡加第 6 type</td>
      </tr>
      <tr>
          <td><a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a></td>
          <td>同骨 — 5 維 audit 漏 topology 是「結構分類 collapse 掉 topology 軸」、是 #125 在 audit dimension surface 的子實例</td>
      </tr>
      <tr>
          <td><a href="../standard-driven-vs-case-driven-domain-judgment/">#118 Standard-driven vs case-driven domain judgment</a></td>
          <td>Sibling — 兩卡都是 <em>寫作前的 domain audit</em>、#118 判 case-driven vs standard-driven、本卡判 topology 是否需要 Type F</td>
      </tr>
      <tr>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a></td>
          <td>同骨 — 模板有「內容欄位 / cadence」兩維度（#122）vs audit 有「6 維 / topology」兩 layer；都是「初始框架漏軸、用實證浮現補位」</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到一半發現 5 維 audit 都 Low、但內容跟 Type B drop-in 不一樣</td>
          <td>Topology 可能是漏掉的維度、補 6 維 audit</td>
      </tr>
      <tr>
          <td>「容量規劃」段比實作段還複雜</td>
          <td>Topology 變動被誤歸 capacity、應該獨立段</td>
      </tr>
      <tr>
          <td>Sharding / partition / region 任一變動</td>
          <td>跑 topology audit、評估是否 Type F</td>
      </tr>
      <tr>
          <td>同 vendor 內升級 / re-layout</td>
          <td>大概率不是 5 type、檢查 topology 是否變</td>
      </tr>
      <tr>
          <td>Type B 結構寫不下實際內容</td>
          <td>可能是 Type F 而非 Type B</td>
      </tr>
      <tr>
          <td>多個 sub-dimension 同時變</td>
          <td>Complex topology migration、結構複雜度 +1 階</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：5 維 audit 漏 topology 是初始框架的盲點；topology 是 <em>資料分佈</em> 而非 <em>資料結構 / 元件 / 抽象</em>、需要獨立 audit 軸。Type F「Topology re-layout」對映 topology = High 的 process content、跟 Type A-E 並列；多軸 High 配對按 <a href="../content-structure-by-max-diff-dimension/">#127</a> 多重歸類規則處理。</p>
<hr>
<h2 id="self-aware-limitation本卡的-6-個未解結構性質疑">Self-aware limitation：本卡的 6 個未解結構性質疑</h2>
<p>第二輪 4-reviewer audit 揭露 6 項結構性 issue、本卡選擇 <em>meta-acknowledgment</em>（記錄）而非 <em>substantive restructure</em>（重寫）— 跟 <a href="../content-structure-by-max-diff-dimension/">#127 self-aware limitation</a> spirit 一致：</p>
<ol>
<li><strong>6 維仍可能漏類</strong>：reviewer 提 identity / authorization / consistency / transactional / data residency 三軸候選；本卡確認 <em>6 維是 current best understanding、不是窮盡</em>；下一輪 batch 跑前優先驗證這些候選軸是否真的獨立</li>
<li><strong>Type F 跟 Type B 結構重疊度高</strong>：anatomy 8 row 中 6 row 跟 Type B 對齊、實質差異在「pre-layout analysis + re-layout 機制」兩段；可能下次 evolution 是 <em>Type B 的 variant</em> 而非並列 type；保留現狀因為「同 cluster」邊界對讀者區分有用</li>
<li><strong>「不需要 parallel run」claim 部分不成立</strong>：multi-region rollout 子情境仍需 parallel run（兩 region 同跑然後切流量）— anatomy 已加註此例外、跟「多重歸類」規則組合應用</li>
<li><strong>主導維度優先序是 audience-dependent heuristic</strong>：DBA 視角 Topology 可能 &gt; Operational、application developer 視角 Schema &gt; Paradigm；當前 <code>Schema &gt; Paradigm &gt; Operational &gt; Topology &gt; Components</code> 預設是「跨 audience 平均」、非 universal；reviewer 識別此 stipulation 性質</li>
<li><strong>「topology 不能塞進既有 5 維」拒絕理由的窄定義依賴</strong>：4 個拒絕點都靠 narrow 既有 5 維定義成立；換個合理定義（如「component = 任何 cluster-internal primitive、包含 partition」）topology 跟 components 邊界會 collapse；保留現狀因為當前定義對寫作判讀有用</li>
<li><strong>既有 5 篇 playbook 沒 retroactive audit</strong>：6 維框架 retroactively 對既有 Type A-E 文章未重審；Splunk → Elastic / Datadog → Grafana / Postgres → Aurora 按 6 維可能變 multi-axis；這是已知 <em>silent grandfathering</em>、不是清白「擴張」</li>
</ol>
<p>下一輪 batch trigger：</p>
<ul>
<li>寫 1-2 篇 Type F dogfood 驗證 anatomy 通用性（Cassandra re-balance / PG partition redesign 是候選）</li>
<li>若浮現 <em>Type F 跟 Type B 結構真同構</em>、考慮降級為 variant</li>
<li>若浮現 <em>identity / consistency / residency 真的獨立軸</em>、再擴 audit 到 7 維</li>
<li>既有 5 篇 retroactive audit 在累積到 10+ migration playbook 後做、單獨成 retrospective report</li>
</ul>
<h3 id="update2026-05-19-第三輪-migration-batch-後4-條-tripwire-全驗證">Update（2026-05-19 第三輪 migration batch 後）：4 條 tripwire 全驗證</h3>
<p>第三輪 migration batch（5 篇）執行了上述 4 條 trigger、各自結果：</p>
<table>
  <thead>
      <tr>
          <th>Tripwire 預測</th>
          <th>第三輪結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type F dogfood × 2 驗證 anatomy 通用性</td>
          <td><strong>完成</strong>：<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 整合">PG partition redesign</a> + <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</a>；anatomy 在 PG / MongoDB 上仍適用、跟 Redis re-sharding 對齊</td>
      </tr>
      <tr>
          <td>Type F vs Type B 結構同構驗證</td>
          <td><strong>部分浮現</strong>：PG partition / Redis re-sharding 不需 parallel run、MongoDB multi-DC 需要；建議 Type F 拆 <em>F-cluster</em>（單 cluster 內、不需 parallel run）+ <em>F-multi-region</em>（跨 region、需 parallel run）兩 sub-type、未來累積更多 case 後 commit</td>
      </tr>
      <tr>
          <td>Identity / consistency / residency 三軸候選驗證</td>
          <td><strong>三軸各 1 case 驗證、工作量分佈支持獨立軸</strong>：<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>（identity、45% 工作量）/ <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</a>（consistency、85% 工作量）/ <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">PG GDPR multi-region</a>（residency、40% 工作量）；累積到 3-5 case / 軸後 commit 升 7-9 維 audit</td>
      </tr>
      <tr>
          <td>既有 5 篇 retroactive audit</td>
          <td>暫不執行、累積到 10+ migration playbook 後再做（當前共 10 篇 migration、剛達 trigger threshold、留下輪 retrospective 處理）</td>
      </tr>
  </tbody>
</table>
<p>3 軸候選驗證 detail：</p>
<ul>
<li><strong>Identity axis</strong>：Vault → AWS Secrets Manager 45% 工作量在 identity model 對位（Vault token vs IAM principal）、不歸 schema / operational / application change；驗證 identity 可獨立發生 + 帶獨立工作量</li>
<li><strong>Consistency axis</strong>：DynamoDB strong → eventual 85% 工作量在 per-call-site contract review、不歸 paradigm / application change；驗證 consistency 可獨立發生 + 帶獨立工作量</li>
<li><strong>Residency axis</strong>：GDPR multi-region 40% 工作量在 compliance（DPIA / evidence collection / DPO sign-off）、reverse-constrain topology + operational + application；驗證 residency 不只是 driver、是 cross-cutting constraint</li>
</ul>
<p>新浮現議題（不在原 tripwire 內）：</p>
<ul>
<li><strong>Residency 是 cross-cutting constraint vs 獨立軸</strong>：reviewer 把 residency 歸為 driver、實證上是 <em>cross-cutting constraint</em> — 反向約束其他維度 + 帶獨立合規工作量；可能需要 <em>constraint layer</em> 概念跟 axis 並列</li>
<li><strong>Type F sub-type 浮現</strong>：multi-region rollout 跟 cluster re-sharding 是不同 sub-type；前者需 parallel run、後者不需；anatomy 在 sub-type 之間有差異</li>
</ul>
]]></content:encoded></item></channel></rss>